如何使用Python解码SSL证书?

43

我如何使用Python解码一个以pem格式(base64编码)编码的证书?例如来自github.com的证书:

-----BEGIN CERTIFICATE-----
MIIHKjCCBhKgAwIBAgIQDnd2il0H8OV5WcoqnVCCtTANBgkqhkiG9w0BAQUFADBp
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSgwJgYDVQQDEx9EaWdpQ2VydCBIaWdoIEFzc3VyYW5j
ZSBFViBDQS0xMB4XDTExMDUyNzAwMDAwMFoXDTEzMDcyOTEyMDAwMFowgcoxHTAb
BgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYBBAGCNzwCAQMTAlVT
MRswGQYLKwYBBAGCNzwCAQITCkNhbGlmb3JuaWExETAPBgNVBAUTCEMzMjY4MTAy
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2Fu
IEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYDVQQDEwpnaXRo
dWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7dOJw11wcgnz
M08acnTZtlqVULtoYZ/3+x8Z4doEMa8VfBp/+XOvHeVDK1YJAEVpSujEW9/Cd1JR
GVvRK9k5ZTagMhkcQXP7MrI9n5jsglsLN2Q5LLcQg3LN8OokS/rZlC7DhRU5qTr2
iNr0J4mmlU+EojdOfCV4OsmDbQIXlXh9R6hVg+4TyBkaszzxX/47AuGF+xFmqwld
n0xD8MckXilyKM7UdWhPJHIprjko/N+NT02Dc3QMbxGbp91i3v/i6xfm/wy/wC0x
O9ZZovLdh0pIe20zERRNNJ8yOPbIGZ3xtj3FRu9RC4rGM+1IYcQdFxu9fLZn6TnP
pVKACvTqzQIDAQABo4IDajCCA2YwHwYDVR0jBBgwFoAUTFjLJfBBT1L0KMiBQ5um
qKDmkuUwHQYDVR0OBBYEFIfRjxlu5IdvU4x3kQdQ36O/VUcgMCUGA1UdEQQeMByC
CmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMIGBBggrBgEFBQcBAQR1MHMwJAYI
KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBLBggrBgEFBQcwAoY/
aHR0cDovL3d3dy5kaWdpY2VydC5jb20vQ0FDZXJ0cy9EaWdpQ2VydEhpZ2hBc3N1
cmFuY2VFVkNBLTEuY3J0MAwGA1UdEwEB/wQCMAAwYQYDVR0fBFowWDAqoCigJoYk
aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL2V2MjAwOWEuY3JsMCqgKKAmhiRodHRw
Oi8vY3JsNC5kaWdpY2VydC5jb20vZXYyMDA5YS5jcmwwggHEBgNVHSAEggG7MIIB
tzCCAbMGCWCGSAGG/WwCATCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGln
aWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCC
AVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABp
AGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBw
AHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQ
AC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQBy
AHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0
ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwBy
AHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBl
AG4AYwBlAC4wHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBEGCWCGSAGG
+EIBAQQEAwIGwDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZIhvcNAQEFBQADggEBABRS
cR+GnW01Poa7ZhqLhZi5AEzLQrVG/AbnRDnI6FLYERQjs3KW6RSUni8AKPfVBEVA
AMb0V0JC3gmJlxENFFxrvQv3GKNfZwLzCThjv8ESnTC6jqVUdFlTZ6EbUFsm2v0T
flkXv0nvlH5FpP06STLwav+JjalhqaqblkbIHOAYHOb7gvQKq1KmyuhUItnbKj1a
InuA6gcF1PnH8FNZX7t3ft6TcEFOI8t4eXnELurXZioY99HFfOISeIKNHeyCngGi
5QK+eKG5WVjFTG9PpTG0SVtemB4uOPYZxDmiSvt5BbjyWeUmEnCtwOh1Ix8Y0Qvg
n2Xkw9dJh1tybLEvrG8=
-----END CERTIFICATE-----
根据ssl-shopper的说法,应该是这样的:
Common Name: github.com
Subject Alternative Names: github.com, www.github.com
Organization: GitHub, Inc.
Locality: San Francisco
State: California
Country: US
Valid From: May 26, 2011
Valid To: July 29, 2013

如何使用Python获取此纯文本?


1
你必须使用Python吗?OpenSSL命令行输出可以为你完成这个任务。 - Joe
我想在 Python 程序中实时从证书文件中获取一些信息。 - puwei219
你可以使用asn1crypto、pyOpenSSL、M2Crypto、cryptography、pyasn1 Python包。这里有一个代码示例(俄语) - jfs
9个回答

65

即使是最新版本的Python标准库也没有包含任何可以解码X.509证书的内容。但是,附加的cryptography软件包支持此功能。引用文档中的一个示例

>>> from cryptography import x509
>>> from cryptography.hazmat.backends import default_backend
>>> cert = x509.load_pem_x509_certificate(pem_data, default_backend())
>>> cert.serial_number
2

