source: TI12-security/trunk/python/ndg.security.server/ndg/security/server/ca/__init__.py @ 2732

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/ca/__init__.py@2732
Revision 2732, 23.9 KB checked in by pjkersha, 13 years ago (diff)

ndg.security.server/ndg/security/server/AttAuthority/init.py,
ndg.security.server/ndg/security/server/ca/init.py,
ndg.security.server/ndg/security/server/SessionMgr/init.py,
ndg.security.common/ndg/security/common/Log/log_services_server.py:

  • replaced all refs to NDG_DIR environment variable with NDGSEC_DIR so that

it's specific to NDG security.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG wrapper to Globus SimpleCA. 
2
3NERC Data Grid Project
4
5P J Kershaw 02/08/05
6
7Copyright (C) 2006 CCLRC & NERC
8
9This software may be distributed under the terms of the Q Public License,
10version 1.0 or later.
11"""
12
13__revision__ = '$Id$'
14
15# Call Globus SimpleCA executables
16from subprocess import *
17
18# Use pipes for stdin/stdout for MyProxy commands
19import os
20
21# Certificate lifetime calculation for MyProxy
22from datetime import datetime
23from datetime import timedelta
24from time import strptime
25
26# Temporaries files created for SimpleCA executables I/O
27import tempfile
28
29# Get list of certificate files
30from glob import glob
31
32# Certificate request generation
33from M2Crypto import X509, BIO, RSA, EVP, m2
34
35# For parsing of properties file
36try: # python 2.5
37    from xml.etree import cElementTree as ElementTree
38except ImportError:
39    # if you've installed it yourself it comes this way
40    import cElementTree as ElementTree
41
42from ndg.security.common.openssl import OpenSSLConfig
43
44
45#_____________________________________________________________________________
46class SimpleCAError(Exception):
47    """Exception handling for NDG SimpleCA class."""
48
49
50#_____________________________________________________________________________
51class SimpleCAPassPhraseError(SimpleCAError):
52    """Specific exception for CA pass-phrase check"""
53
54
55#_____________________________________________________________________________
56class SimpleCA(dict):
57    """Wrapper to Globus SimpleCA - administer NDG user X.509 Certificates
58   
59    @type __validKeys: tuple
60    @cvar __validKeys: valid configuration property keywords used in file
61    and keyword input to __init__ and setProperties()
62   
63    @type __gridCAConfigFile: string
64    @cvar __gridCAConfigFile: name of file containing SSL configuration
65    settings for CA
66       
67    @type __confDir: string
68    @cvar __confDir: configuration directory under $NDGSEC_DIR - default location
69    for properties file
70   
71    @type __propFileName: string
72    @cvar __propFileName: default file name for properties file under
73    __confDir"""
74
75    __validKeys = ( 'portNum',
76                    'useSSL',
77                    'sslCertFile',
78                    'sslKeyFile',
79                    'caCertFile',
80                    'certFile',
81                    'keyFile',
82                    'keyPwd',
83                    'clntCertFile',
84                    'openSSLConfigFilePath',
85                    'certLifetimeDays',
86                    'certExpiryDate',
87                    'certTmpDir',
88                    'caCertFile',
89                    'chkCAPassphraseExe',
90                    'signExe',
91                    'path'  )
92   
93    __gridCASubDir = os.path.join(".globus", "simpleCA")
94    __gridCAConfigFile = "grid-ca-ssl.conf"
95
96    __confDir = "conf"
97    __propFileName = "simpleCAProperties.xml"
98
99
100    def __init__(self,
101                 propFilePath=None,
102                 passphraseFilePath=None,
103                 caPassphrase=None,
104                 **prop):
105        """Initialise environment for calling SimpleCA executables
106
107        SimpleCA([propFilePath=p, ][passphraseFilePath=pp|caPassphrase=cp, ]
108                 [ ... ])
109         
110        @type propFilePath: string       
111        @keyword propFilePath: XML file containing SimpleCA settings.
112           
113        @type caPassphrase: string
114        @keyword caPassphrase: pass-phrase for SimpleCA's private key.
115       
116        @type passphraseFilePath: string
117        @keyword passphraseFilePath: alternative to caPassphrase, Set
118        pass-phrase from a file.
119                                       
120        **prop: optional keywords for SimpleCA settings.  These correspond
121        exactly to the tags in properties file (SimpleCA.__validKeys).  If
122        propFilePath is set, its settings will override those set by these
123        keywords"""
124
125
126        # Base class initialisation
127        dict.__init__(self)
128
129
130        self.__prop = {}
131        self.__dtCertExpiry = None
132
133       
134        # Make a copy of the environment and then reset the path to the above
135        #
136        # Use copy as direct assignment seems to take a reference to
137        # os.environ - if self.__env is changed, so is os.environ
138        self.__env = os.environ.copy()
139
140
141        # Set-up parameter names for certificate request
142        self.__openSSLConfig = OpenSSLConfig()
143
144        self.setProperties(**prop)
145
146       
147        # Set from input or use defaults based or environment variables
148        self.setPropFilePath(propFilePath)
149
150        # If properties file is set any parameters settings in file will
151        # override those set by input keyword
152        self.readProperties()
153
154
155        # Make config file path default setting if not already set
156        if 'openSSLConfigFilePath' not in self.__prop:
157            self.__openSSLConfig.filePath = os.path.join(\
158                                                   self.__openSSLConfig.caDir,
159                                                   self.__gridCAConfigFile)
160            self.__openSSLConfig.read()
161
162           
163        if not os.environ.get('GLOBUS_LOCATION'):
164            raise SimpleCAError, \
165                        "Environment variable \"GLOBUS_LOCATION\" is not set"
166       
167       
168        # Set pass-phrase from file or string input - Check HERE because
169        # property settings made by readProperties and setProperties need to
170        # be in place first
171        if passphraseFilePath is not None:
172            try:
173                caPassphrase = open(passphraseFilePath).read().strip()
174            except Exception, e:
175                raise SimpleCAError, "Reading configuration file: %s" % e
176       
177        self.__caPassphrase = None   
178        if caPassphrase is not None:
179            self.__setCAPassphrase(caPassphrase)
180
181
182    #_________________________________________________________________________
183    def __call__(self):
184        """Return file properties dictionary"""
185        return self.__prop
186
187
188    #_________________________________________________________________________
189    # dict derived methods ...
190    def __repr__(self):
191        """Return file properties dictionary as representation"""
192        return repr(self.__prop)
193
194    # Nb. read only - no __setitem__() method
195    def __delitem__(self, key):
196        "SimpleCA Properties keys cannot be removed"       
197        raise KeyError, 'Keys cannot be deleted from ' + \
198                            self.__class__.__name__
199
200    def __getitem__(self, key):
201        self.__class__.__name__ + """ behaves as data dictionary of SimpleCA
202        file properties"""
203       
204        # Check input key
205        if key in self.__prop:
206            return self.__prop[key]               
207        else:
208            raise KeyError, "Property with key '%s' not found" % key
209   
210    def get(self):
211        return self.__prop.get(kw)
212       
213    def clear(self):
214        raise KeyError, "Data cannot be cleared from " + \
215                            self.__class__.__name__
216   
217    def keys(self):
218        return self.__prop.keys()
219
220    def items(self):
221        return self.__prop.items()
222
223    def values(self):
224        return self.__prop.values()
225
226    def has_key(self, key):
227        return self.__prop.has_key(key)
228
229    # 'in' operator
230    def __contains__(self, key):
231        return key in self.__prop
232
233    #_________________________________________________________________________
234    # End of dict derived methods <--
235
236
237    def __setCAPassphrase(self, caPassphrase):
238        """Give this instance the pass-phrase for the SimpleCA"""
239        self.chkCAPassphrase(caPassphrase)       
240        self.__caPassphrase = caPassphrase
241           
242    caPassphrase = property(fset=__setCAPassphrase,
243                            doc="Enter pass-phrase for Simple CA")
244
245
246    #_________________________________________________________________________
247    def __getOpenSSLConfig(self):
248        "Get OpenSSLConfig object property method"
249        return self.__openSSLConfig
250   
251    openSSLConfig = property(fget=__getOpenSSLConfig,
252                             doc="OpenSSLConfig object")
253
254
255    #_________________________________________________________________________
256    def setPropFilePath(self, val=None):
257        """Set properties file from input or based on environment variable
258        settings"""
259        if not val:
260            if 'NDGSEC_CA_PROPFILEPATH' in os.environ:
261                val = os.environ['NDGSEC_CA_PROPFILEPATH']
262               
263            elif 'NDGSEC_DIR' in os.environ:
264                val = os.path.join(os.environ['NDGSEC_DIR'], 
265                                   self.__class__.__confDir,
266                                   self.__class__.__propFileName)
267            else:
268                raise AttributeError, 'Unable to set default Session ' + \
269                    'Manager properties file path: neither ' + \
270                    '"NDGSEC_CA_PROPFILEPATH" or "NDGSEC_DIR" environment ' + \
271                    'variables are set'
272               
273        if not isinstance(val, basestring):
274            raise AttributeError, "Input Properties file path " + \
275                                  "must be a valid string."
276     
277        self.__propFilePath = val
278       
279    # Also set up as a property
280    propFilePath = property(fset=setPropFilePath,
281                            doc="Set the path to the properties file")   
282                           
283                           
284    #_________________________________________________________________________
285    def setProperties(self, **prop):
286        """Update existing properties from an input dictionary
287        Check input keys are valid names"""
288       
289        for key in prop.keys():
290            if key not in self.__validKeys:
291                raise SimpleCAError, "Property name \"%s\" is invalid" % key
292               
293        self.__prop.update(prop)
294
295
296        # Update path
297        if 'path' in prop:
298            self.__env['PATH'] = self.__prop['path']
299
300
301        # Set expiry date as datetime type
302        if 'certExpiryDate' in prop:
303            try:
304                self.__dtCertExpiry = strptime(prop['certExpiryDate'],
305                                               "%Y %m %d %H %M %S")
306               
307                return datetime(*self.__dtCertExpiry[0:6])
308           
309            except Exception, e:
310                raise SimpleCAError, "certExpiryDate has the format " + \
311                                    "YYYY mm dd HH MM SS. Year, month, " + \
312                                    "day, hour minute, second respectively." 
313
314
315        if 'openSSLConfigFilePath' in prop:
316            self.__openSSLConfig.filePath = prop['openSSLConfigFilePath']
317            self.__openSSLConfig.read()
318           
319           
320    #_________________________________________________________________________   
321    @staticmethod                               
322    def __filtTxt(tag, txt):         
323        if isinstance(txt, basestring):
324            if txt.isdigit():
325                return int(txt)
326           
327            elif tag != 'keyPwd': 
328                # Password may contain leading/trailing spaces
329                return os.path.expandvars(txt.strip())
330       
331        return txt
332
333
334    #_________________________________________________________________________   
335    def readProperties(self, propElem=None):
336        """Read the configuration properties for the Certificate Authority
337       
338        readProperties([propElem=p])
339
340        @type propElem: ElementTree node
341        @keyword propElem: set to read beginning from a ElementTree node.
342        If not set, file self.__propFilePath will be read"""     
343
344        if propElem is None:
345            try:
346                tree = ElementTree.parse(self.__propFilePath)
347                propElem = tree.getroot()
348               
349            except IOError, e:
350                raise SimpleCAError, \
351                                "Error parsing properties file \"%s\": %s" % \
352                                (e.filename, e.strerror)
353
354            except Exception, e:
355                raise SimpleCAError, "Error parsing properties file: %s" % \
356                                    str(e)
357
358
359        # Read properties into a dictionary
360        prop = dict([(elem.tag, self.__filtTxt(elem.tag, elem.text)) \
361                     for elem in propElem])
362
363        # Ensure Certificate Lifetime is converted into a numeric type
364        if 'certLifetimeDays' in prop and \
365           isinstance(prop['certLifetimeDays'], basestring):
366            prop['certLifetimeDays'] = eval(prop['certLifetimeDays'])
367
368
369        # Check for missing properties
370        propKeys = prop.keys()
371        missingKeys = [key for key in self.__class__.__validKeys \
372                       if key not in propKeys]
373        if missingKeys != [] and \
374           'certExpiryDate' in missingKeys and \
375           'certLifetimeDays' in missingKeys:
376            raise SimpleCAError, "The following properties are missing " + \
377                                 "from the properties file: " + \
378                                 ', '.join(missingKeys)
379                               
380        self.setProperties(**prop)
381
382
383    def chkCAPassphrase(self, caPassphrase=None):       
384       
385        if caPassphrase is None:
386            caPassphrase = self.__caPassphrase
387        else:
388            if not isinstance(caPassphrase, basestring):
389                raise SimpleCAPassPhraseError, \
390                                    "CA Pass-phrase must be a valid string"
391                                   
392        try:
393            priKeyFilePath = self.__openSSLConfig.get('CA_default', 
394                                                      'private_key')
395            priKeyFile = BIO.File(open(priKeyFilePath))
396           
397        except Exception, e:
398            raise SimpleCAError, \
399                        "Reading private key for pass-phrase check: %s" % e
400        try:   
401            RSA.load_key_bio(priKeyFile,callback=lambda *ar,**kw:caPassphrase)
402        except:
403            raise SimpleCAPassPhraseError, "Invalid pass-phrase"
404           
405           
406    #_________________________________________________________________________
407    def OldchkCAPassphrase(self, caPassphrase=None):
408        """Check given pass-phrase is correct for CA private key
409       
410        This method allows checking of the pass-phrase without having to
411        call sign() to sign a new certificate.  It makes use of an openssl
412        call where the pass-phrase is required - creation of a CRL
413        (Certificate Revokation List)"""
414       
415       
416        if caPassphrase is None:
417            caPassphrase = self.__caPassphrase
418        else:
419            if not isinstance(caPassphrase, basestring):
420                raise SimpleCAPassPhraseError, \
421                                    "CA Pass-phrase must be a valid string"
422       
423        chkCAPassphraseCmd = [
424            self.__prop['chkCAPassphraseExe'],
425            'ca',
426            '-config',  self.__openSSLConfig.filePath,
427            '-gencrl',
428            '-passin', 'stdin']
429
430        errMsgTmpl = "Verifying CA pass-phrase: %s"
431
432       
433        # Create sign new certificate using grid-ca-sign
434        try:
435            try:
436                # open pipe to pass to stdin
437                chkCAPassphraseR, chkCAPassphraseW = os.pipe()
438                os.write(chkCAPassphraseW, caPassphrase)
439               
440                chkCAPassphraseProc = Popen(chkCAPassphraseCmd,
441                                           stdin=chkCAPassphraseR,
442                                           stdout=PIPE,
443                                           stderr=STDOUT,
444                                           close_fds=True)
445            finally:
446                try:
447                    os.close(chkCAPassphraseR)
448                    os.close(chkCAPassphraseW)
449                except: pass
450
451
452            # File must be closed + close_fds set to True above otherwise
453            # wait() call will hang               
454            if chkCAPassphraseProc.wait():
455                errMsg = chkCAPassphraseProc.stdout.read()
456            else:
457                errMsg = None
458                                       
459        except IOError, e:               
460            raise SimpleCAError, errMsgTmpl % e.strerror
461       
462        except OSError, e:
463            raise SimpleCAError, errMsgTmpl % e.strerror
464       
465        except Exception, e:
466            raise SimpleCAError, errMsgTmpl % e
467       
468       
469        if errMsg is not None:
470            raise SimpleCAPassPhraseError, errMsg
471                 
472                 
473    #_________________________________________________________________________       
474    def _createCertReq(self, CN, nBitsForKey=1024, messageDigest="md5"):
475        """
476        Create a certificate request.
477       
478        @param CN: Common Name for certificate - effectively the same as the
479        username for the MyProxy credential
480        @param nBitsForKey: number of bits for private key generation -
481        default is 1024
482        @param messageDigest: message disgest type - default is MD5
483        @return tuple of certificate request PEM text and private key PEM text
484        """
485       
486        # Check all required certifcate request DN parameters are set               
487        # Create certificate request
488        req = X509.Request()
489   
490        # Generate keys
491        key = RSA.gen_key(nBitsForKey, m2.RSA_F4)
492   
493        # Create public key object
494        pubKey = EVP.PKey()
495        pubKey.assign_rsa(key)
496       
497        # Add the public key to the request
498        req.set_version(0)
499        req.set_pubkey(pubKey)
500       
501        defaultReqDN = self.__openSSLConfig.reqDN       
502           
503        # Set DN
504        x509Name = X509.X509_Name()
505        x509Name.CN = CN
506        x509Name.OU = defaultReqDN['OU']
507        x509Name.O = defaultReqDN['O']
508                       
509        req.set_subject_name(x509Name)
510       
511        req.sign(pubKey, messageDigest)
512       
513        return req, key
514
515
516    #_________________________________________________________________________
517    def sign(self,
518             passphraseFilePath=None,
519             caPassphrase=None,
520             certReq=None,
521             certReqFilePath=None,
522             CN=None,
523             **createCertReqKw):
524       
525        """Sign a certificate request
526
527        @type caPassphrase: string
528        @keyword caPassphrase: pass-phrase for SimpleCA's private key.
529       
530        @type passphraseFilePath: string
531        @keyword passphraseFilePath: alternative to caPassphrase, Set
532        pass-phrase from a file.
533       
534        @type CN: string
535        @keyword CN: common name component of Distinguished Name for new
536        cert.  This keyword is ignored if certReq keyword is set.
537
538        @type certReq: M2Crypto.X509.Request
539        @keyword certReq: certificate request
540       
541        @type **createCertReqKw: dict
542        @param **createCertReqKw: keywords to call to _createCertReq - only
543        applies if certReq is not set.
544       
545        @rtype: tuple
546        @return: signed certificate and private key.  Private key will be
547        None if certReq keyword was passed in
548        """
549       
550        # Set pass phrase via from file       
551        if passphraseFilePath is not None:
552            try:
553                caPassphrase = open(passphraseFilePath).read().strip()
554            except Exception, e:
555                raise SimpleCAError, "Reading pass-phrase file: " + str(e)
556
557        # ... or from string input
558        if caPassphrase is not None:
559            self.__setCAPassphrase(caPassphrase)
560           
561        if self.__caPassphrase is None:
562            raise SimpleCAError, "CA Pass-phrase must be set in order to " + \
563                                "sign a certificate request"
564
565
566        priKey = None
567               
568        if certReq is not None:
569            if isinstance(certReq, X509.Request):
570                certReq = certReq.as_pem()
571               
572            elif not isinstance(certReq, basestring):
573                raise SimpleCAError, "certReq input must be a valid string"
574
575        elif certReqFilePath is not None:
576
577            # Certificate request has been input as a file - check it
578            if not isinstance(certReqFilePath, basestring):
579                raise SimpleCAError, \
580                    "certReqFilePath input must be a valid string"           
581        elif CN is not None:
582           
583            certReq, priKey = self._createCertReq(CN, **createCertReqKw)
584            certReq = certReq.as_pem()
585        else:
586            # The certificate request must be input via either of the
587            # options above
588            raise SimpleCAError, "No input Certificate Request provided"
589
590
591        if certReqFilePath is None:
592            # Certificate request has been passed in as a string or
593            # X509.Request object - write it to a temporary file for input
594            # into the grid-ca-sign executable
595            certReqFile = tempfile.NamedTemporaryFile('w', -1, '.pem',
596                                                    'certReq-',
597                                                    self.__prop['certTmpDir'])
598           
599            open(certReqFile.name, 'w').write(certReq)
600            certReqFilePath = certReqFile.name
601       
602       
603        # If no expiry date was set, use life time in days parameter
604        if self.__dtCertExpiry is None:
605            if 'certLifetimeDays' not in self.__prop:
606                raise SimpleCAError, "No certLifetimeDays parameter set"
607               
608            self.__dtCertExpiry = datetime.utcnow() + \
609                            timedelta(days=self.__prop['certLifetimeDays'])
610
611
612        certFile = tempfile.NamedTemporaryFile('w', -1, '.pem', 'cert-',
613                                               self.__prop['certTmpDir'])
614
615        req = X509.load_request(certReqFilePath)
616        priKeyFilePath = self.__openSSLConfig.get('CA_default', 'private_key')
617        pwdCallback = lambda *ar, **kw: self.__caPassphrase
618        priKey = EVP.load_key(priKeyFilePath, callback=pwdCallback)
619
620        try:
621            cert = req.sign(priKey, 'sha1')
622        except Exception, e:
623            raise SimpleCAError, str(e)
624
625        gridCASignCmd = [
626            self.__prop['signExe'],
627            '-in',  certReqFilePath,
628            '-out', certFile.name,
629            '-enddate', self.__dtCertExpiry.strftime("%y%m%d%H%M%SZ"),
630            '-passin', 'stdin',
631            '-force']
632
633        errMsgTmpl = "Signing certificate request: %s"
634
635       
636        # Create sign new certificate using grid-ca-sign
637        try:
638            try:
639                # open pipe to pass to stdin
640                gridCASignR, gridCASignW = os.pipe()
641                os.write(gridCASignW, self.__caPassphrase)
642               
643                gridCaSignProc = Popen(gridCASignCmd,
644                                       stdin=gridCASignR,
645                                       stdout=PIPE,
646                                       stderr=STDOUT,
647                                       close_fds=True)
648            finally:
649                try:
650                    os.close(gridCASignR)
651                    os.close(gridCASignW)
652                except: pass
653
654
655            # File must be closed + close_fds set to True above otherwise
656            # wait() call will hang               
657            if gridCaSignProc.wait():
658                errMsg = gridCaSignProc.stdout.read()
659                raise SimpleCAError, errMsg
660                                       
661        except IOError, e:               
662            raise SimpleCAError, errMsgTmpl % e.strerror
663       
664        except OSError, e:
665            raise SimpleCAError, errMsgTmpl % e.strerror
666       
667        except Exception, e:
668            raise SimpleCAError, errMsgTmpl % e
669
670
671        try:
672            # Return the certificate file content
673            return open(certFile.name).read(), priKey
674
675        except Exception, e:
676            raise SimpleCAError, \
677            "Reading output certificate file \"%s\": %s" % (certFile.name, e)
678
679
680    #_________________________________________________________________________
681    def revokeCert(self):
682        """Revoke an existing certificate"""
683     
684     
685    #_________________________________________________________________________   
686    def genCRL(self):
687        """Generate a Certificate Revocation List"""
Note: See TracBrowser for help on using the repository browser.