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

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

Refactored ready for integration into ndg.security.server.MyProxy?. TODO: write destroy method and possibly
add info and change password utilities. pyOpenSSL refs are not completely removed so that M2Crypto is needed
only. This required customisations to M2Crypto - addition of type keyword to load_cert_string, load_cert_bio
and load_cert.

  • 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        ownerCertFile = ownerCertFile or certFile
240        ownerKeyFile = ownerKeyFile or keyFile
241       
242        import pdb;pdb.set_trace()
243        context = Context(protocol='sslv3')
244        context.load_cert(ownerCertFile,
245                          keyfile=ownerKeyFile,
246                          callback=lambda *ar, **kw: passphrase)
247   
248        # Disable for compatibility with myproxy server (er, globus)
249        # globus doesn't handle this case, apparently, and instead
250        # chokes in proxy delegation code
251        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
252       
253        # connect to myproxy server
254        if debuglevel(1):   print "debug: connect to myproxy server"
255        conn = Connection(context, sock=socket.socket())
256       
257        # Fudge to avoid checking client cert - seems to pick globus
258        # host/<hostname> one
259        conn.clientPostConnectionCheck = None
260        conn.connect((self.hostname, self.port))
261       
262        # send globus compatibility stuff
263        if debuglevel(1):   
264            print "debug: send globus compat byte"
265        conn.write('0')
266   
267        # send store command
268        if debuglevel(1): 
269            print "debug: send store command"
270           
271        storeCmd = MyProxyClient.__storeCmd % (username, lifetime)
272        conn.write(storeCmd)
273   
274        # process server response
275        if debuglevel(1):   
276            print "debug: get server response for store command request"
277           
278        dat = conn.recv(8192)
279        if debuglevel(1):   
280            print dat
281           
282        respCode, errorTxt = self._deserializeResponse(dat)
283        if respCode:
284            raise GetError, errorTxt
285        else:
286            if debuglevel(1):   
287                print "debug: server response ok"
288       
289        # Send certificate and private key
290        certTxt = X509.load_cert(certFile).as_pem()
291        keyTxt = open(keyFile).read()
292       
293        conn.send(certTxt + keyTxt)
294   
295   
296        # process server response
297        if debuglevel(1):   
298            print "debug: get server response for store command completed"
299        resp = conn.recv(8192)
300        respCode, errorTxt = self._deserializeResponse(resp)
301        if respCode:
302            raise RetrieveError, errorTxt
303        else:
304            if debuglevel(1):
305                print "debug: server response ok"
306       
307       
308    def logon(self, username, passphrase, lifetime=43200):
309        """Retrieve a proxy credential from a MyProxy server
310       
311        Exceptions:  GetError, RetrieveError
312       
313        @param username: username of credential
314        @param passphrase: pass-phrase for private key of credential held on
315        server
316        @return list containing the credentials as strings in PEM format: the
317        proxy certificate, it's private key and the signing certificate.
318        """
319   
320        context = Context(protocol='sslv3')
321       
322        # disable for compatibility with myproxy server (er, globus)
323        # globus doesn't handle this case, apparently, and instead
324        # chokes in proxy delegation code
325        context.set_options(m2.SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS)
326       
327        # connect to myproxy server
328        if debuglevel(1):   print "debug: connect to myproxy server"
329        conn = Connection(context, sock=socket.socket())
330       
331        # Fudge to avoid checking client cert - seems to pick globus
332        # host/<hostname> one
333        conn.clientPostConnectionCheck = None
334        conn.connect((self.hostname, self.port))
335       
336        # send globus compatibility stuff
337        if debuglevel(1):   print "debug: send globus compat byte"
338        conn.write('0')
339   
340        # send get command
341        if debuglevel(1):   print "debug: send get command"
342        getCmd = MyProxyClient.__getCmd % (username,passphrase,lifetime)
343        conn.write(getCmd)
344   
345        # process server response
346        if debuglevel(1):   print "debug: get server response"
347        dat = conn.recv(8192)
348        if debuglevel(1):   print dat
349        respCode, errorTxt = self._deserializeResponse(dat)
350        if respCode:
351            raise GetError, errorTxt
352        else:
353            if debuglevel(1):   print "debug: server response ok"
354       
355        # generate and send certificate request
356        # - The client will generate a public/private key pair and send a
357        #   NULL-terminated PKCS#10 certificate request to the server.
358        if debuglevel(1):   print "debug: send cert request"
359   
360        certReq, priKey = self._createCertReq(username)
361        conn.send(certReq)
362   
363        # process certificates
364        # - 1 byte , number of certs
365        dat = conn.recv(1)
366        nCerts = ord(dat[0])
367       
368        # - n certs
369        if debuglevel(1):   print "debug: receive certs"
370        dat = conn.recv(8192)
371        if debuglevel(2):
372            print "debug: dumping cert data to myproxy.dump"
373            f = file('myproxy.dump','w')
374            f.write(dat)
375            f.close()
376   
377        # process server response
378        if debuglevel(1):   print "debug: get server response"
379        resp = conn.recv(8192)
380        respCode, errorTxt = self._deserializeResponse(resp)
381        if respCode:
382            raise RetrieveError, errorTxt
383        else:
384            if debuglevel(1):   print "debug: server response ok"
385   
386        # deserialize certs from received cert data
387        pemCerts = self._deserializeCerts(dat)
388        if len(pemCerts) != nCerts:
389            RetrieveError, "%d certs expected, %d received" % \
390                                                    (nCerts, len(pemCerts))
391   
392        # write certs and private key to file
393        # - proxy cert
394        # - private key
395        # - rest of cert chain
396        if debuglevel(1):   print "debug: write proxy and certs to",outfile
397       
398        creds = pemCerts[0]+priKey+''.join([cert for cert in pemCerts[1:]])
399       
400        return creds
401       
402
403    def getDelegation(self, *arg, **kw):
404        """Retrieve proxy cert for user - same as logon"""
405        self.logon(*arg, **kw)
406
407
408#_____________________________________________________________________________   
409def main():
410    import sys
411    import optparse
412    import getpass
413   
414    parser = optparse.OptionParser()
415    parser.add_option("-g", 
416                      "--get-delegation", 
417                      dest="getDelegation", 
418                      default=False,
419                      action="store_true",
420                      help="Get delegation / logon")
421   
422    parser.add_option("-c", 
423                      "--certfile", 
424                      dest="certFile", 
425                      default=None,
426                      help="Certificate to be stored")
427   
428    parser.add_option("-y", 
429                      "--keyfile", 
430                      dest="keyFile", 
431                      default=None,
432                      help="Private key to be stored")
433
434    parser.add_option("-s", 
435                      "--pshost", 
436                      dest="host", 
437                      help="The hostname of the MyProxy server to contact")
438   
439    parser.add_option("-p", 
440                      "--psport", 
441                      dest="port", 
442                      default=7512,
443                      type="int",
444                      help="The port of the MyProxy server to contact")
445   
446    parser.add_option("-l", 
447                      "--username", 
448                      dest="username", 
449                      help=\
450    "The username with which the credential is stored on the MyProxy server")
451
452    parser.add_option("-o", 
453                      "--out", 
454                      dest="outfile", 
455                      help=\
456    "The username with which the credential is stored on the MyProxy server")
457
458    parser.add_option("-t", 
459                      "--proxy-lifetime", 
460                      dest="lifetime", 
461                      default=43200,
462                      type="int",
463                      help=\
464    "The username with which the credential is stored on the MyProxy server")
465
466    parser.add_option("-d", 
467                      "--debug", 
468                      dest="debug", 
469                      type="int",
470                      default=0,
471                      help=\
472"Debug mode: 1=print debug info; 2=print as in (1), and dump data to myproxy.dump")
473
474    (options, args) = parser.parse_args()
475   
476    debug = options.debug
477   
478    # process options   
479    username = options.username
480    if not username:
481        if sys.platform == 'win32':
482            username = os.environ["USERNAME"]
483        else:
484            import pwd
485            username = pwd.getpwuid(os.geteuid())[0]
486
487    myProxy = MyProxyClient(hostname=options.host, 
488                            port=options.port,
489                            O='NDG',
490                            OU='BADC')
491   
492    if options.getDelegation:
493               
494        outfile = options.outfile
495        if not outfile:
496            if sys.platform == 'win32':
497                outfile = 'proxy'
498            elif sys.platform in ['linux2','darwin']:
499                outfile = '/tmp/x509up_u%s' % (os.getuid())
500   
501        # Get MyProxy password
502        passphrase = getpass.getpass()
503           
504        # Retrieve proxy cert
505        try:
506            creds = myProxy.logon(username, 
507                                  passphrase, 
508                                  lifetime=options.lifetime)
509            open(outfile, 'w').write(creds)
510            print "A proxy has been received for user %s in %s." % \
511                (username, outfile)
512           
513        except Exception,e:
514            if debuglevel(1):
515                import traceback
516                traceback.print_exc()
517            else:
518                print "Error:", e
519    else:
520        try:
521            myProxy.store(username, 
522                          options.certFile,
523                          options.keyFile,
524                          lifetime=options.lifetime)
525           
526        except Exception, e:
527            if debuglevel(1):
528                import traceback
529                traceback.print_exc()
530            else:
531                import traceback
532                traceback.print_exc()
533                print "Error:", e
534
535
536if __name__ == '__main__':
537    main()
Note: See TracBrowser for help on using the repository browser.