1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
22 #----------------------------------------------------------------------
24 # Implements SFA Credentials
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
36 from dateutil.parser import parse
38 import sfa.util.sfalogging
39 from sfa.trust.certificate import Keypair
40 from sfa.trust.credential_legacy import CredentialLegacy
41 from sfa.trust.rights import *
42 from sfa.trust.gid import *
43 from sfa.util.faults import *
48 # Two years, in seconds
49 DEFAULT_CREDENTIAL_LIFETIME = 60 * 60 * 24 * 365 * 2
53 # . make privs match between PG and PL
54 # . Need to add support for other types of credentials, e.g. tickets
57 signature_template = \
59 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
61 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
62 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
65 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
67 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
68 <DigestValue></DigestValue>
84 # Convert a string into a bool
87 if str.lower() in ['yes','true','1']:
93 # Utility function to get the text of an XML element
95 def getTextNode(element, subele):
96 sub = element.getElementsByTagName(subele)[0]
97 if len(sub.childNodes) > 0:
98 return sub.childNodes[0].nodeValue
103 # Utility function to set the text of an XML element
104 # It creates the element, adds the text to it,
105 # and then appends it to the parent.
107 def append_sub(doc, parent, element, text):
108 ele = doc.createElement(element)
109 ele.appendChild(doc.createTextNode(text))
110 parent.appendChild(ele)
113 # Signature contains information about an xmlsec1 signature
114 # for a signed-credential
117 class Signature(object):
119 def __init__(self, string=None):
121 self.issuer_gid = None
138 def set_refid(self, id):
141 def get_issuer_gid(self):
146 def set_issuer_gid(self, gid):
150 doc = parseString(self.xml)
151 sig = doc.getElementsByTagName("Signature")[0]
152 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
153 keyinfo = sig.getElementsByTagName("X509Data")[0]
154 szgid = getTextNode(keyinfo, "X509Certificate")
155 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
156 self.set_issuer_gid(GID(string=szgid))
159 self.xml = signature_template % (self.get_refid(), self.get_refid())
163 # A credential provides a caller gid with privileges to an object gid.
164 # A signed credential is signed by the object's authority.
166 # Credentials are encoded in one of two ways. The legacy style places
167 # it in the subjectAltName of an X509 certificate. The new credentials
168 # are placed in signed XML.
171 # In general, a signed credential obtained externally should
172 # not be changed else the signature is no longer valid. So, once
173 # you have loaded an existing signed credential, do not call encode() or sign() on it.
175 def filter_creds_by_caller(creds, caller_hrn):
177 Returns a list of creds who's gid caller matches the
180 if not isinstance(creds, list): creds = [creds]
184 tmp_cred = Credential(string=cred)
185 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
186 caller_creds.append(cred)
190 class Credential(object):
193 # Create a Credential object
195 # @param create If true, create a blank x509 certificate
196 # @param subject If subject!=None, create an x509 cert with the subject name
197 # @param string If string!=None, load the credential from the string
198 # @param filename If filename!=None, load the credential from the file
199 # FIXME: create and subject are ignored!
200 def __init__(self, create=False, subject=None, string=None, filename=None):
201 self.gidCaller = None
202 self.gidObject = None
203 self.expiration = None
204 self.privileges = None
205 self.issuer_privkey = None
206 self.issuer_gid = None
207 self.issuer_pubkey = None
209 self.signature = None
214 # Check if this is a legacy credential, translate it if so
215 if string or filename:
219 str = file(filename).read()
221 if str.strip().startswith("-----"):
222 self.legacy = CredentialLegacy(False,string=str)
223 self.translate_legacy(str)
228 # Find an xmlsec1 path
229 self.xmlsec_path = ''
230 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
232 if os.path.isfile(path + '/' + 'xmlsec1'):
233 self.xmlsec_path = path + '/' + 'xmlsec1'
236 def get_subject(self):
237 if not self.gidObject:
239 return self.gidObject.get_subject()
241 def get_signature(self):
242 if not self.signature:
244 return self.signature
246 def set_signature(self, sig):
251 # Translate a legacy credential into a new one
253 # @param String of the legacy credential
255 def translate_legacy(self, str):
256 legacy = CredentialLegacy(False,string=str)
257 self.gidCaller = legacy.get_gid_caller()
258 self.gidObject = legacy.get_gid_object()
259 lifetime = legacy.get_lifetime()
261 # Default to two years
262 self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
264 self.set_lifetime(int(lifetime))
265 self.lifeTime = legacy.get_lifetime()
266 self.set_privileges(legacy.get_privileges())
267 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
270 # Need the issuer's private key and name
271 # @param key Keypair object containing the private key of the issuer
272 # @param gid GID of the issuing authority
274 def set_issuer_keys(self, privkey, gid):
275 self.issuer_privkey = privkey
276 self.issuer_gid = gid
280 # Set this credential's parent
281 def set_parent(self, cred):
286 # set the GID of the caller
288 # @param gid GID object of the caller
290 def set_gid_caller(self, gid):
292 # gid origin caller is the caller's gid by default
293 self.gidOriginCaller = gid
296 # get the GID of the object
298 def get_gid_caller(self):
299 if not self.gidCaller:
301 return self.gidCaller
304 # set the GID of the object
306 # @param gid GID object of the object
308 def set_gid_object(self, gid):
312 # get the GID of the object
314 def get_gid_object(self):
315 if not self.gidObject:
317 return self.gidObject
320 # set the lifetime of this credential
322 # @param lifetime lifetime of credential
323 # . if lifeTime is a datetime object, it is used for the expiration time
324 # . if lifeTime is an integer value, it is considered the number of seconds
325 # remaining before expiration
327 def set_lifetime(self, lifeTime):
328 if isinstance(lifeTime, int):
329 self.expiration = datetime.timedelta(seconds=lifeTime) + datetime.datetime.utcnow()
331 self.expiration = lifeTime
334 # get the lifetime of the credential (in datetime format)
336 def get_lifetime(self):
337 if not self.expiration:
339 return self.expiration
345 # @param privs either a comma-separated list of privileges of a RightList object
347 def set_privileges(self, privs):
348 if isinstance(privs, str):
349 self.privileges = RightList(string = privs)
351 self.privileges = privs
355 # return the privileges as a RightList object
357 def get_privileges(self):
358 if not self.privileges:
360 return self.privileges
363 # determine whether the credential allows a particular operation to be
366 # @param op_name string specifying name of operation ("lookup", "update", etc)
368 def can_perform(self, op_name):
369 rights = self.get_privileges()
374 return rights.can_perform(op_name)
378 # Encode the attributes of the credential into an XML string
379 # This should be done immediately before signing the credential.
381 # In general, a signed credential obtained externally should
382 # not be changed else the signature is no longer valid. So, once
383 # you have loaded an existing signed credential, do not call encode() or sign() on it.
386 # Create the XML document
388 signed_cred = doc.createElement("signed-credential")
389 doc.appendChild(signed_cred)
391 # Fill in the <credential> bit
392 cred = doc.createElement("credential")
393 cred.setAttribute("xml:id", self.get_refid())
394 signed_cred.appendChild(cred)
395 append_sub(doc, cred, "type", "privilege")
396 append_sub(doc, cred, "serial", "8")
397 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
398 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
399 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
400 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
401 append_sub(doc, cred, "uuid", "")
402 if not self.expiration:
403 self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
404 self.expiration = self.expiration.replace(microsecond=0)
405 append_sub(doc, cred, "expires", self.expiration.isoformat())
406 privileges = doc.createElement("privileges")
407 cred.appendChild(privileges)
410 rights = self.get_privileges()
411 for right in rights.rights:
412 priv = doc.createElement("privilege")
413 append_sub(doc, priv, "name", right.kind)
414 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
415 privileges.appendChild(priv)
417 # Add the parent credential if it exists
419 sdoc = parseString(self.parent.get_xml())
420 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
421 p = doc.createElement("parent")
422 p.appendChild(p_cred)
426 # Create the <signatures> tag
427 signatures = doc.createElement("signatures")
428 signed_cred.appendChild(signatures)
430 # Add any parent signatures
432 for cur_cred in self.get_credential_list()[1:]:
433 sdoc = parseString(cur_cred.get_signature().get_xml())
434 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
435 signatures.appendChild(ele)
437 # Get the finished product
438 self.xml = doc.toxml()
441 def save_to_random_tmp_file(self):
442 fp, filename = mkstemp(suffix='cred', text=True)
443 fp = os.fdopen(fp, "w")
444 self.save_to_file(filename, save_parents=True, filep=fp)
447 def save_to_file(self, filename, save_parents=True, filep=None):
453 f = open(filename, "w")
457 def save_to_string(self, save_parents=True):
467 def set_refid(self, rid):
471 # Figure out what refids exist, and update this credential's id
472 # so that it doesn't clobber the others. Returns the refids of
475 def updateRefID(self):
477 self.set_refid('ref0')
482 next_cred = self.parent
484 refs.append(next_cred.get_refid())
486 next_cred = next_cred.parent
491 # Find a unique refid for this credential
492 rid = self.get_refid()
495 rid = "ref%d" % (val + 1)
500 # Return the set of parent credential ref ids
509 # Sign the XML file created by encode()
512 # In general, a signed credential obtained externally should
513 # not be changed else the signature is no longer valid. So, once
514 # you have loaded an existing signed credential, do not call encode() or sign() on it.
517 if not self.issuer_privkey or not self.issuer_gid:
519 doc = parseString(self.get_xml())
520 sigs = doc.getElementsByTagName("signatures")[0]
522 # Create the signature template to be signed
523 signature = Signature()
524 signature.set_refid(self.get_refid())
525 sdoc = parseString(signature.get_xml())
526 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
527 sigs.appendChild(sig_ele)
529 self.xml = doc.toxml()
532 # Split the issuer GID into multiple certificates if it's a chain
533 chain = GID(filename=self.issuer_gid)
536 gid_files.append(chain.save_to_random_tmp_file(False))
537 if chain.get_parent():
538 chain = chain.get_parent()
543 # Call out to xmlsec1 to sign it
544 ref = 'Sig_%s' % self.get_refid()
545 filename = self.save_to_random_tmp_file()
546 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
547 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
550 for gid_file in gid_files:
555 # This is no longer a legacy credential
564 # Retrieve the attributes of the credential from the XML.
565 # This is automatically called by the various get_* methods of
566 # this class and should not need to be called explicitly.
571 doc = parseString(self.xml)
573 signed_cred = doc.getElementsByTagName("signed-credential")
575 # Is this a signed-cred or just a cred?
576 if len(signed_cred) > 0:
577 cred = signed_cred[0].getElementsByTagName("credential")[0]
578 signatures = signed_cred[0].getElementsByTagName("signatures")
579 if len(signatures) > 0:
580 sigs = signatures[0].getElementsByTagName("Signature")
582 cred = doc.getElementsByTagName("credential")[0]
585 self.set_refid(cred.getAttribute("xml:id"))
586 self.set_lifetime(parse(getTextNode(cred, "expires")))
587 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
588 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
592 privs = cred.getElementsByTagName("privileges")[0]
594 for priv in privs.getElementsByTagName("privilege"):
595 kind = getTextNode(priv, "name")
596 deleg = str2bool(getTextNode(priv, "can_delegate"))
598 # Convert * into the default privileges for the credential's type
599 _ , type = urn_to_hrn(self.gidObject.get_urn())
600 rl = rlist.determine_rights(type, self.gidObject.get_urn())
604 rlist.add(Right(kind.strip(), deleg))
605 self.set_privileges(rlist)
609 parent = cred.getElementsByTagName("parent")
611 parent_doc = parent[0].getElementsByTagName("credential")[0]
612 parent_xml = parent_doc.toxml()
613 self.parent = Credential(string=parent_xml)
616 # Assign the signatures to the credentials
618 Sig = Signature(string=sig.toxml())
620 for cur_cred in self.get_credential_list():
621 if cur_cred.get_refid() == Sig.get_refid():
622 cur_cred.set_signature(Sig)
627 # trusted_certs: A list of trusted GID filenames (not GID objects!)
628 # Chaining is not supported within the GIDs by xmlsec1.
631 # . All of the signatures are valid and that the issuers trace back
632 # to trusted roots (performed by xmlsec1)
633 # . The XML matches the credential schema
634 # . That the issuer of the credential is the authority in the target's urn
635 # . In the case of a delegated credential, this must be true of the root
636 # . That all of the gids presented in the credential are valid
637 # . The credential is not expired
639 # -- For Delegates (credentials with parents)
640 # . The privileges must be a subset of the parent credentials
641 # . The privileges must have "can_delegate" set for each delegated privilege
642 # . The target gid must be the same between child and parents
643 # . The expiry time on the child must be no later than the parent
644 # . The signer of the child must be the owner of the parent
646 # -- Verify does *NOT*
647 # . ensure that an xmlrpc client's gid matches a credential gid, that
648 # must be done elsewhere
650 # @param trusted_certs: The certificates of trusted CA certificates
651 def verify(self, trusted_certs):
655 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
656 trusted_cert_objects = []
657 ok_trusted_certs = []
658 for f in trusted_certs:
660 # Failures here include unreadable files
662 trusted_cert_objects.append(GID(filename=f))
663 ok_trusted_certs.append(f)
664 except Exception, exc:
665 sfa.util.sfalogging.logger.error("Failed to load trusted cert from %s: %r", f, exc)
666 trusted_certs = ok_trusted_certs
668 # Use legacy verification if this is a legacy credential
670 self.legacy.verify_chain(trusted_cert_objects)
671 if self.legacy.client_gid:
672 self.legacy.client_gid.verify_chain(trusted_cert_objects)
673 if self.legacy.object_gid:
674 self.legacy.object_gid.verify_chain(trusted_cert_objects)
677 # make sure it is not expired
678 if self.get_lifetime() < datetime.datetime.utcnow():
679 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
681 # Verify the signatures
682 filename = self.save_to_random_tmp_file()
683 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
685 # Verify the gids of this cred and of its parents
686 for cur_cred in self.get_credential_list():
687 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
688 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
691 refs.append("Sig_%s" % self.get_refid())
693 parentRefs = self.updateRefID()
694 for ref in parentRefs:
695 refs.append("Sig_%s" % ref)
698 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
699 % (self.xmlsec_path, ref, cert_args, filename)).read()
700 if not verified.strip().startswith("OK"):
701 raise CredentialNotVerifiable("xmlsec1 error verifying cert: " + verified)
704 # Verify the parents (delegation)
706 self.verify_parent(self.parent)
708 # Make sure the issuer is the target's authority
713 # Creates a list of the credential and its parents, with the root
714 # (original delegated credential) as the last item in the list
715 def get_credential_list(self):
719 list.append(cur_cred)
721 cur_cred = cur_cred.parent
727 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
728 # the original credential or an authority over that namespace.
729 def verify_issuer(self):
730 root_cred = self.get_credential_list()[-1]
731 root_target_gid = root_cred.get_gid_object()
732 root_cred_signer = root_cred.get_signature().get_issuer_gid()
734 if root_target_gid.is_signed_by_cert(root_cred_signer):
735 # cred signer matches target signer, return success
738 root_target_gid_str = root_target_gid.save_to_string()
739 root_cred_signer_str = root_cred_signer.save_to_string()
740 if root_target_gid_str == root_cred_signer_str:
741 # cred signer is target, return success
744 # See if it the signer is an authority over the domain of the target
745 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
746 root_cred_signer_type = root_cred_signer.get_type()
747 if (root_cred_signer_type == 'authority'):
748 #sfa.util.sfalogging.logger.debug('Cred signer is an authority')
749 # signer is an authority, see if target is in authority's domain
750 hrn = root_cred_signer.get_hrn()
751 if root_target_gid.get_hrn().startswith(hrn):
754 # We've required that the credential be signed by an authority
755 # for that domain. Reasonable and probably correct.
756 # A looser model would also allow the signer to be an authority
757 # in my control framework - eg My CA or CH. Even if it is not
758 # the CH that issued these, eg, user credentials.
760 # Give up, credential does not pass issuer verification
762 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
766 # -- For Delegates (credentials with parents) verify that:
767 # . The privileges must be a subset of the parent credentials
768 # . The privileges must have "can_delegate" set for each delegated privilege
769 # . The target gid must be the same between child and parents
770 # . The expiry time on the child must be no later than the parent
771 # . The signer of the child must be the owner of the parent
772 def verify_parent(self, parent_cred):
773 # make sure the rights given to the child are a subset of the
774 # parents rights (and check delegate bits)
775 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
776 raise ChildRightsNotSubsetOfParent(
777 self.parent.get_privileges().save_to_string() + " " +
778 self.get_privileges().save_to_string())
780 # make sure my target gid is the same as the parent's
781 if not parent_cred.get_gid_object().save_to_string() == \
782 self.get_gid_object().save_to_string():
783 raise CredentialNotVerifiable("Target gid not equal between parent and child")
785 # make sure my expiry time is <= my parent's
786 if not parent_cred.get_lifetime() >= self.get_lifetime():
787 raise CredentialNotVerifiable("Delegated credential expires after parent")
789 # make sure my signer is the parent's caller
790 if not parent_cred.get_gid_caller().save_to_string(False) == \
791 self.get_signature().get_issuer_gid().save_to_string(False):
792 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
795 if parent_cred.parent:
796 parent_cred.verify_parent(parent_cred.parent)
799 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
801 Return a delegated copy of this credential, delegated to the
802 specified gid's user.
804 # get the gid of the object we are delegating
805 object_gid = self.get_gid_object()
806 object_hrn = object_gid.get_hrn()
808 # the hrn of the user who will be delegated to
809 delegee_gid = GID(filename=delegee_gidfile)
810 delegee_hrn = delegee_gid.get_hrn()
812 #user_key = Keypair(filename=keyfile)
813 #user_hrn = self.get_gid_caller().get_hrn()
814 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
815 dcred = Credential(subject=subject_string)
816 dcred.set_gid_caller(delegee_gid)
817 dcred.set_gid_object(object_gid)
818 dcred.set_parent(self)
819 dcred.set_lifetime(self.get_lifetime())
820 dcred.set_privileges(self.get_privileges())
821 dcred.get_privileges().delegate_all_privileges(True)
822 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
823 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
829 # Dump the contents of a credential to stdout in human-readable format
831 # @param dump_parents If true, also dump the parent certificates
833 def dump(self, dump_parents=False):
834 print "CREDENTIAL", self.get_subject()
836 print " privs:", self.get_privileges().save_to_string()
839 gidCaller = self.get_gid_caller()
841 gidCaller.dump(8, dump_parents)
844 gidObject = self.get_gid_object()
846 gidObject.dump(8, dump_parents)
849 if self.parent and dump_parents:
851 self.parent.dump_parents()