Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from __future__ import absolute_import 

2 

3import hmac 

4import os 

5import sys 

6import warnings 

7from binascii import hexlify, unhexlify 

8from hashlib import md5, sha1, sha256 

9 

10from ..exceptions import ( 

11 InsecurePlatformWarning, 

12 ProxySchemeUnsupported, 

13 SNIMissingWarning, 

14 SSLError, 

15) 

16from ..packages import six 

17from .url import BRACELESS_IPV6_ADDRZ_RE, IPV4_RE 

18 

19SSLContext = None 

20SSLTransport = None 

21HAS_SNI = False 

22IS_PYOPENSSL = False 

23IS_SECURETRANSPORT = False 

24ALPN_PROTOCOLS = ["http/1.1"] 

25 

26# Maps the length of a digest to a possible hash function producing this digest 

27HASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256} 

28 

29 

30def _const_compare_digest_backport(a, b): 

31 """ 

32 Compare two digests of equal length in constant time. 

33 

34 The digests must be of type str/bytes. 

35 Returns True if the digests match, and False otherwise. 

36 """ 

37 result = abs(len(a) - len(b)) 

38 for left, right in zip(bytearray(a), bytearray(b)): 

39 result |= left ^ right 

40 return result == 0 

41 

42 

43_const_compare_digest = getattr(hmac, "compare_digest", _const_compare_digest_backport) 

44 

45try: # Test for SSL features 

46 import ssl 

47 from ssl import CERT_REQUIRED, wrap_socket 

48except ImportError: 

49 pass 

50 

51try: 

52 from ssl import HAS_SNI # Has SNI? 

53except ImportError: 

54 pass 

55 

56try: 

57 from .ssltransport import SSLTransport 

58except ImportError: 

59 pass 

60 

61 

62try: # Platform-specific: Python 3.6 

63 from ssl import PROTOCOL_TLS 

64 

65 PROTOCOL_SSLv23 = PROTOCOL_TLS 

66except ImportError: 

67 try: 

68 from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS 

69 

70 PROTOCOL_SSLv23 = PROTOCOL_TLS 

71 except ImportError: 

72 PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 

73 

74 

75try: 

76 from ssl import OP_NO_COMPRESSION, OP_NO_SSLv2, OP_NO_SSLv3 

77except ImportError: 

78 OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000 

79 OP_NO_COMPRESSION = 0x20000 

80 

81 

82try: # OP_NO_TICKET was added in Python 3.6 

83 from ssl import OP_NO_TICKET 

84except ImportError: 

85 OP_NO_TICKET = 0x4000 

86 

87 

88# A secure default. 

89# Sources for more information on TLS ciphers: 

90# 

91# - https://wiki.mozilla.org/Security/Server_Side_TLS 

92# - https://www.ssllabs.com/projects/best-practices/index.html 

93# - https://hynek.me/articles/hardening-your-web-servers-ssl-ciphers/ 

94# 

95# The general intent is: 

96# - prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE), 

97# - prefer ECDHE over DHE for better performance, 

98# - prefer any AES-GCM and ChaCha20 over any AES-CBC for better performance and 

99# security, 

100# - prefer AES-GCM over ChaCha20 because hardware-accelerated AES is common, 

101# - disable NULL authentication, MD5 MACs, DSS, and other 

102# insecure ciphers for security reasons. 

103# - NOTE: TLS 1.3 cipher suites are managed through a different interface 

104# not exposed by CPython (yet!) and are enabled by default if they're available. 

105DEFAULT_CIPHERS = ":".join( 

106 [ 

107 "ECDHE+AESGCM", 

108 "ECDHE+CHACHA20", 

109 "DHE+AESGCM", 

110 "DHE+CHACHA20", 

111 "ECDH+AESGCM", 

112 "DH+AESGCM", 

113 "ECDH+AES", 

114 "DH+AES", 

115 "RSA+AESGCM", 

116 "RSA+AES", 

117 "!aNULL", 

118 "!eNULL", 

119 "!MD5", 

120 "!DSS", 

121 ] 

122) 

123 

124try: 

125 from ssl import SSLContext # Modern SSL? 

126except ImportError: 

127 

128 class SSLContext(object): # Platform-specific: Python 2 

129 def __init__(self, protocol_version): 

130 self.protocol = protocol_version 

131 # Use default values from a real SSLContext 

132 self.check_hostname = False 

