由于 AKI DirName 扩展而在 openSSL 中发现根本原因:Python [SSL: CERTIFICATE_VERIFY_FAILED] ...: 无法获取本地颁发者证书

发布于 2025-01-11 10:00:29 字数 7437 浏览 0 评论 0 原文

简而言之,

我得到:[SSL: CERTIFICATE_VERIFY_FAILED] 证书验证失败:无法获取本地颁发者证书 (_ssl.c:1129)

[SSL: CERTIFICATE_VERIFY_FAILED] 证书更糟糕的错误验证失败:证书链中的自签名证书 (_ssl.c:1129)

我怀疑 python 或我自己的代码,但相同纯 openSSL 会出现错误:

openssl s_client -connect my-domain.com:443 -CAfile root.pem -verify 2

   ......

    Verify return code: 21 (unable to verify the first certificate)
    Extended master secret: no
    Max Early Data: 0
---
......

如果 openSSL 不起作用,python 也将不起作用。可以肯定这是针对 TLS 握手时出现的任何错误。 ;-)

比较新旧证书,最终在两个新的附加Authority Key Identifier条目DirNameserial中找到了根本原因。它们以前没有出现过:

openssl x509 -in my-domain.pem -text

The only diff and root cause was Authority Key Identifier with two new entries:

X509v3 Authority Key Identifier:
    keyid:1D:E8:38:95:85:65:A2:D9:44:99:96:30:D1:81:D5:5B:F7:38:CC:8C
    DirName:/C=DE/O=My Company/OU=My OU/CN=My Sub-CA
    serial:02

新的也是最后一个问题:为什么 openSSL 不能与 AKI 中的 DirNameserial 一起使用?


我保留了所有最初的东西,因为这可能会帮助其他人构建灵活的 python 代码......

现在,我下面的所有方法都工作正常,正如预期的那样。

如果 CA 不在信任库中,我会收到预期的 python 错误:
[SSL: CERTIFICATE_VERIFY_FAILED] 证书验证失败:证书链中的自签名证书 (_ssl.c:1129)
在上下文、会话或请求级别设置信任库,采取你想要的,它对我有用。

过去 2 年的情况

过去(2 年前)我将我的私有 CA(根和子 ca)导入到 Win 10 本地计算机信任库
并使用此 代码片段 创建我自己的 urllib3 上下文来加载使用 context.load_default_certs() 的 Windows 商店

一直有效,直到本周,我必须更新我的 CA 和服务器证书。 :-(
我删除了旧的私有 CA 捆绑包,并将新的 CA 像往常一样导入到 Windows 本地计算机信任库和 Firefox 信任库中。
所有浏览器都可以在新的信任库中正常工作!

更新我的私有 CA 后出现新问题

使用 python 请求

,我现在再次遇到这些恼人的错误:步骤代码中的 SSLError - 消息:HTTPSConnectionPool(host='my-domain.com', port=443): Max retries gone url: / (由 SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] 证书验证失败:无法获取本地颁发者证书(_ssl.c:1129)')))

我的代码方法

我当前的代码尝试了不同的方法来解决该问题,但没有任何效果。
我确定(或希望)故障在我这边,但我不再看到它了;-)

import certifi
import json
import os
import requests

    class MyHttpAdapter(HTTPAdapter):
        def init_poolmanager(self, *args, **kwargs):
            # Google: "SSL failure on Windows using python requests" -- usage of Windows Truststore
            # https://stackoverflow.com/questions/42981429/ssl-failure-on-windows-using-python-requests
            context = create_urllib3_context()

