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

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

Incomplete - task 16: NDG Security 2.0.1 - incl. updated Paster templates

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