133 self.verify_mode = ssl.CERT_NONE 

134 self.ca_certs = None 

135 self.options = 0 

136 self.certfile = None 

137 self.keyfile = None 

138 self.ciphers = None 

139 

140 def load_cert_chain(self, certfile, keyfile): 

141 self.certfile = certfile 

142 self.keyfile = keyfile 

143 

144 def load_verify_locations(self, cafile=None, capath=None, cadata=None): 

145 self.ca_certs = cafile 

146 

147 if capath is not None: 

148 raise SSLError("CA directories not supported in older Pythons") 

149 

150 if cadata is not None: 

151 raise SSLError("CA data not supported in older Pythons") 

152 

153 def set_ciphers(self, cipher_suite): 

154 self.ciphers = cipher_suite 

155 

156 def wrap_socket(self, socket, server_hostname=None, server_side=False): 

157 warnings.warn( 

158 "A true SSLContext object is not available. This prevents " 

159 "urllib3 from configuring SSL appropriately and may cause " 

160 "certain SSL connections to fail. You can upgrade to a newer " 

161 "version of Python to solve this. For more information, see " 

162 "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" 

163 "#ssl-warnings", 

164 InsecurePlatformWarning, 

165 ) 

166 kwargs = { 

167 "keyfile": self.keyfile, 

168 "certfile": self.certfile, 

169 "ca_certs": self.ca_certs, 

170 "cert_reqs": self.verify_mode, 

171 "ssl_version": self.protocol, 

172 "server_side": server_side, 

173 } 

174 return wrap_socket(socket, ciphers=self.ciphers, **kwargs) 

175 

176 

177def assert_fingerprint(cert, fingerprint): 

178 """ 

179 Checks if given fingerprint matches the supplied certificate. 

180 

181 :param cert: 

182 Certificate as bytes object. 

183 :param fingerprint: 

184 Fingerprint as string of hexdigits, can be interspersed by colons. 

185 """ 

186 

187 fingerprint = fingerprint.replace(":", "").lower() 

188 digest_length = len(fingerprint) 

189 hashfunc = HASHFUNC_MAP.get(digest_length) 

190 if not hashfunc: 

191 raise SSLError("Fingerprint of invalid length: {0}".format(fingerprint)) 

192 

193 # We need encode() here for py32; works on py2 and p33. 

194 fingerprint_bytes = unhexlify(fingerprint.encode()) 

195 

196 cert_digest = hashfunc(cert).digest() 

197 

198 if not _const_compare_digest(cert_digest, fingerprint_bytes): 

199 raise SSLError( 

200 'Fingerprints did not match. Expected "{0}", got "{1}".'.format( 

201 fingerprint, hexlify(cert_digest) 

202 ) 

203 ) 

204 

205 

206def resolve_cert_reqs(candidate): 

207 """ 

208 Resolves the argument to a numeric constant, which can be passed to 

209 the wrap_socket function/method from the ssl module. 

210 Defaults to :data:`ssl.CERT_REQUIRED`. 

211 If given a string it is assumed to be the name of the constant in the 

212 :mod:`ssl` module or its abbreviation. 

213 (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. 

214 If it's neither `None` nor a string we assume it is already the numeric 

215 constant which can directly be passed to wrap_socket. 

216 """ 

217 if candidate is None: 

218 return CERT_REQUIRED 

219 

220 if isinstance(candidate, str): 

221 res = getattr(ssl, candidate, None) 

222 if res is None: 

223 res = getattr(ssl, "CERT_" + candidate) 

224 return res 

225 

226 return candidate 

227 

228 

229def resolve_ssl_version(candidate): 

230 """ 

231 like resolve_cert_reqs 

232 """ 

233 if candidate is None: 

234 return PROTOCOL_TLS 

235 

236 if isinstance(candidate, str): 

237 res = getattr(ssl, candidate, None) 

238 if res is None: 

239 res = getattr(ssl, "PROTOCOL_" + candidate) 

240 return res 

241 

242 return candidate 

243 

244 

245def create_urllib3_context( 

246 ssl_version=None, cert_reqs=None, options=None, ciphers=None 

247): 

