python sslcertverificationerror带有aiohttp,但openssl很好

发布于 2025-02-13 22:37:45 字数 4133 浏览 2 评论 0 原文

我试图弄清楚为什么Python扔证书_verify_failed 我已配置的某个端点的例外,但是其他工具(例如openssl/ sslscan / sslyze 似乎对此很好。

这样的背景是,我们在旋转证书的同一时间开始收到这些错误。但是,我已经检查了证书订购(实际的CERT + INTERMEDIATES + root,在一个文件中以该顺序从上到下),这不是问题。

我想到的最简单的示例是以下内容:

import certifi
import os
import socket
import ssl

SERVER = "myhost.example.com"
PORT = 443

context_instance = ssl.SSLContext()
context_instance.verify_mode = ssl.CERT_REQUIRED
context_instance.load_verify_locations(
    cafile=os.path.relpath(certifi.where()), capath=None, cadata=None
)
s = socket.socket()
ssl_socket = context_instance.wrap_socket(s)
ssl_socket.connect((SERVER, PORT))
print("Version of the SSL Protocol:", ssl_socket.version())
print("Cipher used:", ssl_socket.cipher())

例如,使用 facebook.com 作为 server 产生以下内容:

Version of the SSL Protocol: TLSv1.3
Cipher used: ('TLS_CHACHA20_POLY1305_SHA256', 'TLSv1.3', 256)

但是当我使用它来测试我们的内部端点,我会收到以下错误(当我使用 google.com ,这也很奇怪):

Traceback (most recent call last):
  File "test.py", line 33, in <module>
    ssl_socket.connect((SERVER, PORT))
  File "/usr/local/lib/python3.8/ssl.py", line 1342, in connect
    self._real_connect(addr, False)
  File "/usr/local/lib/python3.8/ssl.py", line 1333, in _real_connect
    self.do_handshake()
  File "/usr/local/lib/python3.8/ssl.py", line 1309, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1131)

我的问题是:

  1. python 3中的一种万无一失的方法来验证是否启用了TLS端点(我们现在只使用https)配置正确吗?
  2. 是否有一种方法可以实现Python的更多详细错误输出,以了解我的内部终点上的问题是什么?例外没有给出任何可能出错的细节。

作为参考,这是我正在使用的:

  • Python 3.8.13
  • OS:Debian 11
  • ceeldifi == 2022.6.15

,这是OpenSSL的输出:

$ openssl s_client -connect myhost.example.com:443 -4 <<< "Q"

CONNECTED(00000003)
depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
verify return:1
depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
verify return:1
depth=0 CN = *.example.com
verify return:1
---
Certificate chain
 0 s:CN = *.example.com
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
 1 s:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
 2 s:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
   i:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
 3 s:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
   i:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
---
Server certificate

<masked>

subject=CN = *.example.com

issuer=C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 5687 bytes and written 409 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
DONE

I'm trying to figure out why Python is throwing CERTIFICATE_VERIFY_FAILED exceptions for a certain endpoint I have configured, but other tools like OpenSSL / sslscan / sslyze seem to be fine with it.

The context of this is that we started receiving these errors around the same time we were rotating certificates. However, I have checked the certificate ordering (Actual cert + intermediates + root, in one file, in that order from top to bottom) and that is not the problem.

The simplest example I conjured up to test this is the following:

import certifi
import os
import socket
import ssl

SERVER = "myhost.example.com"
PORT = 443

context_instance = ssl.SSLContext()
context_instance.verify_mode = ssl.CERT_REQUIRED
context_instance.load_verify_locations(
    cafile=os.path.relpath(certifi.where()), capath=None, cadata=None
)
s = socket.socket()
ssl_socket = context_instance.wrap_socket(s)
ssl_socket.connect((SERVER, PORT))
print("Version of the SSL Protocol:", ssl_socket.version())
print("Cipher used:", ssl_socket.cipher())

For example, using facebook.com as the SERVER yields the following:

Version of the SSL Protocol: TLSv1.3
Cipher used: ('TLS_CHACHA20_POLY1305_SHA256', 'TLSv1.3', 256)

But when I use this to test our internal endpoint, I get the following error (I also get this when I use google.com also, which is weird):

Traceback (most recent call last):
  File "test.py", line 33, in <module>
    ssl_socket.connect((SERVER, PORT))
  File "/usr/local/lib/python3.8/ssl.py", line 1342, in connect
    self._real_connect(addr, False)
  File "/usr/local/lib/python3.8/ssl.py", line 1333, in _real_connect
    self.do_handshake()
  File "/usr/local/lib/python3.8/ssl.py", line 1309, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate (_ssl.c:1131)

My questions are:

  1. What's a foolproof way in Python 3 to verify if a TLS-enabled endpoint (let's just use HTTPS for now) is configured correctly?
  2. Is there a way to achieve a more verbose error output from Python to see what the problem on my internal endpoint is? The exception does not give any more details as to what could be wrong.

