Skip to content

Commit 59fdd67

Browse files
committed
Issue #1589: Add ssl.match_hostname(), to help implement server identity
verification for higher-level protocols.
1 parent e75bc2c commit 59fdd67

4 files changed

Lines changed: 214 additions & 50 deletions

File tree

Doc/library/ssl.rst

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,27 @@ Functions, Constants, and Exceptions
4545

4646
.. exception:: SSLError
4747

48-
Raised to signal an error from the underlying SSL implementation. This
49-
signifies some problem in the higher-level encryption and authentication
50-
layer that's superimposed on the underlying network connection. This error
48+
Raised to signal an error from the underlying SSL implementation
49+
(currently provided by the OpenSSL library). This signifies some
50+
problem in the higher-level encryption and authentication layer that's
51+
superimposed on the underlying network connection. This error
5152
is a subtype of :exc:`socket.error`, which in turn is a subtype of
52-
:exc:`IOError`.
53+
:exc:`IOError`. The error code and message of :exc:`SSLError` instances
54+
are provided by the OpenSSL library.
55+
56+
.. exception:: CertificateError
57+
58+
Raised to signal an error with a certificate (such as mismatching
59+
hostname). Certificate errors detected by OpenSSL, though, raise
60+
an :exc:`SSLError`.
61+
62+
63+
Socket creation
64+
^^^^^^^^^^^^^^^
65+
66+
The following function allows for standalone socket creation. Starting from
67+
Python 3.2, it can be more flexible to use :meth:`SSLContext.wrap_socket`
68+
instead.
5369

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

@@ -139,6 +155,9 @@ Functions, Constants, and Exceptions
139155
.. versionchanged:: 3.2
140156
New optional argument *ciphers*.
141157

158+
Random generation
159+
^^^^^^^^^^^^^^^^^
160+
142161
.. function:: RAND_status()
143162

144163
Returns True if the SSL pseudo-random number generator has been seeded with
@@ -164,6 +183,32 @@ Functions, Constants, and Exceptions
164183
string (so you can always use :const:`0.0`). See :rfc:`1750` for more
165184
information on sources of entropy.
166185

186+
Certificate handling
187+
^^^^^^^^^^^^^^^^^^^^
188+
189+
.. function:: match_hostname(cert, hostname)
190+
191+
Verify that *cert* (in decoded format as returned by
192+
:meth:`SSLSocket.getpeercert`) matches the given *hostname*. The rules
193+
applied are those for checking the identity of HTTPS servers as outlined
194+
in :rfc:`2818`, except that IP addresses are not currently supported.
195+
In addition to HTTPS, this function should be suitable for checking the
196+
identity of servers in various SSL-based protocols such as FTPS, IMAPS,
197+
POPS and others.
198+
199+
:exc:`CertificateError` is raised on failure. On success, the function
200+
returns nothing::
201+
202+
>>> cert = {'subject': ((('commonName', 'example.com'),),)}
203+
>>> ssl.match_hostname(cert, "example.com")
204+
>>> ssl.match_hostname(cert, "example.org")
205+
Traceback (most recent call last):
206+
File "<stdin>", line 1, in <module>
207+
File "/home/py3k/Lib/ssl.py", line 130, in match_hostname
208+
ssl.CertificateError: hostname 'example.org' doesn't match 'example.com'
209+
210+
.. versionadded:: 3.2
211+
167212
.. function:: cert_time_to_seconds(timestring)
168213

169214
Returns a floating-point value containing a normal seconds-after-the-epoch
@@ -178,7 +223,6 @@ Functions, Constants, and Exceptions
178223
>>> import time
179224
>>> time.ctime(ssl.cert_time_to_seconds("May 9 00:00:00 2007 GMT"))
180225
'Wed May 9 00:00:00 2007'
181-
>>>
182226

183227
.. function:: get_server_certificate(addr, ssl_version=PROTOCOL_SSLv3, ca_certs=None)
184228

@@ -201,6 +245,9 @@ Functions, Constants, and Exceptions
201245
Given a certificate as an ASCII PEM string, returns a DER-encoded sequence of
202246
bytes for that same certificate.
203247

248+
Constants
249+
^^^^^^^^^
250+
204251
.. data:: CERT_NONE
205252

