dns.query.quic() cert validation with custom verify path broken?
Describe the bug
Using verify=/path/to/ca.pem with dns.query.quic() doesn't seem to reject invalid certificates. Passing a selfsigned cert to verify= does not prevent a lookup from working, as demonstrated below.
Is it me doing something wrong? Thanks!
To Reproduce
(venv) user@privat-dev:~/devel/dns_exporter/src$ pip freeze | grep dnspython
dnspython==2.6.1
(venv) user@privat-dev:~/devel/dns_exporter/src$ python
Python 3.10.13 (main, Nov 15 2023, 13:09:29) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import dns.message, dns.name, dns.query
>>> q = dns.message.make_query(dns.name.from_text("example.com"), "A")
>>> dns.query.quic(q, "94.140.14.140", port=853, verify=True)
<DNS message, ID 0>
>>> dns.query.quic(q, "94.140.14.140", port=853, verify="/home/user/devel/dns_exporter/src/tests/certificates/test.crt")
<DNS message, ID 0>
>>>
(venv) user@privat-dev:~/devel/dns_exporter/src$
The file /home/user/devel/dns_exporter/src/tests/certificates/test.crt contains a self-signed certificate.
I haven't got a DoQ server with an invalid certificate handy so I can't test which of these three scenarios is true, but I guess either:
- no certificates are validated by
dns.query.quic() - or more likely that the custom CA logic is a no-op
- or that the custom CA logic doesn't remove system CAs when adding the custom ones
Context (please complete the following information): see above
ps. I cut away a couple of aioquic cryptography deprecation warnings for clarity, they are not relevant to this
Thanks! :)
btw 94.140.14.140 is one of adguards doq servers and they do have IP:94.140.14.140 as a SAN so the certificate should (and does) validate with normal system CAs.
I can testify that if you give it bad certificates validation fails :).
The answer appears to be number 3 on your list, as aioquic always loads certifi and then also loads anything else. Dnspython will have passed the verify string into cafile in this aioquic TLS code:
# load CAs
store = crypto.X509Store()
store.load_locations(certifi.where())
if cadata is not None:
for cert in load_pem_x509_certificates(cadata):
store.add_cert(crypto.X509.from_cryptography(cert))
if cafile is not None or capath is not None:
store.load_locations(cafile, capath)
This was a bit surprising to me too as my expectation was like yours that if you specify a CA then it should be the only source. I'll ask Jeremy.
This is fixed and will be in the next aioquic release.