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

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

Rename SimpleCA to init so that SimpleCA class is part of package.

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