This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

Author samiam
Recipients christian.heimes, samiam
Date 2017-11-10.01:54:08
SpamBayes Score -1.0
Marked as misclassified Yes
Message-id <1510278854.68.0.213398074469.issue31997@psf.upfronthosting.co.za>
In-reply-to
Content
I recently came across an issue in the ssl library and have a simple fix to address it.

When doing hostname verification against an X.509 certificate, a trailing dot (period) in the hostname is matched against the certificate.  But the trailing dot should only be applied to the DNS lookup not the certificate match.

Conversely, a certificate that has a trailing dot in its commonName (probably rare but allowed) should match a hostname without the trailing dot.  As the ssl library is written now, both cases fail.

The truth table below shows the current python ssl DNS matching behavior.

+----------------------------------------+
|  #  hostname    certificate    MATCH   |
| +------------------------------------+ |
| |   dns  dns.   cname cname.         | |
| +------------------------------------+ |
|  1   x            x            TRUE    |
|  2   x                  x      FALSE   |
|  3        x       x            FALSE   |
|  4        x             x      TRUE    |
+----------------------------------------+

Case 1 and 4 currently match as both hostname and certificate strings match exactly when the trailing dot is either present or not.

Case 2 is unlikely, as certificates are rarely signed with a trailing dot in the subject commonName and if they were, clients would ALWAYS have to enter the hostname with a trailing dot to get a match.

Case 3 is more likely where the hostname has a trailing dot, but the certificate does not.  For example, "www.example.com." is used for the DNS lookup, but then, "www.example.com." will not match the certificate due to the trailing dot missing from the certificate.

I propose the truth table should be true in all cases and just ignore the trailing dot in both the hostname and certificate.

As best I can tell, the RFCs [1] are silent on this issue.  Although the hostname and commonName strings currently must match, there are a couple of precedents where ignoring the trailing dot is done in practice.

Web browsers allow a trailing dot in the URI and will accept a certificate even when the certificate doesn't have a trailing dot.  For example, visit Google with trailing dot (https://www.google.com./) from a browser of your choice and check certificate.  It should check out as valid. [2]

Also, at least two language SSL libraries, Ruby [3] and Go [4], match certificates when hostnames contain a trailing dot.  Lastly cURL [5] ignores trailing dots in certs and hostnames.

In summary, I don't feel the current python ssl library is wrong - it is following an interpretation of the RFC.  But I think it can be more permissive, following the spirit of the RFC without sacrificing security.

Patch attached with code change.  If accepted, I can do a more formal PR, backport to 2.7 and add tests.

Thoughts?
-Sam


Example of issue
================

Python 3.7.0a2+ (heads/master-dirty:cbe1756e3e, Nov  3 2017, 15:56:14)
[Clang 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ssl
>>> cert = {'subject': ((('commonName', 'example.com'),),)}
>>> ssl.match_hostname(cert, "example.com")    ## Case 1 from truth table
>>> ssl.match_hostname(cert, "example.com.")   ## Case 3 from truth table
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../cpython/Lib/ssl.py", line 330, in match_hostname
    % (hostname, dnsnames[0]))
ssl.CertificateError: hostname 'example.com.' doesn't match 'example.com'

>>> cert = {'subject': ((('commonName', 'example.com.'),),)}
>>> ssl.match_hostname(cert, "example.com.")   ## Case 4 from truth table
>>> ssl.match_hostname(cert, "example.com")    ## Case 2 from truth table
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../cpython/Lib/ssl.py", line 330, in match_hostname
    % (hostname, dnsnames[0]))
ssl.CertificateError: hostname 'example.com' doesn't match 'example.com.'

References
==========

[1] RFCs - there may be other RFCs addressing X.509 certificates

https://tools.ietf.org/html/rfc6125
https://tools.ietf.org/html/rfc5280
https://tools.ietf.org/html/rfc3986

[2] Old Mozilla thread discussing the trailing dot:

https://bugzilla.mozilla.org/show_bug.cgi?id=134402#c36

I quote:

Yes, it's ok to match "www.example.com." (trailing) to the cert with "www.example.com" (no trailing)
It's also OK to match "www.example.com" (no trailing) to the cert with "www.example.com." (trailing)

[3] Ruby

Ironically Ruby doesn't even take the trailing dot into consideration as it splits the strings using dot as the delimiter.

irb> "www.example.com".split('.') == "www.example.com.".split('.')
=> true

https://github.com/ruby/openssl/blob/master/lib/openssl/ssl.rb#L295

[4] Go

The trailing dot is explicitly removed in Go TLS library.

https://github.com/golang/go/blob/master/src/crypto/x509/verify.go#L913

[5] cURL

cURL strips trailing dot following behavior in browsers.

https://github.com/curl/curl/blob/master/lib/hostcheck.c#L51

The cURL author discusses the trailing dot in a couple threads.

https://github.com/curl/curl/issues/716
https://lists.w3.org/Archives/Public/ietf-http-wg/2016JanMar/0430.html
History
Date User Action Args
2017-11-10 01:54:14samiamsetrecipients: + samiam, christian.heimes
2017-11-10 01:54:14samiamsetmessageid: <1510278854.68.0.213398074469.issue31997@psf.upfronthosting.co.za>
2017-11-10 01:54:14samiamlinkissue31997 messages
2017-11-10 01:54:09samiamcreate