source: TI12-security/trunk/WSSecurity/ndg/wssecurity/common/utils/pki.py @ 6388

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/WSSecurity/ndg/wssecurity/common/utils/pki.py@6388
Revision 6388, 34.4 KB checked in by pjkersha, 10 years ago (diff)

copy X509 M2Crypto wrapper code from ndg.security.common.X509

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