source: TI12-security/branches/ndg-security-1.5.x/ndg_security_common/ndg/security/common/AttCert.py @ 6672

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/branches/ndg-security-1.5.x/ndg_security_common/ndg/security/common/AttCert.py@6672
Revision 6672, 42.3 KB checked in by pjkersha, 10 years ago (diff)

Patched ndg.security.common.AttCert? so that it uses a proxy to ndg.security.common.XMLSec.XMLSecDoc for Python versions >= 2.5.5. This is to allow for PyXML incompatibility with later versions of Python. Disabling XMLSecDoc means that Attribute Certificates are not signed but the NDG Attribute Certificates are no longer used. SAML assertions take their place. NDG AC functionality will be deleted from the trunk.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1"""NDG Attribute Certificate (Authorisation -or Access- Token)
2
3NERC Data Grid Project
4
5"""
6__author__ = "P J Kershaw"
7__date__ = "05/04/05"
8__copyright__ = "(C) 2009 Science and Technology Facilities Council"
9__license__ = "BSD - see LICENSE file in top-level directory"
10__contact__ = "Philip.Kershaw@stfc.ac.uk"
11__revision__ = '$Id$'
12
13import types
14import os
15import re
16import copy
17
18# XML Parsing
19
20# For parsing of properties file
21try: # python 2.5
22    from xml.etree import cElementTree as ElementTree
23except ImportError:
24    # if you've installed it yourself it comes this way
25    import cElementTree as ElementTree
26
27# Time module for use with validity times
28from time import strftime, strptime
29from datetime import datetime, timedelta
30
31# XML signature module based on M2Crypto, ZSI Canonicalization and DOM
32import sys
33import warnings
34if sys.version_info[:2] < (2, 5, 5):
35    from XMLSec import XMLSecDoc, InvalidSignature
36else:
37    msg = ("ndg.security.common.XMLSec not supported for Python versions 2.5.5 "
38           "or later because of PyXML incompatibility")
39    warnings.warn(msg)
40    class XMLSecDoc(object):
41        "XMLSecDoc Stub class: %s" % msg
42        def __init__(self):
43            self.filePath = None
44           
45        def parse(self, *arg):
46            "XMLSecDoc Stub class parse: %s" % msg
47       
48        def applyEnvelopedSignature(self, **kw):
49            "XMLSecDoc Stub class - no signature applied: %s" % msg
50       
51        def verifyEnvelopedSignature(self, **kw):
52            "XMLSecDoc Stub class - no verification executed: %s" % msg
53       
54        def toString(self):
55            "XMLSecDoc Stub class toString returns None: %s" % msg
56            return None
57       
58    class InvalidSignature(Exception):
59        "XMLSecDoc.InvalidSignature Stub class: %s" % msg
60       
61from X509 import X500DN
62from X509 import X500DNError
63
64
65class AttCertError(Exception): 
66    """Exception handling for NDG Attribute Certificate class."""
67
68class AttCertInvalidSignature(AttCertError):
69    """Error with certificate signature"""
70   
71class AttCertNotBeforeTimeError(AttCertError):
72    """Current time is before the Attribute Certificate's not before time"""
73
74class AttCertExpired(AttCertError):
75    """Current time is after the Attribute Certificate's not after time"""
76
77class AttCertReadOnlyDict(dict):
78    def __init__(self, inputDict):
79        super(AttCertReadOnlyDict, self).__init__(inputDict)
80       
81    def __setitem__(self, key, item):
82        raise KeyError("Items are read-only in this dictionary")
83       
84class _MetaAttCert(type):
85    """Enable AttCert to have read only class variables e.g.
86   
87    print AttCert.mappedProvenance is allowed but,
88   
89    AttCert.mappedProvenance = None
90   
91    ... raises - AttributeError: can't set attribute"""
92   
93    def __getVersion(cls):
94        '''Version of THIS format for the certificate'''
95        return '1.0'
96
97    version = property(fget=__getVersion, 
98                       doc="Version of the certificate format")
99   
100    #_________________________________________________________________________   
101    def __getMappedProvenance(cls):
102        '''Text for mapped provenance setting of certificate'''
103        return 'mapped'
104
105    mappedProvenance = property(fget=__getMappedProvenance,
106        doc="Text constant indicating cert has mapped roles from another")
107
108    #_________________________________________________________________________   
109    def __getOriginalProvenance(cls):
110        '''Text for original provenance setting of certificate'''
111        return 'original'
112   
113    origProvenance = property(fget=__getOriginalProvenance,
114        doc="Text constant indicating cert has original and not mapped roles")
115   
116   
117#_____________________________________________________________________________
118class AttCert(dict, XMLSecDoc):
119    """NDG Attribute Certificate (Authorisation or Access Token).
120   
121    @type __validProvenanceSettings: tuple
122    @cvar __validProvenanceSettings: string constants for allowable certificate provenance settings
123   
124    @type namespace: string
125    @cvar namespace: namespace for Attribute Certificate"""
126   
127    __metaclass__ = _MetaAttCert
128
129    # Provenance of certificate may be original or mapped from another
130    # certificate
131    __validProvenanceSettings = ('original', 'mapped')
132    namespace = "urn:ndg:security:attributeCertificate"
133
134    #_________________________________________________________________________   
135    def __init__(self, provenance='original', lifetime=28800, **xmlSecDocKw):
136        """Initialisation - Attribute Certificate file path may be specified.
137        Also, holder and issuer details and signing authority key and
138        certificate.
139       
140        @type lifetime: int
141        @param lifetime: set the lifetime for the certificate in seconds.
142        Defaults to 8 hours.
143       
144        @type **xmlSecDocKw: dict
145        @param **xmlSecDocKw: see XMLSec.XMLSec class for an explanation.
146        Keywords include, filePath for the cert. for reading/writing and
147        cert./private key settings for digital signature and verification."""
148
149        # Base class initialisation
150        dict.__init__(self)
151        XMLSecDoc.__init__(self, **xmlSecDocKw)
152
153        #: Data dictionary version of xml
154        #:
155        #: Nb. RoleSet is an empty list - it will be filled role dictionary
156        #: items [{'role': {'name': '<Name>'}}, ... ]
157        self.__dat = {
158           
159            "version":            AttCert.version,
160            "holder":             '',
161            "issuer":             '',
162            "issuerName":         '',
163            "issuerSerialNumber": 0,
164            "userId":             '',
165            "validity":           {"notBefore": '', "notAfter": ''},
166            "attributes":         {"roleSet": []},
167            "provenance":         ''
168        }
169
170        #: Holder X500DN object - instantiated in read method
171        self.__issuerDN = None
172        #: issuer X500DN object - instantiated in read method
173        self.__holderDN = None
174
175        self.__setProvenance(provenance)
176       
177        #: Certificate life time interval in seconds
178        self.__lifetime = lifetime
179       
180        #: Certificate not before time as datetime type
181        self.__dtNotBefore = None
182       
183        #: Certificate not after time as a datetime type
184        self.__dtNotAfter = None
185
186
187    #_________________________________________________________________________   
188    def __repr__(self):
189        """Override default behaviour to return internal dictionary content"""
190        return str(self.__dat)
191
192
193    #_________________________________________________________________________   
194    def __str__(self):
195        """Override XMLSec.XMLSecDoc equivalent"""
196        return self.toString()
197   
198   
199    #_________________________________________________________________________
200    def toString(self, **kw):
201        """Return certificate file content as a string
202       
203        @param **kw: keywords to XMLSec.XMLSecDoc.toString()
204        @rtype: string
205        @return: content of document"""
206
207        # If doc hasn't been parsed by parent (ie. not signed) return elements
208        # set so far using createXML method
209        return super(AttCert, self).toString(**kw) or self.createXML()
210
211               
212    #_________________________________________________________________________   
213    def __delitem__(self, key):
214        "Attribute Certificate keys cannot be removed"
215       
216        raise AttCertError, 'Keys cannot be deleted from ' + \
217                           self.__class__.__name__
218
219
220    #_________________________________________________________________________   
221    def __getitem__(self, key):
222        """Get an item from the __dat, __dat['validity'] or
223        __dat['attributes'] dictionaries.  This class behaves as data
224        dictionary of Attribute Certificate properties
225
226        @param key: name of key - key can be specified belonging to validity
227        or the attributes sub dictionaries
228        @param item: value to set dictionary item to
229        """
230       
231        # Check input key
232        if key in self.__dat:
233
234            # key recognised
235            item = self.__dat[key]               
236
237        elif key in self.__dat['validity']:
238
239            # Allow indexing via validity keys - a shorthand way of
240            # referencing for convenience
241            item = self.__dat['validity'][key]
242
243        elif key in self.__dat['attributes']:
244
245            # Allow indexing via attributes keys - a shorthand way of
246            # referencing for convenience
247            item = self.__dat['attributes'][key]
248
249        else:
250            # key not recognised as a short or long name version
251            raise KeyError, 'Key "%s" not recognised for %s' % \
252                               (key, self.__class__.__name__)
253
254        if isinstance(item, dict):
255            return AttCertReadOnlyDict(item)
256        else:
257            return item
258
259
260    #_________________________________________________________________________   
261    def __setitem__(self, key, item):       
262        """Set an item from the __dat dictionary.  This class behaves as data
263        dictionary of Attribute Certificate properties
264
265        @type key: string
266        @param key: name of key - key can be specified belonging to validity
267        or the attributes sub dictionaries
268       
269        @type item: string / int
270        @param item: value to set dictionary item to
271        """
272
273        # Check input key
274        if key in self.__dat:
275
276            # key recognised - check if setting provenance
277            if key == "provenance":
278                self.__setProvenance(item)
279               
280            elif key == "version":
281                self.__setVersion(item)
282                 
283            elif key == "holder":
284                self.__setHolder(item)
285               
286            elif key == "issuer":
287                self.__setIssuer(item)
288           
289            elif key == "issuerName":
290                self.__setIssuerName(item)
291           
292            elif key == "issuerSerialNumber":
293                self.__setIssuerSerialNumber(item)
294         
295            elif key == "userId":
296                self.__setUserId(item)
297                   
298            elif key == "validity":
299                raise KeyError, "'%s': use setValidityTime method " % \
300                    key + "to set notBefore/notAfter times"
301                           
302            elif key == "attributes":
303                raise KeyError, "'%s': use addRoles method to " % \
304                    key + "set list of role attributes"           
305            else:   
306                raise KeyError, "Key '%s' not recognised for %s'" % \
307                               (key, self.__class__.__name__)
308
309        elif key in self.__dat['attributes'] or \
310             key in self.__dat['attributes']['roleSet']:
311
312            # To complex to allow direct setting here
313            raise KeyError, "'%s': use addRoles method to " % key + \
314                            "set list of roles"           
315
316        elif key in self.__dat['validity']:
317            # Prevent setting of notBefore/notAfter - restrict to method
318            # setValidityTime
319            raise KeyError, "'%s': use setValidityTime method " % key + \
320                            "to set notBefore/notAfter times"           
321        else:
322            # key not recognised as a short or long name version
323            raise KeyError, "Key '%s' not recognised for %s'" % \
324                               (key, self.__class__.__name__)
325       
326
327    #_________________________________________________________________________   
328    def __eq__(self, attCert):
329        """Return true if all elements are the same"""       
330        try:
331            return min([self.__dat[key] == attCert[key] \
332                       for key in self.__dat.keys()])
333        except:
334            return False
335       
336
337    #_________________________________________________________________________   
338    def __nonzero__(self):
339        """Ensure if <attCertInstance> test yields True"""
340        return True
341   
342   
343    #_________________________________________________________________________   
344    def clear(self):
345        raise AttCertError, "Data cannot be cleared from " + \
346                           self.__class__.__name__
347
348   
349    #_________________________________________________________________________   
350    def copy(self):
351        return copy.copy(self)
352
353   
354    #_________________________________________________________________________   
355    def keys(self):
356        return self.__dat.keys()
357
358    #_________________________________________________________________________   
359    def items(self):
360        return self.__dat.items()
361
362    #_________________________________________________________________________   
363    def values(self):
364        return self.__dat.values()
365
366    #_________________________________________________________________________   
367    def has_key(self, key):
368        return self.__dat.has_key(key)
369
370    # 'in' operator
371    #_________________________________________________________________________   
372    def __contains__(self, key):
373        return key in self.__dat
374
375
376    #
377    # Get/Set methods
378    #
379    # Nb. it's also possible to access the data dictionary parameters via
380    # __setitem__ and __getitem__ standard dictionary methods
381    #
382    #_________________________________________________________________________   
383    def __setVersion(self, version):
384        """Set the version number to be written to file."""       
385        self.__dat['version'] = version
386   
387    #_________________________________________________________________________   
388    def __getVersion(self):
389        """Get version number as set in file."""
390        return self.__dat['version']
391
392    version = property(fget=__getVersion,
393                       fset=__setVersion, 
394                       doc="Attribute Certificate version")
395   
396    #_________________________________________________________________________   
397    def __setHolder(self, holder):
398        """Set holder's Distinguished Name string."""
399        if not isinstance(holder, basestring):
400            raise TypeError("holder DN must be a string")
401
402        self.__dat['holder'] = holder
403   
404    #_________________________________________________________________________   
405    def __getHolder(self):
406        """Get holder's Distinguished Name string."""
407        return self.__dat['holder']
408
409    holder = property(fget=__getHolder,
410                      fset=__setHolder, 
411                      doc="Attribute Certificate holder DN")
412
413    #_________________________________________________________________________   
414    def __getHolderDN(self):
415         """Get the holder's Distinguished Name as an X500DN instance"""
416         return self.__holderDN
417     
418    holderDN = property(fget=__getHolderDN,
419                        doc="Attribute Certificate holder DN as X500DN type")
420   
421    #_________________________________________________________________________   
422    def __setIssuer(self, issuer):
423        """Set issuer's Distinguished Name."""
424        if not isinstance(issuer, basestring):
425            raise TypeError("issuer DN must be a string")
426       
427        self.__dat['issuer'] = issuer
428   
429    #_________________________________________________________________________   
430    def __getIssuer(self):
431        """Get the issuer's Distinguished Name string"""
432        return self.__dat['issuer']
433
434    issuer = property(fget=__getIssuer, 
435                      fset=__setIssuer,
436                      doc="Certificate Issuer DN")
437
438    #_________________________________________________________________________   
439    def __getIssuerDN(self):
440         """Get the issuer's Distinguished Name as an X500DN instance"""
441         return self.__issuerDN
442     
443    issuerDN = property(fget=__getIssuerDN,
444                        doc="Attribute Certificate issuer DN as X500DN type")
445       
446    #_________________________________________________________________________   
447    def __setIssuerName(self, issuerName):
448        """Set the name of the issuer"""
449        if not isinstance(issuerName, basestring):
450            raise TypeError("issuerName must be a string")
451       
452        self.__dat['issuerName'] = issuerName
453   
454    #_________________________________________________________________________   
455    def __getIssuerName(self):
456        """@rtype: string
457        @return: the name of the issuer"""
458        return self.__dat['issuerName']
459
460    issuerName = property(fget=__getIssuerName, 
461                          fset=__setIssuerName,
462                          doc="Certificate Issuer name")
463   
464    #_________________________________________________________________________   
465    def __setIssuerSerialNumber(self, serialNumber):
466        """@param serialNumber: the issuer serial number"""
467        if not isinstance(serialNumber, (int, long)):
468            raise TypeError("issuerSerialNumber must be an integer or a long")
469
470        self.__dat['issuerSerialNumber'] = serialNumber
471   
472    #_________________________________________________________________________   
473    def __getIssuerSerialNumber(self):
474        """@rtype: string
475        @return: the issuer serial number"""
476        return self.__dat['issuerSerialNumber']
477   
478    issuerSerialNumber = property(fget=__getIssuerSerialNumber, 
479                                  fset=__setIssuerSerialNumber,
480                                  doc="Certificate Issuer Serial Number")
481 
482       
483    #_________________________________________________________________________   
484    def __setUserId(self, userId):
485        """Set the name of the userId
486        @type userId: string
487        @param userId: user identifier"""
488        if not isinstance(userId, basestring):
489            raise TypeError("userId must be a string")
490       
491        self.__dat['userId'] = userId
492   
493    #_________________________________________________________________________   
494    def __getUserId(self):
495        """@rtype: string
496        @return: the user idenitifier"""
497        return self.__dat['userId']
498
499    userId = property(fget=__getUserId, 
500                      fset=__setUserId,
501                      doc="Certificate user identifier")
502   
503
504    # Nb. no setValidityNotBefore/setValidityNotAfter methods - use
505    # setValidityTime instead.
506   
507    #_________________________________________________________________________   
508    def getValidityNotBefore(self, asDatetime=False):
509        """Get the validity Not Before date/time string
510
511        @param asDatetime: boolean to True to return as a datetime type
512        Nb. time may not have been set - if so it will be set to None
513       
514        @rtype: string/datetime
515        @return: the not before time"""
516        if asDatetime is True:
517            return self.__dtNotBefore
518        else:
519            return self.__dat['validity']['notBefore']
520
521    validityNotBefore = property(fget=getValidityNotBefore, 
522                                  doc="Validity not before time as a string")
523
524
525    #_________________________________________________________________________   
526    def getValidityNotAfter(self, asDatetime=False):
527        """Get the validity Not After date/time string
528
529        @param asDatetime: boolean set to True to return as a datetime type
530        Nb. time may not have been set - if so it will be set to None
531       
532        @rtype: string/datetime
533        @return: the not after time"""
534        if asDatetime is True:
535            return self.__dtNotAfter
536        else:
537            return self.__dat['validity']['notAfter']
538
539    validityNotAfter = property(fget=getValidityNotAfter, 
540                                doc="Validity not after time as a string")
541
542   
543    #_________________________________________________________________________   
544    def __getRoleSet(self):
545        """@rtype: list of dict type
546        @return: the roleSet as a list of role dictionaries."""
547        return self.__dat['attributes']['roleSet']
548
549    roleSet = property(fget=__getRoleSet, 
550                       doc="Role set dictionary")
551
552    #_________________________________________________________________________   
553    def __getRoles(self):
554        """Return roles as a list
555       
556        @rtype: list
557        @return: list of roles contained in the certificate"""
558       
559        try:
560            return [i.values()[0].values()[0] \
561                    for i in self.__dat['attributes']['roleSet']]
562        except:
563            return []
564       
565    roles = property(fget=__getRoles, 
566                     doc="List of roles in Attribute Certificate")
567
568       
569    #_________________________________________________________________________   
570    def __setProvenance(self, provenance):
571        """Set the provenance for the certificate: 'original' or 'mapped'.
572       
573        @param provenance: string provenance setting"""
574
575        if not self.isValidProvenance(provenance):
576            raise AttCertError, "Provenance must be set to \"" + \
577                   "\" or \"".join(AttCert.__validProvenanceSettings) + "\""
578       
579        self.__dat['provenance'] = provenance
580
581   
582    #_________________________________________________________________________   
583    def __getProvenance(self):
584        """Get the provenance for the certificate.
585       
586        @rtype: string
587        @return: provenance of certificate mapped or original"""
588        return self.__dat['provenance']
589
590    provenance = property(fget=__getProvenance,
591                          fset=__setProvenance, 
592                          doc="Provenance of the cert. - original or mapped")
593   
594
595    #_________________________________________________________________________   
596    def isValidProvenance(self, provenance=None):
597        """Check provenance is set correctly - to 'original'/'mapped'.
598
599        If no provenance argument is provided, test against the setting in
600        the current instance.
601       
602        @param provenance: by default the current provenance setting is
603        checked i.e. self.__dat['provenance'].  Set this keyword to override
604        and check against an alternate provenance setting.
605       
606        @rtype: bool
607        @return: True if certificate has a valid provenance setting
608        """
609       
610        if not provenance:
611            provenance = self.__dat['provenance']
612
613        return provenance in AttCert.__validProvenanceSettings
614       
615
616    #_________________________________________________________________________   
617    def isOriginal(self):
618        """Check for original provenance.
619        @rtype: bool
620        @return: True if certificate has original roles"""
621        return self.__dat['provenance'] == self.__class__.origProvenance
622
623
624    #_________________________________________________________________________   
625    def isMapped(self):
626        """Check for mapped provenance.
627        @rtype: bool
628        @return: True if certificate contain roles mapped from another"""
629        return self.__dat['provenance'] == self.__class__.mappedProvenance
630
631
632    #_________________________________________________________________________   
633    def addRoles(self, roleName):
634        """Add new roles to the roleSet in attributes.
635       
636        @param roleName: role name or list of role names to add to certificate
637        """
638
639        if isinstance(roleName, basestring):
640            roleName = [roleName]
641           
642        self.__dat['attributes']['roleSet'].extend(\
643                                [{'role': {'name': i}} for i in roleName])
644
645
646    #_________________________________________________________________________   
647    def parse(self, xmlTxt, rtnRootElem=False):
648        """Parse an Attribute Certificate content contained in string input
649
650        @param xmlTxt:     Attribute Certificate XML content as string
651        @param rtnRootElem: boolean set to True to return the ElementTree
652        root element
653       
654        @rtype: ElementTree root element
655        @return: root element if rtnRootElem keyword is set to True"""
656       
657        rootElem = ElementTree.XML(xmlTxt)
658
659        # Call generic ElementTree parser
660        self.__parse(rootElem)
661
662
663        # Call base class parser method to initialise DOM objects for
664        # signature validation
665        try:
666            XMLSecDoc.parse(self, xmlTxt)
667
668        except Exception, e:
669            raise AttCertError, "Attribute Certificate: %s" % e
670
671        if rtnRootElem:
672            return rootElem
673
674       
675    #_________________________________________________________________________   
676    def read(self, filePath=None, **xmlSecDocKw):
677        """Read an Attribute Certificate from file
678
679        @param filePath:   file to be read, if omitted XMLSecDoc.__filePath
680        member variable is used instead"""
681
682        if filePath:
683            self.filePath = filePath
684
685        try:   
686            tree = ElementTree.parse(self.filePath)
687            rootElem = tree.getroot()
688        except Exception, e:
689            raise AttCertError, "Attribute Certificate: %s" % e
690       
691        # Call generic ElementTree parser
692        self.__parse(rootElem)
693
694        # Call base class read method to initialise libxml2 objects for
695        # signature validation
696        try:
697            XMLSecDoc.read(self, **xmlSecDocKw)
698
699        except Exception, e:
700            raise AttCertError, "Attribute Certificate: %s" % e
701
702       
703    #_________________________________________________________________________   
704    def __parse(self, rootElem):
705        """Private XML parsing method accepts a ElementTree.Element type
706        as input
707
708        @param rootElem: root element of doc - ElementTree.Element type
709        """
710       
711        # Extract from acInfo tag
712        acInfoElem = rootElem.find("acInfo")
713       
714        if not acInfoElem:
715            raise AttCertError, "<acInfo> tag not found in \"%s\"" % \
716                               self.filePath
717
718
719        # Copy all acInfo tags into dictionary
720        for elem in acInfoElem:
721            if elem.tag not in self.__dat:
722                raise AttCertError, '%s: "<%s>" not recognised.' % \
723                                    (self.filePath, elem.tag)
724
725            # Make sure not to copy validity and attributes tags - handle
726            # these separately below
727            if not elem.getchildren():
728                self.__dat[elem.tag] = elem.text
729
730        # Convert issuer and holder into X500DN instances
731        try:
732            self.__issuerDN = X500DN(dn=self.__dat['issuer'])
733
734        except X500DNError, x500dnErr:
735            raise AttCertError, "Issuer DN: %s" % x500dnErr
736
737
738        try:
739            self.__holderDN = X500DN(dn=self.__dat['holder'])
740        except IndexError:
741            warnings.warn("Error parsing Attribute Certificate holder as an "
742                          "X.500 DN, treating as a regular string instead")
743            self.__holderDN = None
744           
745        except X500DNError, x500dnErr:
746            raise AttCertError, "Holder DN: %s" % x500dnErr
747       
748                                 
749        # Extract validity and attributes subsets
750        self.__dat['validity']['notBefore'] = \
751                                rootElem.findtext("acInfo/validity/notBefore")
752       
753        if self.__dat['validity']['notBefore'] is None:
754            raise AttCertError, "<notBefore> tag not found in \"%s\"" % \
755                                                               self.filePath
756        elif self.__dat['validity']['notBefore'] == '':
757           
758            # Allow empty string setting but re-initialise corresponding
759            # datetime value
760            self.__dtNotBefore = None
761        else:
762            # Update datetime object equivalent
763            self.__dtNotBefore = self.timeStr2datetime(\
764                                        self.__dat['validity']['notBefore'])
765
766       
767        self.__dat['validity']['notAfter'] = \
768                                rootElem.findtext("acInfo/validity/notAfter")
769       
770        if self.__dat['validity']['notAfter'] is None:
771            raise AttCertError, '<notAfter> tag not found in "%s"' % \
772                               self.filePath
773        elif self.__dat['validity']['notBefore'] == '':
774           
775            # Allow empty string setting but re-initialise corresponding
776            # datetime value
777            self.__dtNotAfter = None
778        else:
779            # Update datetime object equivalent
780            self.__dtNotAfter = self.timeStr2datetime(\
781                                        self.__dat['validity']['notAfter'])
782
783        # set up role list
784        roleElem = acInfoElem.findall("attributes/roleSet/role/name")
785        if roleElem is None:
786            raise AttCertError, "<role> tag not found in \"%s\"" % \
787                               self.filePath
788       
789        self.__dat['attributes']['roleSet'] = \
790                                [{'role': {'name': i.text}} for i in roleElem]
791                   
792       
793        if not self.isValidVersion():           
794            raise AttCertError, 'Attribute Certificate version is ' + \
795                               self.__dat['version'] + ' but version ' + \
796                               AttCert.version + ' expected'
797
798
799    #_________________________________________________________________________   
800    def createXML(self):
801        """Create XML for Attribute Token from current data settings and
802        return as a string.  The XML created is MINUS the digital signature.
803        To obtain the signed version, run the applyEnvelopedSignature method
804        (inherited from XMLSecDoc) and pass the attCert object reference into
805        str()
806
807        @rtype: string
808        @return: formatted XML for certificate as a string"""
809
810        # Nb.
811        # * this method is used by AttCert.read()
812        # * Signing by Attribute Authority is separate - see AttCert.sign()
813       
814
815        # Check for valid provenance
816        if not self.isValidProvenance():
817            raise AttCertError, "Provenance must be set to \"" + \
818                   "\" or \"".join(AttCert.__validProvenanceSettings) + "\""
819
820       
821        # Create string of all XML content 
822        try:     
823            xmlTxt = '<attributeCertificate targetNamespace="%s">' % \
824                                                self.__class__.namespace + \
825"""
826    <acInfo>
827        <version>""" + self.__dat['version'] + """</version>
828        <holder>""" + self.__dat['holder'] + """</holder>
829        <issuer>""" + self.__dat['issuer'] + """</issuer>
830        <issuerName>""" + self.__dat['issuerName'] + """</issuerName>
831        <issuerSerialNumber>""" + str(self.__dat['issuerSerialNumber']) +\
832            """</issuerSerialNumber>
833        <userId>""" + self.__dat['userId'] + """</userId>
834        <validity>
835            <notBefore>""" + self.__dat['validity']['notBefore'] + \
836            """</notBefore>
837            <notAfter>""" + self.__dat['validity']['notAfter'] + \
838            """</notAfter>
839        </validity>
840        <attributes>
841            <roleSet>
842            """ + "".join([\
843"""    <role>
844                    <name>""" + i['role']['name'] + """</name>
845                </role>
846            """ for i in self.__dat['attributes']['roleSet']]) + \
847            """</roleSet>
848        </attributes>
849        <provenance>""" + self.__dat['provenance'] + """</provenance>
850    </acInfo>
851</attributeCertificate>"""
852        except:
853            return ''
854
855        # Return XML file content as a string
856        return xmlTxt
857
858
859    def applyEnvelopedSignature(self, **xmlSecDocKw):
860        '''Override super class version to ensure settings have been parsed
861        into a DOM object ready for signature
862       
863        @param **xmlSecDocKw: keywords applying to
864        XMLSecDoc.applyEnvelopedSignature()
865        '''       
866        self.parse(self.createXML())
867        super(AttCert, self).applyEnvelopedSignature(**xmlSecDocKw)
868
869       
870    def setValidityTime(self,
871                        dtNotBefore=None, 
872                        dtNotAfter=None, 
873                        lifetime=None,
874                        notBeforeOffset=None):
875        """Set the notBefore and notAfter times which determine the window for
876        which the Attribute Certificate is valid.  These times are set as
877        datetime types and also the correct string format settings are made
878        ready for output.
879
880        Nb. use UTC time.  lifetime and notBeforeOffset are in seconds
881       
882        @param dtNotBefore: not before time as datetime type.  If omitted,
883        it defaults to the current time
884       
885        @param dtNotAfter: not after time as datetime type.  Defaults to
886        self.__dtNotBefore + self.__lifetime.  If dtNotAfter is set it will
887        reset self.__lifetime to self.__dtNotAfter - self.dtNotBefore
888       
889        @param lifetime: lifetime for certificate in seconds i.e. not after
890        time - not before time.  If dtNotAfter is set then this keyword will
891        be ignored.
892       
893        @param notBeforeOffset: skew the not before time by some offset.  This
894        is useful in cases where system clocks are not correctly synchronized
895        between different hosts.  Set a negative value to skew the offset
896        backward in time.
897        """
898
899        if dtNotBefore is not None:
900            if not isinstance(dtNotBefore, datetime):
901                raise AttCertError, \
902                                "Input not before time must be datetime type"
903           
904            self.__dtNotBefore = dtNotBefore
905           
906        else:
907            # Use current UTC +/- offset
908            self.__dtNotBefore = datetime.utcnow()
909           
910        if notBeforeOffset is not None:
911            self.__dtNotBefore += timedelta(seconds=notBeforeOffset)
912           
913
914        if dtNotAfter is not None:
915            if not isinstance(dtNotAfter, datetime):
916                raise AttCertError, \
917                                "Input not after time must be datetime type"
918
919            # Use input Not After time to calculate a new lifetime setting
920            dtDeltaLifeTime = dtNotAfter - self.__dtNotBefore
921            if dtDeltaLifeTime < timedelta(0):
922                raise AttCertError, "Input Not After time is invalid %s" % \
923                                   str(dtNotAfter)
924
925            self.__lifetime = dtDeltaLifeTime.days*86400 + \
926                              dtDeltaLifeTime.seconds
927
928            self.__dtNotAfter = dtNotAfter
929           
930        else:
931            # Check for input certificate life time interval
932            if lifetime is not None:
933                self.__lifetime = lifetime
934               
935            try:
936                # Make a time delta object from the lifetime expressed in
937                # seconds
938                dtDeltaLifeTime = timedelta(seconds=self.__lifetime)
939            except Exception, e:
940                raise AttCertError("Invalid Certificate lifetime set %.3f" %
941                                   self.__lifetime)
942           
943            # Add certificate lifetime to calculate not after time
944            self.__dtNotAfter = self.__dtNotBefore + dtDeltaLifeTime
945
946       
947        self.__dat['validity']['notBefore'] = \
948                                    self.datetime2timeStr(self.__dtNotBefore)
949       
950        self.__dat['validity']['notAfter'] = \
951                                    self.datetime2timeStr(self.__dtNotAfter)
952
953
954    #_________________________________________________________________________   
955    def datetime2timeStr(self, dtVal):
956        """Convert a datetime object to a notBefore/notAfter time string
957       
958        @param dtVal: input datetime
959       
960        @rtype: string
961        @return: datetime converted into correct string format for AttCert"""
962
963        if not isinstance(dtVal, datetime):
964            raise AttCertError, \
965                        "Invalid datetime object for conversion to string"
966       
967        # Convert from 1-12 to 0-11 month format used in XML file
968        #lDateTime = list(dtVal.utctimetuple()[0:6])
969
970        #lDateTime[1] -= 1
971
972        # Format as a single string with no commas or brackets
973        #return ''.join(re.findall('[0-9 ]', str(lDateTime)))
974
975        # Use 1-12 format
976        # P J Kershaw 09/06/05
977        return dtVal.strftime("%Y %m %d %H %M %S")
978
979
980    #_________________________________________________________________________   
981    def timeStr2datetime(self, sTime):
982        """Convert a notBefore/notAfter time string to a datetime object
983       
984        @param sTime: time in string format as used by AttCert
985        @rtype: datetime
986        @return: datetime type equivalent of string input"""
987
988        try:
989            lTime = strptime(sTime, "%Y %m %d %H %M %S")
990            return datetime(*lTime[0:6])
991       
992        except Exception, e:
993            raise AttCertError, \
994                "Error converting time string into datetime object: %s" % e
995       
996
997    #_________________________________________________________________________   
998    def isValidTime(self, dtNow=None, raiseExcep=False):
999        """Check Attribute Certificate for expiry.  Set raiseExcep to True
1000        to raise an exception with a message indicating the nature of the
1001        time error
1002       
1003        @param dtNow: the time to test against in datetime format.  This time
1004        must be within the range of the not before and not after times in
1005        order for the certificate to be valid.  Defaults to the current
1006        system time
1007       
1008        @param raiseExcep: boolean set to True to raise an exception if the
1009        time is invalid.  Defaults to False in which case no exception is
1010        raised if the time is invalid, instead False is returned
1011       
1012        @rtype: bool
1013        @return: boolean True if time is valid, False if invalid.  Also see
1014        raiseExcep keyword above."""
1015
1016        if not isinstance(self.__dtNotBefore, datetime):
1017            raise AttCertError, "Not Before datetime is not set"
1018
1019        if not isinstance(self.__dtNotAfter, datetime):
1020            raise AttCertError, "Not After datetime is not set"
1021       
1022        if dtNow is None:
1023            dtNow = datetime.utcnow()
1024       
1025        # Testing only
1026        #
1027        # P J Kershaw 02/03/06
1028        #notBefore = self.__dtNotBefore
1029        #notAfter = self.__dtNotAfter
1030        #print "Valid Time? = %d" % (dtNow > notBefore and dtNow < notAfter)
1031        if raiseExcep:
1032            if dtNow < self.__dtNotBefore:
1033                raise AttCertError, "Current time %s " % \
1034                           dtNow.strftime("%d/%m/%Y %H:%M:%S") + \
1035                           "is before Attribute Certificate's " + \
1036                           "not before time of %s" % \
1037                           self.__dtNotBefore.strftime("%d/%m/%Y %H:%M:%S")
1038           
1039            if dtNow > self.__dtNotAfter:
1040                raise AttCertError, "Current time %s " % \
1041                           dtNow.strftime("%d/%m/%Y %H:%M:%S") + \
1042                           "is after Attribute Certificate's " + \
1043                           "expiry time of %s" % \
1044                           self.__dtNotBefore.strftime("%d/%m/%Y %H:%M:%S")               
1045           
1046            return True       
1047        else:
1048            return dtNow > self.__dtNotBefore and dtNow < self.__dtNotAfter
1049       
1050       
1051    def isValidVersion(self):
1052        """Check Attribute Certificate XML file version
1053       
1054        @rtype: bool
1055        @return: boolean True if certificate version matches the expected one,
1056        False otherwise.
1057        """
1058        return self.__dat['version'] == AttCert.version
1059
1060
1061    def isValid(self,
1062                raiseExcep=False,
1063                chkTime=True,
1064                chkVersion=True,
1065                chkProvenance=True,
1066                chkSig=True,
1067                **xmlSecDocKw):
1068        """Check Attribute Certificate is valid:
1069
1070        - Time validity is OK
1071        - XML file version is OK
1072        - valid provenance setting
1073        - Signature is valid.
1074
1075        @param chkTime: set to True to do time validity check (default is
1076        True)
1077
1078        @param chkVersion: set to True to Attribute Certificate file
1079        version (default is True)
1080
1081        @param chkProvenance: set to True to check provenance value is valid
1082        (default is True)
1083
1084        @param chkSig: set to True to check digital signature - for
1085        this certFilePathList must contain the root certificate of the X.509
1086        certificate used to sign the AttCert.  Alternatively, certFilePathList
1087        can be set via __init__ (default chkSig value is True)
1088                               
1089        @param raiseExcep: set to true to raise an exception if invalid
1090        instead of returning False.  Default is to set this flag to False.
1091
1092        @param **xmlSecDocKw: Also accepts keyword arguments corresponding to
1093        XMLSecDoc.verifyEnvelopedSignature().
1094       
1095        @rtype: bool
1096        @return: boolean True if certificate is valid, False otherwise.  Also
1097        see explanation for raiseExcep keyword.                         
1098        """
1099
1100        # Carry out checks in turn - Specific exception error messages are
1101        # raised if flag is set
1102        if chkTime and not self.isValidTime(raiseExcep=raiseExcep):
1103            return False
1104
1105        if chkVersion and not self.isValidVersion():
1106            if raiseExcep:
1107                raise AttCertError('Attribute Certificate version is %s '
1108                                   'but version %s expected' %
1109                                   (self.__dat['version'], AttCert.version))
1110            return False
1111
1112        if chkProvenance and not self.isValidProvenance():
1113            if raiseExcep:
1114                raise AttCertError('Attribute Certificate Provenance must be '
1115                                   'set to "%s"' % "\" or \"".join(
1116                                            AttCert.__validProvenanceSettings))
1117            return False
1118
1119        # Handle exception from XMLSecDoc.isValidSig() regardless of
1120        # raiseExcep flag setting
1121        if chkSig:
1122            try:
1123                self.verifyEnvelopedSignature(**xmlSecDocKw)
1124       
1125            except InvalidSignature, e:
1126                 if raiseExcep:
1127                     raise AttCertInvalidSignature(e)
1128                 else:
1129                     return False
1130               
1131        # All tests passed
1132        return True
1133
1134    @classmethod
1135    def Read(cls, filePath):
1136        """Create a new attribute certificate read in from a file"""
1137        attCert = cls(filePath=filePath)
1138        attCert.read()
1139       
1140        return attCert
1141
1142    @classmethod
1143    def Parse(cls, attCertTxt):
1144        """Create a new attribute certificate from string of file content"""
1145       
1146        attCert = cls()
1147        attCert.parse(attCertTxt)
1148       
1149        return attCert
1150       
1151# Alternative AttCert constructors
1152def AttCertRead(filePath):
1153    """Create a new attribute certificate read in from a file"""
1154   
1155    attCert = AttCert(filePath=filePath)
1156    attCert.read()
1157   
1158    return attCert
1159
1160def AttCertParse(attCertTxt):
1161    """Create a new attribute certificate from string of file content"""
1162   
1163    attCert = AttCert()
1164    attCert.parse(attCertTxt)
1165   
1166    return attCert
Note: See TracBrowser for help on using the repository browser.