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

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

Improve logging in CredWallet? and X509.

  • 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    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#_____________________________________________________________________________
332# Alternative AttCert constructors
333#
334def X509CertRead(filePath):
335    """Create a new X509 certificate read in from a file"""
336
337    x509Cert = X509Cert(filePath=filePath)
338    x509Cert.read()
339   
340    return x509Cert
341
342
343#_____________________________________________________________________________
344def X509CertParse(x509CertTxt):
345    """Create a new X509 certificate from string of file content"""
346
347    x509Cert = X509Cert()
348    x509Cert.parse(x509CertTxt)
349   
350    return x509Cert
351
352
353#_____________________________________________________________________________
354class X509StackError(Exception):
355    """Error from X509Stack type"""
356
357#_____________________________________________________________________________
358class CertIssuerNotFound(X509StackError):
359    """Raise from verifyCertChain if no certificate can be found to verify the
360    input"""
361
362class SelfSignedCert(X509StackError):
363    """Raise from verifyCertChain if cert. is self-signed and
364    rejectSelfSignedCert=True"""
365       
366#_____________________________________________________________________________
367class X509Stack(object):
368    """Wrapper for M2Crypto X509_Stack"""
369   
370    def __init__(self, m2X509Stack=None):
371        """Initialise from an M2Crypto stack object
372       
373        @param m2X509Stack: M2Crypto X.509 stack object
374        @type m2X509Stack: M2Crypto.X509.X509_Stack"""
375       
376        self.__m2X509Stack = m2X509Stack or M2Crypto.X509.X509_Stack()
377       
378    def __len__(self):
379        """@return: length of stack
380        @rtype: int"""
381        return self.__m2X509Stack.__len__()
382
383    def __getitem__(self, idx):
384        """Index stack as an array
385        @param idx: stack index
386        @type idx: int
387        @return: X.509 cert object
388        @rtype: ndg.security.common.X509.X509Cert"""
389       
390        return X509Cert(m2CryptoX509=self.__m2X509Stack.__getitem__(idx))
391   
392    def __iter__(self):
393        """@return: stack iterator
394        @rtype: listiterator"""
395        return iter([X509Cert(m2CryptoX509=i) for i in self.__m2X509Stack])
396
397    def push(self, x509Cert):
398        """Push an X509 certificate onto the stack.
399       
400        @param x509Cert: X509 object.
401        @type x509Cert: M2Crypto.X509.X509,
402        ndg.security.common.X509.X509Cert or basestring
403        @return: The number of X509 objects currently on the stack.
404        @rtype: int"""
405        if isinstance(x509Cert, M2Crypto.X509.X509):
406            return self.__m2X509Stack.push(x509Cert)
407       
408        elif isinstance(x509Cert, X509Cert):
409            return self.__m2X509Stack.push(x509Cert.m2CryptoX509)
410       
411        elif isinstance(x509Cert, basestring):
412            return self.__m2X509Stack.push(\
413                                       X509CertParse(x509Cert).m2CryptoX509)           
414        else:
415            raise X509StackError, "Expecting M2Crypto.X509.X509, " + \
416                "ndg.security.common.X509.X509Cert or string type"
417               
418    def pop(self):
419        """Pop a certificate from the stack.
420       
421        @return: X509 object that was popped, or None if there is nothing
422        to pop.
423        @rtype: ndg.security.common.X509.X509Cert
424        """
425        return X509Cert(m2CryptoX509=self.__m2X509Stack.pop())
426
427
428    def asDER(self):
429        """Return the stack as a DER encoded string
430        @return: DER string
431        @rtype: string"""
432        return self.__m2X509Stack.as_der()
433
434
435    def verifyCertChain(self, 
436                        x509Cert2Verify=None, 
437                        caX509Stack=[],
438                        rejectSelfSignedCert=True):
439        """Treat stack as a list of certificates in a chain of
440        trust.  Validate the signatures through to a single root issuer. 
441
442        @param x509Cert2Verify: X.509 certificate to be verified default is
443        last in the stack
444        @type x509Cert2Verify: X509Cert
445       
446        @param caX509Stack: X.509 stack containing CA certificates that are
447        trusted.
448        @type caX509Stack: X509Stack
449       
450        @param rejectSelfSignedCert: Set to True (default) to raise an
451        SelfSignedCert exception if a certificate in self's stack is
452        self-signed. 
453        @type rejectSelfSignedCert: bool"""
454       
455        n2Validate = len(self)
456        if x509Cert2Verify:
457            # One more to validate in addition to stack content
458            n2Validate += 1
459        else:
460            # Validate starting from last on stack - but check first that it's
461            # populated
462            if n2Validate == 0:
463                raise X509StackError, \
464                "Empty stack and no x509Cert2Verify set: no cert.s to verify"
465
466            x509Cert2Verify = self[-1]
467             
468               
469        # Exit loop if all certs have been validated or if find a self
470        # signed cert.
471        nValidated = 0
472        issuerX509Cert = None
473        while nValidated < n2Validate:               
474            issuerX509Cert = None
475            issuerDN = x509Cert2Verify.issuer
476           
477            # Search for issuing certificate in stack
478            for x509Cert in self:
479                if x509Cert.dn == issuerDN:
480                    # Match found - the cert.'s issuer has been found in the
481                    # stack
482                    issuerX509Cert = x509Cert
483                    break
484                   
485            if issuerX509Cert:
486                # An issuing cert. has been found - use it to check the
487                # signature of the cert. to be verified
488                if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
489                    X509CertError, 'Signature is invalid for cert. "%s"' % \
490                                    x509Cert2Verify.dn
491               
492                # In the next iteration the issuer cert. will be checked:
493                # 1) search for a cert. in the stack that issued it
494                # 2) If found use the issuing cert. to verify
495                x509Cert2Verify = issuerX509Cert
496                nValidated += 1
497            else:
498                # All certs in the stack have been searched
499                break
500
501
502        if issuerX509Cert:           
503            # Check for self-signed certificate
504            if nValidated == 1 and rejectSelfSignedCert and \
505               issuerX509Cert.dn == issuerX509Cert.issuer:
506
507                # If only one iteration occured then it must be a self
508                # signed certificate
509                raise SelfSignedCert, "Certificate is self signed"
510           
511            if not caX509Stack:
512                caX509Stack = [issuerX509Cert]
513                         
514        elif not caX509Stack:
515            raise CertIssuerNotFound, \
516                    'No issuer cert. found for cert. "%s"'%x509Cert2Verify.dn
517           
518        for caCert in caX509Stack:
519            issuerDN = x509Cert2Verify.issuer
520            if caCert.dn == issuerDN:
521                issuerX509Cert = caCert
522                break
523       
524        if issuerX509Cert:   
525            if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
526                X509CertError, 'Signature is invalid for cert. "%s"' % \
527                                x509Cert2Verify.dn
528           
529            # Chain is validated through to CA cert
530            return
531        else:
532            raise CertIssuerNotFound, 'No issuer cert. found for cert. "%s"'%\
533                                x509Cert2Verify.dn
534       
535        # If this point is reached then an issuing cert is missing from the
536        # chain       
537        raise X509CertError, 'Can\'t find issuer cert "%s" for cert "%s"' % \
538                          (x509Cert2Verify.issuer, x509Cert2Verify.dn) 
539
540
541#_____________________________________________________________________________
542def X509StackParseFromDER(derString):
543    """Make a new stack from a DER string
544   
545    @param derString: DER formatted X.509 stack data
546    @type derString: string
547    @return: new stack object
548    @rtype: X509Stack""" 
549    return X509Stack(m2X509Stack=M2Crypto.X509.new_stack_from_der(derString))
550
551
552#_____________________________________________________________________________
553class X500DNError(Exception):
554    """Exception handling for NDG X.500 DN class."""
555
556
557#_____________________________________________________________________________
558# For use with parseSeparator method:
559import re
560
561
562class X500DN(dict):
563    "NDG X500 Distinguished name"
564   
565    # Class attribute - look-up mapping short name attributes to their long
566    # name equivalents
567    # * private *
568    __shortNameLUT = {  'commonName':               'CN',
569                        'organisationalUnitName':   'OU',
570                        'organisation':             'O',
571                        'countryName':              'C',
572                        'emailAddress':             'EMAILADDRESS',
573                        'localityName':             'L',
574                        'stateOrProvinceName':      'ST',
575                        'streetAddress':            'STREET',
576                        'domainComponent':          'DC',
577                        'userid':                   'UID'}
578
579   
580    def __init__(self,
581                 dn=None,
582                 m2CryptoX509Name=None,
583                 separator=None):
584
585        """Create a new X500 Distinguished Name
586
587        m2CryptoX509Name:   initialise using using an M2Crypto.X509.X509_Name
588        dn:                 initialise using a distinguished name string
589        separator:          separator used to delimit dn fields - usually
590                            '/' or ','.  If dn is input and separator is
591                            omitted the separator character will be
592                            automatically parsed from the dn string.
593                            """
594        # Private key data
595        self.__dat = {  'CN':           '',
596                        'OU':           '',
597                        'O':            '',
598                        'C':            '',
599                        'EMAILADDRESS': '',
600                        'L':            '',
601                        'ST':           '',
602                        'STREET':       '',
603                        'DC':           '',
604                        'UID':          ''}
605   
606        dict.__init__(self)
607   
608        self.__separator = None
609       
610        # Check for separator from input
611        if separator is not None:
612            if not isinstance(separator, basestring):
613                raise X500DNError("dn Separator must be a valid string")
614
615            # Check for single character but allow trailing space chars
616            if len(separator.lstrip()) is not 1:
617                raise X500DNError("dn separator must be a single character")
618
619            self.__separator = separator
620
621           
622        if m2CryptoX509Name is not None:
623            # the argument is an x509 dn in m2crypto format
624            #
625            # Hack required here because M2Crypto doesn't
626            # correctly separate emailAddress fields e.g.
627            #
628            # C=SG, ST=Singapore, O=BMTAP Pte Ltd,
629            # OU=Environmental Development,
630            # CN=www.bmtap.com.sg/emailAddress=sjamsul.lakau@bmtasia.com.sg
631            #                    ^
632            # - The slash is left in place
633            #
634            # TODO: re-check this for future M2Crypto releases
635            dnTxt = ', '.join(m2CryptoX509Name.as_text().split('/'))           
636            # End hack
637           
638            self.deserialise(dnTxt)
639           
640        elif dn is not None:
641            # Separator can be parsed from the input DN string - only attempt
642            # if no explict separator was input
643            if self.__separator is None:
644                self.__separator = self.parseSeparator(dn)
645               
646            # Split Distinguished name string into constituent fields
647            self.deserialise(dn)
648
649
650    def __repr__(self):
651        """Override default behaviour to return internal dictionary content"""
652        return self.serialise()
653
654
655    def __str__(self):
656        """Behaviour for print and string statements - convert DN into
657        serialised format."""
658        return self.serialise()
659
660       
661    def __eq__(self, x500dn):
662        """Return true if the all the fields of the two DNs are equal"""
663       
664        if not isinstance(x500dn, X500DN):
665            return False
666
667        return self.__dat.items() == x500dn.items()
668
669       
670    def __ne__(self, x500dn):
671        """Return true if the all the fields of the two DNs are equal"""
672       
673        if not isinstance(x500dn, X500DN):
674            return False
675
676        return self.__dat.items() != x500dn.items()
677
678   
679    def __delitem__(self, key):
680        """Prevent keys from being deleted."""
681        raise X500DNError('Keys cannot be deleted from the X500DN')
682
683
684    def __getitem__(self, key):
685
686        # Check input key
687        if self.__dat.has_key(key):
688
689            # key recognised
690            return self.__dat[key]
691       
692        elif X500DN.__shortNameLUT.has_key(key):
693
694            # key not recognised - but a long name version of the key may
695            # have been passed
696            shortName = X500DN.__shortNameLUT[key]
697            return self.__dat[shortName]
698
699        else:
700            # key not recognised as a short or long name version
701            raise X500DNError('Key "' + key + '" not recognised for X500DN')
702
703
704    def __setitem__(self, key, item):
705       
706        # Check input key
707        if self.__dat.has_key(key):
708
709            # key recognised
710            self.__dat[key] = item
711           
712        elif X500DN.__shortNameLUT.has_key(key):
713               
714            # key not recognised - but a long name version of the key may
715            # have been passed
716            shortName = X500DN.__shortNameLUT[key]
717            self.__dat[shortName] = item
718           
719        else:
720            # key not recognised as a short or long name version
721            raise X500DNError('Key "' + key + '" not recognised for X500DN')
722
723
724    def clear(self):
725        raise X500DNError("Data cannot be cleared from " + self.__class__.__name__)
726
727   
728    def copy(self):
729
730        import copy
731        return copy.copy(self)
732
733   
734    def keys(self):
735        return self.__dat.keys()
736
737
738    def items(self):
739        return self.__dat.items()
740
741
742    def values(self):
743        return self.__dat.values()
744
745
746    def has_key(self, key):
747        return self.__dat.has_key(key)
748
749    # 'in' operator
750    def __contains__(self, key):
751        return key in self.__tags
752
753
754    def get(self, kw):
755        return self.__dat.get(kw)
756
757   
758    def serialise(self, separator=None):
759        """Combine fields in Distinguished Name into a single string."""
760       
761        if separator:
762            if not isinstance(separator, basestring):
763                raise X500DNError("Separator must be a valid string")
764               
765            self.__separator = separator
766           
767        else:
768            # Default to / if no separator is set
769            separator = '/'
770
771
772        # If using '/' then prepend DN with an initial '/' char
773        if separator == '/':
774            sDN = separator
775        else:
776            sDN = ''
777     
778        dnList = []
779        for (key, val) in self.__dat.items():
780            if val:
781                if isinstance(val, tuple):
782                    dnList += [separator.join(["%s=%s" % (key, valSub) \
783                                            for valSub in val])]
784                else:
785                    dnList += ["%s=%s" % (key, val)]
786               
787        sDN += separator.join(dnList)
788                               
789        return sDN
790
791    serialize = serialise
792   
793    def deserialise(self, dn, separator=None):
794        """Break up a DN string into it's constituent fields and use to
795        update the object's dictionary"""
796       
797        if separator:
798            if not isinstance(separator, basestring):
799                raise X500DNError("Separator must be a valid string")
800
801            self.__separator = separator
802
803
804        # If no separator has been set, parse if from the DN string           
805        if self.__separator is None:
806            self.__separator = self.parseSeparator(dn)
807
808        try:
809            dnFields = dn.split(self.__separator)
810            if len(dnFields) < 2:
811                raise X500DNError("Error parsing DN string: \"%s\"" % dn)
812
813           
814            # Split fields into key/value and also filter null fields if
815            # found e.g. a leading '/' in the DN would yield a null field
816            # when split
817           
818            items = [field.split('=') for field in dnFields if field]
819
820            # Reset existing dictionary values
821            self.__dat.fromkeys(self.__dat, '')
822           
823            # Strip leading and trailing space chars and convert into a
824            # dictionary
825            parsedDN = {}
826            for (key, val) in items:
827                key = key.strip()
828                if key in parsedDN:
829                    if isinstance(parsedDN[key], tuple):
830                        parsedDN[key] = tuple(list(parsedDN[key]) + [val])                   
831                    else:
832                        parsedDN[key] = (parsedDN[key], val)
833                else:
834                    parsedDN[key] = val
835               
836            # Copy matching DN fields
837            for key, val in parsedDN.items():
838                if key not in self.__dat and key not in self.__shortNameLUT:
839                    raise X500DNError, \
840                        "Invalid field \"%s\" in input DN string" % key
841
842                self.__dat[key] = val
843
844               
845        except Exception, excep:
846            raise X500DNError("Error de-serialising DN \"%s\": %s" % \
847                              (dn, str(excep)))
848
849    deserialize = deserialise
850   
851    def parseSeparator(self, dn):
852        """Attempt to parse the separator character from a given input
853        DN string.  If not found, return None
854
855        DNs don't use standard separators e.g.
856
857        /C=UK/O=eScience/OU=CLRC/L=DL/CN=AN Other
858        CN=SUM Oneelse,L=Didcot, O=RAL,OU=SSTD
859
860        This function isolates and identifies the character.  - In the above,
861        '/' and ',' respectively"""
862
863
864        # Make a regular expression containing all the possible field
865        # identifiers with equal sign appended and 'or'ed together.  \W should
866        # match the separator which preceeds the field name. \s* allows any
867        # whitespace between field name and field separator to be taken into
868        # account.
869        #
870        # The resulting match should be a list.  The first character in each
871        # element in the list should be the field separator and should be the
872        # same
873        regExpr = '|'.join(['\W\s*'+i+'=' for i in self.__dat.keys()])
874        match = re.findall(regExpr, dn)
875           
876        # In the first example above, the resulting match is:
877        # ['/C=', '/O=', '/OU=', '/L=']
878        # In each element the first character is the separator
879        sepList = [i[0:1] for i in match]
880
881        # All separators should be the same character - return None if they
882        # don't match
883        if not [i for i in sepList if i != sepList[0]]:
884            return sepList[0]
885        else:
886            return None
Note: See TracBrowser for help on using the repository browser.