source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/MyProxy.py @ 3133

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

Major changes to enable trust based on multiple CAs and use of dynamically created user certs from MyProxy? SimpleCA - affects ...
python/ndg.security.server/ndg/security/server/AttAuthority/init.py,
python/ndg.security.test/ndg/security/test/AttAuthority/siteAAttAuthorityProperties.xml,
python/ndg.security.test/ndg/security/test/AttAuthority/siteBAttAuthorityProperties.xml,
python/ndg.security.server/ndg/security/server/conf/attAuthorityProperties.xml,
python/ndg.security.server/ndg/security/server/SessionMgr/init.py,
python/ndg.security.test/ndg/security/test/sessionMgrClient/sessionMgrProperties.xml,
python/ndg.security.server/ndg/security/server/conf/sessionMgrProperties.xml,
python/ndg.security.server/ndg/security/server/MyProxy.py,
python/ndg.security.test/ndg/security/test/sessionMgr/test.py,
python/ndg.security.common/ndg/security/common/CredWallet.py

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
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$'
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
42
43class MyProxyClientError(Exception):
44    """Catch all exception class"""
45   
46class GetError(Exception):
47    """Exceptions arising from get request to server"""
48   
49class RetrieveError(Exception):
50    """Error recovering a response from MyProxy"""
51
52class _HostCheck(SSL.Checker.Checker):
53    """Override SSL.Checker.Checker to allow additional check of MyProxy
54    server identity.  If hostname doesn't match, allow match of host's 
55    Distinguished Name against MYPROXY_SERVER_DN setting"""
56
57    def __init__(self, 
58                 myProxyServerDN=os.environ.get('MYPROXY_SERVER_DN'),
59                 cnHostPfx='host/',
60                 **kw):
61        """Override parent class __init__ to enable setting of myProxyServerDN
62        setting
63       
64        @type myProxyServerDN: string
65        @param myProxyServerDN: Set the expected Distinguished Name of the
66        MyProxy server to avoid errors matching hostnames.  This is useful
67        where the hostname is not fully qualified
68       
69        @type cnHostPfx: string
70        @param cnHostPfx: globus host certificates are
71        generated by default with a 'host/' prefix to the host name.  Set
72        this keyword to '' or None to override and omit the prefix"""
73       
74        SSL.Checker.Checker.__init__(self, **kw)
75       
76        self.myProxyServerDN = myProxyServerDN
77        self.cnHostPfx = cnHostPfx
78       
79       
80    def __call__(self, peerCert, host=None):
81        """Carry out checks on server ID
82        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
83        instance
84        @param host: name of host to check
85        """
86       
87        # Globus host certificate has a "host/" prefix - see explanation in
88        # __init__.__doc__
89        cnHostPfx = isinstance(self.cnHostPfx, basestring) \
90                    and self.cnHostPfx or ''
91        host = None or cnHostPfx + self.host
92       
93        try:
94            SSL.Checker.Checker.__call__(self, peerCert, host=host)
95           
96        except SSL.Checker.WrongHost, e:
97            # Try match against DN set from MYPROXY_SERVER_DN / config
98            # file setting
99            peerCertDN = '/' + \
100                    peerCert.get_subject().as_text().replace(', ', '/')
101            if peerCertDN != self.myProxyServerDN:
102                # They match - drop the exception and return all OK instead
103                raise e
104           
105        return True
106           
107       
108class MyProxyClient(object):
109    """MyProxy client interface
110   
111    Based on protocol definitions in:
112   
113    http://grid.ncsa.uiuc.edu/myproxy/protocol/
114   
115    @type __getCmd: string
116    @cvar __getCmd: get command string
117   
118    @type __infoCmd: string
119    @cvar __infoCmd: info command string
120   
121    @type __destroyCmd: string
122    @cvar __destroyCmd: destroy command string
123   
124    @type __changePassphrase: string
125    @cvar __changePassphrase: command string to change cred pass-phrase
126   
127    @type __storeCmd: string
128    @cvar __storeCmd: store command string
129   
130    @type _hostCertSubDirPath: string
131    @cvar _hostCertSubDirPath: sub-directory path host certificate (as tuple)
132   
133    @type _hostKeySubDirPath: string
134    @cvar _hostKeySubDirPath: sub-directory path to host key (as tuple)
135   
136    @type __validKeys: tuple
137    @cvar __validKeys: sets permissable element names for MyProxy XML config
138    file
139    """
140     
141    __getCmd="""VERSION=MYPROXYv2
142COMMAND=0
143USERNAME=%s
144PASSPHRASE=%s
145LIFETIME=%d\0"""
146 
147    __infoCmd="""VERSION=MYPROXYv2
148COMMAND=2
149USERNAME=%s
150PASSPHRASE=PASSPHRASE
151LIFETIME=0"""
152 
153    __destroyCmd="""VERSION=MYPROXYv2
154COMMAND=3
155USERNAME=%s
156PASSPHRASE=PASSPHRASE
157LIFETIME=0"""
158
159    __changePassphraseCmd="""VERSION=MYPROXYv2
160 COMMAND=4
161 USERNAME=%s
162 PASSPHRASE=%s
163 NEW_PHRASE=%s
164 LIFETIME=0"""
165   
166    __storeCmd="""VERSION=MYPROXYv2
167COMMAND=5
168USERNAME=%s
169PASSPHRASE=
170LIFETIME=%d\0"""
171
172    _hostCertSubDirPath = ('etc', 'hostcert.pem')
173    _hostKeySubDirPath = ('etc', 'hostkey.pem')
174   
175    # valid configuration property keywords
176    __validKeys = ('hostname',
177                   'port',
178                   'serverDN',
179                   'serverCNprefix',
180                   'gridSecurityDir',
181                   'openSSLConfFilePath',
182                   'tmpDir',
183                   'proxyCertMaxLifetime',
184                   'proxyCertLifetime',
185                   'caCertFile')
186
187    # Work out default location of proxy file if it exists.  This is set if a
188    # call has been made previously to logon / get-delegation
189    defProxyFile = sys.platform == 'win32' and 'proxy' or \
190    sys.platform in ('linux2', 'darwin') and '/tmp/x509up_u%s'%(os.getuid()) \
191    or None
192
193    @classmethod
194    def writeProxyFile(cls, proxyCert, proxyPriKey, userCert, filePath=None):
195        """Write out proxy cert to file in the same way as myproxy-logon -
196        proxy cert, private key, user cert.  Nb. output from logon can be
197        passed direct into this method
198       
199        @type proxyCert: string
200        @param proxyCert: proxy certificate
201        @type proxyPriKey: string
202        @param proxyPriKey: private key for proxy
203        @type userCert: string
204        @param userCert: user certificate which issued the proxy
205        @type filePath: string
206        @param filePath: set to override the default filePath"""
207       
208        if filePath is None:
209            filePath = MyProxyClient.defProxyFile
210           
211        if filePath is None:
212            MyProxyClientError, \
213                "Error setting proxy file path - invalid platform?"
214       
215        outStr = proxyCert + proxyPriKey + userCert       
216        open(MyProxyClient.defProxyFile, 'w').write(outStr)
217        try:
218            # Make sure permssions are set correctly
219            os.chmod(MyProxyClient.defProxyFile, 0600)
220        except Exception:
221            # Don't leave the file lying around if couldn't change it's
222            # permissions
223            os.unlink(MyProxyClient.defProxyFile)
224           
225            raise MyProxyClientError, \
226                'Unable to set 0600 permissions for proxy file "%s": %s' % \
227                (MyProxyClient.defProxyFile, e)
228
229    @classmethod
230    def readProxyFile(cls, filePath=None):
231        """Read proxy cert file following the format used by myproxy-logon -
232        proxy, cert, private key, user cert.
233       
234        @rtype: tuple
235        @return: tuple containing proxy cert, private key, user cert"""
236        if filePath is None:
237            filePath = MyProxyClient.defProxyFile
238               
239        proxy = open(MyProxyClient.defProxyFile).read()
240       
241        # Split certs and key into separate tuple items
242        return tuple(['-----BEGIN'+i for i in proxy.split('-----BEGIN')[1:]])
243       
244       
245    #_________________________________________________________________________           
246    def __init__(self, propFilePath=None, **prop):
247        """Make an initial settings for client connections to MyProxy
248       
249        Settings are held in a dictionary which can be set from **prop,
250        a call to setProperties() or by passing settings in an XML file
251        given by propFilePath
252       
253        @param propFilePath:   set properties via a configuration file
254        @param **prop:         set properties via keywords - see __validKeys
255        class variable for a list of these
256        """
257       
258        # Check for parameter names set from input
259        #self.certReqDNparam = None
260
261        # settings dictionary
262        self.__prop = {}
263       
264        # Server host name - take from environment variable if available
265        if 'MYPROXY_SERVER' in os.environ:
266            self.__prop['hostname'] = os.environ['MYPROXY_SERVER']
267           
268        # ... and port number
269        if 'MYPROXY_SERVER_PORT' in os.environ:
270            self.__prop['port'] = int(os.environ['MYPROXY_SERVER_PORT'])
271        else:
272            # Usual default is ...
273            self.__prop['port'] = 7512
274           
275        self.__prop['proxyCertLifetime'] = 43200
276        self.__prop['proxyCertMaxLifetime'] = 43200
277       
278        # Configuration file used to get default subject when generating a
279        # new proxy certificate request
280        self.__openSSLConf = OpenSSLConfig()
281
282       
283        # Properties set via input keywords
284        self.setProperties(**prop)
285
286        # If properties file is set any parameters settings in file will
287        # override those set by input keyword
288        if propFilePath is not None:
289            self.readProperties(propFilePath)
290
291
292    #_________________________________________________________________________
293    def setProperties(self, **prop):
294        """Update existing properties from an input dictionary
295        Check input keys are valid names"""
296       
297        invalidKeys = [key for key in prop if key not in self.__validKeys]
298        if invalidKeys:
299            raise MyProxyClientError, 'Invalid property name(s) set: "%s"' % \
300                                    '", "'.join(invalidKeys)
301               
302        self.__prop.update(prop)
303
304        # Update openssl conf file path
305        #
306        # Check 'prop' to see if they've been in THIS update
307        # Check 'self.__prop' to ensure both are present in
308        # order to construct a file path
309        if 'openSSLConfFilePath' in prop:           
310            self.__openSSLConf.filePath = self.__prop['openSSLConfFilePath']
311            self.__openSSLConf.read()
312           
313
314    #_________________________________________________________________________
315    def readProperties(self, propFilePath=None, propElem=None):
316        """Read XML properties from a file or cElementTree node
317       
318        propFilePath|propertiesElem
319
320        @type propFilePath: string
321        @param propFilePath: set to read from the specified file
322       
323        @type propElem: ElementTree node
324        @param propElem: set to read beginning from a cElementTree node
325        """
326
327        if propFilePath is not None:
328            try:
329                tree = ElementTree.parse(propFilePath)
330                propElem = tree.getroot()
331               
332            except IOError, e:
333                raise MyProxyClientError, \
334                                "Error parsing properties file \"%s\": %s" % \
335                                (e.filename, e.strerror)           
336            except Exception, e:
337                raise MyProxyClientError, \
338                                "Error parsing properties file: %s" % str(e)
339                               
340        if propElem is None:
341            raise MyProxyClientError, \
342                    "Root element for parsing properties file is not defined"
343
344
345        # Get properties as a data dictionary
346        prop = {}
347        try:
348            for elem in propElem:
349                # Check for string type to avoid exceptions from isdigit and
350                # expandvars
351                if isinstance(elem.text, basestring):
352                    if elem.text.isdigit():
353                        prop[elem.tag] = int(elem.text)
354                    else:
355                        prop[elem.tag] = os.path.expandvars(elem.text)
356                else:
357                    prop[elem.tag] = elem.text
358                   
359        except Exception, e:
360            raise SessionMgrError, \
361                "Error parsing tag \"%s\" in properties file" % elem.tag
362       
363        self.setProperties(**prop)
364
365
366    #_________________________________________________________________________
367    def __getOpenSSLConfig(self):
368        "Get OpenSSLConfig object property method"
369        return self.__openSSLConfig
370   
371    openSSLConfig = property(fget=__getOpenSSLConfig,
372                             doc="OpenSSLConfig object")
373
374           
375    #_________________________________________________________________________       
376    def _initConnection(self, 
377                        ownerCertFile=None, 
378                        ownerKeyFile=None,
379                        ownerPassphrase=None):
380        """Initialise connection setting up SSL context and client and
381        server side identity checks
382       
383        @param ownerCertFile: client certificate and owner of credential
384        to be acted on.  Can be a proxy cert + proxy's signing cert.  Cert
385        and private key are not necessary for getDelegation / logon calls
386        @param ownerKeyFile: client private key file
387        @param ownerPassphrase: pass-phrase protecting private key if set -
388        not needed in the case of a proxy private key
389        """
390
391        # Must be version 3 for MyProxy
392        context = SSL.Context(protocol='sslv3')
393
394        if 'caCertFile' in self.__prop:
395            context.load_verify_locations(cafile=self.__prop['caCertFile'])
396                           
397            # Stop if peer's certificate can't be verified
398            context.set_allow_unknown_ca(False)
399        else:
400            context.set_allow_unknown_ca(True)
401           
402        if ownerCertFile:
403            try:
404                context.load_cert_chain(ownerCertFile,
405                                keyfile=ownerKeyFile,
406                                callback=lambda *ar, **kw: ownerPassphrase)
407            except Exception, e:
408                raise MyProxyClientError, \
409                    "Error loading cert. and key for SSL connection: %s" % e
410           
411            # Verify peer's certificate
412            context.set_verify(SSL.verify_peer, 1) 
413       
414           
415        # Disable for compatibility with myproxy server (er, globus)
416        # globus doesn't handle this case, apparently, and instead
417        # chokes in proxy delegation code
418        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
419       
420        # connect to myproxy server
421        conn = SSL.Connection(context, sock=socket.socket())
422       
423        # Check server host identity - if host doesn't match use explicit
424        # 'serverDN'
425        # host/<hostname> one
426        hostCheck = _HostCheck(host=self.__prop['hostname'],
427                               myProxyServerDN=self.__prop.get('serverDN'),
428                               cnHostPfx=self.__prop.get('serverCNprefix'))
429        conn.set_post_connection_check_callback(hostCheck)
430       
431        return conn
432   
433           
434    #_________________________________________________________________________       
435    def _createCertReq(self, CN, nBitsForKey=1024, messageDigest="md5"):
436        """
437        Create a certificate request.
438       
439        @param CN: Common Name for certificate - effectively the same as the
440        username for the MyProxy credential
441        @param nBitsForKey: number of bits for private key generation -
442        default is 1024
443        @param messageDigest: message disgest type - default is MD5
444        @return tuple of certificate request PEM text and private key PEM text
445        """
446       
447        # Check all required certifcate request DN parameters are set               
448        # Create certificate request
449        req = X509.Request()
450   
451        # Generate keys
452        key = RSA.gen_key(nBitsForKey, m2.RSA_F4)
453   
454        # Create public key object
455        pubKey = EVP.PKey()
456        pubKey.assign_rsa(key)
457       
458        # Add the public key to the request
459        req.set_version(0)
460        req.set_pubkey(pubKey)
461       
462        defaultReqDN = self.__openSSLConf.reqDN
463             
464        # Set DN
465        x509Name = X509.X509_Name()
466        x509Name.CN = CN
467       
468        if defaultReqDN:
469            x509Name.OU = defaultReqDN['OU']
470            x509Name.O = defaultReqDN['O']
471                       
472        req.set_subject_name(x509Name)
473       
474        req.sign(pubKey, messageDigest)
475
476        return (req.as_der(), key.as_pem(cipher=None))
477   
478   
479    #_________________________________________________________________________           
480    def _deserializeResponse(self, msg, *fieldNames):
481        """
482        Deserialize a MyProxy server response
483       
484        @param msg: string response message from MyProxy server
485        @*fieldNames: the content of additional fields can be returned by
486        specifying the field name or names as additional arguments e.g. info
487        method passes 'CRED_START_TIME', 'CRED_END_TIME' and 'CRED_OWNER'
488        field names.  The content of fields is returned as an extra element
489        in the tuple response.  This element is itself a dictionary indexed
490        by field name.
491        @return tuple of integer response and errorTxt string (if any)
492        """
493       
494        lines = msg.split('\n')
495       
496        # get response value
497        responselines = filter(lambda x: x.startswith('RESPONSE'), lines)
498        responseline = responselines[0]
499        respCode = int(responseline.split('=')[1])
500       
501        # get error text
502        errorTxt = ""
503        errorlines = filter(lambda x: x.startswith('ERROR'), lines)
504        for e in errorlines:
505            etext = e.split('=', 1)[1]
506            errorTxt += os.linesep + etext
507       
508        if fieldNames:
509            fields = {}
510                       
511            for fieldName in fieldNames:
512                fieldlines = filter(lambda x: x.startswith(fieldName), lines)
513                try:
514                    # Nb. '1' arg to split ensures owner DN is not split up.
515                    field = fieldlines[0].split('=', 1)[1]
516                    fields[fieldName]=field.isdigit() and int(field) or field
517
518                except IndexError:
519                    # Ignore fields that aren't found
520                    pass
521               
522            return respCode, errorTxt, fields
523        else:
524            return respCode, errorTxt
525   
526 
527    #_________________________________________________________________________             
528    def _deserializeCerts(self, inputDat):
529        """Unpack certificates returned from a get delegation call to the
530        server
531       
532        @param inputDat: string containing the proxy cert and private key
533        and signing cert all in DER format
534       
535        @return list containing the equivalent to the input in PEM format"""
536        pemCerts = []       
537        dat = inputDat
538       
539        while dat:   
540            # find start of cert, get length       
541            ind = dat.find('\x30\x82')
542            if ind < 0:
543                break
544               
545            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
546   
547            # extract der-format cert, and convert to pem
548            derCert = dat[ind:ind+len+4]
549           
550            x509 = X509.load_cert_der_string(derCert)
551            pemCert = x509.as_pem()
552           
553            pemCerts.append(pemCert)
554   
555            # trim cert from data
556            dat = dat[ind + len + 4:]
557           
558        return pemCerts
559
560
561    #_________________________________________________________________________   
562    def info(self,
563             username, 
564             ownerCertFile=None,
565             ownerKeyFile=None,
566             ownerPassphrase=None):
567        """return True/False whether credentials exist on the server for a
568        given username
569       
570        Exceptions:  GetError, RetrieveError
571       
572        @type username: string
573        @param username: username selected for credential
574        @type ownerCertFile: string
575        @param ownerCertFile: certificate used for client authentication with
576        the MyProxy server SSL connection.  This ID will be set as the owner
577        of the stored credentials.  Only the owner can later remove
578        credentials with myproxy-destroy or the destroy method.  If not set,
579        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
580        @type ownerKeyFile: string
581        @param ownerKeyFile: corresponding private key file.  See explanation
582        for ownerCertFile
583        @type ownerPassphrase: string
584        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
585        private key is not password protected.
586        """
587        globusLoc = os.environ.get('GLOBUS_LOCATION')
588        if not ownerCertFile:
589            if globusLoc:
590                ownerCertFile = os.path.join(globusLoc, 
591                                         *MyProxyClient._hostCertSubDirPath)
592                ownerKeyFile = os.path.join(globusLoc, 
593                                         *MyProxyClient._hostKeySubDirPath)
594            else:
595                raise MyProxyClientError, \
596            "No client authentication cert. and private key file were given"
597
598        # Set-up SSL connection
599        conn = self._initConnection(ownerCertFile=ownerCertFile,
600                                    ownerKeyFile=ownerKeyFile,
601                                    ownerPassphrase=ownerPassphrase)
602       
603        conn.connect((self.__prop['hostname'], self.__prop['port']))
604       
605        # send globus compatibility stuff
606        conn.write('0')
607   
608        # send info command - ensure conversion from unicode before writing
609        cmd = MyProxyClient.__infoCmd % username
610        conn.write(str(cmd))
611   
612        # process server response
613        dat = conn.recv(8192)
614         
615        # Pass in the names of fields to return in the dictionary 'field'
616        respCode, errorTxt, field = self._deserializeResponse(dat, 
617                                                         'CRED_START_TIME', 
618                                                         'CRED_END_TIME', 
619                                                         'CRED_OWNER')
620
621        return not bool(respCode), errorTxt, field
622
623
624    #_________________________________________________________________________   
625    def changePassphrase(self,
626                         username, 
627                         passphrase,
628                         newPassphrase,
629                         ownerCertFile=None,
630                         ownerKeyFile=None,
631                         ownerPassphrase=None):
632        """change pass-phrase protecting the credentials for a given username
633       
634        Exceptions:  GetError, RetrieveError
635       
636        @param username: username of credential
637        @param passphrase: existing pass-phrase for credential
638        @param newPassphrase: new pass-phrase to replace the existing one.
639        @param ownerCertFile: certificate used for client authentication with
640        the MyProxy server SSL connection.  This ID will be set as the owner
641        of the stored credentials.  Only the owner can later remove
642        credentials with myproxy-destroy or the destroy method.  If not set,
643        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
644        @param ownerKeyFile: corresponding private key file.  See explanation
645        for ownerCertFile
646        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
647        private key is not password protected. 
648        @return none
649        """
650        globusLoc = os.environ.get('GLOBUS_LOCATION')
651        if not ownerCertFile or not ownerKeyFile:
652            if globusLoc:
653                ownerCertFile = os.path.join(globusLoc, 
654                                         *MyProxyClient._hostCertSubDirPath)
655                ownerKeyFile = os.path.join(globusLoc, 
656                                         *MyProxyClient._hostKeySubDirPath)
657            else:
658                raise MyProxyClientError, \
659            "No client authentication cert. and private key file were given"
660       
661        # Set-up SSL connection
662        conn = self._initConnection(ownerCertFile=ownerCertFile,
663                                    ownerKeyFile=ownerKeyFile,
664                                    ownerPassphrase=ownerPassphrase)
665
666        conn.connect((self.__prop['hostname'], self.__prop['port']))
667       
668        # send globus compatibility stuff
669        conn.write('0')
670   
671        # send command - ensure conversion from unicode before writing
672        cmd = MyProxyClient.__changePassphraseCmd % (username, 
673                                                     passphrase,
674                                                     newPassphrase)
675        conn.write(str(cmd))
676   
677        # process server response
678        dat = conn.recv(8192)
679           
680        respCode, errorTxt = self._deserializeResponse(dat)
681        if respCode:
682            raise GetError, errorTxt
683
684
685    #_________________________________________________________________________   
686    def destroy(self,
687                username, 
688                ownerCertFile=None,
689                ownerKeyFile=None,
690                ownerPassphrase=None):
691        """destroy credentials from the server for a given username
692       
693        Exceptions:  GetError, RetrieveError
694       
695        @param username: username selected for credential
696        @param ownerCertFile: certificate used for client authentication with
697        the MyProxy server SSL connection.  This ID will be set as the owner
698        of the stored credentials.  Only the owner can later remove
699        credentials with myproxy-destroy or the destroy method.  If not set,
700        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
701        @param ownerKeyFile: corresponding private key file.  See explanation
702        for ownerCertFile
703        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
704        private key is not password protected. 
705        @return none
706        """
707        globusLoc = os.environ.get('GLOBUS_LOCATION')
708        if not ownerCertFile or not ownerKeyFile:
709            if globusLoc:
710                ownerCertFile = os.path.join(globusLoc, 
711                                         *MyProxyClient._hostCertSubDirPath)
712                ownerKeyFile = os.path.join(globusLoc, 
713                                         *MyProxyClient._hostKeySubDirPath)
714            else:
715                raise MyProxyClientError, \
716            "No client authentication cert. and private key file were given"
717       
718        # Set-up SSL connection
719        conn = self._initConnection(ownerCertFile=ownerCertFile,
720                                    ownerKeyFile=ownerKeyFile,
721                                    ownerPassphrase=ownerPassphrase)
722
723        conn.connect((self.__prop['hostname'], self.__prop['port']))
724       
725        # send globus compatibility stuff
726        conn.write('0')
727   
728        # send destroy command - ensure conversion from unicode before writing
729        cmd = MyProxyClient.__destroyCmd % username
730        conn.write(str(cmd))
731   
732        # process server response
733        dat = conn.recv(8192)
734           
735        respCode, errorTxt = self._deserializeResponse(dat)
736        if respCode:
737            raise GetError, errorTxt
738
739
740    #_________________________________________________________________________   
741    def store(self,
742              username,
743              passphrase, 
744              certFile,
745              keyFile,
746              ownerCertFile=None,
747              ownerKeyFile=None,
748              ownerPassphrase=None,
749              lifetime=None,
750              force=True):
751        """Upload credentials to the server
752       
753        @raise GetError:
754        @raise RetrieveError:
755       
756        @type username: string
757        @param username: username selected for new credential
758        @type passphrase: string
759        @param passphrase: pass-phrase for new credential.  This is the pass
760        phrase which protects keyfile.
761        @type certFile: string
762        @param certFile: user's X.509 certificate in PEM format
763        @type keyFile: string
764        @param keyFile: equivalent private key file in PEM format
765        @type ownerCertFile: string
766        @param ownerCertFile: certificate used for client authentication with
767        the MyProxy server SSL connection.  This ID will be set as the owner
768        of the stored credentials.  Only the owner can later remove
769        credentials with myproxy-destroy or the destroy method.  If not set,
770        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem or if this
771        is not set, certFile
772        @type ownerKeyFile: string
773        @param ownerKeyFile: corresponding private key file.  See explanation
774        for ownerCertFile
775        @type ownerPassphrase: string
776        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
777        private key is not password protected.  Nb. keyFile is expected to
778        be passphrase protected as this will be the passphrase used for
779        logon / getDelegation.
780        @type Force: bool
781        @param force: set to True to overwrite any existing creds with the
782        same username.  If, force=False a check is made with a call to info.
783        If creds already, exist exit without proceeding
784        """
785       
786        lifetime = lifetime or self.__prop['proxyCertMaxLifetime']
787
788        # Inputs must be string type otherwise server will reject the request
789        if isinstance(username, unicode):
790            username = str(username)
791           
792        if isinstance(passphrase, unicode):
793            passphrase = str(passphrase)
794       
795        globusLoc = os.environ.get('GLOBUS_LOCATION')
796        if not ownerCertFile or not ownerKeyFile:
797            if globusLoc:
798                ownerCertFile = os.path.join(globusLoc, 
799                                         *MyProxyClient._hostCertSubDirPath)
800                ownerKeyFile = os.path.join(globusLoc, 
801                                         *MyProxyClient._hostKeySubDirPath)
802            else:
803                # Default so that the owner is the same as the ID of the
804                # credentials to be uploaded.
805                ownerCertFile = certFile
806                ownerKeyFile = keyFile
807                ownerPassphrase = passphrase
808               
809        if not force:
810            # Check credentials don't already exist
811            if self.info(username,
812                         ownerCertFile=ownerCertFile,
813                         ownerKeyFile=ownerKeyFile,
814                         ownerPassphrase=ownerPassphrase)[0]:
815                raise MyProxyClientError, \
816                        "Credentials already exist for user: %s" % username
817
818        # Set up SSL connection
819        conn = self._initConnection(ownerCertFile=ownerCertFile,
820                                    ownerKeyFile=ownerKeyFile,
821                                    ownerPassphrase=ownerPassphrase)
822       
823        conn.connect((self.__prop['hostname'], self.__prop['port']))
824       
825        # send globus compatibility stuff
826        conn.write('0')
827   
828        # send store command - ensure conversion from unicode before writing
829        cmd = MyProxyClient.__storeCmd % (username, lifetime)
830        conn.write(str(cmd))
831   
832        # process server response
833        dat = conn.recv(8192)
834           
835        respCode, errorTxt = self._deserializeResponse(dat)
836        if respCode:
837            raise GetError, errorTxt
838       
839        # Send certificate and private key
840        certTxt = X509.load_cert(certFile).as_pem()
841        keyTxt = open(keyFile).read()
842       
843        conn.send(certTxt + keyTxt)
844   
845   
846        # process server response
847        resp = conn.recv(8192)
848        respCode, errorTxt = self._deserializeResponse(resp)
849        if respCode:
850            raise RetrieveError, errorTxt
851       
852       
853    #_________________________________________________________________________           
854    def logon(self, username, passphrase, lifetime=None):
855        """Retrieve a proxy credential from a MyProxy server
856       
857        Exceptions:  GetError, RetrieveError
858       
859        @type username: basestring
860        @param username: username of credential
861       
862        @type passphrase: basestring
863        @param passphrase: pass-phrase for private key of credential held on
864        server
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.6"
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.