source: TI12-security/trunk/MyProxyClient/myproxy/client.py @ 6837

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/MyProxyClient/myproxy/client.py@6837
Revision 6837, 44.9 KB checked in by pjkersha, 11 years ago (diff)

Fixing peer cert verification

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