我尝试了这 3 个设置来加载不同的 ca 捆绑包(每次尝试只有一个代码行处于活动状态)

            ## --------------------
            # variant 1: usage of Windows Truststore where I imported my root-ca and sub-ca into local computer storage
            # load_default_certs() --> this loads the OS defaults of Windows Truststore!!!!
            context.load_default_certs()
            
            # variant 2: my special ca bundle contains only my private CA only with 3 entries: root-ca, sub-ca and server cert
            # load_verify_locations() --> this loads just a specific CA bundle !!!!
            context.load_verify_locations(cafile="C:/Users/...../my_ca_bundle.pem")
            
            # variant 3: I added my private CA with root-ca, sub-ca and server cert to certifi cacert.pem file as a hack
            # certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
            # load_verify_locations() --> this loads just a specific CA bundle !!!!
            context.load_verify_locations(cafile=certifi.where())
            ## --------------------

以验证我的私有 CA 位于所选存储内,我在 CA 的 CommonName 上执行 assert,因此如果找不到,它会快速失败。如果没问题,稍后会将上下文添加到 session.mount 中:

            # get and print all CAs from loaded list to verify that my CA is within the list
            # notice the server certificate of pem file is not loaded into the list, don't no why, maybe because it's no CA
            json_ca_certs = context.get_ca_certs()
            print("All current context CA certs: {}".format(json.dumps(json_ca_certs, indent=1, sort_keys=True)))
            length=len(json_ca_certs)
            print("Number of certificates in bundle: {}".format(length))

            # fail fast:
            assert "MY CommonName" in str(json_ca_certs), "private CA not found in used bundle"

            kwargs['ssl_context'] = context
            return super().init_poolmanager(*args, **kwargs)

我没有设置这些环境变量,它们是 None

    def __init__(self, **kwargs):

        print("REQUESTS_CA_BUNDLE: '{}'".format(os.environ.get('REQUESTS_CA_BUNDLE')))
        print("CURL_CA_BUNDLE: '{}'".format(os.environ.get('CURL_CA_BUNDLE')))

我创建了一个会话,并且使用我的信任库创建上下文不再有效,我尝试了 3 种不同的方法来使用 session.verify(每次尝试仅激活一个代码行):

        self.session = requests.Session()

        # variant 1: default not working anymore:
        self.session.verify = True

        # variant 2: usage of my special ca bundle does not work
        self.session.verify = "C:/Users/...../my_ca_bundle.pem"

        # variant 3: certifi cacert.pem file contains my private CA and does not work
        # certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
        self.session.verify = certifi.where()

在这里,我最终将适配器安装到会话中:

        # mount with Schema only as prefix to use them for all my calls (this worked in past and should not be a new problem)
        adapter = self.MyHttpAdapter()
        self.session.mount("https://", adapter)

现在我执行 self.session.get() 调用。我再次尝试了这些设置,因为 context cafilesession.verify 都可以工作,直接在会话中的requests

    _api_url = 'https://my-domain.com/api/function'
    
    ###

    # variant 1: does not longer work, no setting above for session.verify nor context.load...():
    response = self.session.get(_api_url)

    # variant 2: verify = my special ca bundle work around does not work:
    response = self.session.get(_api_url, verify="C:/Users/...../my_ca_bundle.pem")
    
    # variant 3: verify = certifi cacert.pem work around does not work:
    response = self.session.get(_api_url, verify=certifi.where())

我添加到请求/会话中。 py 一些打印调试输出,看看使用了什么
会话 request() 使用的最终设置:

        settings = self.merge_environment_settings(
            prep.url, proxies, stream, verify, cert
        )
        print(f"DEBUG: request settings: {settings}")

sessions merge_environment_settings() 就在 return () 之前

        print(f"DEBUG: merge_environment_settings() before return: verify={verify}")
        return {'verify': verify, 'proxies': proxies, 'stream': stream,
                'cert': cert}

只是为了看看我的设置是否被使用:

DEBUG: merge_environment_settings() before return: verify=path_to_ca_bundle.pem
DEBUG: request settings: {'verify': 'path_to_ca_bundle.pem', 'proxies': OrderedDict(), 'stream': False, 'cert': None}

Python 和安装的模块是最新的:
Python 3.9.6(标签/v3.9.6:db3ff76,2021 年 6 月 28 日,15:26:21)
证书==2021.10.8
请求==2.27.1
请求工具带==0.9.1

