繁体   English   中英

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

[英]python ssl (eqivalent of openssl s_client -showcerts ) How to get list of CAs for client certs from server

我有一组接受客户端证书的 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,因此我不得不将其排除在外。

在@Steffen Ullrich 的帮助下,我找到了以下适用于大多数服务器的解决方案。

但是我遇到了解决方案不起作用的情况因此我删除了“正确”答案检查,直到解决方案适用于所有 https 服务器

它需要安装一个外部包

pip install pyopenssl

然后以下工作将起作用:

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()

需要sock.do_handshake()行来触发足够的 SSL 协议。 否则似乎没有填充 client_ca_list 信息。

至少对于我测试的服务器,我必须确保使用 TLSv1.3。 我不知道这是一个错误,一个功能,还是在调用get_client_ca_list()之前是否必须使用 TLSv1.3 调用另一个函数

我不是 pyopenssl 专家,但可以想象,有一种更优雅/更明确的方式来获得相同的行为。

但到目前为止,这适用于我遇到的所有服务器。

作为python中的通用示例

  1. 首先,您需要联系服务器以了解它接受哪些颁发者 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 连接将失败。 这里有很多不好的做法,但不幸的是,在我们真正想要使用正确的证书尝试客户端身份验证之前,它们是必要的,也是从服务器收集消息的唯一方法。

  1. 接下来根据服务器加载证书:
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

在真正的应用程序(不是示例代码段)中,您将拥有某种类型的数据库来获取服务器主题并查找要加载的证书的位置 - 本示例反向执行此操作仅用于演示。

  1. 建立 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错误(如果遇到任何错误),查找字典就在这里

许多验证在 OpenSSL 内部进行验证,而 PyOpenSSL 所做的只是基本验证。 因此,如果我们想要进行客户端身份验证,即在客户端上,我们需要从 OpenSSL 访问这些代码,如果客户端的任何身份验证检查失败,则根据客户端授权或更确切地说是双向 TLS 指示,丢弃来自不受信任服务器的响应

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM