source: security/trunk/python/NDG/AttCert.py @ 474

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/security/trunk/python/NDG/AttCert.py@474
Revision 474, 24.9 KB checked in by pjkersha, 14 years ago (diff)

Renamed AttrCert? -> AttCert? + AttrAuthority? -> AttAuthority?
mapConfig has a new tag <wsdl> to store trusted host AA WSDL URI.

AttAuthority?.py: getTrustedHosts renamed getTrustedHostInfo. The latter
returns host names but also applicable WSDL URI and roles.

Session.py: CredWallet? reqAuthorisation modified to allow call to other
AA WSs in order to retrieve candidate certs for making a mapped cert.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1"""NDG Attribute Certificate (Authentication -or Access- Token)
2
3NERC Data Grid Project
4
5P J Kershaw 05/04/05
6
7Copyright (C) 2005 CCLRC & NERC
8
9This software may be distributed under the terms of the Q Public License,
10version 1.0 or later."""
11
12cvsID = '$Id$'
13
14from UserDict import UserDict
15import types
16import os
17import re
18
19# XML signature module based on xmlsec and libxml2
20from xmlSigDoc import *
21
22# XML Parsing
23import cElementTree as ElementTree
24
25# Time module for use with validity times
26from time import strftime
27from time import strptime
28from datetime import datetime
29from datetime import timedelta
30
31from X509 import X500DN
32from X509 import X500DNError
33
34
35class AttCertError(Exception):
36   
37    """Exception handling for NDG Attribute Certificate class."""
38   
39    def __init__(self, msg):
40        self.__msg = msg
41         
42    def __str__(self):
43        return self.__msg
44   
45
46class AttCert(UserDict, xmlSigDoc):
47
48    """NDG Attribute Certificate (Authentication or Access Token)."""
49
50    # Attribute Certificate file version
51    __version = "1.0"
52
53    # Provenance of certificate may be original or mapped from another
54    # certificate
55    __provenance = ('original', 'mapped')
56
57
58    # Nb. pass xmlSigDoc keyword arguments in xmlSigDocArgs dictionary
59    def __init__(self, filePath=None, lifeTime=-1, **xmlSigDocArgs):
60
61        """Initialisation - Attribute Certificate file path may be specified.
62        Also, holder and issuer details and signing authority key and
63        certificate."""
64
65        # Base class initialisation
66        UserDict.__init__(self)
67        xmlSigDoc.__init__(self, **xmlSigDocArgs)
68
69
70        if filePath is not None:
71            if not isinstance(filePath, basestring):
72                raise AttCertError("Input file path must be a valid string")
73           
74            self.setFilePath(filePath)
75
76
77        # Data dictionary version of xml
78        #
79        # Nb. RoleSet is an empty list - it will be filled role dictionary
80        # items [{'role': {'name': '<Name>'}}, ... ]
81        self.__dat = {
82           
83            "version":            AttCert.__version,
84            "holder":             '',
85            "issuer":             '',
86            "issuerName":         '',
87            "issuerSerialNumber": 0,
88            "validity":           {"notBefore": '', "notAfter": ''},
89            "attributes":         {"roleSet": []},
90            "provenance":         ''
91        }
92
93        # Holder and issuer X500DN objects - instanciated in read method
94        self.__issuerDN = None
95        self.__holderDN = None
96
97
98        # Check for input certificate life time interval - if not set default
99        # to one day
100        if lifeTime is -1:
101            self.__lifeTime = 86400 # 1 day
102        else:
103            self.__lifeTime = lifeTime
104       
105        self.__dtNotBefore = None
106        self.__dtNotAfter = None
107
108
109
110       
111    def __repr__(self):
112        """Override default behaviour to return internal dictionary content"""
113        return str(self.__dat)
114
115               
116    def __delitem__(self, key):
117
118        "Attribute Certificate keys cannot be removed"
119       
120        raise AttCertError('Keys cannot be deleted from ' + AttCert.__name__)
121
122
123    def __getitem__(self, key):
124
125        AttCert.__name__ + """ behaves as data dictionary of Attribute
126        Certificate properties
127
128        Nb. also possible to apply keys belonging validity and attributes
129        sub dictionaries
130        """
131       
132        # Check input key
133        if self.__dat.has_key(key):
134
135            # key recognised
136            return self.__dat[key]               
137
138        elif self.__dat['validity'].has_key(key):
139
140            # Allow indexing via validity keys - a shorthand way of referencing
141            # for convenience
142            return self.__dat['validity'][key]
143
144        elif self.__dat['attributes'].has_key(key):
145
146            # Allow indexing via attirbutes keys - a shorthand way of
147            # referencing for convenience
148            return self.__dat['attributes'][key]
149
150        else:
151            # key not recognised as a short or long name version
152            raise AttCertError('Key "%s" not recognised for %s' % \
153                               (key, AttCert.__name__))
154
155
156    def __setitem__(self, key, item):
157       
158        AttCert.__name__ + """ behaves as data dictionary of Attribute
159        Certificate properties
160
161        Nb. also possible to apply keys belonging validity and attributes
162        sub dictionaries
163        """
164
165        # Check input key
166        if self.__dat.has_key(key):
167
168            # key recognised - check if setting provenance
169            if key is "provenance" and not self.isValidProvenance(item):
170                raise AttCertError("Provenance must be set to \"" + \
171                            "\" or \"".join(AttCert.__provenance) + "\"")
172           
173            self.__dat[key] = item
174
175        elif self.__dat['attributes'].has_key(key):
176
177            # Allow indexing via acInfo keys - a shorthand way of referencing
178            # for convenience
179            return self.__dat['attributes'][key]
180
181        elif self.__dat['validity'].has_key(key):
182               
183            # Prevent setting of notBefore/notAfter - restrict to method
184            # setValidityTime
185            raise AttCertError(\
186                "Use setValidityTime method to set notBefore/notAfter times")
187           
188        else:
189            # key not recognised as a short or long name version
190            raise AttCertError('Key "%s" not recognised for %s' % \
191                               (key, AttCert.__name__))
192       
193
194    def __eq__(self, attCert):
195        """Return true if all elements are the same"""
196       
197        try:
198            return min([self.__dat[key] == attCert[key] \
199                       for key in self.__dat.keys()])
200        except:
201            return False
202       
203
204    def clear(self):
205        raise AttCertError("Data cannot be cleared from " + AttCert.__name__)
206
207   
208    def copy(self):
209
210        import copy
211        return copy.copy(self)
212
213   
214    def keys(self):
215        return self.__dat.keys()
216
217    def items(self):
218        return self.__dat.items()
219
220    def values(self):
221        return self.__dat.values()
222
223    def has_key(self):
224        return self.__dat.has_key()
225
226
227    def getExptdVersion(self):
228        """Return the Attribute Certificate XML expected version."""
229        return AttCert.__version
230
231
232    #
233    # Get/Set methods
234    #
235    # Nb. it's also possible to access the data dictionary parameters via
236    # __setitem__ and __getitem__ standard dictionary methods
237    #
238    def setVersion(self, version):
239        """Set the version number to be written to file."""       
240        self.__dat['version'] = version
241   
242    def getVersion(self):
243        """Get version number as set in file."""
244        return self.__dat['version']
245   
246    def setHolder(self, holder):
247        """Set holder's Distinguished Name string."""
248        self.__dat['holder'] = holder
249   
250    def getHolder(self):
251        """Get holder's Distinguished Name string."""
252        return self.__dat['holder']
253
254    def getHolderDN(self):
255         """Get the holder's Distinguished Name as an X500DN instance"""
256         return self.__holderDN
257   
258    def setIssuer(self, issuer):
259        """Set issuer's Distinguished Name."""
260        self.__dat['issuer'] = issuer
261   
262    def getIssuer(self):
263        """Get the issuer's Distinguished Name string"""
264        return self.__dat['issuer']
265
266    def getIssuerDN(self):
267         """Get the issuer's Distinguished Name as an X500DN instance"""
268         return self.__issuerDN
269       
270    def setIssuerName(self, issuerName):
271        """Set the name of the issuer"""
272        self.__dat['issuerName'] = issuerName
273   
274    def getIssuerName(self):
275        """Get the name of the issuer"""
276        return self.__dat['issuerName']
277   
278    def setIssuerSerialNumber(self, serialNumber):
279        """Set the issuer serial number"""
280        self.__dat['issuerSerialNumber'] = serialNumber
281   
282    def getIssuerSerialNumber(self):
283        """Get the issuer serial number"""
284        return self.__dat['issuerSerialNumber']
285
286
287    # Nb. no setValidityNotBefore/setValidityNotAfter methods - use
288    # setValidityTime instead.
289   
290    def getValidityNotBefore(self, asDatetime=False):
291        """Get the validity Not Before date/time string
292
293        Set asDatetime to True to return as a datetime type
294        Nb. time may not have been set - if so it will be set to None"""
295        if asDatetime is True:
296            return self.__dtNotBefore
297        else:
298            return self.__dat['validity']['notBefore']
299
300
301    def getValidityNotAfter(self, asDatetime=False):
302        """Get the validity Not After date/time string
303
304        Set asDatetime to True to return as a datetime type
305        Nb. time may not have been set - if so it will be set to None"""
306        if asDatetime is True:
307            return self.__dtNotAfter
308        else:
309            return self.__dat['validity']['notAfter']
310
311   
312    def getRoleSet(self):
313        """Get the roleSet as a list of role dictionaries."""
314        return self.__dat['attributes']['roleSet']
315
316
317    def getRoles(self):
318        """Return roles as a list"""
319        try:
320            return [i.values()[0].values()[0] \
321                    for i in self.__dat['attributes']['roleSet']]
322        except:
323            return []
324
325       
326    def setProvenance(self, provenance):
327        """Set the provenance for the certificate: 'original' or 'mapped'."""
328
329        if not self.isValidProvenance(provenance):
330            raise AttCertError("Provenance must be set to \"" + \
331                               "\" or \"".join(AttCert.__provenance) + "\"")
332       
333        self.__dat['provenance'] = provenance
334
335   
336    def getProvenance(self):
337        """Get the provenance for the certificate."""
338        return self.__dat['provenance']
339   
340
341    def isValidProvenance(self, provenance=None):
342        """Check provenance is set correctly - to 'original'/'mapped'.
343
344        If no provenance argument is provided, test against the setting in
345        the current instance.
346        """
347       
348        if not provenance:
349            provenance = self.__dat['provenance']
350
351        return provenance in AttCert.__provenance
352       
353
354    def isOriginal(self):
355        """Check for original provenance."""
356        return self.__dat['provenance'] == 'original'
357
358
359    def isMapped(self):
360        """Check for mapped provenance."""
361        return self.__dat['provenance'] == 'mapped'
362
363
364    def addRoles(self, roleName):
365        """Add new roles to the roleSet in attributes."""
366
367        if isinstance(roleName, basestring):
368            roleName = [roleName]
369           
370        self.__dat['attributes']['roleSet'].extend(\
371                                [{'role': {'name': i}} for i in roleName])
372
373
374
375
376    def parse(self, xmlTxt):
377
378        """Parse an Attribute Certificate content contained in string input
379
380        xmlTxt:     Attribute Certificate XML content as string"""
381       
382        rootElem = ElementTree.XML(xmlTxt)
383
384        # Call generic ElementTree parser
385        self.__parse(rootElem)
386
387
388        # Call base class parser method to initialise libxml2 objects for
389        # signature validation
390        try:
391            xmlSigDoc.parse(self, xmlTxt)
392
393        except xmlSigDocError, xmlSigDocErr:
394            raise AttCertError(str(xmlSigDocErr))
395
396
397
398       
399    def read(self, filePath=None):
400
401        """Read Attribute Certificate
402
403        filePath:   file to be read, if omitted __filePath member variable is
404                    used instead"""
405
406        if filePath:
407            if not isinstance(filePath, basestring):
408                raise AttCertError("Input file path must be a string.")
409
410            self.setFilePath(filePath)
411        else:
412            filePath = self.getFilePath()
413
414
415        try:   
416            tree = ElementTree.parse(filePath)
417            rootElem = tree.getroot()
418        except Exception, e:
419            raise AttCertError("Attribute Certificate: %s" % e)
420       
421        # Call generic ElementTree parser
422        self.__parse(rootElem)
423
424
425        # Call base class read method to initialise libxml2 objects for
426        # signature validation
427        try:
428            xmlSigDoc.read(self)
429
430        except xmlSigDocError, xmlSigDocErr:
431            raise AttCertError(str(xmlSigDocErr))
432
433
434
435       
436    def __parse(self, rootElem):
437
438        """Private XML parsing method accepts a ElementTree.Element type
439        as input
440
441        rootElem:       ElementTree.Element type
442        """
443       
444        # Extract from acInfo tag
445        acInfoElem = rootElem.find("acInfo")
446       
447        if not acInfoElem:
448            raise AttCertError("<acInfo> tag not found in \""+filePath+"\"")
449
450
451        # Copy all acInfo tags into dictionary
452        for elem in acInfoElem:
453       
454            if not self.__dat.has_key(elem.tag):
455                raise AttCertError(self.getFilePath() + "\": <" + \
456                                   elem.tag + "> not recognised.")
457
458            # Make sure not to copy validity and attributes tags - handle these
459            # separately below
460            if not elem.getchildren():
461                self.__dat[elem.tag] = elem.text
462
463        # Convert issuer and holder into X500DN instances
464        try:
465            self.__issuerDN = X500DN(dn=self.__dat['issuer'])
466
467        except X500DNError, x500dnErr:
468            raise AttCertError("Issuer DN: %s" % x500dnErr)
469
470
471        try:
472            self.__holderDN = X500DN(dn=self.__dat['holder'])
473
474        except X500DNError, x500dnErr:
475            raise AttCertError("Holder DN: %s" % x500dnErr)
476       
477                                 
478        # Extract validity and attributes subsets
479        self.__dat['validity']['notBefore'] = \
480                                rootElem.findtext("acInfo/validity/notBefore")
481       
482        if self.__dat['validity']['notBefore'] is None:
483            raise AttCertError("<notBefore> tag not found in \"%s\"" % \
484                               filePath)
485
486        # Update datetime object equivalent
487        self.__dtNotBefore = self.timeStr2datetime(\
488                                        self.__dat['validity']['notBefore'])
489
490       
491        self.__dat['validity']['notAfter'] = \
492                                rootElem.findtext("acInfo/validity/notAfter")
493       
494        if self.__dat['validity']['notAfter'] is None:
495            raise AttCertError("<notAfter> tag not found in \"%s\""%filePath)
496
497
498        # Update datetime object equivalent
499        self.__dtNotAfter = self.timeStr2datetime(\
500                                        self.__dat['validity']['notAfter'])
501
502
503        # set up role list
504        roleElem = acInfoElem.findall("attributes/roleSet/role/name")
505        if roleElem is None:
506            raise AttCertError("<role> tag not found in \"" + filePath + \
507                                "\"")
508       
509        self.__dat['attributes']['roleSet'] = \
510                                [{'role': {'name': i.text}} for i in roleElem]
511                   
512       
513        if not self.isValidVersion():           
514            raise AttCertError('Attribute Certificate version is ' + \
515                               self.__dat['version'] + ' but version ' + \
516                               AttCert.__version + ' expected')
517
518
519
520
521    def createXML(self):
522
523        """Create XML for Attribute Token from current data settings and
524        return as a string.
525
526        Implementation of virtual method defined in xmlSigDoc base class"""
527
528        # Nb.
529        # * this method is used by AttCert.read()
530        # * Signing by Attribute Authority is separate - see AttCert.sign()
531       
532
533        # Check for valid provenance
534        if not self.isValidProvenance():
535            raise AttCertError("Provenance must be set to \"" + \
536                               "\" or \"".join(AttCert.__provenance) + "\"")
537
538       
539        # Create string of all XML content       
540        xmlTxt = \
541"""<attributeCertificate>
542    <acInfo>
543        <version>""" + self.__dat['version'] + """</version>
544        <holder>""" + self.__dat['holder'] + """</holder>
545        <issuer>""" + self.__dat['issuer'] + """</issuer>
546        <issuerName>""" + self.__dat['issuerName'] + """</issuerName>
547        <issuerSerialNumber>""" + str(self.__dat['issuerSerialNumber']) +\
548            """</issuerSerialNumber>
549        <validity>
550            <notBefore>""" + self.__dat['validity']['notBefore'] + \
551            """</notBefore>
552            <notAfter>""" + self.__dat['validity']['notAfter'] + \
553            """</notAfter>
554        </validity>
555        <attributes>
556            <roleSet>
557                """ + \
558        "".join(["""<role>
559                    <name>""" + i['role']['name'] + """</name>
560                </role>
561            """ for i in self.__dat['attributes']['roleSet']]) +\
562            """</roleSet>
563        </attributes>
564        <provenance>""" + self.__dat['provenance'] + """</provenance>
565    </acInfo>
566</attributeCertificate>"""
567
568
569        # Return XML file content as a string
570        return xmlTxt
571
572
573
574
575    def setValidityTime(self,dtNotBefore=None, dtNotAfter=None, lifeTime=-1):
576
577        """Set the notBefore and notAfter times which determine the window for
578        which the Attribute Certificate is valid
579
580        Nb. use UTC time.
581        """
582
583        if dtNotBefore is not None:
584            if not isinstance(dtNotBefore, datetime):
585                raise AttCertError(\
586                    "Input not before time must be datetime type")
587           
588            self.__dtNotBefore = dtNotBefore
589           
590        else:
591            # Use current UTC
592            self.__dtNotBefore = datetime.utcnow()
593
594
595
596        if dtNotAfter is not None and isinstance(dtNotAfter, datetime):
597
598            # Use input Not After time to calculate a new lifetime setting
599            dtDeltaLifeTime = dtNotAfter - self.__dtNotBefore
600            if dtDeltaLifeTime < timedelta(0):
601                raise AttCertError("Input Not After time is invalid %s" % \
602                                    str(dtNotAfter))
603
604            self.__lifeTime = dtDeltaLifeTime.days*86400 + \
605                              dtDeltaLifeTime.seconds
606
607            self.__dtNotAfter = dtNotAfter
608           
609        else:
610            # Check for input certificate life time interval
611            if lifeTime is not -1:
612                self.__lifeTime = lifeTime
613               
614            try:
615                # Make a time delta object from the lifetime expressed in
616                # seconds
617                dtDeltaLifeTime = timedelta(seconds=self.__lifeTime)
618            except Exception, e:
619                raise AttCertError("Invalid Certificate lifetime set %.3f" % \
620                                   self.__lifeTime)
621           
622            # Add certificate lifetime to calculate not after time
623            self.__dtNotAfter = self.__dtNotBefore + dtDeltaLifeTime
624
625       
626        self.__dat['validity']['notBefore'] = \
627                                    self.datetime2timeStr(self.__dtNotBefore)
628       
629        self.__dat['validity']['notAfter'] = \
630                                    self.datetime2timeStr(self.__dtNotAfter)
631
632
633
634
635    def datetime2timeStr(self, dtVal):
636
637        """Convert a datetime object to a notBefore/notAfter time string"""
638
639        if not isinstance(dtVal, datetime):
640            raise AttCertError(\
641                        "Invalid datetime object for conversion to string")
642       
643        # Convert from 1-12 to 0-11 month format used in XML file
644        #lDateTime = list(dtVal.utctimetuple()[0:6])
645
646        #lDateTime[1] -= 1
647
648        # Format as a single string with no commas or brackets
649        #return ''.join(re.findall('[0-9 ]', str(lDateTime)))
650
651        # Use 1-12 format
652        # P J Kershaw 09/06/05
653        return dtVal.strftime("%Y %m %d %H %M %S")
654
655   
656
657    def timeStr2datetime(self, sTime):
658
659        """Convert a notBefore/notAfter time string to a datetime object"""
660
661        # Convert from 0-11 to 1-12 month format used by datetime()
662        try:
663            #lTime = [int(i) for i in sTime.split()]
664            lTime = strptime(sTime, "%Y %m %d %H %M %S")
665           
666            # Use 1-12 format
667            # P J Kershaw 09/05/05
668            #lTime[1] += 1
669       
670            return datetime(lTime[0], lTime[1], lTime[2],
671                            lTime[3], lTime[4], lTime[5])
672        except Exception, e:
673            raise AttCertError(\
674                "Error converting time string into datetime object: %s" % e)
675       
676
677
678
679    def isValidTime(self):
680
681        """Check Attribute Certificate for expiry"""
682
683        if not isinstance(self.__dtNotBefore, datetime):
684            raise AttCertError("Not Before datetime is not set")
685
686        if not isinstance(self.__dtNotAfter, datetime):
687            raise AttCertError("Not After datetime is not set")
688       
689        dtNow = datetime.utcnow()
690        return dtNow > self.__dtNotBefore and dtNow < self.__dtNotAfter
691
692
693       
694       
695    def isValidVersion(self):
696
697        """Check Attribute Certificate XML file version"""
698        return self.__dat['version'] == AttCert.__version
699
700
701
702
703    def isValid(self,
704                raiseExcep=False,
705                chkTime=True,
706                chkVersion=True,
707                chkProvenance=True,
708                chkSig=True,
709                **xmlSigDocKeys):
710
711        """Check Attribute Certificate is valid:
712
713        - Time validity is OK
714        - XML file version is OK
715        - valid provenance setting
716        - Signature is valid.
717
718        chkTime:                set to True to do time validity check (default
719                                is True)
720
721        chkVersion:             set to True to Attribute Certificate file
722                                version (default is True)
723
724        chkProvenance:          set to True to check provenance value is valid
725                                (default is True)
726
727        chkSig:                 set to True to check digital signature - for
728                                this certFilePathList must contain the root
729                                certificate of the X.509 certificate used to
730                                sign the AttCert.  Alternatively,
731                                certFilePathList can be set via __init__
732                                (default chkSig value is True)
733                               
734        raiseExcep:             set to true to raise an exception if invalid
735                                instead of returning False.  Default is to set
736                                this flag to False.
737
738        Also accepts keyword arguments corresponding to xmlSigDoc.isValidSig:
739       
740        xmlTxt:                 string buffer containing the text from the XML
741                                file to be checked.  If omitted, the
742                                filePath argument is used instead.
743
744        filePath:               file path to XML file to be checked.  This
745                                argument is used if no xmlTxt was provided.
746                                If filePath itself is omitted the file set
747                                by self.__filePath is read instead.
748
749        certFilePathList:       list of files paths must contain certificate of
750                                trusted authority used to validate the
751                                signature.  If set, it is copied
752                                into self.__certFilePathList.  If omitted
753                                self.__certFilePathList is used unchanged.                             
754        """
755
756        # Carry out checks in turn - Specific exception error messages are
757        # raised if flag is set
758        if chkTime and not self.isValidTime():
759            if raiseExcep:
760                raise AttCertError("Attribute Certificate time is invalid")
761           
762            return False
763
764       
765        if chkVersion and not self.isValidVersion():
766            if raiseExcep:
767                raise AttCertError('Attribute Certificate version is ' + \
768                                   self.__dat['version'] + ' but version ' + \
769                                   AttCert.__version + ' expected')
770           
771            return False
772
773
774        if chkProvenance and not self.isValidProvenance():
775            if raiseExcep:
776                raise AttCertError(\
777                    "Attribute Certificate Provenance must be set to \"" + \
778                    "\" or \"".join(AttCert.__provenance) + "\"")
779               
780            return False
781
782
783        # Handle exception from xmlSigDoc.isValidSig() regardless of
784        # raiseExcep flag setting
785        try:           
786            if chkSig and not self.isValidSig(**xmlSigDocKeys):
787                if raiseExcep:
788                    raise AttCertError(\
789                                "Attribute Certificate signature is invalid")
790               
791                return False
792       
793        except Exception, e:
794            raise AttCertError(str(e))
795       
796
797        # All tests passed
798        return True
799
800
801
802#_____________________________________________________________________________
803# Alternative AttCert constructors
804#
805def AttCertRead(filePath):
806    """Create a new attribute certificate read in from a file"""
807   
808    attCert = AttCert(filePath)
809    attCert.read()
810   
811    return attCert
812
813
814
815
816def AttCertParse(attCertTxt):
817    """Create a new attribute certificate from string of file content"""
818   
819    attCert = AttCert()
820    attCert.parse(attCertTxt)
821   
822    return attCert
Note: See TracBrowser for help on using the repository browser.