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

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

#941: added classifiers to MyProxyClient setup

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