source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/X509.py @ 6557

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

Fix for ApacheSSLAuthnMiddleware - use comma separated list for accepted DNs. This enables DNs with fields containing spaces to be correctly parsed.

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