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

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

security/architecture/uml/ndg2-dews-security-beta.eap: update from EA upgrade

security/python/ndg.security.client/setup.cfg,
security/python/ndg.security.common/setup.cfg,
security/python/ndg.security.server/setup.cfg,
security/python/ndg.security.test/setup.cfg,
security/python/setup.cfg: new release tag for OMII-UK 1st drop

security/python/ndg.security.server/ndg/security/server/MyProxy.py: iimprove error message for cert file not found - incl. CA cert.

security/python/ndg.security.test/ndg/security/test/sessionMgrClient/README: addtional note about ensuring MYPROXY_SERVER env for server.py shell
security/python/ndg.security.test/ndg/security/test/sessionMgrClient/server.sh: deleted - server.py replaces it

security/python/ndg.security.test/ndg/security/test/sessionMgrClient/SessionMgrClientTest.py: working version with test certs included in SVN and unit test env var refs.

security/python/ndg.security.test/ndg/security/test/sessionMgrClient/sessionMgrProperties.xml: incl. default serverCNprefix elem setting

security/python/ndg.security.test/setup.py: important fixes to ensure test data and test certs are included in package data for egg.

  • 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"""
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"""
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 CA cert., 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        @type lifetime: int
867        @param lifetime: lifetime for generated certificate
868       
869        @rtype: tuple
870        @return credentials as strings in PEM format: the
871        user certificate, it's private key and the issuing certificate.  The
872        issuing certificate is only set if the user certificate is a proxy
873        """
874       
875        lifetime = lifetime or self.__prop['proxyCertLifetime']
876
877        # Generate certificate request here - any errors will be thrown
878        # prior to making the connection and so not upsetting the server
879        #
880        # - The client will generate a public/private key pair and send a
881        #   NULL-terminated PKCS#10 certificate request to the server.
882        certReq, priKey = self._createCertReq(username)
883
884        # Set-up SSL connection
885        conn = self._initConnection()
886        conn.connect((self.__prop['hostname'], self.__prop['port']))
887       
888        # send globus compatibility stuff
889        conn.write('0')
890   
891        # send get command - ensure conversion from unicode before writing
892        cmd = MyProxyClient.__getCmd % (username, passphrase, lifetime)
893        conn.write(str(cmd))
894   
895        # process server response
896        dat = conn.recv(8192)
897        respCode, errorTxt = self._deserializeResponse(dat)
898        if respCode:
899            raise GetError, errorTxt
900       
901        # Send certificate request
902        conn.send(certReq)
903   
904        # process certificates
905        # - 1st byte , number of certs
906        dat = conn.recv(1)
907        nCerts = ord(dat[0])
908       
909        # - n certs
910        dat = conn.recv(8192)
911   
912        # process server response
913        resp = conn.recv(8192)
914        respCode, errorTxt = self._deserializeResponse(resp)
915        if respCode:
916            raise RetrieveError, errorTxt
917   
918        # deserialize certs from received cert data
919        pemCerts = self._deserializeCerts(dat)
920        if len(pemCerts) != nCerts:
921            RetrieveError, "%d certs expected, %d received" % \
922                                                    (nCerts, len(pemCerts))
923   
924        # Return certs and private key
925        # - proxy cert
926        # - private key
927        # - rest of cert chain
928        creds = [pemCerts[0], priKey]
929        creds.extend(pemCerts[1:])
930       
931        return tuple(creds)
932       
933
934    def getDelegation(self, *arg, **kw):
935        """Retrieve proxy cert for user - same as logon"""
936        return self.logon(*arg, **kw)
937
938
939#_____________________________________________________________________________   
940def main():
941    import traceback
942    import pdb;pdb.set_trace()
943    try:
944        CmdLineClient()           
945    except Exception, e:
946        print "Error: ", e
947        print traceback.print_exc()
948        sys.exit(1)
949
950#  help="check whether a credential exists")
951#
952#  help="destroy credential")
953#
954#  help="change pass-phrase protecting credential")
955
956import optparse
957import getpass
958
959class CmdLineClientError(Exception):
960    """Errors related to CmdLineClient class"""
961   
962class CmdLineClient(object):
963    """Command line interface to MyProxyClient class.  Where possible it
964    supports the same options as the Globus myproxy-* client commands"""
965   
966    run = {
967        'info': 'runInfo', 
968        'logon': 'runLogon',
969        'get-delegation': 'runGetDelegation',
970        'destroy': 'runDestroy', 
971        'change-pass': 'runChangePassphrase',
972        'store': 'runStore'
973    }
974
975    initOpts = {
976        'info': '_addInfoOpts', 
977        'logon': '_addLogonOpts',
978        'get-delegation': '_addGetDelegationOpts',
979        'destroy': '_addDestroyOpts', 
980        'change-pass': '_addChangePassphraseOpts',
981        'store': '_addStoreOpts'
982    }
983
984    cmdUsage = {
985        'info': "usage: %prog info arg1 arg2", 
986        'logon': "usage: %prog logon arg1 arg2",
987        'get-delegation': "usage: %prog get-delegation arg1 arg2",
988        'destroy': "usage: %prog destroy arg1 arg2", 
989        'change-pass': "usage: %prog change-pass arg1 arg2",
990        'store': "usage: %prog store arg1 arg2"
991    }
992   
993    # Keep '%prog' in a separate string otherwise it confuses the % operator
994    usage = "usage: %prog" + " [%s] arg1 arg2" % '|'.join(run.keys())   
995    version = "%prog 0.8.7"
996       
997    def __init__(self):
998        """Parse the command line and run the appropriate command"""
999       
1000        self.parser = optparse.OptionParser(usage=self.__class__.usage,
1001                                            version=self.__class__.version)
1002
1003        # Get command - expected to be 1st arg
1004        if len(sys.argv) > 1:
1005            cmd = sys.argv[1]
1006            if cmd not in self.__class__.run:
1007                self.parser.error('"%s" command option not recognised' % cmd)
1008        else:
1009            self.parser.error('No command option set')
1010
1011        # Ammend usage string for specific command
1012        self.parser.set_usage(self.__class__.cmdUsage[cmd])
1013       
1014        # Add options based on the command set
1015        self._addGenericOpts()
1016        getattr(self, self.initOpts[cmd])()
1017       
1018        # Omit command options as parser won't understand it
1019        (self.opts, args) = self.parser.parse_args(args=sys.argv[2:])     
1020   
1021        # Process generic options
1022       
1023        # Not all commands require username option - for those that do, check
1024        # to see if it was set or omitted
1025        if hasattr(self.opts, 'username') and not self.opts.username:
1026            if sys.platform == 'win32':
1027                self.opts.username = os.environ["USERNAME"]
1028            else:
1029                import pwd
1030                self.opts.username = pwd.getpwuid(os.geteuid())[0]
1031   
1032        self.myProxy = MyProxyClient(hostname=self.opts.host,
1033                                     port=self.opts.port)#,
1034#                                     O='NDG',
1035#                                     OU='BADC')
1036
1037        # Run the command
1038        getattr(self, self.run[cmd])()
1039
1040               
1041    def _addGenericOpts(self):
1042        """Generic options applying to all commands"""
1043        self.parser.add_option("-s", 
1044                          "--pshost", 
1045                          dest="host",
1046                          default=os.environ.get('MYPROXY_SERVER'),
1047                          help="The hostname of the MyProxy server to contact")
1048
1049        defPort = int(os.environ.get('MYPROXY_SERVER_PORT') or '7512')
1050        self.parser.add_option("-p", 
1051                          "--psport", 
1052                          dest="port", 
1053                          default=defPort,
1054                          type="int",
1055                          help="The port of the MyProxy server to contact")
1056
1057    def _addInfoOpts(self):
1058        """Add command line options for info"""
1059        self.parser.add_option("-l", 
1060                          "--username", 
1061                          dest="username", 
1062                          help="Username for the delegated proxy")
1063
1064        self.parser.add_option("-C", 
1065                          "--owner-certfile", 
1066                          dest="ownerCertFile", 
1067                          help=\
1068"""Certificate for owner of credentials to be queried.  If omitted, it
1069defaults to $GLOBUS_LOCATION/etc/hostcert.pem.""")
1070       
1071        self.parser.add_option("-Y", 
1072                          "--owner-keyfile", 
1073                          dest="ownerKeyFile", 
1074                          default=None,
1075                          help=\
1076"""Private key for owner of credentials to be stored.  If omitted, it defaults
1077to $GLOBUS_LOCATION/etc/hostkey.pem.""")
1078       
1079        self.parser.add_option("-w", 
1080                          "--owner-keyfile-passphrase", 
1081                          dest="ownerPassphrase", 
1082                          default=None,
1083                          help=\
1084                          "Pass-phrase for Private key used for SSL client")
1085       
1086    def _addLogonOpts(self):
1087        """Add command line options for logon"""
1088        self.parser.add_option("-l", 
1089                               "--username", 
1090                               dest="username", 
1091                               help="Username for the delegated proxy")
1092
1093        self.parser.add_option("-t", 
1094                               "--proxy_lifetime", 
1095                               dest="lifetime", 
1096                               default=43200, # = 12 hours in seconds
1097                               type="int",
1098                               help=\
1099            "Lifetime of proxies delegated by the server (default 12 hours)")
1100
1101        self.parser.add_option("-o", 
1102                               "--out", 
1103                               dest="outfile", 
1104                               help="Location of delegated proxy")
1105
1106    def _addGetDelegationOpts(self):
1107        """Add command line options for Get Delegation"""
1108        self._addLogonOpts()
1109
1110    def _addDestroyOpts(self):
1111        """Add command line options for destroy"""
1112        self.parser.add_option("-l", 
1113                               "--username", 
1114                               dest="username", 
1115                               help=\
1116                       "Username corresponding to credentials to be removed")
1117
1118    def _addChangePassphraseOpts(self):
1119        """Add command line options for change pass-phrase"""
1120        self.parser.add_option("-l", 
1121                          "--username", 
1122                          dest="username", 
1123                          help="Username for the target proxy")
1124       
1125    def _addStoreOpts(self):
1126        """Add command line options for store"""
1127        self.parser.add_option("-l", 
1128                               "--username", 
1129                               dest="username", 
1130                               help=\
1131                       "Username corresponding to credentials to be stored")
1132       
1133        self.parser.add_option("-c", 
1134                          "--certfile", 
1135                          dest="certFile", 
1136                          default=None,
1137                          help="Certificate to be stored")
1138       
1139        self.parser.add_option("-y", 
1140                          "--keyfile", 
1141                          dest="keyFile", 
1142                          default=None,
1143                          help="Private key to be stored")
1144
1145        self.parser.add_option("-C", 
1146                          "--owner-certfile", 
1147                          dest="ownerCertFile", 
1148                          help=\
1149"""Cert. for owner of credentials to be stored.  If omitted, it defaults to
1150$GLOBUS_LOCATION/etc/hostcert.pem.  If this is not readable, the cert. to be
1151stored is set as the owner.""")
1152       
1153        self.parser.add_option("-Y", 
1154                          "--owner-keyfile", 
1155                          dest="ownerKeyFile", 
1156                          default=None,
1157                          help=\
1158"""Private key for owner of credentials to be stored.  If omitted, it defaults
1159to $GLOBUS_LOCATION/etc/hostcert.pem.""")
1160       
1161        self.parser.add_option("-w", 
1162                          "--owner-keyfile-passphrase", 
1163                          dest="ownerPassphrase", 
1164                          default=None,
1165                          help=\
1166                          "Pass-phrase for Private key used for SSL client")
1167
1168        self.parser.add_option("-t", 
1169                               "--proxy_lifetime", 
1170                               dest="lifetime", 
1171                               default=43200, # = 12 hours in seconds
1172                               type="int",
1173                               help=\
1174            "Lifetime of proxies delegated by the server (default 12 hours)")
1175
1176    def runGetDelegation(self):
1177        """Call MyProxyClient.getDelegation"""
1178        self.runLogon()
1179       
1180    def runLogon(self):
1181        """Call MyProxyClient.logon"""
1182        outfile = self.opts.outfile or MyProxyClient.defProxyFile
1183   
1184        # Get MyProxy password
1185        passphrase = getpass.getpass()
1186           
1187        # Retrieve proxy cert
1188        creds = self.myProxy.logon(self.opts.username, 
1189                                   passphrase, 
1190                                   lifetime=self.opts.lifetime)
1191        MyProxyClient.writeProxyFile(*creds)
1192           
1193        print "A proxy has been received for user %s in %s." % \
1194            (self.opts.username, outfile)
1195           
1196    def runChangePassphrase(self):
1197        """Call MyProxyClient.changePassphrase"""
1198       
1199        # Get MyProxy password
1200        passphrase = getpass.getpass(\
1201                     prompt='Enter (current) MyProxy pass phrase: ')
1202       
1203        newPassphrase = getpass.getpass(\
1204                                 prompt='Enter new MyProxy pass phrase: ')
1205       
1206        if newPassphrase != getpass.getpass(\
1207                     prompt='Verifying - Enter new MyProxy pass phrase: '):
1208            raise Exception, "Pass-phrases entered don't match"
1209               
1210        self.myProxy.changePassphrase(username,
1211                             passphrase,
1212                             newPassphrase, 
1213                             self.opts.certFile,
1214                             self.opts.keyFile,
1215                             ownerPassphrase=open('../tmp2').read().strip())           
1216               
1217    def runInfo(self):
1218        """Call MyProxyClient.info"""
1219        if not self.opts.ownerCertFile or not self.opts.ownerKeyFile:
1220#            self.opts.ownerCertFile = MyProxyClient.defProxyFile
1221            # Look for proxy file stored from previous call to logon/
1222            # get-delegation
1223            try:
1224                creds = MyProxyClient.readProxyFile()
1225           
1226                # Copy proxy and user certificates
1227                self.opts.ownerCertFile = './proxy-cert.pem'
1228                self.opts.ownerKeyFile = './proxy-key.pem'
1229                open(self.opts.ownerCertFile, 'w').write(creds[0] + ''.join(creds[2:]))
1230                open(self.opts.ownerKeyFile, 'w').write(creds[1])
1231            except IOError:
1232                # No such file - try proceeding regardless
1233                pass
1234           
1235        credExists, errorTxt, fields = self.myProxy.info(self.opts.username, 
1236                             ownerCertFile=self.opts.ownerCertFile,
1237                             ownerKeyFile=self.opts.ownerKeyFile,
1238                             ownerPassphrase=self.opts.ownerPassphrase)
1239        if errorTxt:
1240            raise CmdLineClientError, "User %s: %s" % (self.opts.username, 
1241                                                       errorTxt)
1242               
1243        elif credExists:
1244            print "username: %s" % self.opts.username
1245            print "owner: %s" % fields['CRED_OWNER']
1246            print "  time left: %d" % \
1247                    (fields['CRED_END_TIME'] - fields['CRED_START_TIME'])
1248        else:
1249            ownerCert = ''#X509.load_cert(self.opts.ownerCertFile)
1250            ownerCertDN = ''#'/' + \
1251                #ownerCert.get_subject().as_text().replace(', ', '/')
1252            print "no credentials found for user %s, owner \"%s\"" % \
1253                (self.opts.username, ownerCertDN)
1254               
1255    def runDestroy(self):
1256        """Call MyProxyClient.destroy"""
1257        self.myProxy.destroy(self.opts.username, 
1258                            ownerCertFile=self.opts.certFile,
1259                            ownerKeyFile=self.opts.keyFile,
1260                            ownerPassphrase=open('../tmp2').read().strip())
1261       
1262    def runStore(self):
1263        self.myProxy.store(self.opts.username, 
1264                          self.opts.certFile,
1265                          self.opts.keyFile,
1266                          ownerCertFile=self.opts.certFile,
1267                          ownerKeyFile=self.opts.keyFile,
1268                          ownerPassphrase=open('./tmp').read().strip(),
1269                          lifetime=self.opts.lifetime)
1270
1271
1272if __name__ == "__main__":
1273    main()
Note: See TracBrowser for help on using the repository browser.