source: TI12-security/trunk/ndg_saml/ndg/saml/utils/m2crypto.py @ 7322

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/ndg_saml/ndg/saml/utils/m2crypto.py@7322
Revision 7322, 55.3 KB checked in by pjkersha, 9 years ago (diff)

Incomplete - task 2: XACML-Security Integration

  • fixes to SOAP client bindings
  • Property svn:keywords set to Id
Line 
1"""SAML 2.0 Utilities module for M2Crypto SSL functionality
2
3NERC DataGrid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "02/07/07"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "http://www.apache.org/licenses/LICENSE-2.0"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id$'
11import logging
12log = logging.getLogger(__name__)
13
14from warnings import warn # warn of impending certificate expiry
15
16import os
17import re
18
19# Handle not before and not after strings
20from time import strptime
21from datetime import datetime
22
23import M2Crypto
24from M2Crypto import SSL, X509
25from M2Crypto.httpslib import HTTPSConnection as _HTTPSConnection
26
27
28class X500DNError(Exception):
29    """Exception handling for NDG X.500 DN class."""
30
31
32class X500DN(dict):
33    "NDG X500 Distinguished name"
34   
35    # Class attribute - look-up mapping short name attributes to their long
36    # name equivalents
37    # * private *
38    __shortNameLUT = {
39        'commonName':               'CN',
40        'organisationalUnitName':   'OU',
41        'organisation':             'O',
42        'countryName':              'C',
43        'emailAddress':             'EMAILADDRESS',
44        'localityName':             'L',
45        'stateOrProvinceName':      'ST',
46        'streetAddress':            'STREET',
47        'domainComponent':          'DC',
48        'userid':                   'UID'
49    }
50    SLASH_PARSER_RE_STR = '/(%s)=' % '|'.join(__shortNameLUT.keys() + 
51                                              __shortNameLUT.values())   
52    SLASH_PARSER_RE = re.compile(SLASH_PARSER_RE_STR)
53
54    COMMA_PARSER_RE_STR = '[,]?\s*(%s)=' % '|'.join(__shortNameLUT.keys() + 
55                                                    __shortNameLUT.values())   
56    COMMA_PARSER_RE = re.compile(COMMA_PARSER_RE_STR)
57   
58    def __init__(self, dn=None, m2CryptoX509Name=None, separator=None):
59
60        """Create a new X500 Distinguished Name
61
62        @type m2CryptoX509Name: M2Crypto.X509.X509_Name
63        @param m2CryptoX509Name:   initialise using using an
64        M2Crypto.X509.X509_Name
65        @type dn: basestring
66        @param dn: initialise using a distinguished name string
67        @type separator: basestring
68        @param: separator: separator used to delimit dn fields - usually '/'
69        or ','.  If dn is input and separator is omitted the separator
70        character will be automatically parsed from the dn string.
71        """
72       
73        # Private key data
74        self.__dat = {}.fromkeys(X500DN.__shortNameLUT.values(), '')
75   
76        dict.__init__(self)
77   
78        self.__separator = None
79       
80        # Check for separator from input
81        if separator is not None:
82            if not isinstance(separator, basestring):
83                raise X500DNError("dn Separator must be a valid string")
84
85            # Check for single character but allow trailing space chars
86            if len(separator.lstrip()) is not 1:
87                raise X500DNError("dn separator must be a single character")
88
89            self.__separator = separator
90       
91        if m2CryptoX509Name is not None:
92            # the argument is an x509 dn in m2crypto format
93            self.deserialise(str(m2CryptoX509Name))
94           
95        elif dn is not None:
96            # Separator can be parsed from the input DN string - only attempt
97            # if no explict separator was input
98            if self.__separator is None:
99                self.__separator = self.parseSeparator(dn)
100               
101            # Split Distinguished name string into constituent fields
102            self.deserialise(dn)
103
104    @classmethod
105    def fromString(cls, dn):
106        """Convenience method for parsing DN string into a new instance
107        """
108        return cls(dn=dn)
109
110    def __repr__(self):
111        """Give representation based on underlying dict object"""
112        return repr(self.__dat)
113       
114    def __str__(self):
115        """Behaviour for print and string statements - convert DN into
116        serialised format."""
117        return self.serialise()
118       
119    def __eq__(self, x500dn):
120        """Return true if the all the fields of the two DNs are equal"""
121       
122        if not isinstance(x500dn, X500DN):
123            return False
124
125        return self.__dat.items() == x500dn.items()
126   
127    def __ne__(self, x500dn):
128        """Return true if the all the fields of the two DNs are equal"""
129       
130        if not isinstance(x500dn, X500DN):
131            return False
132
133        return self.__dat.items() != x500dn.items()
134 
135    def __delitem__(self, key):
136        """Prevent keys from being deleted."""
137        raise X500DNError('Keys cannot be deleted from the X500DN')
138
139    def __getitem__(self, key):
140
141        # Check input key
142        if self.__dat.has_key(key):
143
144            # key recognised
145            return self.__dat[key]
146       
147        elif X500DN.__shortNameLUT.has_key(key):
148
149            # key not recognised - but a long name version of the key may
150            # have been passed
151            shortName = X500DN.__shortNameLUT[key]
152            return self.__dat[shortName]
153
154        else:
155            # key not recognised as a short or long name version
156            raise KeyError('Key "' + key + '" not recognised for X500DN')
157
158    def __setitem__(self, key, item):
159       
160        # Check input key
161        if self.__dat.has_key(key):
162
163            # key recognised
164            self.__dat[key] = item
165           
166        elif X500DN.__shortNameLUT.has_key(key):
167               
168            # key not recognised - but a long name version of the key may
169            # have been passed
170            shortName = X500DN.__shortNameLUT[key]
171            self.__dat[shortName] = item
172           
173        else:
174            # key not recognised as a short or long name version
175            raise KeyError('Key "' + key + '" not recognised for X500DN')
176
177    def clear(self):
178        raise X500DNError("Data cannot be cleared from X500DN")
179
180    def copy(self):
181
182        import copy
183        return copy.copy(self)
184
185    def keys(self):
186        return self.__dat.keys()
187
188    def items(self):
189        return self.__dat.items()
190
191    def values(self):
192        return self.__dat.values()
193
194    def has_key(self, key):
195        return self.__dat.has_key(key)
196
197    # 'in' operator
198    def __contains__(self, key):
199        return self.has_key(key)
200
201    def get(self, *arg):
202        return self.__dat.get(*arg)
203 
204    def serialise(self, separator=None):
205        """Combine fields in Distinguished Name into a single string."""
206       
207        if separator:
208            if not isinstance(separator, basestring):
209                raise X500DNError("Separator must be a valid string")
210       
211            self.__separator = separator
212           
213        else:
214            # Default to / if no separator is set
215            separator = '/'
216
217
218        # If using '/' then prepend DN with an initial '/' char
219        if separator == '/':
220            sDN = separator
221        else:
222            sDN = ''
223     
224        dnList = []
225        for (key, val) in self.__dat.items():
226            if val:
227                if isinstance(val, tuple):
228                    dnList += [separator.join(["%s=%s" % (key, valSub) \
229                                               for valSub in val])]
230                else:
231                    dnList += ["%s=%s" % (key, val)]
232               
233        sDN += separator.join(dnList)
234                               
235        return sDN
236
237    serialize = serialise
238   
239    def deserialise(self, dn, separator=None):
240        """Break up a DN string into it's constituent fields and use to
241        update the object's dictionary"""
242       
243        if separator:
244            if not isinstance(separator, basestring):
245                raise X500DNError("Separator must be a valid string")
246
247            self.__separator = separator
248
249
250        # If no separator has been set, parse if from the DN string           
251        if self.__separator is None:
252            self.__separator = self.parseSeparator(dn)
253
254        if self.__separator == '/':
255            parserRe = self.__class__.SLASH_PARSER_RE
256           
257        elif self.__separator == ',':
258            parserRe = self.__class__.COMMA_PARSER_RE
259        else:
260            raise X500DNError("DN field separator %r not recognised" % 
261                              self.__separator)
262           
263        try:
264            dnFields = parserRe.split(dn)
265            if len(dnFields) < 2:
266                raise X500DNError("Error parsing DN string: \"%s\"" % dn)
267
268            items = zip(dnFields[1::2], dnFields[2::2])
269           
270            # Reset existing dictionary values
271            self.__dat.fromkeys(self.__dat, '')
272           
273            # Strip leading and trailing space chars and convert into a
274            # dictionary
275            parsedDN = {}
276            for key, val in items:
277                key = key.strip()
278                if key in parsedDN:
279                    if isinstance(parsedDN[key], tuple):
280                        parsedDN[key] = tuple(list(parsedDN[key]) + [val])
281                    else:
282                        parsedDN[key] = (parsedDN[key], val)
283                else:
284                    parsedDN[key] = val
285               
286            # Copy matching DN fields
287            for key, val in parsedDN.items():
288                if key not in self.__dat and key not in self.__shortNameLUT:
289                    raise X500DNError('Invalid field "%s" in input DN string' %
290                                      key)
291
292                self.__dat[key] = val
293
294               
295        except Exception, excep:
296            raise X500DNError("Error de-serialising DN \"%s\": %s" % \
297                              (dn, str(excep)))
298
299    deserialize = deserialise
300   
301    def parseSeparator(self, dn):
302        """Attempt to parse the separator character from a given input
303        DN string.  If not found, return None
304
305        DNs don't use standard separators e.g.
306
307        /C=UK/O=eScience/OU=CLRC/L=DL/CN=AN Other
308        CN=SUM Oneelse,L=Didcot, O=RAL,OU=SSTD
309
310        This function isolates and identifies the character.  - In the above,
311        '/' and ',' respectively"""
312
313
314        # Make a regular expression containing all the possible field
315        # identifiers with equal sign appended and 'or'ed together.  \W should
316        # match the separator which preceeds the field name. \s* allows any
317        # whitespace between field name and field separator to be taken into
318        # account.
319        #
320        # The resulting match should be a list.  The first character in each
321        # element in the list should be the field separator and should be the
322        # same
323        regExpr = '|'.join(['\W\s*'+i+'=' for i in self.__dat.keys()])
324        match = re.findall(regExpr, dn)
325           
326        # In the first example above, the resulting match is:
327        # ['/C=', '/O=', '/OU=', '/L=']
328        # In each element the first character is the separator
329        sepList = [i[0:1] for i in match]
330
331        # All separators should be the same character - return None if they
332        # don't match
333        if not [i for i in sepList if i != sepList[0]]:
334            return sepList[0]
335        else:
336            return None
337
338    @classmethod
339    def Parse(cls, dn):
340        """Convenience method to create an X500DN object from a DN string
341        @type dn: basestring
342        @param dn: Distinguished Name
343        """
344        return cls(dn=dn)
345   
346    Deserialise = Deserialize = Parse
347
348
349class X509CertError(Exception):
350    """Exception handling for NDG X.509 Certificate handling class."""
351
352class X509CertReadError(X509CertError):
353    """Error reading in certificate from file"""
354
355class X509CertParseError(X509CertError):
356    """Error parsing a certificate"""
357 
358class X509CertInvalidNotBeforeTime(X509CertError):
359    """Call from X509Cert.isValidTime if certificates not before time is
360    BEFORE the current system time"""
361   
362class X509CertExpired(X509CertError):
363    """Call from X509Cert.isValidTime if certificate has expired"""
364   
365   
366class X509Cert(object):
367    "NDG X509 Certificate Handling"
368
369    formatPEM = M2Crypto.X509.FORMAT_PEM
370    formatDER = M2Crypto.X509.FORMAT_DER
371   
372    def __init__(self, filePath=None, m2CryptoX509=None):
373
374        # Set certificate file path
375        if filePath is not None:
376            if not isinstance(filePath, basestring):
377                raise X509CertError("Certificate File Path input must be a "
378                                    "valid string")
379           
380        self.__filePath = filePath           
381        self.__dn = None
382        self.__dtNotBefore = None
383        self.__dtNotAfter = None
384       
385        if m2CryptoX509:
386            self.__setM2CryptoX509(m2CryptoX509)
387        else:
388            self.__m2CryptoX509 = None
389
390    def read(self, 
391             filePath=None, 
392             format=None, 
393             warningStackLevel=3,
394             **isValidTimeKw):
395        """Read a certificate from PEM encoded DER format file
396       
397        @type filePath: basestring
398        @param filePath: file path of PEM format file to be read
399       
400        @type format: int
401        @param format: format of input file - PEM is the default.  Set to
402        X509Cert.formatDER for DER format
403       
404        @type isValidTimeKw: dict
405        @param isValidTimeKw: keywords to isValidTime() call"""
406
407        if format is None:
408            format = X509Cert.formatPEM
409       
410         # Check for optional input certificate file path
411        if filePath is not None:
412            if not isinstance(filePath, basestring):
413                raise X509CertError("Certificate File Path input must be a "
414                                    "valid string")
415           
416            self.__filePath = filePath
417       
418        try:
419            self.__m2CryptoX509 = M2Crypto.X509.load_cert(self.__filePath,
420                                                          format=format)
421        except Exception, e:
422            raise X509CertReadError("Error loading certificate \"%s\": %s" %
423                                    (self.__filePath, e))
424
425        # Update DN and validity times from M2Crypto X509 object just
426        # created
427        self.__setM2CryptoX509()
428       
429        self.isValidTime(warningStackLevel=warningStackLevel, **isValidTimeKw)
430
431    def parse(self, 
432              certTxt, 
433              format=None, 
434              warningStackLevel=3,
435              **isValidTimeKw):
436        """Read a certificate input as a string
437       
438        @type certTxt: basestring
439        @param certTxt: PEM encoded certificate to parse
440       
441        @type format: int
442        @param format: format of input file - PEM is the default.  Set to
443        X509Cert.formatDER for DER format
444       
445        @type isValidTimeKw: dict
446        @param isValidTimeKw: keywords to isValidTime() call"""
447
448        if format is None:
449            format = X509Cert.formatPEM
450           
451        try:
452            # Create M2Crypto memory buffer and pass to load certificate
453            # method
454            #
455            # Nb. input converted to standard string - buffer method won't
456            # accept unicode type strings
457#            certBIO = M2Crypto.BIO.MemoryBuffer(str(certTxt))
458#            self.__m2CryptoX509 = M2Crypto.X509.load_cert_bio(certBIO)
459            self.__m2CryptoX509 = M2Crypto.X509.load_cert_string(str(certTxt),
460                                                                 format=format)
461        except Exception, e:
462            raise X509CertParseError("Error loading certificate: %s" % e)
463
464        # Update DN and validity times from M2Crypto X509 object just
465        # created
466        self.__setM2CryptoX509()
467       
468        self.isValidTime(warningStackLevel=warningStackLevel, **isValidTimeKw)
469     
470    def __setM2CryptoX509(self, m2CryptoX509=None):
471        """Private method allows class members to be updated from the
472        current M2Crypto object.  __m2CryptoX509 must have been set."""
473       
474        if m2CryptoX509 is not None:
475            if not isinstance(m2CryptoX509, M2Crypto.X509.X509):
476                raise TypeError("Incorrect type for input M2Crypto.X509.X509 "
477                                "object")
478                   
479            self.__m2CryptoX509 = m2CryptoX509
480                   
481        # Get distinguished name
482        m2CryptoX509Name = self.__m2CryptoX509.get_subject()
483
484        # Instantiate X500 Distinguished name
485        self.__dn = X500DN(m2CryptoX509Name=m2CryptoX509Name)
486       
487        # Get not before and not after validity times
488        #
489        # Only option for M2Crypto seems to be to return the times as
490        # formatted strings and then parse them in order to create a datetime
491        # type
492       
493        try:
494            m2CryptoNotBefore = self.__m2CryptoX509.get_not_before()
495            self.__dtNotBefore=self.__m2CryptoUTC2datetime(m2CryptoNotBefore)
496                                       
497        except Exception, e:
498            raise X509CertError("Not Before time: %s" % e)
499
500        try:
501            m2CryptoNotAfter = self.__m2CryptoX509.get_not_after()
502            self.__dtNotAfter = self.__m2CryptoUTC2datetime(m2CryptoNotAfter)
503                                   
504        except Exception, e:
505            raise X509CertError("Not After time: %s" % e)
506
507    def __getM2CryptoX509(self, m2CryptoX509=None):
508        "Return M2Crypto X.509 cert object"
509        return self.__m2CryptoX509
510   
511    m2CryptoX509 = property(fset=__setM2CryptoX509,
512                            fget=__getM2CryptoX509,
513                            doc="M2Crypto.X509.X509 type")
514       
515    def toString(self, **kw):
516        """Return certificate file content as a PEM format
517        string"""
518        return self.asPEM(**kw)
519       
520    def asPEM(self, filePath=None):
521        """Return certificate file content as a PEM format
522        string"""
523       
524        # Check M2Crypto.X509 object has been instantiated - if not call
525        # read method
526        if self.__m2CryptoX509 is None:
527            self.read(filePath)
528           
529        return self.__m2CryptoX509.as_pem()
530
531    def asDER(self):
532        """Return certificate file content in DER format"""
533       
534        # Check M2Crypto.X509 object has been instantiated
535        assert(self.__m2CryptoX509)
536        return self.__m2CryptoX509.as_der()
537
538    # Make some attributes accessible as read-only
539    def __getDN(self):
540        """Get X500 Distinguished Name."""
541        return self.__dn
542
543    dn = property(fget=__getDN, doc="X.509 Distinguished Name")
544   
545    def __getVersion(self):
546        """Get X.509 Certificate version"""
547        if self.__m2CryptoX509 is None:
548            return None
549       
550        return self.__m2CryptoX509.get_version()
551
552    version = property(fget=__getVersion, doc="X.509 Certificate version")
553   
554    def __getSerialNumber(self):
555        """Get Serial Number"""
556        if self.__m2CryptoX509 is None:
557            return None
558       
559        return self.__m2CryptoX509.get_serial_number()
560   
561    serialNumber = property(fget=__getSerialNumber, 
562                            doc="X.509 Certificate Serial Number")
563
564    def __getNotBefore(self):
565        """Get not before validity time as datetime type"""
566        if self.__m2CryptoX509 is None:
567            return None
568       
569        return self.__dtNotBefore
570
571    notBefore = property(fget=__getNotBefore, 
572                         doc="Not before validity time as datetime type")
573   
574    def __getNotAfter(self):
575        """Get not after validity time as datetime type"""
576        if self.__m2CryptoX509 is None:
577            return None
578       
579        return self.__dtNotAfter
580
581    notAfter = property(fget=__getNotAfter, 
582                         doc="Not after validity time as datetime type")
583   
584    def __getPubKey(self):
585        """Get public key
586       
587        @return: RSA public key for certificate
588        @rtype: M2Crypto.RSA.RSA_pub"""
589        if self.__m2CryptoX509 is None:
590            return None
591       
592        return self.__m2CryptoX509.get_pubkey()
593
594    pubKey = property(fget=__getPubKey, doc="Public Key")
595   
596    def __getIssuer(self):
597        """Get Certificate issuer"""
598        if self.__m2CryptoX509 is None:
599            return None
600       
601        # Return as X500DN type
602        return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_issuer())
603
604    issuer = property(fget=__getIssuer, doc="Certificate Issuer")
605   
606    def __getSubject(self):
607        """Get Certificate subject"""
608        if self.__m2CryptoX509 is None:
609            return None
610
611        # Return as X500DN type
612        return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_subject())
613   
614    subject = property(fget=__getSubject, doc="Certificate subject")
615
616    def isValidTime(self, 
617                    raiseExcep=False, 
618                    expiryWarning=True, 
619                    nDaysBeforeExpiryLimit=30,
620                    warningStackLevel=2):
621        """Check Certificate for expiry
622
623        @type raiseExcep: bool
624        @param raiseExcep: set True to raise an exception if certificate is
625        invalid
626       
627        @type expiryWarning: bool
628        @param expiryWarning: set to True to output a warning message if the
629        certificate is due to expire in less than nDaysBeforeExpiryLimit days.
630        Message is sent using warnings.warn and through logging.warning.  No
631        message is set if the certificate has an otherwise invalid time
632       
633        @type nDaysBeforeExpiryLimit: int
634        @param nDaysBeforeExpiryLimit: used in conjunction with the
635        expiryWarning flag.  Set the number of days in advance of certificate
636        expiry from which to start outputing warnings
637       
638        @type warningStackLevel: int
639        @param warningStackLevel: set where in the stack to flag the warning
640        from.  Level 2 will flag it at the level of the caller of this
641        method.  Level 3 would flag at the level of the caller of the caller
642        and so on.
643       
644        @raise X509CertInvalidNotBeforeTime: current time is before the
645        certificate's notBefore time
646        @raise X509CertExpired: current time is after the certificate's
647        notAfter time"""
648
649        if not isinstance(self.__dtNotBefore, datetime):
650            raise X509CertError("Not Before datetime is not set")
651
652        if not isinstance(self.__dtNotAfter, datetime):
653            raise X509CertError("Not After datetime is not set")
654       
655        dtNow = datetime.utcnow()
656        isValidTime = dtNow > self.__dtNotBefore and dtNow < self.__dtNotAfter
657
658        # Helper string for message output
659        if self.__filePath:
660            fileInfo = ' "%s"' % self.__filePath
661        else:
662            fileInfo = ''
663             
664       
665        # Set a warning message for impending expiry of certificate but only
666        # if the certificate is not any other way invalid - see below
667        if isValidTime and expiryWarning:
668            dtTime2Expiry = self.__dtNotAfter - dtNow
669            if dtTime2Expiry.days < nDaysBeforeExpiryLimit:
670                msg = ('Certificate%s with DN "%s" will expire in %d days on: '
671                       '%s' % (fileInfo, 
672                               self.dn, 
673                               dtTime2Expiry.days, 
674                               self.__dtNotAfter))
675                warn(msg, stacklevel=warningStackLevel)
676                log.warning(msg)
677       
678                     
679        if dtNow < self.__dtNotBefore:
680            msg = ("Current time %s is before the certificate's Not Before "
681                   'Time %s for certificate%s with DN "%s"' % 
682                   (dtNow, self.__dtNotBefore, fileInfo, self.dn))
683            log.error(msg)
684            if raiseExcep:
685                raise X509CertInvalidNotBeforeTime(msg)
686           
687        elif dtNow > self.__dtNotAfter:
688            msg = ('Certificate%s with DN "%s" has expired: the time now is '
689                   '%s and the certificate expiry is %s.' %(fileInfo,
690                                                            self.dn, 
691                                                            dtNow, 
692                                                            self.__dtNotAfter))
693            log.error(msg)
694            if raiseExcep:
695                raise X509CertExpired(msg)
696
697        # If exception flag is not set return validity as bool
698        return isValidTime
699
700    def __m2CryptoUTC2datetime(self, m2CryptoUTC):
701        """Convert M2Crypto UTC time string as returned by get_not_before/
702        get_not_after methods into datetime type"""
703       
704        datetimeRE = "([a-zA-Z]{3} {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4}).*"
705        sM2CryptoUTC = None
706       
707        try:
708            # Convert into string
709            sM2CryptoUTC = str(m2CryptoUTC)
710           
711            # Check for expected format - string may have trailing GMT - ignore
712            sTime = re.findall(datetimeRE, sM2CryptoUTC)[0]
713
714            # Convert into a tuple
715            lTime = strptime(sTime, "%b %d %H:%M:%S %Y")[0:6]
716
717            return datetime(lTime[0], lTime[1], lTime[2],
718                            lTime[3], lTime[4], lTime[5])
719                                   
720        except Exception, e:
721            msg = "Error parsing M2Crypto UTC"
722            if sM2CryptoUTC is not None:
723                msg += ": " + sM2CryptoUTC
724               
725            raise X509CertError(msg)
726   
727    def verify(self, pubKey, **kw):
728        """Verify a certificate against the public key of the
729        issuer
730       
731        @param pubKey: public key of cert that issued self
732        @type pubKey: M2Crypto.RSA.RSA_pub
733        @param **kw: keywords to pass to M2Crypto.X509.X509 -
734        'pkey'
735        @type: dict
736        @return: True if verifies OK, False otherwise
737        @rtype: bool
738        """
739        return bool(self.__m2CryptoX509.verify(pubKey, **kw))
740
741    @classmethod
742    def Read(cls, filePath, warningStackLevel=4, **isValidTimeKw):
743        """Create a new X509 certificate read in from a file"""
744        x509Cert = cls(filePath=filePath) 
745        x509Cert.read(warningStackLevel=warningStackLevel, **isValidTimeKw)
746       
747        return x509Cert
748   
749    @classmethod
750    def Parse(cls, x509CertTxt, warningStackLevel=4, **isValidTimeKw):
751        """Create a new X509 certificate from string of file content"""
752        x509Cert = cls()     
753        x509Cert.parse(x509CertTxt, 
754                       warningStackLevel=warningStackLevel,
755                       **isValidTimeKw)
756       
757        return x509Cert
758
759    @classmethod
760    def fromM2Crypto(cls, m2CryptoX509):
761        """Convenience method to instantiate a new object from an M2Crypto
762        X.509 certificate object"""
763        x509Cert = cls(m2CryptoX509=m2CryptoX509)
764        return x509Cert
765   
766   
767class X509StackError(X509CertError):
768    """Error from X509Stack type"""
769
770
771class X509StackEmptyError(X509CertError):
772    """Expecting non-zero length X509Stack"""
773
774
775class X509CertIssuerNotFound(X509CertError):
776    """Raise from verifyCertChain if no certificate can be found to verify the
777    input"""
778
779
780class SelfSignedCert(X509CertError):
781    """Raise from verifyCertChain if cert. is self-signed and
782    rejectSelfSignedCert=True"""
783
784
785class X509CertInvalidSignature(X509CertError):
786    """X.509 Certificate has an invalid signature"""       
787       
788class X509Stack(object):
789    """Wrapper for M2Crypto X509_Stack"""
790   
791    def __init__(self, m2X509Stack=None):
792        """Initialise from an M2Crypto stack object
793       
794        @param m2X509Stack: M2Crypto X.509 stack object
795        @type m2X509Stack: M2Crypto.X509.X509_Stack"""
796       
797        self.__m2X509Stack = m2X509Stack or M2Crypto.X509.X509_Stack()
798       
799    def __len__(self):
800        """@return: length of stack
801        @rtype: int"""
802        return self.__m2X509Stack.__len__()
803
804    def __getitem__(self, idx):
805        """Index stack as an array
806        @param idx: stack index
807        @type idx: int
808        @return: X.509 cert object
809        @rtype: ndg.security.common.X509.X509Cert"""
810       
811        return X509Cert(m2CryptoX509=self.__m2X509Stack.__getitem__(idx))
812   
813    def __iter__(self):
814        """@return: stack iterator
815        @rtype: listiterator"""
816        return iter([X509Cert(m2CryptoX509=i) for i in self.__m2X509Stack])
817
818    def push(self, x509Cert):
819        """Push an X509 certificate onto the stack.
820       
821        @param x509Cert: X509 object.
822        @type x509Cert: M2Crypto.X509.X509,
823        ndg.security.common.X509.X509Cert or basestring
824        @return: The number of X509 objects currently on the stack.
825        @rtype: int"""
826        if isinstance(x509Cert, M2Crypto.X509.X509):
827            return self.__m2X509Stack.push(x509Cert)
828       
829        elif isinstance(x509Cert, X509Cert):
830            return self.__m2X509Stack.push(x509Cert.m2CryptoX509)
831       
832        elif isinstance(x509Cert, basestring):
833            return self.__m2X509Stack.push(\
834                                       X509Cert.Parse(x509Cert).m2CryptoX509)           
835        else:
836            raise X509StackError("Expecting M2Crypto.X509.X509, ndg.security."
837                                 "common.X509.X509Cert or string type")
838               
839    def pop(self):
840        """Pop a certificate from the stack.
841       
842        @return: X509 object that was popped, or None if there is nothing
843        to pop.
844        @rtype: ndg.security.common.X509.X509Cert
845        """
846        return X509Cert(m2CryptoX509=self.__m2X509Stack.pop())
847
848    def asDER(self):
849        """Return the stack as a DER encoded string
850        @return: DER string
851        @rtype: string"""
852        return self.__m2X509Stack.as_der()
853
854    def verifyCertChain(self, 
855                        x509Cert2Verify=None, 
856                        caX509Stack=[],
857                        rejectSelfSignedCert=True):
858        """Treat stack as a list of certificates in a chain of
859        trust.  Validate the signatures through to a single root issuer. 
860
861        @param x509Cert2Verify: X.509 certificate to be verified default is
862        last in the stack
863        @type x509Cert2Verify: X509Cert
864       
865        @param caX509Stack: X.509 stack containing CA certificates that are
866        trusted.
867        @type caX509Stack: X509Stack
868       
869        @param rejectSelfSignedCert: Set to True (default) to raise an
870        SelfSignedCert exception if a certificate in self's stack is
871        self-signed. 
872        @type rejectSelfSignedCert: bool"""
873       
874        n2Validate = len(self)
875        if x509Cert2Verify:
876            # One more to validate in addition to stack content
877            n2Validate += 1
878        else:
879            # Validate starting from last on stack - but check first that it's
880            # populated
881            if n2Validate == 0:
882                raise X509StackEmptyError("Empty stack and no x509Cert2Verify "
883                                          "set: no cert.s to verify")
884
885            x509Cert2Verify = self[-1]
886             
887        # Exit loop if all certs have been validated or if find a self
888        # signed cert.
889        nValidated = 0
890        issuerX509Cert = None
891        while nValidated < n2Validate:
892            issuerX509Cert = None
893            issuerDN = x509Cert2Verify.issuer
894           
895            # Search for issuing certificate in stack
896            for x509Cert in self:
897                if x509Cert.dn == issuerDN:
898                    # Match found - the cert.'s issuer has been found in the
899                    # stack
900                    issuerX509Cert = x509Cert
901                    break
902                   
903            if issuerX509Cert:
904                # An issuing cert. has been found - use it to check the
905                # signature of the cert. to be verified
906                if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
907                    X509CertInvalidSignature('Signature is invalid for cert. '
908                                             '"%s"' % x509Cert2Verify.dn)
909               
910                # In the next iteration the issuer cert. will be checked:
911                # 1) search for a cert. in the stack that issued it
912                # 2) If found use the issuing cert. to verify
913                x509Cert2Verify = issuerX509Cert
914                nValidated += 1
915            else:
916                # All certs in the stack have been searched
917                break
918
919
920        if issuerX509Cert:           
921            # Check for self-signed certificate
922            if (nValidated == 1 and rejectSelfSignedCert and 
923                issuerX509Cert.dn == issuerX509Cert.issuer):
924
925                # If only one iteration occurred then it must be a self
926                # signed certificate
927                raise SelfSignedCert("Certificate is self signed: [DN=%s]" %
928                                     issuerX509Cert.dn)
929           
930            if not caX509Stack:
931                caX509Stack = [issuerX509Cert]
932                         
933        elif not caX509Stack:
934            raise X509CertIssuerNotFound('No issuer certificate found for '
935                                         'certificate "%s"' % 
936                                         x509Cert2Verify.dn)
937           
938        for caCert in caX509Stack:
939            issuerDN = x509Cert2Verify.issuer
940            if caCert.dn == issuerDN:
941                issuerX509Cert = caCert
942                break
943       
944        if issuerX509Cert:
945            if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
946                X509CertInvalidSignature('Signature is invalid for cert. "%s"' %
947                                         x509Cert2Verify.dn)
948           
949            # Chain is validated through to CA cert
950            return
951        else:
952            raise X509CertIssuerNotFound('No issuer cert. found for '
953                                         'certificate "%s"'%x509Cert2Verify.dn)
954       
955        # If this point is reached then an issuing cert is missing from the
956        # chain       
957        raise X509CertIssuerNotFound('Can\'t find issuer cert "%s" for '
958                                     'certificate "%s"' %
959                                     (x509Cert2Verify.issuer, 
960                                      x509Cert2Verify.dn))
961
962
963class InvalidCertSignature(SSL.Checker.SSLVerificationError):
964    """Raise if verification against CA cert public key fails"""
965
966
967class InvalidCertDN(SSL.Checker.SSLVerificationError):
968    """Raise if verification against a list acceptable DNs fails"""
969   
970
971class HostCheck(SSL.Checker.Checker, object):
972    """Override SSL.Checker.Checker to enable alternate Common Name
973    setting match for peer cert"""
974
975    def __init__(self, 
976                 peerCertDN=None, 
977                 peerCertCN=None,
978                 acceptedDNs=[], 
979                 caCertList=[],
980                 caCertFilePathList=[], 
981                 **kw):
982        """Override parent class __init__ to enable setting of myProxyServerDN
983        setting
984       
985        @type peerCertDN: string/list
986        @param peerCertDN: Set the expected Distinguished Name of the
987        server to avoid errors matching hostnames.  This is useful
988        where the hostname is not fully qualified. 
989
990        *param acceptedDNs: a list of acceptable DNs.  This enables validation
991        where the expected DN is where against a limited list of certs.
992       
993        @type peerCertCN: string
994        @param peerCertCN: enable alternate Common Name to peer
995        hostname
996       
997        @type caCertList: list type of M2Crypto.X509.X509 types
998        @param caCertList: CA X.509 certificates - if set the peer cert's
999        CA signature is verified against one of these.  At least one must
1000        verify
1001       
1002        @type caCertFilePathList: list string types
1003        @param caCertFilePathList: same as caCertList except input as list
1004        of CA cert file paths"""
1005       
1006        SSL.Checker.Checker.__init__(self, **kw)
1007       
1008        self.peerCertDN = peerCertDN
1009        self.peerCertCN = peerCertCN
1010        self.acceptedDNs = acceptedDNs
1011       
1012        if caCertList:
1013            self.caCertList = caCertList
1014        elif caCertFilePathList:
1015            self.caCertFilePathList = caCertFilePathList
1016        else:
1017            # Set default to enable len() test in __call__
1018            self.__caCertStack = ()
1019           
1020    def __call__(self, peerCert, host=None):
1021        """Carry out checks on server ID
1022        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
1023        instance
1024        @param host: name of host to check
1025        """
1026        if peerCert is None:
1027            raise SSL.Checker.NoCertificate('SSL Peer did not return '
1028                                            'certificate')
1029
1030        peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ', '/')
1031        try:
1032            SSL.Checker.Checker.__call__(self, peerCert, host=self.peerCertCN)
1033           
1034        except SSL.Checker.WrongHost, e:
1035            # Try match against peerCertDN set   
1036            if peerCertDN != self.peerCertDN:
1037                raise e
1038
1039        # At least one match should be found in the list - first convert to
1040        # NDG X500DN type to allow per field matching for DN comparison
1041        peerCertX500DN = X500DN(dn=peerCertDN)
1042       
1043        if self.acceptedDNs:
1044           matchFound = False
1045           for dn in self.acceptedDNs:
1046               x500dn = X500DN(dn=dn)
1047               if x500dn == peerCertX500DN:
1048                   matchFound = True
1049                   break
1050               
1051           if not matchFound:
1052               raise InvalidCertDN('Peer cert DN "%s" doesn\'t match '
1053                                   'verification list' % peerCertDN)
1054
1055        if len(self.__caCertStack) > 0:
1056            try:
1057                self.__caCertStack.verifyCertChain(
1058                           x509Cert2Verify=X509Cert(m2CryptoX509=peerCert))
1059            except Exception, e:
1060                raise InvalidCertSignature("Peer certificate verification "
1061                                           "against CA certificate failed: %s" 
1062                                           % e)
1063             
1064        # They match - drop the exception and return all OK instead         
1065        return True
1066     
1067    def __setCACertList(self, caCertList):
1068        """Set list of CA certs - peer cert must validate against at least one
1069        of these"""
1070        self.__caCertStack = X509Stack()
1071        for caCert in caCertList:
1072            self.__caCertStack.push(caCert)
1073
1074    caCertList = property(fset=__setCACertList,
1075                          doc="list of CA certificates - the peer certificate "
1076                              "must validate against one")
1077
1078    def __setCACertsFromFileList(self, caCertFilePathList):
1079        '''Read CA certificates from file and add them to the X.509
1080        stack
1081       
1082        @type caCertFilePathList: basestring, list or tuple
1083        @param caCertFilePathList: list of file paths for CA certificates to
1084        be used to verify certificate used to sign message.  If a single
1085        string item is input then this is converted into a tuple
1086        '''
1087        if isinstance(caCertFilePathList, basestring):
1088            caCertFilePathList = (caCertFilePathList,)
1089           
1090        elif not isinstance(caCertFilePathList, (list, tuple)):
1091            raise TypeError('Expecting a basestring, list or tuple type for '
1092                            '"caCertFilePathList"')
1093
1094        self.__caCertStack = X509Stack()
1095
1096        for caCertFilePath in caCertFilePathList:
1097            self.__caCertStack.push(X509.load_cert(caCertFilePath))
1098       
1099    caCertFilePathList = property(fset=__setCACertsFromFileList,
1100                                  doc="list of CA certificate file paths - "
1101                                      "peer certificate must validate against "
1102                                      "one")
1103
1104
1105class HTTPSConnection(_HTTPSConnection):
1106    """Modified version of M2Crypto equivalent to enable custom checks with
1107    the peer and timeout settings
1108   
1109    @type defReadTimeout: M2Crypto.SSL.timeout
1110    @cvar defReadTimeout: default timeout for read operations
1111    @type defWriteTimeout: M2Crypto.SSL.timeout
1112    @cvar defWriteTimeout: default timeout for write operations"""   
1113    defReadTimeout = SSL.timeout(sec=20.)
1114    defWriteTimeout = SSL.timeout(sec=20.)
1115   
1116    def __init__(self, *args, **kw):
1117        '''Overload to enable setting of post connection check
1118        callback to SSL.Connection
1119       
1120        type *args: tuple
1121        param *args: args which apply to M2Crypto.httpslib.HTTPSConnection
1122        type **kw: dict
1123        param **kw: additional keywords
1124        @type postConnectionCheck: SSL.Checker.Checker derivative
1125        @keyword postConnectionCheck: set class for checking peer
1126        @type readTimeout: M2Crypto.SSL.timeout
1127        @keyword readTimeout: readTimeout - set timeout for read
1128        @type writeTimeout: M2Crypto.SSL.timeout
1129        @keyword writeTimeout: similar to read timeout'''
1130       
1131        self._postConnectionCheck = kw.pop('postConnectionCheck',
1132                                           SSL.Checker.Checker)
1133       
1134        if 'readTimeout' in kw:
1135            if not isinstance(kw['readTimeout'], SSL.timeout):
1136                raise AttributeError("readTimeout must be of type "
1137                                     "M2Crypto.SSL.timeout")
1138            self.readTimeout = kw.pop('readTimeout')
1139        else:
1140            self.readTimeout = HTTPSConnection.defReadTimeout
1141             
1142        if 'writeTimeout' in kw:
1143            if not isinstance(kw['writeTimeout'], SSL.timeout):
1144                raise AttributeError("writeTimeout must be of type "
1145                                     "M2Crypto.SSL.timeout") 
1146            self.writeTimeout = kw.pop('writeTimeout')
1147        else:
1148            self.writeTimeout = HTTPSConnection.defWriteTimeout
1149   
1150        self._clntCertFilePath = kw.pop('clntCertFilePath', None)
1151        self._clntPriKeyFilePath = kw.pop('clntPriKeyFilePath', None)
1152       
1153        _HTTPSConnection.__init__(self, *args, **kw)
1154       
1155        # load up certificate stuff
1156        if (self._clntCertFilePath is not None and 
1157            self._clntPriKeyFilePath is not None):
1158            self.ssl_ctx.load_cert(self._clntCertFilePath, 
1159                                   self._clntPriKeyFilePath)
1160       
1161       
1162    def connect(self):
1163        '''Overload M2Crypto.httpslib.HTTPSConnection to enable
1164        custom post connection check of peer certificate and socket timeout'''
1165
1166        self.sock = SSL.Connection(self.ssl_ctx)
1167        self.sock.set_post_connection_check_callback(self._postConnectionCheck)
1168
1169        self.sock.set_socket_read_timeout(self.readTimeout)
1170        self.sock.set_socket_write_timeout(self.writeTimeout)
1171
1172        self.sock.connect((self.host, self.port))
1173
1174    def putrequest(self, method, url, **kw):
1175        '''Overload to work around bug with unicode type URL'''
1176        url = str(url)
1177        _HTTPSConnection.putrequest(self, method, url, **kw) 
1178         
1179             
1180class SSLContextProxy(object):
1181    """Holder for M2Crypto.SSL.Context parameters"""
1182    PRE_VERIFY_FAIL, PRE_VERIFY_OK = range(2)
1183   
1184    SSL_CERT_FILEPATH_OPTNAME = "sslCertFilePath"
1185    SSL_PRIKEY_FILEPATH_OPTNAME = "sslPriKeyFilePath"
1186    SSL_PRIKEY_PWD_OPTNAME = "sslPriKeyPwd"
1187    SSL_CACERT_FILEPATH_OPTNAME = "sslCACertFilePath"
1188    SSL_CACERT_DIRPATH_OPTNAME = "sslCACertDir"
1189    SSL_VALID_DNS_OPTNAME = "sslValidDNs"
1190   
1191    OPTNAMES = (
1192        SSL_CERT_FILEPATH_OPTNAME,
1193        SSL_PRIKEY_FILEPATH_OPTNAME,
1194        SSL_PRIKEY_PWD_OPTNAME,
1195        SSL_CACERT_FILEPATH_OPTNAME,
1196        SSL_CACERT_DIRPATH_OPTNAME,
1197        SSL_VALID_DNS_OPTNAME
1198    )
1199   
1200    __slots__ = tuple(["__%s" % name for name in OPTNAMES])
1201    del name
1202   
1203    VALID_DNS_PAT = re.compile(',\s*')
1204   
1205    def __init__(self):
1206        self.__sslCertFilePath = None
1207        self.__sslPriKeyFilePath = None
1208        self.__sslPriKeyPwd = None
1209        self.__sslCACertFilePath = None
1210        self.__sslCACertDir = None
1211        self.__sslValidDNs = []
1212
1213    def createCtx(self, depth=9, **kw):
1214        """Create an M2Crypto SSL Context from this objects properties
1215        @type depth: int
1216        @param depth: max. depth of certificate to verify against
1217        @type kw: dict
1218        @param kw: M2Crypto.SSL.Context keyword arguments
1219        @rtype: M2Crypto.SSL.Context
1220        @return M2Crypto SSL context object
1221        """
1222        ctx = SSL.Context(**kw)
1223       
1224        # Configure context according to this proxy's attributes
1225        if self.sslCertFilePath and self.sslPriKeyFilePath:
1226            # Pass client certificate
1227            ctx.load_cert(self.sslCertFilePath, 
1228                          self.__sslPriKeyFilePath, 
1229                          lambda *arg, **kw: self.sslPriKeyPwd)
1230            log.debug("Set client certificate and key in SSL Context")
1231        else:
1232            log.debug("No client certificate or key set in SSL Context")
1233           
1234        if self.sslCACertFilePath or self.sslCACertDir:
1235            # Set CA certificates in order to verify peer
1236            ctx.load_verify_locations(self.sslCACertFilePath, 
1237                                      self.sslCACertDir)
1238            mode = SSL.verify_peer|SSL.verify_fail_if_no_peer_cert
1239        else:
1240            mode = SSL.verify_fail_if_no_peer_cert
1241            log.warning('No CA certificate files set: mode set to '
1242                        '"verify_fail_if_no_peer_cert" only')
1243           
1244        if len(self.sslValidDNs) > 0:
1245            # Set custom callback in order to verify peer certificate DN
1246            # against whitelist
1247            callback = self.createVerifySSLPeerCertCallback()
1248            log.debug('Set peer certificate Distinguished Name check set in '
1249                      'SSL Context')
1250        else:
1251            callback = None
1252            log.warning('No peer certificate Distinguished Name check set in '
1253                        'SSL Context')
1254           
1255        ctx.set_verify(mode, depth, callback=callback)
1256           
1257        return ctx
1258 
1259    def copy(self, sslCtxProxy):
1260        """Copy settings from another context object
1261        """
1262        if not isinstance(sslCtxProxy, SSLContextProxy):
1263            raise TypeError('Expecting %r for copy method input object; '
1264                            'got %r' % (SSLContextProxy, type(sslCtxProxy)))
1265       
1266        for name in SSLContextProxy.OPTNAMES:
1267            setattr(self, name, getattr(sslCtxProxy, name))
1268           
1269    def createVerifySSLPeerCertCallback(self):
1270        """Create a callback function to enable the DN of the peer in an SSL
1271        connection to be verified against a whitelist. 
1272       
1273        Nb. Making this function within the scope of a method of the class to
1274        enables to access instance variables
1275        """
1276       
1277        def _verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
1278            '''SSL verify callback function used to control the behaviour when
1279            the SSL_VERIFY_PEER flag is set.  See:
1280           
1281            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
1282           
1283            This implementation applies verification in order to check the DN
1284            of the peer certificate against a whitelist
1285           
1286            @type preVerifyOK: int
1287            @param preVerifyOK: If a verification error is found, this
1288            parameter will be set to 0
1289            @type x509StoreCtx: M2Crypto.X509.X509_Store_Context
1290            @param x509StoreCtx: locate the certificate to be verified and
1291            perform additional verification steps as needed
1292            @rtype: int
1293            @return: controls the strategy of the further verification process.
1294            - If verify_callback returns 0, the verification process is
1295            immediately stopped with "verification failed" state. If
1296            SSL_VERIFY_PEER is set, a verification failure alert is sent to the
1297            peer and the TLS/SSL handshake is terminated.
1298            - If verify_callback returns 1, the verification process is
1299            continued.
1300            If verify_callback always returns 1, the TLS/SSL handshake will not
1301            be terminated with respect to verification failures and the
1302            connection
1303            will be established. The calling process can however retrieve the
1304            error code of the last verification error using
1305            SSL_get_verify_result or by maintaining its own error storage
1306            managed by verify_callback.
1307            '''
1308            if preVerifyOK == 0:
1309                # Something is wrong with the certificate don't bother
1310                # proceeding any further
1311                log.error("verifyCallback: pre-verify OK flagged an error "
1312                          "with the peer certificate, returning error state "
1313                          "to caller ...")
1314                return preVerifyOK
1315           
1316            x509CertChain = x509StoreCtx.get1_chain()
1317            for cert in x509CertChain:
1318                x509Cert = X509Cert.fromM2Crypto(cert)
1319                if x509Cert.dn in self.sslValidDNs:
1320                    return preVerifyOK
1321               
1322                subject = cert.get_subject()
1323                dn = subject.as_text()
1324                log.debug("verifyCallback: dn = %r", dn)
1325               
1326            # No match found so return fail status
1327            log.debug("No match for peer certificate %s in DN whitelist %r",
1328                      x509Cert.dn, self.sslValidDNs)
1329            return SSLContextProxy.PRE_VERIFY_FAIL
1330       
1331        return _verifySSLPeerCertCallback
1332
1333    def _getSSLCertFilePath(self):
1334        return self.__sslCertFilePath
1335   
1336    def _setSSLCertFilePath(self, filePath):
1337        "Set X.509 cert file path property method"
1338       
1339        if isinstance(filePath, basestring):
1340            filePath = os.path.expandvars(filePath)
1341           
1342        elif filePath is not None:
1343            raise TypeError("X.509 cert. file path must be a valid string")
1344       
1345        self.__sslCertFilePath = filePath
1346               
1347    sslCertFilePath = property(fset=_setSSLCertFilePath,
1348                               fget=_getSSLCertFilePath,
1349                               doc="File path to X.509 cert.")
1350       
1351    def _getSSLCACertFilePath(self):
1352        """Get file path for list of CA cert or certs used to validate SSL
1353        connections
1354       
1355        @rtype sslCACertFilePath: basestring
1356        @return sslCACertFilePathList: file path to file containing concatenated
1357        PEM encoded CA certificates."""
1358        return self.__sslCACertFilePath
1359   
1360    def _setSSLCACertFilePath(self, value):
1361        """Set CA cert file path
1362       
1363        @type sslCACertFilePath: basestring, list, tuple or None
1364        @param sslCACertFilePath: file path to CA certificate file.  If None
1365        then the input is quietly ignored."""
1366        if isinstance(value, basestring):
1367            self.__sslCACertFilePath = os.path.expandvars(value)
1368           
1369        elif value is None:
1370            self.__sslCACertFilePath = value
1371           
1372        else:
1373            raise TypeError("Input CA Certificate file path must be "
1374                            "a valid string or None type: %r" % type(value)) 
1375       
1376       
1377    sslCACertFilePath = property(fget=_getSSLCACertFilePath,
1378                                 fset=_setSSLCACertFilePath,
1379                                 doc="Path to file containing concatenated PEM "
1380                                     "encoded CA Certificates - used for "
1381                                     "verification of peer certs in SSL "
1382                                     "connection")
1383       
1384    def _getSSLCACertDir(self):
1385        """Get file path for list of CA cert or certs used to validate SSL
1386        connections
1387       
1388        @rtype sslCACertDir: basestring
1389        @return sslCACertDirList: directory containing PEM encoded CA
1390        certificates."""
1391        return self.__sslCACertDir
1392   
1393    def _setSSLCACertDir(self, value):
1394        """Set CA cert or certs to validate AC signatures, signatures
1395        of Attribute Authority SOAP responses and SSL connections where
1396        AA SOAP service is run over SSL.
1397       
1398        @type sslCACertDir: basestring
1399        @param sslCACertDir: directory containing CA certificate files.
1400        """
1401        if isinstance(value, basestring):
1402            self.__sslCACertDir = os.path.expandvars(value)
1403        elif value is None:
1404            self.__sslCACertDir = value
1405        else:
1406            raise TypeError("Input CA Certificate directroy must be "
1407                            "a valid string or None type: %r" % type(value))     
1408       
1409    sslCACertDir = property(fget=_getSSLCACertDir,
1410                            fset=_setSSLCACertDir,
1411                            doc="Path to directory containing PEM encoded CA "
1412                                "Certificates used for verification of peer "
1413                                "certs in SSL connection.   Files in the "
1414                                "directory must be named with the form "
1415                                "<hash>.0 where <hash> can be obtained using "
1416                                "openssl x509 -in cert -hash -noout or using "
1417                                "the c_rehash OpenSSL script")
1418   
1419    def _getSslValidDNs(self):
1420        return self.__sslValidDNs
1421
1422    def _setSslValidDNs(self, value):
1423        if isinstance(value, basestring): 
1424            pat = SSLContextProxy.VALID_DNS_PAT
1425            self.__sslValidDNs = [X500DN.fromString(dn) 
1426                                  for dn in pat.split(value)]
1427           
1428        elif isinstance(value, (tuple, list)):
1429            self.__sslValidDNs = [X500DN.fromString(dn) for dn in value]
1430        else:
1431            raise TypeError('Expecting list/tuple or basestring type for "%s" '
1432                            'attribute; got %r' %
1433                            (SSLContextProxy.SSL_VALID_DNS_OPTNAME, 
1434                             type(value)))
1435   
1436    sslValidDNs = property(_getSslValidDNs, 
1437                           _setSslValidDNs, 
1438                           doc="whitelist of acceptable certificate "
1439                               "Distinguished Names for peer certificates in "
1440                               "SSL requests")
1441
1442    def _getSSLPriKeyFilePath(self):
1443        return self.__sslPriKeyFilePath
1444   
1445    def _setSSLPriKeyFilePath(self, filePath):
1446        "Set ssl private key file path property method"
1447       
1448        if isinstance(filePath, basestring):
1449            filePath = os.path.expandvars(filePath)
1450
1451        elif filePath is not None:
1452            raise TypeError("Private key file path must be a valid "
1453                            "string or None type")
1454       
1455        self.__sslPriKeyFilePath = filePath
1456       
1457    sslPriKeyFilePath = property(fget=_getSSLPriKeyFilePath,
1458                                 fset=_setSSLPriKeyFilePath,
1459                                 doc="File path to SSL private key")
1460 
1461    def _setSSLPriKeyPwd(self, sslPriKeyPwd):
1462        "Set method for ssl private key file password"
1463        if not isinstance(sslPriKeyPwd, (type(None), basestring)):
1464            raise TypeError("Signing private key password must be None "
1465                            "or a valid string")
1466       
1467        # Explicitly convert to string as M2Crypto OpenSSL wrapper fails with
1468        # unicode type
1469        self.__sslPriKeyPwd = str(sslPriKeyPwd)
1470
1471    def _getSSLPriKeyPwd(self):
1472        "Get property method for SSL private key"
1473        return self.__sslPriKeyPwd
1474       
1475    sslPriKeyPwd = property(fset=_setSSLPriKeyPwd,
1476                             fget=_getSSLPriKeyPwd,
1477                             doc="Password protecting SSL private key file")
1478
1479    def __getstate__(self):
1480        '''Enable pickling for use with beaker.session'''
1481        _dict = {}
1482        for attrName in SSLContextProxy.__slots__:
1483            # Ugly hack to allow for derived classes setting private member
1484            # variables
1485            if attrName.startswith('__'):
1486                attrName = "_SSLContextProxy" + attrName
1487               
1488            _dict[attrName] = getattr(self, attrName)
1489           
1490        return _dict
1491       
1492    def __setstate__(self, attrDict):
1493        '''Enable pickling for use with beaker.session'''
1494        for attr, val in attrDict.items():
1495            setattr(self, attr, val)
Note: See TracBrowser for help on using the repository browser.