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