Merge lp:~therve/txaws/ssl-verify into lp:txaws

Proposed by Thomas Herve
Status: Merged
Merged at revision: 103
Proposed branch: lp:~therve/txaws/ssl-verify
Merge into: lp:txaws
Diff against target: 502 lines (+372/-8)
11 files modified
txaws/client/base.py (+13/-6)
txaws/client/ssl.py (+100/-0)
txaws/client/tests/badprivate.ssl (+15/-0)
txaws/client/tests/badpublic.ssl (+23/-0)
txaws/client/tests/private.ssl (+15/-0)
txaws/client/tests/private_san.ssl (+16/-0)
txaws/client/tests/public.ssl (+22/-0)
txaws/client/tests/public_san.ssl (+12/-0)
txaws/client/tests/test_client.py (+151/-0)
txaws/service.py (+4/-1)
txaws/version.py (+1/-1)
To merge this branch: bzr merge lp:~therve/txaws/ssl-verify
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Review via email: mp+83740@code.launchpad.net

Description of the change

The branch implements optional SSL hostname verification, set by changing the endpoint passed to the client (for backward compatibility reasons).

To post a comment you must log in.
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Nice fix, +1!

It would be cool to push VerifyingContextFactory to Twisted, could you please open a ticket for it in the Twisted trac if there's none already?

review: Approve
Revision history for this message
Thomas Herve (therve) wrote :

