source: TI12-security/branches/MyProxyClient-pyopenssl/myproxy/client.py @ 6835

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

Completed port to PyOpenSSL from M2Crypto with all unit tests passed. Merge back into the main trunk as next step.

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
28import base64
29import traceback
30from cStringIO import StringIO
31from ConfigParser import SafeConfigParser
32
33from OpenSSL import crypto, SSL
34
35from myproxy.utils.openssl import OpenSSLConfig, OpenSSLConfigError
36
37
38class CaseSensitiveConfigParser(SafeConfigParser):
39    '''Subclass the SafeConfigParser - to preserve the original string case of
40    config section names
41    '''   
42    def optionxform(self, optionstr):
43        '''Extend SafeConfigParser.optionxform to preserve case of option names
44        '''
45        return optionstr
46
47
48#class _HostCheck(SSL.Checker.Checker):
49#    """Override SSL.Checker.Checker to allow additional check of MyProxy
50#    server identity.  If hostname doesn't match, allow match of host's 
51#    Distinguished Name against MYPROXY_SERVER_DN setting"""
52#
53#    def __init__(self,
54#                 myProxyServerDN=os.environ.get('MYPROXY_SERVER_DN'),
55#                 cnHostPfx='host/',
56#                 **kw):
57#        """Override parent class __init__ to enable setting of myProxyServerDN
58#        setting
59#       
60#        @type myProxyServerDN: string
61#        @param myProxyServerDN: Set the expected Distinguished Name of the
62#        MyProxy server to avoid errors matching hostnames.  This is useful
63#        where the hostname is not fully qualified
64#       
65#        @type cnHostPfx: string
66#        @param cnHostPfx: globus host certificates are
67#        generated by default with a 'host/' prefix to the host name.  Set
68#        this keyword to '' or None to override and omit the prefix"""
69#       
70#        SSL.Checker.Checker.__init__(self, **kw)
71#       
72#        self.myProxyServerDN = myProxyServerDN
73#        self.cnHostPfx = cnHostPfx
74#       
75#       
76#    def __call__(self, peerCert, host=None):
77#        """Carry out checks on server ID
78#        @type peerCert: basestring
79#        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
80#        instance
81#        @type host: basestring
82#        @param host: name of host to check
83#        """
84#       
85#        # Globus host certificate has a "host/" prefix - see explanation in
86#        # __init__.__doc__
87#        cnHostPfx = isinstance(self.cnHostPfx, basestring) \
88#                    and self.cnHostPfx or ''
89#        host = None or cnHostPfx + self.host
90#       
91#        try:
92#            SSL.Checker.Checker.__call__(self, peerCert, host=host)
93#           
94#        except SSL.Checker.WrongHost, e:
95#            # Try match against DN set from MYPROXY_SERVER_DN / config
96#            # file setting
97#            peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ','/')
98#           
99#            # If they match drop the exception and return all OK instead
100#            if peerCertDN != self.myProxyServerDN:
101#                raise
102#           
103#        return True
104   
105   
106class MyProxyClientError(Exception):
107    """Base exception class for MyProxyClient exceptions"""
108
109class MyProxyClientConfigError(MyProxyClientError):
110    """Error with configuration"""
111     
112class MyProxyClientGetError(MyProxyClientError):
113    """Exceptions arising from get request to server"""
114   
115class MyProxyClientRetrieveError(MyProxyClientError):
116    """Error recovering a response from MyProxy"""
117
118class MyProxyCredentialsAlreadyExist(MyProxyClientError):
119    """Attempting to upload credentials to the server which already exist.  -
120    See MyProxyClient.store
121    """
122   
123class MyProxyClientGetTrustRootsError(MyProxyClientError):
124    """Error retrieving trust roots"""
125           
126       
127class MyProxyClient(object):
128    """MyProxy client interface
129   
130    Based on protocol definitions in:
131   
132    http://grid.ncsa.uiuc.edu/myproxy/protocol/
133   
134    @type GET_CMD: string
135    @cvar GET_CMD: get command string
136   
137    @type INFO_CMD: string
138    @cvar INFO_CMD: info command string
139   
140    @type DESTROY_CMD: string
141    @cvar DESTROY_CMD: destroy command string
142   
143    @type CHANGE_PASSPHRASE_CMD: string
144    @cvar CHANGE_PASSPHRASE_CMD: command string to change cred pass-phrase
145   
146    @type STORE_CMD: string
147    @cvar STORE_CMD: store command string
148   
149    @type GET_TRUST_ROOTS_CMD: string
150    @cvar GET_TRUST_ROOTS_CMD: get trust roots command string
151   
152    @type _hostCertSubDirPath: string
153    @cvar _hostCertSubDirPath: sub-directory path host certificate (as tuple)
154   
155    @type _hostKeySubDirPath: string
156    @cvar _hostKeySubDirPath: sub-directory path to host key (as tuple)
157   
158    @type PRIKEY_NBITS: int
159    @cvar PRIKEY_NBITS: default number of bits for private key generated
160   
161    @type MESSAGE_DIGEST_TYPE: string
162    @cvar MESSAGE_DIGEST_TYPE: message digest type is MD5
163   
164    @type SERVER_RESP_BLK_SIZE: int
165    @cvar SERVER_RESP_BLK_SIZE: block size for retrievals from server
166   
167    @type MAX_RECV_TRIES: int
168    @cvar MAX_RECV_TRIES: maximum number of retrievals of size
169    SERVER_RESP_BLK_SIZE before this client gives up
170   
171    @type DEF_PROXY_FILEPATH: string
172    @cvar DEF_PROXY_FILEPATH: default location for proxy file to be written to
173   
174    @type PROXY_FILE_PERMISSIONS: int
175    @cvar PROXY_FILE_PERMISSIONS: file permissions returned proxy file is
176    created with
177   
178    @type propertyDefaults: tuple
179    @cvar propertyDefaults: sets permissable element names for MyProxy config
180    file
181    """
182     
183    GET_CMD="""VERSION=MYPROXYv2
184COMMAND=0
185USERNAME=%s
186PASSPHRASE=%s
187LIFETIME=%d"""
188 
189    INFO_CMD="""VERSION=MYPROXYv2
190COMMAND=2
191USERNAME=%s
192PASSPHRASE=PASSPHRASE
193LIFETIME=0"""
194 
195    DESTROY_CMD="""VERSION=MYPROXYv2
196COMMAND=3
197USERNAME=%s
198PASSPHRASE=PASSPHRASE
199LIFETIME=0"""
200
201    CHANGE_PASSPHRASE_CMD="""VERSION=MYPROXYv2
202 COMMAND=4
203 USERNAME=%s
204 PASSPHRASE=%s
205 NEW_PHRASE=%s
206 LIFETIME=0"""
207   
208    STORE_CMD="""VERSION=MYPROXYv2
209COMMAND=5
210USERNAME=%s
211PASSPHRASE=
212LIFETIME=%d"""
213
214    GET_TRUST_ROOTS_CMD="""VERSION=MYPROXYv2
215COMMAND=7
216USERNAME=%s
217PASSPHRASE=%s
218LIFETIME=0
219TRUSTED_CERTS=1"""
220
221    _hostCertSubDirPath = ('etc', 'hostcert.pem')
222    _hostKeySubDirPath = ('etc', 'hostkey.pem')
223   
224    # Work out default location of proxy file if it exists.  This is set if a
225    # call has been made previously to logon / get-delegation
226    DEF_PROXY_FILEPATH = sys.platform == ('win32' and 'proxy' or 
227                                    sys.platform in ('linux2', 'darwin') and 
228                                    '/tmp/x509up_u%s' % (os.getuid()) 
229                                    or None) 
230   
231    PRIKEY_NBITS = 2048 #4096
232    MESSAGE_DIGEST_TYPE = "md5"
233    SERVER_RESP_BLK_SIZE = 8192
234    MAX_RECV_TRIES = 1024
235   
236    # valid configuration property keywords
237    propertyDefaults = {
238       'hostname':              'localhost',
239       'port':                  7512,
240       'serverDN':              '',
241       'serverCNPrefix':        'host/',
242       'openSSLConfFilePath':   '',
243       'proxyCertMaxLifetime':  43200,
244       'proxyCertLifetime':     43200,
245       'caCertFilePath':        None,
246       'caCertDir':             None
247    }
248   
249    # Restrict attributes to the above properties, their equivalent
250    # protected values + extra OpenSSL config object.
251    __slots__ = propertyDefaults.copy()
252    __slots__.update(dict([('_'+k, v) for k,v in propertyDefaults.items()] +
253                          [('_openSSLConfig', None),
254                           ('_cfg',           None)]
255                          )
256                     )
257       
258    def __init__(self, cfgFilePath=None, **prop):
259        """Make any initial settings for client connections to MyProxy
260       
261        Settings are held in a dictionary which can be set from **prop,
262        a call to setProperties() or by passing settings in an XML file
263        given by cfgFilePath
264       
265        @param cfgFilePath:   set properties via a configuration file
266        @param **prop:         set properties via keywords - see
267        propertyDefaults class variable for a list of these
268        """
269       
270        # Default settings.  Nb. '_' - override property methods in order to
271        # set defaults
272        for opt, val in MyProxyClient.propertyDefaults.items():
273            setattr(self, '_'+opt, val)
274
275        # Configuration file used to get default subject when generating a
276        # new proxy certificate request
277        self._openSSLConfig = OpenSSLConfig()
278       
279        # Server host name - take from environment variable if available
280        self.hostname = os.environ.get('MYPROXY_SERVER',
281                                    MyProxyClient.propertyDefaults['hostname'])
282           
283        # ... and port number
284        self.port = int(os.environ.get('MYPROXY_SERVER_PORT', 
285                                       MyProxyClient.propertyDefaults['port']))
286
287        # Server Distinguished Name
288        self.serverDN = os.environ.get('MYPROXY_SERVER_DN',
289                                    MyProxyClient.propertyDefaults['serverDN'])
290       
291        # Environment variable may be quoted
292        if self.serverDN:
293            self.serverDN = self.serverDN.strip('"')
294           
295        # keyword settings
296        for opt, val in prop.items():
297            setattr(self, opt, val)
298       
299        # If properties file is set any parameters settings in file will
300        # override those set by input keyword
301        if cfgFilePath is not None:
302            self.parseConfig(cfg=cfgFilePath)
303
304
305    def parseConfig(self, cfg, section='DEFAULT'):
306        '''Extract parameters from _cfg config object'''
307       
308        if isinstance(cfg, basestring):
309            cfgFilePath = os.path.expandvars(cfg)
310            self._cfg = CaseSensitiveConfigParser()
311            self._cfg.read(cfgFilePath)
312        else:
313            cfgFilePath = None
314            self._cfg = cfg
315       
316        for key, val in self._cfg.items(section):
317            setattr(self, key, val)
318       
319    # Get/Set Property methods
320    def _getHostname(self):
321        return self._hostname
322   
323    def _setHostname(self, val):
324        if not isinstance(val, basestring):
325            raise AttributeError("Expecting string type for hostname "
326                                 "attribute")
327        self._hostname = val
328       
329    hostname = property(fget=_getHostname,
330                        fset=_setHostname,
331                        doc="hostname of MyProxy server")
332   
333    def _getPort(self):
334        return self._port
335   
336    def _setPort(self, val):
337        if isinstance(val, basestring):
338            self._port = int(val)
339        elif isinstance(val, int):
340            self._port = val
341        else:
342            raise AttributeError("Expecting int type for port attribute")
343   
344    port = property(fget=_getPort,
345                    fset=_setPort,
346                    doc="Port number for MyProxy server")
347   
348    def _getServerDN(self):
349        return self._serverDN
350   
351    def _setServerDN(self, val):
352        if not isinstance(val, basestring):
353            raise AttributeError("Expecting string type for serverDN "
354                                 "attribute")
355        self._serverDN = val
356   
357    serverDN = property(fget=_getServerDN,
358                        fset=_setServerDN,
359                        doc="Distinguished Name for MyProxy Server "
360                            "Certificate")
361   
362    def _getServerCNPrefix(self):
363        return self._serverCNPrefix
364   
365    def _setServerCNPrefix(self, val):
366        if not isinstance(val, basestring):
367            raise AttributeError("Expecting string type for serverCNPrefix "
368                                 "attribute")
369        self._serverCNPrefix = val
370   
371    serverCNPrefix = property(fget=_getServerCNPrefix,
372                              fset=_setServerCNPrefix,
373                              doc="Prefix if any for Server Certificate DN "
374                                  "Common Name e.g. 'host/'")
375   
376    def _getOpenSSLConfFilePath(self):
377        return self._openSSLConfFilePath
378   
379    def _setOpenSSLConfFilePath(self, val):
380        if not isinstance(val, basestring):
381            raise AttributeError("Expecting string type for "
382                                 "openSSLConfFilePath attribute")
383        self._openSSLConfFilePath = os.path.expandvars(val)
384        self._openSSLConfig.filePath = self._openSSLConfFilePath
385        self._openSSLConfig.read() 
386   
387    openSSLConfFilePath = property(fget=_getOpenSSLConfFilePath,
388                                   fset=_setOpenSSLConfFilePath,
389                                   doc="file path for OpenSSL config file")
390   
391    def _getProxyCertMaxLifetime(self):
392        return self._proxyCertMaxLifetime
393   
394    def _setProxyCertMaxLifetime(self, val):
395        if isinstance(val, basestring):
396            self._proxyCertMaxLifetime = int(val)
397           
398        elif isinstance(val, int):
399            self._proxyCertMaxLifetime = val
400        else:
401            raise AttributeError("Expecting int type for proxyCertMaxLifetime "
402                                 "attribute")
403   
404    proxyCertMaxLifetime = property(fget=_getProxyCertMaxLifetime,
405                                    fset=_setProxyCertMaxLifetime,
406                                    doc="Default max. lifetime allowed for "
407                                        "Proxy Certificate retrieved - used "
408                                        "by store method")
409   
410    def _getProxyCertLifetime(self):
411        return self._proxyCertLifetime
412   
413    def _setProxyCertLifetime(self, val):
414        if isinstance(val, basestring):
415            self._proxyCertLifetime = int(val)
416        elif isinstance(val, int):
417            self._proxyCertLifetime = val
418        else:
419            raise AttributeError("Expecting int type for proxyCertLifetime "
420                                 "attribute")
421   
422    proxyCertLifetime = property(fget=_getProxyCertLifetime,
423                                 fset=_setProxyCertLifetime,
424                                 doc="Default proxy cert. lifetime used in "
425                                     "logon request")
426   
427    def _getCACertFilePath(self):
428        return self._caCertFilePath
429   
430    def _setCACertFilePath(self, val):
431        '''@type val: basestring
432        @param val: file path for CA certificate to be used to verify
433        MyProxy server certificate'''
434       
435        if isinstance(val, basestring):
436            if val == '':
437                self._caCertFilePath = None
438            else:
439                self._caCertFilePath = os.path.expandvars(val)
440               
441        elif isinstance(val, None):
442            raise AttributeError("Expecting string type for caCertFilePath "
443                                 "attribute")       
444       
445    caCertFilePath = property(fget=_getCACertFilePath,
446                              fset=_setCACertFilePath,
447                              doc="CA certificate file path - MyProxy server "
448                                  "certificate must validate against it and/"
449                                  "or any present in caCertDir")
450
451    def _getCACertDir(self):
452        return self._caCertDir
453
454    def _setCACertDir(self, val):
455        '''Specify a directory containing PEM encoded CA certs. used for
456        validation of MyProxy server certificate.
457       
458        Set to None to make M2Crypto.SSL.Context.load_verify_locations ignore
459        this parameter
460       
461        @type val: basestring/None
462        @param val: directory path'''
463       
464        if isinstance(val, basestring):
465            if val == '':
466                self._caCertDir = None
467            else:
468                self._caCertDir = os.path.expandvars(val)
469               
470        elif isinstance(val, None):
471            self._caCertDir = val   
472        else:
473            raise AttributeError("Expecting string or None type for caCertDir "
474                                 "attribute")
475       
476    caCertDir = property(fget=_getCACertDir,
477                         fset=_setCACertDir,
478                         doc="directory containing PEM encoded CA "
479                             "certificates.  Use along with caCertFilePath "
480                             "setting to validate MyProxy server certificate")
481
482
483    def _getOpenSSLConfig(self):
484        "Get OpenSSLConfig object property method"
485        return self._openSSLConfig
486   
487    openSSLConfig = property(fget=_getOpenSSLConfig,
488                             doc="OpenSSLConfig object")
489
490         
491    def _initConnection(self, 
492                        ownerCertFile=None, 
493                        ownerKeyFile=None,
494                        ownerPassphrase=None):
495        """Initialise connection setting up SSL context and client and
496        server side identity checks
497       
498        @type ownerCertFile: basestring
499        @param ownerCertFile: client certificate and owner of credential
500        to be acted on.  Can be a proxy cert + proxy's signing cert.  Cert
501        and private key are not necessary for getDelegation / logon calls
502        @type ownerKeyFile: basestring
503        @param ownerKeyFile: client private key file
504        @type ownerPassphrase: basestring
505        @param ownerPassphrase: pass-phrase protecting private key if set -
506        not needed in the case of a proxy private key
507        """
508        # Must be version 3 for MyProxy
509        context = SSL.Context(SSL.SSLv3_METHOD)
510       
511        if self.caCertFilePath or self.caCertDir:
512            context.load_verify_locations(self.caCertFilePath, self.caCertDir)
513           
514        if ownerCertFile:
515            try:
516#                context.use_certificate_file(ownerCertFile)
517                context.use_certificate_chain_file(ownerCertFile)
518                def pwdCallback(passphraseMaxLen, 
519                                promptPassphraseTwice,
520                                passphrase):
521                    if passphrase is None:
522                        return True
523                    else:
524                        if len(passphrase) > passphraseMaxLen:
525                            log.error('Passphrase length %d is greater than '
526                                      'the maximum length allowed %d',
527                                      len(passphrase), passphraseMaxLen)
528                            return ''
529                        return passphrase
530                   
531                context.set_passwd_cb(pwdCallback, ownerPassphrase)
532                context.use_privatekey_file(ownerKeyFile)
533            except Exception:
534                raise MyProxyClientConfigError("Loading certificate "
535                                               "and private key for SSL "
536                                               "connection [also check CA "
537                                               "certificate settings]: %s" % 
538                                               traceback.format_exc()) 
539           
540        # Verify peer's certificate
541        def callback(connection, x509Cert, *arg, **kw):
542            return True
543       
544        context.set_verify(SSL.VERIFY_PEER, callback) 
545       
546           
547        # Disable for compatibility with myproxy server (er, globus)
548        # globus doesn't handle this case, apparently, and instead
549        # chokes in proxy delegation code
550        context.set_options(SSL.OP_DONT_INSERT_EMPTY_FRAGMENTS)
551       
552        # connect to myproxy server
553        conn = SSL.Connection(context, socket.socket())
554       
555        # Check server host identity - if host doesn't match use explicit
556        # 'serverDN'
557        # host/<hostname> one
558#        hostCheck = _HostCheck(host=self.hostname,
559#                               myProxyServerDN=self.serverDN,
560#                               cnHostPfx=self.serverCNPrefix)
561#        conn.set_post_connection_check_callback(hostCheck)
562       
563        return conn
564       
565    def _createKeyPair(self, nBitsForKey=PRIKEY_NBITS):
566        """Generate key pair and return as PEM encoded string
567        @type nBitsForKey: int
568        @param nBitsForKey: number of bits for private key generation -
569        default is 2048
570        @rtype: string
571        @return: public/private key pair
572        """
573        keyPair = crypto.PKey()
574        keyPair.generate_key(crypto.TYPE_RSA, nBitsForKey)
575       
576        return keyPair
577           
578    def _createCertReq(self, CN, keyPair, messageDigest=MESSAGE_DIGEST_TYPE):
579        """Create a certificate request.
580       
581        @type CN: basestring
582        @param CN: Common Name for certificate - effectively the same as the
583        username for the MyProxy credential
584        @type keyPair: string/None
585        @param keyPair: public/private key pair
586        @type messageDigest: basestring
587        @param messageDigest: message digest type - default is MD5
588        @rtype: tuple
589        @return certificate request PEM text and private key PEM text
590        """
591       
592        # Check all required certifcate request DN parameters are set               
593        # Create certificate request
594        certReq = crypto.X509Req()
595       
596        # Create public key object
597        certReq.set_pubkey(keyPair)
598       
599        # Add the public key to the request
600        certReq.sign(keyPair, messageDigest)
601       
602        derCertReq = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, 
603                                                     certReq)
604
605        return derCertReq
606   
607    def _deserializeResponse(self, msg, *fieldNames):
608        """
609        Deserialize a MyProxy server response
610       
611        @param msg: string response message from MyProxy server
612        @return: tuple of integer response and errorTxt string (if any) and all
613        the fields parsed.  fields is a list of two element, field name, field
614        value tuples.
615        @rtype: tuple
616        """ 
617        lines = msg.split('\n')
618        fields = [tuple(line.split('=', 1)) for line in lines][:-1]
619       
620        # get response value
621        respCode = [int(v) for k, v in fields if k == 'RESPONSE'][0]
622
623        # get error text
624        errorTxt = os.linesep.join([v for k, v in fields if k == 'ERROR'])
625       
626        # Check for custom fields requested by caller to this method
627        if fieldNames:
628            fieldsDict = {}
629            for k, v in fields:
630                names = [name for name in fieldNames if k.startswith(name)]
631                if len(names) == 0:
632                    continue
633                else:
634                    if v.isdigit():
635                        fieldsDict[k] = int(v)
636                    else:
637                        fieldsDict[k] = v
638             
639            # Return additional dict item in tuple 
640            return respCode, errorTxt, fieldsDict
641        else:
642            return respCode, errorTxt   
643 
644    def _deserializeCerts(self, inputDat):
645        """Unpack certificates returned from a get delegation call to the
646        server
647       
648        @param inputDat: string containing the proxy cert and private key
649        and signing cert all in DER format
650       
651        @return list containing the equivalent to the input in PEM format"""
652        pemCerts = []
653        dat = inputDat
654       
655        while dat:
656            # find start of cert, get length       
657            ind = dat.find('\x30\x82')
658            if ind < 0:
659                break
660               
661            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
662   
663            # extract der-format cert, and convert to pem
664            derCert = dat[ind:ind+len+4]
665            x509Cert = crypto.load_certificate(crypto.FILETYPE_ASN1, derCert)
666            pemCert = crypto.dump_certificate(crypto.FILETYPE_PEM, x509Cert)       
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 = open(certFile).read()
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._createKeyPair(nBitsForKey=nBitsForKey)
1054               
1055            certReq = self._createCertReq(username, keyPair)
1056
1057        if keyPair is not None: 
1058            pemKeyPair = crypto.dump_privatekey(crypto.FILETYPE_PEM, keyPair)
1059       
1060        # Set-up SSL connection
1061        conn = self._initConnection()
1062        conn.connect((self.hostname, self.port))
1063       
1064        # send globus compatibility stuff
1065        conn.write('0')
1066   
1067        # send get command - ensure conversion from unicode before writing
1068        cmd = MyProxyClient.GET_CMD % (username, passphrase, lifetime)
1069        conn.write(str(cmd))
1070   
1071        # process server response
1072        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1073        respCode, errorTxt = self._deserializeResponse(dat)
1074        if respCode:
1075            raise MyProxyClientGetError(errorTxt)
1076       
1077        # Send certificate request
1078        conn.send(certReq)
1079   
1080        # process certificates
1081        # - 1st byte , number of certs
1082        dat = conn.recv(1)
1083        nCerts = ord(dat[0])
1084       
1085        # - n certs
1086        dat = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1087   
1088        # process server response
1089        resp = conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1090        respCode, errorTxt = self._deserializeResponse(resp)
1091        if respCode:
1092            raise MyProxyClientRetrieveError(errorTxt)
1093   
1094        # deserialize certs from received cert data
1095        pemCerts = self._deserializeCerts(dat)
1096        if len(pemCerts) != nCerts:
1097            MyProxyClientRetrieveError("%d certs expected, %d received" % 
1098                                       (nCerts, len(pemCerts)))
1099   
1100        if keyPair is not None:
1101            # Return certs and private key
1102            # - proxy or dynamically issued certificate (MyProxy CA mode)
1103            # - private key
1104            # - rest of cert chain if proxy cert issued
1105            creds = [pemCerts[0], pemKeyPair]
1106            creds.extend(pemCerts[1:])
1107        else:
1108            # Key generated externally - return certificate chain only
1109            creds = pemCerts
1110
1111       
1112        return tuple(creds)
1113
1114    def getDelegation(self, *arg, **kw):
1115        """Retrieve proxy cert for user - same as logon"""
1116        return self.logon(*arg, **kw)
1117   
1118    def getTrustRoots(self, username='', passphrase=''):
1119        """Get trust roots for the given MyProxy server
1120       
1121        @type username: basestring
1122        @param username: username (optional)
1123       
1124        @type passphrase: basestring
1125        @param passphrase: pass-phrase (optional)
1126        server
1127       
1128        @return: trust root files as a dictionary keyed by file name with each
1129        item value set to the file contents
1130        @rtype: dict
1131        """
1132        # Set-up SSL connection
1133        conn = self._initConnection()
1134        conn.connect((self.hostname, self.port))
1135       
1136        # send globus compatibility stuff
1137        conn.write('0')
1138   
1139        # send get command - ensure conversion from unicode before writing
1140        cmd = MyProxyClient.GET_TRUST_ROOTS_CMD % (username, passphrase)
1141        conn.write(str(cmd))
1142   
1143        # process server response chunks until all consumed
1144        dat = ''
1145        try:
1146            for tries in range(MyProxyClient.MAX_RECV_TRIES):
1147                dat += conn.recv(MyProxyClient.SERVER_RESP_BLK_SIZE)
1148        except SSL.SysCallError:
1149            # Expect this exception when response content exhausted
1150            pass
1151       
1152        # Precaution
1153        if tries == MyProxyClient.MAX_RECV_TRIES:
1154            log.warning('Maximum %d tries reached for getTrustRoots response '
1155                        'block retrieval with block size %d', 
1156                        MyProxyClient.MAX_RECV_TRIES,
1157                        MyProxyClient.SERVER_RESP_BLK_SIZE)
1158           
1159        respCode, errorTxt, fileData = self._deserializeResponse(dat, 
1160                                                                'TRUSTED_CERTS',
1161                                                                'FILEDATA_')
1162        if respCode:
1163            raise MyProxyClientGetTrustRootsError(errorTxt)
1164       
1165        filesDict = dict([(k, base64.b64decode(v)) 
1166                          for k, v in fileData.items() if k != 'TRUSTED_CERTS'])
1167       
1168        return filesDict
1169       
Note: See TracBrowser for help on using the repository browser.