source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/authnservice/myproxy.py @ 4306

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/authnservice/myproxy.py@4306
Revision 4306, 47.8 KB checked in by pjkersha, 11 years ago (diff)

Renamed MyProxy?.py -> myproxy.py

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