in short

I got: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

what is a much worse error than [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1129)

And I suspected python or my own code, but the same error occurs with pure openSSL:

openssl s_client -connect my-domain.com:443 -CAfile root.pem -verify 2

   ......

    Verify return code: 21 (unable to verify the first certificate)
    Extended master secret: no
    Max Early Data: 0
---
......

If openSSL does not work, python will not work, too. Fore sure this is for any error at TLS handshake. ;-)

Comparing old and new certificates, the root cause was finally found in two new addtional Authority Key Identifier entries DirName and serial. They where not present before:

openssl x509 -in my-domain.pem -text

The only diff and root cause was Authority Key Identifier with two new entries:

X509v3 Authority Key Identifier:
    keyid:1D:E8:38:95:85:65:A2:D9:44:99:96:30:D1:81:D5:5B:F7:38:CC:8C
    DirName:/C=DE/O=My Company/OU=My OU/CN=My Sub-CA
    serial:02

New and final question: Why openSSL doesn't work with DirName and serial within AKI?


I keep all the initial stuff, because that might help other to build up flexible python code.....

Now, all my approaches below are working fine, as expected.

If CA ist not in truststore I get the expected python error:
[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1129)
Setting truststore at context, session or requests level, take what you want, it works for me.

situation last 2 years

In past (2 years ago) I imported my private CA (root and sub-ca) into Win 10 local computer truststore
and used this code snippet to create my own urllib3 context to load the Windows store with context.load_default_certs()

That worked until this week, where I had to renew my CA and server certificate. :-(
I deleted old private CA bundle and imported new CA into Windows local computer truststore and firefox truststore as usual.
All browser are working fine with new truststore!

new Problem after update of my private CA

With python requests I now get again these annoying errors:

SSLError in steps code - Message: HTTPSConnectionPool(host='my-domain.com', port=443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)')))

my approaches in code

My current code tries different ways to solve the issue, but nothing works.
I'm sure (or hope) the fault is at my side, but I don't see it anymore ;-)

import certifi
import json
import os
import requests

    class MyHttpAdapter(HTTPAdapter):
        def init_poolmanager(self, *args, **kwargs):
            # Google: "SSL failure on Windows using python requests" -- usage of Windows Truststore
            # https://stackoverflow.com/questions/42981429/ssl-failure-on-windows-using-python-requests
            context = create_urllib3_context()

I tried those 3 settings to load different ca bundles (only one code line is active per attempt)

            ## --------------------
            # variant 1: usage of Windows Truststore where I imported my root-ca and sub-ca into local computer storage
            # load_default_certs() --> this loads the OS defaults of Windows Truststore!!!!
            context.load_default_certs()
            
            # variant 2: my special ca bundle contains only my private CA only with 3 entries: root-ca, sub-ca and server cert
            # load_verify_locations() --> this loads just a specific CA bundle !!!!
            context.load_verify_locations(cafile="C:/Users/...../my_ca_bundle.pem")
            
            # variant 3: I added my private CA with root-ca, sub-ca and server cert to certifi cacert.pem file as a hack
            # certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
            # load_verify_locations() --> this loads just a specific CA bundle !!!!
            context.load_verify_locations(cafile=certifi.where())
            ## --------------------

To verify that my private CA is within the selected store, I do an assert on the CommonName of my CA, thus it would fail fast if not found. If it's fine, the context is added to the session.mount later on:

            # get and print all CAs from loaded list to verify that my CA is within the list
            # notice the server certificate of pem file is not loaded into the list, don't no why, maybe because it's no CA
            json_ca_certs = context.get_ca_certs()
            print("All current context CA certs: {}".format(json.dumps(json_ca_certs, indent=1, sort_keys=True)))
            length=len(json_ca_certs)
            print("Number of certificates in bundle: {}".format(length))

            # fail fast:
            assert "MY CommonName" in str(json_ca_certs), "private CA not found in used bundle"

            kwargs['ssl_context'] = context
            return super().init_poolmanager(*args, **kwargs)

