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