Free: thanks for the review! I added some more checks, would you mind looking at them?

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Still looks good. +1.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'txaws/client/base.py'
2--- txaws/client/base.py 2011-04-21 21:16:37 +0000
3+++ txaws/client/base.py 2011-11-29 18:51:38 +0000
4@@ -3,7 +3,7 @@
5 except ImportError:
6 from xml.parsers.expat import ExpatError as ParseError
7
8-from twisted.internet import reactor, ssl
9+from twisted.internet.ssl import ClientContextFactory
10 from twisted.web import http
11 from twisted.web.client import HTTPClientFactory
12 from twisted.web.error import Error as TwistedWebError
13@@ -12,6 +12,7 @@
14 from txaws.credentials import AWSCredentials
15 from txaws.exception import AWSResponseParseError
16 from txaws.service import AWSServiceEndpoint
17+from txaws.client.ssl import VerifyingContextFactory
18
19
20 def error_wrapper(error, errorClass):
21@@ -73,13 +74,16 @@
22
23 class BaseQuery(object):
24
25- def __init__(self, action=None, creds=None, endpoint=None):
26+ def __init__(self, action=None, creds=None, endpoint=None, reactor=None):
27 if not action:
28 raise TypeError("The query requires an action parameter.")
29 self.factory = HTTPClientFactory
30 self.action = action
31 self.creds = creds
32 self.endpoint = endpoint
33+ if reactor is None:
34+ from twisted.internet import reactor
35+ self.reactor = reactor
36 self.client = None
37
38 def get_page(self, url, *args, **kwds):
39@@ -92,11 +96,14 @@
40 contextFactory = None
41 scheme, host, port, path = parse(url)
42 self.client = self.factory(url, *args, **kwds)
43- if scheme == 'https':
44- contextFactory = ssl.ClientContextFactory()
45- reactor.connectSSL(host, port, self.client, contextFactory)
46+ if scheme == "https":
47+ if self.endpoint.ssl_hostname_verification:
48+ contextFactory = VerifyingContextFactory(host)
49+ else:
50+ contextFactory = ClientContextFactory()
51+ self.reactor.connectSSL(host, port, self.client, contextFactory)
52 else:
53- reactor.connectTCP(host, port, self.client)
54+ self.reactor.connectTCP(host, port, self.client)
55 return self.client.deferred
56
57 def get_request_headers(self, *args, **kwds):
58
59=== added file 'txaws/client/ssl.py'
60--- txaws/client/ssl.py 1970-01-01 00:00:00 +0000
61+++ txaws/client/ssl.py 2011-11-29 18:51:38 +0000
62@@ -0,0 +1,100 @@
63+from glob import glob
64+import os
65+import re
66+
67+from OpenSSL import SSL
68+from OpenSSL.crypto import load_certificate, FILETYPE_PEM
69+
70+from twisted.internet.ssl import CertificateOptions
71+
72+
73+__all__ = ["VerifyingContextFactory", "get_ca_certs"]
74+
75+
76+class VerifyingContextFactory(CertificateOptions):
77+ """
78+ A SSL context factory to pass to C{connectSSL} to check for hostname
79+ validity.
80+ """
81+
82+ def __init__(self, host, caCerts=None):
83+ if caCerts is None:
84+ caCerts = get_global_ca_certs()
85+ CertificateOptions.__init__(self, verify=True, caCerts=caCerts)
86+ self.host = host
87+
88+ def _dnsname_match(self, dn, host):
89+ pats = []
90+ for frag in dn.split(r"."):
91+ if frag == "*":
92+ pats.append("[^.]+")
93+ else:
94+ frag = re.escape(frag)
95+ pats.append(frag.replace(r"\*", "[^.]*"))
96+
97+ rx = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE)
98+ return bool(rx.match(host))
99+
100+ def verify_callback(self, connection, x509, errno, depth, preverifyOK):
101+ # Only check depth == 0 on chained certificates.
102+ if depth == 0:
103+ dns_found = False
104+ if getattr(x509, "get_extension", None) is not None:
105+ for index in range(x509.get_extension_count()):
106+ extension = x509.get_extension(index)
107+ if extension.get_short_name() != "subjectAltName":
108+ continue
109+ data = str(extension)
110+ for element in data.split(", "):
111+ key, value = element.split(":")
112+ if key != "DNS":
113+ continue
114+ if self._dnsname_match(value, self.host):
115+ return preverifyOK
116+ dns_found = True
117+ break
118+ if not dns_found:
119+ commonName = x509.get_subject().commonName
120+ if commonName is None:
121+ return False
122+ if not self._dnsname_match(commonName, self.host):
123+ return False
124+ else:
125+ return False
126+ return preverifyOK
127+
128+ def _makeContext(self):
129+ context = CertificateOptions._makeContext(self)
130+ context.set_verify(
131+ SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
132+ self.verify_callback)
133+ return context
134+
135+
136+def get_ca_certs(files="/etc/ssl/certs/*.pem"):
137+ """Retrieve a list of CAs pointed by C{files}."""
138+ certificateAuthorityMap = {}
139+ for certFileName in glob(files):
140+ # There might be some dead symlinks in there, so let's make sure it's
141+ # real.
142+ if not os.path.exists(certFileName):
143+ continue
144+ certFile = open(certFileName)
145+ data = certFile.read()
146+ certFile.close()
147+ x509 = load_certificate(FILETYPE_PEM, data)
148+ digest = x509.digest("sha1")
149+ # Now, de-duplicate in case the same cert has multiple names.
150+ certificateAuthorityMap[digest] = x509
151+ return certificateAuthorityMap.values()
152+
153+
154+_ca_certs = None
155+
156+
157+def get_global_ca_certs():
158+ """Retrieve a singleton of CA certificates."""
159+ global _ca_certs
160+ if _ca_certs is None:
161+ _ca_certs = get_ca_certs()
162+ return _ca_certs
163
164=== added file 'txaws/client/tests/badprivate.ssl'
165--- txaws/client/tests/badprivate.ssl 1970-01-01 00:00:00 +0000
166+++ txaws/client/tests/badprivate.ssl 2011-11-29 18:51:38 +0000
167@@ -0,0 +1,15 @@
168+-----BEGIN RSA PRIVATE KEY-----
169+MIICXgIBAAKBgQDGYFWP2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed3
170+0tAkAXH1gOwQZbARFlUn0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1
171+dhK1xpe1h5y09AjCz02xxzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQAB
172+AoGBAKfv+983yJfgcO9QwzLULlrilQNfk36r6y6QAG7y84T7uU10spSs4kno80mL
173+58yF2YTNrC91scdePrMEDikldUVcCqtPYcZKHyw5+4aGaDDO244tznexOQnQcNIe
174+2BbLFuh+jmJpoFIY/H7EsLQQzn6+6dGPnYGBQfiyitWfAXRNAkEA/ShQkYCRAHgq
175+g6WBIYsw/ISQydhiMiKrL2ZUXERT+pWU9MoSdMskgyMi3S7wzwJQXkrHA36q8QkL
176++H8n5K+f5wJBAMiajfEtv0wRW0awX40qJtuqW3cSKeGHBH9mMObcRJd5OcK6giC/
177+Cc5st/ZcuE/8i4r44DfeC+cwY6QdIqI8rdMCQQCKuq78LWJIyZEyt12+ThK4LsVR
178+d1zIcKsyvHb6YQ9MQPBx/NKEYlZN7tFKOFEKgBAevAe3aJCwqe5/bN8luQB9AkEA
179+uQVD8bR+AgzoIPS/zJWaLXSc09/e3PIJBfAdHnD+mq7mxWH8b3OD+e5wZjvyi2Ok
180+2NLfCug0FlGdNVrh/Lz2nQJATdcNvHNzJcWOHe05lo+xAqkjz73FWGpPNrdXRigG
181+YnjIsZVy4k48xIxPhT2rC44yo1iPEP5EnHCE2bLyUlTAYA==
182+-----END RSA PRIVATE KEY-----
183
184=== added file 'txaws/client/tests/badpublic.ssl'
185--- txaws/client/tests/badpublic.ssl 1970-01-01 00:00:00 +0000
186+++ txaws/client/tests/badpublic.ssl 2011-11-29 18:51:38 +0000
187@@ -0,0 +1,23 @@
188+-----BEGIN CERTIFICATE-----
189+MIIDzjCCAzegAwIBAgIJANqT3vXxSVFjMA0GCSqGSIb3DQEBBQUAMIGhMQswCQYD
190+VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8G
191+A1UEChMYRmFrZSBMYW5kc2NhcGUgKFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0
192+eTESMBAGA1UEAxMJbG9jYWxob3N0MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNh
193+bm9uaWNhbC5jb20wHhcNMDkwMTA5MTUyNTAwWhcNMTkwMTA3MTUyNTAwWjCBoTEL
194+MAkGA1UEBhMCQlIxDzANBgNVBAgTBlBhcmFuYTERMA8GA1UEBxMIQ3VyaXRpYmEx
195+ITAfBgNVBAoTGEZha2UgTGFuZHNjYXBlIChUZXN0aW5nKTERMA8GA1UECxMIU2Vj
196+dXJpdHkxEjAQBgNVBAMTCWxvY2FsaG9zdDEkMCIGCSqGSIb3DQEJARYVYW5kcmVh
197+c0BjYW5vbmljYWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGYFWP
198+2Ine2OFIPjX+Tu+S403KW63EWq/I1DYXiezLoUpYPed30tAkAXH1gOwQZbARFlUn
199+0LgvXDSpuQLvgKQZwP/e1D8SvZUZ6nexW+aYlPE9kjd1dhK1xpe1h5y09AjCz02x
200+xzcFzrJrJ47uU7vV+FGArE8FFh3hO+dz0/PmZQIDAQABo4IBCjCCAQYwHQYDVR0O
201+BBYEFF4A8+YHCLAt19OtWTjIjBKzLUokMIHWBgNVHSMEgc4wgcuAFF4A8+YHCLAt
202+19OtWTjIjBKzLUokoYGnpIGkMIGhMQswCQYDVQQGEwJCUjEPMA0GA1UECBMGUGFy
203+YW5hMREwDwYDVQQHEwhDdXJpdGliYTEhMB8GA1UEChMYRmFrZSBMYW5kc2NhcGUg
204+KFRlc3RpbmcpMREwDwYDVQQLEwhTZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0
205+MSQwIgYJKoZIhvcNAQkBFhVhbmRyZWFzQGNhbm9uaWNhbC5jb22CCQDak9718UlR
206+YzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBABszkA3CCzt+nTOX+A7/
207+I98DvI0W1Ss0J+Tq+diLr+kw6Z5ZTj5hrIS/x6XhVHjpim4724UBXA0Sels4JXbw
208+hhJovuncExce316gAol/9eEzTffZ9mt1jZQy9LL7IAENiobnsj2F65zNaJzXp5UC
209+rE/h/xIxz9rAmXtVOWHqZLcw
210+-----END CERTIFICATE-----
211
212=== added file 'txaws/client/tests/private.ssl'
213--- txaws/client/tests/private.ssl 1970-01-01 00:00:00 +0000
214+++ txaws/client/tests/private.ssl 2011-11-29 18:51:38 +0000
215@@ -0,0 +1,15 @@
216+-----BEGIN RSA PRIVATE KEY-----
217+MIICWwIBAAKBgQDX2VNEDZHtl5nimNocshar8pBmjqiGn9olCR2LcKifuJY4bFTg
218+qib+Rr3v2DwDTbOMaquRSxFgwLJLCug3WclsGrYSPIsFCx+k3XhqM61JXEwrKuIp
219+Js893XHkeg3SEFua/oVfDxNfJttoHW3FbsnDx5964kYwGExjJcH73GInUQIDAQAB
220+AoGASiM9NEys6Lx/gJMbp2uL2fdwnak2PTc+iCX/XduOL34JKswawyfuSLwnlO/i
221+fQf9OaeR0k/EYkUNeDUA2bIfOj6wWS8tamnX4fxL7A20y5VyqMMah8mcerZgtPdS
222+7ZtYCbeijWSKpHgjALc2Hym7R68WZI+IHe0DQkcW6WxOMFkCQQD2jqHZn/Qtd62u
223+mWVwIx6G7+Po5vzd86KyWWftdUtVCY9DmiX1rmWXbJhLnmaKCLkmHxyBvw7Biarr
224+ZnCAafebAkEA4B2dSpLi7bAzjCa7JBtzV9kr1FVZOl2vA+9BqTAjCQu0b9VDEm8V
225+x0061Z8rN7Og3ECGtKH/r3/4RnHUPpwJgwJAdyZQkvHYt4xJc8IPolRmcUFGu4u9
226+Eammq1fHgJqZcBvxjvLUe1jvIXFKW+jNltFGYGTSiuUAxYi4/49+uJ/9FwJAGBB1
227+/DTrcvQxhMH/5C+iYfNqtmD3tMGscjK1jTIjAOyl0kBG9GrDHuRXBesSW+fIxP2U
228+uT6P0std4EqGrLZaewJAHT0n/3tXnsPjj+BMlC4ZkRKgPJ4I7zTU1XSlLY5zbMoV
229+NvtHLlq7ttiarsH95xyge69uV1/zJVj/IiS71YY9PQ==
230+-----END RSA PRIVATE KEY-----
231
232=== added file 'txaws/client/tests/private_san.ssl'
233--- txaws/client/tests/private_san.ssl 1970-01-01 00:00:00 +0000
234+++ txaws/client/tests/private_san.ssl 2011-11-29 18:51:38 +0000
235@@ -0,0 +1,16 @@
236+-----BEGIN PRIVATE KEY-----
237+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMzOcwF0HIQcenxS
238+RrKT7fOdJYZVPzvGuDiwUpp6WNuyAJ6JVCP83SzTp3yXmKfDFO/m00WdKxF28W3s
239+Q/NrVvQQCzfh2NhDtxom7p9JOVW9j7NkO4SFOLHh7JxbLT1j0MGTJX0bxJkro78v
240+wB/OAN4/eiQdli2y6D6skqdb9QXHAgMBAAECgYAvSP7+d+tZiSWybGCMPGE03LRc
241+NnRZ/cBsvjDkH5lCZ++Cqtw1Tt1VyywhNPL20LCVzuo6aVYXOyn0ohbyLXcuinpE
242+rVopV9nUPr0EFOo+yccDNNPQJ2tevlYfEsS6afcfLcUinRUSvVojHDrODADduLR8
243+uA3Le95tChcVwe6NuQJBAOvdRTG858BB9zJdjyd4QoqTA1k0rs+VC3svVUT9l16+
244+gZLZ75wTLbtrkGRN/iiAVPemgqQYmNvuXtUByO3QFmUCQQDeSmt+z2dNCx78mUWQ
245+HFcyJP0g2gz/IEnxx/9Rin/9xSo+ycuNvbSwphSHxYl20wVFA72vp/zuOWO3WaXr
246+umK7AkB6pDJfe2dRu7sqcCWIk2qeHXVHRDKFc21l3yXKWsYDmLFNR47kq8BCzNpm
247+nXtDWf9USjtx0exhp1+eCHCO331VAkALAMwJXuLSIXbLMhsLYxu9067j7WcvSb3f
248+RfMRajWjrhrFON/miDlldRMXFWQUiaV9IQ5Gn54ZfKW+8aUQ4gz5AkB+yOVkouwj
249+QVngotLjasbgvE8WugbweLInHN1W2ucsLKSpSADoE/djQ5NnwuolmnrhpQT5BWcQ
250+j3o7Gf/nXS+r
251+-----END PRIVATE KEY-----
252
253=== added file 'txaws/client/tests/public.ssl'
254--- txaws/client/tests/public.ssl 1970-01-01 00:00:00 +0000
255+++ txaws/client/tests/public.ssl 2011-11-29 18:51:38 +0000
256@@ -0,0 +1,22 @@
257+-----BEGIN CERTIFICATE-----
258+MIIDnDCCAwWgAwIBAgIJALPjWsknBC15MA0GCSqGSIb3DQEBBQUAMIGRMQswCQYD
259+VQQGEwJCUjEPMA0GA1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAG
260+A1UEChMJTGFuZHNjYXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2Nh
261+bGhvc3QxJDAiBgkqhkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTAeFw0w
262+OTAxMDgxNjQxMzlaFw0xOTAxMDYxNjQxMzlaMIGRMQswCQYDVQQGEwJCUjEPMA0G
263+A1UECBMGUGFyYW5hMREwDwYDVQQHEwhDdXJpdGliYTESMBAGA1UEChMJTGFuZHNj
264+YXBlMRAwDgYDVQQLEwdUZXN0aW5nMRIwEAYDVQQDEwlsb2NhbGhvc3QxJDAiBgkq
265+hkiG9w0BCQEWFWFuZHJlYXNAY2Fub25pY2FsLmNvbTCBnzANBgkqhkiG9w0BAQEF
266+AAOBjQAwgYkCgYEA19lTRA2R7ZeZ4pjaHLIWq/KQZo6ohp/aJQkdi3Con7iWOGxU
267+4Kom/ka979g8A02zjGqrkUsRYMCySwroN1nJbBq2EjyLBQsfpN14ajOtSVxMKyri
268+KSbPPd1x5HoN0hBbmv6FXw8TXybbaB1txW7Jw8efeuJGMBhMYyXB+9xiJ1ECAwEA
269+AaOB+TCB9jAdBgNVHQ4EFgQU3eUz2XxK1J/oavkn/hAvYfGOZM0wgcYGA1UdIwSB
270+vjCBu4AU3eUz2XxK1J/oavkn/hAvYfGOZM2hgZekgZQwgZExCzAJBgNVBAYTAkJS
271+MQ8wDQYDVQQIEwZQYXJhbmExETAPBgNVBAcTCEN1cml0aWJhMRIwEAYDVQQKEwlM
272+YW5kc2NhcGUxEDAOBgNVBAsTB1Rlc3RpbmcxEjAQBgNVBAMTCWxvY2FsaG9zdDEk
273+MCIGCSqGSIb3DQEJARYVYW5kcmVhc0BjYW5vbmljYWwuY29tggkAs+NayScELXkw
274+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQBZQcqhHAsasX3WtCXlIKqH
275+hE4ZdsvtPHOnoWPxxN4CZEyu2YJ2PMXCkA7yISNokAZgkOpkYGPWwV3CwNCw032u
276++ngwIo2sxx7ag8tVrYkIda717oBw7opDMVrjTNhZdak7s+hg+s9ZDPUMMcbJFtlN
277+lmayn/uZSyog4Y+yriB1tQ==
278+-----END CERTIFICATE-----
279
280=== added file 'txaws/client/tests/public_san.ssl'
281--- txaws/client/tests/public_san.ssl 1970-01-01 00:00:00 +0000
282+++ txaws/client/tests/public_san.ssl 2011-11-29 18:51:38 +0000
283@@ -0,0 +1,12 @@
284+-----BEGIN CERTIFICATE-----
285+MIIB1jCCAT+gAwIBAgIJAMG1W/zdYglWMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
286+BAMTCWxvY2FsaG9zdDAeFw0xMTExMjkxODIxNTdaFw0yMTExMjYxODIxNTdaMBQx
287+EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
288+zM5zAXQchBx6fFJGspPt850lhlU/O8a4OLBSmnpY27IAnolUI/zdLNOnfJeYp8MU
289+7+bTRZ0rEXbxbexD82tW9BALN+HY2EO3Gibun0k5Vb2Ps2Q7hIU4seHsnFstPWPQ
290+wZMlfRvEmSujvy/AH84A3j96JB2WLbLoPqySp1v1BccCAwEAAaMwMC4wLAYDVR0R
291+BCUwI4IJMTI3LjAuMC4xgglsb2NhbGhvc3SCCzE5Mi4xNjguMC4xMA0GCSqGSIb3
292+DQEBBQUAA4GBACgPQt3A+kq8Jus+vCIvhbKjU6HaId5gHvRhvM+SnBb/K8llDInh
293+vS2bpVasSprTbPQjnqh6vVEj0jB/52p8pliZ5Q0pnaEZRYJtUnyeQCz8mwS16h5o
294+KiKfQclPKkM0p0wiQPz1sxju7bbYRm2PoCvoNl08c5RhstKSwF9XmuTx
295+-----END CERTIFICATE-----
296
297=== modified file 'txaws/client/tests/test_client.py'
298--- txaws/client/tests/test_client.py 2011-04-21 21:16:37 +0000
299+++ txaws/client/tests/test_client.py 2011-11-29 18:51:38 +0000
300@@ -1,6 +1,11 @@
301 import os
302
303+from OpenSSL.crypto import load_certificate, FILETYPE_PEM
304+from OpenSSL.SSL import Error as SSLError
305+from OpenSSL.version import __version__ as pyopenssl_version
306+
307 from twisted.internet import reactor
308+from twisted.internet.ssl import DefaultOpenSSLContextFactory
309 from twisted.internet.error import ConnectionRefusedError
310 from twisted.protocols.policies import WrappingFactory
311 from twisted.python import log
312@@ -11,9 +16,23 @@
313 from twisted.web.error import Error as TwistedWebError
314
315 from txaws.client.base import BaseClient, BaseQuery, error_wrapper
316+from txaws.client.ssl import VerifyingContextFactory
317+from txaws.service import AWSServiceEndpoint
318 from txaws.testing.base import TXAWSTestCase
319
320
321+def sibpath(path):
322+ return os.path.join(os.path.dirname(__file__), path)
323+
324+
325+PRIVKEY = sibpath("private.ssl")
326+PUBKEY = sibpath("public.ssl")
327+BADPRIVKEY = sibpath("badprivate.ssl")
328+BADPUBKEY = sibpath("badpublic.ssl")
329+PRIVSANKEY = sibpath("private_san.ssl")
330+PUBSANKEY = sibpath("public_san.ssl")
331+
332+
333 class ErrorWrapperTestCase(TXAWSTestCase):
334
335 def test_204_no_content(self):
336@@ -148,3 +167,135 @@
337 d = query.get_page(self._get_url("file"))
338 d.addCallback(query.get_response_headers)
339 return d.addCallback(check_results)
340+
341+ def test_ssl_hostname_verification(self):
342+ """
343+ If the endpoint passed to L{BaseQuery} has C{ssl_hostname_verification}
344+ sets to C{True}, a L{VerifyingContextFactory} is passed to
345+ C{connectSSL}.
346+ """
347+
348+ class FakeReactor(object):
349+
350+ def __init__(self):
351+ self.connects = []
352+
353+ def connectSSL(self, host, port, client, factory):
354+ self.connects.append((host, port, client, factory))
355+
356+ fake_reactor = FakeReactor()
357+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
358+ query = BaseQuery("an action", "creds", endpoint, fake_reactor)
359+ query.get_page("https://example.com/file")
360+ [(host, port, client, factory)] = fake_reactor.connects
361+ self.assertEqual("example.com", host)
362+ self.assertEqual(443, port)
363+ self.assertTrue(isinstance(factory, VerifyingContextFactory))
364+ self.assertEqual("example.com", factory.host)
365+ self.assertNotEqual([], factory.caCerts)
366+
367+
368+class BaseQuerySSLTestCase(TXAWSTestCase):
369+
370+ def setUp(self):
371+ self.cleanupServerConnections = 0
372+ name = self.mktemp()
373+ os.mkdir(name)
374+ FilePath(name).child("file").setContent("0123456789")
375+ r = static.File(name)
376+ self.site = server.Site(r, timeout=None)
377+ self.wrapper = WrappingFactory(self.site)
378+ from txaws.client import ssl
379+ pub_key = file(PUBKEY)
380+ pub_key_data = pub_key.read()
381+ pub_key.close()
382+ pub_key_san = file(PUBSANKEY)
383+ pub_key_san_data = pub_key_san.read()
384+ pub_key_san.close()
385+ ssl._ca_certs = [load_certificate(FILETYPE_PEM, pub_key_data),
386+ load_certificate(FILETYPE_PEM, pub_key_san_data)]
387+
388+ def tearDown(self):
389+ from txaws.client import ssl
390+ ssl._ca_certs = None
391+ # If the test indicated it might leave some server-side connections
392+ # around, clean them up.
393+ connections = self.wrapper.protocols.keys()
394+ # If there are fewer server-side connections than requested,
395+ # that's okay. Some might have noticed that the client closed
396+ # the connection and cleaned up after themselves.
397+ for n in range(min(len(connections), self.cleanupServerConnections)):
398+ proto = connections.pop()
399+ log.msg("Closing %r" % (proto,))
400+ proto.transport.loseConnection()
401+ if connections:
402+ log.msg("Some left-over connections; this test is probably buggy.")
403+ return self.port.stopListening()
404+
405+ def _get_url(self, path):
406+ return "https://localhost:%d/%s" % (self.portno, path)
407+
408+ def test_ssl_verification_positive(self):
409+ """
410+ The L{VerifyingContextFactory} properly allows to connect to the
411+ endpoint if the certificates match.
412+ """
413+ context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY)
414+ self.port = reactor.listenSSL(
415+ 0, self.site, context_factory, interface="127.0.0.1")
416+ self.portno = self.port.getHost().port
417+
418+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
419+ query = BaseQuery("an action", "creds", endpoint)
420+ d = query.get_page(self._get_url("file"))
421+ return d.addCallback(self.assertEquals, "0123456789")
422+
423+ def test_ssl_verification_negative(self):
424+ """
425+ The L{VerifyingContextFactory} fails with a SSL error the certificates
426+ can't be checked.
427+ """
428+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
429+ self.port = reactor.listenSSL(
430+ 0, self.site, context_factory, interface="127.0.0.1")
431+ self.portno = self.port.getHost().port
432+
433+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
434+ query = BaseQuery("an action", "creds", endpoint)
435+ d = query.get_page(self._get_url("file"))
436+ return self.assertFailure(d, SSLError)
437+
438+ def test_ssl_verification_bypassed(self):
439+ """
440+ L{BaseQuery} doesn't use L{VerifyingContextFactory}
441+ if C{ssl_hostname_verification} is C{False}, thus allowing to connect
442+ to non-secure endpoints.
443+ """
444+ context_factory = DefaultOpenSSLContextFactory(BADPRIVKEY, BADPUBKEY)
445+ self.port = reactor.listenSSL(
446+ 0, self.site, context_factory, interface="127.0.0.1")
447+ self.portno = self.port.getHost().port
448+
449+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=False)
450+ query = BaseQuery("an action", "creds", endpoint)
451+ d = query.get_page(self._get_url("file"))
452+ return d.addCallback(self.assertEquals, "0123456789")
453+
454+ def test_ssl_subject_alt_name(self):
455+ """
456+ L{VerifyingContextFactory} supports checking C{subjectAltName} in the
457+ certificate if it's available.
458+ """
459+ context_factory = DefaultOpenSSLContextFactory(PRIVSANKEY, PUBSANKEY)
460+ self.port = reactor.listenSSL(
461+ 0, self.site, context_factory, interface="127.0.0.1")
462+ self.portno = self.port.getHost().port
463+
464+ endpoint = AWSServiceEndpoint(ssl_hostname_verification=True)
465+ query = BaseQuery("an action", "creds", endpoint)
466+ d = query.get_page("https://127.0.0.1:%d/file" % (self.portno,))
467+ return d.addCallback(self.assertEquals, "0123456789")
468+
469+ if pyopenssl_version < "0.12":
470+ test_ssl_subject_alt_name.skip = (
471+ "subjectAltName not supported by older PyOpenSSL")
472
473=== modified file 'txaws/service.py'
474--- txaws/service.py 2011-08-11 23:10:54 +0000
475+++ txaws/service.py 2011-11-29 18:51:38 +0000
476@@ -19,13 +19,16 @@
477 """
478 @param uri: The URL for the service.
479 @param method: The HTTP method used when accessing a service.
480+ @param ssl_hostname_verification: Whether or not SSL hotname verification
481+ will be done when connecting to the endpoint.
482 """
483
484- def __init__(self, uri="", method="GET"):
485+ def __init__(self, uri="", method="GET", ssl_hostname_verification=False):
486 self.host = ""
487 self.port = None
488 self.path = "/"
489 self.method = method
490+ self.ssl_hostname_verification = ssl_hostname_verification
491 self._parse_uri(uri)
492 if not self.scheme:
493 self.scheme = "http"
494
495=== modified file 'txaws/version.py'
496--- txaws/version.py 2009-11-19 18:46:16 +0000
497+++ txaws/version.py 2011-11-29 18:51:38 +0000
498@@ -1,3 +1,3 @@
499-txaws = "0.0.1"
500+txaws = "0.2.2"
501 ec2_api = "2008-12-01"
502 s3_api = "2006-03-01"

Subscribers

People subscribed via source and target branches

to all changes: