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

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

Incomplete - task 2: XACML-Security Integration

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