You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
8.4KB

  1. import os
  2. import socket
  3. import atexit
  4. import re
  5. import functools
  6. import urllib.request
  7. import http.client
  8. from pkg_resources import ResolutionError, ExtractionError
  9. try:
  10. import ssl
  11. except ImportError:
  12. ssl = None
  13. __all__ = [
  14. 'VerifyingHTTPSHandler', 'find_ca_bundle', 'is_available', 'cert_paths',
  15. 'opener_for'
  16. ]
  17. cert_paths = """
  18. /etc/pki/tls/certs/ca-bundle.crt
  19. /etc/ssl/certs/ca-certificates.crt
  20. /usr/share/ssl/certs/ca-bundle.crt
  21. /usr/local/share/certs/ca-root.crt
  22. /etc/ssl/cert.pem
  23. /System/Library/OpenSSL/certs/cert.pem
  24. /usr/local/share/certs/ca-root-nss.crt
  25. /etc/ssl/ca-bundle.pem
  26. """.strip().split()
  27. try:
  28. HTTPSHandler = urllib.request.HTTPSHandler
  29. HTTPSConnection = http.client.HTTPSConnection
  30. except AttributeError:
  31. HTTPSHandler = HTTPSConnection = object
  32. is_available = ssl is not None and object not in (
  33. HTTPSHandler, HTTPSConnection)
  34. try:
  35. from ssl import CertificateError, match_hostname
  36. except ImportError:
  37. try:
  38. from backports.ssl_match_hostname import CertificateError
  39. from backports.ssl_match_hostname import match_hostname
  40. except ImportError:
  41. CertificateError = None
  42. match_hostname = None
  43. if not CertificateError:
  44. class CertificateError(ValueError):
  45. pass
  46. if not match_hostname: # noqa: C901 # 'If 59' is too complex (21) # FIXME
  47. def _dnsname_match(dn, hostname, max_wildcards=1):
  48. """Matching according to RFC 6125, section 6.4.3
  49. https://tools.ietf.org/html/rfc6125#section-6.4.3
  50. """
  51. pats = []
  52. if not dn:
  53. return False
  54. # Ported from python3-syntax:
  55. # leftmost, *remainder = dn.split(r'.')
  56. parts = dn.split(r'.')
  57. leftmost = parts[0]
  58. remainder = parts[1:]
  59. wildcards = leftmost.count('*')
  60. if wildcards > max_wildcards:
  61. # Issue #17980: avoid denials of service by refusing more
  62. # than one wildcard per fragment. A survey of established
  63. # policy among SSL implementations showed it to be a
  64. # reasonable choice.
  65. raise CertificateError(
  66. "too many wildcards in certificate DNS name: " + repr(dn))
  67. # speed up common case w/o wildcards
  68. if not wildcards:
  69. return dn.lower() == hostname.lower()
  70. # RFC 6125, section 6.4.3, subitem 1.
  71. # The client SHOULD NOT attempt to match a
  72. # presented identifier in which the wildcard
  73. # character comprises a label other than the
  74. # left-most label.
  75. if leftmost == '*':
  76. # When '*' is a fragment by itself, it matches a non-empty dotless
  77. # fragment.
  78. pats.append('[^.]+')
  79. elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
  80. # RFC 6125, section 6.4.3, subitem 3.
  81. # The client SHOULD NOT attempt to match a presented identifier
  82. # where the wildcard character is embedded within an A-label or
  83. # U-label of an internationalized domain name.
  84. pats.append(re.escape(leftmost))
  85. else:
  86. # Otherwise, '*' matches any dotless string, e.g. www*
  87. pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
  88. # add the remaining fragments, ignore any wildcards
  89. for frag in remainder:
  90. pats.append(re.escape(frag))
  91. pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
  92. return pat.match(hostname)
  93. def match_hostname(cert, hostname):
  94. """Verify that *cert* (in decoded format as returned by
  95. SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
  96. rules are followed, but IP addresses are not accepted for *hostname*.
  97. CertificateError is raised on failure. On success, the function
  98. returns nothing.
  99. """
  100. if not cert:
  101. raise ValueError("empty or no certificate")
  102. dnsnames = []
  103. san = cert.get('subjectAltName', ())
  104. for key, value in san:
  105. if key == 'DNS':
  106. if _dnsname_match(value, hostname):
  107. return
  108. dnsnames.append(value)
  109. if not dnsnames:
  110. # The subject is only checked when there is no dNSName entry
  111. # in subjectAltName
  112. for sub in cert.get('subject', ()):
  113. for key, value in sub:
  114. # XXX according to RFC 2818, the most specific Common Name
  115. # must be used.
  116. if key == 'commonName':
  117. if _dnsname_match(value, hostname):
  118. return
  119. dnsnames.append(value)
  120. if len(dnsnames) > 1:
  121. raise CertificateError(
  122. "hostname %r doesn't match either of %s"
  123. % (hostname, ', '.join(map(repr, dnsnames))))
  124. elif len(dnsnames) == 1:
  125. raise CertificateError(
  126. "hostname %r doesn't match %r"
  127. % (hostname, dnsnames[0]))
  128. else:
  129. raise CertificateError(
  130. "no appropriate commonName or "
  131. "subjectAltName fields were found")
  132. class VerifyingHTTPSHandler(HTTPSHandler):
  133. """Simple verifying handler: no auth, subclasses, timeouts, etc."""
  134. def __init__(self, ca_bundle):
  135. self.ca_bundle = ca_bundle
  136. HTTPSHandler.__init__(self)
  137. def https_open(self, req):
  138. return self.do_open(
  139. lambda host, **kw: VerifyingHTTPSConn(host, self.ca_bundle, **kw),
  140. req
  141. )
  142. class VerifyingHTTPSConn(HTTPSConnection):
  143. """Simple verifying connection: no auth, subclasses, timeouts, etc."""
  144. def __init__(self, host, ca_bundle, **kw):
  145. HTTPSConnection.__init__(self, host, **kw)
  146. self.ca_bundle = ca_bundle
  147. def connect(self):
  148. sock = socket.create_connection(
  149. (self.host, self.port), getattr(self, 'source_address', None)
  150. )
  151. # Handle the socket if a (proxy) tunnel is present
  152. if hasattr(self, '_tunnel') and getattr(self, '_tunnel_host', None):
  153. self.sock = sock
  154. self._tunnel()
  155. # http://bugs.python.org/issue7776: Python>=3.4.1 and >=2.7.7
  156. # change self.host to mean the proxy server host when tunneling is
  157. # being used. Adapt, since we are interested in the destination
  158. # host for the match_hostname() comparison.
  159. actual_host = self._tunnel_host
  160. else:
  161. actual_host = self.host
  162. if hasattr(ssl, 'create_default_context'):
  163. ctx = ssl.create_default_context(cafile=self.ca_bundle)
  164. self.sock = ctx.wrap_socket(sock, server_hostname=actual_host)
  165. else:
  166. # This is for python < 2.7.9 and < 3.4?
  167. self.sock = ssl.wrap_socket(
  168. sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle
  169. )
  170. try:
  171. match_hostname(self.sock.getpeercert(), actual_host)
  172. except CertificateError:
  173. self.sock.shutdown(socket.SHUT_RDWR)
  174. self.sock.close()
  175. raise
  176. def opener_for(ca_bundle=None):
  177. """Get a urlopen() replacement that uses ca_bundle for verification"""
  178. return urllib.request.build_opener(
  179. VerifyingHTTPSHandler(ca_bundle or find_ca_bundle())
  180. ).open
  181. # from jaraco.functools
  182. def once(func):
  183. @functools.wraps(func)
  184. def wrapper(*args, **kwargs):
  185. if not hasattr(func, 'always_returns'):
  186. func.always_returns = func(*args, **kwargs)
  187. return func.always_returns
  188. return wrapper
  189. @once
  190. def get_win_certfile():
  191. try:
  192. import wincertstore
  193. except ImportError:
  194. return None
  195. class CertFile(wincertstore.CertFile):
  196. def __init__(self):
  197. super(CertFile, self).__init__()
  198. atexit.register(self.close)
  199. def close(self):
  200. try:
  201. super(CertFile, self).close()
  202. except OSError:
  203. pass
  204. _wincerts = CertFile()
  205. _wincerts.addstore('CA')
  206. _wincerts.addstore('ROOT')
  207. return _wincerts.name
  208. def find_ca_bundle():
  209. """Return an existing CA bundle path, or None"""
  210. extant_cert_paths = filter(os.path.isfile, cert_paths)
  211. return (
  212. get_win_certfile()
  213. or next(extant_cert_paths, None)
  214. or _certifi_where()
  215. )
  216. def _certifi_where():
  217. try:
  218. return __import__('certifi').where()
  219. except (ImportError, ResolutionError, ExtractionError):
  220. pass