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
37 from lxml import etree
38 from StringIO import StringIO
39 from sfa.util.faults import *
40 from sfa.util.sfalogging import sfa_logger
41 from sfa.trust.certificate import Keypair
42 from sfa.trust.credential_legacy import CredentialLegacy
43 from sfa.trust.rights import Right, Rights
44 from sfa.trust.gid import GID
45 from sfa.util.xrn import urn_to_hrn
48 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
52 # . make privs match between PG and PL
53 # . Need to add support for other types of credentials, e.g. tickets
56 signature_template = \
58 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
60 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
61 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
64 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
66 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
67 <DigestValue></DigestValue>
83 # Convert a string into a bool
86 if str.lower() in ['yes','true','1']:
92 # Utility function to get the text of an XML element
94 def getTextNode(element, subele):
95 sub = element.getElementsByTagName(subele)[0]
96 if len(sub.childNodes) > 0:
97 return sub.childNodes[0].nodeValue
102 # Utility function to set the text of an XML element
103 # It creates the element, adds the text to it,
104 # and then appends it to the parent.
106 def append_sub(doc, parent, element, text):
107 ele = doc.createElement(element)
108 ele.appendChild(doc.createTextNode(text))
109 parent.appendChild(ele)
112 # Signature contains information about an xmlsec1 signature
113 # for a signed-credential
116 class Signature(object):
118 def __init__(self, string=None):
120 self.issuer_gid = None
137 def set_refid(self, id):
140 def get_issuer_gid(self):
145 def set_issuer_gid(self, gid):
149 doc = parseString(self.xml)
150 sig = doc.getElementsByTagName("Signature")[0]
151 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
152 keyinfo = sig.getElementsByTagName("X509Data")[0]
153 szgid = getTextNode(keyinfo, "X509Certificate")
154 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
155 self.set_issuer_gid(GID(string=szgid))
158 self.xml = signature_template % (self.get_refid(), self.get_refid())
162 # A credential provides a caller gid with privileges to an object gid.
163 # A signed credential is signed by the object's authority.
165 # Credentials are encoded in one of two ways. The legacy style places
166 # it in the subjectAltName of an X509 certificate. The new credentials
167 # are placed in signed XML.
170 # In general, a signed credential obtained externally should
171 # not be changed else the signature is no longer valid. So, once
172 # you have loaded an existing signed credential, do not call encode() or sign() on it.
174 def filter_creds_by_caller(creds, caller_hrn):
176 Returns a list of creds who's gid caller matches the
179 if not isinstance(creds, list): creds = [creds]
183 tmp_cred = Credential(string=cred)
184 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
185 caller_creds.append(cred)
189 class Credential(object):
192 # Create a Credential object
194 # @param create If true, create a blank x509 certificate
195 # @param subject If subject!=None, create an x509 cert with the subject name
196 # @param string If string!=None, load the credential from the string
197 # @param filename If filename!=None, load the credential from the file
198 # FIXME: create and subject are ignored!
199 def __init__(self, create=False, subject=None, string=None, filename=None):
200 self.gidCaller = None
201 self.gidObject = None
202 self.expiration = None
203 self.privileges = None
204 self.issuer_privkey = None
205 self.issuer_gid = None
206 self.issuer_pubkey = None
208 self.signature = None
213 # Check if this is a legacy credential, translate it if so
214 if string or filename:
218 str = file(filename).read()
219 self.filename=filename
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 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
263 self.set_expiration(int(lifetime))
264 self.lifeTime = legacy.get_lifetime()
265 self.set_privileges(legacy.get_privileges())
266 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
269 # Need the issuer's private key and name
270 # @param key Keypair object containing the private key of the issuer
271 # @param gid GID of the issuing authority
273 def set_issuer_keys(self, privkey, gid):
274 self.issuer_privkey = privkey
275 self.issuer_gid = gid
279 # Set this credential's parent
280 def set_parent(self, cred):
285 # set the GID of the caller
287 # @param gid GID object of the caller
289 def set_gid_caller(self, gid):
291 # gid origin caller is the caller's gid by default
292 self.gidOriginCaller = gid
295 # get the GID of the object
297 def get_gid_caller(self):
298 if not self.gidCaller:
300 return self.gidCaller
303 # set the GID of the object
305 # @param gid GID object of the object
307 def set_gid_object(self, gid):
311 # get the GID of the object
313 def get_gid_object(self):
314 if not self.gidObject:
316 return self.gidObject
321 # Expiration: an absolute UTC time of expiration (as either an int or datetime)
323 def set_expiration(self, expiration):
324 if isinstance(expiration, int):
325 self.expiration = datetime.datetime.fromtimestamp(expiration)
327 self.expiration = expiration
331 # get the lifetime of the credential (in datetime format)
333 def get_expiration(self):
334 if not self.expiration:
336 return self.expiration
340 def get_lifetime(self):
341 return self.get_expiration()
346 # @param privs either a comma-separated list of privileges of a Rights object
348 def set_privileges(self, privs):
349 if isinstance(privs, str):
350 self.privileges = Rights(string = privs)
352 self.privileges = privs
356 # return the privileges as a Rights object
358 def get_privileges(self):
359 if not self.privileges:
361 return self.privileges
364 # determine whether the credential allows a particular operation to be
367 # @param op_name string specifying name of operation ("lookup", "update", etc)
369 def can_perform(self, op_name):
370 rights = self.get_privileges()
375 return rights.can_perform(op_name)
379 # Encode the attributes of the credential into an XML string
380 # This should be done immediately before signing the credential.
382 # In general, a signed credential obtained externally should
383 # not be changed else the signature is no longer valid. So, once
384 # you have loaded an existing signed credential, do not call encode() or sign() on it.
387 # Create the XML document
389 signed_cred = doc.createElement("signed-credential")
390 doc.appendChild(signed_cred)
392 # Fill in the <credential> bit
393 cred = doc.createElement("credential")
394 cred.setAttribute("xml:id", self.get_refid())
395 signed_cred.appendChild(cred)
396 append_sub(doc, cred, "type", "privilege")
397 append_sub(doc, cred, "serial", "8")
398 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
399 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
400 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
401 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
402 append_sub(doc, cred, "uuid", "")
403 if not self.expiration:
404 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
405 self.expiration = self.expiration.replace(microsecond=0)
406 append_sub(doc, cred, "expires", self.expiration.isoformat())
407 privileges = doc.createElement("privileges")
408 cred.appendChild(privileges)
411 rights = self.get_privileges()
412 for right in rights.rights:
413 priv = doc.createElement("privilege")
414 append_sub(doc, priv, "name", right.kind)
415 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
416 privileges.appendChild(priv)
418 # Add the parent credential if it exists
420 sdoc = parseString(self.parent.get_xml())
421 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
422 p = doc.createElement("parent")
423 p.appendChild(p_cred)
427 # Create the <signatures> tag
428 signatures = doc.createElement("signatures")
429 signed_cred.appendChild(signatures)
431 # Add any parent signatures
433 for cur_cred in self.get_credential_list()[1:]:
434 sdoc = parseString(cur_cred.get_signature().get_xml())
435 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
436 signatures.appendChild(ele)
438 # Get the finished product
439 self.xml = doc.toxml()
442 def save_to_random_tmp_file(self):
443 fp, filename = mkstemp(suffix='cred', text=True)
444 fp = os.fdopen(fp, "w")
445 self.save_to_file(filename, save_parents=True, filep=fp)
448 def save_to_file(self, filename, save_parents=True, filep=None):
454 f = open(filename, "w")
457 self.filename=filename
459 def save_to_string(self, save_parents=True):
469 def set_refid(self, rid):
473 # Figure out what refids exist, and update this credential's id
474 # so that it doesn't clobber the others. Returns the refids of
477 def updateRefID(self):
479 self.set_refid('ref0')
484 next_cred = self.parent
486 refs.append(next_cred.get_refid())
488 next_cred = next_cred.parent
493 # Find a unique refid for this credential
494 rid = self.get_refid()
497 rid = "ref%d" % (val + 1)
502 # Return the set of parent credential ref ids
511 # Sign the XML file created by encode()
514 # In general, a signed credential obtained externally should
515 # not be changed else the signature is no longer valid. So, once
516 # you have loaded an existing signed credential, do not call encode() or sign() on it.
519 if not self.issuer_privkey or not self.issuer_gid:
521 doc = parseString(self.get_xml())
522 sigs = doc.getElementsByTagName("signatures")[0]
524 # Create the signature template to be signed
525 signature = Signature()
526 signature.set_refid(self.get_refid())
527 sdoc = parseString(signature.get_xml())
528 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
529 sigs.appendChild(sig_ele)
531 self.xml = doc.toxml()
534 # Split the issuer GID into multiple certificates if it's a chain
535 chain = GID(filename=self.issuer_gid)
538 gid_files.append(chain.save_to_random_tmp_file(False))
539 if chain.get_parent():
540 chain = chain.get_parent()
545 # Call out to xmlsec1 to sign it
546 ref = 'Sig_%s' % self.get_refid()
547 filename = self.save_to_random_tmp_file()
548 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
549 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
552 for gid_file in gid_files:
557 # This is no longer a legacy credential
566 # Retrieve the attributes of the credential from the XML.
567 # This is automatically called by the various get_* methods of
568 # this class and should not need to be called explicitly.
573 doc = parseString(self.xml)
575 signed_cred = doc.getElementsByTagName("signed-credential")
577 # Is this a signed-cred or just a cred?
578 if len(signed_cred) > 0:
579 cred = signed_cred[0].getElementsByTagName("credential")[0]
580 signatures = signed_cred[0].getElementsByTagName("signatures")
581 if len(signatures) > 0:
582 sigs = signatures[0].getElementsByTagName("Signature")
584 cred = doc.getElementsByTagName("credential")[0]
587 self.set_refid(cred.getAttribute("xml:id"))
588 self.set_expiration(parse(getTextNode(cred, "expires")))
589 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
590 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
594 privs = cred.getElementsByTagName("privileges")[0]
596 for priv in privs.getElementsByTagName("privilege"):
597 kind = getTextNode(priv, "name")
598 deleg = str2bool(getTextNode(priv, "can_delegate"))
600 # Convert * into the default privileges for the credential's type
601 _ , type = urn_to_hrn(self.gidObject.get_urn())
602 rl = rlist.determine_rights(type, self.gidObject.get_urn())
606 rlist.add(Right(kind.strip(), deleg))
607 self.set_privileges(rlist)
611 parent = cred.getElementsByTagName("parent")
613 parent_doc = parent[0].getElementsByTagName("credential")[0]
614 parent_xml = parent_doc.toxml()
615 self.parent = Credential(string=parent_xml)
618 # Assign the signatures to the credentials
620 Sig = Signature(string=sig.toxml())
622 for cur_cred in self.get_credential_list():
623 if cur_cred.get_refid() == Sig.get_refid():
624 cur_cred.set_signature(Sig)
629 # trusted_certs: A list of trusted GID filenames (not GID objects!)
630 # Chaining is not supported within the GIDs by xmlsec1.
633 # . All of the signatures are valid and that the issuers trace back
634 # to trusted roots (performed by xmlsec1)
635 # . The XML matches the credential schema
636 # . That the issuer of the credential is the authority in the target's urn
637 # . In the case of a delegated credential, this must be true of the root
638 # . That all of the gids presented in the credential are valid
639 # . The credential is not expired
641 # -- For Delegates (credentials with parents)
642 # . The privileges must be a subset of the parent credentials
643 # . The privileges must have "can_delegate" set for each delegated privilege
644 # . The target gid must be the same between child and parents
645 # . The expiry time on the child must be no later than the parent
646 # . The signer of the child must be the owner of the parent
648 # -- Verify does *NOT*
649 # . ensure that an xmlrpc client's gid matches a credential gid, that
650 # must be done elsewhere
652 # @param trusted_certs: The certificates of trusted CA certificates
653 # @param schema: The RelaxNG schema to validate the credential against
654 def verify(self, trusted_certs, schema=None):
658 # validate against RelaxNG schema
660 if schema and os.path.exists(schema):
661 tree = etree.parse(StringIO(self.xml))
662 schema_doc = etree.parse(schema)
663 xmlschema = etree.XMLSchema(schema_doc)
664 if not xmlschema.validate(tree):
665 error = xmlschema.error_log.last_error
666 message = "%s (line %s)" % (error.message, error.line)
667 raise CredentialNotVerifiable(message)
670 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
671 trusted_cert_objects = []
672 ok_trusted_certs = []
673 for f in trusted_certs:
675 # Failures here include unreadable files
677 trusted_cert_objects.append(GID(filename=f))
678 ok_trusted_certs.append(f)
679 except Exception, exc:
680 sfa_logger().error("Failed to load trusted cert from %s: %r"%( f, exc))
681 trusted_certs = ok_trusted_certs
683 # Use legacy verification if this is a legacy credential
685 self.legacy.verify_chain(trusted_cert_objects)
686 if self.legacy.client_gid:
687 self.legacy.client_gid.verify_chain(trusted_cert_objects)
688 if self.legacy.object_gid:
689 self.legacy.object_gid.verify_chain(trusted_cert_objects)
693 # make sure it is not expired
694 if self.get_expiration() < datetime.datetime.utcnow():
695 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
697 # Verify the signatures
698 filename = self.save_to_random_tmp_file()
699 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
701 # Verify the gids of this cred and of its parents
702 for cur_cred in self.get_credential_list():
703 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
704 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
707 refs.append("Sig_%s" % self.get_refid())
709 parentRefs = self.updateRefID()
710 for ref in parentRefs:
711 refs.append("Sig_%s" % ref)
714 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
715 % (self.xmlsec_path, ref, cert_args, filename)).read()
716 if not verified.strip().startswith("OK"):
717 raise CredentialNotVerifiable("xmlsec1 error verifying cert: " + verified)
720 # Verify the parents (delegation)
722 self.verify_parent(self.parent)
724 # Make sure the issuer is the target's authority
729 # Creates a list of the credential and its parents, with the root
730 # (original delegated credential) as the last item in the list
731 def get_credential_list(self):
735 list.append(cur_cred)
737 cur_cred = cur_cred.parent
743 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
744 # the original credential or an authority over that namespace.
745 def verify_issuer(self):
746 root_cred = self.get_credential_list()[-1]
747 root_target_gid = root_cred.get_gid_object()
748 root_cred_signer = root_cred.get_signature().get_issuer_gid()
750 if root_target_gid.is_signed_by_cert(root_cred_signer):
751 # cred signer matches target signer, return success
754 root_target_gid_str = root_target_gid.save_to_string()
755 root_cred_signer_str = root_cred_signer.save_to_string()
756 if root_target_gid_str == root_cred_signer_str:
757 # cred signer is target, return success
760 # See if it the signer is an authority over the domain of the target
761 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
762 root_cred_signer_type = root_cred_signer.get_type()
763 if (root_cred_signer_type == 'authority'):
764 #sfa_logger().debug('Cred signer is an authority')
765 # signer is an authority, see if target is in authority's domain
766 hrn = root_cred_signer.get_hrn()
767 if root_target_gid.get_hrn().startswith(hrn):
770 # We've required that the credential be signed by an authority
771 # for that domain. Reasonable and probably correct.
772 # A looser model would also allow the signer to be an authority
773 # in my control framework - eg My CA or CH. Even if it is not
774 # the CH that issued these, eg, user credentials.
776 # Give up, credential does not pass issuer verification
778 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()))
782 # -- For Delegates (credentials with parents) verify that:
783 # . The privileges must be a subset of the parent credentials
784 # . The privileges must have "can_delegate" set for each delegated privilege
785 # . The target gid must be the same between child and parents
786 # . The expiry time on the child must be no later than the parent
787 # . The signer of the child must be the owner of the parent
788 def verify_parent(self, parent_cred):
789 # make sure the rights given to the child are a subset of the
790 # parents rights (and check delegate bits)
791 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
792 raise ChildRightsNotSubsetOfParent(
793 self.parent.get_privileges().save_to_string() + " " +
794 self.get_privileges().save_to_string())
796 # make sure my target gid is the same as the parent's
797 if not parent_cred.get_gid_object().save_to_string() == \
798 self.get_gid_object().save_to_string():
799 raise CredentialNotVerifiable("Target gid not equal between parent and child")
801 # make sure my expiry time is <= my parent's
802 if not parent_cred.get_expiration() >= self.get_expiration():
803 raise CredentialNotVerifiable("Delegated credential expires after parent")
805 # make sure my signer is the parent's caller
806 if not parent_cred.get_gid_caller().save_to_string(False) == \
807 self.get_signature().get_issuer_gid().save_to_string(False):
808 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
811 if parent_cred.parent:
812 parent_cred.verify_parent(parent_cred.parent)
815 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
817 Return a delegated copy of this credential, delegated to the
818 specified gid's user.
820 # get the gid of the object we are delegating
821 object_gid = self.get_gid_object()
822 object_hrn = object_gid.get_hrn()
824 # the hrn of the user who will be delegated to
825 delegee_gid = GID(filename=delegee_gidfile)
826 delegee_hrn = delegee_gid.get_hrn()
828 #user_key = Keypair(filename=keyfile)
829 #user_hrn = self.get_gid_caller().get_hrn()
830 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
831 dcred = Credential(subject=subject_string)
832 dcred.set_gid_caller(delegee_gid)
833 dcred.set_gid_object(object_gid)
834 dcred.set_parent(self)
835 dcred.set_expiration(self.get_expiration())
836 dcred.set_privileges(self.get_privileges())
837 dcred.get_privileges().delegate_all_privileges(True)
838 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
839 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
846 def get_filename(self):
847 return getattr(self,'filename',None)
849 # @param dump_parents If true, also dump the parent certificates
850 def dump (self, *args, **kwargs):
851 print self.dump_string(*args, **kwargs)
853 def dump_string(self, dump_parents=False):
855 result += "CREDENTIAL %s\n" % self.get_subject()
856 filename=self.get_filename()
857 if filename: result += "Filename %s\n"%filename
858 result += " privs: %s\n" % self.get_privileges().save_to_string()
859 gidCaller = self.get_gid_caller()
861 result += " gidCaller:\n"
862 result += gidCaller.dump_string(8, dump_parents)
864 gidObject = self.get_gid_object()
866 result += " gidObject:\n"
867 result += gidObject.dump_string(8, dump_parents)
869 if self.parent and dump_parents:
871 result += self.parent.dump_string(dump_parents)