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

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

#1004 Security Filter:

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