248 """All arguments have the same meaning as ``ssl_wrap_socket``. 

249 

250 By default, this function does a lot of the same work that 

251 ``ssl.create_default_context`` does on Python 3.4+. It: 

252 

253 - Disables SSLv2, SSLv3, and compression 

254 - Sets a restricted set of server ciphers 

255 

256 If you wish to enable SSLv3, you can do:: 

257 

258 from urllib3.util import ssl_ 

259 context = ssl_.create_urllib3_context() 

260 context.options &= ~ssl_.OP_NO_SSLv3 

261 

262 You can do the same to enable compression (substituting ``COMPRESSION`` 

263 for ``SSLv3`` in the last line above). 

264 

265 :param ssl_version: 

266 The desired protocol version to use. This will default to 

267 PROTOCOL_SSLv23 which will negotiate the highest protocol that both 

268 the server and your installation of OpenSSL support. 

269 :param cert_reqs: 

270 Whether to require the certificate verification. This defaults to 

271 ``ssl.CERT_REQUIRED``. 

272 :param options: 

273 Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, 

274 ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. 

275 :param ciphers: 

276 Which cipher suites to allow the server to select. 

277 :returns: 

278 Constructed SSLContext object with specified options 

279 :rtype: SSLContext 

280 """ 

281 context = SSLContext(ssl_version or PROTOCOL_TLS) 

282 

283 context.set_ciphers(ciphers or DEFAULT_CIPHERS) 

284 

285 # Setting the default here, as we may have no ssl module on import 

286 cert_reqs = ssl.CERT_REQUIRED if cert_reqs is None else cert_reqs 

287 

288 if options is None: 

289 options = 0 

290 # SSLv2 is easily broken and is considered harmful and dangerous 

291 options |= OP_NO_SSLv2 

292 # SSLv3 has several problems and is now dangerous 

293 options |= OP_NO_SSLv3 

294 # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ 

295 # (issue #309) 

296 options |= OP_NO_COMPRESSION 

297 # TLSv1.2 only. Unless set explicitly, do not request tickets. 

298 # This may save some bandwidth on wire, and although the ticket is encrypted, 

299 # there is a risk associated with it being on wire, 

300 # if the server is not rotating its ticketing keys properly. 

301 options |= OP_NO_TICKET 

302 

303 context.options |= options 

304 

305 # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is 

306 # necessary for conditional client cert authentication with TLS 1.3. 

307 # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older 

308 # versions of Python. We only enable on Python 3.7.4+ or if certificate 

309 # verification is enabled to work around Python issue #37428 

310 # See: https://bugs.python.org/issue37428 

311 if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr( 

312 context, "post_handshake_auth", None 

313 ) is not None: 

314 context.post_handshake_auth = True 

315 

316 context.verify_mode = cert_reqs 

317 if ( 

318 getattr(context, "check_hostname", None) is not None 

319 ): # Platform-specific: Python 3.2 

320 # We do our own verification, including fingerprints and alternative 

321 # hostnames. So disable it here 

322 context.check_hostname = False 

323 

324 # Enable logging of TLS session keys via defacto standard environment variable 

325 # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values. 

326 if hasattr(context, "keylog_filename"): 

327 sslkeylogfile = os.environ.get("SSLKEYLOGFILE") 

328 if sslkeylogfile: 

329 context.keylog_filename = sslkeylogfile 

330 

331 return context 

332 

333 

334def ssl_wrap_socket( 

335 sock, 

336 keyfile=None, 

337 certfile=None, 

338 cert_reqs=None, 

339 ca_certs=None, 

340 server_hostname=None, 

341 ssl_version=None, 

342 ciphers=None, 

343 ssl_context=None, 

344 ca_cert_dir=None, 

345 key_password=None, 

346 ca_cert_data=None, 

347 tls_in_tls=False, 

348): 

349 """ 

350 All arguments except for server_hostname, ssl_context, and ca_cert_dir have 

351 the same meaning as they do when using :func:`ssl.wrap_socket`. 

352 

353 :param server_hostname: 

354 When SNI is supported, the expected hostname of the certificate 

355 :param ssl_context: 

356 A pre-made :class:`SSLContext` object. If none is provided, one will 

357 be created using :func:`create_urllib3_context`. 

358 :param ciphers: 

359 A string of ciphers we wish the client to support. 

360 :param ca_cert_dir: 

361 A directory containing CA certificates in multiple separate files, as 

362 supported by OpenSSL's -CApath flag or the capath argument to 

363 SSLContext.load_verify_locations(). 

364 :param key_password: 

365 Optional password if the keyfile is encrypted. 

366 :param ca_cert_data: 

367 Optional string containing CA certificates in PEM format suitable for 

368 passing as the cadata parameter to SSLContext.load_verify_locations() 

369 :param tls_in_tls: 

370 Use SSLTransport to wrap the existing socket. 

371 """ 

