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

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