206253
Possible value for :attr:`SSLContext.verify_mode`, or the ``cert_reqs``
@@ -683,80 +730,67 @@ should use the following idiom::
683730
Client-side operation
684731
^^^^^^^^^^^^^^^^^^^^^
685732

686-
This example connects to an SSL server, prints the server's address and
687-
certificate, sends some bytes, and reads part of the response::
733+
This example connects to an SSL server and prints the server's certificate::
688734

689735
import socket, ssl, pprint
690736

691737
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
692-
693738
# require a certificate from the server
694739
ssl_sock = ssl.wrap_socket(s,
695740
ca_certs="/etc/ca_certs_file",
696741
cert_reqs=ssl.CERT_REQUIRED)
697-
698742
ssl_sock.connect(('www.verisign.com', 443))
699743

700-
print(repr(ssl_sock.getpeername()))
701744
pprint.pprint(ssl_sock.getpeercert())
702-
print(pprint.pformat(ssl_sock.getpeercert()))
703-
704-
# Set a simple HTTP request -- use http.client in actual code.
705-
ssl_sock.sendall(b"GET / HTTP/1.0\r\nHost: www.verisign.com\r\n\r\n")
706-
707-
# Read a chunk of data. Will not necessarily
708-
# read all the data returned by the server.
709-
data = ssl_sock.recv()
710-
711745
# note that closing the SSLSocket will also close the underlying socket
712746
ssl_sock.close()
713747

714-
As of September 6, 2007, the certificate printed by this program looked like
748+
As of October 6, 2010, the certificate printed by this program looks like
715749
this::
716750

717-
{'notAfter': 'May 8 23:59:59 2009 GMT',
718-
'subject': ((('serialNumber', '2497886'),),
719-
(('1.3.6.1.4.1.311.60.2.1.3', 'US'),),
720-
(('1.3.6.1.4.1.311.60.2.1.2', 'Delaware'),),
721-
(('countryName', 'US'),),
722-
(('postalCode', '94043'),),
723-
(('stateOrProvinceName', 'California'),),
724-
(('localityName', 'Mountain View'),),
725-
(('streetAddress', '487 East Middlefield Road'),),
726-
(('organizationName', 'VeriSign, Inc.'),),
727-
(('organizationalUnitName',
728-
'Production Security Services'),),
729-
(('organizationalUnitName',
730-
'Terms of use at www.verisign.com/rpa (c)06'),),
731-
(('commonName', 'www.verisign.com'),))}
732-
733-
which is a fairly poorly-formed ``subject`` field.
751+
{'notAfter': 'May 25 23:59:59 2012 GMT',
752+
'subject': ((('1.3.6.1.4.1.311.60.2.1.3', 'US'),),
753+
(('1.3.6.1.4.1.311.60.2.1.2', 'Delaware'),),
754+
(('businessCategory', 'V1.0, Clause 5.(b)'),),
755+
(('serialNumber', '2497886'),),
756+
(('countryName', 'US'),),
757+
(('postalCode', '94043'),),
758+
(('stateOrProvinceName', 'California'),),
759+
(('localityName', 'Mountain View'),),
760+
(('streetAddress', '487 East Middlefield Road'),),
761+
(('organizationName', 'VeriSign, Inc.'),),
762+
(('organizationalUnitName', ' Production Security Services'),),
763+
(('commonName', 'www.verisign.com'),))}
734764

735765
This other example first creates an SSL context, instructs it to verify
736766
certificates sent by peers, and feeds it a set of recognized certificate
737767
authorities (CA)::
738768

739769
>>> context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
740-
>>> context.verify_mode = ssl.CERT_OPTIONAL
770+
>>> context.verify_mode = ssl.CERT_REQUIRED
741771
>>> context.load_verify_locations("/etc/ssl/certs/ca-bundle.crt")
742772

743773
(it is assumed your operating system places a bundle of all CA certificates
744774
in ``/etc/ssl/certs/ca-bundle.crt``; if not, you'll get an error and have
745775
to adjust the location)
746776

747-
When you use the context to connect to a server, :const:`CERT_OPTIONAL`
777+
When you use the context to connect to a server, :const:`CERT_REQUIRED`
748778
validates the server certificate: it ensures that the server certificate
749779
was signed with one of the CA certificates, and checks the signature for
750780
correctness::
751781

