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

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

ndg.security.common/ndg/security/common/X509.py: X509Stack.verifyCertChain - make sure issuerX509Cert is defined for empty stack case.

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