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.

260 lines
8.3 KiB

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