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

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

Testing with new getTrustRoots method added.

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