merging with geni-api branch
[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             for cur_cred in self.get_credential_list()[1:]:
400                 sdoc = parseString(cur_cred.get_signature().get_xml())
401                 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
402                 signatures.appendChild(ele)
403                 
404         # Get the finished product
405         self.xml = doc.toxml()
406
407
408     def save_to_random_tmp_file(self):       
409         fp, filename = mkstemp(suffix='cred', text=True)
410         fp = os.fdopen(fp, "w")
411         self.save_to_file(filename, save_parents=True, filep=fp)
412         return filename
413     
414     def save_to_file(self, filename, save_parents=True, filep=None):
415         if not self.xml:
416             self.encode()
417         if filep:
418             f = filep 
419         else:
420             f = open(filename, "w")
421         f.write(self.xml)
422         f.close()
423
424     def save_to_string(self, save_parents=True):
425         if not self.xml:
426             self.encode()
427         return self.xml
428
429     def get_refid(self):
430         if not self.refid:
431             self.refid = 'ref0'
432         return self.refid
433
434     def set_refid(self, rid):
435         self.refid = rid
436
437     ##
438     # Figure out what refids exist, and update this credential's id
439     # so that it doesn't clobber the others.  Returns the refids of
440     # the parents.
441     
442     def updateRefID(self):
443         if not self.parent:
444             self.set_refid('ref0')
445             return []
446         
447         refs = []
448
449         next_cred = self.parent
450         while next_cred:
451             refs.append(next_cred.get_refid())
452             if next_cred.parent:
453                 next_cred = next_cred.parent
454             else:
455                 next_cred = None
456
457         
458         # Find a unique refid for this credential
459         rid = self.get_refid()
460         while rid in refs:
461             val = int(rid[3:])
462             rid = "ref%d" % (val + 1)
463
464         # Set the new refid
465         self.set_refid(rid)
466
467         # Return the set of parent credential ref ids
468         return refs
469
470     def get_xml(self):
471         if not self.xml:
472             self.encode()
473         return self.xml
474
475     ##
476     # Sign the XML file created by encode()
477     #
478     # WARNING:
479     # In general, a signed credential obtained externally should
480     # not be changed else the signature is no longer valid.  So, once
481     # you have loaded an existing signed credential, do not call encode() or sign() on it.
482
483     def sign(self):
484         if not self.issuer_privkey or not self.issuer_gid:
485             return
486         doc = parseString(self.get_xml())
487         sigs = doc.getElementsByTagName("signatures")[0]
488
489         # Create the signature template to be signed
490         signature = Signature()
491         signature.set_refid(self.get_refid())
492         sdoc = parseString(signature.get_xml())        
493         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
494         sigs.appendChild(sig_ele)
495
496         self.xml = doc.toxml()
497
498
499         # Split the issuer GID into multiple certificates if it's a chain
500         chain = GID(filename=self.issuer_gid)
501         gid_files = []
502         while chain:
503             gid_files.append(chain.save_to_random_tmp_file(False))
504             if chain.get_parent():
505                 chain = chain.get_parent()
506             else:
507                 chain = None
508
509
510         # Call out to xmlsec1 to sign it
511         ref = 'Sig_%s' % self.get_refid()
512         filename = self.save_to_random_tmp_file()
513         signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
514                  % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
515         os.remove(filename)
516
517         for gid_file in gid_files:
518             os.remove(gid_file)
519
520         self.xml = signed
521
522         # This is no longer a legacy credential
523         if self.legacy:
524             self.legacy = None
525
526         # Update signatures
527         self.decode()
528
529         
530
531         
532     ##
533     # Retrieve the attributes of the credential from the XML.
534     # This is automatically called by the various get_* methods of
535     # this class and should not need to be called explicitly.
536
537     def decode(self):
538         if not self.xml:
539             return
540         doc = parseString(self.xml)
541         sigs = []
542         signed_cred = doc.getElementsByTagName("signed-credential")
543
544         # Is this a signed-cred or just a cred?
545         if len(signed_cred) > 0:
546             cred = signed_cred[0].getElementsByTagName("credential")[0]
547             signatures = signed_cred[0].getElementsByTagName("signatures")
548             if len(signatures) > 0:
549                 sigs = signatures[0].getElementsByTagName("Signature")
550         else:
551             cred = doc.getElementsByTagName("credential")[0]
552         
553
554
555         self.set_refid(cred.getAttribute("xml:id"))
556         self.set_lifetime(parse(getTextNode(cred, "expires")))
557         self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
558         self.gidObject = GID(string=getTextNode(cred, "target_gid"))   
559
560
561         # Process privileges
562         privs = cred.getElementsByTagName("privileges")[0]
563         rlist = RightList()
564         for priv in privs.getElementsByTagName("privilege"):
565             kind = getTextNode(priv, "name")
566             deleg = str2bool(getTextNode(priv, "can_delegate"))
567             if kind == '*':
568                 # Convert * into the default privileges for the credential's type                
569                 _ , type = urn_to_hrn(self.gidObject.get_urn())
570                 rl = rlist.determine_rights(type, self.gidObject.get_urn())
571                 for r in rl.rights:
572                     rlist.add(r)
573             else:
574                 rlist.add(Right(kind.strip(), deleg))
575         self.set_privileges(rlist)
576
577
578         # Is there a parent?
579         parent = cred.getElementsByTagName("parent")
580         if len(parent) > 0:
581             parent_doc = parent[0].getElementsByTagName("credential")[0]
582             parent_xml = parent_doc.toxml()
583             self.parent = Credential(string=parent_xml)
584             self.updateRefID()
585
586         # Assign the signatures to the credentials
587         for sig in sigs:
588             Sig = Signature(string=sig.toxml())
589
590             for cur_cred in self.get_credential_list():
591                 if cur_cred.get_refid() == Sig.get_refid():
592                     cur_cred.set_signature(Sig)
593                                     
594             
595     ##
596     # Verify
597     #   trusted_certs: A list of trusted GID filenames (not GID objects!) 
598     #                  Chaining is not supported within the GIDs by xmlsec1.
599     #    
600     # Verify that:
601     # . All of the signatures are valid and that the issuers trace back
602     #   to trusted roots (performed by xmlsec1)
603     # . The XML matches the credential schema
604     # . That the issuer of the credential is the authority in the target's urn
605     #    . In the case of a delegated credential, this must be true of the root
606     # . That all of the gids presented in the credential are valid
607     # . The credential is not expired
608     #
609     # -- For Delegates (credentials with parents)
610     # . The privileges must be a subset of the parent credentials
611     # . The privileges must have "can_delegate" set for each delegated privilege
612     # . The target gid must be the same between child and parents
613     # . The expiry time on the child must be no later than the parent
614     # . The signer of the child must be the owner of the parent
615     #
616     # -- Verify does *NOT*
617     # . ensure that an xmlrpc client's gid matches a credential gid, that
618     #   must be done elsewhere
619     #
620     # @param trusted_certs: The certificates of trusted CA certificates
621     
622     def verify(self, trusted_certs):
623         if not self.xml:
624             self.decode()        
625         trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
626
627         # Use legacy verification if this is a legacy credential
628         if self.legacy:
629             self.legacy.verify_chain(trusted_cert_objects)
630             if self.legacy.client_gid:
631                 self.legacy.client_gid.verify_chain(trusted_cert_objects)
632             if self.legacy.object_gid:
633                 self.legacy.object_gid.verify_chain(trusted_cert_objects)
634             return True
635         
636         # make sure it is not expired
637         if self.get_lifetime() < datetime.datetime.utcnow():
638             raise CredentialNotVerifiable("credential is expired")
639
640         # Verify the signatures
641         filename = self.save_to_random_tmp_file()
642         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
643
644         # Verify the gids of this cred and of its parents
645
646
647
648         for cur_cred in self.get_credential_list():
649             cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
650             cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)            
651
652
653         refs = []
654         refs.append("Sig_%s" % self.get_refid())
655
656         parentRefs = self.updateRefID()
657         for ref in parentRefs:
658             refs.append("Sig_%s" % ref)
659
660         for ref in refs:
661             verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
662                             % (self.xmlsec_path, ref, cert_args, filename)).read()
663             if not verified.strip().startswith("OK"):
664                 raise CredentialNotVerifiable("xmlsec1 error: " + verified)
665         os.remove(filename)
666
667         # Verify the parents (delegation)
668         if self.parent:
669             self.verify_parent(self.parent)
670
671         # Make sure the issuer is the target's authority
672         self.verify_issuer()
673         return True
674
675     ##
676     # Creates a list of the credential and its parents, with the root 
677     # (original delegated credential) as the last item in the list
678     def get_credential_list(self):    
679         cur_cred = self
680         list = []
681         while cur_cred:
682             list.append(cur_cred)
683             if cur_cred.parent:
684                 cur_cred = cur_cred.parent
685             else:
686                 cur_cred = None
687         return list
688     
689     ##
690     # Make sure the credential's target gid was signed by (or is the same) as the entity that signed
691     # the original credential.  
692     def verify_issuer(self):                
693         root_cred = self.get_credential_list()[-1]
694         root_target_gid = root_cred.get_gid_object()
695         root_cred_signer = root_cred.get_signature().get_issuer_gid()
696         
697         if root_target_gid.is_signed_by_cert(root_cred_signer) or \
698             root_target_gid.save_to_string() == root_cred_signer.save_to_string():
699             pass
700         else:            
701             raise CredentialNotVerifiable("Could not verify credential signer")
702         
703
704     ##
705     # -- For Delegates (credentials with parents) verify that:
706     # . The privileges must be a subset of the parent credentials
707     # . The privileges must have "can_delegate" set for each delegated privilege
708     # . The target gid must be the same between child and parents
709     # . The expiry time on the child must be no later than the parent
710     # . The signer of the child must be the owner of the parent
711         
712     def verify_parent(self, parent_cred):
713         # make sure the rights given to the child are a subset of the
714         # parents rights (and check delegate bits)
715         if not parent_cred.get_privileges().is_superset(self.get_privileges()):
716             raise ChildRightsNotSubsetOfParent(
717                 self.parent.get_privileges().save_to_string() + " " +
718                 self.get_privileges().save_to_string())
719
720         # make sure my target gid is the same as the parent's
721         if not parent_cred.get_gid_object().save_to_string() == \
722            self.get_gid_object().save_to_string():
723             raise CredentialNotVerifiable("target gid not equal between parent and child")
724
725         # make sure my expiry time is <= my parent's
726         if not parent_cred.get_lifetime() >= self.get_lifetime():
727             raise CredentialNotVerifiable("delegated credential expires after parent")
728
729         # make sure my signer is the parent's caller
730         if not parent_cred.get_gid_caller().save_to_string(False) == \
731            self.get_signature().get_issuer_gid().save_to_string(False):
732             raise CredentialNotVerifiable("delegated credential not signed by parent caller")
733                 
734         if parent_cred.parent:
735             parent_cred.verify_parent(parent_cred.parent)
736
737     ##
738     # Dump the contents of a credential to stdout in human-readable format
739     #
740     # @param dump_parents If true, also dump the parent certificates
741
742     def dump(self, dump_parents=False):
743         print "CREDENTIAL", self.get_subject()
744
745         print "      privs:", self.get_privileges().save_to_string()
746
747         print "  gidCaller:"
748         gidCaller = self.get_gid_caller()
749         if gidCaller:
750             gidCaller.dump(8, dump_parents)
751
752         print "  gidObject:"
753         gidObject = self.get_gid_object()
754         if gidObject:
755             gidObject.dump(8, dump_parents)
756
757
758         if self.parent and dump_parents:
759             print "PARENT",
760             self.parent.dump_parents()
761