752782
>>> conn = context.wrap_socket(socket.socket(socket.AF_INET))
753783
>>> conn.connect(("linuxfr.org", 443))
754784

755-
You should then fetch the certificate and check its fields for conformity.
756-
Here, the ``commonName`` field in the ``subject`` matches the desired HTTPS
757-
host ``linuxfr.org``::
785+
You should then fetch the certificate and check its fields for conformity::
758786

759-
>>> pprint.pprint(conn.getpeercert())
787+
>>> cert = conn.getpeercert()
788+
>>> ssl.match_hostname(cert, "linuxfr.org")
789+
790+
Visual inspection shows that the certificate does identify the desired service
791+
(that is, the HTTPS host ``linuxfr.org``)::
792+
793+
>>> pprint.pprint(cert)
760794
{'notAfter': 'Jun 26 21:41:46 2011 GMT',
761795
'subject': ((('commonName', 'linuxfr.org'),),),
762796
'subjectAltName': (('DNS', 'linuxfr.org'), ('othername', '<unsupported>'))}
@@ -776,7 +810,6 @@ the server::
776810
b'',
777811
b'']
778812

779-
780813
See the discussion of :ref:`ssl-security` below.
781814

782815

@@ -842,12 +875,10 @@ peer, it can be insecure, especially in client mode where most of time you
842875
would like to ensure the authenticity of the server you're talking to.
843876
Therefore, when in client mode, it is highly recommended to use
844877
:const:`CERT_REQUIRED`. However, it is in itself not sufficient; you also
845-
have to check that the server certificate (obtained with
846-
:meth:`SSLSocket.getpeercert`) matches the desired service. The exact way
847-
of doing so depends on the higher-level protocol used; for example, with
848-
HTTPS, you'll check that the host name in the URL matches either the
849-
``commonName`` field in the ``subjectName``, or one of the ``DNS`` fields
850-
in the ``subjectAltName``.
878+
have to check that the server certificate, which can be obtained by calling
879+
:meth:`SSLSocket.getpeercert`, matches the desired service. For many
880+
protocols and applications, the service can be identified by the hostname;
881+
in this case, the :func:`match_hostname` function can be used.
851882

852883
In server mode, if you want to authenticate your clients using the SSL layer
853884
(rather than using a higher-level authentication mechanism), you'll also have

