我有一组接受客户端证书的 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中的一个通用示例
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
在一个真实的应用程序(不是示例片段)中,您将拥有某种数据库来获取服务器主题并查找要加载的证书的位置 - 此示例反向执行此操作仅用于演示。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/