python ssl(等效于 openssl s_client -showcerts)如何从服务器获取客户端证书的 CA 列表

标签 python ssl client-certificates pyopenssl

我有一组接受客户端证书的 nginx 服务器。
他们有 ssl_client_certificate包含一个或多个 CA 的文件的选项
如果我使用 Web 浏览器,那么 Web 浏览器似乎会收到客户端证书的有效 CA 列表。浏览器仅显示由这些 CA 之一签名的客户端证书。
以下 openssl 命令为我提供了 CA 证书列表:

openssl s_client -showcerts -servername myserver.com -connect myserver.com:443 </dev/null
我感兴趣的线条如下所示:
---
Acceptable client certificate CA names
/C=XX/O=XX XXXX
/C=YY/O=Y/OU=YY YYYYYL
...
Client Certificate Types: RSA sign, DSA sign, ECDSA sign

如何使用 python 获得相同的信息?
我确实有以下代码片段,它允许获取服务器的证书,但此代码不返回客户端证书的 CA 列表。
import ssl

def get_server_cert(hostname, port):
    conn = ssl.create_connection((hostname, port))
    context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    sock = context.wrap_socket(conn, server_hostname=hostname)
    cert = sock.getpeercert(True)
    cert = ssl.DER_cert_to_PEM_cert(cert)
    return cerft
我希望找到 getpeercert() 的功能等价物,类似于 getpeercas()但什么也没找到。
当前解决方法:
import os
import subprocess


def get_client_cert_cas(hostname, port):
    """
    returns a list of CAs, for which client certs are accepted
    """

    cmd = [
        "openssl",
        "s_client",
        "-showcerts",
        "-servername",  hostname,
        "-connect",  hostname + ":" + str(port),
        ]

    stdin = open(os.devnull, "r")
    stderr = open(os.devnull, "w")

    output = subprocess.check_output(cmd, stdin=stdin, stderr=stderr)
    ca_signatures = []
    state = 0
    for line in output.decode().split("\n"):
        print(state, line)
        if state == 0:
            if line == "Acceptable client certificate CA names":
                state = 1
        elif state == 1:
            if line.startswith("Client Certificate Types:"):
                break
            ca_signatures.append(line)
    return ca_signatures
更新:pyopenssl 的解决方案(感谢 Steffen Ullrich)
@Steffen Ulrich 建议使用 pyopenssl,它有一个方法 get_client_ca_list()这帮助我编写了一个小代码片段。
下面的代码似乎工作。不确定它是否可以改进或是否有任何坑。
如果在接下来的几天内没有人回答,我会将其发布为潜在答案。
import socket
from OpenSSL import SSL

def get_client_cert_cas(hostname, port):
    ctx = SSL.Context(SSL.SSLv23_METHOD)
    # If we don't force to NOT use TLSv1.3 get_client_ca_list() returns
    # an empty result
    ctx.set_options(SSL.OP_NO_TLSv1_3)
    sock = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM))
    # next line for SNI
    sock.set_tlsext_host_name(hostname.encode("utf-8"))
    sock.connect((hostname, port))
    # without handshake get_client_ca_list will be empty
    sock.do_handshake()  
    return sock.get_client_ca_list()
更新:2021-03-31
以上建议的解决方案使用 pyopenssl在大多数情况下都有效。
然而sock.get_client_ca_list())执行 sock.connect((hostname, port)) 后无法立即调用
在这两个命令之间似乎需要一些操作。
最初我使用 sock.send(b"G") ,但现在我使用 sock.do_handshake() ,看起来更干净一些。
更奇怪的是,该解决方案不适用于 TLSv1.3,所以我不得不排除它。

最佳答案

