如何使用 python 建立 TLS 连接?

How to make a TLS connection using python?

我想创建到服务器的 TLS 连接。然后,我想向服务器发送一些加密数据。我知道主机名和端口,并且有证书。令人惊讶的是,我还收到了服务器的私钥。但是我觉得收到私钥是不正常的。

第一个问题是,我真的需要私钥来建立 TLS 连接吗?

顺便说一下,我正在使用这个 python 脚本

import socket
import ssl

server_addr = '**.**.**.**'
server_port = ****
server_cert = 'server.crt'
server_key  = 'server.key'        # I use the private key

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.verify_mode = ssl.CERT_REQUIRED
context.load_cert_chain(certfile=server_cert, keyfile=server_key)

bindsocket = socket.socket()
bindsocket.connect((server_addr, server_port))

我在上面的脚本中使用了私钥。它没有任何错误。但是,当我尝试使用 bind() 而不是 connect() 时,即

bindsocket.bind((server_addr, server_port))

我收到以下错误:

OSError: [Errno 99] Cannot assign requested address

我看了很多关于上述错误的相关问题,但是,我仍然不明白为什么会这样。因为,我有主机名、端口、证书和密钥,我希望能成功创建 TLS 连接。

第二个问题是如何建立TLS连接?我的脚本正确吗?

非常感谢任何改进脚本的评论。

所以,首先,你绝对不能有私钥!顾名思义,它是私​​有的,不需要建立连接。
您可以拥有 public 密钥,但即使您使用标准 SSL 并且您信任签署服务器证书的 CA,也没有必要。
你确定,这是私钥?文件是否以 -----BEGIN PRIVATE KEY----- 开头?用 openssl rsa -noout -text -in server.key.
检查 有关非对称加密的更多信息,请参阅 wikipedia article and this post

一路走来:
使用 socket.bind() 可以将套接字绑定到本地计算机上的端口。这是不可能的,因为您的机器没有地址(您提供服务器地址)。
从您的代码来看,您似乎正试图将套接字作为服务器打开。为此,您将需要私钥,但随后您将接受连接,而不是自己连接到其他机器。我有一种感觉,你在这里混淆了两件事。
参考python documentation of socket.bind() and to this question,因为这似乎是密切相关的。
另请查看 python documentation on ssl。我举了这个例子,它满足了您从上述文档中要求的要求:

import socket, ssl, pprint

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# require a certificate from the server
ssl_sock = ssl.wrap_socket(s,
                           ca_certs="/etc/ca_certs_file",
                           cert_reqs=ssl.CERT_REQUIRED)
ssl_sock.connect(('www.verisign.com', 443))

pprint.pprint(ssl_sock.getpeercert())
# note that closing the SSLSocket will also close the underlying socket
ssl_sock.close()

也可以看看 example on how to open a SSL socket in server mode

进一步思考:
您真的需要自己完成所有 TLS 工作吗?例如,如果服务器使用 HTTPS(SSL 加密的 HTTP),您可以只使用 http.client 库。

如果您需要我澄清一些事情,请随时询问。我会相应地更新我的答案。

编辑:
正如您所指出的,您想在服务器模式下打开一个端口,我为您做了一个示例(它很大程度上依赖于 python 文档示例):

import socket, ssl

context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

bindsocket = socket.socket()
bindsocket.bind(('127.0.0.1', 10023))
bindsocket.listen(5)

def deal_with_client(connstream):
    data = connstream.recv(1024)
    # empty data means the client is finished with us
    while data:
        print(data)
        data = connstream.recv(1024)

while True:
    newsocket, fromaddr = bindsocket.accept()
    connstream = context.wrap_socket(newsocket, server_side=True)
    try:
        deal_with_client(connstream)
    finally:
        connstream.shutdown(socket.SHUT_RDWR)
        connstream.close()

运行它:

% python3 ssltest.py
b'hello server!\n'
b'this is data\n'

客户端:

% openssl s_client -connect 127.0.0.1:10023
CONNECTED(00000005)
depth=0 C = SE, ST = Some-State, O = Internet Widgits Pty Ltd
verify error:num=18:self signed certificate
verify return:1
depth=0 C = SE, ST = Some-State, O = Internet Widgits Pty Ltd
verify return:1
---
Certificate chain
 0 s:C = SE, ST = Some-State, O = Internet Widgits Pty Ltd
   i:C = SE, ST = Some-State, O = Internet Widgits Pty Ltd
---
Server certificate
-----BEGIN CERTIFICATE-----
 ... certificate ...
-----END CERTIFICATE-----
subject=C = SE, ST = Some-State, O = Internet Widgits Pty Ltd

issuer=C = SE, ST = Some-State, O = Internet Widgits Pty Ltd

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 2272 bytes and written 404 bytes
Verification error: self signed certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    ... session stuff ...
    Extended master secret: yes
---
hello server!
this is data
^C

如果你使用像 HTTP 这样的常用协议,你应该使用一个库。这是一个烧瓶的例子:

>>> from flask import Flask
>>> app = Flask(__name__)
>>> 
>>> @app.route("/")
... def hello():
...     return "Hello World!"
... 
>>> if __name__ == "__main__":
...     app.run(ssl_context=('cert.pem', 'key.pem'))
... 
 * Serving Flask app "__main__" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on https://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [06/Aug/2020 11:45:50] "GET / HTTP/1.1" 200 -