【问题标题】:How to validate / verify an X509 Certificate chain of trust in Python?如何在 Python 中验证/验证 X509 证书信任链?
【发布时间】:2015-06-08 02:03:33
【问题描述】:

我正在实现一个使用 API 的 Web 应用程序。在响应期间,API 服务器通过 X509 证书(PEM 格式,由签名证书和一个或多个指向根 CA 证书的中间证书组成)的链接发送我必须下载并使用该证书进行进一步验证。

在使用证书之前,我需要确保链中的所有证书结合起来创建一个信任链到受信任的根 CA 证书(以检测和避免任何恶意请求)。我在 python 中很难做到这一点,我对该主题的研究没有产生任何有用的东西。

使用请求和 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://stackoverflow.com/a/1088224/4984533)也不起作用。

在关于这个问题的另一个线程上(https://stackoverflow.com/a/4427081),方丈解释说 m2crypto 无法进行此验证,并说他已经编写了一个扩展来允许验证(使用模块 m2ext),但他的补丁似乎从来没有工作, 即使我知道它是有效的,也总是返回 false:

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://stackoverflow.com/a/9007764/4984533 的一个类似线程上也有这个答案,其中 John Matthews 声称编写了一个补丁可以做到这一点,但不幸的是补丁链接现在已经失效——无论如何,该线程上有一条评论说明该补丁不适用于 openssl 0.9.8e。

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

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

【问题讨论】:

  • Btw pyopenssl 包装了 openssl,因此您无需从命令行使用它。如果您还没有python.org/dev/peps/pep-0476,也值得一读
  • 感谢 Avi 的链接,我以前没见过。然而,它对我目前的困境没有帮助(它似乎特别是关于 http 客户端)。如有必要,我使用命令行程序进行验证没有问题——但如果我需要在每次请求时将 pem 文件写入磁盘,则没有问题。如果 openssl verify 命令可以接收原始字符串,那么我可以使用我想的(尽管对于我认为在 Python 中肯定是微不足道的事情来说,这似乎是一个 hacky 解决方法)。

标签: python validation ssl m2crypto


【解决方案1】:

虽然 Avi Das 的响应对于使用单个叶证书验证单个信任锚的简单情况是有效的,但 它将信任置于中间证书。这意味着在发送中间证书以及客户端证书的情况下,整个链都是可信的。

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

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

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

它包含了为什么这是一个糟糕的想法的示例。我怎么强调都不为过:通过信任中间体来验证你的链类似于根本不执行任何检查


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

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

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

此时两个线程似乎都过时了。

我知道这与问题的情况不符,但是:如果您要在 TLS 套接字中使用证书验证来构建某些东西,只需使用 Python 中已有的模块即可。 永远不要重新发明轮子,特别是关于密码学。加密很难;唯一简单的事情就是把它搞砸。

【讨论】:

    【解决方案2】:

    我查看了 pyopenssl 库,发现它用于证书链验证。以下示例来自their tests,似乎在做您想做的事,即验证信任链到受信任的根证书。这是X509Store and 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”行之后),但它仍然没有返回。非常感谢您对此的想法和时间!
    • 啊!我错过了该文档中直接盯着我的评论说“无”是一个有效的回应——我很抱歉。我一直在尝试使用自己的证书进行测试,但出现“无法获得 [本地] 颁发者证书”错误。在命令行上,我正在使用类似这样的东西来成功验证:openssl verify -untrusted intermediate_cert.pem -CAfile rootcert.pem tovalidate.pem(没有 -untrusted 开关,它会失败并出现类似的错误)——在你的示例中intermediate_server_cert 是我的证书是否正确验证?
    • 正确,intermediate_server_cert 是在此示例中得到验证的证书。我认为该错误通常意味着链中某处缺少特定证书。不过这很奇怪,我尝试了一个不同的例子,它确实为我解决了。
    • 非常感谢@Avi 的帮助。我仍在努力,试图克服这个错误。我已将我正在尝试验证的证书粘贴到此处pastebin.com/z9TbDPVi,如果您愿意尝试,我将不胜感激。
    • 你是对的,这是一个完全错误的答案。 信任中间证书是一个非常非常糟糕的主意。我添加了一个正确的答案,或者至少不那么不正确。
    【解决方案3】:

    使用https://duo.com/labs/research/chain-of-fools上的优秀解释改编之前的解决方案(来自avi_das),并在代码中添加了一些cmets。基本上,我们只需要在证书受信任(例如根证书)或被另一个证书(例如中间证书)验证/信任时才将证书添加到存储中。您不能一次性将所有证书添加到存储中,因为此时您需要使用存储中的正确证书验证链中的每个证书。下面的代码 cmets 没有那么抽象。

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

    【讨论】:

      【解决方案4】:

      Paul-Armand 的精彩回答和傻瓜链文章。这对我有用。注意:我需要适当地设置商店的验证日期。

      但是,我找到了一种避免将不受信任的证书添加为受信任的方法。我检查了 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 不支持它们。

      【讨论】:

        猜你喜欢
        • 2011-04-06
        • 2013-01-28
        • 1970-01-01
        • 1970-01-01
        • 2012-09-28
        • 1970-01-01
        • 2011-11-11
        • 2020-05-23
        • 2013-12-20
        相关资源
        最近更新 更多