Skip to content

Commit 4e66d78

Browse files
committed
first pass at verify_fingerprint
1 parent 3e60305 commit 4e66d78

File tree

2 files changed

+38
-4
lines changed

2 files changed

+38
-4
lines changed

aiohttp/connector.py

+34-4
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@
88
import traceback
99
import warnings
1010

11+
from binascii import unhexlify
1112
from collections import defaultdict
13+
from hashlib import md5, sha1, sha256
1214
from itertools import chain
1315
from math import ceil
1416

1517
from . import hdrs
1618
from .client import ClientRequest
1719
from .errors import ServerDisconnectedError
1820
from .errors import HttpProxyError, ProxyConnectionError
19-
from .errors import ClientOSError, ClientTimeoutError
21+
from .errors import ClientOSError, ClientTimeoutError, FingerprintMismatch
2022
from .helpers import BasicAuth
2123

2224

@@ -25,6 +27,12 @@
2527
PY_34 = sys.version_info >= (3, 4)
2628
PY_343 = sys.version_info >= (3, 4, 3)
2729

30+
HASHFUNC_BY_DIGESTLEN = {
31+
16: md5,
32+
20: sha1,
33+
32: sha256,
34+
}
35+
2836

2937
class Connection(object):
3038

@@ -347,13 +355,15 @@ class TCPConnector(BaseConnector):
347355
"""TCP connector.
348356
349357
:param bool verify_ssl: Set to True to check ssl certifications.
358+
:param str verify_fingerprint: Set to a string of hex digits to
359+
verify the ssl cert fingerprint matches.
350360
:param bool resolve: Set to True to do DNS lookup for host name.
351361
:param family: socket address family
352362
:param args: see :class:`BaseConnector`
353363
:param kwargs: see :class:`BaseConnector`
354364
"""
355365

356-
def __init__(self, *, verify_ssl=True,
366+
def __init__(self, *, verify_ssl=True, verify_fingerprint=None,
357367
resolve=False, family=socket.AF_INET, ssl_context=None,
358368
**kwargs):
359369
super().__init__(**kwargs)
@@ -364,6 +374,14 @@ def __init__(self, *, verify_ssl=True,
364374
"verify_ssl=False or specify ssl_context, not both.")
365375

366376
self._verify_ssl = verify_ssl
377+
if verify_fingerprint:
378+
verify_fingerprint = verify_fingerprint.replace(':', '').lower()
379+
digestlen, odd = divmod(len(verify_fingerprint), 2)
380+
if odd or digestlen not in HASHFUNC_BY_DIGESTLEN:
381+
raise ValueError('Fingerprint is of invalid length.')
382+
self._hashfunc = HASHFUNC_BY_DIGESTLEN[digestlen]
383+
self._fingerprint_bytes = unhexlify(verify_fingerprint)
384+
self._verify_fingerprint = verify_fingerprint
367385
self._ssl_context = ssl_context
368386
self._family = family
369387
self._resolve = resolve
@@ -374,6 +392,11 @@ def verify_ssl(self):
374392
"""Do check for ssl certifications?"""
375393
return self._verify_ssl
376394

395+
@property
396+
def verify_fingerprint(self):
397+
"""Verify ssl cert fingerprint matches?"""
398+
return self._verify_fingerprint
399+
377400
@property
378401
def ssl_context(self):
379402
"""SSLContext instance for https requests.
@@ -464,11 +487,18 @@ def _create_connection(self, req):
464487

465488
for hinfo in hosts:
466489
try:
467-
return (yield from self._loop.create_connection(
490+
conn = yield from self._loop.create_connection(
468491
self._factory, hinfo['host'], hinfo['port'],
469492
ssl=sslcontext, family=hinfo['family'],
470493
proto=hinfo['proto'], flags=hinfo['flags'],
471-
server_hostname=hinfo['hostname'] if sslcontext else None))
494+
server_hostname=hinfo['hostname'] if sslcontext else None)
495+
if self._verify_fingerprint:
496+
sock = conn[0]._sock
497+
cert = sock.getpeercert(True)
498+
digest = self._hashfunc(cert).digest()
499+
if digest != self._fingerprint_bytes:
500+
raise FingerprintMismatch
501+
return conn
472502
except OSError as e:
473503
exc = e
474504
else:

aiohttp/errors.py

+4
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,7 @@ class LineLimitExceededParserError(ParserError):
170170
def __init__(self, msg, limit):
171171
super().__init__(msg)
172172
self.limit = limit
173+
174+
175+
class FingerprintMismatch(Exception):
176+
"""SSL certificate does not match expected fingerprint."""

0 commit comments

Comments
 (0)