Lots of credential updates.. can actually perform list now. Delegation/parents/verif...
[sfa.git] / sfa / trust / credential.py
1 ##
2 # Implements SFA Credentials
3 #
4 # Credentials are layered on top of certificates, and are essentially a
5 # certificate that stores a tuple of parameters.
6 ##
7
8 ### $Id$
9 ### $URL$
10
11 import xmlrpclib
12 import random
13 import os
14 import datetime
15
16 import xml.dom.minidom
17 from xml.dom.minidom import Document
18 from sfa.trust.credential_legacy import CredentialLegacy
19 from sfa.trust.certificate import Certificate
20 from sfa.trust.rights import *
21 from sfa.trust.gid import *
22 from sfa.util.faults import *
23 from sfa.util.sfalogging import logger
24
25
26 # TODO:
27 # . Need to verify credential parents (delegation)
28 # . implement verify_chain
29 # . Need to manage ref #s
30 # . Need to add privileges (make PG and PL privs work together and add delegation per privilege instead of global)
31 # . Need to test
32
33 signature_template = \
34 '''
35 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
36     <SignedInfo>
37       <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
38       <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
39       <Reference URI="#%s">
40       <Transforms>
41         <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
42       </Transforms>
43       <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
44       <DigestValue></DigestValue>
45       </Reference>
46     </SignedInfo>
47     <SignatureValue />
48       <KeyInfo>
49         <X509Data>
50           <X509SubjectName/>
51           <X509IssuerSerial/>
52           <X509Certificate/>
53         </X509Data>
54       <KeyValue />
55       </KeyInfo>
56     </Signature>
57 '''
58
59
60
61 ##
62 # Credential is a tuple:
63 #    (GIDCaller, GIDObject, Expiration (in UTC time), Privileges, DelegateBit)
64 #
65 # These fields are encoded in one of two ways.  The legacy style places
66 # it in the subjectAltName of an X509 certificate.  The new credentials
67 # are placed in signed XML.
68
69
70 class Credential(object):
71     gidCaller = None
72     gidObject = None
73     expiration = None
74     privileges = None
75     delegate = False
76     issuer_privkey = None
77     issuer_gid = None
78     issuer_pubkey = None
79     parent = None
80     xml = None
81     max_refid = 0
82     ##
83     # Create a Credential object
84     #
85     # @param create If true, create a blank x509 certificate
86     # @param subject If subject!=None, create an x509 cert with the subject name
87     # @param string If string!=None, load the credential from the string
88     # @param filename If filename!=None, load the credential from the file
89
90     def __init__(self, create=False, subject=None, string=None, filename=None):
91
92         # Check if this is a legacy credential, translate it if so
93         if string or filename:
94             if string:                
95                 str = string
96             elif filename:
97                 str = file(filename).read()
98                 
99             if str.strip().startswith("-----"):
100                 self.translate_legacy(str)
101             else:
102                 self.xml = str
103                 self.decode()
104                 
105     ##
106     # Translate a legacy credential into a new one
107     #
108     # @param String of the legacy credential
109
110     def translate_legacy(self, str):
111         legacy = CredentialLegacy(False,string=str)
112         self.gidCaller = legacy.get_gid_caller()
113         self.gidObject = legacy.get_gid_object()
114         lifetime = legacy.get_lifetime()
115         if not lifetime:
116             self.set_lifetime(3600)
117         else:
118             self.set_lifetime(int(lifetime))
119         self.lifeTime = legacy.get_lifetime()
120         self.privileges = legacy.get_privileges()
121         self.delegate = legacy.get_delegate()
122
123     ##
124     # Need the issuer's private key and name
125     # @param key Keypair object containing the private key of the issuer
126     # @param gid GID of the issuing authority
127
128     def set_issuer_keys(self, privkey, gid):
129         self.issuer_privkey = privkey
130         self.issuer_gid = gid
131
132     def set_issuer(self, issuer):
133         issuer = issuer
134
135     def set_subject(self, subject):
136         subject = subject
137         
138     def set_pubkey(self, pubkey):
139         self.issuer_pubkey = pubkey
140
141     def set_parent(self, cred):
142         self.parent = cred
143
144     ##
145     # set the GID of the caller
146     #
147     # @param gid GID object of the caller
148
149     def set_gid_caller(self, gid):
150         self.gidCaller = gid
151         # gid origin caller is the caller's gid by default
152         self.gidOriginCaller = gid
153
154     ##
155     # get the GID of the object
156
157     def get_gid_caller(self):
158         if not self.gidCaller:
159             self.decode()
160         return self.gidCaller
161
162     ##
163     # set the GID of the object
164     #
165     # @param gid GID object of the object
166
167     def set_gid_object(self, gid):
168         self.gidObject = gid
169
170     ##
171     # get the GID of the object
172
173     def get_gid_object(self):
174         if not self.gidObject:
175             self.decode()
176         return self.gidObject
177
178     ##
179     # set the lifetime of this credential
180     #
181     # @param lifetime lifetime of credential
182     # . if lifeTime is a datetime object, it is used for the expiration time
183     # . if lifeTime is an integer value, it is considered the number of minutes
184     #   remaining before expiration
185
186     def set_lifetime(self, lifeTime):
187         if isinstance(lifeTime, int):
188             self.expiration = datetime.timedelta(seconds=lifeTime*60) + datetime.datetime.utcnow()
189         else:
190             self.expiration = lifeTime
191
192     ##
193     # get the lifetime of the credential (in minutes)
194
195     def get_lifetime(self):
196         if not self.lifeTime:
197             self.decode()
198         return self.expiration
199
200     ##
201     # set the delegate bit
202     #
203     # @param delegate boolean (True or False)
204
205     def set_delegate(self, delegate):
206         self.delegate = delegate
207
208     ##
209     # get the delegate bit
210
211     def get_delegate(self):
212         if not self.delegate:
213             self.decode()
214         return self.delegate
215
216     ##
217     # set the privileges
218     #
219     # @param privs either a comma-separated list of privileges of a RightList object
220
221     def set_privileges(self, privs):
222         if isinstance(privs, str):
223             self.privileges = RightList(string = privs)
224         else:
225             self.privileges = privs
226
227     ##
228     # return the privileges as a RightList object
229
230     def get_privileges(self):
231         if not self.privileges:
232             self.decode()
233         return self.privileges
234
235     ##
236     # determine whether the credential allows a particular operation to be
237     # performed
238     #
239     # @param op_name string specifying name of operation ("lookup", "update", etc)
240
241     def can_perform(self, op_name):
242         rights = self.get_privileges()
243         
244         if not rights:
245             return False
246
247         return rights.can_perform(op_name)
248
249     def append_sub(self, doc, parent, element, text):
250         ele = doc.createElement(element)
251         ele.appendChild(doc.createTextNode(text))
252         parent.appendChild(ele)
253
254     ##
255     # Encode the attributes of the credential into an XML string    
256     # This should be done immediately before signing the credential.    
257
258     def encode(self):
259         p_sigs = None
260
261         # Get information from the parent credential
262         if self.parent:
263             if not self.parent.xml:
264                 self.parent.encode()            
265             p_doc = xml.dom.minidom.parseString(self.parent.xml)
266             p_signed_cred = p_doc.getElementsByTagName("signed-credential")[0]
267             p_cred = p_signed_cred.getElementsByTagName("credential")[0]               
268             p_signatures = p_signed_cred.getElementsByTagName("signatures")[0]
269             p_sigs = p_signatures.getElementsByTagName("Signature")
270
271         # Create the XML document
272         doc = Document()
273         signed_cred = doc.createElement("signed-credential")
274         doc.appendChild(signed_cred)  
275         
276
277         # Fill in the <credential> bit
278         refid = "ref%d" % (self.max_refid + 1)
279         self.max_refid += 1
280         
281         cred = doc.createElement("credential")
282         cred.setAttribute("xml:id", refid)
283         signed_cred.appendChild(cred)
284         self.append_sub(doc, cred, "type", "privilege")
285         self.append_sub(doc, cred, "serial", "8")
286         self.append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
287         self.append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
288         self.append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
289         self.append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
290         self.append_sub(doc, cred, "uuid", "")
291         if  not self.expiration:
292             self.set_lifetime(3600)
293         self.expiration = self.expiration.replace(microsecond=0)
294         self.append_sub(doc, cred, "expires", self.expiration.isoformat())
295         privileges = doc.createElement("privileges")
296         cred.appendChild(privileges)
297
298         if self.privileges:
299             rights = self.privileges.save_to_string().split(",")
300             for right in rights:
301                 priv = doc.createElement("privilege")
302                 self.append_sub(doc, priv, "name", right.strip())
303                 self.append_sub(doc, priv, "can_delegate", str(self.delegate))
304                 privileges.appendChild(priv)
305
306         # Add the parent credential if it exists
307         if self.parent:
308             parent = doc.createElement("parent")
309             parent.appendChild(p_cred)
310             cred.appendChild(parent)
311         
312
313         signed_cred.appendChild(cred)
314
315
316         # Fill in the <signature> bit
317         signatures = doc.createElement("signatures")
318
319         sz_sig = signature_template % (refid,refid)
320
321         sdoc = xml.dom.minidom.parseString(sz_sig)
322         sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
323         signatures.appendChild(sig_ele)
324         
325         # Add any existing signatures from the parent credential
326         if p_sigs:
327             for sig in p_sigs:
328                 signatures.appendChild(sig)
329
330
331         # Add any parent signatures
332         if self.parent:
333             for sig in p_sigs:
334                 signatures.appendChild(sig)
335
336         signed_cred.appendChild(signatures)
337         # Get the finished product
338         self.xml = doc.toxml()
339         #print doc.toprettyxml()
340
341
342     def save_to_random_tmp_file(self):
343         filename = "/tmp/cred_%d" % random.randint(0,999999999)
344         self.save_to_file(filename)
345         return filename
346     
347     def save_to_file(self, filename, save_parents=True):
348         if not self.xml:
349             self.encode()
350         f = open(filename, "w")
351         f.write(self.xml)
352         f.close()
353
354     def save_to_string(self, save_parents=True):
355         if not self.xml:
356             self.encode()
357         return self.xml
358
359     def sign(self):
360         if not self.xml:
361             self.encode()
362         
363         # Call out to xmlsec1 to sign it
364         ref = 'Sig_ref%d' % self.max_refid
365         filename = self.save_to_random_tmp_file()
366         signed = os.popen('/usr/bin/xmlsec1 --sign --node-id "%s" --privkey-pem %s,%s %s' \
367                  % (ref, self.issuer_privkey, self.issuer_gid, filename)).read()
368         os.remove(filename)
369
370         self.xml = signed
371
372     def getTextNode(self, element, subele):
373         sub = element.getElementsByTagName(subele)[0]
374         return sub.childNodes[0].nodeValue
375
376     ##
377     # Retrieve the attributes of the credential from the XML.
378     # This is automatically caleld by the various get_* methods of
379     # this class and should not need to be called explicitly.
380
381     def decode(self):
382         doc = xml.dom.minidom.parseString(self.xml)
383         signed_cred = doc.getElementsByTagName("signed-credential")[0]
384         cred = signed_cred.getElementsByTagName("credential")[0]
385         signatures = signed_cred.getElementsByTagName("signatures")[0]
386         sigs = signatures.getElementsByTagName("Signature")
387
388
389         self.max_refid = int(cred.getAttribute("xml:id")[3:])
390         sz_expires = self.getTextNode(cred, "expires")
391         if sz_expires != '':
392             self.expiration = datetime.datetime.strptime(sz_expires, '%Y-%m-%dT%H:%M:%S')            
393         self.lifeTime = self.getTextNode(cred, "expires")
394         self.gidCaller = GID(string=self.getTextNode(cred, "owner_gid"))
395         self.gidObject = GID(string=self.getTextNode(cred, "target_gid"))
396         privs = cred.getElementsByTagName("privileges")[0]
397         sz_privs = ''
398         delegates = []
399         for priv in privs.getElementsByTagName("privilege"):
400             sz_privs += self.getTextNode(priv, "name")
401             sz_privs += ","
402             delegates.append(self.getTextNode(priv, "can_delegate"))
403
404         # Can we delegate?
405         delegate = False
406         if "false" not in delegates:
407             self.delegate = True
408
409         # Make the rights list
410         sz_privs.rstrip(", ")
411         self.privileges = RightList(string=sz_privs)
412         self.delegate
413
414         
415     ##
416     # For a simple credential (no delegation) verify..
417     # . That the signature is valid for the credential's xml:id
418     # . That the signer's pub key matches the pub key of the target (object)
419     # . That the target/owner urns match those in the gids
420     #
421     # @param trusted_certs: The certificates of trusted CA certificates
422     
423     def verify(self, trusted_certs):
424         if not self.xml:
425             self.decode()        
426
427         # Verify the sigatures
428         filename = self.save_to_random_tmp_file()
429         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
430
431         ref = "Sig_ref%d" % self.max_refid
432         verified = os.popen('/usr/bin/xmlsec1 --verify --node-id "%s" %s %s 2>&1' \
433                             % (ref, cert_args, filename)).read()
434
435         if not verified.strip().startswith("OK"):
436             raise CredentialNotVerifiable("xmlsec1 error: " + verified)
437
438         os.remove(filename)
439
440     ##
441     # Verify that a chain of credentials is valid (see cert.py:verify). In
442     # addition to the checks for ordinary certificates, verification also
443     # ensures that the delegate bit was set by each parent in the chain. If
444     # a delegate bit was not set, then an exception is thrown.
445     #
446     # Each credential must be a subset of the rights of the parent.
447
448     def verify_chain(self, trusted_certs):
449         return
450
451  ##    def verify_chain(self, trusted_certs = None):
452 ##         # do the normal certificate verification stuff
453 ##         Certificate.verify_chain(self, trusted_certs)
454
455 ##         if self.parent:
456 ##             # make sure the parent delegated rights to the child
457 ##             if not self.parent.get_delegate():
458 ##                 raise MissingDelegateBit(self.parent.get_subject())
459
460 ##             # make sure the rights given to the child are a subset of the
461 ##             # parents rights
462 ##             if not self.parent.get_privileges().is_superset(self.get_privileges()):
463 ##                 raise ChildRightsNotSubsetOfParent(self.get_subject() 
464 ##                                                    + " " + self.parent.get_privileges().save_to_string()
465 ##                                                    + " " + self.get_privileges().save_to_string())
466
467 ##         return
468
469     ##
470     # Dump the contents of a credential to stdout in human-readable format
471     #
472     # @param dump_parents If true, also dump the parent certificates
473
474     def dump(self, dump_parents=False):
475         print "CREDENTIAL", self.get_subject()
476
477         print "      privs:", self.get_privileges().save_to_string()
478
479         print "  gidCaller:"
480         gidCaller = self.get_gid_caller()
481         if gidCaller:
482             gidCaller.dump(8, dump_parents)
483
484         print "  gidObject:"
485         gidObject = self.get_gid_object()
486         if gidObject:
487             gidObject.dump(8, dump_parents)
488
489         print "   delegate:", self.get_delegate()
490
491         if self.parent and dump_parents:
492            print "PARENT",
493            self.parent.dump(dump_parents)
494