Full API implemented.
[sfa.git] / sfa / trust / credential.py
1 ##
2 # Implements SFA Credentials
3 #
4 # Credentials are signed XML files that assign a subject gid privileges to an object gid
5 ##
6
7 ### $Id$
8 ### $URL$
9
10 import os
11 import datetime
12 from xml.dom.minidom import Document, parseString
13 from tempfile import mkstemp
14
15 from sfa.trust.credential_legacy import CredentialLegacy
16 from sfa.trust.rights import *
17 from sfa.trust.gid import *
18 from sfa.util.faults import *
19
20 from sfa.util.sfalogging import logger
21 from dateutil.parser import parse
22
23
24
25 # Two years, in seconds 
26 DEFAULT_CREDENTIAL_LIFETIME = 60 * 60 * 24 * 365 * 2
27
28
29 # TODO:
30 # . make privs match between PG and PL
31 # . Need to add support for other types of credentials, e.g. tickets
32
33
34
35 signature_template = \
36 '''
37 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
38     <SignedInfo>
39       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
40       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
41       <Reference URI="#%s">
42       <Transforms>
43         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
44       </Transforms>
45       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
46       <DigestValue></DigestValue>
47       </Reference>
48     </SignedInfo>
49     <SignatureValue />
50       <KeyInfo>
51         <X509Data>
52           <X509SubjectName/>
53           <X509IssuerSerial/>
54           <X509Certificate/>
55         </X509Data>
56       <KeyValue />
57       </KeyInfo>
58     </Signature>
59 '''
60
61 ##
62 # Convert a string into a bool
63
64 def str2bool(str):
65     if str.lower() in ['yes','true','1']:
66         return True
67     return False
68
69
70
71 ##
72 # Utility function to get the text of an XML element
73
74 def getTextNode(element, subele):
75     sub = element.getElementsByTagName(subele)[0]
76     if len(sub.childNodes) > 0:            
77         return sub.childNodes[0].nodeValue
78     else:
79         return None
80         
81 ##
82 # Utility function to set the text of an XML element
83 # It creates the element, adds the text to it,
84 # and then appends it to the parent.
85
86 def append_sub(doc, parent, element, text):
87     ele = doc.createElement(element)
88     ele.appendChild(doc.createTextNode(text))
89     parent.appendChild(ele)
90
91 ##
92 # Signature contains information about an xmlsec1 signature
93 # for a signed-credential
94 #
95
96 class Signature(object):
97
98     
99     def __init__(self, string=None):
100         self.refid = None
101         self.issuer_gid = None
102         self.xml = None
103         if string:
104             self.xml = string
105             self.decode()
106
107
108
109     def get_refid(self):
110         if not self.refid:
111             self.decode()
112         return self.refid
113
114     def get_xml(self):
115         if not self.xml:
116             self.encode()
117         return self.xml
118
119     def set_refid(self, id):
120         self.refid = id
121
122     def get_issuer_gid(self):
123         if not self.gid:
124             self.decode()
125         return self.gid        
126
127     def set_issuer_gid(self, gid):
128         self.gid = gid
129
130     def decode(self):
131         doc = parseString(self.xml)
132         sig = doc.getElementsByTagName("Signature")[0]
133         self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
134         keyinfo = sig.getElementsByTagName("X509Data")[0]
135         szgid = getTextNode(keyinfo, "X509Certificate")
136         szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
137         self.set_issuer_gid(GID(string=szgid))        
138         
139     def encode(self):
140         self.xml = signature_template % (self.get_refid(), self.get_refid())
141
142
143 ##
144 # A credential provides a caller gid with privileges to an object gid.
145 # A signed credential is signed by the object's authority.
146 #
147 # Credentials are encoded in one of two ways.  The legacy style places
148 # it in the subjectAltName of an X509 certificate.  The new credentials
149 # are placed in signed XML.
150 #
151 # WARNING:
152 # In general, a signed credential obtained externally should
153 # not be changed else the signature is no longer valid.  So, once
154 # you have loaded an existing signed credential, do not call encode() or sign() on it.
155
156
157 class Credential(object):
158
159
160     ##
161     # Create a Credential object
162     #
163     # @param create If true, create a blank x509 certificate
164     # @param subject If subject!=None, create an x509 cert with the subject name
165     # @param string If string!=None, load the credential from the string
166     # @param filename If filename!=None, load the credential from the file
167
168     def __init__(self, create=False, subject=None, string=None, filename=None):
169         self.gidCaller = None
170         self.gidObject = None
171         self.expiration = None
172         self.privileges = None
173         self.issuer_privkey = None
174         self.issuer_gid = None
175         self.issuer_pubkey = None
176         self.parent = None
177         self.signature = None
178         self.xml = None
179         self.refid = None
180         self.legacy = None
181
182
183
184
185         # Check if this is a legacy credential, translate it if so
186         if string or filename:
187             if string:                
188                 str = string
189             elif filename:
190                 str = file(filename).read()
191                 
192             if str.strip().startswith("-----"):
193                 self.legacy = CredentialLegacy(False,string=str)
194                 self.translate_legacy(str)
195             else:
196                 self.xml = str
197                 self.decode()
198
199         # Find an xmlsec1 path
200         self.xmlsec_path = ''
201         paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
202         for path in paths:
203             if os.path.isfile(path + '/' + 'xmlsec1'):
204                 self.xmlsec_path = path + '/' + 'xmlsec1'
205                 break
206
207
208     def get_signature(self):
209         if not self.signature:
210             self.decode()
211         return self.signature
212
213     def set_signature(self, sig):
214         self.signature = sig
215
216         
217     ##
218     # Translate a legacy credential into a new one
219     #
220     # @param String of the legacy credential
221
222     def translate_legacy(self, str):
223         legacy = CredentialLegacy(False,string=str)
224         self.gidCaller = legacy.get_gid_caller()
225         self.gidObject = legacy.get_gid_object()
226         lifetime = legacy.get_lifetime()
227         if not lifetime:
228             # Default to two years
229             self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
230         else:
231             self.set_lifetime(int(lifetime))
232         self.lifeTime = legacy.get_lifetime()
233         self.set_privileges(legacy.get_privileges())
234         self.get_privileges().delegate_all_privileges(legacy.get_delegate())
235
236     ##
237     # Need the issuer's private key and name
238     # @param key Keypair object containing the private key of the issuer
239     # @param gid GID of the issuing authority
240
241     def set_issuer_keys(self, privkey, gid):
242         self.issuer_privkey = privkey
243         self.issuer_gid = gid
244
245
246     ##
247     # Set this credential's parent
248     def set_parent(self, cred):
249         self.parent = cred
250         self.updateRefID()
251
252     ##
253     # set the GID of the caller
254     #
255     # @param gid GID object of the caller
256
257     def set_gid_caller(self, gid):
258         self.gidCaller = gid
259         # gid origin caller is the caller's gid by default
260         self.gidOriginCaller = gid
261
262     ##
263     # get the GID of the object
264
265     def get_gid_caller(self):
266         if not self.gidCaller:
267             self.decode()
268         return self.gidCaller
269
270     ##
271     # set the GID of the object
272     #
273     # @param gid GID object of the object
274
275     def set_gid_object(self, gid):
276         self.gidObject = gid
277
278     ##
279     # get the GID of the object
280
281     def get_gid_object(self):
282         if not self.gidObject:
283             self.decode()
284         return self.gidObject
285
286     ##
287     # set the lifetime of this credential
288     #
289     # @param lifetime lifetime of credential
290     # . if lifeTime is a datetime object, it is used for the expiration time
291     # . if lifeTime is an integer value, it is considered the number of seconds
292     #   remaining before expiration
293
294     def set_lifetime(self, lifeTime):
295         if isinstance(lifeTime, int):
296             self.expiration = datetime.timedelta(seconds=lifeTime) + datetime.datetime.utcnow()
297         else:
298             self.expiration = lifeTime
299
300     ##
301     # get the lifetime of the credential (in datetime format)
302
303     def get_lifetime(self):
304         if not self.expiration:
305             self.decode()
306         return self.expiration
307
308  
309     ##
310     # set the privileges
311     #
312     # @param privs either a comma-separated list of privileges of a RightList object
313
314     def set_privileges(self, privs):
315         if isinstance(privs, str):
316             self.privileges = RightList(string = privs)
317         else:
318             self.privileges = privs
319         
320
321     ##
322     # return the privileges as a RightList object
323
324     def get_privileges(self):
325         if not self.privileges:
326             self.decode()
327         return self.privileges
328
329     ##
330     # determine whether the credential allows a particular operation to be
331     # performed
332     #
333     # @param op_name string specifying name of operation ("lookup", "update", etc)
334
335     def can_perform(self, op_name):
336         rights = self.get_privileges()
337         
338         if not rights:
339             return False
340
341         return rights.can_perform(op_name)
342
343
344     ##
345     # Encode the attributes of the credential into an XML string    
346     # This should be done immediately before signing the credential.    
347     # WARNING:
348     # In general, a signed credential obtained externally should
349     # not be changed else the signature is no longer valid.  So, once
350     # you have loaded an existing signed credential, do not call encode() or sign() on it.
351
352     def encode(self):
353         # Create the XML document
354         doc = Document()
355         signed_cred = doc.createElement("signed-credential")
356         doc.appendChild(signed_cred)  
357         
358         # Fill in the <credential> bit        
359         cred = doc.createElement("credential")
360         cred.setAttribute("xml:id", self.get_refid())
361         signed_cred.appendChild(cred)
362         append_sub(doc, cred, "type", "privilege")
363         append_sub(doc, cred, "serial", "8")
364         append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
365         append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
366         append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
367         append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
368         append_sub(doc, cred, "uuid", "")
369         if  not self.expiration:
370             self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
371         self.expiration = self.expiration.replace(microsecond=0)
372         append_sub(doc, cred, "expires", self.expiration.isoformat())
373         privileges = doc.createElement("privileges")
374         cred.appendChild(privileges)
375
376         if self.privileges:
377             rights = self.get_privileges()
378             for right in rights.rights:
379                 priv = doc.createElement("privilege")
380                 append_sub(doc, priv, "name", right.kind)
381                 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
382                 privileges.appendChild(priv)
383
384         # Add the parent credential if it exists
385         if self.parent:
386             sdoc = parseString(self.parent.get_xml())
387             p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
388             p = doc.createElement("parent")
389             p.appendChild(p_cred)
390             cred.appendChild(p)
391
392
393         # Create the <signatures> tag
394         signatures = doc.createElement("signatures")
395         signed_cred.appendChild(signatures)
396
397         # Add any parent signatures
398         if self.parent:
399             cur_cred = self.parent
400             while cur_cred:
401                 sdoc = parseString(cur_cred.get_signature().get_xml())
402                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
403                 signatures.appendChild(ele)
404
405                 if cur_cred.parent:
406                     cur_cred = cur_cred.parent
407                 else:
408                     cur_cred = None
409                 
410         # Get the finished product
411         self.xml = doc.toxml()
412
413
414     def save_to_random_tmp_file(self):       
415         fp, filename = mkstemp(suffix='cred', text=True)
416         fp = os.fdopen(fp, "w")
417         self.save_to_file(filename, save_parents=True, filep=fp)
418         return filename
419     
420     def save_to_file(self, filename, save_parents=True, filep=None):
421         if not self.xml:
422             self.encode()
423         if filep:
424             f = filep 
425         else:
426             f = open(filename, "w")
427         f.write(self.xml)
428         f.close()
429
430     def save_to_string(self, save_parents=True):
431         if not self.xml:
432             self.encode()
433         return self.xml
434
435     def get_refid(self):
436         if not self.refid:
437             self.refid = 'ref0'
438         return self.refid
439
440     def set_refid(self, rid):
441         self.refid = rid
442
443     ##
444     # Figure out what refids exist, and update this credential's id
445     # so that it doesn't clobber the others.  Returns the refids of
446     # the parents.
447     
448     def updateRefID(self):
449         if not self.parent:
450             self.set_refid('ref0')
451             return []
452         
453         refs = []
454
455         next_cred = self.parent
456         while next_cred:
457             refs.append(next_cred.get_refid())
458             if next_cred.parent:
459                 next_cred = next_cred.parent
460             else:
461                 next_cred = None
462
463         
464         # Find a unique refid for this credential
465         rid = self.get_refid()
466         while rid in refs:
467             val = int(rid[3:])
468             rid = "ref%d" % (val + 1)
469
470         # Set the new refid
471         self.set_refid(rid)
472
473         # Return the set of parent credential ref ids
474         return refs
475
476     def get_xml(self):
477         if not self.xml:
478             self.encode()
479         return self.xml
480
481     ##
482     # Sign the XML file created by encode()
483     #
484     # WARNING:
485     # In general, a signed credential obtained externally should
486     # not be changed else the signature is no longer valid.  So, once
487     # you have loaded an existing signed credential, do not call encode() or sign() on it.
488
489     def sign(self):
490         if not self.issuer_privkey or not self.issuer_gid:
491             return
492         doc = parseString(self.get_xml())
493         sigs = doc.getElementsByTagName("signatures")[0]
494
495         # Create the signature template to be signed
496         signature = Signature()
497         signature.set_refid(self.get_refid())
498         sdoc = parseString(signature.get_xml())        
499         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
500         sigs.appendChild(sig_ele)
501
502         self.xml = doc.toxml()
503
504
505         # Split the issuer GID into multiple certificates if it's a chain
506         chain = GID(filename=self.issuer_gid)
507         gid_files = []
508         while chain:
509             gid_files.append(chain.save_to_random_tmp_file(False))
510             if chain.get_parent():
511                 chain = chain.get_parent()
512             else:
513                 chain = None
514
515
516         # Call out to xmlsec1 to sign it
517         ref = 'Sig_%s' % self.get_refid()
518         filename = self.save_to_random_tmp_file()
519         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
520                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
521         os.remove(filename)
522
523         for gid_file in gid_files:
524             os.remove(gid_file)
525
526         self.xml = signed
527
528         # This is no longer a legacy credential
529         if self.legacy:
530             self.legacy = None
531
532         # Update signatures
533         self.decode()
534
535         
536
537         
538     ##
539     # Retrieve the attributes of the credential from the XML.
540     # This is automatically called by the various get_* methods of
541     # this class and should not need to be called explicitly.
542
543     def decode(self):
544         if not self.xml:
545             return
546         doc = parseString(self.xml)
547         sigs = []
548         signed_cred = doc.getElementsByTagName("signed-credential")
549
550         # Is this a signed-cred or just a cred?
551         if len(signed_cred) > 0:
552             cred = signed_cred[0].getElementsByTagName("credential")[0]
553             signatures = signed_cred[0].getElementsByTagName("signatures")
554             if len(signatures) > 0:
555                 sigs = signatures[0].getElementsByTagName("Signature")
556         else:
557             cred = doc.getElementsByTagName("credential")[0]
558         
559
560
561         self.set_refid(cred.getAttribute("xml:id"))
562         self.set_lifetime(parse(getTextNode(cred, "expires")))
563         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
564         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
565
566
567         # Process privileges
568         privs = cred.getElementsByTagName("privileges")[0]
569         rlist = RightList()
570         for priv in privs.getElementsByTagName("privilege"):
571             kind = getTextNode(priv, "name")
572             deleg = str2bool(getTextNode(priv, "can_delegate"))
573             if kind == '*':
574                 # Convert * into the default privileges for the credential's type                
575                 _ , type = urn_to_hrn(self.gidObject.get_urn())
576                 rl = rlist.determine_rights(type, self.gidObject.get_urn())
577                 for r in rl.rights:
578                     rlist.add(r)
579             else:
580                 rlist.add(Right(kind.strip(), deleg))
581         self.set_privileges(rlist)
582
583
584         # Is there a parent?
585         parent = cred.getElementsByTagName("parent")
586         if len(parent) > 0:
587             parent_doc = parent[0].getElementsByTagName("credential")[0]
588             parent_xml = parent_doc.toxml()
589             self.parent = Credential(string=parent_xml)
590             self.updateRefID()
591
592         # Assign the signatures to the credentials
593         for sig in sigs:
594             Sig = Signature(string=sig.toxml())
595
596             cur_cred = self
597             while cur_cred:
598                 if cur_cred.get_refid() == Sig.get_refid():
599                     cur_cred.set_signature(Sig)
600                     
601                 if cur_cred.parent:
602                     cur_cred = cur_cred.parent
603                 else:
604                     cur_cred = None
605                 
606             
607     ##
608     # Verify that:
609     # . All of the signatures are valid and that the issuers trace back
610     #   to trusted roots (performed by xmlsec1)
611     # . The XML matches the credential schema
612     # . That the issuer of the credential is the authority in the target's urn
613     #    . In the case of a delegated credential, this must be true of the root
614     # . That all of the gids presented in the credential are valid
615     #
616     # -- For Delegates (credentials with parents)
617     # . The privileges must be a subset of the parent credentials
618     # . The privileges must have "can_delegate" set for each delegated privilege
619     # . The target gid must be the same between child and parents
620     # . The expiry time on the child must be no later than the parent
621     # . The signer of the child must be the owner of the parent
622     #
623     # -- Verify does *NOT*
624     # . ensure that an xmlrpc client's gid matches a credential gid, that
625     #   must be done elsewhere
626     #
627     # @param trusted_certs: The certificates of trusted CA certificates
628     
629     def verify(self, trusted_certs):
630         if not self.xml:
631             self.decode()        
632
633         trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
634
635         # Use legacy verification if this is a legacy credential
636         if self.legacy:
637             self.legacy.verify_chain(trusted_cert_objects)
638             if self.legacy.client_gid:
639                 self.legacy.client_gid.verify_chain(trusted_cert_objects)
640             if self.legacy.object_gid:
641                 self.legacy.object_gid.verify_chain(trusted_cert_objects)
642             return True
643
644         # Verify the signatures
645         filename = self.save_to_random_tmp_file()
646         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
647
648         # Verify the gids of this cred and of its parents
649
650         cur_cred = self
651         while cur_cred:
652             cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
653             cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
654             if cur_cred.parent:
655                 cur_cred = cur_cred.parent
656             else:
657                 cur_cred = None
658         
659         refs = []
660         refs.append("Sig_%s" % self.get_refid())
661
662         parentRefs = self.updateRefID()
663         for ref in parentRefs:
664             refs.append("Sig_%s" % ref)
665
666         for ref in refs:
667             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
668                             % (self.xmlsec_path, ref, cert_args, filename)).read()
669             if not verified.strip().startswith("OK"):
670                 raise CredentialNotVerifiable("xmlsec1 error: " + verified)
671         os.remove(filename)
672
673         # Verify the parents (delegation)
674         if self.parent:
675             self.verify_parent(self.parent)
676         # Make sure the issuer is the target's authority
677         self.verify_issuer()
678         return True
679
680         
681     ##
682     # Make sure the issuer of this credential is the target's authority
683     def verify_issuer(self):        
684         target_authority = get_authority(self.get_gid_object().get_urn())
685
686         
687         # Find the root credential's signature
688         cur_cred = self
689         while cur_cred:            
690             if cur_cred.parent:
691                 cur_cred = cur_cred.parent
692             else:
693                 root_issuer = cur_cred.get_signature().get_issuer_gid().get_urn()
694                 cur_cred = None
695
696         # Ensure that the signer of the root credential is the target_authority
697         target_authority = hrn_to_urn(target_authority, 'authority')
698
699         if root_issuer != target_authority:
700             raise CredentialNotVerifiable("issuer (%s) != authority of target (%s)" \
701                                           % (root_issuer, target_authority))
702
703     ##
704     # -- For Delegates (credentials with parents) verify that:
705     # . The privileges must be a subset of the parent credentials
706     # . The privileges must have "can_delegate" set for each delegated privilege
707     # . The target gid must be the same between child and parents
708     # . The expiry time on the child must be no later than the parent
709     # . The signer of the child must be the owner of the parent
710         
711     def verify_parent(self, parent_cred):
712         # make sure the rights given to the child are a subset of the
713         # parents rights (and check delegate bits)
714         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
715             raise ChildRightsNotSubsetOfParent(
716                 self.parent.get_privileges().save_to_string() + " " +
717                 self.get_privileges().save_to_string())
718
719         # make sure my target gid is the same as the parent's
720         if not parent_cred.get_gid_object().save_to_string() == \
721            self.get_gid_object().save_to_string():
722             raise CredentialNotVerifiable("target gid not equal between parent and child")
723
724         # make sure my expiry time is <= my parent's
725         if not parent_cred.get_lifetime() >= self.get_lifetime():
726             raise CredentialNotVerifiable("delegated credential expires after parent")
727
728         # make sure my signer is the parent's caller
729         if not parent_cred.get_gid_caller().save_to_string(False) == \
730            self.get_signature().get_issuer_gid().save_to_string(False):
731             raise CredentialNotVerifiable("delegated credential not signed by parent caller")
732                 
733         if parent_cred.parent:
734             parent_cred.verify_parent(parent_cred.parent)
735
736     ##
737     # Dump the contents of a credential to stdout in human-readable format
738     #
739     # @param dump_parents If true, also dump the parent certificates
740
741     def dump(self, dump_parents=False):
742         print "CREDENTIAL", self.get_subject()
743
744         print "      privs:", self.get_privileges().save_to_string()
745
746         print "  gidCaller:"
747         gidCaller = self.get_gid_caller()
748         if gidCaller:
749             gidCaller.dump(8, dump_parents)
750
751         print "  gidObject:"
752         gidObject = self.get_gid_object()
753         if gidObject:
754             gidObject.dump(8, dump_parents)
755
756
757         if self.parent and dump_parents:
758             print "PARENT",
759             self.parent.dump_parents()
760