source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/myproxy.py @ 4384

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/myproxy.py@4384
Revision 4384, 47.7 KB checked in by pjkersha, 12 years ago (diff)

SessionMgr? -> SessionManager?

Line 
1"""MyProxy Client interface
2
3NERC Data Grid Project
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) 2007 STFC & NERC"
13__license__ = \
14"""This software may be distributed under the terms of the Q Public
15License, version 1.0 or later.
16
17For myproxy_logon see Access Grid Toolkit Public License (AGTPL):
18
19http://www-unix.mcs.anl.gov/fl/research/accessgrid/about/license.html
20
21This product includes software developed by and/or derived from the Access
22Grid Project (http://www.accessgrid.org) to which the U.S. Government retains
23certain rights."""
24__contact__ = "P.J.Kershaw@rl.ac.uk"
25__revision__ = '$Id: MyProxy.py 3196 2008-01-10 14:45:59Z pjkersha $'
26
27import sys, os
28import socket
29from M2Crypto import X509, RSA, EVP, m2, BIO, SSL
30
31import base64
32
33# For parsing of properties file
34try: # python 2.5
35    from xml.etree import cElementTree as ElementTree
36except ImportError:
37    # if you've installed it yourself it comes this way
38    import cElementTree as ElementTree
39
40from ndg.security.common.openssl import OpenSSLConfig, OpenSSLConfigError
41
42class MyProxyClientError(Exception):
43    """Catch all exception class"""
44   
45class GetError(Exception):
46    """Exceptions arising from get request to server"""
47   
48class RetrieveError(Exception):
49    """Error recovering a response from MyProxy"""
50
51class _HostCheck(SSL.Checker.Checker):
52    """Override SSL.Checker.Checker to allow additional check of MyProxy
53    server identity.  If hostname doesn't match, allow match of host's 
54    Distinguished Name against MYPROXY_SERVER_DN setting"""
55
56    def __init__(self, 
57                 myProxyServerDN=os.environ.get('MYPROXY_SERVER_DN'),
58                 cnHostPfx='host/',
59                 **kw):
60        """Override parent class __init__ to enable setting of myProxyServerDN
61        setting
62       
63        @type myProxyServerDN: string
64        @param myProxyServerDN: Set the expected Distinguished Name of the
65        MyProxy server to avoid errors matching hostnames.  This is useful
66        where the hostname is not fully qualified
67       
68        @type cnHostPfx: string
69        @param cnHostPfx: globus host certificates are
70        generated by default with a 'host/' prefix to the host name.  Set
71        this keyword to '' or None to override and omit the prefix"""
72       
73        SSL.Checker.Checker.__init__(self, **kw)
74       
75        self.myProxyServerDN = myProxyServerDN
76        self.cnHostPfx = cnHostPfx
77       
78       
79    def __call__(self, peerCert, host=None):
80        """Carry out checks on server ID
81        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
82        instance
83        @param host: name of host to check
84        """
85       
86        # Globus host certificate has a "host/" prefix - see explanation in
87        # __init__.__doc__
88        cnHostPfx = isinstance(self.cnHostPfx, basestring) \
89                    and self.cnHostPfx or ''
90        host = None or cnHostPfx + self.host
91       
92        try:
93            SSL.Checker.Checker.__call__(self, peerCert, host=host)
94           
95        except SSL.Checker.WrongHost, e:
96            # Try match against DN set from MYPROXY_SERVER_DN / config
97            # file setting
98            peerCertDN = '/' + \
99                    peerCert.get_subject().as_text().replace(', ', '/')
100            if peerCertDN != self.myProxyServerDN:
101                # They match - drop the exception and return all OK instead
102                raise e
103           
104        return True
105           
106       
107class MyProxyClient(object):
108    """MyProxy client interface
109   
110    Based on protocol definitions in:
111   
112    http://grid.ncsa.uiuc.edu/myproxy/protocol/
113   
114    @type __getCmd: string
115    @cvar __getCmd: get command string
116   
117    @type __infoCmd: string
118    @cvar __infoCmd: info command string
119   
120    @type __destroyCmd: string
121    @cvar __destroyCmd: destroy command string
122   
123    @type __changePassphrase: string
124    @cvar __changePassphrase: command string to change cred pass-phrase
125   
126    @type __storeCmd: string
127    @cvar __storeCmd: store command string
128   
129    @type _hostCertSubDirPath: string
130    @cvar _hostCertSubDirPath: sub-directory path host certificate (as tuple)
131   
132    @type _hostKeySubDirPath: string
133    @cvar _hostKeySubDirPath: sub-directory path to host key (as tuple)
134   
135    @type __validKeys: tuple
136    @cvar __validKeys: sets permissable element names for MyProxy XML config
137    file
138    """
139     
140    __getCmd="""VERSION=MYPROXYv2
141COMMAND=0
142USERNAME=%s
143PASSPHRASE=%s
144LIFETIME=%d"""
145 
146    __infoCmd="""VERSION=MYPROXYv2
147COMMAND=2
148USERNAME=%s
149PASSPHRASE=PASSPHRASE
150LIFETIME=0"""
151 
152    __destroyCmd="""VERSION=MYPROXYv2
153COMMAND=3
154USERNAME=%s
155PASSPHRASE=PASSPHRASE
156LIFETIME=0"""
157
158    __changePassphraseCmd="""VERSION=MYPROXYv2
159 COMMAND=4
160 USERNAME=%s
161 PASSPHRASE=%s
162 NEW_PHRASE=%s
163 LIFETIME=0"""
164   
165    __storeCmd="""VERSION=MYPROXYv2
166COMMAND=5
167USERNAME=%s
168PASSPHRASE=
169LIFETIME=%d"""
170
171    _hostCertSubDirPath = ('etc', 'hostcert.pem')
172    _hostKeySubDirPath = ('etc', 'hostkey.pem')
173   
174    # valid configuration property keywords
175    __validKeys = ('hostname',
176                   'port',
177                   'serverDN',
178                   'serverCNprefix',
179                   'gridSecurityDir',
180                   'openSSLConfFilePath',
181                   'tmpDir',
182                   'proxyCertMaxLifetime',
183                   'proxyCertLifetime',
184                   'caCertFile')
185
186    # Work out default location of proxy file if it exists.  This is set if a
187    # call has been made previously to logon / get-delegation
188    defProxyFile = sys.platform == 'win32' and 'proxy' or \
189    sys.platform in ('linux2', 'darwin') and '/tmp/x509up_u%s'%(os.getuid()) \
190    or None
191
192    @classmethod
193    def writeProxyFile(cls, proxyCert, proxyPriKey, userCert, filePath=None):
194        """Write out proxy cert to file in the same way as myproxy-logon -
195        proxy cert, private key, user cert.  Nb. output from logon can be
196        passed direct into this method
197       
198        @type proxyCert: string
199        @param proxyCert: proxy certificate
200        @type proxyPriKey: string
201        @param proxyPriKey: private key for proxy
202        @type userCert: string
203        @param userCert: user certificate which issued the proxy
204        @type filePath: string
205        @param filePath: set to override the default filePath"""
206       
207        if filePath is None:
208            filePath = MyProxyClient.defProxyFile
209           
210        if filePath is None:
211            MyProxyClientError, \
212                "Error setting proxy file path - invalid platform?"
213       
214        outStr = proxyCert + proxyPriKey + userCert       
215        open(MyProxyClient.defProxyFile, 'w').write(outStr)
216        try:
217            # Make sure permssions are set correctly
218            os.chmod(MyProxyClient.defProxyFile, 0600)
219        except Exception:
220            # Don't leave the file lying around if couldn't change it's
221            # permissions
222            os.unlink(MyProxyClient.defProxyFile)
223           
224            raise MyProxyClientError, \
225                'Unable to set 0600 permissions for proxy file "%s": %s' % \
226                (MyProxyClient.defProxyFile, e)
227
228    @classmethod
229    def readProxyFile(cls, filePath=None):
230        """Read proxy cert file following the format used by myproxy-logon -
231        proxy, cert, private key, user cert.
232       
233        @rtype: tuple
234        @return: tuple containing proxy cert, private key, user cert"""
235        if filePath is None:
236            filePath = MyProxyClient.defProxyFile
237               
238        proxy = open(MyProxyClient.defProxyFile).read()
239       
240        # Split certs and key into separate tuple items
241        return tuple(['-----BEGIN'+i for i in proxy.split('-----BEGIN')[1:]])
242       
243       
244    def __init__(self, propFilePath=None, **prop):
245        """Make any initial settings for client connections to MyProxy
246       
247        Settings are held in a dictionary which can be set from **prop,
248        a call to setProperties() or by passing settings in an XML file
249        given by propFilePath
250       
251        @param propFilePath:   set properties via a configuration file
252        @param **prop:         set properties via keywords - see __validKeys
253        class variable for a list of these
254        """
255       
256        # Check for parameter names set from input
257        #self.certReqDNparam = None
258
259        # settings dictionary
260        self.__prop = {}
261       
262        # Server host name - take from environment variable if available
263        if 'MYPROXY_SERVER' in os.environ:
264            self.__prop['hostname'] = os.environ['MYPROXY_SERVER']
265           
266        # ... and port number
267        if 'MYPROXY_SERVER_PORT' in os.environ:
268            self.__prop['port'] = int(os.environ['MYPROXY_SERVER_PORT'])
269        else:
270            # Usual default is ...
271            self.__prop['port'] = 7512
272           
273        self.__prop['proxyCertLifetime'] = 43200
274        self.__prop['proxyCertMaxLifetime'] = 43200
275       
276        # Configuration file used to get default subject when generating a
277        # new proxy certificate request
278        self.__openSSLConf = OpenSSLConfig()
279
280        # Properties set via input keywords
281        self.setProperties(**prop)
282
283        # If properties file is set any parameters settings in file will
284        # override those set by input keyword
285        if propFilePath is not None:
286            self.readProperties(propFilePath)
287
288
289    #_________________________________________________________________________
290    def setProperties(self, **prop):
291        """Update existing properties from an input dictionary
292        Check input keys are valid names"""
293       
294        invalidKeys = [key for key in prop if key not in self.__validKeys]
295        if invalidKeys:
296            raise MyProxyClientError, 'Invalid property name(s) set: "%s"' % \
297                                    '", "'.join(invalidKeys)
298               
299        self.__prop.update(prop)
300
301        # Update openssl conf file path
302        #
303        # Check 'prop' to see if they've been in THIS update
304        # Check 'self.__prop' to ensure both are present in
305        # order to construct a file path
306        if 'openSSLConfFilePath' in prop:           
307            self.__openSSLConf.filePath = self.__prop['openSSLConfFilePath']
308            self.__openSSLConf.read()
309           
310
311    #_________________________________________________________________________
312    def readProperties(self, propFilePath=None, propElem=None):
313        """Read XML properties from a file or cElementTree node
314       
315        propFilePath|propertiesElem
316
317        @type propFilePath: string
318        @param propFilePath: set to read from the specified file
319       
320        @type propElem: ElementTree node
321        @param propElem: set to read beginning from a cElementTree node
322        """
323
324        if propFilePath is not None:
325            try:
326                tree = ElementTree.parse(propFilePath)
327                propElem = tree.getroot()
328               
329            except IOError, e:
330                raise MyProxyClientError, \
331                                "Error parsing properties file \"%s\": %s" % \
332                                (e.filename, e.strerror)           
333            except Exception, e:
334                raise MyProxyClientError, \
335                                "Error parsing properties file: %s" % str(e)
336                               
337        if propElem is None:
338            raise MyProxyClientError, \
339                    "Root element for parsing properties file is not defined"
340
341
342        # Get properties as a data dictionary
343        prop = {}
344        try:
345            for elem in propElem:
346                # Check for string type to avoid exceptions from isdigit and
347                # expandvars
348                if isinstance(elem.text, basestring):
349                    if elem.text.isdigit():
350                        prop[elem.tag] = int(elem.text)
351                    else:
352                        prop[elem.tag] = os.path.expandvars(elem.text)
353                else:
354                    prop[elem.tag] = elem.text
355                   
356        except Exception, e:
357            raise SessionManagerError, \
358                "Error parsing tag \"%s\" in properties file" % elem.tag
359       
360        self.setProperties(**prop)
361
362
363    #_________________________________________________________________________
364    def __getOpenSSLConfig(self):
365        "Get OpenSSLConfig object property method"
366        return self.__openSSLConfig
367   
368    openSSLConfig = property(fget=__getOpenSSLConfig,
369                             doc="OpenSSLConfig object")
370
371           
372    #_________________________________________________________________________       
373    def _initConnection(self, 
374                        ownerCertFile=None, 
375                        ownerKeyFile=None,
376                        ownerPassphrase=None):
377        """Initialise connection setting up SSL context and client and
378        server side identity checks
379       
380        @param ownerCertFile: client certificate and owner of credential
381        to be acted on.  Can be a proxy cert + proxy's signing cert.  Cert
382        and private key are not necessary for getDelegation / logon calls
383        @param ownerKeyFile: client private key file
384        @param ownerPassphrase: pass-phrase protecting private key if set -
385        not needed in the case of a proxy private key
386        """
387
388        # Must be version 3 for MyProxy
389        context = SSL.Context(protocol='sslv3')
390
391        if 'caCertFile' in self.__prop:
392            context.load_verify_locations(cafile=self.__prop['caCertFile'])
393                           
394            # Stop if peer's certificate can't be verified
395            context.set_allow_unknown_ca(False)
396        else:
397            context.set_allow_unknown_ca(True)
398           
399        if ownerCertFile:
400            try:
401                context.load_cert_chain(ownerCertFile,
402                                keyfile=ownerKeyFile,
403                                callback=lambda *ar, **kw: ownerPassphrase)
404            except Exception, e:
405                raise MyProxyClientError, \
406            "Error loading CA cert., cert. and key for SSL connection: %s" % e
407           
408            # Verify peer's certificate
409            context.set_verify(SSL.verify_peer, 1) 
410       
411           
412        # Disable for compatibility with myproxy server (er, globus)
413        # globus doesn't handle this case, apparently, and instead
414        # chokes in proxy delegation code
415        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
416       
417        # connect to myproxy server
418        conn = SSL.Connection(context, sock=socket.socket())
419       
420        # Check server host identity - if host doesn't match use explicit
421        # 'serverDN'
422        # host/<hostname> one
423        hostCheck = _HostCheck(host=self.__prop['hostname'],
424                               myProxyServerDN=self.__prop.get('serverDN'),
425                               cnHostPfx=self.__prop.get('serverCNprefix'))
426        conn.set_post_connection_check_callback(hostCheck)
427       
428        return conn
429   
430           
431    #_________________________________________________________________________       
432    def _createCertReq(self, CN, nBitsForKey=1024, messageDigest="md5"):
433        """
434        Create a certificate request.
435       
436        @param CN: Common Name for certificate - effectively the same as the
437        username for the MyProxy credential
438        @param nBitsForKey: number of bits for private key generation -
439        default is 1024
440        @param messageDigest: message disgest type - default is MD5
441        @return tuple of certificate request PEM text and private key PEM text
442        """
443       
444        # Check all required certifcate request DN parameters are set               
445        # Create certificate request
446        req = X509.Request()
447   
448        # Generate keys
449        key = RSA.gen_key(nBitsForKey, m2.RSA_F4)
450   
451        # Create public key object
452        pubKey = EVP.PKey()
453        pubKey.assign_rsa(key)
454       
455        # Add the public key to the request
456        req.set_version(0)
457        req.set_pubkey(pubKey)
458       
459        defaultReqDN = self.__openSSLConf.reqDN
460             
461        # Set DN
462        x509Name = X509.X509_Name()
463        x509Name.CN = CN
464       
465        if defaultReqDN:
466            x509Name.OU = defaultReqDN['OU']
467            x509Name.O = defaultReqDN['O']
468                       
469        req.set_subject_name(x509Name)
470       
471        req.sign(pubKey, messageDigest)
472
473        return (req.as_der(), key.as_pem(cipher=None))
474   
475   
476    #_________________________________________________________________________           
477    def _deserializeResponse(self, msg, *fieldNames):
478        """
479        Deserialize a MyProxy server response
480       
481        @param msg: string response message from MyProxy server
482        @*fieldNames: the content of additional fields can be returned by
483        specifying the field name or names as additional arguments e.g. info
484        method passes 'CRED_START_TIME', 'CRED_END_TIME' and 'CRED_OWNER'
485        field names.  The content of fields is returned as an extra element
486        in the tuple response.  This element is itself a dictionary indexed
487        by field name.
488        @return tuple of integer response and errorTxt string (if any)
489        """
490       
491        lines = msg.split('\n')
492       
493        # get response value
494        responselines = filter(lambda x: x.startswith('RESPONSE'), lines)
495        responseline = responselines[0]
496        respCode = int(responseline.split('=')[1])
497       
498        # get error text
499        errorTxt = ""
500        errorlines = filter(lambda x: x.startswith('ERROR'), lines)
501        for e in errorlines:
502            etext = e.split('=', 1)[1]
503            errorTxt += os.linesep + etext
504       
505        if fieldNames:
506            fields = {}
507                       
508            for fieldName in fieldNames:
509                fieldlines = filter(lambda x: x.startswith(fieldName), lines)
510                try:
511                    # Nb. '1' arg to split ensures owner DN is not split up.
512                    field = fieldlines[0].split('=', 1)[1]
513                    fields[fieldName]=field.isdigit() and int(field) or field
514
515                except IndexError:
516                    # Ignore fields that aren't found
517                    pass
518               
519            return respCode, errorTxt, fields
520        else:
521            return respCode, errorTxt
522   
523 
524    #_________________________________________________________________________             
525    def _deserializeCerts(self, inputDat):
526        """Unpack certificates returned from a get delegation call to the
527        server
528       
529        @param inputDat: string containing the proxy cert and private key
530        and signing cert all in DER format
531       
532        @return list containing the equivalent to the input in PEM format"""
533        pemCerts = []       
534        dat = inputDat
535       
536        while dat:   
537            # find start of cert, get length       
538            ind = dat.find('\x30\x82')
539            if ind < 0:
540                break
541               
542            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
543   
544            # extract der-format cert, and convert to pem
545            derCert = dat[ind:ind+len+4]
546           
547            x509 = X509.load_cert_der_string(derCert)
548            pemCert = x509.as_pem()
549           
550            pemCerts.append(pemCert)
551   
552            # trim cert from data
553            dat = dat[ind + len + 4:]
554           
555        return pemCerts
556
557
558    #_________________________________________________________________________   
559    def info(self,
560             username, 
561             ownerCertFile=None,
562             ownerKeyFile=None,
563             ownerPassphrase=None):
564        """return True/False whether credentials exist on the server for a
565        given username
566       
567        Exceptions:  GetError, RetrieveError
568       
569        @type username: string
570        @param username: username selected for credential
571        @type ownerCertFile: string
572        @param ownerCertFile: certificate used for client authentication with
573        the MyProxy server SSL connection.  This ID will be set as the owner
574        of the stored credentials.  Only the owner can later remove
575        credentials with myproxy-destroy or the destroy method.  If not set,
576        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
577        @type ownerKeyFile: string
578        @param ownerKeyFile: corresponding private key file.  See explanation
579        for ownerCertFile
580        @type ownerPassphrase: string
581        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
582        private key is not password protected.
583        """
584        globusLoc = os.environ.get('GLOBUS_LOCATION')
585        if not ownerCertFile:
586            if globusLoc:
587                ownerCertFile = os.path.join(globusLoc, 
588                                         *MyProxyClient._hostCertSubDirPath)
589                ownerKeyFile = os.path.join(globusLoc, 
590                                         *MyProxyClient._hostKeySubDirPath)
591            else:
592                raise MyProxyClientError, \
593            "No client authentication cert. and private key file were given"
594
595        # Set-up SSL connection
596        conn = self._initConnection(ownerCertFile=ownerCertFile,
597                                    ownerKeyFile=ownerKeyFile,
598                                    ownerPassphrase=ownerPassphrase)
599       
600        conn.connect((self.__prop['hostname'], self.__prop['port']))
601       
602        # send globus compatibility stuff
603        conn.write('0')
604   
605        # send info command - ensure conversion from unicode before writing
606        cmd = MyProxyClient.__infoCmd % username
607        conn.write(str(cmd))
608   
609        # process server response
610        dat = conn.recv(8192)
611         
612        # Pass in the names of fields to return in the dictionary 'field'
613        respCode, errorTxt, field = self._deserializeResponse(dat, 
614                                                         'CRED_START_TIME', 
615                                                         'CRED_END_TIME', 
616                                                         'CRED_OWNER')
617
618        return not bool(respCode), errorTxt, field
619
620
621    #_________________________________________________________________________   
622    def changePassphrase(self,
623                         username, 
624                         passphrase,
625                         newPassphrase,
626                         ownerCertFile=None,
627                         ownerKeyFile=None,
628                         ownerPassphrase=None):
629        """change pass-phrase protecting the credentials for a given username
630       
631        Exceptions:  GetError, RetrieveError
632       
633        @param username: username of credential
634        @param passphrase: existing pass-phrase for credential
635        @param newPassphrase: new pass-phrase to replace the existing one.
636        @param ownerCertFile: certificate used for client authentication with
637        the MyProxy server SSL connection.  This ID will be set as the owner
638        of the stored credentials.  Only the owner can later remove
639        credentials with myproxy-destroy or the destroy method.  If not set,
640        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
641        @param ownerKeyFile: corresponding private key file.  See explanation
642        for ownerCertFile
643        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
644        private key is not password protected. 
645        @return none
646        """
647        globusLoc = os.environ.get('GLOBUS_LOCATION')
648        if not ownerCertFile or not ownerKeyFile:
649            if globusLoc:
650                ownerCertFile = os.path.join(globusLoc, 
651                                         *MyProxyClient._hostCertSubDirPath)
652                ownerKeyFile = os.path.join(globusLoc, 
653                                         *MyProxyClient._hostKeySubDirPath)
654            else:
655                raise MyProxyClientError, \
656            "No client authentication cert. and private key file were given"
657       
658        # Set-up SSL connection
659        conn = self._initConnection(ownerCertFile=ownerCertFile,
660                                    ownerKeyFile=ownerKeyFile,
661                                    ownerPassphrase=ownerPassphrase)
662
663        conn.connect((self.__prop['hostname'], self.__prop['port']))
664       
665        # send globus compatibility stuff
666        conn.write('0')
667   
668        # send command - ensure conversion from unicode before writing
669        cmd = MyProxyClient.__changePassphraseCmd % (username, 
670                                                     passphrase,
671                                                     newPassphrase)
672        conn.write(str(cmd))
673   
674        # process server response
675        dat = conn.recv(8192)
676           
677        respCode, errorTxt = self._deserializeResponse(dat)
678        if respCode:
679            raise GetError, errorTxt
680
681
682    #_________________________________________________________________________   
683    def destroy(self,
684                username, 
685                ownerCertFile=None,
686                ownerKeyFile=None,
687                ownerPassphrase=None):
688        """destroy credentials from the server for a given username
689       
690        Exceptions:  GetError, RetrieveError
691       
692        @param username: username selected for credential
693        @param ownerCertFile: certificate used for client authentication with
694        the MyProxy server SSL connection.  This ID will be set as the owner
695        of the stored credentials.  Only the owner can later remove
696        credentials with myproxy-destroy or the destroy method.  If not set,
697        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
698        @param ownerKeyFile: corresponding private key file.  See explanation
699        for ownerCertFile
700        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
701        private key is not password protected. 
702        @return none
703        """
704        globusLoc = os.environ.get('GLOBUS_LOCATION')
705        if not ownerCertFile or not ownerKeyFile:
706            if globusLoc:
707                ownerCertFile = os.path.join(globusLoc, 
708                                         *MyProxyClient._hostCertSubDirPath)
709                ownerKeyFile = os.path.join(globusLoc, 
710                                         *MyProxyClient._hostKeySubDirPath)
711            else:
712                raise MyProxyClientError, \
713            "No client authentication cert. and private key file were given"
714       
715        # Set-up SSL connection
716        conn = self._initConnection(ownerCertFile=ownerCertFile,
717                                    ownerKeyFile=ownerKeyFile,
718                                    ownerPassphrase=ownerPassphrase)
719
720        conn.connect((self.__prop['hostname'], self.__prop['port']))
721       
722        # send globus compatibility stuff
723        conn.write('0')
724   
725        # send destroy command - ensure conversion from unicode before writing
726        cmd = MyProxyClient.__destroyCmd % username
727        conn.write(str(cmd))
728   
729        # process server response
730        dat = conn.recv(8192)
731           
732        respCode, errorTxt = self._deserializeResponse(dat)
733        if respCode:
734            raise GetError, errorTxt
735
736
737    #_________________________________________________________________________   
738    def store(self,
739              username,
740              passphrase, 
741              certFile,
742              keyFile,
743              ownerCertFile=None,
744              ownerKeyFile=None,
745              ownerPassphrase=None,
746              lifetime=None,
747              force=True):
748        """Upload credentials to the server
749       
750        @raise GetError:
751        @raise RetrieveError:
752       
753        @type username: string
754        @param username: username selected for new credential
755        @type passphrase: string
756        @param passphrase: pass-phrase for new credential.  This is the pass
757        phrase which protects keyfile.
758        @type certFile: string
759        @param certFile: user's X.509 certificate in PEM format
760        @type keyFile: string
761        @param keyFile: equivalent private key file in PEM format
762        @type ownerCertFile: string
763        @param ownerCertFile: certificate used for client authentication with
764        the MyProxy server SSL connection.  This ID will be set as the owner
765        of the stored credentials.  Only the owner can later remove
766        credentials with myproxy-destroy or the destroy method.  If not set,
767        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem or if this
768        is not set, certFile
769        @type ownerKeyFile: string
770        @param ownerKeyFile: corresponding private key file.  See explanation
771        for ownerCertFile
772        @type ownerPassphrase: string
773        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
774        private key is not password protected.  Nb. keyFile is expected to
775        be passphrase protected as this will be the passphrase used for
776        logon / getDelegation.
777        @type Force: bool
778        @param force: set to True to overwrite any existing creds with the
779        same username.  If, force=False a check is made with a call to info.
780        If creds already, exist exit without proceeding
781        """
782       
783        lifetime = lifetime or self.__prop['proxyCertMaxLifetime']
784
785        # Inputs must be string type otherwise server will reject the request
786        if isinstance(username, unicode):
787            username = str(username)
788           
789        if isinstance(passphrase, unicode):
790            passphrase = str(passphrase)
791       
792        globusLoc = os.environ.get('GLOBUS_LOCATION')
793        if not ownerCertFile or not ownerKeyFile:
794            if globusLoc:
795                ownerCertFile = os.path.join(globusLoc, 
796                                         *MyProxyClient._hostCertSubDirPath)
797                ownerKeyFile = os.path.join(globusLoc, 
798                                         *MyProxyClient._hostKeySubDirPath)
799            else:
800                # Default so that the owner is the same as the ID of the
801                # credentials to be uploaded.
802                ownerCertFile = certFile
803                ownerKeyFile = keyFile
804                ownerPassphrase = passphrase
805               
806        if not force:
807            # Check credentials don't already exist
808            if self.info(username,
809                         ownerCertFile=ownerCertFile,
810                         ownerKeyFile=ownerKeyFile,
811                         ownerPassphrase=ownerPassphrase)[0]:
812                raise MyProxyClientError, \
813                        "Credentials already exist for user: %s" % username
814
815        # Set up SSL connection
816        conn = self._initConnection(ownerCertFile=ownerCertFile,
817                                    ownerKeyFile=ownerKeyFile,
818                                    ownerPassphrase=ownerPassphrase)
819       
820        conn.connect((self.__prop['hostname'], self.__prop['port']))
821       
822        # send globus compatibility stuff
823        conn.write('0')
824   
825        # send store command - ensure conversion from unicode before writing
826        cmd = MyProxyClient.__storeCmd % (username, lifetime)
827        conn.write(str(cmd))
828   
829        # process server response
830        dat = conn.recv(8192)
831           
832        respCode, errorTxt = self._deserializeResponse(dat)
833        if respCode:
834            raise GetError, errorTxt
835       
836        # Send certificate and private key
837        certTxt = X509.load_cert(certFile).as_pem()
838        keyTxt = open(keyFile).read()
839       
840        conn.send(certTxt + keyTxt)
841   
842   
843        # process server response
844        resp = conn.recv(8192)
845        respCode, errorTxt = self._deserializeResponse(resp)
846        if respCode:
847            raise RetrieveError, errorTxt
848       
849       
850    #_________________________________________________________________________           
851    def logon(self, username, passphrase, lifetime=None):
852        """Retrieve a proxy credential from a MyProxy server
853       
854        Exceptions:  GetError, RetrieveError
855       
856        @type username: basestring
857        @param username: username of credential
858       
859        @type passphrase: basestring
860        @param passphrase: pass-phrase for private key of credential held on
861        server
862       
863        @type lifetime: int
864        @param lifetime: lifetime for generated certificate
865       
866        @rtype: tuple
867        @return credentials as strings in PEM format: the
868        user certificate, it's private key and the issuing certificate.  The
869        issuing certificate is only set if the user certificate is a proxy
870        """
871       
872        lifetime = lifetime or self.__prop['proxyCertLifetime']
873
874        # Generate certificate request here - any errors will be thrown
875        # prior to making the connection and so not upsetting the server
876        #
877        # - The client will generate a public/private key pair and send a
878        #   NULL-terminated PKCS#10 certificate request to the server.
879        certReq, priKey = self._createCertReq(username)
880
881        # Set-up SSL connection
882        conn = self._initConnection()
883        conn.connect((self.__prop['hostname'], self.__prop['port']))
884       
885        # send globus compatibility stuff
886        conn.write('0')
887   
888        # send get command - ensure conversion from unicode before writing
889        cmd = MyProxyClient.__getCmd % (username, passphrase, lifetime)
890        conn.write(str(cmd))
891   
892        # process server response
893        dat = conn.recv(8192)
894        respCode, errorTxt = self._deserializeResponse(dat)
895        if respCode:
896            raise GetError, errorTxt
897       
898        # Send certificate request
899        conn.send(certReq)
900   
901        # process certificates
902        # - 1st byte , number of certs
903        dat = conn.recv(1)
904        nCerts = ord(dat[0])
905       
906        # - n certs
907        dat = conn.recv(8192)
908   
909        # process server response
910        resp = conn.recv(8192)
911        respCode, errorTxt = self._deserializeResponse(resp)
912        if respCode:
913            raise RetrieveError, errorTxt
914   
915        # deserialize certs from received cert data
916        pemCerts = self._deserializeCerts(dat)
917        if len(pemCerts) != nCerts:
918            RetrieveError, "%d certs expected, %d received" % \
919                                                    (nCerts, len(pemCerts))
920   
921        # Return certs and private key
922        # - proxy cert
923        # - private key
924        # - rest of cert chain
925        creds = [pemCerts[0], priKey]
926        creds.extend(pemCerts[1:])
927       
928        return tuple(creds)
929       
930
931    def getDelegation(self, *arg, **kw):
932        """Retrieve proxy cert for user - same as logon"""
933        return self.logon(*arg, **kw)
934
935
936#_____________________________________________________________________________   
937def main():
938    import traceback
939    import pdb;pdb.set_trace()
940    try:
941        CmdLineClient()           
942    except Exception, e:
943        print "Error: ", e
944        print traceback.print_exc()
945        sys.exit(1)
946
947#  help="check whether a credential exists")
948#
949#  help="destroy credential")
950#
951#  help="change pass-phrase protecting credential")
952
953import optparse
954import getpass
955
956class CmdLineClientError(Exception):
957    """Errors related to CmdLineClient class"""
958   
959class CmdLineClient(object):
960    """Command line interface to MyProxyClient class.  Where possible it
961    supports the same options as the Globus myproxy-* client commands"""
962   
963    run = {
964        'info': 'runInfo', 
965        'logon': 'runLogon',
966        'get-delegation': 'runGetDelegation',
967        'destroy': 'runDestroy', 
968        'change-pass': 'runChangePassphrase',
969        'store': 'runStore'
970    }
971
972    initOpts = {
973        'info': '_addInfoOpts', 
974        'logon': '_addLogonOpts',
975        'get-delegation': '_addGetDelegationOpts',
976        'destroy': '_addDestroyOpts', 
977        'change-pass': '_addChangePassphraseOpts',
978        'store': '_addStoreOpts'
979    }
980
981    cmdUsage = {
982        'info': "usage: %prog info arg1 arg2", 
983        'logon': "usage: %prog logon arg1 arg2",
984        'get-delegation': "usage: %prog get-delegation arg1 arg2",
985        'destroy': "usage: %prog destroy arg1 arg2", 
986        'change-pass': "usage: %prog change-pass arg1 arg2",
987        'store': "usage: %prog store arg1 arg2"
988    }
989   
990    # Keep '%prog' in a separate string otherwise it confuses the % operator
991    usage = "usage: %prog" + " [%s] arg1 arg2" % '|'.join(run.keys())   
992    version = "%prog 0.8.7"
993       
994    def __init__(self):
995        """Parse the command line and run the appropriate command"""
996       
997        self.parser = optparse.OptionParser(usage=self.__class__.usage,
998                                            version=self.__class__.version)
999
1000        # Get command - expected to be 1st arg
1001        if len(sys.argv) > 1:
1002            cmd = sys.argv[1]
1003            if cmd not in self.__class__.run:
1004                self.parser.error('"%s" command option not recognised' % cmd)
1005        else:
1006            self.parser.error('No command option set')
1007
1008        # Ammend usage string for specific command
1009        self.parser.set_usage(self.__class__.cmdUsage[cmd])
1010       
1011        # Add options based on the command set
1012        self._addGenericOpts()
1013        getattr(self, self.initOpts[cmd])()
1014       
1015        # Omit command options as parser won't understand it
1016        (self.opts, args) = self.parser.parse_args(args=sys.argv[2:])     
1017   
1018        # Process generic options
1019       
1020        # Not all commands require username option - for those that do, check
1021        # to see if it was set or omitted
1022        if hasattr(self.opts, 'username') and not self.opts.username:
1023            if sys.platform == 'win32':
1024                self.opts.username = os.environ["USERNAME"]
1025            else:
1026                import pwd
1027                self.opts.username = pwd.getpwuid(os.geteuid())[0]
1028   
1029        self.myProxy = MyProxyClient(hostname=self.opts.host,
1030                                     port=self.opts.port)#,
1031#                                     O='NDG',
1032#                                     OU='BADC')
1033
1034        # Run the command
1035        getattr(self, self.run[cmd])()
1036
1037               
1038    def _addGenericOpts(self):
1039        """Generic options applying to all commands"""
1040        self.parser.add_option("-s", 
1041                          "--pshost", 
1042                          dest="host",
1043                          default=os.environ.get('MYPROXY_SERVER'),
1044                          help="The hostname of the MyProxy server to contact")
1045
1046        defPort = int(os.environ.get('MYPROXY_SERVER_PORT') or '7512')
1047        self.parser.add_option("-p", 
1048                          "--psport", 
1049                          dest="port", 
1050                          default=defPort,
1051                          type="int",
1052                          help="The port of the MyProxy server to contact")
1053
1054    def _addInfoOpts(self):
1055        """Add command line options for info"""
1056        self.parser.add_option("-l", 
1057                          "--username", 
1058                          dest="username", 
1059                          help="Username for the delegated proxy")
1060
1061        self.parser.add_option("-C", 
1062                          "--owner-certfile", 
1063                          dest="ownerCertFile", 
1064                          help=\
1065"""Certificate for owner of credentials to be queried.  If omitted, it
1066defaults to $GLOBUS_LOCATION/etc/hostcert.pem.""")
1067       
1068        self.parser.add_option("-Y", 
1069                          "--owner-keyfile", 
1070                          dest="ownerKeyFile", 
1071                          default=None,
1072                          help=\
1073"""Private key for owner of credentials to be stored.  If omitted, it defaults
1074to $GLOBUS_LOCATION/etc/hostkey.pem.""")
1075       
1076        self.parser.add_option("-w", 
1077                          "--owner-keyfile-passphrase", 
1078                          dest="ownerPassphrase", 
1079                          default=None,
1080                          help=\
1081                          "Pass-phrase for Private key used for SSL client")
1082       
1083    def _addLogonOpts(self):
1084        """Add command line options for logon"""
1085        self.parser.add_option("-l", 
1086                               "--username", 
1087                               dest="username", 
1088                               help="Username for the delegated proxy")
1089
1090        self.parser.add_option("-t", 
1091                               "--proxy_lifetime", 
1092                               dest="lifetime", 
1093                               default=43200, # = 12 hours in seconds
1094                               type="int",
1095                               help=\
1096            "Lifetime of proxies delegated by the server (default 12 hours)")
1097
1098        self.parser.add_option("-o", 
1099                               "--out", 
1100                               dest="outfile", 
1101                               help="Location of delegated proxy")
1102
1103    def _addGetDelegationOpts(self):
1104        """Add command line options for Get Delegation"""
1105        self._addLogonOpts()
1106
1107    def _addDestroyOpts(self):
1108        """Add command line options for destroy"""
1109        self.parser.add_option("-l", 
1110                               "--username", 
1111                               dest="username", 
1112                               help=\
1113                       "Username corresponding to credentials to be removed")
1114
1115    def _addChangePassphraseOpts(self):
1116        """Add command line options for change pass-phrase"""
1117        self.parser.add_option("-l", 
1118                          "--username", 
1119                          dest="username", 
1120                          help="Username for the target proxy")
1121       
1122    def _addStoreOpts(self):
1123        """Add command line options for store"""
1124        self.parser.add_option("-l", 
1125                               "--username", 
1126                               dest="username", 
1127                               help=\
1128                       "Username corresponding to credentials to be stored")
1129       
1130        self.parser.add_option("-c", 
1131                          "--certfile", 
1132                          dest="certFile", 
1133                          default=None,
1134                          help="Certificate to be stored")
1135       
1136        self.parser.add_option("-y", 
1137                          "--keyfile", 
1138                          dest="keyFile", 
1139                          default=None,
1140                          help="Private key to be stored")
1141
1142        self.parser.add_option("-C", 
1143                          "--owner-certfile", 
1144                          dest="ownerCertFile", 
1145                          help=\
1146"""Cert. for owner of credentials to be stored.  If omitted, it defaults to
1147$GLOBUS_LOCATION/etc/hostcert.pem.  If this is not readable, the cert. to be
1148stored is set as the owner.""")
1149       
1150        self.parser.add_option("-Y", 
1151                          "--owner-keyfile", 
1152                          dest="ownerKeyFile", 
1153                          default=None,
1154                          help=\
1155"""Private key for owner of credentials to be stored.  If omitted, it defaults
1156to $GLOBUS_LOCATION/etc/hostcert.pem.""")
1157       
1158        self.parser.add_option("-w", 
1159                          "--owner-keyfile-passphrase", 
1160                          dest="ownerPassphrase", 
1161                          default=None,
1162                          help=\
1163                          "Pass-phrase for Private key used for SSL client")
1164
1165        self.parser.add_option("-t", 
1166                               "--proxy_lifetime", 
1167                               dest="lifetime", 
1168                               default=43200, # = 12 hours in seconds
1169                               type="int",
1170                               help=\
1171            "Lifetime of proxies delegated by the server (default 12 hours)")
1172
1173    def runGetDelegation(self):
1174        """Call MyProxyClient.getDelegation"""
1175        self.runLogon()
1176       
1177    def runLogon(self):
1178        """Call MyProxyClient.logon"""
1179        outfile = self.opts.outfile or MyProxyClient.defProxyFile
1180   
1181        # Get MyProxy password
1182        passphrase = getpass.getpass()
1183           
1184        # Retrieve proxy cert
1185        creds = self.myProxy.logon(self.opts.username, 
1186                                   passphrase, 
1187                                   lifetime=self.opts.lifetime)
1188        MyProxyClient.writeProxyFile(*creds)
1189           
1190        print "A proxy has been received for user %s in %s." % \
1191            (self.opts.username, outfile)
1192           
1193    def runChangePassphrase(self):
1194        """Call MyProxyClient.changePassphrase"""
1195       
1196        # Get MyProxy password
1197        passphrase = getpass.getpass(\
1198                     prompt='Enter (current) MyProxy pass phrase: ')
1199       
1200        newPassphrase = getpass.getpass(\
1201                                 prompt='Enter new MyProxy pass phrase: ')
1202       
1203        if newPassphrase != getpass.getpass(\
1204                     prompt='Verifying - Enter new MyProxy pass phrase: '):
1205            raise Exception, "Pass-phrases entered don't match"
1206               
1207        self.myProxy.changePassphrase(username,
1208                             passphrase,
1209                             newPassphrase, 
1210                             self.opts.certFile,
1211                             self.opts.keyFile,
1212                             ownerPassphrase=open('../tmp2').read().strip())           
1213               
1214    def runInfo(self):
1215        """Call MyProxyClient.info"""
1216        if not self.opts.ownerCertFile or not self.opts.ownerKeyFile:
1217#            self.opts.ownerCertFile = MyProxyClient.defProxyFile
1218            # Look for proxy file stored from previous call to logon/
1219            # get-delegation
1220            try:
1221                creds = MyProxyClient.readProxyFile()
1222           
1223                # Copy proxy and user certificates
1224                self.opts.ownerCertFile = './proxy-cert.pem'
1225                self.opts.ownerKeyFile = './proxy-key.pem'
1226                open(self.opts.ownerCertFile, 'w').write(creds[0] + ''.join(creds[2:]))
1227                open(self.opts.ownerKeyFile, 'w').write(creds[1])
1228            except IOError:
1229                # No such file - try proceeding regardless
1230                pass
1231           
1232        credExists, errorTxt, fields = self.myProxy.info(self.opts.username, 
1233                             ownerCertFile=self.opts.ownerCertFile,
1234                             ownerKeyFile=self.opts.ownerKeyFile,
1235                             ownerPassphrase=self.opts.ownerPassphrase)
1236        if errorTxt:
1237            raise CmdLineClientError, "User %s: %s" % (self.opts.username, 
1238                                                       errorTxt)
1239               
1240        elif credExists:
1241            print "username: %s" % self.opts.username
1242            print "owner: %s" % fields['CRED_OWNER']
1243            print "  time left: %d" % \
1244                    (fields['CRED_END_TIME'] - fields['CRED_START_TIME'])
1245        else:
1246            ownerCert = ''#X509.load_cert(self.opts.ownerCertFile)
1247            ownerCertDN = ''#'/' + \
1248                #ownerCert.get_subject().as_text().replace(', ', '/')
1249            print "no credentials found for user %s, owner \"%s\"" % \
1250                (self.opts.username, ownerCertDN)
1251               
1252    def runDestroy(self):
1253        """Call MyProxyClient.destroy"""
1254        self.myProxy.destroy(self.opts.username, 
1255                            ownerCertFile=self.opts.certFile,
1256                            ownerKeyFile=self.opts.keyFile,
1257                            ownerPassphrase=open('../tmp2').read().strip())
1258       
1259    def runStore(self):
1260        self.myProxy.store(self.opts.username, 
1261                          self.opts.certFile,
1262                          self.opts.keyFile,
1263                          ownerCertFile=self.opts.certFile,
1264                          ownerKeyFile=self.opts.keyFile,
1265                          ownerPassphrase=open('./tmp').read().strip(),
1266                          lifetime=self.opts.lifetime)
1267
1268
1269if __name__ == "__main__":
1270    main()
Note: See TracBrowser for help on using the repository browser.