Lib/ssl.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"""
5656

5757
import textwrap
58+
import re
5859

5960
import _ssl # if we can't import it, let the error propagate
6061

@@ -85,6 +86,64 @@
8586
import errno
8687

8788

89+
class CertificateError(ValueError):
90+
pass
91+
92+
93+
def _dnsname_to_pat(dn):
94+
pats = []
95+
for frag in dn.split(r'.'):
96+
if frag == '*':
97+
# When '*' is a fragment by itself, it matches a non-empty dotless
98+
# fragment.
99+
pats.append('[^.]+')
100+
else:
101+
# Otherwise, '*' matches any dotless fragment.
102+
frag = re.escape(frag)
103+
pats.append(frag.replace(r'\*', '[^.]*'))
104+
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
105+
106+
107+
def match_hostname(cert, hostname):
108+
"""Verify that *cert* (in decoded format as returned by
109+
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
110+
are mostly followed, but IP addresses are not accepted for *hostname*.
111+
112+
CertificateError is raised on failure. On success, the function
113+
returns nothing.
114+
"""
115+
if not cert:
116+
raise ValueError("empty or no certificate")
117+
dnsnames = []
118+
san = cert.get('subjectAltName', ())
119+
for key, value in san:
120+
if key == 'DNS':
121+
if _dnsname_to_pat(value).match(hostname):
122+
return
123+
dnsnames.append(value)
124+
if not san:
125+
# The subject is only checked when subjectAltName is empty
126+
for sub in cert.get('subject', ()):
127+
for key, value in sub:
128+
# XXX according to RFC 2818, the most specific Common Name
129+
# must be used.
130+
if key == 'commonName':
131+
if _dnsname_to_pat(value).match(hostname):
132+
return
133+
dnsnames.append(value)
134+
if len(dnsnames) > 1:
135+
raise CertificateError("hostname %r "
136+
"doesn't match either of %s"
137+
% (hostname, ', '.join(map(repr, dnsnames))))
138+
elif len(dnsnames) == 1:
139+
raise CertificateError("hostname %r "
140+
"doesn't match %r"
141+
% (hostname, dnsnames[0]))
142+
else:
143+
raise CertificateError("no appropriate commonName or "
144+
"subjectAltName fields were found")
145+
146+
88147
class SSLContext(_SSLContext):
89148
"""An SSLContext holds various SSL-related configuration options and
90149
data, such as certificates and possibly a private key."""

Lib/test/test_ssl.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,77 @@ def test_errors(self):
208208
ssl.wrap_socket(socket.socket(), certfile=WRONGCERT, keyfile=WRONGCERT)
209209
self.assertEqual(cm.exception.errno, errno.ENOENT)
210210

211+
def test_match_hostname(self):
212+
def ok(cert, hostname):
213+
ssl.match_hostname(cert, hostname)
214+
def fail(cert, hostname):
215+
self.assertRaises(ssl.CertificateError,
216+
ssl.match_hostname, cert, hostname)
217+
218+
cert = {'subject': ((('commonName', 'example.com'),),)}
219+
ok(cert, 'example.com')
220+
ok(cert, 'ExAmple.cOm')
221+
fail(cert, 'www.example.com')
222+
fail(cert, '.example.com')
223+
fail(cert, 'example.org')
224+
fail(cert, 'exampleXcom')
225+
226+
cert = {'subject': ((('commonName', '*.a.com'),),)}
227+
ok(cert, 'foo.a.com')
228+
fail(cert, 'bar.foo.a.com')
229+
fail(cert, 'a.com')
230+
fail(cert, 'Xa.com')
231+
fail(cert, '.a.com')
232+
233+
cert = {'subject': ((('commonName', 'a.*.com'),),)}
234+
ok(cert, 'a.foo.com')
235+
fail(cert, 'a..com')
236+
fail(cert, 'a.com')
237+
238+
cert = {'subject': ((('commonName', 'f*.com'),),)}
239+
ok(cert, 'foo.com')
240+
ok(cert, 'f.com')
241+
fail(cert, 'bar.com')
242+
fail(cert, 'foo.a.com')
243+
fail(cert, 'bar.foo.com')
244+
245+
# Slightly fake real-world example
246+
cert = {'notAfter': 'Jun 26 21:41:46 2011 GMT',
247+
'subject': ((('commonName', 'linuxfrz.org'),),),
248+
'subjectAltName': (('DNS', 'linuxfr.org'),
249+
('DNS', 'linuxfr.com'),
250+
('othername', '<unsupported>'))}
251+
ok(cert, 'linuxfr.org')
252+
ok(cert, 'linuxfr.com')
253+
# Not a "DNS" entry
254+
fail(cert, '<unsupported>')
255+
# When there is a subjectAltName, commonName isn't used
256+
fail(cert, 'linuxfrz.org')
257+
258+
# A pristine real-world example
259+
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
260+
'subject': ((('countryName', 'US'),),
261+
(('stateOrProvinceName', 'California'),),
262+
(('localityName', 'Mountain View'),),
263+
(('organizationName', 'Google Inc'),),
264+
(('commonName', 'mail.google.com'),))}
265+
ok(cert, 'mail.google.com')
266+
fail(cert, 'gmail.com')
267+
# Only commonName is considered
268+
fail(cert, 'California')
269+
270+
# Neither commonName nor subjectAltName
271+
cert = {'notAfter': 'Dec 18 23:59:59 2011 GMT',
272+
'subject': ((('countryName', 'US'),),
273+
(('stateOrProvinceName', 'California'),),
274+
(('localityName', 'Mountain View'),),
275+
(('organizationName', 'Google Inc'),))}
276+
fail(cert, 'mail.google.com')
277+
278+
# Empty cert / no cert
279+
self.assertRaises(ValueError, ssl.match_hostname, None, 'example.com')
280+
self.assertRaises(ValueError, ssl.match_hostname, {}, 'example.com')
281+
211282

212283
class ContextTests(unittest.TestCase):
213284

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ Core and Builtins
9292
Library
9393
-------
9494

95+
- Issue #1589: Add ssl.match_hostname(), to help implement server identity
96+
verification for higher-level protocols.
97+
9598
- Issue #9759: GzipFile now raises ValueError when an operation is attempted
9699
after the file is closed. Patch by Jeffrey Finkelstein.
97100

0 commit comments

Comments
 (0)