source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/X509.py @ 4603

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/X509.py@4603
Revision 4603, 28.2 KB checked in by pjkersha, 12 years ago (diff)

#1004: initial work on Security Filter:

  • started new SSLClientAuthNMiddleware WSGI to filter configured URL paths applying SSL client authentication. Gets SSL client certificate from Apache SSL_CLIENT_CERT environment variable and so to work it requires deployment in mod_wsgi with SSL in order to pick up environ.
  • AppLoaderMiddleware?: convenience utility enables mod_wsgi application to parse a Paste Deploy style ini file containing an app and app pipeline as required.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""X.509 certificate handling class encapsulates M2Crypto.X509
2
3NERC Data Grid Project
4"""
5__author__ = "P J Kershaw"
6__date__ = "05/04/05"
7__copyright__ = "(C) 2007 STFC & NERC"
8__license__ = \
9"""This software may be distributed under the terms of the Q Public
10License, version 1.0 or later."""
11__contact__ = "Philip.Kershaw@stfc.ac.uk"
12__revision__ = '$Id$'
13
14
15import types
16import re
17
18# Handle not before and not after strings
19from time import strftime
20from time import strptime
21from datetime import datetime
22
23import M2Crypto
24
25
26class X509CertError(Exception):
27    """Exception handling for NDG X.509 Certificate handling class."""
28
29class X509CertInvalidNotBeforeTime(X509CertError):
30    """Call from X509Cert.isValidTime if certificates not before time is
31    BEFORE the current system time"""
32   
33class X509CertExpired(X509CertError):
34    """Call from X509Cert.isValidTime if certificate has expired"""
35
36   
37class X509Cert(object):
38    "NDG X509 Certificate Handling"
39
40    def __init__(self, filePath=None, m2CryptoX509=None):
41
42        # Set certificate file path
43        if filePath is not None:
44            if not isinstance(filePath, basestring):
45                raise X509CertError(
46                        "Certificate File Path input must be a valid string")
47           
48        self.__filePath = filePath           
49        self.__dn = None
50        self.__dtNotBefore = None
51        self.__dtNotAfter = None
52       
53        if m2CryptoX509:
54            self.__setM2CryptoX509(m2CryptoX509)
55       
56
57    def read(self, filePath=None):
58        """Read a certificate from file"""
59       
60        # Check for optional input certificate file path
61        if filePath is not None:
62            if not isinstance(filePath, basestring):
63                raise X509CertError(
64                    "Certificate File Path input must be a valid string")
65           
66            self.__filePath = filePath
67       
68        try:
69            self.__m2CryptoX509 = M2Crypto.X509.load_cert(self.__filePath)
70        except Exception, e:
71            raise X509CertError("Error loading certificate \"%s\": %s" % \
72                                (self.__filePath, str(e)))
73
74        # Update DN and validity times from M2Crypto X509 object just
75        # created
76        self.__setM2CryptoX509()
77
78
79    def parse(self, certTxt):
80        """Read a certificate input as a string"""
81
82        try:
83            # Create M2Crypto memory buffer and pass to load certificate
84            # method
85            #
86            # Nb. input converted to standard string - buffer method won't
87            # accept unicode type strings
88            certBIO = M2Crypto.BIO.MemoryBuffer(str(certTxt))
89            self.__m2CryptoX509 = M2Crypto.X509.load_cert_bio(certBIO)
90           
91        except Exception, e:
92            raise X509CertError, "Error loading certificate: %s" % str(e)
93
94        # Update DN and validity times from M2Crypto X509 object just
95        # created
96        self.__setM2CryptoX509()
97
98     
99    def __setM2CryptoX509(self, m2CryptoX509=None):
100        """Private method allows class members to be updated from the
101        current M2Crypto object.  __m2CryptoX509 must have been set."""
102       
103        if m2CryptoX509 is not None:
104            if not isinstance(m2CryptoX509, M2Crypto.X509.X509):
105                raise TypeError, \
106                    "Incorrect type for input M2Crypto.X509.X509 object"
107                   
108            self.__m2CryptoX509 = m2CryptoX509
109           
110           
111        # Get distinguished name
112        m2CryptoX509Name = self.__m2CryptoX509.get_subject()
113
114        # Instantiate X500 Distinguished name
115        self.__dn = X500DN(m2CryptoX509Name=m2CryptoX509Name)
116
117
118        # Get not before and not after validity times
119        #
120        # Only option for M2Crypto seems to be to return the times as
121        # formatted strings and then parse them in order to create a datetime
122        # type
123       
124        try:
125            m2CryptoNotBefore = self.__m2CryptoX509.get_not_before()
126            self.__dtNotBefore=self.__m2CryptoUTC2datetime(m2CryptoNotBefore)
127                                       
128        except Exception, e:
129            raise X509CertError, "Not Before time: " + str(e)
130
131       
132        try:
133            m2CryptoNotAfter = self.__m2CryptoX509.get_not_after()
134            self.__dtNotAfter = self.__m2CryptoUTC2datetime(m2CryptoNotAfter)
135                                   
136        except Exception, e:
137            raise X509CertError, "Not After time: " + str(e)
138
139
140    def __getM2CryptoX509(self, m2CryptoX509=None):
141        "Return M2Crypto X.509 cert object"
142        return self.__m2CryptoX509
143   
144   
145    m2CryptoX509 = property(fset=__setM2CryptoX509,
146                            fget=__getM2CryptoX509,
147                            doc="M2Crypto.X509.X509 type")
148
149       
150    def toString(self, **kw):
151        """Return certificate file content as a PEM format
152        string"""
153        return self.asPEM(**kw)
154       
155    def asPEM(self, filePath=None):
156        """Return certificate file content as a PEM format
157        string"""
158       
159        # Check M2Crypto.X509 object has been instantiated - if not call
160        # read method
161        if self.__m2CryptoX509 is None:
162            self.read(filePath)
163           
164        return self.__m2CryptoX509.as_pem()
165
166       
167    def asDER(self):
168        """Return certificate file content in DER format"""
169       
170        # Check M2Crypto.X509 object has been instantiated
171        assert(self.__m2CryptoX509)
172        return self.__m2CryptoX509.as_der()
173
174   
175    # Make some attributes accessible as read-only
176    def __getDN(self):
177        """Get X500 Distinguished Name."""
178        return self.__dn
179
180    dn = property(fget=__getDN, doc="X.509 Distinguished Name")
181
182
183    def __getVersion(self):
184        """Get X.509 Certificate version"""
185        if self.__m2CryptoX509 is None:
186            return None
187       
188        return self.__m2CryptoX509.get_version()
189
190    version = property(fget=__getVersion, doc="X.509 Certificate version")
191       
192       
193    def __getSerialNumber(self):
194        """Get Serial Number"""
195        if self.__m2CryptoX509 is None:
196            return None
197       
198        return self.__m2CryptoX509.get_serial_number()
199   
200    serialNumber = property(fget=__getSerialNumber, 
201                            doc="X.509 Certificate Serial Number")
202       
203
204    def __getNotBefore(self):
205        """Get not before validity time as datetime type"""
206        if self.__m2CryptoX509 is None:
207            return None
208       
209        return self.__dtNotBefore
210
211    notBefore = property(fget=__getNotBefore, 
212                         doc="Not before validity time as datetime type")
213       
214       
215    def __getNotAfter(self):
216        """Get not after validity time as datetime type"""
217        if self.__m2CryptoX509 is None:
218            return None
219       
220        return self.__dtNotAfter
221
222    notAfter = property(fget=__getNotAfter, 
223                         doc="Not after validity time as datetime type")
224       
225       
226    def __getPubKey(self):
227        """Get public key
228       
229        @return: RSA public key for certificate
230        @rtype: M2Crypto.RSA.RSA_pub"""
231        if self.__m2CryptoX509 is None:
232            return None
233       
234        return self.__m2CryptoX509.get_pubkey()
235
236    pubKey = property(fget=__getPubKey,  doc="Public Key")
237       
238       
239    def __getIssuer(self):
240        """Get Certificate issuer"""
241        if self.__m2CryptoX509 is None:
242            return None
243       
244        # Return as X500DN type
245        return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_issuer())
246
247    issuer = property(fget=__getIssuer,  doc="Certificate Issuer")
248       
249   
250    def __getSubject(self):
251        """Get Certificate subject"""
252        if self.__m2CryptoX509 is None:
253            return None
254
255        # Return as X500DN type
256        return X500DN(m2CryptoX509Name=self.__m2CryptoX509.get_subject())
257   
258    subject = property(fget=__getSubject,  doc="Certificate subject")
259
260
261    def isValidTime(self, raiseExcep=False):
262        """Check Certificate for expiry
263
264        raiseExcep: set True to raise an exception if certificate is invalid"""
265
266        if not isinstance(self.__dtNotBefore, datetime):
267            raise X509CertError("Not Before datetime is not set")
268
269        if not isinstance(self.__dtNotAfter, datetime):
270            raise X509CertError("Not After datetime is not set")
271       
272        dtNow = datetime.utcnow()
273
274        if raiseExcep:
275            if dtNow < self.__dtNotBefore:
276                raise X509CertInvalidNotBeforeTime, \
277                    "Current time is before the certificate's Not Before Time"
278           
279            elif dtNow > self.__dtNotAfter:
280                raise X509CertExpired, \
281                    "Certificate, %s, has expired: the time now is %s  \
282                    and the certificate expiry is %s." \
283                    %(self.__filePath, dtNow, self.__dtNotAfter)
284        else:
285            return dtNow > self.__dtNotBefore and dtNow < self.__dtNotAfter
286
287
288
289
290    def __m2CryptoUTC2datetime(self, m2CryptoUTC):
291        """Convert M2Crypto UTC time string as returned by get_not_before/
292        get_not_after methods into datetime type"""
293       
294        datetimeRE = "([a-zA-Z]{3} {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4}).*"
295        sM2CryptoUTC = None
296       
297        try:
298            # Convert into string
299            sM2CryptoUTC = str(m2CryptoUTC)
300           
301            # Check for expected format - string may have trailing GMT - ignore
302            sTime = re.findall(datetimeRE, sM2CryptoUTC)[0]
303
304            # Convert into a tuple
305            lTime = strptime(sTime, "%b %d %H:%M:%S %Y")[0:6]
306
307            return datetime(lTime[0], lTime[1], lTime[2],
308                            lTime[3], lTime[4], lTime[5])
309                                   
310        except Exception, e:
311            msg = "Error parsing M2Crypto UTC"
312            if sM2CryptoUTC is not None:
313                msg += ": " + sM2CryptoUTC
314               
315            raise X509CertError(msg)
316       
317    def verify(self, pubKey, **kw):
318        """Verify a certificate against the public key of the
319        issuer
320       
321        @param pubKey: public key of cert that issued self
322        @type pubKey: M2Crypto.RSA.RSA_pub
323        @param **kw: keywords to pass to M2Crypto.X509.X509 -
324        'pkey'
325        @type: dict
326        @return: True if verifies OK, False otherwise
327        @rtype: bool
328        """
329        return bool(self.__m2CryptoX509.verify(pubKey, **kw))
330
331    @classmethod
332    def Read(cls, filePath):
333        """Create a new X509 certificate read in from a file"""
334   
335        x509Cert = cls(filePath=filePath)
336        x509Cert.read()
337       
338        return x509Cert
339   
340    @classmethod
341    def Parse(cls, x509CertTxt):
342        """Create a new X509 certificate from string of file content"""
343   
344        x509Cert = cls()
345        x509Cert.parse(x509CertTxt)
346       
347        return x509Cert
348       
349#_____________________________________________________________________________
350# Alternative AttCert constructors
351#
352def X509CertRead(filePath):
353    """Create a new X509 certificate read in from a file"""
354
355    x509Cert = X509Cert(filePath=filePath)
356    x509Cert.read()
357   
358    return x509Cert
359
360
361#_____________________________________________________________________________
362def X509CertParse(x509CertTxt):
363    """Create a new X509 certificate from string of file content"""
364
365    x509Cert = X509Cert()
366    x509Cert.parse(x509CertTxt)
367   
368    return x509Cert
369
370
371#_____________________________________________________________________________
372class X509StackError(Exception):
373    """Error from X509Stack type"""
374
375#_____________________________________________________________________________
376class CertIssuerNotFound(X509StackError):
377    """Raise from verifyCertChain if no certificate can be found to verify the
378    input"""
379
380class SelfSignedCert(X509StackError):
381    """Raise from verifyCertChain if cert. is self-signed and
382    rejectSelfSignedCert=True"""
383       
384#_____________________________________________________________________________
385class X509Stack(object):
386    """Wrapper for M2Crypto X509_Stack"""
387   
388    def __init__(self, m2X509Stack=None):
389        """Initialise from an M2Crypto stack object
390       
391        @param m2X509Stack: M2Crypto X.509 stack object
392        @type m2X509Stack: M2Crypto.X509.X509_Stack"""
393       
394        self.__m2X509Stack = m2X509Stack or M2Crypto.X509.X509_Stack()
395       
396    def __len__(self):
397        """@return: length of stack
398        @rtype: int"""
399        return self.__m2X509Stack.__len__()
400
401    def __getitem__(self, idx):
402        """Index stack as an array
403        @param idx: stack index
404        @type idx: int
405        @return: X.509 cert object
406        @rtype: ndg.security.common.X509.X509Cert"""
407       
408        return X509Cert(m2CryptoX509=self.__m2X509Stack.__getitem__(idx))
409   
410    def __iter__(self):
411        """@return: stack iterator
412        @rtype: listiterator"""
413        return iter([X509Cert(m2CryptoX509=i) for i in self.__m2X509Stack])
414
415    def push(self, x509Cert):
416        """Push an X509 certificate onto the stack.
417       
418        @param x509Cert: X509 object.
419        @type x509Cert: M2Crypto.X509.X509,
420        ndg.security.common.X509.X509Cert or basestring
421        @return: The number of X509 objects currently on the stack.
422        @rtype: int"""
423        if isinstance(x509Cert, M2Crypto.X509.X509):
424            return self.__m2X509Stack.push(x509Cert)
425       
426        elif isinstance(x509Cert, X509Cert):
427            return self.__m2X509Stack.push(x509Cert.m2CryptoX509)
428       
429        elif isinstance(x509Cert, basestring):
430            return self.__m2X509Stack.push(\
431                                       X509CertParse(x509Cert).m2CryptoX509)           
432        else:
433            raise X509StackError, "Expecting M2Crypto.X509.X509, " + \
434                "ndg.security.common.X509.X509Cert or string type"
435               
436    def pop(self):
437        """Pop a certificate from the stack.
438       
439        @return: X509 object that was popped, or None if there is nothing
440        to pop.
441        @rtype: ndg.security.common.X509.X509Cert
442        """
443        return X509Cert(m2CryptoX509=self.__m2X509Stack.pop())
444
445
446    def asDER(self):
447        """Return the stack as a DER encoded string
448        @return: DER string
449        @rtype: string"""
450        return self.__m2X509Stack.as_der()
451
452
453    def verifyCertChain(self, 
454                        x509Cert2Verify=None, 
455                        caX509Stack=[],
456                        rejectSelfSignedCert=True):
457        """Treat stack as a list of certificates in a chain of
458        trust.  Validate the signatures through to a single root issuer. 
459
460        @param x509Cert2Verify: X.509 certificate to be verified default is
461        last in the stack
462        @type x509Cert2Verify: X509Cert
463       
464        @param caX509Stack: X.509 stack containing CA certificates that are
465        trusted.
466        @type caX509Stack: X509Stack
467       
468        @param rejectSelfSignedCert: Set to True (default) to raise an
469        SelfSignedCert exception if a certificate in self's stack is
470        self-signed. 
471        @type rejectSelfSignedCert: bool"""
472       
473        n2Validate = len(self)
474        if x509Cert2Verify:
475            # One more to validate in addition to stack content
476            n2Validate += 1
477        else:
478            # Validate starting from last on stack - but check first that it's
479            # populated
480            if n2Validate == 0:
481                raise X509StackError, \
482                "Empty stack and no x509Cert2Verify set: no cert.s to verify"
483
484            x509Cert2Verify = self[-1]
485             
486               
487        # Exit loop if all certs have been validated or if find a self
488        # signed cert.
489        nValidated = 0
490        issuerX509Cert = None
491        while nValidated < n2Validate:               
492            issuerX509Cert = None
493            issuerDN = x509Cert2Verify.issuer
494           
495            # Search for issuing certificate in stack
496            for x509Cert in self:
497                if x509Cert.dn == issuerDN:
498                    # Match found - the cert.'s issuer has been found in the
499                    # stack
500                    issuerX509Cert = x509Cert
501                    break
502                   
503            if issuerX509Cert:
504                # An issuing cert. has been found - use it to check the
505                # signature of the cert. to be verified
506                if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
507                    X509CertError, 'Signature is invalid for cert. "%s"' % \
508                                    x509Cert2Verify.dn
509               
510                # In the next iteration the issuer cert. will be checked:
511                # 1) search for a cert. in the stack that issued it
512                # 2) If found use the issuing cert. to verify
513                x509Cert2Verify = issuerX509Cert
514                nValidated += 1
515            else:
516                # All certs in the stack have been searched
517                break
518
519
520        if issuerX509Cert:           
521            # Check for self-signed certificate
522            if nValidated == 1 and rejectSelfSignedCert and \
523               issuerX509Cert.dn == issuerX509Cert.issuer:
524
525                # If only one iteration occured then it must be a self
526                # signed certificate
527                raise SelfSignedCert, "Certificate is self signed"
528           
529            if not caX509Stack:
530                caX509Stack = [issuerX509Cert]
531                         
532        elif not caX509Stack:
533            raise CertIssuerNotFound, \
534                    'No issuer cert. found for cert. "%s"'%x509Cert2Verify.dn
535           
536        for caCert in caX509Stack:
537            issuerDN = x509Cert2Verify.issuer
538            if caCert.dn == issuerDN:
539                issuerX509Cert = caCert
540                break
541       
542        if issuerX509Cert:   
543            if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
544                X509CertError, 'Signature is invalid for cert. "%s"' % \
545                                x509Cert2Verify.dn
546           
547            # Chain is validated through to CA cert
548            return
549        else:
550            raise CertIssuerNotFound, 'No issuer cert. found for cert. "%s"'%\
551                                x509Cert2Verify.dn
552       
553        # If this point is reached then an issuing cert is missing from the
554        # chain       
555        raise X509CertError, 'Can\'t find issuer cert "%s" for cert "%s"' % \
556                          (x509Cert2Verify.issuer, x509Cert2Verify.dn) 
557
558
559def X509StackParseFromDER(derString):
560    """Make a new stack from a DER string
561   
562    @param derString: DER formatted X.509 stack data
563    @type derString: string
564    @return: new stack object
565    @rtype: X509Stack""" 
566    return X509Stack(m2X509Stack=M2Crypto.X509.new_stack_from_der(derString))
567
568
569class X500DNError(Exception):
570    """Exception handling for NDG X.500 DN class."""
571
572
573# For use with parseSeparator method:
574import re
575
576
577class X500DN(dict):
578    "NDG X500 Distinguished name"
579   
580    # Class attribute - look-up mapping short name attributes to their long
581    # name equivalents
582    # * private *
583    __shortNameLUT = {
584        'commonName':               'CN',
585        'organisationalUnitName':   'OU',
586        'organisation':             'O',
587        'countryName':              'C',
588        'emailAddress':             'EMAILADDRESS',
589        'localityName':             'L',
590        'stateOrProvinceName':      'ST',
591        'streetAddress':            'STREET',
592        'domainComponent':              'DC',
593        'userid':                       'UID'
594    }
595
596   
597    def __init__(self, dn=None, m2CryptoX509Name=None, separator=None):
598
599        """Create a new X500 Distinguished Name
600
601        @type m2CryptoX509Name: M2Crypto.X509.X509_Name
602        @param m2CryptoX509Name:   initialise using using an
603        M2Crypto.X509.X509_Name
604        @type dn: basestring
605        @param dn: initialise using a distinguished name string
606        @type separator: basestring
607        @param: separator: separator used to delimit dn fields - usually '/'
608        or ','.  If dn is input and separator is omitted the separator
609        character will be automatically parsed from the dn string.
610        """
611       
612        # Private key data
613        self.__dat = {}.fromkeys(X500DN.__shortNameLUT.values(), '')
614   
615        dict.__init__(self)
616   
617        self.__separator = None
618       
619        # Check for separator from input
620        if separator is not None:
621            if not isinstance(separator, basestring):
622                raise X500DNError("dn Separator must be a valid string")
623
624            # Check for single character but allow trailing space chars
625            if len(separator.lstrip()) is not 1:
626                raise X500DNError("dn separator must be a single character")
627
628            self.__separator = separator
629           
630        if m2CryptoX509Name is not None:
631            # the argument is an x509 dn in m2crypto format
632            self.deserialise(str(m2CryptoX509Name))
633           
634        elif dn is not None:
635            # Separator can be parsed from the input DN string - only attempt
636            # if no explict separator was input
637            if self.__separator is None:
638                self.__separator = self.parseSeparator(dn)
639               
640            # Split Distinguished name string into constituent fields
641            self.deserialise(dn)
642
643    def __repr__(self):
644        """Override default behaviour to return internal dictionary content"""
645        return self.serialise()
646
647    def __str__(self):
648        """Behaviour for print and string statements - convert DN into
649        serialised format."""
650        return self.serialise()
651       
652    def __eq__(self, x500dn):
653        """Return true if the all the fields of the two DNs are equal"""
654       
655        if not isinstance(x500dn, X500DN):
656            return False
657
658        return self.__dat.items() == x500dn.items()
659   
660    def __ne__(self, x500dn):
661        """Return true if the all the fields of the two DNs are equal"""
662       
663        if not isinstance(x500dn, X500DN):
664            return False
665
666        return self.__dat.items() != x500dn.items()
667 
668    def __delitem__(self, key):
669        """Prevent keys from being deleted."""
670        raise X500DNError('Keys cannot be deleted from the X500DN')
671
672    def __getitem__(self, key):
673
674        # Check input key
675        if self.__dat.has_key(key):
676
677            # key recognised
678            return self.__dat[key]
679       
680        elif X500DN.__shortNameLUT.has_key(key):
681
682            # key not recognised - but a long name version of the key may
683            # have been passed
684            shortName = X500DN.__shortNameLUT[key]
685            return self.__dat[shortName]
686
687        else:
688            # key not recognised as a short or long name version
689            raise KeyError('Key "' + key + '" not recognised for X500DN')
690
691    def __setitem__(self, key, item):
692       
693        # Check input key
694        if self.__dat.has_key(key):
695
696            # key recognised
697            self.__dat[key] = item
698           
699        elif X500DN.__shortNameLUT.has_key(key):
700               
701            # key not recognised - but a long name version of the key may
702            # have been passed
703            shortName = X500DN.__shortNameLUT[key]
704            self.__dat[shortName] = item
705           
706        else:
707            # key not recognised as a short or long name version
708            raise KeyError('Key "' + key + '" not recognised for X500DN')
709
710    def clear(self):
711        raise X500DNError("Data cannot be cleared from X500DN")
712
713    def copy(self):
714
715        import copy
716        return copy.copy(self)
717
718    def keys(self):
719        return self.__dat.keys()
720
721    def items(self):
722        return self.__dat.items()
723
724    def values(self):
725        return self.__dat.values()
726
727    def has_key(self, key):
728        return self.__dat.has_key(key)
729
730    # 'in' operator
731    def __contains__(self, key):
732        return key in self.__tags
733
734    def get(self, *arg):
735        return self.__dat.get(*arg)
736 
737    def serialise(self, separator=None):
738        """Combine fields in Distinguished Name into a single string."""
739       
740        if separator:
741            if not isinstance(separator, basestring):
742                raise X500DNError("Separator must be a valid string")
743               
744            self.__separator = separator
745           
746        else:
747            # Default to / if no separator is set
748            separator = '/'
749
750
751        # If using '/' then prepend DN with an initial '/' char
752        if separator == '/':
753            sDN = separator
754        else:
755            sDN = ''
756     
757        dnList = []
758        for (key, val) in self.__dat.items():
759            if val:
760                if isinstance(val, tuple):
761                    dnList += [separator.join(["%s=%s" % (key, valSub) \
762                                               for valSub in val])]
763                else:
764                    dnList += ["%s=%s" % (key, val)]
765               
766        sDN += separator.join(dnList)
767                               
768        return sDN
769
770    serialize = serialise
771   
772    def deserialise(self, dn, separator=None):
773        """Break up a DN string into it's constituent fields and use to
774        update the object's dictionary"""
775       
776        if separator:
777            if not isinstance(separator, basestring):
778                raise X500DNError("Separator must be a valid string")
779
780            self.__separator = separator
781
782
783        # If no separator has been set, parse if from the DN string           
784        if self.__separator is None:
785            self.__separator = self.parseSeparator(dn)
786
787        try:
788            dnFields = dn.split(self.__separator)
789            if len(dnFields) < 2:
790                raise X500DNError("Error parsing DN string: \"%s\"" % dn)
791
792           
793            # Split fields into key/value and also filter null fields if
794            # found e.g. a leading '/' in the DN would yield a null field
795            # when split
796           
797            items = [field.split('=') for field in dnFields if field]
798
799            # Reset existing dictionary values
800            self.__dat.fromkeys(self.__dat, '')
801           
802            # Strip leading and trailing space chars and convert into a
803            # dictionary
804            parsedDN = {}
805            for (key, val) in items:
806                key = key.strip()
807                if key in parsedDN:
808                    if isinstance(parsedDN[key], tuple):
809                        parsedDN[key] = tuple(list(parsedDN[key]) + [val])                   
810                    else:
811                        parsedDN[key] = (parsedDN[key], val)
812                else:
813                    parsedDN[key] = val
814               
815            # Copy matching DN fields
816            for key, val in parsedDN.items():
817                if key not in self.__dat and key not in self.__shortNameLUT:
818                    raise X500DNError('Invalid field "%s" in input DN string' %
819                                      key)
820
821                self.__dat[key] = val
822
823               
824        except Exception, excep:
825            raise X500DNError("Error de-serialising DN \"%s\": %s" % \
826                              (dn, str(excep)))
827
828    deserialize = deserialise
829   
830    def parseSeparator(self, dn):
831        """Attempt to parse the separator character from a given input
832        DN string.  If not found, return None
833
834        DNs don't use standard separators e.g.
835
836        /C=UK/O=eScience/OU=CLRC/L=DL/CN=AN Other
837        CN=SUM Oneelse,L=Didcot, O=RAL,OU=SSTD
838
839        This function isolates and identifies the character.  - In the above,
840        '/' and ',' respectively"""
841
842
843        # Make a regular expression containing all the possible field
844        # identifiers with equal sign appended and 'or'ed together.  \W should
845        # match the separator which preceeds the field name. \s* allows any
846        # whitespace between field name and field separator to be taken into
847        # account.
848        #
849        # The resulting match should be a list.  The first character in each
850        # element in the list should be the field separator and should be the
851        # same
852        regExpr = '|'.join(['\W\s*'+i+'=' for i in self.__dat.keys()])
853        match = re.findall(regExpr, dn)
854           
855        # In the first example above, the resulting match is:
856        # ['/C=', '/O=', '/OU=', '/L=']
857        # In each element the first character is the separator
858        sepList = [i[0:1] for i in match]
859
860        # All separators should be the same character - return None if they
861        # don't match
862        if not [i for i in sepList if i != sepList[0]]:
863            return sepList[0]
864        else:
865            return None
Note: See TracBrowser for help on using the repository browser.