作为python中的一个通用示例

  • 首先您需要联系服务器以了解它接受哪些颁发者 CA 主题:

  • from socket import socket, AF_INET, SOCK_STREAM
    from OpenSSL import SSL
    from OpenSSL.crypto import X509Name
    from certifi import where
    import idna
    
    
    def get_server_expected_client_subjects(host :str, port :int = 443) -> list[X509Name]:
        expected_subjects = []
        ctx = SSL.Context(method=SSL.SSLv23_METHOD)
        ctx.verify_mode = SSL.VERIFY_NONE
        ctx.check_hostname = False
        conn = SSL.Connection(ctx, socket(AF_INET, SOCK_STREAM))
        conn.connect((host, port))
        conn.settimeout(3)
        conn.set_tlsext_host_name(idna.encode(host))
        conn.setblocking(1)
        conn.set_connect_state()
        try:
            conn.do_handshake()
            expected_subjects :list[X509Name] = conn.get_client_ca_list()
        except SSL.Error as err:
            raise SSL.Error from err
        finally:
            conn.close()
        return expected_subjects
    
    这没有客户端证书,因此 TLS 连接将失败。这里有很多不好的做法,但不幸的是,在我们真正想要使用正确的证书尝试客户端身份验证之前,它们是必要的,也是从服务器收集消息的唯一方法。
  • 接下来根据服务器加载证书:

  • from pathlib import Path
    from OpenSSL.crypto import load_certificate, FILETYPE_PEM
    from pathlib import Path
    
    def check_client_cert_issuer(client_pem :str, expected_subjects :list) -> str:
        client_cert = None
        if len(expected_subjects) > 0:
            client_cert_path = Path(client_pem)
            cert = load_certificate(FILETYPE_PEM, client_cert_path.read_bytes())
            issuer_subject = cert.get_issuer()
            for check in expected_subjects:
                if issuer_subject.commonName == check.commonName:
                    client_cert = client_pem
                    break
        if client_cert is None or not isinstance(client_cert, str):
            raise Exception('X509_V_ERR_SUBJECT_ISSUER_MISMATCH') # OpenSSL error code 29
        return client_cert
    
    在一个真实的应用程序(不是示例片段)中,您将拥有某种数据库来获取服务器主题并查找要加载的证书的位置 - 此示例反向执行此操作仅用于演示。
  • 建立 TLS 连接,并捕获任何 OpenSSL 错误:

  • from socket import socket, AF_INET, SOCK_STREAM
    from OpenSSL import SSL
    from OpenSSL.crypto import X509, FILETYPE_PEM
    from certifi import where
    import idna
    
    
    def openssl_verifier(conn :SSL.Connection, server_cert :X509, errno :int, depth :int, preverify_ok :int):
        ok = 1
        verifier_errors = conn.get_app_data()
        if not isinstance(verifier_errors, list):
            verifier_errors = []
        if errno in OPENSSL_CODES.keys():
            ok = 0
            verifier_errors.append((server_cert, OPENSSL_CODES[errno]))
        conn.set_app_data(verifier_errors)
        return ok
    
    client_pem = '/path/to/client.pem'
    client_issuer_ca = '/path/to/ca.pem'
    host = 'example.com'
    port = 443
    
    ctx = SSL.Context(method=SSL.SSLv23_METHOD) # will negotiate TLS1.3 or lower protocol, what every is highest possible during negotiation
    ctx.load_verify_locations(cafile=where())
    if client_pem is not None:
        ctx.use_certificate_file(certfile=client_pem, filetype=FILETYPE_PEM)
        if client_issuer_ca is not None:
            ctx.load_client_ca(cafile=client_issuer_ca)
    ctx.set_verify(SSL.VERIFY_NONE, openssl_verifier)
    ctx.check_hostname = False
    conn = SSL.Connection(ctx, socket(AF_INET, SOCK_STREAM))
    conn.connect((host, port))
    conn.settimeout(3)
    conn.set_tlsext_host_name(idna.encode(host))
    conn.setblocking(1)
    conn.set_connect_state()
    try:
        conn.do_handshake()
        verifier_errors = conn.get_app_data()
    except SSL.Error as err:
        raise SSL.Error from err
    finally:
        conn.close()
    
    # handle your errors in your main app
    print(verifier_errors)
    
    只要确保你处理那些OPENSSL_CODES如果遇到任何错误,查找字典 is here .
    许多验证发生上一页 OpenSSL 本身内部的验证和所有 PyOpenSSL 将做的只是基本验证。因此,如果我们想要进行客户端身份验证,我们需要从 OpenSSL 访问这些代码,即在客户端上,如果客户端授权或双向 TLS 指令未通过客户端的任何身份验证检查,则丢弃来自不受信任服务器的响应

    关于python ssl(等效于 openssl s_client -showcerts)如何从服务器获取客户端证书的 CA 列表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64644890/

    相关文章:

    python - 使用 Python xlwings 设置边框

    python - pandas 最终排名状态

    asp.net - 如何静默请求特定的 SSL 客户端证书

    c# - 使用 Owin 的客户端证书身份验证

    wcf - BizTalk WCF-BasicHttp Identity Editor 字段说明

    python - 在 python 中使用元组循环提取数据

    python - MyPy - 处理可能的 None 返回类型

    security - 从一个 HTTPS 站点向另一个 HTTPS 站点提交表单是否安全?

    java - keystore 加载

    ssl - 安装新签名证书后 RabbitMQ 连接问题