I do not set those environment variables, they are None

    def __init__(self, **kwargs):

        print("REQUESTS_CA_BUNDLE: '{}'".format(os.environ.get('REQUESTS_CA_BUNDLE')))
        print("CURL_CA_BUNDLE: '{}'".format(os.environ.get('CURL_CA_BUNDLE')))

I create a session and as creation of context with my truststore does not longer work, I try 3 different ways to use session.verify (one code line active per attempt only):

        self.session = requests.Session()

        # variant 1: default not working anymore:
        self.session.verify = True

        # variant 2: usage of my special ca bundle does not work
        self.session.verify = "C:/Users/...../my_ca_bundle.pem"

        # variant 3: certifi cacert.pem file contains my private CA and does not work
        # certifi.where(): C:\Users\........\Python39\Lib\site-packages\certifi\cacert.pem
        self.session.verify = certifi.where()

Here I finally mount my adapter to the session:

        # mount with Schema only as prefix to use them for all my calls (this worked in past and should not be a new problem)
        adapter = self.MyHttpAdapter()
        self.session.mount("https://", adapter)

Now I do my self.session.get() calls. Once again I tried those settings, since context cafile nor session.verify works, direct at requests within session:

    _api_url = 'https://my-domain.com/api/function'
    
    ###

    # variant 1: does not longer work, no setting above for session.verify nor context.load...():
    response = self.session.get(_api_url)

    # variant 2: verify = my special ca bundle work around does not work:
    response = self.session.get(_api_url, verify="C:/Users/...../my_ca_bundle.pem")
    
    # variant 3: verify = certifi cacert.pem work around does not work:
    response = self.session.get(_api_url, verify=certifi.where())

I added to requests/sessions.py some print debug output, to see what is used
sessions request() used final settings:

        settings = self.merge_environment_settings(
            prep.url, proxies, stream, verify, cert
        )
        print(f"DEBUG: request settings: {settings}")

sessions merge_environment_settings() right before the return ()

        print(f"DEBUG: merge_environment_settings() before return: verify={verify}")
        return {'verify': verify, 'proxies': proxies, 'stream': stream,
                'cert': cert}

Just to see, that my settings are used:

DEBUG: merge_environment_settings() before return: verify=path_to_ca_bundle.pem
DEBUG: request settings: {'verify': 'path_to_ca_bundle.pem', 'proxies': OrderedDict(), 'stream': False, 'cert': None}

Python and installed modules are up-to-date:
Python 3.9.6 (tags/v3.9.6:db3ff76, Jun 28 2021, 15:26:21)
certifi==2021.10.8
requests==2.27.1
requests-toolbelt==0.9.1

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

会发光的星星闪亮亮i 2025-01-18 10:00:29

带有简短问题和解决方案的初始条目已经太长了。由于它现在可以使用新的 CA 和服务器证书,因此我将对此答案提出最新问题。

rfc3280 4.2.1.1 授权密钥标识符

The authority key identifier extension provides a means of
identifying the public key corresponding to the private key used to
sign a certificate.  This extension is used where an issuer has
multiple signing keys (either due to multiple concurrent key pairs or
due to changeover).  The identification MAY be based on either the
key identifier (the subject key identifier in the issuer's
certificate) or on the issuer name and serial number.

The keyIdentifier field of the authorityKeyIdentifier extension MUST
be included in all certificates generated by conforming CAs to
facilitate certification path construction......

...
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::=  { id-ce 35 }

AuthorityKeyIdentifier ::= SEQUENCE {
  keyIdentifier             [0] KeyIdentifier           OPTIONAL,
  authorityCertIssuer       [1] GeneralNames            OPTIONAL,
  authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL  }

注意事项:全部三个AuthorityKeyIdentifier 属性是可选的,并且不链接在一起。
但在前面的段落中,人们可以读到:
该标识可以基于1.密钥标识符(颁发者证书中的主题密钥标识符)2 。发行人名称和序列号