另一个可能可选的插件包是pyopenssl。 这是对OpenSSL C API的轻量级封装,这意味着您可以可能做到您想要的,但是预计需要花费几天时间阅读文档。

如果您无法安装Python插件包,但拥有openssl命令行工具,

import subprocess
cert_txt = subprocess.check_output(["openssl", "x509", "-text", "-noout", 
                                    "-in", certificate])

应该可以生产出与您从cert_txt网络工具获得的大致相同的内容。

顺便提一下,直接对base64解码会产生二进制无意义的字符,因为这里有两层编码。 X.509证书ASN.1数据结构,序列化为X.690 DER格式,然后由于DER是二进制格式,进行了Base64装甲以便于文件传输。(在那个你只能可靠地发送7位ASCII字符的九十年代撰写了许多此领域的标准。)


9
上面例子中的pem_data需要是一个字节对象,而不是一个字符串对象。 - HSchmale
6
我的证书是一个字符串: pem_data = ssl.get_server_certificate((hostname, port)),因此我需要将其转换为字节:cert = x509.load_pem_x509_certificate(str.encode(pem_data), default_backend()) - Marcin Kulik
1
想要更新 load_pem_x509_certificate 函数签名:根据文档,现在应该是:cryptography.x509.load_pem_x509_certificate(data)。您不需要使用 default_backend - Klutch27
打印('{0:x}'.format(int(cert.serial_number)))以获得一个完整的强大复制粘贴示例。 - Dmitry Ivanov

32

您可以使用pyasn1pyasn1-modules软件包来解析这种数据。例如:

from pyasn1_modules import pem, rfc2459
from pyasn1.codec.der import decoder

substrate = pem.readPemFromFile(open('cert.pem'))
cert = decoder.decode(substrate, asn1Spec=rfc2459.Certificate())[0]
print(cert.prettyPrint())

阅读pyasn1的文档来了解更多信息。


26

注意:

关于问题中的证书(PEM):

  • 将其保存在名为q016899247.crt的文件中(位于脚本(code00.py)的dir目录下)

  • 结束标签:("-----END CERTIFICATE----")末尾缺少一个连字符(-),在问题 @VERSION #4.中已经修正

code00.py:

#!/usr/bin/env python

import os
import ssl
import sys
from pprint import pprint as pp


def main(*argv):
    cert_file_base_name = "q016899247.crt"
    cert_file_name = os.path.join(os.path.dirname(__file__), cert_file_base_name)
    try:
        cert_dict = ssl._ssl._test_decode_cert(cert_file_name)
    except Exception as e:
        print("Error decoding certificate: {:}".format(e))
    else:
        print("Certificate ({:s}) data:\n".format(cert_file_base_name))
        pp(cert_dict)


if __name__ == "__main__":
    print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                   64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    rc = main(*sys.argv[1:])
    print("\nDone.")
    sys.exit(rc)

输出:

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q016899247]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" ./code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 064bit on win32

Certificate (q016899247.crt) data:

{'OCSP': ('http://ocsp.digicert.com',),
 'caIssuers': ('http://www.digicert.com/CACerts/DigiCertHighAssuranceEVCA-1.crt',),
 'crlDistributionPoints': ('http://crl3.digicert.com/ev2009a.crl',
                           'http://crl4.digicert.com/ev2009a.crl'),
 'issuer': ((('countryName', 'US'),),
            (('organizationName', 'DigiCert Inc'),),
            (('organizationalUnitName', 'www.digicert.com'),),
            (('commonName', 'DigiCert High Assurance EV CA-1'),)),
 'notAfter': 'Jul 29 12:00:00 2013 GMT',
 'notBefore': 'May 27 00:00:00 2011 GMT',
 'serialNumber': '0E77768A5D07F0E57959CA2A9D5082B5',
 'subject': ((('businessCategory', 'Private Organization'),),
             (('jurisdictionCountryName', 'US'),),
             (('jurisdictionStateOrProvinceName', 'California'),),
             (('serialNumber', 'C3268102'),),
             (('countryName', 'US'),),
             (('stateOrProvinceName', 'California'),),
             (('localityName', 'San Francisco'),),
             (('organizationName', 'GitHub, Inc.'),),
             (('commonName', 'github.com'),)),
 'subjectAltName': (('DNS', 'github.com'), ('DNS', 'www.github.com')),
 'version': 3}

Done.

4
一般来说,调用以下划线为前缀的函数是不好的,它们仅供内部使用,并且不能保证函数签名在版本之间保持不变。话虽如此,你做得很好! - Twirrim
@Twirrim:谢谢,我知道我上次编辑帖子时忘记了一些东西。我重新措辞,让它更好地反映现实情况。 - CristiFati
在不允许使用加密模块的环境中,将SSL证书解码为文本的好方法。虽然不确定是否调用内部SSL函数。 - Aby Sam Ross
@AbySamRoss:ssl是一个加密模块(它只是Python标准库的一部分)。它将实际工作委托给OpenSSL(或其他类似的加密提供者)。所有其他模块都是这样做的。 - CristiFati
1
我知道这是被禁止的果实,但这太棒了。多亏了你,我能够编写一个Python库/脚本,可以下载整个证书链,只要证书上配置了“caIssuers”扩展。https://gist.github.com/flisboac/48762e176061b520a606868bc4ce089f。非常感谢你的这个想法! - Flávio Lisbôa

