1 | """Extend M2Crypto SSL functionality for cert verification and custom |
---|
2 | timeout settings. |
---|
3 | |
---|
4 | NERC Data Grid Project""" |
---|
5 | __author__ = "P J Kershaw" |
---|
6 | __date__ = "02/07/07" |
---|
7 | __copyright__ = "(C) 2007 STFC & NERC" |
---|
8 | __license__ = \ |
---|
9 | """This software may be distributed under the terms of the Q Public |
---|
10 | License, version 1.0 or later.""" |
---|
11 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
12 | __revision__ = '$Id$' |
---|
13 | |
---|
14 | import httplib |
---|
15 | import socket |
---|
16 | |
---|
17 | from M2Crypto import SSL, X509 |
---|
18 | from M2Crypto.httpslib import HTTPSConnection as _HTTPSConnection |
---|
19 | |
---|
20 | from ndg.security.common.X509 import X509Cert, X509Stack, X500DN |
---|
21 | |
---|
22 | class InvalidCertSignature(SSL.Checker.SSLVerificationError): |
---|
23 | """Raise if verification against CA cert public key fails""" |
---|
24 | |
---|
25 | class InvalidCertDN(SSL.Checker.SSLVerificationError): |
---|
26 | """Raise if verification against a list acceptable DNs fails""" |
---|
27 | |
---|
28 | |
---|
29 | class HostCheck(SSL.Checker.Checker, object): |
---|
30 | """Override SSL.Checker.Checker to enable alternate Common Name |
---|
31 | setting match for peer cert""" |
---|
32 | |
---|
33 | def __init__(self, |
---|
34 | peerCertDN=None, |
---|
35 | peerCertCN=None, |
---|
36 | acceptedDNs=[], |
---|
37 | caCertList=[], |
---|
38 | caCertFilePathList=[], |
---|
39 | **kw): |
---|
40 | """Override parent class __init__ to enable setting of myProxyServerDN |
---|
41 | setting |
---|
42 | |
---|
43 | @type peerCertDN: string/list |
---|
44 | @param peerCertDN: Set the expected Distinguished Name of the |
---|
45 | server to avoid errors matching hostnames. This is useful |
---|
46 | where the hostname is not fully qualified. |
---|
47 | |
---|
48 | *param acceptedDNs: a list of acceptable DNs. This enables validation |
---|
49 | where the expected DN is where against a limited list of certs. |
---|
50 | |
---|
51 | @type peerCertCN: string |
---|
52 | @param peerCertCN: enable alternate Common Name to peer |
---|
53 | hostname |
---|
54 | |
---|
55 | @type caCertList: list type of M2Crypto.X509.X509 types |
---|
56 | @param caCertList: CA X.509 certificates - if set the peer cert's |
---|
57 | CA signature is verified against one of these. At least one must |
---|
58 | verify |
---|
59 | |
---|
60 | @type caCertFilePathList: list string types |
---|
61 | @param caCertFilePathList: same as caCertList except input as list |
---|
62 | of CA cert file paths""" |
---|
63 | |
---|
64 | SSL.Checker.Checker.__init__(self, **kw) |
---|
65 | |
---|
66 | self.peerCertDN = peerCertDN |
---|
67 | self.peerCertCN = peerCertCN |
---|
68 | self.acceptedDNs = acceptedDNs |
---|
69 | |
---|
70 | if caCertList: |
---|
71 | self.caCertList = caCertList |
---|
72 | elif caCertFilePathList: |
---|
73 | self.caCertFilePathList = caCertFilePathList |
---|
74 | |
---|
75 | def __call__(self, peerCert, host=None): |
---|
76 | """Carry out checks on server ID |
---|
77 | @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509 |
---|
78 | instance |
---|
79 | @param host: name of host to check |
---|
80 | """ |
---|
81 | if peerCert is None: |
---|
82 | raise SSL.Checker.NoCertificate('SSL Peer did not return ' |
---|
83 | 'certificate') |
---|
84 | |
---|
85 | peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ', '/') |
---|
86 | try: |
---|
87 | SSL.Checker.Checker.__call__(self, peerCert, host=self.peerCertCN) |
---|
88 | |
---|
89 | except SSL.Checker.WrongHost, e: |
---|
90 | # Try match against peerCertDN set |
---|
91 | if peerCertDN != self.peerCertDN: |
---|
92 | raise e |
---|
93 | |
---|
94 | # At least one match should be found in the list - first convert to |
---|
95 | # NDG X500DN type to allow per field matching for DN comparison |
---|
96 | peerCertX500DN = X500DN(dn=peerCertDN) |
---|
97 | |
---|
98 | if self.acceptedDNs: |
---|
99 | matchFound = False |
---|
100 | for dn in self.acceptedDNs: |
---|
101 | x500dn = X500DN(dn=dn) |
---|
102 | if x500dn == peerCertX500DN: |
---|
103 | matchFound = True |
---|
104 | break |
---|
105 | |
---|
106 | if not matchFound: |
---|
107 | raise InvalidCertDN('Peer cert DN "%s" doesn\'t match ' |
---|
108 | 'verification list' % peerCertDN) |
---|
109 | |
---|
110 | if len(self.__caCertStack) > 0: |
---|
111 | try: |
---|
112 | self.__caCertStack.verifyCertChain(\ |
---|
113 | x509Cert2Verify=X509Cert(m2CryptoX509=peerCert)) |
---|
114 | except Exception, e: |
---|
115 | raise InvalidCertSignature("Peer certificate verification " |
---|
116 | "against CA cert failed: %s" % e) |
---|
117 | |
---|
118 | # They match - drop the exception and return all OK instead |
---|
119 | return True |
---|
120 | |
---|
121 | def __setCACertList(self, caCertList): |
---|
122 | """Set list of CA certs - peer cert must validate against at least one |
---|
123 | of these""" |
---|
124 | self.__caCertStack = X509Stack() |
---|
125 | for caCert in caCertList: |
---|
126 | self.__caCertStack.push(caCert) |
---|
127 | |
---|
128 | caCertList = property(fset=__setCACertList, |
---|
129 | doc="list of CA certs - peer cert must validate against one") |
---|
130 | |
---|
131 | def __setCACertsFromFileList(self, caCertFilePathList): |
---|
132 | '''Read CA certificates from file and add them to the X.509 |
---|
133 | stack |
---|
134 | |
---|
135 | @type caCertFilePathList: list or tuple |
---|
136 | @param caCertFilePathList: list of file paths for CA certificates to |
---|
137 | be used to verify certificate used to sign message''' |
---|
138 | |
---|
139 | if not isinstance(caCertFilePathList, list) and \ |
---|
140 | not isinstance(caCertFilePathList, tuple): |
---|
141 | raise AttributeError( |
---|
142 | 'Expecting a list or tuple for "caCertFilePathList"') |
---|
143 | |
---|
144 | self.__caCertStack = X509Stack() |
---|
145 | |
---|
146 | for caCertFilePath in caCertFilePathList: |
---|
147 | self.__caCertStack.push(X509.load_cert(caCertFilePath)) |
---|
148 | |
---|
149 | caCertFilePathList = property(fset=__setCACertsFromFileList, |
---|
150 | doc="list of CA cert file paths - peer cert must validate against one") |
---|
151 | |
---|
152 | |
---|
153 | class HTTPSConnection(_HTTPSConnection): |
---|
154 | """Modified version of M2Crypto equivalent to enable custom checks with |
---|
155 | the peer and timeout settings |
---|
156 | |
---|
157 | @type defReadTimeout: M2Crypto.SSL.timeout |
---|
158 | @cvar defReadTimeout: default timeout for read operations |
---|
159 | @type defWriteTimeout: M2Crypto.SSL.timeout |
---|
160 | @cvar defWriteTimeout: default timeout for write operations""" |
---|
161 | defReadTimeout = SSL.timeout(sec=20.) |
---|
162 | defWriteTimeout = SSL.timeout(sec=20.) |
---|
163 | |
---|
164 | def __init__(self, *args, **kw): |
---|
165 | '''Overload to enable setting of post connection check |
---|
166 | callback to SSL.Connection |
---|
167 | |
---|
168 | type *args: tuple |
---|
169 | param *args: args which apply to M2Crypto.httpslib.HTTPSConnection |
---|
170 | type **kw: dict |
---|
171 | param **kw: additional keywords |
---|
172 | @type postConnectionCheck: SSL.Checker.Checker derivative |
---|
173 | @keyword postConnectionCheck: set class for checking peer |
---|
174 | @type readTimeout: M2Crypto.SSL.timeout |
---|
175 | @keyword readTimeout: readTimeout - set timeout for read |
---|
176 | @type writeTimeout: M2Crypto.SSL.timeout |
---|
177 | @keyword writeTimeout: similar to read timeout''' |
---|
178 | |
---|
179 | self._postConnectionCheck = kw.pop('postConnectionCheck', |
---|
180 | SSL.Checker.Checker) |
---|
181 | |
---|
182 | if 'readTimeout' in kw: |
---|
183 | if not isinstance(kw['readTimeout'], SSL.timeout): |
---|
184 | raise AttributeError("readTimeout must be of type " + \ |
---|
185 | "M2Crypto.SSL.timeout") |
---|
186 | self.readTimeout = kw.pop('readTimeout') |
---|
187 | else: |
---|
188 | self.readTimeout = HTTPSConnection.defReadTimeout |
---|
189 | |
---|
190 | if 'writeTimeout' in kw: |
---|
191 | if not isinstance(kw['writeTimeout'], SSL.timeout): |
---|
192 | raise AttributeError("writeTimeout must be of type " + \ |
---|
193 | "M2Crypto.SSL.timeout") |
---|
194 | self.writeTimeout = kw.pop('writeTimeout') |
---|
195 | else: |
---|
196 | self.writeTimeout = HTTPSConnection.defWriteTimeout |
---|
197 | |
---|
198 | self._clntCertFilePath = kw.pop('clntCertFilePath', None) |
---|
199 | self._clntPriKeyFilePath = kw.pop('clntPriKeyFilePath', None) |
---|
200 | |
---|
201 | _HTTPSConnection.__init__(self, *args, **kw) |
---|
202 | |
---|
203 | # load up certificate stuff |
---|
204 | if self._clntCertFilePath is not None and \ |
---|
205 | self._clntPriKeyFilePath is not None: |
---|
206 | self.ssl_ctx.load_cert(self._clntCertFilePath, |
---|
207 | self._clntPriKeyFilePath) |
---|
208 | |
---|
209 | |
---|
210 | def connect(self): |
---|
211 | '''Overload M2Crypto.httpslib.HTTPSConnection to enable |
---|
212 | custom post connection check of peer certificate and socket timeout''' |
---|
213 | |
---|
214 | self.sock = SSL.Connection(self.ssl_ctx) |
---|
215 | self.sock.set_post_connection_check_callback(self._postConnectionCheck) |
---|
216 | |
---|
217 | self.sock.set_socket_read_timeout(self.readTimeout) |
---|
218 | self.sock.set_socket_write_timeout(self.writeTimeout) |
---|
219 | |
---|
220 | self.sock.connect((self.host, self.port)) |
---|
221 | |
---|
222 | def putrequest(self, method, url, **kw): |
---|
223 | '''Overload to work around bug with unicode type URL''' |
---|
224 | url = str(url) |
---|
225 | _HTTPSConnection.putrequest(self, method, url, **kw) |
---|