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

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

Added SOAPFault handling for ndg.soap package. Needs integration into ndg.saml.saml2.binding.soap.server.wsgi.queryinterface to enable SAML query interface to do better error reporting.

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