1 | """MyProxy Client interface |
---|
2 | |
---|
3 | Developed for the NERC DataGrid Project: http://ndg.nerc.ac.uk/ |
---|
4 | |
---|
5 | Major re-write of an original class. This updated version implements methods |
---|
6 | with SSL calls with PyOpenSSL rather use calls to myproxy client executables as |
---|
7 | in the original. This version is adapted and extended from an original |
---|
8 | program myproxy_logon by Tom Uram <turam@mcs.anl.gov> |
---|
9 | """ |
---|
10 | __author__ = "P J Kershaw" |
---|
11 | __date__ = "02/06/05" |
---|
12 | __copyright__ = "(C) 2010 Science and Technology Facilities Council" |
---|
13 | __license__ = """BSD - See LICENSE file in top-level directory |
---|
14 | |
---|
15 | For myproxy_logon see Access Grid Toolkit Public License (AGTPL) |
---|
16 | |
---|
17 | This product includes software developed by and/or derived from the Access |
---|
18 | Grid Project (http://www.accessgrid.org) to which the U.S. Government retains |
---|
19 | certain rights.""" |
---|
20 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |
---|
21 | __revision__ = '$Id: $' |
---|
22 | import logging |
---|
23 | log = logging.getLogger(__name__) |
---|
24 | |
---|
25 | import sys |
---|
26 | import os |
---|
27 | import socket |
---|
28 | import base64 |
---|
29 | import re |
---|
30 | import traceback |
---|
31 | |
---|
32 | from OpenSSL import crypto, SSL |
---|
33 | |
---|
34 | from myproxy.utils.openssl import OpenSSLConfig |
---|
35 | from myproxy.utils import CaseSensitiveConfigParser |
---|
36 | |
---|
37 | |
---|
38 | class MyProxyServerSSLCertVerification(object): |
---|
39 | """Check MyProxy server identity. If hostname doesn't match, allow match of |
---|
40 | host's Distinguished Name against MYPROXY_SERVER_DN setting""" |
---|
41 | DN_LUT = { |
---|
42 | 'commonName': 'CN', |
---|
43 | 'organisationalUnitName': 'OU', |
---|
44 | 'organisation': 'O', |
---|
45 | 'countryName': 'C', |
---|
46 | 'emailAddress': 'EMAILADDRESS', |
---|
47 | 'localityName': 'L', |
---|
48 | 'stateOrProvinceName': 'ST', |
---|
49 | 'streetAddress': 'STREET', |
---|
50 | 'domainComponent': 'DC', |
---|
51 | 'userid': 'UID' |
---|
52 | } |
---|
53 | PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values()) |
---|
54 | PARSER_RE = re.compile(PARSER_RE_STR) |
---|
55 | |
---|
56 | SERVER_CN_PREFIX = 'host/' |
---|
57 | |
---|
58 | __slots__ = ('__hostname', '__cnPrefix', '__certDN') |
---|
59 | |
---|
60 | def __init__(self, |
---|
61 | certDN=None, |
---|
62 | hostname=None, |
---|
63 | cnPrefix=SERVER_CN_PREFIX): |
---|
64 | """Override parent class __init__ to enable setting of certDN |
---|
65 | setting |
---|
66 | |
---|
67 | @type certDN: string |
---|
68 | @param certDN: Set the expected Distinguished Name of the |
---|
69 | MyProxy server to avoid errors matching hostnames. This is useful |
---|
70 | where the hostname is not fully qualified |
---|
71 | """ |
---|
72 | self.__cnPrefix = None |
---|
73 | self.__certDN = None |
---|
74 | self.__hostname = None |
---|
75 | |
---|
76 | if certDN is not None: |
---|
77 | self.certDN = certDN |
---|
78 | |
---|
79 | if hostname is not None: |
---|
80 | self.hostname = hostname |
---|
81 | |
---|
82 | self.cnPrefix = cnPrefix |
---|
83 | |
---|
84 | def __call__(self, connection, peerCert, errorStatus, errorDepth, |
---|
85 | successStatus): |
---|
86 | """Verify MyProxy server certificate |
---|
87 | |
---|
88 | @type connection: OpenSSL.SSL.Connection |
---|
89 | @param connection: SSL connection object |
---|
90 | @type peerCert: basestring |
---|
91 | @param peerCert: MyProxy server host certificate as OpenSSL.crypto.X509 |
---|
92 | instance |
---|
93 | @type errorStatus: int |
---|
94 | @param errorStatus: error code to return if verification fails |
---|
95 | @type errorDepth: int |
---|
96 | @param errorDepth: |
---|
97 | @type successStatus: int |
---|
98 | @param successStatus: |
---|
99 | @rtype: int |
---|
100 | @return: status code |
---|
101 | """ |
---|
102 | if peerCert.has_expired(): |
---|
103 | # Any expired certificate in the chain should result in an error |
---|
104 | log.error('Certificate %r in peer certificate chain has expired', |
---|
105 | peerCert.get_subject()) |
---|
106 | |
---|
107 | return errorStatus |
---|
108 | |
---|
109 | elif errorDepth == 0: |
---|
110 | # Only interested in DN of last certificate in the chain - this must |
---|
111 | # match the expected MyProxy Server DN setting |
---|
112 | peerCertSubj = peerCert.get_subject() |
---|
113 | peerCertDN = peerCertSubj.get_components() |
---|
114 | peerCertDN.sort() |
---|
115 | |
---|
116 | if self.certDN is None: |
---|
117 | # Check hostname against peer certificate CN field instead: |
---|
118 | if self.hostname is None: |
---|
119 | log.error('No "hostname" or "certDN" set to check peer ' |
---|
120 | 'certificate against') |
---|
121 | return errorStatus |
---|
122 | |
---|
123 | cn = self.cnPrefix + self.hostname |
---|
124 | if peerCertSubj.commonName == cn: |
---|
125 | return successStatus |
---|
126 | else: |
---|
127 | log.error('Peer certificate CN %r doesn\'t match the ' |
---|
128 | 'expected CN %r', peerCertSubj.commonName, cn) |
---|
129 | return errorStatus |
---|
130 | else: |
---|
131 | if peerCertDN == self.certDN: |
---|
132 | return successStatus |
---|
133 | else: |
---|
134 | log.error('Peer certificate DN %r doesn\'t match the ' |
---|
135 | 'expected DN %r', peerCertDN, self.certDN) |
---|
136 | return errorStatus |
---|
137 | else: |
---|
138 | return successStatus |
---|
139 | |
---|
140 | def _getCertDN(self): |
---|
141 | return self.__certDN |
---|
142 | |
---|
143 | def _setCertDN(self, val): |
---|
144 | if isinstance(val, basestring): |
---|
145 | # Allow for quoted DN |
---|
146 | certDN = val.strip('"') |
---|
147 | |
---|
148 | dnFields = self.__class__.PARSER_RE.split(certDN) |
---|
149 | if len(dnFields) < 2: |
---|
150 | raise TypeError('Error parsing DN string: "%s"' % certDN) |
---|
151 | |
---|
152 | self.__certDN = zip(dnFields[1::2], dnFields[2::2]) |
---|
153 | self.__certDN.sort() |
---|
154 | |
---|
155 | elif not isinstance(val, list): |
---|
156 | for i in val: |
---|
157 | if not len(i) == 2: |
---|
158 | raise TypeError('Expecting list of two element DN field, ' |
---|
159 | 'DN field value pairs for "certDN" ' |
---|
160 | 'attribute') |
---|
161 | self.__certDN = val |
---|
162 | else: |
---|
163 | raise TypeError('Expecting list or string type for "certDN" ' |
---|
164 | 'attribute') |
---|
165 | |
---|
166 | certDN = property(fget=_getCertDN, |
---|
167 | fset=_setCertDN, |
---|
168 | doc="Distinguished Name for MyProxy Server Certificate") |
---|
169 | |
---|
170 | # Get/Set Property methods |
---|
171 | def _getHostname(self): |
---|
172 | return self.__hostname |
---|
173 | |
---|
174 | def _setHostname(self, val): |
---|
175 | if not isinstance(val, basestring): |
---|
176 | raise TypeError("Expecting string type for hostname " |
---|
177 | "attribute") |
---|
178 | self.__hostname = val |
---|
179 | |
---|
180 | hostname = property(fget=_getHostname, |
---|
181 | fset=_setHostname, |
---|
182 | doc="hostname of MyProxy server") |
---|
183 | |
---|
184 | def _getCNPrefix(self): |
---|
185 | """References SSL Certificate verification object property!""" |
---|
186 | return self.__cnPrefix |
---|
187 | |
---|
188 | def _setCNPrefix(self, val): |
---|
189 | """Sets SSL Certificate verification object property!""" |
---|
190 | if not isinstance(val, basestring): |
---|
191 | raise TypeError("Expecting string type for hostname " |
---|
192 | "attribute") |
---|
193 | self.__cnPrefix = val |
---|
194 | |
---|
195 | cnPrefix = property(fget=_getCNPrefix, |
---|
196 | fset=_setCNPrefix, |
---|
197 | doc="Prefix for MyProxy Server Certificate " |
---|
198 | "Distinguished Name CommonName field; usually set " |
---|
199 | "to 'host/' for Globus host certificates") |
---|
200 | |
---|
201 | |
---|
202 | class MyProxyClientError(Exception): |
---|
203 | """Base exception class for MyProxyClient exceptions""" |
---|
204 | |
---|
205 | |
---|
206 | class MyProxyClientConfigError(MyProxyClientError): |
---|
207 | """Error with configuration""" |
---|
208 | |
---|
209 | |
---|
210 | class MyProxyClientGetError(MyProxyClientError): |
---|
211 | """Exceptions arising from get request to server""" |
---|
212 | |
---|
213 | |
---|
214 | class MyProxyClientRetrieveError(MyProxyClientError): |
---|
215 | """Error recovering a response from MyProxy""" |
---|
216 | |
---|
217 | |
---|
218 | class MyProxyCredentialsAlreadyExist(MyProxyClientError): |
---|
219 | """Attempting to upload credentials to the server which already exist. - |
---|
220 | See MyProxyClient.store |
---|
221 | """ |
---|
222 | |
---|
223 | |
---|
224 | class MyProxyClientGetTrustRootsError(MyProxyClientError): |
---|
225 | """Error retrieving trust roots""" |
---|
226 | |
---|
227 | |
---|
228 | class MyProxyClient(object): |
---|
229 | """MyProxy client interface |
---|
230 | |
---|
231 | Based on protocol definitions in: |
---|
232 | |
---|
233 | http://grid.ncsa.uiuc.edu/myproxy/protocol/ |
---|
234 | |
---|
235 | @type MYPROXY_SERVER_ENVVARNAME: string |
---|
236 | @cvar MYPROXY_SERVER_ENVVARNAME: server environment variable name |
---|
237 | |
---|
238 | @type MYPROXY_SERVER_PORT_ENVVARNAME: string |
---|
239 | @cvar MYPROXY_SERVER_PORT_ENVVARNAME: port environment variable name |
---|
240 | |
---|
241 | @type MYPROXY_SERVER_DN_ENVVARNAME: string |
---|
242 | @cvar MYPROXY_SERVER_DN_ENVVARNAME: server certificate Distinguished Name |
---|
243 | environment variable name |
---|
244 | |
---|
245 | @type GLOBUS_LOCATION_ENVVARNAME: string |
---|
246 | @param GLOBUS_LOCATION_ENVVARNAME: 'GLOBUS_LOCATION' environment variable |
---|
247 | name |
---|
248 | |
---|
249 | @type GET_CMD: string |
---|
250 | @cvar GET_CMD: get command string |
---|
251 | |
---|
252 | @type INFO_CMD: string |
---|
253 | @cvar INFO_CMD: info command string |
---|
254 | |
---|
255 | @type DESTROY_CMD: string |
---|
256 | @cvar DESTROY_CMD: destroy command string |
---|
257 | |
---|
258 | @type CHANGE_PASSPHRASE_CMD: string |
---|
259 | @cvar CHANGE_PASSPHRASE_CMD: command string to change cred pass-phrase |
---|
260 | |
---|
261 | @type STORE_CMD: string |
---|
262 | @cvar STORE_CMD: store command string |
---|
263 | |
---|
264 | @type GET_TRUST_ROOTS_CMD: string |
---|
265 | @cvar GET_TRUST_ROOTS_CMD: get trust roots command string |
---|
266 | |
---|
267 | @type TRUSTED_CERTS_FIELDNAME: string |
---|
268 | @param TRUSTED_CERTS_FIELDNAME: field name in get trust roots response for |
---|
269 | trusted certificate file names |
---|
270 | |
---|
271 | @type TRUSTED_CERTS_FILEDATA_FIELDNAME_PREFIX: string |
---|
272 | @param TRUSTED_CERTS_FILEDATA_FIELDNAME_PREFIX: field name prefix in get |
---|
273 | trust roots response for trusted certificate file contents |
---|
274 | |
---|
275 | @type HOSTCERT_SUBDIRPATH: string |
---|
276 | @cvar HOSTCERT_SUBDIRPATH: sub-directory path host certificate (as tuple) |
---|
277 | |
---|
278 | @type HOSTKEY_SUBDIRPATH: string |
---|
279 | @cvar HOSTKEY_SUBDIRPATH: sub-directory path to host key (as tuple) |
---|
280 | |
---|
281 | @type PRIKEY_NBITS: int |
---|
282 | @cvar PRIKEY_NBITS: default number of bits for private key generated |
---|
283 | |
---|
284 | @type MESSAGE_DIGEST_TYPE: string |
---|
285 | @cvar MESSAGE_DIGEST_TYPE: message digest type is MD5 |
---|
286 | |
---|
287 | @type SERVER_RESP_BLK_SIZE: int |
---|
288 | @cvar SERVER_RESP_BLK_SIZE: block size for retrievals from server |
---|
289 | |
---|
290 | @type MAX_RECV_TRIES: int |
---|
291 | @cvar MAX_RECV_TRIES: maximum number of retrievals of size |
---|
292 | SERVER_RESP_BLK_SIZE before this client gives up |
---|
293 | |
---|
294 | @type DEF_PROXY_FILEPATH: string |
---|
295 | @cvar DEF_PROXY_FILEPATH: default location for proxy file to be written to |
---|
296 | |
---|
297 | @type PROXY_FILE_PERMISSIONS: int |
---|
298 | @cvar PROXY_FILE_PERMISSIONS: file permissions returned proxy file is |
---|
299 | created with |
---|
300 | |
---|
301 | @type PROPERTY_DEFAULTS: tuple |
---|
302 | @cvar PROPERTY_DEFAULTS: sets permissable element names for MyProxy config |
---|
303 | file |
---|
304 | |
---|
305 | @type ROOT_USERNAME: string |
---|
306 | @cvar ROOT_USERNAME: root username - used to determine output directory |
---|
307 | for trust roots |
---|
308 | |
---|
309 | @type ROOT_TRUSTROOT_DIR: string |
---|
310 | @param ROOT_TRUSTROOT_DIR: default trust root directory if running as root |
---|
311 | user |
---|
312 | |
---|
313 | @type USER_TRUSTROOT_DIR: string |
---|
314 | @param USER_TRUSTROOT_DIR: default trust root directory for users other |
---|
315 | than root |
---|
316 | |
---|
317 | @type X509_CERT_DIR_ENVVARNAME: string |
---|
318 | @param X509_CERT_DIR_ENVVARNAME: environment variable name 'X509_CERT_DIR', |
---|
319 | which if set points to the location of the trust roots |
---|
320 | """ |
---|
321 | MYPROXY_SERVER_ENVVARNAME = 'MYPROXY_SERVER' |
---|
322 | MYPROXY_SERVER_PORT_ENVVARNAME = 'MYPROXY_SERVER_PORT' |
---|
323 | MYPROXY_SERVER_DN_ENVVARNAME = 'MYPROXY_SERVER_DN' |
---|
324 | |
---|
325 | GLOBUS_LOCATION_ENVVARNAME = 'GLOBUS_LOCATION' |
---|
326 | |
---|
327 | GET_CMD="""VERSION=MYPROXYv2 |
---|
328 | COMMAND=0 |
---|
329 | USERNAME=%s |
---|
330 | PASSPHRASE=%s |
---|
331 | LIFETIME=%d""" |
---|
332 | |
---|
333 | PUT_CMD="""VERSION=MYPROXYv2 |
---|
334 | COMMAND=1 |
---|
335 | USERNAME=%s |
---|
336 | PASSPHRASE=<pass phrase> |
---|
337 | LIFETIME=%d""" |
---|
338 | |
---|
339 | INFO_CMD="""VERSION=MYPROXYv2 |
---|
340 | COMMAND=2 |
---|
341 | USERNAME=%s |
---|
342 | PASSPHRASE=PASSPHRASE |
---|
343 | LIFETIME=0""" |
---|
344 | |
---|
345 | DESTROY_CMD="""VERSION=MYPROXYv2 |
---|
346 | COMMAND=3 |
---|
347 | USERNAME=%s |
---|
348 | PASSPHRASE=PASSPHRASE |
---|
349 | LIFETIME=0""" |
---|
350 | |
---|
351 | CHANGE_PASSPHRASE_CMD="""VERSION=MYPROXYv2 |
---|
352 | COMMAND=4 |
---|
353 | USERNAME=%s |
---|
354 | PASSPHRASE=%s |
---|
355 | NEW_PHRASE=%s |
---|
356 | LIFETIME=0""" |
---|
357 | |
---|
358 | STORE_CMD="""VERSION=MYPROXYv2 |
---|
359 | COMMAND=5 |
---|
360 | USERNAME=%s |
---|
361 | PASSPHRASE= |
---|
362 | LIFETIME=%d""" |
---|
363 | |
---|
364 | GET_TRUST_ROOTS_CMD="""VERSION=MYPROXYv2 |
---|
365 | COMMAND=7 |
---|
366 | USERNAME=%s |
---|
367 | PASSPHRASE=%s |
---|
368 | LIFETIME=0 |
---|
369 | TRUSTED_CERTS=1""" |
---|
370 | |
---|
371 | TRUSTED_CERTS_FIELDNAME = 'TRUSTED_CERTS' |
---|
372 | TRUSTED_CERTS_FILEDATA_FIELDNAME_PREFIX = 'FILEDATA_' |
---|
373 | |
---|
374 | HOSTCERT_SUBDIRPATH = ('etc', 'hostcert.pem') |
---|
375 | HOSTKEY_SUBDIRPATH = ('etc', 'hostkey.pem') |
---|
376 | |
---|
377 | PROXY_FILE_PERMISSIONS = 0600 |
---|
378 | |
---|
379 | # Work out default location of proxy file if it exists. This is set if a |
---|
380 | # call has been made previously to logon / get-delegation |
---|
381 | DEF_PROXY_FILEPATH = sys.platform == ('win32' and 'proxy' or |
---|
382 | sys.platform in ('linux2', 'darwin') and |
---|
383 | '/tmp/x509up_u%s' % (os.getuid()) |
---|
384 | or None) |
---|
385 | |
---|
386 | PRIKEY_NBITS = 4096 |
---|
387 | MESSAGE_DIGEST_TYPE = "md5" |
---|
388 | SERVER_RESP_BLK_SIZE = 8192 |
---|
389 | MAX_RECV_TRIES = 1024 |
---|
390 | |
---|
391 | # valid configuration property keywords |
---|
392 | PROPERTY_DEFAULTS = { |
---|
393 | 'hostname': 'localhost', |
---|
394 | 'port': 7512, |
---|
395 | 'cnPrefix': MyProxyServerSSLCertVerification.SERVER_CN_PREFIX, |
---|
396 | 'serverDN': None, |
---|
397 | 'openSSLConfFilePath': '', |
---|
398 | 'proxyCertMaxLifetime': 43200, |
---|
399 | 'proxyCertLifetime': 43200, |
---|
400 | 'caCertDir': None |
---|
401 | } |
---|
402 | |
---|
403 | ROOT_USERNAME = 'root' |
---|
404 | ROOT_TRUSTROOT_DIR = '/etc/grid-security/certificates' |
---|
405 | USER_TRUSTROOT_DIR = '~/.globus/certificates' |
---|
406 | X509_CERT_DIR_ENVVARNAME = 'X509_CERT_DIR' |
---|
407 | |
---|
408 | # Restrict attributes to the above properties, their equivalent |
---|
409 | # protected values + extra OpenSSL config object. |
---|
410 | __slots__ = tuple(['__' + k for k in PROPERTY_DEFAULTS.keys()]) |
---|
411 | __slots__ += ('__openSSLConfig', '__cfg', '__serverSSLCertVerify') |
---|
412 | |
---|
413 | def __init__(self, cfgFilePath=None, **prop): |
---|
414 | """Make any initial settings for client connections to MyProxy |
---|
415 | |
---|
416 | Settings are held in a dictionary which can be set from **prop, |
---|
417 | a call to setProperties() or by passing settings in an XML file |
---|
418 | given by cfgFilePath |
---|
419 | |
---|
420 | @param cfgFilePath: set properties via a configuration file |
---|
421 | @type cfgFilePath: basestring |
---|
422 | @param **prop: set properties via keywords - see |
---|
423 | PROPERTY_DEFAULTS class variable for a list of these |
---|
424 | @type **prop: dict |
---|
425 | """ |
---|
426 | self.__serverSSLCertVerify = MyProxyServerSSLCertVerification() |
---|
427 | self.__hostname = None |
---|
428 | self.__port = None |
---|
429 | self.__serverDN = None |
---|
430 | self.__openSSLConfFilePath = None |
---|
431 | self.__proxyCertMaxLifetime = MyProxyClient.PROPERTY_DEFAULTS[ |
---|
432 | 'proxyCertMaxLifetime'] |
---|
433 | self.__proxyCertLifetime = MyProxyClient.PROPERTY_DEFAULTS[ |
---|
434 | 'proxyCertLifetime'] |
---|
435 | self.__caCertDir = None |
---|
436 | |
---|
437 | self.__cfg = None |
---|
438 | |
---|
439 | # Configuration file used to get default subject when generating a |
---|
440 | # new proxy certificate request |
---|
441 | self.__openSSLConfig = OpenSSLConfig() |
---|
442 | |
---|
443 | # Server host name - take from environment variable if available |
---|
444 | self.hostname = os.environ.get(MyProxyClient.MYPROXY_SERVER_ENVVARNAME, |
---|
445 | MyProxyClient.PROPERTY_DEFAULTS['hostname']) |
---|
446 | |
---|
447 | # ... and port number |
---|
448 | self.port = int(os.environ.get( |
---|
449 | MyProxyClient.MYPROXY_SERVER_PORT_ENVVARNAME, |
---|
450 | MyProxyClient.PROPERTY_DEFAULTS['port'])) |
---|
451 | |
---|
452 | # Server Distinguished Name |
---|
453 | serverDN = os.environ.get(MyProxyClient.MYPROXY_SERVER_DN_ENVVARNAME, |
---|
454 | MyProxyClient.PROPERTY_DEFAULTS['serverDN']) |
---|
455 | if serverDN is not None: |
---|
456 | self.serverDN = serverDN |
---|
457 | |
---|
458 | # Set trust root - the directory containing the CA certificates for |
---|
459 | # verifying the MyProxy server's SSL certificate |
---|
460 | self.setDefaultCACertDir() |
---|
461 | |
---|
462 | # Any keyword settings override the defaults above |
---|
463 | for opt, val in prop.items(): |
---|
464 | setattr(self, opt, val) |
---|
465 | |
---|
466 | # If properties file is set any parameters settings in file will |
---|
467 | # override those set by input keyword or the defaults |
---|
468 | if cfgFilePath is not None: |
---|
469 | self.parseConfig(cfg=cfgFilePath) |
---|
470 | |
---|
471 | def setDefaultCACertDir(self): |
---|
472 | '''Make default trust root setting - the directory containing the CA |
---|
473 | certificates for verifying the MyProxy server's SSL certificate. |
---|
474 | |
---|
475 | The setting is made by using standard Globus defined locations and |
---|
476 | environment variable settings |
---|
477 | ''' |
---|
478 | |
---|
479 | # Check for X509_CERT_DIR environment variable |
---|
480 | x509CertDir = os.environ.get(MyProxyClient.X509_CERT_DIR_ENVVARNAME) |
---|
481 | if x509CertDir is not None: |
---|
482 | self.caCertDir = x509CertDir |
---|
483 | |
---|
484 | # Check for running as root user |
---|
485 | elif os.environ.get(MyProxyClient.ROOT_USERNAME) is not None: |
---|
486 | self.caCertDir = MyProxyClient.ROOT_TRUSTROOT_DIR |
---|
487 | |
---|
488 | # Default to non-root standard location |
---|
489 | else: |
---|
490 | self.caCertDir = os.path.expanduser( |
---|
491 | MyProxyClient.USER_TRUSTROOT_DIR) |
---|
492 | |
---|
493 | def _getServerSSLCertVerify(self): |
---|
494 | return self.__serverSSLCertVerify |
---|
495 | |
---|
496 | def _setServerSSLCertVerify(self, value): |
---|
497 | if not isinstance(value, MyProxyServerSSLCertVerification): |
---|
498 | raise TypeError('Expecting %r derived type for ' |
---|
499 | '"serverSSLCertVerify" attribute; got %r' % |
---|
500 | MyProxyServerSSLCertVerification, |
---|
501 | value) |
---|
502 | self.__serverSSLCertVerify = value |
---|
503 | |
---|
504 | serverSSLCertVerify = property(_getServerSSLCertVerify, |
---|
505 | _setServerSSLCertVerify, |
---|
506 | doc="Class with a __call__ method which is " |
---|
507 | "passed to the SSL context to verify " |
---|
508 | "the peer (MyProxy server) certificate " |
---|
509 | "in the SSL handshake between this " |
---|
510 | "client and the MyProxy server") |
---|
511 | |
---|
512 | def parseConfig(self, cfg, section='DEFAULT'): |
---|
513 | '''Extract parameters from _cfg config object''' |
---|
514 | |
---|
515 | if isinstance(cfg, basestring): |
---|
516 | cfgFilePath = os.path.expandvars(cfg) |
---|
517 | self.__cfg = CaseSensitiveConfigParser() |
---|
518 | self.__cfg.read(cfgFilePath) |
---|
519 | else: |
---|
520 | cfgFilePath = None |
---|
521 | self.__cfg = cfg |
---|
522 | |
---|
523 | for key, val in self.__cfg.items(section): |
---|
524 | setattr(self, key, val) |
---|
525 | |
---|
526 | # Get/Set Property methods |
---|
527 | def _getHostname(self): |
---|
528 | return self.__hostname |
---|
529 | |
---|
530 | def _setHostname(self, val): |
---|
531 | """Also sets SSL Certificate verification object property!""" |
---|
532 | if not isinstance(val, basestring): |
---|
533 | raise TypeError("Expecting string type for hostname " |
---|
534 | "attribute") |
---|
535 | self.__hostname = val |
---|
536 | self.__serverSSLCertVerify.hostname = val |
---|
537 | |
---|
538 | hostname = property(fget=_getHostname, |
---|
539 | fset=_setHostname, |
---|
540 | doc="hostname of MyProxy server") |
---|
541 | |
---|
542 | def _getPort(self): |
---|
543 | return self.__port |
---|
544 | |
---|
545 | def _setPort(self, val): |
---|
546 | if isinstance(val, basestring): |
---|
547 | self.__port = int(val) |
---|
548 | elif isinstance(val, int): |
---|
549 | self.__port = val |
---|
550 | else: |
---|
551 | raise TypeError("Expecting int type for port attribute") |
---|
552 | |
---|
553 | port = property(fget=_getPort, |
---|
554 | fset=_setPort, |
---|
555 | doc="Port number for MyProxy server") |
---|
556 | |
---|
557 | def _getServerDN(self): |
---|
558 | return self.__serverDN |
---|
559 | |
---|
560 | def _setServerDN(self, val): |
---|
561 | """Also sets SSL Certificate verification object property!""" |
---|
562 | if not isinstance(val, basestring): |
---|
563 | raise TypeError("Expecting string type for serverDN attribute") |
---|
564 | |
---|
565 | self.__serverDN = val |
---|
566 | self.__serverSSLCertVerify.certDN = val |
---|
567 | |
---|
568 | serverDN = property(fget=_getServerDN, |
---|
569 | fset=_setServerDN, |
---|
570 | doc="Distinguished Name for MyProxy Server " |
---|
571 | "Certificate") |
---|
572 | |
---|
573 | def _getServerCNPrefix(self): |
---|
574 | """References SSL Certificate verification object property!""" |
---|
575 | return self.__serverSSLCertVerify.cnPrefix |
---|
576 | |
---|
577 | def _setServerCNPrefix(self, val): |
---|
578 | """Sets SSL Certificate verification object property!""" |
---|
579 | self.__serverSSLCertVerify.cnPrefix = val |
---|
580 | |
---|
581 | serverCNPrefix = property(fget=_getServerCNPrefix, |
---|
582 | fset=_setServerCNPrefix, |
---|
583 | doc="Prefix for MyProxy Server Certificate " |
---|
584 | "Distinguished Name CoomonName field; " |
---|
585 | "usually set to host/ for Globus host " |
---|
586 | "certificates") |
---|
587 | |
---|
588 | def _getOpenSSLConfFilePath(self): |
---|
589 | return self.__openSSLConfFilePath |
---|
590 | |
---|
591 | def _setOpenSSLConfFilePath(self, val): |
---|
592 | if not isinstance(val, basestring): |
---|
593 | raise TypeError('Expecting string type for "openSSLConfFilePath" ' |
---|
594 | 'attribute') |
---|
595 | |
---|
596 | self.__openSSLConfFilePath = os.path.expandvars(val) |
---|
597 | self.__openSSLConfig.filePath = self.__openSSLConfFilePath |
---|
598 | self.__openSSLConfig.read() |
---|
599 | |
---|
600 | openSSLConfFilePath = property(fget=_getOpenSSLConfFilePath, |
---|
601 | fset=_setOpenSSLConfFilePath, |
---|
602 | doc="file path for OpenSSL config file") |
---|
603 | |
---|
604 | def _getProxyCertMaxLifetime(self): |
---|
605 | return self.__proxyCertMaxLifetime |
---|
606 | |
---|
607 | def _setProxyCertMaxLifetime(self, val): |
---|
608 | if isinstance(val, basestring): |
---|
609 | self.__proxyCertMaxLifetime = int(val) |
---|
610 | |
---|
611 | elif isinstance(val, int): |
---|
612 | self.__proxyCertMaxLifetime = val |
---|
613 | else: |
---|
614 | raise TypeError("Expecting int type for proxyCertMaxLifetime " |
---|
615 | "attribute") |
---|
616 | |
---|
617 | proxyCertMaxLifetime = property(fget=_getProxyCertMaxLifetime, |
---|
618 | fset=_setProxyCertMaxLifetime, |
---|
619 | doc="Default max. lifetime allowed for " |
---|
620 | "Proxy Certificate retrieved - used " |
---|
621 | "by store method") |
---|
622 | |
---|
623 | def _getProxyCertLifetime(self): |
---|
624 | return self.__proxyCertLifetime |
---|
625 | |
---|
626 | def _setProxyCertLifetime(self, val): |
---|
627 | if isinstance(val, basestring): |
---|
628 | self.__proxyCertLifetime = int(val) |
---|
629 | elif isinstance(val, int): |
---|
630 | self.__proxyCertLifetime = val |
---|
631 | else: |
---|
632 | raise TypeError("Expecting int type for proxyCertLifetime " |
---|
633 | "attribute") |
---|
634 | |
---|
635 | proxyCertLifetime = property(fget=_getProxyCertLifetime, |
---|
636 | fset=_setProxyCertLifetime, |
---|
637 | doc="Default proxy cert. lifetime used in " |
---|
638 | "logon request") |
---|
639 | |
---|
640 | def _getCACertDir(self): |
---|
641 | return self.__caCertDir |
---|
642 | |
---|
643 | def _setCACertDir(self, val): |
---|
644 | '''Specify a directory containing PEM encoded CA certs. used for |
---|
645 | validation of MyProxy server certificate. |
---|
646 | |
---|
647 | Set to None to make OpenSSL.SSL.Context.load_verify_locations ignore |
---|
648 | this parameter |
---|
649 | |
---|
650 | @type val: basestring/None |
---|
651 | @param val: directory path''' |
---|
652 | |
---|
653 | if isinstance(val, basestring): |
---|
654 | if val == '': |
---|
655 | self.__caCertDir = None |
---|
656 | else: |
---|
657 | self.__caCertDir = os.path.expandvars(val) |
---|
658 | |
---|
659 | elif isinstance(val, None): |
---|
660 | self.__caCertDir = val |
---|
661 | else: |
---|
662 | raise TypeError("Expecting string or None type for caCertDir " |
---|
663 | "attribute") |
---|
664 | |
---|
665 | caCertDir = property(fget=_getCACertDir, |
---|
666 | fset=_setCACertDir, |
---|
667 | doc="trust roots directory containing PEM encoded CA " |
---|
668 | "certificates to validate MyProxy server " |
---|
669 | "certificate") |
---|
670 | |
---|
671 | |
---|
672 | def _getOpenSSLConfig(self): |
---|
673 | "Get OpenSSLConfig object property method" |
---|
674 | return self.__openSSLConfig |
---|
675 | |
---|
676 | openSSLConfig = property(fget=_getOpenSSLConfig, doc="OpenSSLConfig object") |
---|
677 | |
---|
678 | def _initConnection(self, |
---|
679 | certFile=None, |
---|
680 | keyFile=None, |
---|
681 | keyFilePassphrase=None, |
---|
682 | verifyPeerWithTrustRoots=True): |
---|
683 | """Initialise connection setting up SSL context and client and |
---|
684 | server side identity checks |
---|
685 | |
---|
686 | @type sslCertFile: basestring |
---|
687 | @param sslCertFile: certificate for SSL client authentication. It may |
---|
688 | be owner of a credential to be acted on or the concatenated proxy |
---|
689 | certificate + proxy's signing cert. SSL client authentication is not |
---|
690 | necessary for getDelegation / logon calls |
---|
691 | @type sslKeyFile: basestring |
---|
692 | @param sslKeyFile: client private key file |
---|
693 | @type keyFilePassphrase: basestring |
---|
694 | @param keyFilePassphrase: pass-phrase protecting private key if set |
---|
695 | @type verifyPeerWithTrustRoots: bool |
---|
696 | @param verifyPeerWithTrustRoots: verify MyProxy server's SSL certificate |
---|
697 | against a list of trusted CA certificates in the CA certificate |
---|
698 | directory set by the "CaCertDir" attribute. This should always be set |
---|
699 | to True for MyProxy client calls unless using the 'bootstrap' trust |
---|
700 | roots mode available with logon and get trust roots calls |
---|
701 | """ |
---|
702 | # Must be version 3 for MyProxy |
---|
703 | context = SSL.Context(SSL.SSLv3_METHOD) |
---|
704 | |
---|
705 | if verifyPeerWithTrustRoots: |
---|
706 | context.load_verify_locations(None, self.caCertDir) |
---|
707 | verifyMode = SSL.VERIFY_PEER|SSL.VERIFY_FAIL_IF_NO_PEER_CERT |
---|
708 | else: |
---|
709 | log.warning("SSL Context verify mode set to SSL.VERIFY_NONE") |
---|
710 | verifyMode = SSL.VERIFY_NONE |
---|
711 | |
---|
712 | # Verify peer's (MyProxy server) certificate |
---|
713 | context.set_verify(verifyMode, self.__serverSSLCertVerify) |
---|
714 | |
---|
715 | if certFile: |
---|
716 | try: |
---|
717 | context.use_certificate_chain_file(certFile) |
---|
718 | def pwdCallback(passphraseMaxLen, |
---|
719 | promptPassphraseTwice, |
---|
720 | passphrase): |
---|
721 | """Private key file password callback function""" |
---|
722 | if len(passphrase) > passphraseMaxLen: |
---|
723 | log.error('Passphrase length %d is greater than the ' |
---|
724 | 'maximum length allowed %d', |
---|
725 | len(passphrase), passphraseMaxLen) |
---|
726 | return '' |
---|
727 | |
---|
728 | return passphrase |
---|
729 | |
---|
730 | if keyFilePassphrase is not None: |
---|
731 | context.set_passwd_cb(pwdCallback, keyFilePassphrase) |
---|
732 | |
---|
733 | context.use_privatekey_file(keyFile) |
---|
734 | except Exception: |
---|
735 | raise MyProxyClientConfigError("Loading certificate " |
---|
736 | "and private key for SSL " |
---|
737 | "connection [also check CA " |
---|
738 | "certificate settings]: %s" % |
---|
739 | traceback.format_exc()) |
---|
740 | |
---|
741 | # Disable for compatibility with myproxy server (er, globus) |
---|
742 | # globus doesn't handle this case, apparently, and instead |
---|
743 | # chokes in proxy delegation code |
---|
744 | context.set_options(SSL.OP_DONT_INSERT_EMPTY_FRAGMENTS) |
---|
745 | |
---|
746 | # connect to myproxy server |
---|
747 | conn = SSL.Connection(context, socket.socket()) |
---|
748 | |
---|
749 | return conn |
---|
750 | |
---|
751 | def _createKeyPair(self, nBitsForKey=PRIKEY_NBITS): |
---|
752 | """Generate key pair and return as PEM encoded string |
---|
753 | @type nBitsForKey: int |
---|
754 | @param nBitsForKey: number of bits for private key generation - |
---|
755 | default is 2048 |
---|
756 | @rtype: OpenSSL.crypto.PKey |
---|
757 | @return: public/private key pair |
---|
758 | """ |
---|
759 | keyPair = crypto.PKey() |
---|
760 | keyPair.generate_key(crypto.TYPE_RSA, nBitsForKey) |
---|
761 | |
---|
762 | return keyPair |
---|
763 | |
---|
764 | def _createCertReq(self, CN, keyPair, messageDigest=MESSAGE_DIGEST_TYPE): |
---|
765 | """Create a certificate request. |
---|
766 | |
---|
767 | @type CN: basestring |
---|
768 | @param CN: Common Name for certificate - effectively the same as the |
---|
769 | username for the MyProxy credential |
---|
770 | @type keyPair: string/None |
---|
771 | @param keyPair: public/private key pair |
---|
772 | @type messageDigest: basestring |
---|
773 | @param messageDigest: message digest type - default is MD5 |
---|
774 | @rtype: base string |
---|
775 | @return certificate request PEM text and private key PEM text |
---|
776 | """ |
---|
777 | |
---|
778 | # Check all required certifcate request DN parameters are set |
---|
779 | # Create certificate request |
---|
780 | certReq = crypto.X509Req() |
---|
781 | |
---|
782 | # Create public key object |
---|
783 | certReq.set_pubkey(keyPair) |
---|
784 | |
---|
785 | # Add the public key to the request |
---|
786 | certReq.sign(keyPair, messageDigest) |
---|
787 | |
---|
788 | derCertReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, |
---|
789 | certReq) |
---|
790 | |
---|
791 | return derCertReq |
---|
792 | |
---|
793 | def _deserializeResponse(self, msg, *fieldNames): |
---|
794 | """ |
---|
795 | Deserialize a MyProxy server response |
---|
796 | |
---|
797 | @param msg: string response message from MyProxy server |
---|
798 | @return: tuple of integer response and errorTxt string (if any) and all |
---|
799 | the fields parsed. fields is a list of two element, field name, field |
---|
800 | value tuples. |
---|
801 | @rtype: tuple |
---|
802 | """ |
---|
803 | lines = msg.split('\n') |
---|
804 | fields = [tuple(line.split('=', 1)) for line in lines][:-1] |
---|
805 | |
---|
806 | # get response value |
---|
807 | respCode = [int(v) for k, v in fields if k == 'RESPONSE'][0] |
---|
808 | |
---|
809 | # get error text |
---|
810 | errorTxt = os.linesep.join([v for k, v in fields if k == 'ERROR']) |
---|
811 | |
---|
812 | # Check for custom fields requested by caller to this method |
---|
813 | if fieldNames: |
---|
814 | fieldsDict = {} |
---|
815 | for k, v in fields: |
---|
816 | names = [name for name in fieldNames if k.startswith(name)] |
---|
817 | if len(names) == 0: |
---|
818 | continue |
---|
819 | else: |
---|
820 | if v.isdigit(): |
---|
821 | fieldsDict[k] = int(v) |
---|
822 | else: |
---|
823 | fieldsDict[k] = v |
---|
824 | |
---|
825 | # Return additional dict item in tuple |
---|
826 | return respCode, errorTxt, fieldsDict |
---|
827 | else: |
---|
828 | return respCode, errorTxt |
---|
829 | |
---|
830 | def _deserializeCerts(self, inputDat): |
---|
831 | """Unpack certificates returned from a get delegation call to the |
---|
832 | server |
---|
833 | |
---|
834 | @param inputDat: string containing the proxy cert and private key |
---|
835 | and signing cert all in DER format |
---|
836 | |
---|
837 | @return list containing the equivalent to the input in PEM format""" |
---|
838 | pemCerts = [] |
---|
839 | dat = inputDat |
---|
840 | |
---|
841 | while dat: |
---|
842 | # find start of cert, get length |
---|
843 | ind = dat.find('\x30\x82') |
---|
844 | if ind < 0: |
---|
845 | break |
---|
846 | |
---|
847 | len = 256*ord(dat[ind+2]) + ord(dat[ind+3]) |
---|
848 | |
---|
849 | # extract der-format cert, and convert to pem |
---|
850 | derCert = dat[ind:ind+len+4] |
---|
851 | x509Cert = crypto.load_certificate(crypto.FILETYPE_ASN1, derCert) |
---|
852 | pemCert = crypto.dump_certificate(crypto.FILETYPE_PEM, x509Cert) |
---|
853 | pemCerts.append(pemCert) |
---|
854 | |
---|
855 | # trim cert from data |
---|
856 | dat = dat[ind + len + 4:] |
---|
857 | |
---|
858 | return pemCerts |
---|
859 | |
---|
860 | @classmethod |
---|
861 | def writeProxyFile(cls, proxyCert, proxyPriKey, userX509Cert, |
---|
862 | filePath=None): |
---|
863 | """Write out proxy cert to file in the same way as myproxy-logon - |
---|
864 | proxy cert, private key, user cert. Nb. output from logon can be |
---|
865 | passed direct into this method |
---|
866 | |
---|
867 | @type proxyCert: string |
---|
868 | @param proxyCert: proxy certificate |
---|
869 | @type proxyPriKey: string |
---|
870 | @param proxyPriKey: private key for proxy |
---|
871 | @type userX509Cert: string |
---|
872 | @param userX509Cert: user certificate which issued the proxy |
---|
873 | @type filePath: string |
---|
874 | @param filePath: set to override the default filePath""" |
---|
875 | |
---|
876 | if filePath is None: |
---|
877 | filePath = MyProxyClient.DEF_PROXY_FILEPATH |
---|
878 | |
---|
879 | if filePath is None: |
---|
880 | MyProxyClientConfigError("Error setting proxy file path - invalid " |
---|
881 | "platform?") |
---|
882 | |
---|
883 | outStr = proxyCert + proxyPriKey + userX509Cert |
---|
884 | open(MyProxyClient.DEF_PROXY_FILEPATH, 'w').write(outStr) |
---|
885 | try: |
---|
886 | # Make sure permissions are set correctly |
---|
887 | os.chmod(MyProxyClient.DEF_PROXY_FILEPATH, |
---|
888 | MyProxyClient.PROXY_FILE_PERMISSIONS) |
---|
889 | except Exception, e: |
---|
890 | # Don't leave the file lying around if couldn't change it's |
---|
891 | # permissions |
---|
892 | os.unlink(MyProxyClient.DEF_PROXY_FILEPATH) |
---|
893 | |
---|
894 | log.error('Unable to set %o permissions for proxy file "%s": %s'% |
---|
895 | (MyProxyClient.PROXY_FILE_PERMISSIONS, |
---|
896 | MyProxyClient.DEF_PROXY_FILEPATH, e)) |
---|
897 | raise |
---|
898 | |
---|
899 | @classmethod |
---|
900 | def readProxyFile(cls, filePath=None): |
---|
901 | """Read proxy cert file following the format used by myproxy-logon - |
---|
902 | proxy, cert, private key, user cert. |
---|
903 | |
---|
904 | @rtype: tuple |
---|
905 | @return: tuple containing proxy cert, private key, user cert""" |
---|
906 | if filePath is None: |
---|
907 | filePath = MyProxyClient.DEF_PROXY_FILEPATH |
---|
908 | |
---|
909 | if filePath is None: |
---|
910 | MyProxyClientConfigError("Error setting proxy file path - invalid " |
---|
911 | "platform?") |
---|
912 | |
---|
913 | proxy = open(MyProxyClient.DEF_PROXY_FILEPATH).read() |
---|
914 | |
---|
915 | # Split certs and key into separate tuple items |
---|
916 | return tuple(['-----BEGIN'+i for i in proxy.split('-----BEGIN')[1:]]) |
---|
917 | |
---|
918 | def put(self, |
---|
919 | username, |
---|
920 | passphrase, |
---|
921 | userCertFile, |
---|
922 | userKeyFile, |
---|
923 | lifetime=None, |
---|
924 | sslCertFile=None, |
---|
925 | sslKeyFile=None, |
---|
926 | sslKeyFilePassphrase=None): |
---|
927 | """Store a proxy credential on the server |
---|
928 | |
---|
929 | Unfortunately this method is not implemented as it requires the creation |
---|
930 | of a proxy certificate by the client but PyOpenSSL doesn't currently |
---|
931 | support the required proxyCertInfo X.509 certificate extension |
---|
932 | |
---|
933 | @raise NotImplementedError: see above |
---|
934 | |
---|
935 | @type username: string |
---|
936 | @param username: username selected for new credential |
---|
937 | @type passphrase: string |
---|
938 | @param passphrase: pass-phrase for new credential. This will be used |
---|
939 | by the server to authenticate later requests. IT must be at least |
---|
940 | 6 characters. The server may impose other restrictions too depending |
---|
941 | on its configuration. |
---|
942 | @type certFile: string |
---|
943 | @param certFile: user's X.509 proxy certificate in PEM format |
---|
944 | @type keyFile: string |
---|
945 | @param keyFile: equivalent private key file in PEM format |
---|
946 | @type sslCertFile: string |
---|
947 | @param sslCertFile: certificate used for client authentication with |
---|
948 | the MyProxy server SSL connection. If not set, |
---|
949 | this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem |
---|
950 | @type sslKeyFile: string |
---|
951 | @param sslKeyFile: corresponding private key file. See explanation |
---|
952 | for sslCertFile |
---|
953 | @type sslKeyFilePassphrase: string |
---|
954 | @param sslKeyFilePassphrase: passphrase for sslKeyFile. Omit if the |
---|
955 | private key is not password protected. |
---|
956 | @type lifetime: int / None |
---|
957 | @param lifetime: the maximum lifetime allowed for retrieved proxy |
---|
958 | credentials in seconds. defaults to proxyCertMaxLifetime attribute value |
---|
959 | """ |
---|
960 | raise NotImplementedError('put method is not currently implemented. ' |
---|
961 | 'It requires the creation of a proxy ' |
---|
962 | 'certificate by the client but PyOpenSSL ' |
---|
963 | 'doesn\'t currently support the required ' |
---|
964 | 'proxyCertInfo X.509 certificate extension.') |
---|
965 | |
---|
966 | def info(self, |
---|
967 | username, |
---|
968 | sslCertFile=None, |
---|
969 | sslKeyFile=None, |
---|
970 | sslKeyFilePassphrase=None): |
---|
971 | """return True/False whether credentials exist on the server for a |
---|
972 | given username |
---|
973 | |
---|
974 | @raise MyProxyClientGetError: |
---|
975 | @raise MyProxyClientRetrieveError: |
---|
976 | |
---|
977 | @type username: string |
---|
978 | @param username: username selected for credential |
---|
979 | @type sslCertFile: string |
---|
980 | @param sslCertFile: certificate used for client authentication with |
---|
981 | the MyProxy server SSL connection. This ID will be set as the owner |
---|
982 | of the stored credentials. Only the owner can later remove |
---|
983 | credentials with myproxy-destroy or the destroy method. If not set, |
---|
984 | this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem |
---|
985 | @type sslKeyFile: string |
---|
986 | @param sslKeyFile: corresponding private key file. See explanation |
---|
987 | for sslCertFile |
---|
988 | @type sslKeyFilePassphrase: string |
---|
989 | @param sslKeyFilePassphrase: passphrase for sslKeyFile. Omit if the |
---|
990 | private key is not password protected. |
---|
991 | """ |
---|
992 | globusLoc = os.environ.get(MyProxyClient.GLOBUS_LOCATION_ENVVARNAME) |
---|
993 | if not sslCertFile: |
---|
994 | if globusLoc: |
---|
995 | sslCertFile = os.path.join(globusLoc, |
---|
996 | *MyProxyClient.HOSTCERT_SUBDIRPATH) |
---|
997 | sslKeyFile = os.path.join(globusLoc, |
---|
998 | *MyProxyClient.HOSTKEY_SUBDIRPATH) |
---|
999 | else: |
---|
1000 | raise MyProxyClientError( |
---|
1001 | "No client authentication cert. and private key file were given") |
---|
1002 | |
---|
1003 | # Set-up SSL connection |
---|
1004 | conn = self._initConnection(certFile=sslCertFile, |
---|
1005 | keyFile=sslKeyFile, |
---|
1006 | keyFilePassphrase=sslKeyFilePassphrase) |
---|
1007 | |
---|
1008 | conn.connect((self.hostname, self.port)) |
---|
1009 | |
---|
1010 | # send globus compatibility stuff |
---|
1011 | conn.write('0') |
---|
1012 | |
---|
1013 | # send info command - ensure conversion from unicode before writing |
---|
1014 | cmd = MyProxyClient.INFO_CMD % username |
---|
1015 | conn.write(str(cmd)) |
---|
1016 | |
---|
1017 | # process server response |
---|
1018 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1019 | |
---|
1020 | # Pass in the names of fields to return in the dictionary 'field' |
---|
1021 | respCode, errorTxt, field = self._deserializeResponse(dat, |
---|
1022 | 'CRED_START_TIME', |
---|
1023 | 'CRED_END_TIME', |
---|
1024 | 'CRED_OWNER') |
---|
1025 | |
---|
1026 | return not bool(respCode), errorTxt, field |
---|
1027 | |
---|
1028 | def changePassphrase(self, |
---|
1029 | username, |
---|
1030 | passphrase, |
---|
1031 | newPassphrase, |
---|
1032 | sslCertFile=None, |
---|
1033 | sslKeyFile=None, |
---|
1034 | sslKeyFilePassphrase=None): |
---|
1035 | """change pass-phrase protecting the credentials for a given username |
---|
1036 | |
---|
1037 | @raise MyProxyClientGetError: |
---|
1038 | @raise MyProxyClientRetrieveError: |
---|
1039 | |
---|
1040 | @param username: username of credential |
---|
1041 | @param passphrase: existing pass-phrase for credential |
---|
1042 | @param newPassphrase: new pass-phrase to replace the existing one. |
---|
1043 | @param sslCertFile: certificate used for client authentication with |
---|
1044 | the MyProxy server SSL connection. This ID will be set as the owner |
---|
1045 | of the stored credentials. Only the owner can later remove |
---|
1046 | credentials with myproxy-destroy or the destroy method. If not set, |
---|
1047 | this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem |
---|
1048 | @param sslKeyFile: corresponding private key file. See explanation |
---|
1049 | for sslCertFile |
---|
1050 | @param sslKeyFilePassphrase: passphrase for sslKeyFile. Omit if the |
---|
1051 | private key is not password protected. |
---|
1052 | @return none |
---|
1053 | """ |
---|
1054 | globusLoc = os.environ.get(MyProxyClient.GLOBUS_LOCATION_ENVVARNAME) |
---|
1055 | if not sslCertFile or not sslKeyFile: |
---|
1056 | if globusLoc: |
---|
1057 | sslCertFile = os.path.join(globusLoc, |
---|
1058 | *MyProxyClient.HOSTCERT_SUBDIRPATH) |
---|
1059 | sslKeyFile = os.path.join(globusLoc, |
---|
1060 | *MyProxyClient.HOSTKEY_SUBDIRPATH) |
---|
1061 | else: |
---|
1062 | raise MyProxyClientError( |
---|
1063 | "No client authentication cert. and private key file were given") |
---|
1064 | |
---|
1065 | # Set-up SSL connection |
---|
1066 | conn = self._initConnection(certFile=sslCertFile, |
---|
1067 | keyFile=sslKeyFile, |
---|
1068 | keyFilePassphrase=sslKeyFilePassphrase) |
---|
1069 | |
---|
1070 | conn.connect((self.hostname, self.port)) |
---|
1071 | |
---|
1072 | # send globus compatibility stuff |
---|
1073 | conn.write('0') |
---|
1074 | |
---|
1075 | # send command - ensure conversion from unicode before writing |
---|
1076 | cmd = MyProxyClient.CHANGE_PASSPHRASE_CMD % (username, |
---|
1077 | passphrase, |
---|
1078 | newPassphrase) |
---|
1079 | conn.write(str(cmd)) |
---|
1080 | |
---|
1081 | # process server response |
---|
1082 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1083 | |
---|
1084 | respCode, errorTxt = self._deserializeResponse(dat) |
---|
1085 | if respCode: |
---|
1086 | raise MyProxyClientGetError(errorTxt) |
---|
1087 | |
---|
1088 | def destroy(self, |
---|
1089 | username, |
---|
1090 | sslCertFile=None, |
---|
1091 | sslKeyFile=None, |
---|
1092 | sslKeyFilePassphrase=None): |
---|
1093 | """destroy credentials from the server for a given username |
---|
1094 | |
---|
1095 | @raise MyProxyClientGetError: |
---|
1096 | @raise MyProxyClientRetrieveError: |
---|
1097 | |
---|
1098 | @param username: username selected for credential |
---|
1099 | @param sslCertFile: certificate used for client authentication with |
---|
1100 | the MyProxy server SSL connection. This ID will be set as the owner |
---|
1101 | of the stored credentials. Only the owner can later remove |
---|
1102 | credentials with myproxy-destroy or the destroy method. If not set, |
---|
1103 | this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem |
---|
1104 | @param sslKeyFile: corresponding private key file. See explanation |
---|
1105 | for sslCertFile |
---|
1106 | @param sslKeyFilePassphrase: passphrase for sslKeyFile. Omit if the |
---|
1107 | private key is not password protected. |
---|
1108 | @return none |
---|
1109 | """ |
---|
1110 | globusLoc = os.environ.get(MyProxyClient.GLOBUS_LOCATION_ENVVARNAME) |
---|
1111 | if not sslCertFile or not sslKeyFile: |
---|
1112 | if globusLoc: |
---|
1113 | sslCertFile = os.path.join(globusLoc, |
---|
1114 | *MyProxyClient.HOSTCERT_SUBDIRPATH) |
---|
1115 | sslKeyFile = os.path.join(globusLoc, |
---|
1116 | *MyProxyClient.HOSTKEY_SUBDIRPATH) |
---|
1117 | else: |
---|
1118 | raise MyProxyClientError( |
---|
1119 | "No client authentication cert. and private key file were given") |
---|
1120 | |
---|
1121 | # Set-up SSL connection |
---|
1122 | conn = self._initConnection(certFile=sslCertFile, |
---|
1123 | keyFile=sslKeyFile, |
---|
1124 | keyFilePassphrase=sslKeyFilePassphrase) |
---|
1125 | |
---|
1126 | conn.connect((self.hostname, self.port)) |
---|
1127 | |
---|
1128 | # send globus compatibility stuff |
---|
1129 | conn.write('0') |
---|
1130 | |
---|
1131 | # send destroy command - ensure conversion from unicode before writing |
---|
1132 | cmd = MyProxyClient.DESTROY_CMD % username |
---|
1133 | conn.write(str(cmd)) |
---|
1134 | |
---|
1135 | # process server response |
---|
1136 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1137 | |
---|
1138 | respCode, errorTxt = self._deserializeResponse(dat) |
---|
1139 | if respCode: |
---|
1140 | raise MyProxyClientGetError(errorTxt) |
---|
1141 | |
---|
1142 | def store(self, |
---|
1143 | username, |
---|
1144 | passphrase, |
---|
1145 | certFile, |
---|
1146 | keyFile, |
---|
1147 | sslCertFile=None, |
---|
1148 | sslKeyFile=None, |
---|
1149 | sslKeyFilePassphrase=None, |
---|
1150 | lifetime=None, |
---|
1151 | force=True): |
---|
1152 | """Upload credentials to the server |
---|
1153 | |
---|
1154 | @raise MyProxyClientGetError: |
---|
1155 | @raise MyProxyClientRetrieveError: |
---|
1156 | |
---|
1157 | @type username: string |
---|
1158 | @param username: username selected for new credential |
---|
1159 | @type passphrase: string |
---|
1160 | @param passphrase: pass-phrase for new credential. This is the pass |
---|
1161 | phrase which protects keyfile. |
---|
1162 | @type certFile: string |
---|
1163 | @param certFile: user's X.509 certificate in PEM format |
---|
1164 | @type keyFile: string |
---|
1165 | @param keyFile: equivalent private key file in PEM format |
---|
1166 | @type sslCertFile: string |
---|
1167 | @param sslCertFile: certificate used for client authentication with |
---|
1168 | the MyProxy server SSL connection. This ID will be set as the owner |
---|
1169 | of the stored credentials. Only the owner can later remove |
---|
1170 | credentials with myproxy-destroy or the destroy method. If not set, |
---|
1171 | this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem or if this |
---|
1172 | is not set, certFile |
---|
1173 | @type sslKeyFile: string |
---|
1174 | @param sslKeyFile: corresponding private key file. See explanation |
---|
1175 | for sslCertFile |
---|
1176 | @type sslKeyFilePassphrase: string |
---|
1177 | @param sslKeyFilePassphrase: passphrase for sslKeyFile. Omit if the |
---|
1178 | private key is not password protected. Nb. keyFile is expected to |
---|
1179 | be passphrase protected as this will be the passphrase used for |
---|
1180 | logon / getDelegation. |
---|
1181 | @type Force: bool |
---|
1182 | @param force: set to True to overwrite any existing creds with the |
---|
1183 | same username. If, force=False a check is made with a call to info. |
---|
1184 | If creds already, exist exit without proceeding |
---|
1185 | """ |
---|
1186 | |
---|
1187 | lifetime = lifetime or self.proxyCertMaxLifetime |
---|
1188 | |
---|
1189 | # Inputs must be string type otherwise server will reject the request |
---|
1190 | if isinstance(username, unicode): |
---|
1191 | username = str(username) |
---|
1192 | |
---|
1193 | if isinstance(passphrase, unicode): |
---|
1194 | passphrase = str(passphrase) |
---|
1195 | |
---|
1196 | globusLoc = os.environ.get(MyProxyClient.GLOBUS_LOCATION_ENVVARNAME) |
---|
1197 | if not sslCertFile or not sslKeyFile: |
---|
1198 | if globusLoc: |
---|
1199 | sslCertFile = os.path.join(globusLoc, |
---|
1200 | *MyProxyClient.HOSTCERT_SUBDIRPATH) |
---|
1201 | sslKeyFile = os.path.join(globusLoc, |
---|
1202 | *MyProxyClient.HOSTKEY_SUBDIRPATH) |
---|
1203 | else: |
---|
1204 | # Default so that the owner is the same as the ID of the |
---|
1205 | # credentials to be uploaded. |
---|
1206 | sslCertFile = certFile |
---|
1207 | sslKeyFile = keyFile |
---|
1208 | sslKeyFilePassphrase = passphrase |
---|
1209 | |
---|
1210 | if not force: |
---|
1211 | # Check credentials don't already exist |
---|
1212 | if self.info(username, |
---|
1213 | sslCertFile=sslCertFile, |
---|
1214 | sslKeyFile=sslKeyFile, |
---|
1215 | sslKeyFilePassphrase=sslKeyFilePassphrase)[0]: |
---|
1216 | raise MyProxyCredentialsAlreadyExist( |
---|
1217 | "Credentials already exist for user: %s" % username) |
---|
1218 | |
---|
1219 | # Set up SSL connection |
---|
1220 | conn = self._initConnection(certFile=sslCertFile, |
---|
1221 | keyFile=sslKeyFile, |
---|
1222 | keyFilePassphrase=sslKeyFilePassphrase) |
---|
1223 | |
---|
1224 | conn.connect((self.hostname, self.port)) |
---|
1225 | |
---|
1226 | # send globus compatibility stuff |
---|
1227 | conn.write('0') |
---|
1228 | |
---|
1229 | # send store command - ensure conversion from unicode before writing |
---|
1230 | cmd = MyProxyClient.STORE_CMD % (username, lifetime) |
---|
1231 | conn.write(str(cmd)) |
---|
1232 | |
---|
1233 | # process server response |
---|
1234 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1235 | |
---|
1236 | respCode, errorTxt = self._deserializeResponse(dat) |
---|
1237 | if respCode: |
---|
1238 | raise MyProxyClientGetError(errorTxt) |
---|
1239 | |
---|
1240 | # Send certificate and private key |
---|
1241 | certTxt = open(certFile).read() |
---|
1242 | keyTxt = open(keyFile).read() |
---|
1243 | |
---|
1244 | conn.send(certTxt + keyTxt) |
---|
1245 | |
---|
1246 | |
---|
1247 | # process server response |
---|
1248 | resp = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1249 | respCode, errorTxt = self._deserializeResponse(resp) |
---|
1250 | if respCode: |
---|
1251 | raise MyProxyClientRetrieveError(errorTxt) |
---|
1252 | |
---|
1253 | def logon(self, username, passphrase, lifetime=None, keyPair=None, |
---|
1254 | certReq=None, nBitsForKey=PRIKEY_NBITS, bootstrap=False, |
---|
1255 | updateTrustRoots=False): |
---|
1256 | """Retrieve a proxy credential from a MyProxy server |
---|
1257 | |
---|
1258 | Exceptions: MyProxyClientGetError, MyProxyClientRetrieveError |
---|
1259 | |
---|
1260 | @type username: basestring |
---|
1261 | @param username: username of credential |
---|
1262 | |
---|
1263 | @type passphrase: basestring |
---|
1264 | @param passphrase: pass-phrase for private key of credential held on |
---|
1265 | server |
---|
1266 | |
---|
1267 | @type lifetime: int |
---|
1268 | @param lifetime: lifetime for generated certificate |
---|
1269 | |
---|
1270 | @type keyPair: OpenSSL.crypto.PKey |
---|
1271 | @param keyPair: Public/Private key pair. This is ignored if a |
---|
1272 | certificate request is passed via the certReq keyword |
---|
1273 | |
---|
1274 | @type certReq: string |
---|
1275 | @param certReq: ASN1 format certificate request, if none set, one is |
---|
1276 | created along with a key pair |
---|
1277 | |
---|
1278 | @type nBitsForKey: int |
---|
1279 | @param nBitsForKey: number of bits to use when generating key pair, |
---|
1280 | defaults to the PRIKEY_NBITS class variable setting. This keyword is |
---|
1281 | ignored if a key pair is passed in from an external source via the |
---|
1282 | keyPair keyword |
---|
1283 | |
---|
1284 | @rtype: tuple |
---|
1285 | @return credentials as strings in PEM format: the |
---|
1286 | user certificate, it's private key and the issuing certificate. The |
---|
1287 | issuing certificate is only set if the user certificate is a proxy |
---|
1288 | |
---|
1289 | @type bootstrap: bool |
---|
1290 | @param bootstrap: If set to True, bootstrap trust roots i.e. connect to |
---|
1291 | MyProxy server without verification of the server's SSL certificate |
---|
1292 | against any CA certificates. Set to False, for default behaviour: |
---|
1293 | verify server SSL certificate against CA certificates held in location |
---|
1294 | set by the "caCertDir" attribute. If bootstrap is set, updateTrustRoots |
---|
1295 | will be forced to True also |
---|
1296 | |
---|
1297 | @type updateTrustRoots: bool |
---|
1298 | @param updateTrustRoots: set to True to update the trust roots |
---|
1299 | """ |
---|
1300 | if bootstrap: |
---|
1301 | log.info('Bootstrapping MyProxy server root of trust.') |
---|
1302 | |
---|
1303 | # Bootstrap implies update to trust roots |
---|
1304 | updateTrustRoots = True |
---|
1305 | |
---|
1306 | if updateTrustRoots: |
---|
1307 | self.getTrustRoots(username, |
---|
1308 | passphrase, |
---|
1309 | writeToCACertDir=True, |
---|
1310 | bootstrap=bootstrap) |
---|
1311 | |
---|
1312 | lifetime = lifetime or self.proxyCertLifetime |
---|
1313 | |
---|
1314 | # Certificate request may be passed as an input but if not generate it |
---|
1315 | # here request here |
---|
1316 | if certReq is None: |
---|
1317 | # If no key pair was passed, generate here |
---|
1318 | if keyPair is None: |
---|
1319 | keyPair = self._createKeyPair(nBitsForKey=nBitsForKey) |
---|
1320 | |
---|
1321 | certReq = self._createCertReq(username, keyPair) |
---|
1322 | |
---|
1323 | if keyPair is not None: |
---|
1324 | pemKeyPair = crypto.dump_privatekey(crypto.FILETYPE_PEM, keyPair) |
---|
1325 | |
---|
1326 | # Set-up SSL connection |
---|
1327 | conn = self._initConnection() |
---|
1328 | conn.connect((self.hostname, self.port)) |
---|
1329 | |
---|
1330 | # send globus compatibility stuff |
---|
1331 | conn.write('0') |
---|
1332 | |
---|
1333 | # send get command - ensure conversion from unicode before writing |
---|
1334 | cmd = MyProxyClient.GET_CMD % (username, passphrase, lifetime) |
---|
1335 | |
---|
1336 | conn.write(str(cmd)) |
---|
1337 | |
---|
1338 | # process server response |
---|
1339 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1340 | |
---|
1341 | respCode, errorTxt = self._deserializeResponse(dat) |
---|
1342 | if respCode: |
---|
1343 | raise MyProxyClientGetError(errorTxt) |
---|
1344 | |
---|
1345 | # Send certificate request |
---|
1346 | conn.send(certReq) |
---|
1347 | |
---|
1348 | # process certificates |
---|
1349 | # - 1st byte , number of certs |
---|
1350 | dat = conn.recv(1) |
---|
1351 | nCerts = ord(dat[0]) |
---|
1352 | |
---|
1353 | # - n certs |
---|
1354 | dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1355 | |
---|
1356 | # process server response |
---|
1357 | resp = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1358 | respCode, errorTxt = self._deserializeResponse(resp) |
---|
1359 | if respCode: |
---|
1360 | raise MyProxyClientRetrieveError(errorTxt) |
---|
1361 | |
---|
1362 | # deserialize certs from received cert data |
---|
1363 | pemCerts = self._deserializeCerts(dat) |
---|
1364 | if len(pemCerts) != nCerts: |
---|
1365 | MyProxyClientRetrieveError("%d certs expected, %d received" % |
---|
1366 | (nCerts, len(pemCerts))) |
---|
1367 | |
---|
1368 | if keyPair is not None: |
---|
1369 | # Return certs and private key |
---|
1370 | # - proxy or dynamically issued certificate (MyProxy CA mode) |
---|
1371 | # - private key |
---|
1372 | # - rest of cert chain if proxy cert issued |
---|
1373 | creds = [pemCerts[0], pemKeyPair] |
---|
1374 | creds.extend(pemCerts[1:]) |
---|
1375 | else: |
---|
1376 | # Key generated externally - return certificate chain only |
---|
1377 | creds = pemCerts |
---|
1378 | |
---|
1379 | |
---|
1380 | return tuple(creds) |
---|
1381 | |
---|
1382 | def getDelegation(self, *arg, **kw): |
---|
1383 | """Retrieve proxy cert for user - same as logon""" |
---|
1384 | return self.logon(*arg, **kw) |
---|
1385 | |
---|
1386 | def getTrustRoots(self, |
---|
1387 | username='', |
---|
1388 | passphrase='', |
---|
1389 | writeToCACertDir=False, |
---|
1390 | bootstrap=False): |
---|
1391 | """Get trust roots for the given MyProxy server |
---|
1392 | |
---|
1393 | @type username: basestring |
---|
1394 | @param username: username (optional) |
---|
1395 | |
---|
1396 | @type passphrase: basestring |
---|
1397 | @param passphrase: pass-phrase (optional) |
---|
1398 | server |
---|
1399 | |
---|
1400 | @type writeToCACertDir: bool |
---|
1401 | @param writeToCACertDir: if set to True, write the retrieved trust roots |
---|
1402 | out to the directory specified by the "caCertDir" attribute |
---|
1403 | |
---|
1404 | @type bootstrap: bool |
---|
1405 | @param bootstrap: If set to True, bootstrap trust roots i.e. connect to |
---|
1406 | MyProxy server without verification of the server's SSL certificate |
---|
1407 | against any CA certificates. Set to False, for default behaviour: |
---|
1408 | verify server SSL certificate against CA certificates held in location |
---|
1409 | set by the "caCertDir" attribute. |
---|
1410 | |
---|
1411 | @return: trust root files as a dictionary keyed by file name with each |
---|
1412 | item value set to the file contents |
---|
1413 | @rtype: dict |
---|
1414 | """ |
---|
1415 | if bootstrap: |
---|
1416 | log.info('Bootstrapping MyProxy server root of trust.') |
---|
1417 | |
---|
1418 | # Set-up SSL connection |
---|
1419 | conn = self._initConnection(verifyPeerWithTrustRoots=(not bootstrap)) |
---|
1420 | conn.connect((self.hostname, self.port)) |
---|
1421 | |
---|
1422 | # send globus compatibility stuff |
---|
1423 | conn.write('0') |
---|
1424 | |
---|
1425 | # send get command - ensure conversion from unicode before writing |
---|
1426 | cmd = MyProxyClient.GET_TRUST_ROOTS_CMD % (username, passphrase) |
---|
1427 | conn.write(str(cmd)) |
---|
1428 | |
---|
1429 | # process server response chunks until all consumed |
---|
1430 | dat = '' |
---|
1431 | tries = 0 |
---|
1432 | try: |
---|
1433 | for tries in range(MyProxyClient.MAX_RECV_TRIES): |
---|
1434 | dat += conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1435 | except SSL.SysCallError: |
---|
1436 | # Expect this exception when response content exhausted |
---|
1437 | pass |
---|
1438 | |
---|
1439 | # Precaution |
---|
1440 | if tries == MyProxyClient.MAX_RECV_TRIES: |
---|
1441 | log.warning('Maximum %d tries reached for getTrustRoots response ' |
---|
1442 | 'block retrieval with block size %d', |
---|
1443 | MyProxyClient.MAX_RECV_TRIES, |
---|
1444 | MyProxyClient.SERVER_RESP_BLK_SIZE) |
---|
1445 | |
---|
1446 | fieldName = MyProxyClient.TRUSTED_CERTS_FIELDNAME |
---|
1447 | prefix = MyProxyClient.TRUSTED_CERTS_FILEDATA_FIELDNAME_PREFIX |
---|
1448 | respCode, errorTxt, fileData = self._deserializeResponse(dat, |
---|
1449 | fieldName, |
---|
1450 | prefix) |
---|
1451 | if respCode: |
---|
1452 | raise MyProxyClientGetTrustRootsError(errorTxt) |
---|
1453 | |
---|
1454 | filesDict = dict([(k.split(prefix, 1)[1], base64.b64decode(v)) |
---|
1455 | for k, v in fileData.items() if k != fieldName]) |
---|
1456 | |
---|
1457 | if writeToCACertDir: |
---|
1458 | for fileName, fileContents in filesDict.items(): |
---|
1459 | filePath = os.path.join(self.caCertDir, fileName) |
---|
1460 | open(filePath, 'wb').write(fileContents) |
---|
1461 | |
---|
1462 | return filesDict |
---|
1463 | |
---|