如何在Python中验证/验证X509证书的信任链?

15

我正在开发一个利用API的Web应用程序。在响应中,API服务器会发送一个链接到一个X509证书(以PEM格式组成签名证书和一个或多个中间证书到根CA证书),我必须下载并使用它进行进一步的验证。

在使用证书之前,我需要确保证书链中的所有证书合并在一起创建了到受信任的根CA证书的信任链(以检测和避免任何恶意请求)。但我在Python中很难做到这一点,对于这个问题的研究也没有得到有用的结果。

使用requests和M2Crypto可以轻松地获取和加载证书。

import requests
from M2Crypto import RSA, X509

mypem = requests.get('https://server.com/my_certificate.pem')   
cert = X509.load_cert_string(str(mypem.text), X509.FORMAT_PEM)

然而,验证证书链是一个问题。我不能将证书写入磁盘以使用类似于子进程的命令行实用程序(如openssl),因此必须通过Python完成。我也没有任何打开的连接,因此使用基于连接的验证解决方案(如在此答案/线程中提到的:https://dev59.com/TXNA5IYBdhLWcg3wHp-B#1088224)也不起作用。

在关于此问题的另一个线程(https://dev59.com/l2855IYBdhLWcg3wViu3#4427081)中,abbot解释说m2crypto无法进行此验证,并说他编写了一个扩展来允许验证(使用模块m2ext),但他的修补程序似乎从不起作用,即使我知道它是有效的:

from m2ext import SSL
ctx = SSL.Context()
ctx.load_verify_locations(capath='/etc/ssl/certs/') # I have run c_rehash in this directory to generate a list of cert files with signature based names
if not ctx.validate_certificate(cert): # always happens
    print('Invalid certificate!') 

此外,在类似的线程中,这里还有一个答案https://dev59.com/I03Sa4cB1Zd3GeqPsiSi#9007764,John Matthews声称他写了一个可以做到这一点的补丁,但不幸的是,该补丁链接现已失效--而且在那个线程中有一个评论说该补丁不能与openssl 0.9.8e一起使用。

所有与在Python中验证证书链信任相关的回答似乎要么链接到无效的补丁,要么返回到m2ext

是否有一种简单、直接的方法来验证我的证书链信任在Python中?


顺便提一下,pyopenssl包装了openssl,因此您不需要从命令行使用它。如果您还没有阅读过,这也值得一读:https://www.python.org/dev/peps/pep-0476/ - Avi Das
谢谢链接,Avi。我之前没有看到过这个。然而,它对我当前的困境没有帮助(似乎它是关于http客户端的)。如果需要,我没有使用命令行程序进行验证的问题——但不是在每个请求时都需要将pem文件写入磁盘。如果openssl verify命令可以接受原始字符串,那么我可以使用它(尽管它似乎是对我认为在Python中肯定是微不足道的事情的一个hacky解决方法)。 - speznot
4个回答

17

虽然Avi Das的回答对于验证单个信任锚点和单个叶证书的微不足道的情况是有效的,但它会让中间证书得到信任。这意味着在发送中间证书以及客户端证书的情况下,整个链都是可信的。

不要这样做。 pyOpenSSL测试中发现的代码存在缺陷!

我在Python的cryptography-dev邮件列表上找到了这个主题(链接返回到这个答案):https://mail.python.org/pipermail/cryptography-dev/2016-August/000676.html

我们可以看到这段代码没有区分根证书和中间证书。如果我们查看文档,add_cert本身就添加了一个受信任的证书(也许add_trusted_cert会是一个更好的名称?)。

它包括有关为什么这是一个可怕的想法的示例。我无法强调这一点:通过信任中间证书来验证您的链类似于根本不执行任何检查。


话虽如此,那么在Python中应该如何验证证书链呢?我找到的最佳替代方法是https://github.com/wbond/certvalidator,它似乎可以胜任此工作。

还有一些存在缺陷的替代方案:

这是一些受人尊敬的Python加密库的当前状态:

这两个帖子目前看起来都没有更新了。

我知道这不符合问题的情况,但: 如果您使用TLS套接字中的证书验证构建某些东西,请使用Python中已有的模块。永远不要重新发明轮子,特别是涉及加密学。 加密很难; 它唯一容易的事情就是搞砸它。


https://github.com/pyca/pyopenssl/pull/948 是 473 的更新 PR,并已合并。因此,也许可以安全地使用 chain 参数与 X509StoreContext - Tim Tisdall
下面可以找到一个带有chainX509StoreContext示例:https://dev59.com/tV0a5IYBdhLWcg3wHlek#70643492。 - Tim Tisdall

3

我研究了pyopenssl库,并发现以下内容可用于证书链验证。下面的示例取自他们的测试,似乎可以实现您想要的功能,即验证信任链到受信任的根证书。这里是X509Store和X509StoreContext的相关文档

from OpenSSL.crypto import load_certificate, load_privatekey
from OpenSSL.crypto import X509Store, X509StoreContext
from six import u, b, binary_type, PY3
root_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIIC7TCCAlagAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UE
BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU
ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwIhgPMjAwOTAzMjUxMjM2
NThaGA8yMDE3MDYxMTEyMzY1OFowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklM
MRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9U
ZXN0aW5nIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPmaQumL
urpE527uSEHdL1pqcDRmWzu+98Y6YHzT/J7KWEamyMCNZ6fRW1JCR782UQ8a07fy
2xXsKy4WdKaxyG8CcatwmXvpvRQ44dSANMihHELpANTdyVp6DCysED6wkQFurHlF
1dshEaJw8b/ypDhmbVIo6Ci1xvCJqivbLFnbAgMBAAGjgbswgbgwHQYDVR0OBBYE
FINVdy1eIfFJDAkk51QJEo3IfgSuMIGIBgNVHSMEgYAwfoAUg1V3LV4h8UkMCSTn
VAkSjch+BK6hXKRaMFgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UE
BxMHQ2hpY2FnbzEQMA4GA1UEChMHVGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBS
b290IENBggg9DMTgxt659DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB
AGGCDazMJGoWNBpc03u6+smc95dEead2KlZXBATOdFT1VesY3+nUOqZhEhTGlDMi
hkgaZnzoIq/Uamidegk4hirsCT/R+6vsKAAxNTcBjUeZjlykCJWy5ojShGftXIKY
w/njVbKMXrvc83qmTdGl3TAM0fxQIpqgcglFLveEBgzn
-----END CERTIFICATE-----
""")
intermediate_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIICVzCCAcCgAwIBAgIRAMPzhm6//0Y/g2pmnHR2C4cwDQYJKoZIhvcNAQENBQAw
WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAw
DgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwHhcNMTQw
ODI4MDIwNDA4WhcNMjQwODI1MDIwNDA4WjBmMRUwEwYDVQQDEwxpbnRlcm1lZGlh
dGUxDDAKBgNVBAoTA29yZzERMA8GA1UECxMIb3JnLXVuaXQxCzAJBgNVBAYTAlVT
MQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU2FuIERpZWdvMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDYcEQw5lfbEQRjr5Yy4yxAHGV0b9Al+Lmu7wLHMkZ/ZMmK
FGIbljbviiD1Nz97Oh2cpB91YwOXOTN2vXHq26S+A5xe8z/QJbBsyghMur88CjdT
21H2qwMa+r5dCQwEhuGIiZ3KbzB/n4DTMYI5zy4IYPv0pjxShZn4aZTCCK2IUwID
AQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAPIWSkLX
QRMApOjjyC+tMxumT5e2pMqChHmxobQK4NMdrf2VCx+cRT6EmY8sK3/Xl/X8UBQ+
9n5zXb1ZwhW/sTWgUvmOceJ4/XVs9FkdWOOn1J0XBch9ZIiFe/s5ASIgG7fUdcUF
9mAWS6FK2ca3xIh5kIupCXOFa0dPvlw/YUFT
-----END CERTIFICATE-----
""")
untrusted_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIICWDCCAcGgAwIBAgIRAPQFY9jfskSihdiNSNdt6GswDQYJKoZIhvcNAQENBQAw
ZjEVMBMGA1UEAxMMaW50ZXJtZWRpYXRlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT
CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh
biBEaWVnbzAeFw0xNDA4MjgwMjEwNDhaFw0yNDA4MjUwMjEwNDhaMG4xHTAbBgNV
BAMTFGludGVybWVkaWF0ZS1zZXJ2aWNlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT
CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh
biBEaWVnbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqpJZygd+w1faLOr1
iOAmbBhx5SZWcTCZ/ZjHQTJM7GuPT624QkqsixFghRKdDROwpwnAP7gMRukLqiy4
+kRuGT5OfyGggL95i2xqA+zehjj08lSTlvGHpePJgCyTavIy5+Ljsj4DKnKyuhxm
biXTRrH83NDgixVkObTEmh/OVK0CAwEAATANBgkqhkiG9w0BAQ0FAAOBgQBa0Npw
UkzjaYEo1OUE1sTI6Mm4riTIHMak4/nswKh9hYup//WVOlr/RBSBtZ7Q/BwbjobN
3bfAtV7eSAqBsfxYXyof7G1ALANQERkq3+oyLP1iVt08W1WOUlIMPhdCF/QuCwy6
x9MJLhUCGLJPM+O2rAPWVD9wCmvq10ALsiH3yA==
-----END CERTIFICATE-----
""")

root_cert = load_certificate(FILETYPE_PEM, root_cert_pem)
intermediate_cert = load_certificate(FILETYPE_PEM, intermediate_cert_pem)
untrusted_cert = load_certificate(FILETYPE_PEM, untrusted_cert_pem)
store = X509Store()
store.add_cert(root_cert)
store.add_cert(intermediate_cert)
store_ctx = X509StoreContext(store, untrusted_cert)
print(store_ctx.verify_certificate())

所以,我无法让这个例子工作--我错过了什么吗?verify_cert调用总是返回None(对于您的示例提供的证书和我自己测试的证书)。我不得不在顶部添加FILETYPE_PEM导入,以及从OpenSSL.crypto导入的其他导入。我还尝试删除您证书字符串中多余的换行符(在“END CERTIFICATE”行之后),但它仍然返回none。非常感谢您对此的想法和时间! - speznot
啊!我错过了文档中直接对我说None是有效响应的注释——抱歉。我一直在尝试使用自己的证书进行测试,但出现了“无法获取[本地]颁发者证书”的错误。在命令行中,我使用类似以下内容的命令成功验证:openssl verify -untrusted intermediate_cert.pem -CAfile rootcert.pem tovalidate.pem(如果没有-untrusted开关,则会失败,并显示类似我看到的错误)——您的示例中是否正确,即intermediate_server_cert是我要验证的证书? - speznot
1
正确的,intermediate_server_cert是在这个例子中被验证的证书。我认为这个错误通常意味着某个证书在链中丢失了。不过很奇怪,我尝试了另一个例子,它却成功地解决了。 - Avi Das
非常感谢您的帮助 @Avi。我仍在努力,试图克服那个错误。如果您愿意尝试它,我已经在这里粘贴了我尝试验证的证书http://pastebin.com/z9TbDPVi,我将不胜感激。 - speznot
5
你说得对,这个回答完全是错误的。相信中间证书是一个非常、非常糟糕的想法。我添加了一个正确的答案,或者至少更少出错。 - ralphje
显示剩余3条评论

2
保罗-阿曼德和愚蠢的链文章都给出了非常棒的答案。这对我很有用。注意:我需要适当地设置商店的验证日期。
然而,我找到了一种避免将不受信任的证书添加为信任证书的方法。我查看了 pyopenssl 代码(链接在此https://github.com/pyca/pyopenssl/blob/main/src/OpenSSL/crypto.py),发现 X509StoreContext 有一个 "chain" 参数,所以我尝试将中间证书放在那里。
# .... paste Paul-Armand's example here ....

store = OpenSSL.crypto.X509Store()
store.add_cert(root_cert)

# Set time to reuse certs from Paul-Armand's example
validation_date = datetime.datetime.strptime("2016-01-01", "%Y-%m-%d")
store.set_time(validation_date)

# This is the change, added param [intermediate_cert]
store_ctx = OpenSSL.crypto.X509StoreContext(store, untrusted_cert, [intermediate_cert])

print("Untrusted chain example")
try:
    store_ctx.verify_certificate()
    print("Verify - OK")
except OpenSSL.crypto.X509StoreContextError:
    print("Verify failed - bad")

这段话的意思是:对我来说,所有东西都验证通过了,并且更接近我在命令行中使用 OpenSSL 时的方式(-untrusted intermediate.pem)。但我真希望在2019年1月就弄清楚了 pyopenssl,那样就可以省去多年使用“certvalidate” Python替代方案的时间了。我重新审视了一下,因为我正在转向使用EdDSA25519密钥,而 certvalidate 不支持它们。

2

我使用了https://duo.com/labs/research/chain-of-fools上的卓越解释来修改之前avi_das的解决方案,并在代码中添加了一些注释。基本上,我们只需要在证书受信任时(例如根证书)或被其他证书验证/受信任时(例如中间证书)将证书添加到存储中。您不能一次将所有证书添加到存储中,因为您需要在该时刻使用存储中的正确证书验证链中的每个证书。以下是代码注释的具体说明。

import OpenSSL
from six import u, b, binary_type, PY3

root_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIIC7TCCAlagAwIBAgIIPQzE4MbeufQwDQYJKoZIhvcNAQEFBQAwWDELMAkGA1UE
BhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdU
ZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwIhgPMjAwOTAzMjUxMjM2
NThaGA8yMDE3MDYxMTEyMzY1OFowWDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklM
MRAwDgYDVQQHEwdDaGljYWdvMRAwDgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9U
ZXN0aW5nIFJvb3QgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPmaQumL
urpE527uSEHdL1pqcDRmWzu+98Y6YHzT/J7KWEamyMCNZ6fRW1JCR782UQ8a07fy
2xXsKy4WdKaxyG8CcatwmXvpvRQ44dSANMihHELpANTdyVp6DCysED6wkQFurHlF
1dshEaJw8b/ypDhmbVIo6Ci1xvCJqivbLFnbAgMBAAGjgbswgbgwHQYDVR0OBBYE
FINVdy1eIfFJDAkk51QJEo3IfgSuMIGIBgNVHSMEgYAwfoAUg1V3LV4h8UkMCSTn
VAkSjch+BK6hXKRaMFgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJJTDEQMA4GA1UE
BxMHQ2hpY2FnbzEQMA4GA1UEChMHVGVzdGluZzEYMBYGA1UEAxMPVGVzdGluZyBS
b290IENBggg9DMTgxt659DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GB
AGGCDazMJGoWNBpc03u6+smc95dEead2KlZXBATOdFT1VesY3+nUOqZhEhTGlDMi
hkgaZnzoIq/Uamidegk4hirsCT/R+6vsKAAxNTcBjUeZjlykCJWy5ojShGftXIKY
w/njVbKMXrvc83qmTdGl3TAM0fxQIpqgcglFLveEBgzn
-----END CERTIFICATE-----
""")
intermediate_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIICVzCCAcCgAwIBAgIRAMPzhm6//0Y/g2pmnHR2C4cwDQYJKoZIhvcNAQENBQAw
WDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAklMMRAwDgYDVQQHEwdDaGljYWdvMRAw
DgYDVQQKEwdUZXN0aW5nMRgwFgYDVQQDEw9UZXN0aW5nIFJvb3QgQ0EwHhcNMTQw
ODI4MDIwNDA4WhcNMjQwODI1MDIwNDA4WjBmMRUwEwYDVQQDEwxpbnRlcm1lZGlh
dGUxDDAKBgNVBAoTA29yZzERMA8GA1UECxMIb3JnLXVuaXQxCzAJBgNVBAYTAlVT
MQswCQYDVQQIEwJDQTESMBAGA1UEBxMJU2FuIERpZWdvMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDYcEQw5lfbEQRjr5Yy4yxAHGV0b9Al+Lmu7wLHMkZ/ZMmK
FGIbljbviiD1Nz97Oh2cpB91YwOXOTN2vXHq26S+A5xe8z/QJbBsyghMur88CjdT
21H2qwMa+r5dCQwEhuGIiZ3KbzB/n4DTMYI5zy4IYPv0pjxShZn4aZTCCK2IUwID
AQABoxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAPIWSkLX
QRMApOjjyC+tMxumT5e2pMqChHmxobQK4NMdrf2VCx+cRT6EmY8sK3/Xl/X8UBQ+
9n5zXb1ZwhW/sTWgUvmOceJ4/XVs9FkdWOOn1J0XBch9ZIiFe/s5ASIgG7fUdcUF
9mAWS6FK2ca3xIh5kIupCXOFa0dPvlw/YUFT
-----END CERTIFICATE-----
""")
untrusted_cert_pem = b("""-----BEGIN CERTIFICATE-----
MIICWDCCAcGgAwIBAgIRAPQFY9jfskSihdiNSNdt6GswDQYJKoZIhvcNAQENBQAw
ZjEVMBMGA1UEAxMMaW50ZXJtZWRpYXRlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT
CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh
biBEaWVnbzAeFw0xNDA4MjgwMjEwNDhaFw0yNDA4MjUwMjEwNDhaMG4xHTAbBgNV
BAMTFGludGVybWVkaWF0ZS1zZXJ2aWNlMQwwCgYDVQQKEwNvcmcxETAPBgNVBAsT
CG9yZy11bml0MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEjAQBgNVBAcTCVNh
biBEaWVnbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqpJZygd+w1faLOr1
iOAmbBhx5SZWcTCZ/ZjHQTJM7GuPT624QkqsixFghRKdDROwpwnAP7gMRukLqiy4
+kRuGT5OfyGggL95i2xqA+zehjj08lSTlvGHpePJgCyTavIy5+Ljsj4DKnKyuhxm
biXTRrH83NDgixVkObTEmh/OVK0CAwEAATANBgkqhkiG9w0BAQ0FAAOBgQBa0Npw
UkzjaYEo1OUE1sTI6Mm4riTIHMak4/nswKh9hYup//WVOlr/RBSBtZ7Q/BwbjobN
3bfAtV7eSAqBsfxYXyof7G1ALANQERkq3+oyLP1iVt08W1WOUlIMPhdCF/QuCwy6
x9MJLhUCGLJPM+O2rAPWVD9wCmvq10ALsiH3yA==
-----END CERTIFICATE-----
""")

# load certificates
root_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, root_cert_pem)
intermediate_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, intermediate_cert_pem)
untrusted_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, untrusted_cert_pem)

# Trust the root certificate
store = OpenSSL.crypto.X509Store()
store.add_cert(root_cert)

# only add intermediate if it can be verified by the root
store_ctx = OpenSSL.crypto.X509StoreContext(store, intermediate_cert)
print(store_ctx.verify_certificate())
store.add_cert(intermediate_cert)

# now that root and intermediate are trused, you can verify the end certificate using the store
store_ctx = OpenSSL.crypto.X509StoreContext(store, untrusted_cert)
print(store_ctx.verify_certificate())

这个对我来说非常有效。 - lauc.exon.nod

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