5
这段代码会输出证书文件的内容:
import OpenSSL.crypto

cert = OpenSSL.crypto.load_certificate(
      OpenSSL.crypto.FILETYPE_PEM,
      open('/path/to/cert/file.crt').read()
)

print OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_TEXT, cert)

试一下吧。


5

这允许从SSL证书中提取特定的值:

from cryptography import x509
from cryptography.hazmat.backends import default_backend

hostname = 'google.com.com'
port = 443
cert = ssl.get_server_certificate((hostname, port))
certDecoded = x509.load_pem_x509_certificate(str.encode(cert), 
default_backend())
print(certDecoded.issuer)
print(certDecoded.subject)
print(certDecoded.not_valid_after)
print(certDecoded.not_valid_before)

1
这对我有效。如果您用于本地文件,请将其作为二进制文件打开:with open(cert_filepath, 'br') as cert_content: cert_data = cert_content.read() cert_decoded = x509.load_pem_x509_certificate(cert_data, default_backend()) - Lethargos

2
有另一种方法可以使用_test_decode_certificate而不使用内部实现。不过,这种方法有点不正当。
import ssl

ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

# The filepath to your PEM-encoded x509 cert
ctx.load_verify_locations("369fa1ef21f5476c02814c637d83f71d851f867348eef21d1eb0058671d0e5a6.crt")

certificate_details = ctx.get_ca_certs()

在代码中,这是另一个进入函数_decode_certificate的入口点,_test_decode_certificate也使用了该函数。
你可以在CPython源代码https://github.com/python/cpython/blob/main/Modules/_ssl.c#L4578中看到它的工作原理。

如果您无法安装任何第三方库,我认为最好的答案是使用ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)来避免弃用警告。 - hoefling
@hoefling 感谢你的提示。我一段时间前更新了答案。 - nijave

1

您可以从这里下载代码。它仅从.pem和.cer类型的证书中提取数据。

否则,可以使用以下片段解码pem证书:

    #import pem & pyOpenSSL module

    certs = pem.parse_file(file_path)  # using pem module
    for pem_certificates in certs:
        strcert = str(pem_certificates)
        loadCert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,strcert)
        print(loadCert.get_issuer())```

 

0
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
import os

with open("es01.pem", "rb") as f:
    cert= x509.load_pem_x509_certificate(f.read(), default_backend())

print (f"Serial Number: {cert.serial_number}")
print (f"Extensions: {cert.extensions}")

fingerprint_hashed_byte_string = cert.fingerprint(hashes.SHA256())
fingerprint_hex_string = ':'.join([format(byte, '02x') for byte in fingerprint_hashed_byte_string])
print (f"Fingerprint: {fingerprint_hex_string}")
print (f"Issuer: {cert.issuer}")
print (f"Not Valid Before: {cert.not_valid_before}")
print (f"Not Valid After: {cert.not_valid_after}")

"""
OR SIMPLY USING OPENSSL...

openssl x509 -in es01.pem -noout -serial
serial=12D498EEF25477293EE0CB9A287E530FE2466EF2

openssl x509 -noout -fingerprint -sha256 -inform pem -in es01.pem
sha256 Fingerprint=69:B5:89:3E:7C:9F:D8:70:B1:B5:55:97:B3:87:43:CB:3F:55:D1:0E:8E:D6:58:9B:19:7E:CA:F6:B3:F5:17:50

openssl x509 -noout -ext basicConstraints -inform pem -in es01.pem
X509v3 Basic Constraints: 
    CA:FALSE

openssl x509 -in es01.pem -noout -ext subjectAltName
X509v3 Subject Alternative Name: 
    DNS:es01, IP Address:127.0.0.1, DNS:localhost

openssl x509 -in es01.pem -noout -startdate
notBefore=Apr 18 15:36:36 2023 GMT

openssl x509 -in es01.pem -noout -enddate
notAfter=Apr 17 15:36:36 2025 GMT

openssl x509 -in es01.pem -noout -issuer
issuer=C = US, ST = Minneosta, L = Bloomington, O = TEST-CA, OU = CERTS, CN = INTERMEDIATE-CA
"""

你的回答可以通过提供更多支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人能够确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

-3

我不确定你是怎么得到它的,但另一种简单的安装方法是将其写成二进制文件,然后使用os运行它。

import os

cert= function_gives_binary_cert()
with open('RecvdCert.der','wb') as file:
     file.write(cert)

os.startfile('RecvdCert.der')

小心运行来自未知来源的二进制文件。如果只是想解码,请使用其他答案中提到的 OpenSSL。


os.startfile仅适用于Windows系统,它不会执行文件,而是使用应用程序打开它。此外,证书不是可执行文件。os.startfile的结果取决于已注册以打开文件扩展名的应用程序。这类似于在Windows shell上双击文件。 - borellini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接