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

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

ndg.security.server.MyProxy?: added CmdLineClient? class for MyProxy? client script. Part complete.

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