For reference, this is what I'm using:

  • Python 3.8.13
  • OS: Debian 11
  • certifi==2022.6.15

And this is the output from OpenSSL:

$ openssl s_client -connect myhost.example.com:443 -4 <<< "Q"

CONNECTED(00000003)
depth=2 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
verify return:1
depth=1 C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
verify return:1
depth=0 CN = *.example.com
verify return:1
---
Certificate chain
 0 s:CN = *.example.com
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
 1 s:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
   i:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
 2 s:C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
   i:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
 3 s:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
   i:C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
---
Server certificate

<masked>

subject=CN = *.example.com

issuer=C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2

---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: RSA-PSS
Server Temp Key: X25519, 253 bits
---
SSL handshake has read 5687 bytes and written 409 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Server public key is 2048 bit
Secure Renegotiation IS NOT supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
DONE

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

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

发布评论

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

评论(1

粉红×色少女 2025-02-20 22:37:45

对于未来,这里有几个有见地的答案,它们都为整体答案做出了贡献:

dave_thompson_085 a>的答案对于确定,如果要检查启用TLS的服务器端点是否提供正确的证书,则是 对于运行以下命令之一很有用:

对于服务器仅使用一个主机名的情况:

$ openssl s_client -connect myhost.example.com:443 -4 <<< "Q"

对于服务器具有多个TLS主机的情况,您想检查当您 不要 传递服务器主机名:

$ openssl s_client -noservername -connect myhost.example.com:443 -4 <<< "Q"

如果它是HTTP服务器,这将显示默认的后端。

对于Python位,您需要传递 server_hostname 。这样的事情能够在我测试过的所有有效的HTTPS网站上工作:

import os
import socket
import ssl

import certifi

SERVER = "myhost.example.com"
PORT = 443

context_instance = ssl.SSLContext()
context_instance.verify_mode = ssl.CERT_REQUIRED
context_instance.check_hostname = True
context_instance.load_verify_locations(
    cafile=os.path.relpath(certifi.where()), capath=None, cadata=None
)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_socket = context_instance.wrap_socket(s, server_hostname=SERVER)

ssl_socket.connect((SERVER, PORT))
print("Server hostname:", ssl_socket.server_hostname)
print("Version of the SSL Protocol:", ssl_socket.version())
print("Cipher used:", ssl_socket.cipher())

最后, 真正的基础 问题是 aiohttp == 3.7.0 我们在应用程序中使用的代码>。在,以下行显示了我们遇到的问题:

修复了导致可变遮挡的错误螺纹Resolver.sresve以将已解决的IP返回为每个记录中的主机名,从而阻止了HTTPS连接的验证。

# 5110 aiohttp 的损坏版本如下:

import asyncio

import aiohttp

SERVER = "myhost.example.com"


async def main():
    async with aiohttp.ClientSession(raise_for_status=True) as session:
        async with session.get(f"https://{SERVER}") as r:
            body = await r.json()
            print(body)


if __name__ == "__main__":
    asyncio.run(main())

For the future, there are a couple of insightful answers here, and all of them contribute to the overall answer:

dave_thompson_085's answer was useful in determining that, if you want to check whether your TLS-enabled server endpoint is serving the right certificate, it's useful to run one of the following commands:

For cases where the server only serves one hostname:

$ openssl s_client -connect myhost.example.com:443 -4 <<< "Q"

For cases where the server has multiple TLS hosts that it serves, and you want to check what happens when you don't pass a server hostname:

$ openssl s_client -noservername -connect myhost.example.com:443 -4 <<< "Q"

This will show a default backend if it's an HTTP server.

For the Python bit, you need to pass in the server_hostname. Something like this was able to work for all valid HTTPS sites I tested:

import os
import socket
import ssl

import certifi

SERVER = "myhost.example.com"
PORT = 443

context_instance = ssl.SSLContext()
context_instance.verify_mode = ssl.CERT_REQUIRED
context_instance.check_hostname = True
context_instance.load_verify_locations(
    cafile=os.path.relpath(certifi.where()), capath=None, cadata=None
)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssl_socket = context_instance.wrap_socket(s, server_hostname=SERVER)

ssl_socket.connect((SERVER, PORT))
print("Server hostname:", ssl_socket.server_hostname)
print("Version of the SSL Protocol:", ssl_socket.version())
print("Cipher used:", ssl_socket.cipher())

Finally, the real underlying problem was the version of aiohttp==3.7.0 that we were using in our application. In the CHANGELOG for version 3.7.1, the following line shows the problem we were having:

Fix a variable-shadowing bug causing ThreadedResolver.resolve to return the resolved IP as the hostname in each record, which prevented validation of HTTPS connections. #5110

The script I ran to test the broken versions of aiohttp was the following:

import asyncio

import aiohttp

SERVER = "myhost.example.com"


async def main():
    async with aiohttp.ClientSession(raise_for_status=True) as session:
        async with session.get(f"https://{SERVER}") as r:
            body = await r.json()
            print(body)


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