【问题标题】:How can I do certificate pinning for TLS sockets in python 2.7?如何在 python 2.7 中为 TLS 套接字进行证书固定?
【发布时间】:2018-08-17 15:07:56
【问题描述】:

我有一个使用自签名证书的 python TLS 服务器。这样可行。代码现在看起来像这样:

#!/usr/bin/python

import socket, ssl

context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
context.load_cert_chain(certfile="server.crt", keyfile="server.key")

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

while True:
    newsocket, fromaddr = bindsocket.accept()
    connstream = context.wrap_socket(newsocket, server_side=True)
    try:
        print("Got connection!")
    finally:
        connstream.shutdown(socket.SHUT_RDWR)
        connstream.close()

我现在正在尝试在 python 中创建一个连接到该服务器的客户端。在第一次连接尝试时,我想检索公钥或公钥的哈希,然后在所有未来的连接上验证这一点。如何使用 python 和 ssl 包?

这是我正在玩的代码:

#!/usr/bin/python

import ssl, socket, pprint

context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = False


cnx = context.wrap_socket(socket.socket(socket.AF_INET), certfile="server.crt")
cnx.connect(('127.0.0.1', 8888))
pprint.pprint(cnx.getpeercert())

就目前而言,它失败了,因为没有证书链来验证证书。不过,我不在乎。我关心的是我正在与之交谈的服务器具有与公钥匹配的私钥。我该怎么办?

【问题讨论】:

  • 当客户端连接到服务器时,它会获得服务器证书,这是它带有一些元数据的公钥。所以你必须在客户端硬编码你想要的公钥,并测试它是否与服务器发送给你的内容相匹配。
  • 您是否尝试将证书本身用作 CA 文件,以便仍然完成验证并且设计上仅限于一个给定的证书?当然,如果您需要考虑多个自动签名,则不能这样做。
  • 使用ssl 似乎不太可能(您总是可以使用openssl 命令,但这会很难看)但是使用PyOpenSSL,当您拥有证书时,您可以在其上使用get_pubkey() 来检索公钥。我仍然建议保留证书验证,将其存储在本地似乎是一个小限制,否则您可能会错过许多安全问题。
  • 仅仅检查公钥是不够的。想象一下,一个伪造的服务器被配置为向您发送带有您想要的特定公钥的无效证书(这很容易知道,因为您的真实服务器将其广播到任何连接的客户端)和另一个本地制作的私钥。证书签名显然不匹配,但您根本不检查它。您将相信公钥部分并继续使用 TLS。当然,只要您完全使用127.0.0.1 并且没有不安全的帐户,您就很安全,但仍然不验证证书根本不是一个好主意。
  • ClientKeyExchange不一定使用PreMasterSecret,在不做RSA认证的情况下可以使用其他密钥(参见TLS 1.2 RFC 7.4.7),那就是Diffie-Hellman。所以这一切都取决于双方协商的密码套件。另请注意,ClientKeyExchange 和 PreMasterSecret 在 TLS 1.3 中消失了。简而言之,我仍然认为禁用证书验证会使您面临太多危险。如果您不喜欢证书,请改用 TLS-PSK,这样会更干净。

标签: python python-2.7 ssl certificate tls1.2


【解决方案1】:

所以我完成了这项工作,尽管这并不是我所希望的。我最大的抱怨是它需要将整个服务器证书存储在磁盘上的文件中,而我真正关心的是公钥。但是,这是我的工作:

#!/usr/bin/python

import ssl, socket, pprint, os, sys

# If we haven't retrieved the certificate previously...
if not os.path.isfile('server_cert.pem'):

    # Grab the cert
    cert = ssl.get_server_certificate(('127.0.0.1', 8888))

    # If it worked...
    if cert:

        # Write it to a file
        with open('server_cert.pem', 'w') as f:
            f.write(cert)
    else:
        sys.exit()

# Prepare context, including reference to the server's certificate
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = False
context.load_verify_locations(cafile='server_cert.pem')
cnx = context.wrap_socket(socket.socket(socket.AF_INET))

# Connect and evaluate
cnx.connect(('127.0.0.1', 8888))

我会稍等片刻将此答案标记为已接受,看看是否有人能找到更清洁的方法。

【讨论】:

  • 如果您不喜欢将其作为文件保存在磁盘上,您始终可以将其作为字符串存储在您的代码中,并在load_verify_locaions() 中使用cadata 而不是cafile。当然,你需要维护它......
  • 这不是证书固定,而是首次使用时信任。有区别。
  • 密钥分发方法的区别(意思是,如果它是通过其他一些安全通道分发的,而不是在设备首次使用时提供的,则它是证书固定)?
  • TOFU(首次使用时信任)例如被ssh 使用:当您第一次连接到主机时,它会显示从主机检索到的密钥(或从它派生的任何内容)并询问如果你认出它并想走得更远(因为它在本地没有任何方式)。如果是,它会存储密钥,然后在以后的每个新连接中检查它。这基本上就是您上面的代码所做的。它有效......但需要注意的是,如果在第一次连接时交换以任何方式受到损害和/或用户不小心,那么所有未来的交换都会受到损害而不会被发现。
猜你喜欢
  • 2019-10-08
  • 1970-01-01
  • 2014-06-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-19
  • 2021-12-04
  • 2021-09-10
  • 1970-01-01
相关资源
最近更新 更多