Upstream version 10.38.222.0
[platform/framework/web/crosswalk.git] / src / tools / findit / https.py
1 # Copyright (c) 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4 """
5 Provides a utility function for https connections with certificate verification.
6
7 The verification is based on http://tools.ietf.org/html/rfc6125#section-6.4.3
8 and the code is from Lib/ssl.py in python3:
9   http://hg.python.org/cpython/file/4dac45f88d45/Lib/ssl.py
10
11 One use case is to download Chromium DEPS file in a secure way:
12   https://src.chromium.org/chrome/trunk/src/DEPS
13
14 Notice: python 2.7 or newer is required.
15 """
16
17 import httplib
18 import os
19 import re
20 import socket
21 import ssl
22 import urllib2
23
24
25 _SCRIPT_DIR = os.path.dirname(__file__)
26 _TRUSTED_ROOT_CERTS = os.path.join(_SCRIPT_DIR, 'cacert.pem')
27
28
29 class CertificateError(ValueError):
30   pass
31
32
33 def _DNSNameMatch(dn, hostname, max_wildcards=1):
34   """Matching according to RFC 6125, section 6.4.3
35
36   http://tools.ietf.org/html/rfc6125#section-6.4.3
37   """
38   pats = []
39   if not dn:
40     return False
41
42   parts = dn.split(r'.')
43   leftmost = parts[0]
44   remainder = parts[1:]
45
46   wildcards = leftmost.count('*')
47   if wildcards > max_wildcards:
48     # Issue #17980: avoid denials of service by refusing more
49     # than one wildcard per fragment.  A survery of established
50     # policy among SSL implementations showed it to be a
51     # reasonable choice.
52     raise CertificateError(
53         'too many wildcards in certificate DNS name: ' + repr(dn))
54
55   # speed up common case w/o wildcards
56   if not wildcards:
57     return dn.lower() == hostname.lower()
58
59   # RFC 6125, section 6.4.3, subitem 1.
60   # The client SHOULD NOT attempt to match a presented identifier in which
61   # the wildcard character comprises a label other than the left-most label.
62   if leftmost == '*':
63     # When '*' is a fragment by itself, it matches a non-empty dotless
64     # fragment.
65     pats.append('[^.]+')
66   elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
67     # RFC 6125, section 6.4.3, subitem 3.
68     # The client SHOULD NOT attempt to match a presented identifier
69     # where the wildcard character is embedded within an A-label or
70     # U-label of an internationalized domain name.
71     pats.append(re.escape(leftmost))
72   else:
73     # Otherwise, '*' matches any dotless string, e.g. www*
74     pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
75
76   # add the remaining fragments, ignore any wildcards
77   for frag in remainder:
78     pats.append(re.escape(frag))
79
80   pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
81   return pat.match(hostname)
82
83
84 def _MatchHostname(cert, hostname):
85   """Verify that *cert* (in decoded format as returned by
86   SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125
87   rules are followed, but IP addresses are not accepted for *hostname*.
88
89   CertificateError is raised on failure. On success, the function
90   returns nothing.
91   """
92   if not cert:
93     raise ValueError('empty or no certificate, match_hostname needs a '
94                      'SSL socket or SSL context with either '
95                      'CERT_OPTIONAL or CERT_REQUIRED')
96   dnsnames = []
97   san = cert.get('subjectAltName', ())
98   for key, value in san:
99     if key == 'DNS':
100       if _DNSNameMatch(value, hostname):
101         return
102       dnsnames.append(value)
103   if not dnsnames:
104     # The subject is only checked when there is no dNSName entry
105     # in subjectAltName
106     for sub in cert.get('subject', ()):
107       for key, value in sub:
108         # XXX according to RFC 2818, the most specific Common Name
109         # must be used.
110         if key == 'commonName':
111           if _DNSNameMatch(value, hostname):
112             return
113           dnsnames.append(value)
114   if len(dnsnames) > 1:
115     raise CertificateError('hostname %r doesn\'t match either of %s'
116                            % (hostname, ', '.join(map(repr, dnsnames))))
117   elif len(dnsnames) == 1:
118     raise CertificateError('hostname %r doesn\'t match %r'
119                            % (hostname, dnsnames[0]))
120   else:
121     raise CertificateError('no appropriate commonName or '
122                            'subjectAltName fields were found')
123
124
125 class HTTPSConnection(httplib.HTTPSConnection):
126
127   def __init__(self, host, root_certs=_TRUSTED_ROOT_CERTS, **kwargs):
128     self.root_certs = root_certs
129     httplib.HTTPSConnection.__init__(self, host, **kwargs)
130
131   def connect(self):
132     # Overrides for certificate verification.
133     args = [(self.host, self.port), self.timeout,]
134     if self.source_address:
135       args.append(self.source_address)
136     sock = socket.create_connection(*args)
137
138     if self._tunnel_host:
139       self.sock = sock
140       self._tunnel()
141
142     # Wrap the socket for verification with the root certs.
143     kwargs = {}
144     if self.root_certs is not None:
145       kwargs.update(cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.root_certs)
146     self.sock = ssl.wrap_socket(sock, **kwargs)
147
148     # Check hostname.
149     try:
150       _MatchHostname(self.sock.getpeercert(), self.host)
151     except CertificateError:
152       self.sock.shutdown(socket.SHUT_RDWR)
153       self.sock.close()
154       raise
155
156
157 class HTTPSHandler(urllib2.HTTPSHandler):
158
159   def __init__(self, root_certs=_TRUSTED_ROOT_CERTS):
160     urllib2.HTTPSHandler.__init__(self)
161     self.root_certs = root_certs
162
163   def https_open(self, req):
164     # Pass a reference to the function below so that verification against
165     # trusted root certs could be injected.
166     return self.do_open(self.GetConnection, req)
167
168   def GetConnection(self, host, **kwargs):
169     params = dict(root_certs=self.root_certs)
170     params.update(kwargs)
171     return HTTPSConnection(host, **params)
172
173
174 def SendRequest(https_url):
175   """Send request to the given https url, and return the server response.
176
177   Args:
178     https_url: The https url to send request to.
179
180   Returns:
181     A string that is the response from the server.
182
183   Raises:
184     ValueError: Unexpected value is received during certificate verification.
185     CertificateError: Certificate verification fails.
186   """
187   if not https_url or not https_url.startswith('https://'):
188     raise ValueError('Not a https request for url %s.' % str(https_url))
189
190   url_opener = urllib2.build_opener(HTTPSHandler)
191   return url_opener.open(https_url).read()
192
193
194 if __name__ == '__main__':
195   print SendRequest('https://src.chromium.org/chrome/trunk/src/DEPS')