diff -r 08db9306e8af Doc/library/ssl.rst --- a/Doc/library/ssl.rst Wed Oct 06 15:05:45 2010 +0200 +++ b/Doc/library/ssl.rst Wed Oct 06 22:27:28 2010 +0200 @@ -45,11 +45,27 @@ Functions, Constants, and Exceptions .. exception:: SSLError - Raised to signal an error from the underlying SSL implementation. This - signifies some problem in the higher-level encryption and authentication - layer that's superimposed on the underlying network connection. This error + Raised to signal an error from the underlying SSL implementation + (currently provided by the OpenSSL library). This signifies some + problem in the higher-level encryption and authentication layer that's + superimposed on the underlying network connection. This error is a subtype of :exc:`socket.error`, which in turn is a subtype of - :exc:`IOError`. + :exc:`IOError`. The error code and message of :exc:`SSLError` instances + are provided by the OpenSSL library. + +.. exception:: CertificateError + + Raised to signal an error with a certificate (such as mismatching + hostname). Certificate errors detected by OpenSSL, though, raise + an :exc:`SSLError`. + + +Socket creation +^^^^^^^^^^^^^^^ + +The following function allows for standalone socket creation. Starting from +Python 3.2, it can be more flexible to use :meth:`SSLContext.wrap_socket` +instead. .. function:: wrap_socket(sock, keyfile=None, certfile=None, server_side=False, cert_reqs=CERT_NONE, ssl_version={see docs}, ca_certs=None, do_handshake_on_connect=True, suppress_ragged_eofs=True, ciphers=None) @@ -139,6 +155,9 @@ Functions, Constants, and Exceptions .. versionchanged:: 3.2 New optional argument *ciphers*. +Random generation +^^^^^^^^^^^^^^^^^ + .. function:: RAND_status() Returns True if the SSL pseudo-random number generator has been seeded with @@ -164,6 +183,32 @@ Functions, Constants, and Exceptions string (so you can always use :const:`0.0`). See :rfc:`1750` for more information on sources of entropy. +Certificate handling +^^^^^^^^^^^^^^^^^^^^ + +.. function:: match_hostname(cert, hostname) + + Verify that *cert* (in decoded format as returned by + :meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules + applied are those for checking the identity of HTTPS servers as outlined + in :rfc:`2818`, except that IP addresses are not currently supported. + In addition to HTTPS, this function should be suitable for checking the + identity of servers in various SSL-based protocols such as FTPS, IMAPS, + POPS and others. + + :exc:`CertificateError` is raised on failure. On success, the function + returns nothing:: + + >>> cert = {'subject': ((('commonName', 'example.com'),),)} + >>> ssl.match_hostname(cert, "example.com") + >>> ssl.match_hostname(cert, "example.org") + Traceback (most recent call last): + File "", line 1, in + File "/home/py3k/Lib/ssl.py", line 130, in match_hostname + ssl.CertificateError: hostname 'example.org' doesn't match 'example.com' + + .. versionadded:: 3.2 + .. function:: cert_time_to_seconds(timestring) Returns a floating-point value containing a normal seconds-after-the-epoch @@ -178,7 +223,6 @@ Functions, Constants, and Exceptions >>> import time >>> time.ctime(ssl.cert_time_to_seconds("May 9 00:00:00 2007 GMT")) 'Wed May 9 00:00:00 2007' - >>> .. function:: get_server_certificate(addr, ssl_version=PROTOCOL_SSLv3, ca_certs=None) @@ -201,6 +245,9 @@ Functions, Constants, and Exceptions Given a certificate as an ASCII PEM string, returns a DER-encoded sequence of bytes for that same certificate. +Constants +^^^^^^^^^ + .. data:: CERT_NONE Possible value for :attr:`SSLContext.verify_mode`, or the ``cert_reqs`` @@ -683,68 +730,51 @@ should use the following idiom:: Client-side operation ^^^^^^^^^^^^^^^^^^^^^ -This example connects to an SSL server, prints the server's address and -certificate, sends some bytes, and reads part of the response:: +This example connects to an SSL server and prints the server's certificate:: import socket, ssl, pprint s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # require a certificate from the server ssl_sock = ssl.wrap_socket(s, ca_certs="/etc/ca_certs_file", cert_reqs=ssl.CERT_REQUIRED) - ssl_sock.connect(('www.verisign.com', 443)) - print(repr(ssl_sock.getpeername())) pprint.pprint(ssl_sock.getpeercert()) - print(pprint.pformat(ssl_sock.getpeercert())) - - # Set a simple HTTP request -- use http.client in actual code. - ssl_sock.sendall(b"GET / HTTP/1.0\r\nHost: www.verisign.com\r\n\r\n") - - # Read a chunk of data. Will not necessarily - # read all the data returned by the server. - data = ssl_sock.recv() - # note that closing the SSLSocket will also close the underlying socket ssl_sock.close() -As of September 6, 2007, the certificate printed by this program looked like +As of October 6, 2010, the certificate printed by this program looks like this:: - {'notAfter': 'May 8 23:59:59 2009 GMT', - 'subject': ((('serialNumber', '2497886'),), - (('1.3.6.1.4.1.311.60.2.1.3', 'US'),), - (('1.3.6.1.4.1.311.60.2.1.2', 'Delaware'),), - (('countryName', 'US'),), - (('postalCode', '94043'),), - (('stateOrProvinceName', 'California'),), - (('localityName', 'Mountain View'),), - (('streetAddress', '487 East Middlefield Road'),), - (('organizationName', 'VeriSign, Inc.'),), - (('organizationalUnitName', - 'Production Security Services'),), - (('organizationalUnitName', - 'Terms of use at www.verisign.com/rpa (c)06'),), - (('commonName', 'www.verisign.com'),))} - -which is a fairly poorly-formed ``subject`` field. + {'notAfter': 'May 25 23:59:59 2012 GMT', + 'subject': ((('1.3.6.1.4.1.311.60.2.1.3', 'US'),), + (('1.3.6.1.4.1.311.60.2.1.2', 'Delaware'),), + (('businessCategory', 'V1.0, Clause 5.(b)'),), + (('serialNumber', '2497886'),), + (('countryName', 'US'),), + (('postalCode', '94043'),), + (('stateOrProvinceName', 'California'),), + (('localityName', 'Mountain View'),), + (('streetAddress', '487 East Middlefield Road'),), + (('organizationName', 'VeriSign, Inc.'),), + (('organizationalUnitName', ' Production Security Services'),), + (('commonName', 'www.verisign.com'),))} This other example first creates an SSL context, instructs it to verify certificates sent by peers, and feeds it a set of recognized certificate authorities (CA):: >>> context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - >>> context.verify_mode = ssl.CERT_OPTIONAL + >>> context.verify_mode = ssl.CERT_REQUIRED >>> context.load_verify_locations("/etc/ssl/certs/ca-bundle.crt") (it is assumed your operating system places a bundle of all CA certificates in ``/etc/ssl/certs/ca-bundle.crt``; if not, you'll get an error and have to adjust the location) -When you use the context to connect to a server, :const:`CERT_OPTIONAL` +When you use the context to connect to a server, :const:`CERT_REQUIRED` validates the server certificate: it ensures that the server certificate was signed with one of the CA certificates, and checks the signature for correctness:: @@ -752,11 +782,15 @@ correctness:: >>> conn = context.wrap_socket(socket.socket(socket.AF_INET)) >>> conn.connect(("linuxfr.org", 443)) -You should then fetch the certificate and check its fields for conformity. -Here, the ``commonName`` field in the ``subject`` matches the desired HTTPS -host ``linuxfr.org``:: +You should then fetch the certificate and check its fields for conformity:: - >>> pprint.pprint(conn.getpeercert()) + >>> cert = conn.getpeercert() + >>> ssl.match_hostname(cert, "linuxfr.org") + +Visual inspection shows that the certificate does identify the desired service +(that is, the HTTPS host ``linuxfr.org``):: + + >>> pprint.pprint(cert) {'notAfter': 'Jun 26 21:41:46 2011 GMT', 'subject': ((('commonName', 'linuxfr.org'),),), 'subjectAltName': (('DNS', 'linuxfr.org'), ('othername', ''))} @@ -776,7 +810,6 @@ the server:: b'', b''] - See the discussion of :ref:`ssl-security` below. @@ -842,12 +875,10 @@ peer, it can be insecure, especially in would like to ensure the authenticity of the server you're talking to. Therefore, when in client mode, it is highly recommended to use :const:`CERT_REQUIRED`. However, it is in itself not sufficient; you also -have to check that the server certificate (obtained with -:meth:`SSLSocket.getpeercert`) matches the desired service. The exact way -of doing so depends on the higher-level protocol used; for example, with -HTTPS, you'll check that the host name in the URL matches either the -``commonName`` field in the ``subjectName``, or one of the ``DNS`` fields -in the ``subjectAltName``. +have to check that the server certificate, which can be obtained by calling +:meth:`SSLSocket.getpeercert`, matches the desired service. For many +protocols and applications, the service can be identified by the hostname; +in this case, the :func:`match_hostname` function can be used. In server mode, if you want to authenticate your clients using the SSL layer (rather than using a higher-level authentication mechanism), you'll also have diff -r 08db9306e8af Lib/ssl.py --- a/Lib/ssl.py Wed Oct 06 15:05:45 2010 +0200 +++ b/Lib/ssl.py Wed Oct 06 22:27:28 2010 +0200 @@ -55,6 +55,7 @@ PROTOCOL_TLSv1 """ import textwrap +import re import _ssl # if we can't import it, let the error propagate @@ -85,6 +86,64 @@ import traceback import errno +class CertificateError(ValueError): + pass + + +def _dnsname_to_pat(dn): + pats = [] + for frag in dn.split(r'.'): + if frag == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + else: + # Otherwise, '*' matches any dotless fragment. + frag = re.escape(frag) + pats.append(frag.replace(r'\*', '[^.]*')) + return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules + are mostly followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if not san: + # The subject is only checked when subjectAltName is empty + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") + + class SSLContext(_SSLContext): """An SSLContext holds various SSL-related configuration options and data, such as certificates and possibly a private key.""" diff -r 08db9306e8af Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py Wed Oct 06 15:05:45 2010 +0200 +++ b/Lib/test/test_ssl.py Wed Oct 06 22:27:28 2010 +0200 @@ -208,6 +208,77 @@ class BasicSocketTests(unittest.TestCase ssl.wrap_socket(socket.socket(), certfile=WRONGCERT, keyfile=WRONGCERT) self.assertEqual(cm.exception.errno, errno.ENOENT) + def test_match_hostname(self): + def ok(cert, hostname): + ssl.match_hostname(cert, hostname) + def fail(cert, hostname): + self.assertRaises(ssl.CertificateError, + ssl.match_hostname, cert, hostname) + + cert = {'subject': ((('commonName', 'example.com'),),)} + ok(cert, 'example.com') + ok(cert, 'ExAmple.cOm') + fail(cert, 'www.example.com') + fail(cert, '.example.com') + fail(cert, 'example.org') + fail(cert, 'exampleXcom') + + cert = {'subject': ((('commonName', '*.a.com'),),)} + ok(cert, 'foo.a.com') + fail(cert, 'bar.foo.a.com') + fail(cert, 'a.com') + fail(cert, 'Xa.com') + fail(cert, '.a.com') + + cert = {'subject': ((('commonName', 'a.*.com'),),)} + ok(cert, 'a.foo.com') + fail(cert, 'a..com') + fail(cert, 'a.com') + + cert = {'subject': ((('commonName', 'f*.com'),),)} + ok(cert, 'foo.com') + ok(cert, 'f.com') + fail(cert, 'bar.com') + fail(cert, 'foo.a.com') + fail(cert, 'bar.foo.com') + + # Slightly fake real-world example + cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT', + 'subject': ((('commonName', 'linuxfrz.org'),),), + 'subjectAltName': (('DNS', 'linuxfr.org'), + ('DNS', 'linuxfr.com'), + ('othername', ''))} + ok(cert, 'linuxfr.org') + ok(cert, 'linuxfr.com') + # Not a "DNS" entry + fail(cert, '') + # When there is a subjectAltName, commonName isn't used + fail(cert, 'linuxfrz.org') + + # A pristine real-world example + cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT', + 'subject': ((('countryName', 'US'),), + (('stateOrProvinceName', 'California'),), + (('localityName', 'Mountain View'),), + (('organizationName', 'Google Inc'),), + (('commonName', 'mail.google.com'),))} + ok(cert, 'mail.google.com') + fail(cert, 'gmail.com') + # Only commonName is considered + fail(cert, 'California') + + # Neither commonName nor subjectAltName + cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT', + 'subject': ((('countryName', 'US'),), + (('stateOrProvinceName', 'California'),), + (('localityName', 'Mountain View'),), + (('organizationName', 'Google Inc'),))} + fail(cert, 'mail.google.com') + + # Empty cert / no cert + self.assertRaises(ValueError, ssl.match_hostname, None, 'example.com') + self.assertRaises(ValueError, ssl.match_hostname, {}, 'example.com') + class ContextTests(unittest.TestCase):