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

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

Re-run unit tests following fixes:

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