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

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

python/ndg.security.server/ndg/security/server/ca/init.py:

  • Get CA directory from OpenSSLConfig.caDir property
  • re-written chkCAPassphrase so that it uses M2Crypto code rather than a system call to

openssl.

python/ndg.security.common/ndg/security/common/openssl.py: rewritten OpenSSLConfig class
to use SafeConfigParser?. This required an override to allow OpenSSL config file style
querks.

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