[PATCH] url: check subjectAltName when verifying ssl certificate

Yuya Nishihara yuya at tcha.org
Sat Jan 8 09:36:16 CST 2011


# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1294500936 -32400
# Node ID 1fd9c9664ed04383fc6c8e84c70c0edea475c314
# Parent  dd7da001a984115806c3d3457a63186f9dfa9a91
url: check subjectAltName when verifying ssl certificate

Now it verifies certificate in the same manner as py3k implementation:
http://svn.python.org/view/python/branches/py3k/Lib/ssl.py?view=markup#match_hostname

diff --git a/mercurial/url.py b/mercurial/url.py
--- a/mercurial/url.py
+++ b/mercurial/url.py
@@ -506,13 +506,26 @@ class httphandler(keepalive.HTTPHandler)
 
 def _verifycert(cert, hostname):
     '''Verify that cert (in socket.getpeercert() format) matches hostname.
-    CRLs and subjectAltName are not handled.
+    CRLs is not handled.
 
     Returns error message if any problems are found and None on success.
     '''
     if not cert:
         return _('no certificate received')
     dnsname = hostname.lower()
+    def matchdnsname(certname):
+        return (certname == dnsname or
+                '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
+
+    san = cert.get('subjectAltName', [])
+    if san:
+        certnames = [value.lower() for key, value in san if key == 'DNS']
+        for name in certnames:
+            if matchdnsname(name):
+                return None
+        return _('certificate is for %s') % ', '.join(certnames)
+
+    # subject is only checked when subjectAltName is empty
     for s in cert.get('subject', []):
         key, value = s[0]
         if key == 'commonName':
@@ -521,11 +534,10 @@ def _verifycert(cert, hostname):
                 certname = value.lower().encode('ascii')
             except UnicodeEncodeError:
                 return _('IDN in certificate not supported')
-            if (certname == dnsname or
-                '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1]):
+            if matchdnsname(certname):
                 return None
             return _('certificate is for %s') % certname
-    return _('no commonName found in certificate')
+    return _('no commonName or subjectAltName found in certificate')
 
 if has_https:
     class BetterHTTPS(httplib.HTTPSConnection):
diff --git a/tests/test-url.py b/tests/test-url.py
--- a/tests/test-url.py
+++ b/tests/test-url.py
@@ -25,6 +25,18 @@ check(_verifycert(cert('*.example.com'),
 check(_verifycert(cert('*.example.com'), 'w.w.example.com'),
       'certificate is for *.example.com')
 
+# Test subjectAltName
+san_cert = {'subject': ((('commonName', 'example.com'),),),
+            'subjectAltName': (('DNS', '*.example.net'),
+                               ('DNS', 'example.net'))}
+check(_verifycert(san_cert, 'example.net'),
+      None)
+check(_verifycert(san_cert, 'foo.example.net'),
+      None)
+# subject is only checked when subjectAltName is empty
+check(_verifycert(san_cert, 'example.com'),
+      'certificate is for *.example.net, example.net')
+
 # Avoid some pitfalls
 check(_verifycert(cert('*.foo'), 'foo'),
       'certificate is for *.foo')
@@ -33,7 +45,7 @@ check(_verifycert(cert('*o'), 'foo'),
 
 check(_verifycert({'subject': ()},
                   'example.com'),
-      'no commonName found in certificate')
+      'no commonName or subjectAltName found in certificate')
 check(_verifycert(None, 'example.com'),
       'no certificate received')
 


More information about the Mercurial-devel mailing list