如何使用 Python 请求库检查 OCSP 客户端证书吊销?
How to check OCSP client certificate revocation using Python Requests library?
如何使用 Python 请求库向 EJBCA OSCP 响应程序发出简单 证书吊销状态请求?
示例:
# Determine if certificate has been revoked
ocsp_url = req_cert.extensions[2].value[0].access_location.value
ocsp_headers = {"whatGoes: here?"}
ocsp_body = {"What goes here?"}
ocsp_response = requests.get(ocsp_url, ocsp_headers, ocsp_body)
if (ocsp_response == 'revoked'):
return func.HttpResponse(
"Certificate is not valid (Revoked)."
)
基本上包括以下步骤:
- 检索主机名的相应证书
- 如果证书中包含相应的条目,您可以通过AuthorityInformationAccessOID.CA_ISSUERS查询扩展,如果成功,它将为您提供link颁发者证书
- 使用此 link
检索颁发者证书
- 同样你通过AuthorityInformationAccessOID.OCSP得到对应的OCSP服务器
- 使用有关当前证书、issuer_cert 和 ocsp 服务器的信息,您可以提供 OCSPRequestBuilder 以创建 OCSP 请求
- 使用
requests.get
获取OCSP响应
- 从 OCSP 响应中检索
certificate_status
要检索主机名和端口的证书,您可以使用这个很好的答案:. The OCSP handling in Python is documented here: https://cryptography.io/en/latest/x509/ocsp.html。
代码
如果将以上几点转换成self-contained例子,它看起来像这样:
import base64
import ssl
import requests
from urllib.parse import urljoin
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.x509 import ocsp
from cryptography.x509.ocsp import OCSPResponseStatus
from cryptography.x509.oid import ExtensionOID, AuthorityInformationAccessOID
def get_cert_for_hostname(hostname, port):
conn = ssl.create_connection((hostname, port))
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
sock = context.wrap_socket(conn, server_hostname=hostname)
certDER = sock.getpeercert(True)
certPEM = ssl.DER_cert_to_PEM_cert(certDER)
return x509.load_pem_x509_certificate(certPEM.encode('ascii'), default_backend())
def get_issuer(cert):
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS).value
issuers = [ia for ia in aia if ia.access_method == AuthorityInformationAccessOID.CA_ISSUERS]
if not issuers:
raise Exception(f'no issuers entry in AIA')
return issuers[0].access_location.value
def get_ocsp_server(cert):
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS).value
ocsps = [ia for ia in aia if ia.access_method == AuthorityInformationAccessOID.OCSP]
if not ocsps:
raise Exception(f'no ocsp server entry in AIA')
return ocsps[0].access_location.value
def get_issuer_cert(ca_issuer):
issuer_response = requests.get(ca_issuer)
if issuer_response.ok:
issuerDER = issuer_response.content
issuerPEM = ssl.DER_cert_to_PEM_cert(issuerDER)
return x509.load_pem_x509_certificate(issuerPEM.encode('ascii'), default_backend())
raise Exception(f'fetching issuer cert failed with response status: {issuer_response.status_code}')
def get_oscp_request(ocsp_server, cert, issuer_cert):
builder = ocsp.OCSPRequestBuilder()
builder = builder.add_certificate(cert, issuer_cert, SHA256())
req = builder.build()
req_path = base64.b64encode(req.public_bytes(serialization.Encoding.DER))
return urljoin(ocsp_server + '/', req_path.decode('ascii'))
def get_ocsp_cert_status(ocsp_server, cert, issuer_cert):
ocsp_resp = requests.get(get_oscp_request(ocsp_server, cert, issuer_cert))
if ocsp_resp.ok:
ocsp_decoded = ocsp.load_der_ocsp_response(ocsp_resp.content)
if ocsp_decoded.response_status == OCSPResponseStatus.SUCCESSFUL:
return ocsp_decoded.certificate_status
else:
raise Exception(f'decoding ocsp response failed: {ocsp_decoded.response_status}')
raise Exception(f'fetching ocsp cert status failed with response status: {ocsp_resp.status_code}')
def get_cert_status_for_host(hostname, port):
print(' hostname:', hostname, "port:", port)
cert = get_cert_for_hostname(hostname, port)
ca_issuer = get_issuer(cert)
print(' issuer ->', ca_issuer)
issuer_cert = get_issuer_cert(ca_issuer)
ocsp_server = get_ocsp_server(cert)
print(' ocsp_server ->', ocsp_server)
return get_ocsp_cert_status(ocsp_server, cert, issuer_cert)
测试 1:良好证书
像下面这样的带有良好证书的测试调用
status = get_cert_status_for_host('software7.com', 443)
print('software7.com:', status, '\n')
结果如下:
hostname: software7.com port: 443
issuer -> http://cacerts.digicert.com/EncryptionEverywhereDVTLSCA-G1.crt
ocsp_server -> http://ocsp.digicert.com
software7.com: OCSPCertStatus.GOOD
测试 2:吊销证书
当然你也必须用吊销的证书做一个反测试。这里revoked.badssl.com是首选:
status = get_cert_status_for_host('revoked.badssl.com', 443)
print('revoked.badssl.com:', status, '\n')
输出为:
hostname: revoked.badssl.com port: 443
issuer -> http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt
ocsp_server -> http://ocsp.digicert.com
revoked.badssl.com: OCSPCertStatus.REVOKED
AIA 检索颁发者证书
证书关系的典型场景如下所示:
服务器在TLS握手期间提供服务器证书,通常还有一个或多个中间证书。 'usually' 这个词是有意使用的:一些服务器被配置为不提供中间证书。然后浏览器使用 AIA 抓取来构建认证链。
证书颁发机构信息访问扩展中最多可以存在两个条目:用于下载颁发者证书的条目和 link 到 OCSP 服务器。
这些条目也可能丢失,但是检查 100 个最流行服务器的证书的简短测试脚本显示这些条目通常包含在 public 证书颁发机构颁发的证书中。
CA Issuers 条目也可能丢失,但是当有关 OCSP 服务器的信息可用时,可以对其进行测试,例如使用 self-signed 证书的 OpenSSL:
在这种情况下,您必须从 TLS 握手中的链中确定颁发者证书,它是紧接在链中服务器证书之后的证书,另请参见上图。
只是为了完整起见:还有一种情况有时会发生,尤其是与 self-signed 证书结合使用时:如果没有使用中间证书,相应的根证书(例如在本地信任库中可用) 必须用作颁发者证书。
如何使用 Python 请求库向 EJBCA OSCP 响应程序发出简单 证书吊销状态请求?
示例:
# Determine if certificate has been revoked
ocsp_url = req_cert.extensions[2].value[0].access_location.value
ocsp_headers = {"whatGoes: here?"}
ocsp_body = {"What goes here?"}
ocsp_response = requests.get(ocsp_url, ocsp_headers, ocsp_body)
if (ocsp_response == 'revoked'):
return func.HttpResponse(
"Certificate is not valid (Revoked)."
)
基本上包括以下步骤:
- 检索主机名的相应证书
- 如果证书中包含相应的条目,您可以通过AuthorityInformationAccessOID.CA_ISSUERS查询扩展,如果成功,它将为您提供link颁发者证书
- 使用此 link 检索颁发者证书
- 同样你通过AuthorityInformationAccessOID.OCSP得到对应的OCSP服务器
- 使用有关当前证书、issuer_cert 和 ocsp 服务器的信息,您可以提供 OCSPRequestBuilder 以创建 OCSP 请求
- 使用
requests.get
获取OCSP响应 - 从 OCSP 响应中检索
certificate_status
要检索主机名和端口的证书,您可以使用这个很好的答案:
代码
如果将以上几点转换成self-contained例子,它看起来像这样:
import base64
import ssl
import requests
from urllib.parse import urljoin
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.x509 import ocsp
from cryptography.x509.ocsp import OCSPResponseStatus
from cryptography.x509.oid import ExtensionOID, AuthorityInformationAccessOID
def get_cert_for_hostname(hostname, port):
conn = ssl.create_connection((hostname, port))
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
sock = context.wrap_socket(conn, server_hostname=hostname)
certDER = sock.getpeercert(True)
certPEM = ssl.DER_cert_to_PEM_cert(certDER)
return x509.load_pem_x509_certificate(certPEM.encode('ascii'), default_backend())
def get_issuer(cert):
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS).value
issuers = [ia for ia in aia if ia.access_method == AuthorityInformationAccessOID.CA_ISSUERS]
if not issuers:
raise Exception(f'no issuers entry in AIA')
return issuers[0].access_location.value
def get_ocsp_server(cert):
aia = cert.extensions.get_extension_for_oid(ExtensionOID.AUTHORITY_INFORMATION_ACCESS).value
ocsps = [ia for ia in aia if ia.access_method == AuthorityInformationAccessOID.OCSP]
if not ocsps:
raise Exception(f'no ocsp server entry in AIA')
return ocsps[0].access_location.value
def get_issuer_cert(ca_issuer):
issuer_response = requests.get(ca_issuer)
if issuer_response.ok:
issuerDER = issuer_response.content
issuerPEM = ssl.DER_cert_to_PEM_cert(issuerDER)
return x509.load_pem_x509_certificate(issuerPEM.encode('ascii'), default_backend())
raise Exception(f'fetching issuer cert failed with response status: {issuer_response.status_code}')
def get_oscp_request(ocsp_server, cert, issuer_cert):
builder = ocsp.OCSPRequestBuilder()
builder = builder.add_certificate(cert, issuer_cert, SHA256())
req = builder.build()
req_path = base64.b64encode(req.public_bytes(serialization.Encoding.DER))
return urljoin(ocsp_server + '/', req_path.decode('ascii'))
def get_ocsp_cert_status(ocsp_server, cert, issuer_cert):
ocsp_resp = requests.get(get_oscp_request(ocsp_server, cert, issuer_cert))
if ocsp_resp.ok:
ocsp_decoded = ocsp.load_der_ocsp_response(ocsp_resp.content)
if ocsp_decoded.response_status == OCSPResponseStatus.SUCCESSFUL:
return ocsp_decoded.certificate_status
else:
raise Exception(f'decoding ocsp response failed: {ocsp_decoded.response_status}')
raise Exception(f'fetching ocsp cert status failed with response status: {ocsp_resp.status_code}')
def get_cert_status_for_host(hostname, port):
print(' hostname:', hostname, "port:", port)
cert = get_cert_for_hostname(hostname, port)
ca_issuer = get_issuer(cert)
print(' issuer ->', ca_issuer)
issuer_cert = get_issuer_cert(ca_issuer)
ocsp_server = get_ocsp_server(cert)
print(' ocsp_server ->', ocsp_server)
return get_ocsp_cert_status(ocsp_server, cert, issuer_cert)
测试 1:良好证书
像下面这样的带有良好证书的测试调用
status = get_cert_status_for_host('software7.com', 443)
print('software7.com:', status, '\n')
结果如下:
hostname: software7.com port: 443
issuer -> http://cacerts.digicert.com/EncryptionEverywhereDVTLSCA-G1.crt
ocsp_server -> http://ocsp.digicert.com
software7.com: OCSPCertStatus.GOOD
测试 2:吊销证书
当然你也必须用吊销的证书做一个反测试。这里revoked.badssl.com是首选:
status = get_cert_status_for_host('revoked.badssl.com', 443)
print('revoked.badssl.com:', status, '\n')
输出为:
hostname: revoked.badssl.com port: 443
issuer -> http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt
ocsp_server -> http://ocsp.digicert.com
revoked.badssl.com: OCSPCertStatus.REVOKED
AIA 检索颁发者证书
证书关系的典型场景如下所示:
服务器在TLS握手期间提供服务器证书,通常还有一个或多个中间证书。 'usually' 这个词是有意使用的:一些服务器被配置为不提供中间证书。然后浏览器使用 AIA 抓取来构建认证链。
证书颁发机构信息访问扩展中最多可以存在两个条目:用于下载颁发者证书的条目和 link 到 OCSP 服务器。
这些条目也可能丢失,但是检查 100 个最流行服务器的证书的简短测试脚本显示这些条目通常包含在 public 证书颁发机构颁发的证书中。
CA Issuers 条目也可能丢失,但是当有关 OCSP 服务器的信息可用时,可以对其进行测试,例如使用 self-signed 证书的 OpenSSL:
在这种情况下,您必须从 TLS 握手中的链中确定颁发者证书,它是紧接在链中服务器证书之后的证书,另请参见上图。
只是为了完整起见:还有一种情况有时会发生,尤其是与 self-signed 证书结合使用时:如果没有使用中间证书,相应的根证书(例如在本地信任库中可用) 必须用作颁发者证书。