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

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

Working unit tested SAML Authorisation service with XACML back-end. TODO: Add Policy Information Point interface to XACML PDP to enable user attribute queries based on matching resource rule constraining attributes.

  • 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
17import os
18import errno
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            log.error(msg)
373            if raiseExcep:
374                raise X509CertExpired(msg)
375
376        # If exception flag is not set return validity as bool
377        return isValidTime
378
379
380
381
382    def __m2CryptoUTC2datetime(self, m2CryptoUTC):
383        """Convert M2Crypto UTC time string as returned by get_not_before/
384        get_not_after methods into datetime type"""
385       
386        datetimeRE = "([a-zA-Z]{3} {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4}).*"
387        sM2CryptoUTC = None
388       
389        try:
390            # Convert into string
391            sM2CryptoUTC = str(m2CryptoUTC)
392           
393            # Check for expected format - string may have trailing GMT - ignore
394            sTime = re.findall(datetimeRE, sM2CryptoUTC)[0]
395
396            # Convert into a tuple
397            lTime = strptime(sTime, "%b %d %H:%M:%S %Y")[0:6]
398
399            return datetime(lTime[0], lTime[1], lTime[2],
400                            lTime[3], lTime[4], lTime[5])
401                                   
402        except Exception, e:
403            msg = "Error parsing M2Crypto UTC"
404            if sM2CryptoUTC is not None:
405                msg += ": " + sM2CryptoUTC
406               
407            raise X509CertError(msg)
408       
409    def verify(self, pubKey, **kw):
410        """Verify a certificate against the public key of the
411        issuer
412       
413        @param pubKey: public key of cert that issued self
414        @type pubKey: M2Crypto.RSA.RSA_pub
415        @param **kw: keywords to pass to M2Crypto.X509.X509 -
416        'pkey'
417        @type: dict
418        @return: True if verifies OK, False otherwise
419        @rtype: bool
420        """
421        return bool(self.__m2CryptoX509.verify(pubKey, **kw))
422
423    @classmethod
424    def Read(cls, filePath, warningStackLevel=4, **isValidTimeKw):
425        """Create a new X509 certificate read in from a file"""
426        x509Cert = cls(filePath=filePath) 
427        x509Cert.read(warningStackLevel=warningStackLevel, **isValidTimeKw)
428       
429        return x509Cert
430   
431    @classmethod
432    def Parse(cls, x509CertTxt, warningStackLevel=4, **isValidTimeKw):
433        """Create a new X509 certificate from string of file content"""
434        x509Cert = cls()     
435        x509Cert.parse(x509CertTxt, 
436                       warningStackLevel=warningStackLevel,
437                       **isValidTimeKw)
438       
439        return x509Cert
440
441    @classmethod
442    def fromM2Crypto(cls, m2CryptoX509):
443        """Convenience method to instantiate a new object from an M2Crypto
444        X.509 certificate object"""
445        x509Cert = cls(m2CryptoX509=m2CryptoX509)
446        return x509Cert
447   
448# Alternative AttCert constructors
449def X509CertRead(filePath, warningStackLevel=4, **isValidTimeKw):
450    """Create a new X509 certificate read in from a file"""
451
452    x509Cert = X509Cert(filePath=filePath)   
453    x509Cert.read(warningStackLevel=warningStackLevel, **isValidTimeKw)
454   
455    return x509Cert
456
457def X509CertParse(x509CertTxt, warningStackLevel=4, **isValidTimeKw):
458    """Create a new X509 certificate from string of file content"""
459
460    x509Cert = X509Cert()
461    x509Cert.parse(x509CertTxt, 
462                   warningStackLevel=warningStackLevel, 
463                   **isValidTimeKw)
464   
465    return x509Cert
466
467
468class X509StackError(X509CertError):
469    """Error from X509Stack type"""
470
471
472class X509StackEmptyError(X509CertError):
473    """Expecting non-zero length X509Stack"""
474
475
476class X509CertIssuerNotFound(X509CertError):
477    """Raise from verifyCertChain if no certificate can be found to verify the
478    input"""
479
480
481class SelfSignedCert(X509CertError):
482    """Raise from verifyCertChain if cert. is self-signed and
483    rejectSelfSignedCert=True"""
484
485
486class X509CertInvalidSignature(X509CertError):
487    """X.509 Certificate has an invalid signature"""
488       
489       
490class X509Stack(object):
491    """Wrapper for M2Crypto X509_Stack"""
492   
493    def __init__(self, m2X509Stack=None):
494        """Initialise from an M2Crypto stack object
495       
496        @param m2X509Stack: M2Crypto X.509 stack object
497        @type m2X509Stack: M2Crypto.X509.X509_Stack"""
498       
499        self.__m2X509Stack = m2X509Stack or M2Crypto.X509.X509_Stack()
500       
501    def __len__(self):
502        """@return: length of stack
503        @rtype: int"""
504        return self.__m2X509Stack.__len__()
505
506    def __getitem__(self, idx):
507        """Index stack as an array
508        @param idx: stack index
509        @type idx: int
510        @return: X.509 cert object
511        @rtype: ndg.security.common.X509.X509Cert"""
512       
513        return X509Cert(m2CryptoX509=self.__m2X509Stack.__getitem__(idx))
514   
515    def __iter__(self):
516        """@return: stack iterator
517        @rtype: listiterator"""
518        return iter([X509Cert(m2CryptoX509=i) for i in self.__m2X509Stack])
519
520    def push(self, x509Cert):
521        """Push an X509 certificate onto the stack.
522       
523        @param x509Cert: X509 object.
524        @type x509Cert: M2Crypto.X509.X509,
525        ndg.security.common.X509.X509Cert or basestring
526        @return: The number of X509 objects currently on the stack.
527        @rtype: int"""
528        if isinstance(x509Cert, M2Crypto.X509.X509):
529            return self.__m2X509Stack.push(x509Cert)
530       
531        elif isinstance(x509Cert, X509Cert):
532            return self.__m2X509Stack.push(x509Cert.m2CryptoX509)
533       
534        elif isinstance(x509Cert, basestring):
535            return self.__m2X509Stack.push(\
536                                       X509CertParse(x509Cert).m2CryptoX509)           
537        else:
538            raise X509StackError("Expecting M2Crypto.X509.X509, ndg.security."
539                                 "common.X509.X509Cert or string type")
540               
541    def pop(self):
542        """Pop a certificate from the stack.
543       
544        @return: X509 object that was popped, or None if there is nothing
545        to pop.
546        @rtype: ndg.security.common.X509.X509Cert
547        """
548        return X509Cert(m2CryptoX509=self.__m2X509Stack.pop())
549
550    def asDER(self):
551        """Return the stack as a DER encoded string
552        @return: DER string
553        @rtype: string"""
554        return self.__m2X509Stack.as_der()
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        # Exit loop if all certs have been validated or if find a self
590        # signed cert.
591        nValidated = 0
592        issuerX509Cert = None
593        while nValidated < n2Validate:
594            issuerX509Cert = None
595            issuerDN = x509Cert2Verify.issuer
596           
597            # Search for issuing certificate in stack
598            for x509Cert in self:
599                if x509Cert.dn == issuerDN:
600                    # Match found - the cert.'s issuer has been found in the
601                    # stack
602                    issuerX509Cert = x509Cert
603                    break
604                   
605            if issuerX509Cert:
606                # An issuing cert. has been found - use it to check the
607                # signature of the cert. to be verified
608                if not x509Cert2Verify.verify(issuerX509Cert.pubKey):
609                    X509CertInvalidSignature('Signature is invalid for cert. '
610                                             '"%s"' % x509Cert2Verify.dn)
611               
612                # In the next iteration the issuer cert. will be checked:
613                # 1) search for a cert. in the stack that issued it
614                # 2) If found use the issuing cert. to verify
615                x509Cert2Verify = issuerX509Cert
616                nValidated += 1
617            else:
618                # All certs in the stack have been searched
619                break
620
621
622        if issuerX509Cert:           
623            # Check for self-signed certificate
624            if (nValidated == 1 and rejectSelfSignedCert and 
625                issuerX509Cert.dn == issuerX509Cert.issuer):
626
627                # If only one iteration occurred then it must be a self
628                # signed certificate
629                raise SelfSignedCert("Certificate is self signed: [DN=%s]" %
630                                     issuerX509Cert.dn)
631           
632            if not caX509Stack:
633                caX509Stack = [issuerX509Cert]
634                         
635        elif not caX509Stack:
636            raise X509CertIssuerNotFound('No issuer certificate found for '
637                                         'certificate "%s"' % 
638                                         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 = X500DN.PARSER_RE.split(dn)
904            if len(dnFields) < 2:
905                raise X500DNError("Error parsing DN string: \"%s\"" % dn)
906
907            items = zip(dnFields[1::2], dnFields[2::2])
908           
909            # Reset existing dictionary values
910            self.__dat.fromkeys(self.__dat, '')
911           
912            # Strip leading and trailing space chars and convert into a
913            # dictionary
914            parsedDN = {}
915            for key, val in items:
916                key = key.strip()
917                if key in parsedDN:
918                    if isinstance(parsedDN[key], tuple):
919                        parsedDN[key] = tuple(list(parsedDN[key]) + [val])
920                    else:
921                        parsedDN[key] = (parsedDN[key], val)
922                else:
923                    parsedDN[key] = val
924               
925            # Copy matching DN fields
926            for key, val in parsedDN.items():
927                if key not in self.__dat and key not in self.__shortNameLUT:
928                    raise X500DNError('Invalid field "%s" in input DN string' %
929                                      key)
930
931                self.__dat[key] = val
932
933               
934        except Exception, excep:
935            raise X500DNError("Error de-serialising DN \"%s\": %s" % \
936                              (dn, str(excep)))
937
938    deserialize = deserialise
939   
940    def parseSeparator(self, dn):
941        """Attempt to parse the separator character from a given input
942        DN string.  If not found, return None
943
944        DNs don't use standard separators e.g.
945
946        /C=UK/O=eScience/OU=CLRC/L=DL/CN=AN Other
947        CN=SUM Oneelse,L=Didcot, O=RAL,OU=SSTD
948
949        This function isolates and identifies the character.  - In the above,
950        '/' and ',' respectively"""
951
952
953        # Make a regular expression containing all the possible field
954        # identifiers with equal sign appended and 'or'ed together.  \W should
955        # match the separator which preceeds the field name. \s* allows any
956        # whitespace between field name and field separator to be taken into
957        # account.
958        #
959        # The resulting match should be a list.  The first character in each
960        # element in the list should be the field separator and should be the
961        # same
962        regExpr = '|'.join(['\W\s*'+i+'=' for i in self.__dat.keys()])
963        match = re.findall(regExpr, dn)
964           
965        # In the first example above, the resulting match is:
966        # ['/C=', '/O=', '/OU=', '/L=']
967        # In each element the first character is the separator
968        sepList = [i[0:1] for i in match]
969
970        # All separators should be the same character - return None if they
971        # don't match
972        if not [i for i in sepList if i != sepList[0]]:
973            return sepList[0]
974        else:
975            return None
976
977    @classmethod
978    def Parse(cls, dn):
979        """Convenience method to create an X500DN object from a DN string
980        @type dn: basestring
981        @param dn: Distinguished Name
982        """
983        return cls(dn=dn)
984   
985    Deserialise = Deserialize = Parse
Note: See TracBrowser for help on using the repository browser.