相反,我们在rfc4158 3.5.12 “匹配关键标识符(KID)”

...
NOTE:  Although required to be present by [RFC3280], it is extremely
important that KIDs be used only as sorting criteria or as hints
during certification path building.  KIDs are not required to match
during certification path validation and cannot be used to eliminate
certificates.  This is of critical importance for interoperating
across domains and multi-vendor implementations where the KIDs may
not be calculated in the same fashion.

这只是一个排序标准,而不是验证标准。

最后在openSSL FAQ 15“为什么OpenSSL设置权限密钥标识符( AKID)扩展名错误?”

It doesn’t: this extension is often the cause of confusion.
Consider a certificate chain A->B->C so that A signs B and B signs C. Suppose certificate C contains AKID.
The purpose of this extension is to identify the authority certificate B. This can be done either by including the subject key identifier of B or its issuer name and serial number.
In this latter case because it is identifying certificate B it must contain the issuer name and serial number of B.
It is often wrongly assumed that it should contain the subject name of B. If it did this would be redundant information because it would duplicate the issuer name of C.

所以,他们的意思是这样???
这可以通过包含

  1. B 的主题密钥标识符
    来完成
    --> <--
  2. 发行人名称序列号
    在后一种情况下,因为它正在识别证书 B
    它必须包含 B 的发行人名称和序列号

这周我终于迷茫了……

The initial entry with short question and solution is already much too long. As it works now with new CA and server certificate, I will put latest question to this answer.

In rfc3280 4.2.1.1 Authority Key Identifier

The authority key identifier extension provides a means of
identifying the public key corresponding to the private key used to
sign a certificate.  This extension is used where an issuer has
multiple signing keys (either due to multiple concurrent key pairs or
due to changeover).  The identification MAY be based on either the
key identifier (the subject key identifier in the issuer's
certificate) or on the issuer name and serial number.

The keyIdentifier field of the authorityKeyIdentifier extension MUST
be included in all certificates generated by conforming CAs to
facilitate certification path construction......

...
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::=  { id-ce 35 }

AuthorityKeyIdentifier ::= SEQUENCE {
  keyIdentifier             [0] KeyIdentifier           OPTIONAL,
  authorityCertIssuer       [1] GeneralNames            OPTIONAL,
  authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL  }

Notice: All three AuthorityKeyIdentifier attributes are OPTIONAL and not linked together.

But in the paragraph before one could read:
The identification MAY be based on either the 1. key identifier (the subject key identifier in the issuer's certificate) or on the 2. issuer name and serial number.

In contrast we see the NOTE in rfc4158 3.5.12 "Matching Key Identifiers (KIDs)"

...
NOTE:  Although required to be present by [RFC3280], it is extremely
important that KIDs be used only as sorting criteria or as hints
during certification path building.  KIDs are not required to match
during certification path validation and cannot be used to eliminate
certificates.  This is of critical importance for interoperating
across domains and multi-vendor implementations where the KIDs may
not be calculated in the same fashion.

It's a sorting criteria only and not a validation criteria.

And finally in openSSL FAQ 15 "Why does OpenSSL set the authority key identifier (AKID) extension incorrectly?"

It doesn’t: this extension is often the cause of confusion.
Consider a certificate chain A->B->C so that A signs B and B signs C. Suppose certificate C contains AKID.
The purpose of this extension is to identify the authority certificate B. This can be done either by including the subject key identifier of B or its issuer name and serial number.
In this latter case because it is identifying certificate B it must contain the issuer name and serial number of B.
It is often wrongly assumed that it should contain the subject name of B. If it did this would be redundant information because it would duplicate the issuer name of C.

So, they mean this???
This can be done either by including

  1. the subject key identifier of B
    --> or <--
  2. its issuer name and serial number.
    In this latter case because it is identifying certificate B
    it must contain the issuer name and serial number of B.

I'm finally confused for this week...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文