source: TI12-security/trunk/python/Tests/MyProxyClient/m2CryptoMyPxClnt.py @ 1854

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/Tests/MyProxyClient/m2CryptoMyPxClnt.py@1854
Revision 1854, 25.1 KB checked in by pjkersha, 13 years ago (diff)

Working version of info with additional information passed back about
credential owner and start and end times for the credential.

Also, removed debug info code.

  • Property svn:executable set to *
Line 
1#!/usr/bin/env python
2"""MyProxy Client interface
3
4Based on original program myproxy_logon Tom Uram <turam@mcs.anl.gov>
5
6NERC Data Grid Project
7
8@author P J Kershaw 05/12/06
9
10@copyright (C) 2006 CCLRC & NERC
11
12@license This software may be distributed under the terms of the Q Public
13License, version 1.0 or later.
14"""
15
16import os
17import socket
18from M2Crypto import X509, RSA, EVP, m2, BIO
19from M2Crypto.SSL.Context import Context
20from M2Crypto.SSL.Connection import Connection
21
22import re
23import base64
24
25
26class MyProxyClientError(Exception):
27    """Catch all exception class"""
28   
29class GetError(Exception):
30    """Exceptions arising from get request to server"""
31   
32class RetrieveError(Exception):
33    """Error recovering a response from MyProxy"""
34
35
36class MyProxyClient(object):
37    """MyProxy client interface
38   
39    @cvar __getCmd: get command string
40    @cvar __storeCmd: store command string
41    @cvar _certReqParamName: names of parameters needed to generate a
42    certificate request e.g. CN, OU etc.
43    """
44     
45    __getCmd="""VERSION=MYPROXYv2
46COMMAND=0
47USERNAME=%s
48PASSPHRASE=%s
49LIFETIME=%d\0"""
50
51    __infoCmd="""VERSION=MYPROXYv2
52COMMAND=2
53USERNAME=%s
54PASSPHRASE=PASSPHRASE
55LIFETIME=0"""
56 
57    __destroyCmd="""VERSION=MYPROXYv2
58COMMAND=3
59USERNAME=%s
60PASSPHRASE=PASSPHRASE
61LIFETIME=0"""
62   
63    __storeCmd="""VERSION=MYPROXYv2
64COMMAND=5
65USERNAME=%s
66PASSPHRASE=
67LIFETIME=%d\0"""
68
69 
70    _certReqParamName = ('O', 'OU')
71
72    #_________________________________________________________________________           
73    def __init__(self, 
74                 hostname=os.environ.get('MYPROXY_SERVER'), 
75                 port=7512,
76                 **certReqKw):
77        """
78        @param hostname string for MyProxy server - defaults to
79        MYPROXY_SERVER environment variable
80        @param integer port number MyProxy is running on
81        """
82        self.hostname = hostname
83        self.port = port
84       
85        # Set-up parameter names for certificate request
86        self.__certReqParam = {}.fromkeys(MyProxyClient._certReqParamName)
87       
88        # Check for parameter names set from input
89        self.certReqParam = certReqKw
90
91    #_________________________________________________________________________       
92    def __setCertReqParam(self, dict):
93        '''certReqParam property set method - forces setting of certificate
94        request parameter names to valid values
95       
96        @param dict: dictionary of parameters'''
97       
98        invalidKw = [k for k in dict \
99                     if k not in MyProxyClient._certReqParamName]
100        if invalidKw:
101            raise MyProxyClientError, \
102    "Invalid certificate request keyword(s): %s.  Valid keywords are: %s" % \
103    (', '.join(invalidKw), ', '.join(MyProxyClient._certReqParamName))
104   
105        self.__certReqParam.update(dict)
106
107    #_________________________________________________________________________       
108    def __getCertReqParam(self):
109        """certReqParam property set method - for Certificate request
110        parameters dict"""
111        return self.__certReqParam
112   
113   
114    certReqParam = property(fset=__setCertReqParam,
115                            fget=__getCertReqParam,
116                            doc="Dictionary of parameters for cert. request")
117   
118    #_________________________________________________________________________       
119    def _createCertReq(self, CN, nBitsForKey=1024, messageDigest="md5"):
120        """
121        Create a certificate request.
122       
123        @param CN: Common Name for certificate - effectively the same as the
124        username for the MyProxy credential
125        @param nBitsForKey: number of bits for private key generation -
126        default is 1024
127        @param messageDigest: message disgest type - default is MD5
128        @return tuple of certificate request PEM text and private key PEM text
129        """
130       
131        # Check all required certifcate request DN parameters are set               
132        # Create certificate request
133        req = X509.Request()
134   
135        # Generate keys
136        key = RSA.gen_key(nBitsForKey, m2.RSA_F4)
137   
138        # Create public key object
139        pubKey = EVP.PKey()
140        pubKey.assign_rsa(key)
141       
142        # Add the public key to the request
143        req.set_version(0)
144        req.set_pubkey(pubKey)
145       
146        # Set DN
147        x509Name = X509.X509_Name()
148        x509Name.CN = CN
149        x509Name.OU = self.__certReqParam['OU']
150        x509Name.O = self.__certReqParam['O']
151        req.set_subject_name(x509Name)
152       
153        req.sign(pubKey, messageDigest)
154       
155        return (req.as_asn1(), key.as_pem(cipher=None))
156   
157   
158    #_________________________________________________________________________           
159    def _deserializeResponse(self, msg, *fieldNames):
160        """
161        Deserialize a MyProxy server response
162       
163        @param msg: string response message from MyProxy server
164        @*fieldNames: the content of additional fields can be returned by
165        specifying the field name or names as additional arguments e.g. info
166        method passes 'CRED_START_TIME', 'CRED_END_TIME' and 'CRED_OWNER'
167        field names.  The content of fields is returned as an extra element
168        in the tuple response.  This element is itself a dictionary indexed
169        by field name.
170        @return tuple of integer response and errorTxt string (if any)
171        """
172       
173        lines = msg.split('\n')
174       
175        # get response value
176        responselines = filter(lambda x: x.startswith('RESPONSE'), lines)
177        responseline = responselines[0]
178        respCode = int(responseline.split('=')[1])
179       
180        # get error text
181        errorTxt = ""
182        errorlines = filter(lambda x: x.startswith('ERROR'), lines)
183        for e in errorlines:
184            etext = e.split('=', 1)[1]
185            errorTxt += etext
186       
187        if fieldNames:
188            fields = {}
189                       
190            for fieldName in fieldNames:
191                fieldlines = filter(lambda x: x.startswith(fieldName), lines)
192                try:
193                    # Nb. '1' arg to split ensures owner DN is not split up.
194                    field = fieldlines[0].split('=', 1)[1]
195                    fields[fieldName]=field.isdigit() and int(field) or field
196
197                except IndexError:
198                    # Ignore fields that aren't found
199                    pass
200               
201            return respCode, errorTxt, fields
202        else:
203            return respCode, errorTxt
204   
205 
206    #_________________________________________________________________________             
207    def _deserializeCerts(self, inputDat):
208        """Unpack certificates returned from a get delegation call to the
209        server
210       
211        @param inputDat: string containing the proxy cert and private key
212        and signing cert all in DER format
213       
214        @return list containing the equivalent to the input in PEM format"""
215        pemCerts = []       
216        dat = inputDat
217       
218        while dat:   
219            # find start of cert, get length       
220            ind = dat.find('\x30\x82')
221            if ind < 0:
222                break
223               
224            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
225   
226            # extract der-format cert, and convert to pem
227            derCert = dat[ind:ind+len+4]
228           
229            x509 = X509.load_cert_string(derCert, type=X509.TYPE_ASN1)
230            pemCert = x509.as_pem()
231           
232            pemCerts.append(pemCert)
233   
234            # trim cert from data
235            dat = dat[ind + len + 4:]
236           
237        return pemCerts
238
239
240    #_________________________________________________________________________   
241    def info(self,
242             username, 
243             ownerCertFile=None,
244             ownerKeyFile=None,
245             ownerPassphrase=None):
246        """return True/False whether credentials exist on the server for a
247        given username
248       
249        Exceptions:  GetError, StoreCredError
250       
251        @param username: username selected for credential
252        @param ownerCertFile: certificate used for client authentication with
253        the MyProxy server SSL connection.  This ID will be set as the owner
254        of the stored credentials.  Only the owner can later remove
255        credentials with myproxy-destroy or the destroy method.  If not set,
256        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
257        @param ownerKeyFile: corresponding private key file.  See explanation
258        for ownerCertFile
259        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
260        private key is not password protected. 
261        @return none
262        """
263        globusLoc = os.environ.get('GLOBUS_LOCATION')
264        if not ownerCertFile or not ownerKeyFile:
265            if globusLoc:
266                ownerCertFile = os.path.join(globusLoc, 'etc', 'hostcert.pem')
267                ownerKeyFile = os.path.join(globusLoc, 'etc', 'hostkey.pem')
268            else:
269                raise MyProxyClientError, \
270            "No client authentication cert. and private key file were given"
271       
272        import pdb;pdb.set_trace()
273        context = Context(protocol='sslv3')
274        context.load_cert(ownerCertFile,
275                          keyfile=ownerKeyFile,
276                          callback=lambda *ar, **kw: ownerPassphrase)
277   
278        # Disable for compatibility with myproxy server (er, globus)
279        # globus doesn't handle this case, apparently, and instead
280        # chokes in proxy delegation code
281        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
282       
283        # connect to myproxy server
284        conn = Connection(context, sock=socket.socket())
285       
286        # Fudge to avoid checking client cert - seems to pick globus
287        # host/<hostname> one
288        conn.clientPostConnectionCheck = None
289        conn.connect((self.hostname, self.port))
290       
291        # send globus compatibility stuff
292        conn.write('0')
293   
294        # send info command
295        cmd = MyProxyClient.__infoCmd % (username)
296        conn.write(cmd)
297   
298        # process server response
299        dat = conn.recv(8192)
300         
301        # Pass in the names of fields to return in the dictionary 'field'
302        respCode, errorTxt, field = self._deserializeResponse(dat, 
303                                                         'CRED_START_TIME', 
304                                                         'CRED_END_TIME', 
305                                                         'CRED_OWNER')
306
307        return not bool(respCode), errorTxt, field
308
309
310    #_________________________________________________________________________   
311    def destroy(self,
312                username, 
313                ownerCertFile=None,
314                ownerKeyFile=None,
315                ownerPassphrase=None):
316        """destroy credentials from the server for a given username
317       
318        Exceptions:  GetError, StoreCredError
319       
320        @param username: username selected for credential
321        @param ownerCertFile: certificate used for client authentication with
322        the MyProxy server SSL connection.  This ID will be set as the owner
323        of the stored credentials.  Only the owner can later remove
324        credentials with myproxy-destroy or the destroy method.  If not set,
325        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem
326        @param ownerKeyFile: corresponding private key file.  See explanation
327        for ownerCertFile
328        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
329        private key is not password protected. 
330        @return none
331        """
332        globusLoc = os.environ.get('GLOBUS_LOCATION')
333        if not ownerCertFile or not ownerKeyFile:
334            if globusLoc:
335                ownerCertFile = os.path.join(globusLoc, 'etc', 'hostcert.pem')
336                ownerKeyFile = os.path.join(globusLoc, 'etc', 'hostkey.pem')
337            else:
338                raise MyProxyClientError, \
339            "No client authentication cert. and private key file were given"
340       
341        import pdb;pdb.set_trace()
342        context = Context(protocol='sslv3')
343        context.load_cert(ownerCertFile,
344                          keyfile=ownerKeyFile,
345                          callback=lambda *ar, **kw: ownerPassphrase)
346   
347        # Disable for compatibility with myproxy server (er, globus)
348        # globus doesn't handle this case, apparently, and instead
349        # chokes in proxy delegation code
350        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
351       
352        # connect to myproxy server
353        conn = Connection(context, sock=socket.socket())
354       
355        # Fudge to avoid checking client cert - seems to pick globus
356        # host/<hostname> one
357        conn.clientPostConnectionCheck = None
358        conn.connect((self.hostname, self.port))
359       
360        # send globus compatibility stuff
361        conn.write('0')
362   
363        # send store command
364        cmd = MyProxyClient.__destroyCmd % (username)
365        conn.write(cmd)
366   
367        # process server response
368        dat = conn.recv(8192)
369           
370        respCode, errorTxt = self._deserializeResponse(dat)
371        if respCode:
372            raise GetError, errorTxt
373
374
375    #_________________________________________________________________________   
376    def store(self,
377              username, 
378              certFile,
379              keyFile,
380              ownerCertFile=None,
381              ownerKeyFile=None,
382              ownerPassphrase=None,
383              lifetime=43200):
384        """Upload credentials to the server
385       
386        Exceptions:  GetError, StoreCredError
387       
388        @param username: username selected for credential
389        @param certFile: user's X.509 certificate in PEM format
390        @param keyFile: equivalent private key file in PEM format
391        @param ownerCertFile: certificate used for client authentication with
392        the MyProxy server SSL connection.  This ID will be set as the owner
393        of the stored credentials.  Only the owner can later remove
394        credentials with myproxy-destroy or the destroy method.  If not set,
395        this argument defaults to $GLOBUS_LOCATION/etc/hostcert.pem or if this
396        is not set, certFile
397        @param ownerKeyFile: corresponding private key file.  See explanation
398        for ownerCertFile
399        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
400        private key is not password protected.  Nb. keyFile is expected to
401        be passphrase protected as this will be the passphrase used for
402        logon / getDelegation.
403        @return none
404        """
405        globusLoc = os.environ.get('GLOBUS_LOCATION')
406        if not ownerCertFile or not ownerKeyFile:
407            if globusLoc:
408                ownerCertFile = os.path.join(globusLoc, 'etc', 'hostcert.pem')
409                ownerKeyFile = os.path.join(globusLoc, 'etc', 'hostkey.pem')
410            else:
411                ownerCertFile = certFile
412                ownerKeyFile = keyFile
413       
414        import pdb;pdb.set_trace()
415        context = Context(protocol='sslv3')
416        context.load_cert(ownerCertFile,
417                          keyfile=ownerKeyFile,
418                          callback=lambda *ar, **kw: ownerPassphrase)
419#        context.load_cert('../hostcert.pem',
420#                          keyfile='../hostkey.pem',
421#                          callback=lambda *ar, **kw: ownerPassphrase)
422   
423        # Disable for compatibility with myproxy server (er, globus)
424        # globus doesn't handle this case, apparently, and instead
425        # chokes in proxy delegation code
426        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
427       
428        # connect to myproxy server
429        conn = Connection(context, sock=socket.socket())
430       
431        # Fudge to avoid checking client cert - seems to pick globus
432        # host/<hostname> one
433        conn.clientPostConnectionCheck = None
434        conn.connect((self.hostname, self.port))
435       
436        # send globus compatibility stuff
437        conn.write('0')
438   
439        # send store command
440        cmd = MyProxyClient.__storeCmd % (username, lifetime)
441        conn.write(cmd)
442   
443        # process server response
444        dat = conn.recv(8192)
445           
446        respCode, errorTxt = self._deserializeResponse(dat)
447        if respCode:
448            raise GetError, errorTxt
449       
450        # Send certificate and private key
451        certTxt = X509.load_cert(certFile).as_pem()
452        keyTxt = open(keyFile).read()
453       
454        conn.send(certTxt + keyTxt)
455   
456   
457        # process server response
458        resp = conn.recv(8192)
459        respCode, errorTxt = self._deserializeResponse(resp)
460        if respCode:
461            raise RetrieveError, errorTxt
462       
463    #_________________________________________________________________________           
464    def logon(self, username, passphrase, lifetime=43200):
465        """Retrieve a proxy credential from a MyProxy server
466       
467        Exceptions:  GetError, RetrieveError
468       
469        @param username: username of credential
470        @param passphrase: pass-phrase for private key of credential held on
471        server
472        @return list containing the credentials as strings in PEM format: the
473        proxy certificate, it's private key and the signing certificate.
474        """
475   
476        context = Context(protocol='sslv3')
477       
478        # disable for compatibility with myproxy server (er, globus)
479        # globus doesn't handle this case, apparently, and instead
480        # chokes in proxy delegation code
481        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
482       
483        # connect to myproxy server
484        conn = Connection(context, sock=socket.socket())
485       
486        # Fudge to avoid checking client cert - seems to pick globus
487        # host/<hostname> one
488        conn.clientPostConnectionCheck = None
489        conn.connect((self.hostname, self.port))
490       
491        # send globus compatibility stuff
492        conn.write('0')
493   
494        # send get command
495        cmd = MyProxyClient.__getCmd % (username,passphrase,lifetime)
496        conn.write(cmd)
497   
498        # process server response
499        dat = conn.recv(8192)
500        respCode, errorTxt = self._deserializeResponse(dat)
501        if respCode:
502            raise GetError, errorTxt
503       
504        # generate and send certificate request
505        # - The client will generate a public/private key pair and send a
506        #   NULL-terminated PKCS#10 certificate request to the server.
507        certReq, priKey = self._createCertReq(username)
508        conn.send(certReq)
509   
510        # process certificates
511        # - 1 byte , number of certs
512        dat = conn.recv(1)
513        nCerts = ord(dat[0])
514       
515        # - n certs
516        dat = conn.recv(8192)
517   
518        # process server response
519        resp = conn.recv(8192)
520        respCode, errorTxt = self._deserializeResponse(resp)
521        if respCode:
522            raise RetrieveError, errorTxt
523   
524        # deserialize certs from received cert data
525        pemCerts = self._deserializeCerts(dat)
526        if len(pemCerts) != nCerts:
527            RetrieveError, "%d certs expected, %d received" % \
528                                                    (nCerts, len(pemCerts))
529   
530        # write certs and private key to file
531        # - proxy cert
532        # - private key
533        # - rest of cert chain
534        creds = pemCerts[0]+priKey+''.join([cert for cert in pemCerts[1:]])
535       
536        return creds
537       
538
539    def getDelegation(self, *arg, **kw):
540        """Retrieve proxy cert for user - same as logon"""
541        self.logon(*arg, **kw)
542
543
544#_____________________________________________________________________________   
545def main():
546    import sys
547    import optparse
548    import getpass
549   
550    parser = optparse.OptionParser()
551    parser.add_option("-i", 
552                      "--info", 
553                      dest="info", 
554                      default=False,
555                      action="store_true",
556                      help="check whether a credential exists")
557
558    parser.add_option("-z", 
559                      "--destroy", 
560                      dest="destroy", 
561                      default=False,
562                      action="store_true",
563                      help="destroy credential")
564
565    parser.add_option("-g", 
566                      "--get-delegation", 
567                      dest="getDelegation", 
568                      default=False,
569                      action="store_true",
570                      help="Get delegation / logon")
571   
572    parser.add_option("-c", 
573                      "--certfile", 
574                      dest="certFile", 
575                      default=None,
576                      help="Certificate to be stored")
577   
578    parser.add_option("-y", 
579                      "--keyfile", 
580                      dest="keyFile", 
581                      default=None,
582                      help="Private key to be stored")
583   
584    parser.add_option("-w", 
585                      "--keyfile-passphrase", 
586                      dest="ownerPassphrase", 
587                      default=None,
588                      help="Pass-phrase for Private key used for SSL client")
589
590    parser.add_option("-s", 
591                      "--pshost", 
592                      dest="host", 
593                      help="The hostname of the MyProxy server to contact")
594   
595    parser.add_option("-p", 
596                      "--psport", 
597                      dest="port", 
598                      default=7512,
599                      type="int",
600                      help="The port of the MyProxy server to contact")
601   
602    parser.add_option("-l", 
603                      "--username", 
604                      dest="username", 
605                      help=\
606    "The username with which the credential is stored on the MyProxy server")
607
608    parser.add_option("-o", 
609                      "--out", 
610                      dest="outfile", 
611                      help=\
612    "The username with which the credential is stored on the MyProxy server")
613
614    parser.add_option("-t", 
615                      "--proxy-lifetime", 
616                      dest="lifetime", 
617                      default=43200,
618                      type="int",
619                      help=\
620    "The username with which the credential is stored on the MyProxy server")
621
622    (options, args) = parser.parse_args()
623   
624
625    # process options   
626    username = options.username
627    if not username:
628        if sys.platform == 'win32':
629            username = os.environ["USERNAME"]
630        else:
631            import pwd
632            username = pwd.getpwuid(os.geteuid())[0]
633
634    hostname = options.host or os.environ.get('MYPROXY_SERVER')
635    myProxy = MyProxyClient(hostname=hostname,
636                            port=options.port,
637                            O='NDG',
638                            OU='BADC')
639   
640    if options.getDelegation:
641               
642        outfile = options.outfile
643        if not outfile:
644            if sys.platform == 'win32':
645                outfile = 'proxy'
646            elif sys.platform in ['linux2','darwin']:
647                outfile = '/tmp/x509up_u%s' % (os.getuid())
648   
649        # Get MyProxy password
650        passphrase = getpass.getpass()
651           
652        # Retrieve proxy cert
653        try:
654            creds = myProxy.logon(username, 
655                                  passphrase, 
656                                  lifetime=options.lifetime)
657            open(outfile, 'w').write(creds)
658            print "A proxy has been received for user %s in %s." % \
659                (username, outfile)
660           
661        except Exception,e:
662            print "Error:", e
663            sys.exit(1)
664               
665    elif options.info:
666        try:
667            credExists, errorTxt, fields = myProxy.info(username, 
668                             options.certFile,
669                             options.keyFile,
670                             ownerPassphrase=open('../tmp2').read().strip())
671            if credExists:
672                print "username: %s" % username
673                print "owner: %s" % fields['CRED_OWNER']
674                print "  time left: %d" % \
675                        (fields['CRED_END_TIME'] - fields['CRED_START_TIME'])
676            else:
677                ownerCert = X509.load_cert(options.certFile)
678                ownerCertDN = '/' + \
679                    ownerCert.get_subject().as_text().replace(', ', '/')
680                print "no credentials found for user %s, owner \"%s\"" % \
681                    (username, ownerCertDN)
682
683        except Exception, e:
684            print "Error:", e
685            sys.exit(1)
686               
687    elif options.destroy:
688        try:
689            myProxy.destroy(username, 
690                            ownerCertFile=options.certFile,
691                            ownerKeyFile=options.keyFile,
692                            ownerPassphrase=open('../tmp2').read().strip())
693           
694        except Exception, e:
695            print "Error:", e
696            sys.exit(1)
697    else:
698        try:
699            myProxy.store(username, 
700                          options.certFile,
701                          options.keyFile,
702                          ownerCertFile=options.certFile,
703                          ownerKeyFile=options.keyFile,
704                          ownerPassphrase=open('../tmp2').read().strip(),
705                          lifetime=options.lifetime)
706           
707        except Exception, e:
708            print "Error:", e
709            sys.exit(1)
710
711
712if __name__ == '__main__':
713    main()
714   
Note: See TracBrowser for help on using the repository browser.