如何使用 TLS 连接与服务器通信?

问题描述:

我正在使用 python 和 SSL 模块建立到服务器的 TLS 连接.然后我想向这个服务器发送一些数据.为此,我在名为cert.pem"的文件中有一个CA 证书客户端证书客户端证书密钥.尽管我的服务器代码工作正常,但我的客户端代码无法连接到服务器.我只是模仿了 SSL 模块示例,我做到了不明白为什么会失败.

I am using python and SSL module to establish a TLS connection to a server. Then I would like to send some data to this server. To do this, I have a CA-Certificate, Client Certificate, and Client Certificate Key in a file called "cert.pem". Although my server code works fine, my client code fails to connect to the server. I just mimicked the SSL module example and I do not understand why it fails.

运行良好的服务器代码:

import socket, ssl

cert_chain  = 'cert.pem'
host_addr   = socket.gethostbyname(socket.gethostname())
host_port   = 10023

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile=cert_chain)
bindsocket = socket.socket()
bindsocket.bind((host_addr, host_port))
bindsocket.listen(5)

while True:
  print("Waiting for client")
  newsocket, fromaddr = bindsocket.accept()
  conn = context.wrap_socket(newsocket, server_side=True)
  print("SSL established. Peer: {}".format(conn.getpeercert()))
  buf = b''  # Buffer to hold received client data
  try:
    while True:
      data = conn.recv(4096)
      if data:      # Client sent us data. Append to buffer
        buf += data
      else:         # No more data from client. Show buffer and close connection.
        print("Received:", buf)
        break
  finally:
    print("Closing connection")
    conn.shutdown(socket.SHUT_RDWR)
    conn.close()

失败的客户端代码:

import socket, ssl

server_addr = '**.***.***.***'
server_port = 3335
cert_chain  = 'cert.pem'

context = ssl.create_default_context()
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
context.load_verify_locations("/etc/ssl/certs/ca-bundle.crt")
conn = context.wrap_socket(socket.socket(socket.AF_INET), server_hostname=server_addr)
conn.connect((server_addr, server_port))

conn.send(b"Hello, world!")
conn.close()

当我运行客户端代码时,出现此错误:

When I run the client code, I get this error:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] 证书验证失败:无法获取本地颁发者证书 (_ssl.c:1091)

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1091)

但是,当我简单地使用这个命令时,我能够发送数据:

However when I simply use this command, I am able to send data:

openssl s_client -connect 127.0.0.1:10023

问题

  • 我的客户端代码有什么问题?
  • 为什么 openssl s_client 命令有效而 python 代码无效?
  • Questions

    • What is wrong with my client code?
    • Why the openssl s_client command works but the python code does not?
    • 我非常感谢任何改进代码的建议.

      I highly appreciate any suggestions to improve the code.

我在这里看到两个问题:

I see two problems here:

a) 根据您的代码,您的服务器侦听端口 10023,但您的客户端尝试连接到 3335.但这可能只是您发布的代码中的问题.如果它在您运行的代码中,您将根本无法建立连接.

a) According to your code, your server listens on port 10023, but your client tries to connect to 3335. But that is probably just a problem in the code you posted. If it was in the code you run, you would not have been able to establish a connection at all.

b) 当客户端无法验证服务器证书时,您会收到 CERTIFICATE_VERIFY_FAILED.例如,当您浏览到 https://*.com 时,您会在地址栏中看到某种锁定符号,表明您的连接是安全的.这是因为服务器为域 *.com 提供了一个有效且受信任的证书.在您的情况下,您的证书很可能既无效也不可信.
为此,证书需要将 IP 地址或主机名(无论您在 wrap_socket 方法中作为 server_hostname 提供的内容)作为 SAN 并且证书需要由受信任的 CA 签名.由于您可能使用的是自签名证书,因此情况并非如此,因为您的信任库是 /etc/ssl/certs/ca-bundle.crt.当您颁发自签名证书时,您可以将 IP/主机名添加到 SAN 和 CN,并且在您的代码中,您可以尝试将证书添加到 /etc/ssl/certs/ca-bundle.crt代码>.
出于开发目的,您可以将 verify_mode 设置为 CERT_NONE(请参阅 doc) 应该可以解决您目前的所有问题.不过,这不应在生产中使用.

b) You get a CERTIFICATE_VERIFY_FAILED when the server certificate can not be verified by the client. For example, when you browse to https://*.com you will see some sort of lock-symbol in your address bar, indicating, that your connection is secure. That is because the server supplies a certificate that is valid and trusted for the domain *.com. In your case, your certificate is most probably neither valid, nor trusted.
For that to work, the certificate would need to have the IP address or hostname (whatever you supply as server_hostname in the wrap_socket method) as SAN and the certificate would need to be signed by a trusted CA. As you are probably using a self-signed certificate, this will not be the case, because your trust-store is /etc/ssl/certs/ca-bundle.crt. When you issue your self-signed certificate, you can add the IP/hostname to SAN and CN and in your code, you can try to add the certificate to /etc/ssl/certs/ca-bundle.crt.
For development purposes, you can set verify_mode to CERT_NONE (see doc) which should solve all your problems for the moment. This should not be used in production, though.