New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
certdb: use certutil and match_hostname for cert verification #490
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of simplifications
ipalib/x509.py
Outdated
| else: | ||
| name = ava.dotted_string | ||
| match_ava = (name, ava.value) | ||
| match_rdn.append(match_ava) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Surplus () in match_ava = (name, ava.value). How about you collapse both lines into one? match_rdn.append((name, ava.value)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, why not.
ipalib/x509.py
Outdated
| else: | ||
| match_cert['subjectAltName'] = match_san = [] | ||
| for value in san.get_values_for_type(cryptography.x509.DNSName): | ||
| match_dnsname = ('DNS', str(value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you use str() here? It's either unnecessary (if value is already a str) or wrong (if value is bytes or DNSName instance). Since get_values_for_type returns the value attribute of DNSName instances and DNSName.value is guaranteed to be text, the call is not necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember TBH - this patch has been laying on my HDD for quite a while.
ipalib/x509.py
Outdated
|
|
||
| try: | ||
| ssl.match_hostname(match_cert, hostname) | ||
| except ssl.SSLError as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
match_hostname raises a subclass of ValueError. It's part of the API. There is no need to catch SSLError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see.
ipalib/x509.py
Outdated
| if ava.oid == cryptography.x509.oid.NameOID.COMMON_NAME: | ||
| name = 'commonName' | ||
| else: | ||
| name = ava.dotted_string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can safely omit other attribute value assertion. Python's match_hostname will both ignore other AVAs and never support AVAs OID in dotted string notation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense.
|
Do we ensure that the function is always called with an IDN A-Label encoded hostname? |
|
@tiran, how do I ensure that? |
|
The hostname must be ASCII text. Something like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hostname verification is a mess. I got it wrong multiple times, caused at least one CVE and found multiple other CVEs. :/
ipalib/x509.py
Outdated
| value = DNSName(value).ToASCII() | ||
| match_san.append(('DNS', value)) | ||
|
|
||
| ssl.match_hostname(match_cert, hostname) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ssl.match_hostname expects an IDN A-label, use DNSName(hostname).ToASCII() here.
ipalib/x509.py
Outdated
| else: | ||
| match_cert['subjectAltName'] = match_san = [] | ||
| for value in san.value.get_values_for_type(cryptography.x509.DNSName): | ||
| value = DNSName(value).ToASCII() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You must not our DNSName class here. It's an invalid and therefore unsafe conversion. I fear we cannot convert back safely. In general IDNA is not a bijective encoding. This is especially true for different kinds of IDNA. PyCA cryptography uses IDNA 2008 UTS46, DNSName uses IDNA 2003.
Consider this example:
>>> from ipapython.dnsutil import DNSName
>>> import idna
>>> asn1_string = 'xn--strae-oqa.de'
>>> pyca_dnsname = idna.decode(asn1_string)
>>> DNSName(pyca_dnsname).ToASCII()
u'strasse.de'
I talked to @reaperhulk about the issue. Paul pointed me to pyca/cryptography#3357.
Yes, hostname verification is a mess. I got it wrong multiple times, caused at least one CVE and found multiple other CVEs. :/
|
I think this PR is ready now. |
|
@tiran Could you please finish the review? I guess we can omit the change in |
|
I tried to use the wonderful github tool to resolve conflicts to make this more review-friendly but I guess it kind of missed the magic, it's ready for review anyway, please, finish it. |
|
github magic is bad magic :/ It still shows up as 'conflicting' for me. I'll try to find time to review the issue tomorrow, Thursday latest. |
| cert = x509.load_certificate(cert, x509.DER) | ||
|
|
||
| if not cert.subject: | ||
| raise ValueError("has empty subject") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RFC 5280 section 4.1.2.6 mandates subject only for certs with basic constraint CA: TRUE. Please check for basic constraint first.
| raise ValueError("missing basic constraints") | ||
|
|
||
| if not bc.ca: | ||
| raise ValueError("not a CA certificate") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is the verify_ca_cert_validity method used? Is it used to check for just any CA cert or also used to verify that the CA cert can be used to sign an intermediate CA in Dogtag? In the latter case, this should also check that path_length is >= 1.
|
|
||
| if not bc.ca: | ||
| raise ValueError("not a CA certificate") | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation is missing one important field. As of RFC 5280 section 4.2.1.3, CA certs must have a key usage extension with at least keyCertSign bit set.
| match_cert = {} | ||
|
|
||
| match_cert['subject'] = match_subject = [] | ||
| for rdn in cert.subject.rdns: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of RFC 5280 section 4.1.2.6, the subject field of an EE cert is optional and may not be present, https://no-subject.badssl.com/
I think this needs an additional check to verify that cert.subject is present and/or not None.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What RFC 5280 actually states is that the subject might be empty, not that the subject field is optional.
Empty subject has an empty list of RDNs:
>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.x509 import load_pem_x509_certificate
>>> load_pem_x509_certificate(open('no-subject.crt', 'rb').read(), default_backend()).subject.rdns
[]
i.e. no extra check is necessary.
|
|
||
| values = get_san_a_label_dns_names(cert) | ||
| if values: | ||
| match_cert['subjectAltName'] = match_san = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of RFC 6125 section 6.4.4. you also have to take SRV-ID and URI-ID into account. Since ssl.match_hostname supports IP addresses, you also need to include iPAddress fields or validate that hostname never looks like an IP address.
This will cause a security bug as soon as https://bugs.python.org/issue28196 is fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ssl.match_hostname in Python 2 supports only DNS SANs, so I didn't bother including support for anything else.
I guess adding support for other SAN types won't hurt, that is if we don't mind the different behavior in Python 2 vs 3.
| cert.tbs_certificate_bytes, | ||
| asn1Spec=rfc2459.TBSCertificate() | ||
| )[0] | ||
| OID_SAN = univ.ObjectIdentifier('2.5.29.17') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use SUBJECT_ALTERNATIVE_NAME from cryptography.x509.oid.
|
Your PR is going to remove the last import from python-nss. Awesome! Please remove the requirement from |
Use certutil and ssl.match_hostname calls instead of python-nss for certificate verification.
Remove the unused python-nss dependency.
|
Awesome indeed! As for your suggestions to improve the validation, I completely agree with them, but the focus of this PR is to refactor the current validation not to use python-nss, which it delivers. Could you please file a ticket for the improvements, so that it gets more visibility and can be properly tracked? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK!
I created ticket https://pagure.io/freeipa/issue/6840 to track the remaining nit-picks. Since all validation functions are only used for sanity checks and user feedback, it is safe to handle them at a later point.
Use certutil and ssl.match_hostname calls instead of python-nss for
certificate verification.