Skip to content
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

Closed
wants to merge 2 commits into from
Closed

certdb: use certutil and match_hostname for cert verification #490

wants to merge 2 commits into from

Conversation

HonzaCholasta
Copy link
Contributor

@HonzaCholasta HonzaCholasta commented Feb 21, 2017

Use certutil and ssl.match_hostname calls instead of python-nss for
certificate verification.

Copy link
Member

@tiran tiran left a 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)
Copy link
Member

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

Copy link
Contributor Author

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))
Copy link
Member

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.

Copy link
Contributor Author

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:
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

@tiran
Copy link
Member

tiran commented Feb 21, 2017

Do we ensure that the function is always called with an IDN A-Label encoded hostname? ssl.match_hostname assumes that all parts are A-labels, not U-labels.

@HonzaCholasta
Copy link
Contributor Author

@tiran, how do I ensure that?

@tiran
Copy link
Member

tiran commented Feb 21, 2017

The hostname must be ASCII text. Something like hostname.encode('ascii') should catch non-ASCII text and Python 3 bytes.

Copy link
Member

@tiran tiran left a 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)
Copy link
Member

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()
Copy link
Member

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. :/

@HonzaCholasta HonzaCholasta changed the title [WIP] certdb: use certutil and match_hostname for cert verification certdb: use certutil and match_hostname for cert verification Mar 8, 2017
@HonzaCholasta
Copy link
Contributor Author

I think this PR is ready now.

@stlaz
Copy link
Contributor

stlaz commented Mar 27, 2017

@tiran Could you please finish the review? I guess we can omit the change in .spec.in for the review time being.

@stlaz
Copy link
Contributor

stlaz commented Mar 28, 2017

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.

@tiran
Copy link
Member

tiran commented Mar 28, 2017

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.

@tiran tiran self-assigned this Mar 28, 2017
cert = x509.load_certificate(cert, x509.DER)

if not cert.subject:
raise ValueError("has empty subject")
Copy link
Member

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")
Copy link
Member

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

Copy link
Member

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:
Copy link
Member

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.

Copy link
Contributor Author

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 = []
Copy link
Member

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.

Copy link
Contributor Author

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')
Copy link
Member

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.

@tiran
Copy link
Member

tiran commented Mar 29, 2017

Your PR is going to remove the last import from python-nss. Awesome!

Please remove the requirement from ipapython/setup.py and freeipa.spec.in, too.

Jan Cholasta added 2 commits March 30, 2017 10:23
Use certutil and ssl.match_hostname calls instead of python-nss for
certificate verification.
Remove the unused python-nss dependency.
@HonzaCholasta
Copy link
Contributor Author

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?

Copy link
Member

@tiran tiran left a 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.

@tiran tiran added the ack Pull Request approved, can be merged label Mar 31, 2017
@MartinBasti
Copy link
Contributor

master:

  • 9183cf2 certdb: use certutil and match_hostname for cert verification
  • 2b33230 setup, pylint, spec file: drop python-nss dependency

@MartinBasti MartinBasti added the pushed Pull Request has already been pushed label Mar 31, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ack Pull Request approved, can be merged pushed Pull Request has already been pushed
Projects
None yet
4 participants