372 context = ssl_context 

373 if context is None: 

374 # Note: This branch of code and all the variables in it are no longer 

375 # used by urllib3 itself. We should consider deprecating and removing 

376 # this code. 

377 context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) 

378 

379 if ca_certs or ca_cert_dir or ca_cert_data: 

380 try: 

381 context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) 

382 except (IOError, OSError) as e: 

383 raise SSLError(e) 

384 

385 elif ssl_context is None and hasattr(context, "load_default_certs"): 

386 # try to load OS default certs; works well on Windows (require Python3.4+) 

387 context.load_default_certs() 

388 

389 # Attempt to detect if we get the goofy behavior of the 

390 # keyfile being encrypted and OpenSSL asking for the 

391 # passphrase via the terminal and instead error out. 

392 if keyfile and key_password is None and _is_key_file_encrypted(keyfile): 

393 raise SSLError("Client private key is encrypted, password is required") 

394 

395 if certfile: 

396 if key_password is None: 

397 context.load_cert_chain(certfile, keyfile) 

398 else: 

399 context.load_cert_chain(certfile, keyfile, key_password) 

400 

401 try: 

402 if hasattr(context, "set_alpn_protocols"): 

403 context.set_alpn_protocols(ALPN_PROTOCOLS) 

404 except NotImplementedError: 

405 pass 

406 

407 # If we detect server_hostname is an IP address then the SNI 

408 # extension should not be used according to RFC3546 Section 3.1 

409 use_sni_hostname = server_hostname and not is_ipaddress(server_hostname) 

410 # SecureTransport uses server_hostname in certificate verification. 

411 send_sni = (use_sni_hostname and HAS_SNI) or ( 

412 IS_SECURETRANSPORT and server_hostname 

413 ) 

414 # Do not warn the user if server_hostname is an invalid SNI hostname. 

415 if not HAS_SNI and use_sni_hostname: 

416 warnings.warn( 

417 "An HTTPS request has been made, but the SNI (Server Name " 

418 "Indication) extension to TLS is not available on this platform. " 

419 "This may cause the server to present an incorrect TLS " 

420 "certificate, which can cause validation failures. You can upgrade to " 

421 "a newer version of Python to solve this. For more information, see " 

422 "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" 

423 "#ssl-warnings", 

424 SNIMissingWarning, 

425 ) 

426 

427 if send_sni: 

428 ssl_sock = _ssl_wrap_socket_impl( 

429 sock, context, tls_in_tls, server_hostname=server_hostname 

430 ) 

431 else: 

432 ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls) 

433 return ssl_sock 

434 

435 

436def is_ipaddress(hostname): 

437 """Detects whether the hostname given is an IPv4 or IPv6 address. 

438 Also detects IPv6 addresses with Zone IDs. 

439 

440 :param str hostname: Hostname to examine. 

441 :return: True if the hostname is an IP address, False otherwise. 

442 """ 

443 if not six.PY2 and isinstance(hostname, bytes): 

444 # IDN A-label bytes are ASCII compatible. 

445 hostname = hostname.decode("ascii") 

446 return bool(IPV4_RE.match(hostname) or BRACELESS_IPV6_ADDRZ_RE.match(hostname)) 

447 

448 

449def _is_key_file_encrypted(key_file): 

450 """Detects if a key file is encrypted or not.""" 

451 with open(key_file, "r") as f: 

452 for line in f: 

453 # Look for Proc-Type: 4,ENCRYPTED 

454 if "ENCRYPTED" in line: 

455 return True 

456 

457 return False 

458 

459 

460def _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname=None): 

461 if tls_in_tls: 

462 if not SSLTransport: 

463 # Import error, ssl is not available. 

464 raise ProxySchemeUnsupported( 

465 "TLS in TLS requires support for the 'ssl' module" 

466 ) 

467 

468 SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) 

469 return SSLTransport(sock, ssl_context, server_hostname) 

470 

471 if server_hostname: 

472 return ssl_context.wrap_socket(sock, server_hostname=server_hostname) 

473 else: 

474 return ssl_context.wrap_socket(sock)