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

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

Working getTrustRoots method but SEGV errors with private key conversion to PEM format. Will make a branch to revisit pyOpenSSL and dump M2Crypto.

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) 2010 Science and Technology Facilities Council"
13__license__ = """BSD - See LICENSE file in top-level directory
14
15For myproxy_logon see Access Grid Toolkit Public License (AGTPL)
16
17This product includes software developed by and/or derived from the Access
18Grid Project (http://www.accessgrid.org) to which the U.S. Government retains
19certain rights."""
20__contact__ = "Philip.Kershaw@stfc.ac.uk"
21__revision__ = '$Id: $'
22import logging
23log = logging.getLogger(__name__)
24
25import sys
26import os
27import socket
28from M2Crypto import X509, RSA, EVP, m2, BIO, SSL, util
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 = 2048 #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 key pair 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        keyPair = RSA.gen_key(nBitsForKey, 65537L,#m2.RSA_F4,
560                              callback=lambda *arg, **kw: None)
561       
562        return keyPair
563           
564    def _createCertReq(self, CN, keyPair, messageDigest=MESSAGE_DIGEST_TYPE):
565        """Create a certificate request.
566       
567        @type CN: basestring
568        @param CN: Common Name for certificate - effectively the same as the
569        username for the MyProxy credential
570        @type keyPair: string/None
571        @param keyPair: public/private key pair
572        @type messageDigest: basestring
573        @param messageDigest: message digest type - default is MD5
574        @rtype: tuple
575        @return certificate request PEM text and private key PEM text
576        """
577       
578        # Check all required certifcate request DN parameters are set               
579        # Create certificate request
580        req = X509.Request()
581       
582        # Create public key object
583        pubKey = EVP.PKey()
584        pubKey.assign_rsa(keyPair)
585       
586        # Add the public key to the request
587        req.set_version(0)
588        req.set_pubkey(pubKey)
589       
590        defaultReqDN = self._openSSLConfig.reqDN
591             
592        # Set DN
593        x509Name = X509.X509_Name()
594        x509Name.CN = CN
595       
596        if defaultReqDN:
597            x509Name.OU = defaultReqDN['OU']
598            x509Name.O = defaultReqDN['O']
599                       
600        req.set_subject_name(x509Name)
601        req.sign(pubKey, messageDigest)
602
603        return req.as_der()
604   
605    def _deserializeResponse(self, msg, *fieldNames):
606        """
607        Deserialize a MyProxy server response
608       
609        @param msg: string response message from MyProxy server
610        @return: tuple of integer response and errorTxt string (if any) and all
611        the fields parsed.  fields is a list of two element, field name, field
612        value tuples.
613        @rtype: tuple
614        """ 
615        lines = msg.split('\n')
616        fields = [tuple(line.split('=', 1)) for line in lines][:-1]
617       
618        # get response value
619        respCode = [int(v) for k, v in fields if k == 'RESPONSE'][0]
620
621        # get error text
622        errorTxt = os.linesep.join([v for k, v in fields if k == 'ERROR'])
623       
624        # Check for custom fields requested by caller to this method
625        if fieldNames:
626            fieldsDict = {}
627            for k, v in fields:
628                names = [name for name in fieldNames if k.startswith(name)]
629                if len(names) == 0:
630                    continue
631                else:
632                    if v.isdigit():
633                        fieldsDict[k] = int(v)
634                    else:
635                        fieldsDict[k] = v
636             
637            # Return additional dict item in tuple 
638            return respCode, errorTxt, fieldsDict
639        else:
640            return respCode, errorTxt   
641 
642    def _deserializeCerts(self, inputDat):
643        """Unpack certificates returned from a get delegation call to the
644        server
645       
646        @param inputDat: string containing the proxy cert and private key
647        and signing cert all in DER format
648       
649        @return list containing the equivalent to the input in PEM format"""
650        pemCerts = []       
651        dat = inputDat
652       
653        while dat:   
654            # find start of cert, get length       
655            ind = dat.find('\x30\x82')
656            if ind < 0:
657                break
658               
659            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
660   
661            # extract der-format cert, and convert to pem
662            derCert = dat[ind:ind+len+4]
663           
664            x509 = X509.load_cert_der_string(derCert)
665            pemCert = x509.as_pem()
666           
667            pemCerts.append(pemCert)
668   
669            # trim cert from data
670            dat = dat[ind + len + 4:]
671           
672        return pemCerts
673   
674   
675    @classmethod
676    def writeProxyFile(cls,proxyCert,proxyPriKey,userX509Cert,filePath=None):
677        """Write out proxy cert to file in the same way as myproxy-logon -
678        proxy cert, private key, user cert.  Nb. output from logon can be
679        passed direct into this method
680       
681        @type proxyCert: string
682        @param proxyCert: proxy certificate
683        @type proxyPriKey: string
684        @param proxyPriKey: private key for proxy
685        @type userX509Cert: string
686        @param userX509Cert: user certificate which issued the proxy
687        @type filePath: string
688        @param filePath: set to override the default filePath"""
689       
690        if filePath is None:
691            filePath = MyProxyClient.DEF_PROXY_FILEPATH
692           
693        if filePath is None:
694            MyProxyClientConfigError("Error setting proxy file path - invalid "
695                                     "platform?")
696       
697        outStr = proxyCert + proxyPriKey + userX509Cert       
698        open(MyProxyClient.DEF_PROXY_FILEPATH, 'w').write(outStr)
699        try:
700            # Make sure permissions are set correctly
701            os.chmod(MyProxyClient.DEF_PROXY_FILEPATH, 
702                     MyProxyClient.PROXY_FILE_PERMISSIONS)
703        except Exception, e:
704            # Don't leave the file lying around if couldn't change it's
705            # permissions
706            os.unlink(MyProxyClient.DEF_PROXY_FILEPATH)
707           
708            log.error('Unable to set %o permissions for proxy file "%s": %s'% 
709                      (MyProxyClient.PROXY_FILE_PERMISSIONS,
710                       MyProxyClient.DEF_PROXY_FILEPATH, e))
711            raise
712
713    @classmethod
714    def readProxyFile(cls, filePath=None):
715        """Read proxy cert file following the format used by myproxy-logon -
716        proxy, cert, private key, user cert.
717       
718        @rtype: tuple
719        @return: tuple containing proxy cert, private key, user cert"""
720        if filePath is None:
721            filePath = MyProxyClient.DEF_PROXY_FILEPATH
722           
723        if filePath is None:
724            MyProxyClientConfigError("Error setting proxy file path - invalid "
725                                     "platform?")
726               
727        proxy = open(MyProxyClient.DEF_PROXY_FILEPATH).read()
728       
729        # Split certs and key into separate tuple items
730        return tuple(['-----BEGIN'+i for i in proxy.split('-----BEGIN')[1:]])
731   
732
733    def info(self,
734             username, 
735             ownerCertFile=None,
736             ownerKeyFile=None,
737             ownerPassphrase=None):
738        """return True/False whether credentials exist on the server for a
739        given username
740       
741        @raise MyProxyClientGetError:
742        @raise MyProxyClientRetrieveError:
743       
744        @type username: string
745        @param username: username selected for credential
746        @type ownerCertFile: string
747        @param ownerCertFile: certificate used for client authentication with
748        the MyProxy server SSL connection.  This ID will be set as the owner
749        of the stored credentials.  Only the owner can later remove
750        credentials with myproxy-destroy or the destroy method.  If not set,
751        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
752        @type ownerKeyFile: string
753        @param ownerKeyFile: corresponding private key file.  See explanation
754        for ownerCertFile
755        @type ownerPassphrase: string
756        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
757        private key is not password protected.
758        """
759        globusLoc = os.environ.get('GLOBUS_LOCATION')
760        if not ownerCertFile:
761            if globusLoc:
762                ownerCertFile = os.path.join(globusLoc, 
763                                            *MyProxyClient._hostCertSubDirPath)
764                ownerKeyFile = os.path.join(globusLoc, 
765                                            *MyProxyClient._hostKeySubDirPath)
766            else:
767                raise MyProxyClientError(
768            "No client authentication cert. and private key file were given")
769
770        # Set-up SSL connection
771        conn = self._initConnection(ownerCertFile=ownerCertFile,
772                                    ownerKeyFile=ownerKeyFile,
773                                    ownerPassphrase=ownerPassphrase)
774       
775        conn.connect((self.hostname, self.port))
776       
777        # send globus compatibility stuff
778        conn.write('0')
779   
780        # send info command - ensure conversion from unicode before writing
781        cmd = MyProxyClient.INFO_CMD % username
782        conn.write(str(cmd))
783   
784        # process server response
785        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
786         
787        # Pass in the names of fields to return in the dictionary 'field'
788        respCode, errorTxt, field = self._deserializeResponse(dat, 
789                                                              'CRED_START_TIME', 
790                                                              'CRED_END_TIME', 
791                                                              'CRED_OWNER')
792
793        return not bool(respCode), errorTxt, field
794
795
796    def changePassphrase(self,
797                         username, 
798                         passphrase,
799                         newPassphrase,
800                         ownerCertFile=None,
801                         ownerKeyFile=None,
802                         ownerPassphrase=None):
803        """change pass-phrase protecting the credentials for a given username
804       
805        @raise MyProxyClientGetError:
806        @raise MyProxyClientRetrieveError:
807       
808        @param username: username of credential
809        @param passphrase: existing pass-phrase for credential
810        @param newPassphrase: new pass-phrase to replace the existing one.
811        @param ownerCertFile: certificate used for client authentication with
812        the MyProxy server SSL connection.  This ID will be set as the owner
813        of the stored credentials.  Only the owner can later remove
814        credentials with myproxy-destroy or the destroy method.  If not set,
815        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
816        @param ownerKeyFile: corresponding private key file.  See explanation
817        for ownerCertFile
818        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
819        private key is not password protected. 
820        @return none
821        """
822        globusLoc = os.environ.get('GLOBUS_LOCATION')
823        if not ownerCertFile or not ownerKeyFile:
824            if globusLoc:
825                ownerCertFile = os.path.join(globusLoc, 
826                                         *MyProxyClient._hostCertSubDirPath)
827                ownerKeyFile = os.path.join(globusLoc, 
828                                         *MyProxyClient._hostKeySubDirPath)
829            else:
830                raise MyProxyClientError(
831            "No client authentication cert. and private key file were given")
832       
833        # Set-up SSL connection
834        conn = self._initConnection(ownerCertFile=ownerCertFile,
835                                    ownerKeyFile=ownerKeyFile,
836                                    ownerPassphrase=ownerPassphrase)
837
838        conn.connect((self.hostname, self.port))
839       
840        # send globus compatibility stuff
841        conn.write('0')
842   
843        # send command - ensure conversion from unicode before writing
844        cmd = MyProxyClient.CHANGE_PASSPHRASE_CMD % (username, 
845                                                     passphrase,
846                                                     newPassphrase)
847        conn.write(str(cmd))
848   
849        # process server response
850        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
851           
852        respCode, errorTxt = self._deserializeResponse(dat)
853        if respCode:
854            raise MyProxyClientGetError(errorTxt)
855
856
857    def destroy(self,
858                username, 
859                ownerCertFile=None,
860                ownerKeyFile=None,
861                ownerPassphrase=None):
862        """destroy credentials from the server for a given username
863       
864        @raise MyProxyClientGetError:
865        @raise MyProxyClientRetrieveError:
866       
867        @param username: username selected for credential
868        @param ownerCertFile: certificate used for client authentication with
869        the MyProxy server SSL connection.  This ID will be set as the owner
870        of the stored credentials.  Only the owner can later remove
871        credentials with myproxy-destroy or the destroy method.  If not set,
872        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
873        @param ownerKeyFile: corresponding private key file.  See explanation
874        for ownerCertFile
875        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
876        private key is not password protected. 
877        @return none
878        """
879        globusLoc = os.environ.get('GLOBUS_LOCATION')
880        if not ownerCertFile or not ownerKeyFile:
881            if globusLoc:
882                ownerCertFile = os.path.join(globusLoc, 
883                                         *MyProxyClient._hostCertSubDirPath)
884                ownerKeyFile = os.path.join(globusLoc, 
885                                         *MyProxyClient._hostKeySubDirPath)
886            else:
887                raise MyProxyClientError(
888            "No client authentication cert. and private key file were given")
889       
890        # Set-up SSL connection
891        conn = self._initConnection(ownerCertFile=ownerCertFile,
892                                    ownerKeyFile=ownerKeyFile,
893                                    ownerPassphrase=ownerPassphrase)
894
895        conn.connect((self.hostname, self.port))
896       
897        # send globus compatibility stuff
898        conn.write('0')
899   
900        # send destroy command - ensure conversion from unicode before writing
901        cmd = MyProxyClient.DESTROY_CMD % username
902        conn.write(str(cmd))
903   
904        # process server response
905        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
906           
907        respCode, errorTxt = self._deserializeResponse(dat)
908        if respCode:
909            raise MyProxyClientGetError(errorTxt)
910
911
912    def store(self,
913              username,
914              passphrase, 
915              certFile,
916              keyFile,
917              ownerCertFile=None,
918              ownerKeyFile=None,
919              ownerPassphrase=None,
920              lifetime=None,
921              force=True):
922        """Upload credentials to the server
923       
924        @raise MyProxyClientGetError:
925        @raise MyProxyClientRetrieveError:
926       
927        @type username: string
928        @param username: username selected for new credential
929        @type passphrase: string
930        @param passphrase: pass-phrase for new credential.  This is the pass
931        phrase which protects keyfile.
932        @type certFile: string
933        @param certFile: user's X.509 certificate in PEM format
934        @type keyFile: string
935        @param keyFile: equivalent private key file in PEM format
936        @type ownerCertFile: string
937        @param ownerCertFile: certificate used for client authentication with
938        the MyProxy server SSL connection.  This ID will be set as the owner
939        of the stored credentials.  Only the owner can later remove
940        credentials with myproxy-destroy or the destroy method.  If not set,
941        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem or if this
942        is not set, certFile
943        @type ownerKeyFile: string
944        @param ownerKeyFile: corresponding private key file.  See explanation
945        for ownerCertFile
946        @type ownerPassphrase: string
947        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
948        private key is not password protected.  Nb. keyFile is expected to
949        be passphrase protected as this will be the passphrase used for
950        logon / getDelegation.
951        @type Force: bool
952        @param force: set to True to overwrite any existing creds with the
953        same username.  If, force=False a check is made with a call to info.
954        If creds already, exist exit without proceeding
955        """
956       
957        lifetime = lifetime or self.proxyCertMaxLifetime
958
959        # Inputs must be string type otherwise server will reject the request
960        if isinstance(username, unicode):
961            username = str(username)
962           
963        if isinstance(passphrase, unicode):
964            passphrase = str(passphrase)
965       
966        globusLoc = os.environ.get('GLOBUS_LOCATION')
967        if not ownerCertFile or not ownerKeyFile:
968            if globusLoc:
969                ownerCertFile = os.path.join(globusLoc, 
970                                         *MyProxyClient._hostCertSubDirPath)
971                ownerKeyFile = os.path.join(globusLoc, 
972                                         *MyProxyClient._hostKeySubDirPath)
973            else:
974                # Default so that the owner is the same as the ID of the
975                # credentials to be uploaded.
976                ownerCertFile = certFile
977                ownerKeyFile = keyFile
978                ownerPassphrase = passphrase
979               
980        if not force:
981            # Check credentials don't already exist
982            if self.info(username,
983                         ownerCertFile=ownerCertFile,
984                         ownerKeyFile=ownerKeyFile,
985                         ownerPassphrase=ownerPassphrase)[0]:
986                raise MyProxyCredentialsAlreadyExist(
987                        "Credentials already exist for user: %s" % username)
988
989        # Set up SSL connection
990        conn = self._initConnection(ownerCertFile=ownerCertFile,
991                                    ownerKeyFile=ownerKeyFile,
992                                    ownerPassphrase=ownerPassphrase)
993       
994        conn.connect((self.hostname, self.port))
995       
996        # send globus compatibility stuff
997        conn.write('0')
998   
999        # send store command - ensure conversion from unicode before writing
1000        cmd = MyProxyClient.STORE_CMD % (username, lifetime)
1001        conn.write(str(cmd))
1002   
1003        # process server response
1004        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1005           
1006        respCode, errorTxt = self._deserializeResponse(dat)
1007        if respCode:
1008            raise MyProxyClientGetError(errorTxt)
1009       
1010        # Send certificate and private key
1011        certTxt = X509.load_cert(certFile).as_pem()
1012        keyTxt = open(keyFile).read()
1013       
1014        conn.send(certTxt + keyTxt)
1015   
1016   
1017        # process server response
1018        resp = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1019        respCode, errorTxt = self._deserializeResponse(resp)
1020        if respCode:
1021            raise MyProxyClientRetrieveError(errorTxt)
1022       
1023         
1024    def logon(self, username, passphrase, lifetime=None, keyPair=None, 
1025              certReq=None, nBitsForKey=PRIKEY_NBITS):
1026        """Retrieve a proxy credential from a MyProxy server
1027       
1028        Exceptions:  MyProxyClientGetError, MyProxyClientRetrieveError
1029       
1030        @type username: basestring
1031        @param username: username of credential
1032       
1033        @type passphrase: basestring
1034        @param passphrase: pass-phrase for private key of credential held on
1035        server
1036       
1037        @type lifetime: int
1038        @param lifetime: lifetime for generated certificate
1039       
1040        @rtype: tuple
1041        @return credentials as strings in PEM format: the
1042        user certificate, it's private key and the issuing certificate.  The
1043        issuing certificate is only set if the user certificate is a proxy
1044        """
1045       
1046        lifetime = lifetime or self.proxyCertLifetime
1047
1048        # Certificate request may be passed as an input but if not generate it
1049        # here request here
1050        if certReq is None:
1051            # If no key pair was passed, generate here
1052            if keyPair is None:
1053                keyPair = self._createKeys(nBitsForKey=nBitsForKey)
1054               
1055            certReq = self._createCertReq(username, keyPair)
1056
1057        if keyPair is not None: 
1058            pemKeyPair = keyPair.as_pem(cipher=None, 
1059                                        callback=util.no_passphrase_callback)
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 keyPair 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            creds = [pemCerts[0], pemKeyPair]
1107            creds.extend(pemCerts[1:])
1108        else:
1109            # Key generated externally - return certificate chain only
1110            creds = pemCerts
1111
1112       
1113        return tuple(creds)
1114
1115    def getDelegation(self, *arg, **kw):
1116        """Retrieve proxy cert for user - same as logon"""
1117        return self.logon(*arg, **kw)
1118   
1119    def getTrustRoots(self, username='', passphrase=''):
1120        """Get trust roots for the given MyProxy server
1121       
1122        @type username: basestring
1123        @param username: username (optional)
1124       
1125        @type passphrase: basestring
1126        @param passphrase: pass-phrase (optional)
1127        server
1128       
1129        @return: trust root files as a dictionary keyed by file name with each
1130        item value set to the file contents
1131        @rtype: dict
1132        """
1133        # Set-up SSL connection
1134        conn = self._initConnection()
1135        conn.connect((self.hostname, self.port))
1136       
1137        # send globus compatibility stuff
1138        conn.write('0')
1139   
1140        # send get command - ensure conversion from unicode before writing
1141        cmd = MyProxyClient.GET_TRUST_ROOTS_CMD % (username, passphrase)
1142        conn.write(str(cmd))
1143   
1144        # process server response chunks until all consumed
1145        dat = ''
1146        try:
1147            for tries in range(MyProxyClient.MAX_RECV_TRIES):
1148                dat += conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1149        except SSL.SSLError:
1150            # Expect this exception when response content exhausted
1151            pass
1152       
1153        # Precaution
1154        if tries == MyProxyClient.MAX_RECV_TRIES:
1155            log.warning('Maximum %d tries reached for getTrustRoots response '
1156                        'block retrieval with block size %d', 
1157                        MyProxyClient.MAX_RECV_TRIES,
1158                        MyProxyClient.SERVER_RESP_BLK_SIZE)
1159           
1160        respCode, errorTxt, fileData = self._deserializeResponse(dat, 
1161                                                                'TRUSTED_CERTS',
1162                                                                'FILEDATA_')
1163        if respCode:
1164            raise MyProxyClientGetTrustRootsError(errorTxt)
1165       
1166        filesDict = dict([(k, base64.b64decode(v)) 
1167                          for k, v in fileData.items() if k != 'TRUSTED_CERTS'])
1168       
1169        return filesDict
1170       
Note: See TracBrowser for help on using the repository browser.