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

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

Set default hostname in main and also temporarily set the input cert/key as
the ones used for SSL client authentication.

  • 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
36debug = 0
37def debuglevel(level):
38    global debug
39    return debug >= level
40
41class MyProxyClient(object):
42    """MyProxy client interface
43   
44    @cvar __getCmd: get command string
45    @cvar __storeCmd: store command string
46    @cvar _certReqParamName: names of parameters needed to generate a
47    certificate request e.g. CN, OU etc.
48    """
49     
50    __getCmd="""VERSION=MYPROXYv2
51COMMAND=0
52USERNAME=%s
53PASSPHRASE=%s
54LIFETIME=%d\0"""
55   
56    __storeCmd="""VERSION=MYPROXYv2
57COMMAND=5
58USERNAME=%s
59PASSPHRASE=
60LIFETIME=%d\0"""
61
62    _certReqParamName = ('O', 'OU')
63
64    #_________________________________________________________________________           
65    def __init__(self, 
66                 hostname=os.environ.get('MYPROXY_SERVER'), 
67                 port=7512,
68                 **certReqKw):
69        """
70        @param hostname string for MyProxy server - defaults to
71        MYPROXY_SERVER environment variable
72        @param integer port number MyProxy is running on
73        """
74        self.hostname = hostname
75        self.port = port
76       
77        # Set-up parameter names for certificate request
78        self.__certReqParam = {}.fromkeys(MyProxyClient._certReqParamName)
79       
80        # Check for parameter names set from input
81        self.certReqParam = certReqKw
82
83    #_________________________________________________________________________       
84    def __setCertReqParam(self, dict):
85        '''certReqParam property set method - forces setting of certificate
86        request parameter names to valid values
87       
88        @param dict: dictionary of parameters'''
89       
90        invalidKw = [k for k in dict \
91                     if k not in MyProxyClient._certReqParamName]
92        if invalidKw:
93            raise MyProxyClientError, \
94    "Invalid certificate request keyword(s): %s.  Valid keywords are: %s" % \
95    (', '.join(invalidKw), ', '.join(MyProxyClient._certReqParamName))
96   
97        self.__certReqParam.update(dict)
98
99    #_________________________________________________________________________       
100    def __getCertReqParam(self):
101        """certReqParam property set method - for Certificate request
102        parameters dict"""
103        return self.__certReqParam
104   
105   
106    certReqParam = property(fset=__setCertReqParam,
107                            fget=__getCertReqParam,
108                            doc="Dictionary of parameters for cert. request")
109   
110    #_________________________________________________________________________       
111    def _createCertReq(self, CN, nBitsForKey=1024, messageDigest="md5"):
112        """
113        Create a certificate request.
114       
115        @param CN: Common Name for certificate - effectively the same as the
116        username for the MyProxy credential
117        @param nBitsForKey: number of bits for private key generation -
118        default is 1024
119        @param messageDigest: message disgest type - default is MD5
120        @return tuple of certificate request PEM text and private key PEM text
121        """
122       
123        # Check all required certifcate request DN parameters are set               
124        # Create certificate request
125        req = X509.Request()
126   
127        # Generate keys
128        key = RSA.gen_key(nBitsForKey, m2.RSA_F4)
129   
130        # Create public key object
131        pubKey = EVP.PKey()
132        pubKey.assign_rsa(key)
133       
134        # Add the public key to the request
135        req.set_version(0)
136        req.set_pubkey(pubKey)
137       
138        # Set DN
139        x509Name = X509.X509_Name()
140        x509Name.CN = CN
141        x509Name.OU = self.__certReqParam['OU']
142        x509Name.O = self.__certReqParam['O']
143        req.set_subject_name(x509Name)
144       
145        req.sign(pubKey, messageDigest)
146       
147        return (req.as_asn1(), key.as_pem(cipher=None))
148   
149   
150    #_________________________________________________________________________           
151    def _deserializeResponse(self, msg):
152        """
153        Deserialize a MyProxy server response
154       
155        @param msg: string response message from MyProxy server
156        @return tuple of integer response and errorTxt string (if any)
157        """
158       
159        lines = msg.split('\n')
160       
161        # get response value
162        responselines = filter(lambda x: x.startswith('RESPONSE'), lines)
163        responseline = responselines[0]
164        respCode = int(responseline.split('=')[1])
165       
166        # get error text
167        errorTxt = ""
168        errorlines = filter(lambda x: x.startswith('ERROR'), lines)
169        for e in errorlines:
170            etext = e.split('=')[1]
171            errorTxt += etext
172       
173        return respCode, errorTxt
174     
175 
176    #_________________________________________________________________________             
177    def _deserializeCerts(self, inputDat):
178        """Unpack certificates returned from a get delegation call to the
179        server
180       
181        @param inputDat: string containing the proxy cert and private key
182        and signing cert all in DER format
183       
184        @return list containing the equivalent to the input in PEM format"""
185        pemCerts = []       
186        dat = inputDat
187       
188        while dat:   
189            # find start of cert, get length       
190            ind = dat.find('\x30\x82')
191            if ind < 0:
192                break
193               
194            len = 256*ord(dat[ind+2]) + ord(dat[ind+3])
195   
196            # extract der-format cert, and convert to pem
197            derCert = dat[ind:ind+len+4]
198           
199            x509 = X509.load_cert_string(derCert, type=X509.TYPE_ASN1)
200            pemCert = x509.as_pem()
201           
202            pemCerts.append(pemCert)
203   
204            # trim cert from data
205            dat = dat[ind + len + 4:]
206           
207        return pemCerts
208
209
210    #_________________________________________________________________________   
211    def store(self,
212              username, 
213              certFile,
214              keyFile,
215              ownerCertFile=None,
216              ownerKeyFile=None,
217              ownerPassphrase=None,
218              lifetime=43200):
219        """Upload credentials to the server
220       
221        Exceptions:  GetError, StoreCredError
222       
223        @param username: username selected for credential
224        @param certFile: user's X.509 certificate in PEM format
225        @param keyFile: equivalent private key file in PEM format
226        @param ownerCertFile: certificate used for client authentication with
227        the MyProxy server SSL connection.  This ID will be set as the owner
228        of the stored credentials.  Only the owner can later remove
229        credentials with myproxy-destroy or the destroy method.  If not set,
230        this argument defaults to certFile
231        @param ownerKeyFile: corresponding private key file.  See explanation
232        for ownerCertFile
233        @param ownerPassphrase: passphrase for ownerKeyFile.  Omit if the
234        private key is not password protected.  Nb. keyFile is expected to
235        be passphrase protected as this will be the passphrase used for
236        logon / getDelegation.
237        @return none
238        """
239        globusLoc = os.environ.get('GLOBUS_LOCATION')
240        if not ownerCertFile or not ownerKeyFile:
241            if globusLoc:
242                ownerCertFile = os.path.join(globusLoc, 'etc', 'hostcert.pem')
243                ownerKeyFile = os.path.join(globusLoc, 'etc', 'hostkey.pem')
244            else:
245                ownerCertFile = certFile
246                ownerKeyFile = keyFile
247       
248        import pdb;pdb.set_trace()
249        context = Context(protocol='sslv3')
250        context.load_cert(ownerCertFile,
251                          keyfile=ownerKeyFile,
252                          callback=lambda *ar, **kw: ownerPassphrase)
253   
254        # Disable for compatibility with myproxy server (er, globus)
255        # globus doesn't handle this case, apparently, and instead
256        # chokes in proxy delegation code
257        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
258       
259        # connect to myproxy server
260        if debuglevel(1):   print "debug: connect to myproxy server"
261        conn = Connection(context, sock=socket.socket())
262       
263        # Fudge to avoid checking client cert - seems to pick globus
264        # host/<hostname> one
265        conn.clientPostConnectionCheck = None
266        conn.connect((self.hostname, self.port))
267       
268        # send globus compatibility stuff
269        if debuglevel(1):   
270            print "debug: send globus compat byte"
271        conn.write('0')
272   
273        # send store command
274        if debuglevel(1): 
275            print "debug: send store command"
276           
277        storeCmd = MyProxyClient.__storeCmd % (username, lifetime)
278        conn.write(storeCmd)
279   
280        # process server response
281        if debuglevel(1):   
282            print "debug: get server response for store command request"
283           
284        dat = conn.recv(8192)
285        if debuglevel(1):   
286            print dat
287           
288        respCode, errorTxt = self._deserializeResponse(dat)
289        if respCode:
290            raise GetError, errorTxt
291        else:
292            if debuglevel(1):   
293                print "debug: server response ok"
294       
295        # Send certificate and private key
296        certTxt = X509.load_cert(certFile).as_pem()
297        keyTxt = open(keyFile).read()
298       
299        conn.send(certTxt + keyTxt)
300   
301   
302        # process server response
303        if debuglevel(1):   
304            print "debug: get server response for store command completed"
305        resp = conn.recv(8192)
306        respCode, errorTxt = self._deserializeResponse(resp)
307        if respCode:
308            raise RetrieveError, errorTxt
309        else:
310            if debuglevel(1):
311                print "debug: server response ok"
312       
313       
314    def logon(self, username, passphrase, lifetime=43200):
315        """Retrieve a proxy credential from a MyProxy server
316       
317        Exceptions:  GetError, RetrieveError
318       
319        @param username: username of credential
320        @param passphrase: pass-phrase for private key of credential held on
321        server
322        @return list containing the credentials as strings in PEM format: the
323        proxy certificate, it's private key and the signing certificate.
324        """
325   
326        context = Context(protocol='sslv3')
327       
328        # disable for compatibility with myproxy server (er, globus)
329        # globus doesn't handle this case, apparently, and instead
330        # chokes in proxy delegation code
331        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
332       
333        # connect to myproxy server
334        if debuglevel(1):   print "debug: connect to myproxy server"
335        conn = Connection(context, sock=socket.socket())
336       
337        # Fudge to avoid checking client cert - seems to pick globus
338        # host/<hostname> one
339        conn.clientPostConnectionCheck = None
340        conn.connect((self.hostname, self.port))
341       
342        # send globus compatibility stuff
343        if debuglevel(1):   print "debug: send globus compat byte"
344        conn.write('0')
345   
346        # send get command
347        if debuglevel(1):   print "debug: send get command"
348        getCmd = MyProxyClient.__getCmd % (username,passphrase,lifetime)
349        conn.write(getCmd)
350   
351        # process server response
352        if debuglevel(1):   print "debug: get server response"
353        dat = conn.recv(8192)
354        if debuglevel(1):   print dat
355        respCode, errorTxt = self._deserializeResponse(dat)
356        if respCode:
357            raise GetError, errorTxt
358        else:
359            if debuglevel(1):   print "debug: server response ok"
360       
361        # generate and send certificate request
362        # - The client will generate a public/private key pair and send a
363        #   NULL-terminated PKCS#10 certificate request to the server.
364        if debuglevel(1):   print "debug: send cert request"
365   
366        certReq, priKey = self._createCertReq(username)
367        conn.send(certReq)
368   
369        # process certificates
370        # - 1 byte , number of certs
371        dat = conn.recv(1)
372        nCerts = ord(dat[0])
373       
374        # - n certs
375        if debuglevel(1):   print "debug: receive certs"
376        dat = conn.recv(8192)
377        if debuglevel(2):
378            print "debug: dumping cert data to myproxy.dump"
379            f = file('myproxy.dump','w')
380            f.write(dat)
381            f.close()
382   
383        # process server response
384        if debuglevel(1):   print "debug: get server response"
385        resp = conn.recv(8192)
386        respCode, errorTxt = self._deserializeResponse(resp)
387        if respCode:
388            raise RetrieveError, errorTxt
389        else:
390            if debuglevel(1):   print "debug: server response ok"
391   
392        # deserialize certs from received cert data
393        pemCerts = self._deserializeCerts(dat)
394        if len(pemCerts) != nCerts:
395            RetrieveError, "%d certs expected, %d received" % \
396                                                    (nCerts, len(pemCerts))
397   
398        # write certs and private key to file
399        # - proxy cert
400        # - private key
401        # - rest of cert chain
402        if debuglevel(1):   print "debug: write proxy and certs to",outfile
403       
404        creds = pemCerts[0]+priKey+''.join([cert for cert in pemCerts[1:]])
405       
406        return creds
407       
408
409    def getDelegation(self, *arg, **kw):
410        """Retrieve proxy cert for user - same as logon"""
411        self.logon(*arg, **kw)
412
413
414#_____________________________________________________________________________   
415def main():
416    import sys
417    import optparse
418    import getpass
419   
420    parser = optparse.OptionParser()
421    parser.add_option("-g", 
422                      "--get-delegation", 
423                      dest="getDelegation", 
424                      default=False,
425                      action="store_true",
426                      help="Get delegation / logon")
427   
428    parser.add_option("-c", 
429                      "--certfile", 
430                      dest="certFile", 
431                      default=None,
432                      help="Certificate to be stored")
433   
434    parser.add_option("-y", 
435                      "--keyfile", 
436                      dest="keyFile", 
437                      default=None,
438                      help="Private key to be stored")
439
440    parser.add_option("-s", 
441                      "--pshost", 
442                      dest="host", 
443                      help="The hostname of the MyProxy server to contact")
444   
445    parser.add_option("-p", 
446                      "--psport", 
447                      dest="port", 
448                      default=7512,
449                      type="int",
450                      help="The port of the MyProxy server to contact")
451   
452    parser.add_option("-l", 
453                      "--username", 
454                      dest="username", 
455                      help=\
456    "The username with which the credential is stored on the MyProxy server")
457
458    parser.add_option("-o", 
459                      "--out", 
460                      dest="outfile", 
461                      help=\
462    "The username with which the credential is stored on the MyProxy server")
463
464    parser.add_option("-t", 
465                      "--proxy-lifetime", 
466                      dest="lifetime", 
467                      default=43200,
468                      type="int",
469                      help=\
470    "The username with which the credential is stored on the MyProxy server")
471
472    parser.add_option("-d", 
473                      "--debug", 
474                      dest="debug", 
475                      type="int",
476                      default=0,
477                      help=\
478"Debug mode: 1=print debug info; 2=print as in (1), and dump data to myproxy.dump")
479
480    (options, args) = parser.parse_args()
481   
482    debug = options.debug
483   
484    # process options   
485    username = options.username
486    if not username:
487        if sys.platform == 'win32':
488            username = os.environ["USERNAME"]
489        else:
490            import pwd
491            username = pwd.getpwuid(os.geteuid())[0]
492
493    hostname = options.host or os.environ.get('MYPROXY_SERVER')
494    myProxy = MyProxyClient(hostname=hostname,
495                            port=options.port,
496                            O='NDG',
497                            OU='BADC')
498   
499    if options.getDelegation:
500               
501        outfile = options.outfile
502        if not outfile:
503            if sys.platform == 'win32':
504                outfile = 'proxy'
505            elif sys.platform in ['linux2','darwin']:
506                outfile = '/tmp/x509up_u%s' % (os.getuid())
507   
508        # Get MyProxy password
509        passphrase = getpass.getpass()
510           
511        # Retrieve proxy cert
512        try:
513            creds = myProxy.logon(username, 
514                                  passphrase, 
515                                  lifetime=options.lifetime)
516            open(outfile, 'w').write(creds)
517            print "A proxy has been received for user %s in %s." % \
518                (username, outfile)
519           
520        except Exception,e:
521            if debuglevel(1):
522                import traceback
523                traceback.print_exc()
524            else:
525                print "Error:", e
526    else:
527        try:
528            myProxy.store(username, 
529                          options.certFile,
530                          options.keyFile,
531                          ownerCertFile=options.certFile,
532                          ownerKeyFile=options.keyFile,
533                          ownerPassphrase=open('../tmp2').read().strip(),
534                          lifetime=options.lifetime)
535           
536        except Exception, e:
537            if debuglevel(1):
538                import traceback
539                traceback.print_exc()
540            else:
541                import traceback
542                traceback.print_exc()
543                print "Error:", e
544
545
546if __name__ == '__main__':
547    main()
Note: See TracBrowser for help on using the repository browser.