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
31 from tempfile import mkstemp
32 import dateutil.parser
33 from StringIO import StringIO
34 from xml.dom.minidom import Document, parseString
35 from lxml import etree
37 from sfa.util.faults import *
38 from sfa.util.sfalogging import logger
39 from sfa.trust.certificate import Keypair
40 from sfa.trust.credential_legacy import CredentialLegacy
41 from sfa.trust.rights import Right, Rights
42 from sfa.trust.gid import GID
43 from sfa.util.xrn import urn_to_hrn
46 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
50 # . make privs match between PG and PL
51 # . Need to add support for other types of credentials, e.g. tickets
54 signature_template = \
56 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
58 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
59 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
62 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
64 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
65 <DigestValue></DigestValue>
81 # Convert a string into a bool
84 if str.lower() in ['true','1']:
90 # Utility function to get the text of an XML element
92 def getTextNode(element, subele):
93 sub = element.getElementsByTagName(subele)[0]
94 if len(sub.childNodes) > 0:
95 return sub.childNodes[0].nodeValue
100 # Utility function to set the text of an XML element
101 # It creates the element, adds the text to it,
102 # and then appends it to the parent.
104 def append_sub(doc, parent, element, text):
105 ele = doc.createElement(element)
106 ele.appendChild(doc.createTextNode(text))
107 parent.appendChild(ele)
110 # Signature contains information about an xmlsec1 signature
111 # for a signed-credential
114 class Signature(object):
116 def __init__(self, string=None):
118 self.issuer_gid = None
135 def set_refid(self, id):
138 def get_issuer_gid(self):
143 def set_issuer_gid(self, gid):
147 doc = parseString(self.xml)
148 sig = doc.getElementsByTagName("Signature")[0]
149 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
150 keyinfo = sig.getElementsByTagName("X509Data")[0]
151 szgid = getTextNode(keyinfo, "X509Certificate")
152 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
153 self.set_issuer_gid(GID(string=szgid))
156 self.xml = signature_template % (self.get_refid(), self.get_refid())
160 # A credential provides a caller gid with privileges to an object gid.
161 # A signed credential is signed by the object's authority.
163 # Credentials are encoded in one of two ways. The legacy style places
164 # it in the subjectAltName of an X509 certificate. The new credentials
165 # are placed in signed XML.
168 # In general, a signed credential obtained externally should
169 # not be changed else the signature is no longer valid. So, once
170 # you have loaded an existing signed credential, do not call encode() or sign() on it.
172 def filter_creds_by_caller(creds, caller_hrn):
174 Returns a list of creds who's gid caller matches the
177 if not isinstance(creds, list): creds = [creds]
181 tmp_cred = Credential(string=cred)
182 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
183 caller_creds.append(cred)
187 class Credential(object):
190 # Create a Credential object
192 # @param create If true, create a blank x509 certificate
193 # @param subject If subject!=None, create an x509 cert with the subject name
194 # @param string If string!=None, load the credential from the string
195 # @param filename If filename!=None, load the credential from the file
196 # FIXME: create and subject are ignored!
197 def __init__(self, create=False, subject=None, string=None, filename=None):
198 self.gidCaller = None
199 self.gidObject = None
200 self.expiration = None
201 self.privileges = None
202 self.issuer_privkey = None
203 self.issuer_gid = None
204 self.issuer_pubkey = None
206 self.signature = None
211 # Check if this is a legacy credential, translate it if so
212 if string or filename:
216 str = file(filename).read()
217 self.filename=filename
219 if str.strip().startswith("-----"):
220 self.legacy = CredentialLegacy(False,string=str)
221 self.translate_legacy(str)
226 # Find an xmlsec1 path
227 self.xmlsec_path = ''
228 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
230 if os.path.isfile(path + '/' + 'xmlsec1'):
231 self.xmlsec_path = path + '/' + 'xmlsec1'
234 def get_subject(self):
235 if not self.gidObject:
237 return self.gidObject.get_subject()
239 def get_signature(self):
240 if not self.signature:
242 return self.signature
244 def set_signature(self, sig):
249 # Translate a legacy credential into a new one
251 # @param String of the legacy credential
253 def translate_legacy(self, str):
254 legacy = CredentialLegacy(False,string=str)
255 self.gidCaller = legacy.get_gid_caller()
256 self.gidObject = legacy.get_gid_object()
257 lifetime = legacy.get_lifetime()
259 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
261 self.set_expiration(int(lifetime))
262 self.lifeTime = legacy.get_lifetime()
263 self.set_privileges(legacy.get_privileges())
264 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
267 # Need the issuer's private key and name
268 # @param key Keypair object containing the private key of the issuer
269 # @param gid GID of the issuing authority
271 def set_issuer_keys(self, privkey, gid):
272 self.issuer_privkey = privkey
273 self.issuer_gid = gid
277 # Set this credential's parent
278 def set_parent(self, cred):
283 # set the GID of the caller
285 # @param gid GID object of the caller
287 def set_gid_caller(self, gid):
289 # gid origin caller is the caller's gid by default
290 self.gidOriginCaller = gid
293 # get the GID of the object
295 def get_gid_caller(self):
296 if not self.gidCaller:
298 return self.gidCaller
301 # set the GID of the object
303 # @param gid GID object of the object
305 def set_gid_object(self, gid):
309 # get the GID of the object
311 def get_gid_object(self):
312 if not self.gidObject:
314 return self.gidObject
319 # Expiration: an absolute UTC time of expiration (as either an int or datetime)
321 def set_expiration(self, expiration):
322 if isinstance(expiration, int):
323 self.expiration = datetime.datetime.fromtimestamp(expiration)
325 self.expiration = expiration
329 # get the lifetime of the credential (in datetime format)
331 def get_expiration(self):
332 if not self.expiration:
334 return self.expiration
338 def get_lifetime(self):
339 return self.get_expiration()
344 # @param privs either a comma-separated list of privileges of a Rights object
346 def set_privileges(self, privs):
347 if isinstance(privs, str):
348 self.privileges = Rights(string = privs)
350 self.privileges = privs
354 # return the privileges as a Rights object
356 def get_privileges(self):
357 if not self.privileges:
359 return self.privileges
362 # determine whether the credential allows a particular operation to be
365 # @param op_name string specifying name of operation ("lookup", "update", etc)
367 def can_perform(self, op_name):
368 rights = self.get_privileges()
373 return rights.can_perform(op_name)
377 # Encode the attributes of the credential into an XML string
378 # This should be done immediately before signing the credential.
380 # In general, a signed credential obtained externally should
381 # not be changed else the signature is no longer valid. So, once
382 # you have loaded an existing signed credential, do not call encode() or sign() on it.
385 # Create the XML document
387 signed_cred = doc.createElement("signed-credential")
388 doc.appendChild(signed_cred)
390 # Fill in the <credential> bit
391 cred = doc.createElement("credential")
392 cred.setAttribute("xml:id", self.get_refid())
393 signed_cred.appendChild(cred)
394 append_sub(doc, cred, "type", "privilege")
395 append_sub(doc, cred, "serial", "8")
396 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
397 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
398 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
399 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
400 append_sub(doc, cred, "uuid", "")
401 if not self.expiration:
402 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
403 self.expiration = self.expiration.replace(microsecond=0)
404 append_sub(doc, cred, "expires", self.expiration.isoformat())
405 privileges = doc.createElement("privileges")
406 cred.appendChild(privileges)
409 rights = self.get_privileges()
410 for right in rights.rights:
411 priv = doc.createElement("privilege")
412 append_sub(doc, priv, "name", right.kind)
413 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
414 privileges.appendChild(priv)
416 # Add the parent credential if it exists
418 sdoc = parseString(self.parent.get_xml())
419 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
420 p = doc.createElement("parent")
421 p.appendChild(p_cred)
425 # Create the <signatures> tag
426 signatures = doc.createElement("signatures")
427 signed_cred.appendChild(signatures)
429 # Add any parent signatures
431 for cur_cred in self.get_credential_list()[1:]:
432 sdoc = parseString(cur_cred.get_signature().get_xml())
433 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
434 signatures.appendChild(ele)
436 # Get the finished product
437 self.xml = doc.toxml()
440 def save_to_random_tmp_file(self):
441 fp, filename = mkstemp(suffix='cred', text=True)
442 fp = os.fdopen(fp, "w")
443 self.save_to_file(filename, save_parents=True, filep=fp)
446 def save_to_file(self, filename, save_parents=True, filep=None):
452 f = open(filename, "w")
455 self.filename=filename
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_expiration(dateutil.parser.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 # @param schema: The RelaxNG schema to validate the credential against
652 def verify(self, trusted_certs, schema=None):
656 # validate against RelaxNG schema
658 if schema and os.path.exists(schema):
659 tree = etree.parse(StringIO(self.xml))
660 schema_doc = etree.parse(schema)
661 xmlschema = etree.XMLSchema(schema_doc)
662 if not xmlschema.validate(tree):
663 error = xmlschema.error_log.last_error
664 message = "%s (line %s)" % (error.message, error.line)
665 raise CredentialNotVerifiable(message)
668 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
669 trusted_cert_objects = []
670 ok_trusted_certs = []
671 for f in trusted_certs:
673 # Failures here include unreadable files
675 trusted_cert_objects.append(GID(filename=f))
676 ok_trusted_certs.append(f)
677 except Exception, exc:
678 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
679 trusted_certs = ok_trusted_certs
681 # Use legacy verification if this is a legacy credential
683 self.legacy.verify_chain(trusted_cert_objects)
684 if self.legacy.client_gid:
685 self.legacy.client_gid.verify_chain(trusted_cert_objects)
686 if self.legacy.object_gid:
687 self.legacy.object_gid.verify_chain(trusted_cert_objects)
691 # make sure it is not expired
692 if self.get_expiration() < datetime.datetime.utcnow():
693 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
695 # Verify the signatures
696 filename = self.save_to_random_tmp_file()
697 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
699 # Verify the gids of this cred and of its parents
700 for cur_cred in self.get_credential_list():
701 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
702 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
705 refs.append("Sig_%s" % self.get_refid())
707 parentRefs = self.updateRefID()
708 for ref in parentRefs:
709 refs.append("Sig_%s" % ref)
712 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
713 % (self.xmlsec_path, ref, cert_args, filename)).read()
714 if not verified.strip().startswith("OK"):
715 raise CredentialNotVerifiable("xmlsec1 error verifying cert: " + verified)
718 # Verify the parents (delegation)
720 self.verify_parent(self.parent)
722 # Make sure the issuer is the target's authority
727 # Creates a list of the credential and its parents, with the root
728 # (original delegated credential) as the last item in the list
729 def get_credential_list(self):
733 list.append(cur_cred)
735 cur_cred = cur_cred.parent
741 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
742 # the original credential or an authority over that namespace.
743 def verify_issuer(self):
744 root_cred = self.get_credential_list()[-1]
745 root_target_gid = root_cred.get_gid_object()
746 root_cred_signer = root_cred.get_signature().get_issuer_gid()
748 if root_target_gid.is_signed_by_cert(root_cred_signer):
749 # cred signer matches target signer, return success
752 root_target_gid_str = root_target_gid.save_to_string()
753 root_cred_signer_str = root_cred_signer.save_to_string()
754 if root_target_gid_str == root_cred_signer_str:
755 # cred signer is target, return success
758 # See if it the signer is an authority over the domain of the target
759 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
760 root_cred_signer_type = root_cred_signer.get_type()
761 if (root_cred_signer_type == 'authority'):
762 #logger.debug('Cred signer is an authority')
763 # signer is an authority, see if target is in authority's domain
764 hrn = root_cred_signer.get_hrn()
765 if root_target_gid.get_hrn().startswith(hrn):
768 # We've required that the credential be signed by an authority
769 # for that domain. Reasonable and probably correct.
770 # A looser model would also allow the signer to be an authority
771 # in my control framework - eg My CA or CH. Even if it is not
772 # the CH that issued these, eg, user credentials.
774 # Give up, credential does not pass issuer verification
776 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()))
780 # -- For Delegates (credentials with parents) verify that:
781 # . The privileges must be a subset of the parent credentials
782 # . The privileges must have "can_delegate" set for each delegated privilege
783 # . The target gid must be the same between child and parents
784 # . The expiry time on the child must be no later than the parent
785 # . The signer of the child must be the owner of the parent
786 def verify_parent(self, parent_cred):
787 # make sure the rights given to the child are a subset of the
788 # parents rights (and check delegate bits)
789 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
790 raise ChildRightsNotSubsetOfParent(
791 self.parent.get_privileges().save_to_string() + " " +
792 self.get_privileges().save_to_string())
794 # make sure my target gid is the same as the parent's
795 if not parent_cred.get_gid_object().save_to_string() == \
796 self.get_gid_object().save_to_string():
797 raise CredentialNotVerifiable("Target gid not equal between parent and child")
799 # make sure my expiry time is <= my parent's
800 if not parent_cred.get_expiration() >= self.get_expiration():
801 raise CredentialNotVerifiable("Delegated credential expires after parent")
803 # make sure my signer is the parent's caller
804 if not parent_cred.get_gid_caller().save_to_string(False) == \
805 self.get_signature().get_issuer_gid().save_to_string(False):
806 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
809 if parent_cred.parent:
810 parent_cred.verify_parent(parent_cred.parent)
813 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
815 Return a delegated copy of this credential, delegated to the
816 specified gid's user.
818 # get the gid of the object we are delegating
819 object_gid = self.get_gid_object()
820 object_hrn = object_gid.get_hrn()
822 # the hrn of the user who will be delegated to
823 delegee_gid = GID(filename=delegee_gidfile)
824 delegee_hrn = delegee_gid.get_hrn()
826 #user_key = Keypair(filename=keyfile)
827 #user_hrn = self.get_gid_caller().get_hrn()
828 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
829 dcred = Credential(subject=subject_string)
830 dcred.set_gid_caller(delegee_gid)
831 dcred.set_gid_object(object_gid)
832 dcred.set_parent(self)
833 dcred.set_expiration(self.get_expiration())
834 dcred.set_privileges(self.get_privileges())
835 dcred.get_privileges().delegate_all_privileges(True)
836 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
837 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
844 def get_filename(self):
845 return getattr(self,'filename',None)
847 # @param dump_parents If true, also dump the parent certificates
848 def dump (self, *args, **kwargs):
849 print self.dump_string(*args, **kwargs)
851 def dump_string(self, dump_parents=False):
853 result += "CREDENTIAL %s\n" % self.get_subject()
854 filename=self.get_filename()
855 if filename: result += "Filename %s\n"%filename
856 result += " privs: %s\n" % self.get_privileges().save_to_string()
857 gidCaller = self.get_gid_caller()
859 result += " gidCaller:\n"
860 result += gidCaller.dump_string(8, dump_parents)
862 if self.get_signature():
864 self.get_signature().get_issuer_gid().dump(8, dump_parents)
866 gidObject = self.get_gid_object()
868 result += " gidObject:\n"
869 result += gidObject.dump_string(8, dump_parents)
871 if self.parent and dump_parents:
873 result += self.parent.dump(True)