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

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

python/ndg.security.server/ndg/security/server/ca/server-config.tac:

  • added check to ensure CA pass-phrase is set and if not prompt for from command line.
  • do a get call for 'clntCertFile' dict key so that it can be optional

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

  • use $HOME/.globus/simpleCA/grid-ca-ssl.conf as the default SSL config file
  • key access methods raise KeyError? on exception
  • PassPhrase? -> passphrase

python/ndg.security.server/ndg/security/server/MyProxy.py:

  • certReqDNParam attribute is no longer needed - use openSSLConfig.reqDN instead.

python/conf/simpleCAProperties.xml,
python/ndg.security.test/ndg/security/test/ca/simpleCAProperties.xml:
explanation about default openSSLConfigFilePath setting

python/ndg.security.common/ndg/security/common/wsSecurity.py: check X.509 cert text
on 64th char for newline not 65th.

python/ndg.security.common/ndg/security/common/openssl.py: fix to error reading file
exception message.

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