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

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