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
27 # privileges to an object gid
30 from __future__ import print_function
36 from tempfile import mkstemp
37 from xml.dom.minidom import Document, parseString
39 from sfa.util.py23 import PY3, StringType, StringIO
41 from xml.parsers.expat import ExpatError
43 from sfa.util.faults import (CredentialNotVerifiable,
44 ChildRightsNotSubsetOfParent)
45 from sfa.util.sfalogging import logger
46 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
47 from sfa.trust.rights import Right, Rights, determine_rights
48 from sfa.trust.gid import GID
49 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
53 from lxml import etree
60 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
64 # . make privs match between PG and PL
65 # . Need to add support for other types of credentials, e.g. tickets
66 # . add namespaces to signed-credential element?
70 <Signature xml:id="Sig_{refid}" xmlns="http://www.w3.org/2000/09/xmldsig#">
72 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
73 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
74 <Reference URI="#{refid}">
76 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
78 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
79 <DigestValue></DigestValue>
95 # Convert a string into a bool
96 # used to convert an xsd:boolean to a Python boolean
100 if str.lower() in ('true', '1'):
106 # Utility function to get the text of an XML element
108 def getTextNode(element, subele):
109 sub = element.getElementsByTagName(subele)[0]
110 if len(sub.childNodes) > 0:
111 return sub.childNodes[0].nodeValue
116 # Utility function to set the text of an XML element
117 # It creates the element, adds the text to it,
118 # and then appends it to the parent.
121 def append_sub(doc, parent, element, text):
122 ele = doc.createElement(element)
123 ele.appendChild(doc.createTextNode(text))
124 parent.appendChild(ele)
127 # Signature contains information about an xmlsec1 signature
128 # for a signed-credential
132 class Signature(object):
134 def __init__(self, string=None):
136 self.issuer_gid = None
152 def set_refid(self, id):
155 def get_issuer_gid(self):
160 def set_issuer_gid(self, gid):
164 # Helper function to pull characters off the front of a string if
166 def remove_prefix(text, prefix):
167 if text and prefix and text.startswith(prefix):
168 return text[len(prefix):]
172 doc = parseString(self.xml)
173 except ExpatError as e:
174 logger.log_exc("Failed to parse credential, {}".format(self.xml))
176 sig = doc.getElementsByTagName("Signature")[0]
177 # This code until the end of function rewritten by Aaron Helsinger
178 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
179 # The xml:id tag is optional, and could be in a
180 # Reference xml:id or Reference UID sub element instead
181 if not ref_id or ref_id == '':
182 reference = sig.getElementsByTagName('Reference')[0]
183 ref_id = remove_prefix(
184 reference.getAttribute('xml:id').strip(), "Sig_")
185 if not ref_id or ref_id == '':
186 ref_id = remove_prefix(
187 reference.getAttribute('URI').strip(), "#")
188 self.set_refid(ref_id)
189 keyinfos = sig.getElementsByTagName("X509Data")
191 for keyinfo in keyinfos:
192 certs = keyinfo.getElementsByTagName("X509Certificate")
194 if len(cert.childNodes) > 0:
195 szgid = cert.childNodes[0].nodeValue
196 szgid = szgid.strip()
197 szgid = "-----BEGIN CERTIFICATE-----\n"\
198 "{}\n-----END CERTIFICATE-----".format(
205 raise CredentialNotVerifiable(
206 "Malformed XML: No certificate found in signature")
207 self.set_issuer_gid(GID(string=gids))
210 self.xml = signature_format.format(refid=self.get_refid())
213 # A credential provides a caller gid with privileges to an object gid.
214 # A signed credential is signed by the object's authority.
216 # Credentials are encoded in one of two ways. The legacy style (now
217 # unsupported) places it in the subjectAltName of an X509 certificate.
218 # The new credentials are placed in signed XML.
221 # In general, a signed credential obtained externally should
222 # not be changed else the signature is no longer valid. So, once
223 # you have loaded an existing signed credential, do not call encode() or
227 def filter_creds_by_caller(creds, caller_hrn_list):
229 Returns a list of creds who's gid caller matches the
232 if not isinstance(creds, list):
234 if not isinstance(caller_hrn_list, list):
235 caller_hrn_list = [caller_hrn_list]
239 tmp_cred = Credential(string=cred)
240 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
242 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
243 caller_creds.append(cred)
249 class Credential(object):
251 SFA_CREDENTIAL_TYPE = "geni_sfa"
254 # Create a Credential object
256 # @param create If true, create a blank x509 certificate
257 # @param subject If subject!=None,
258 # create an x509 cert with the subject name
259 # @param string If string!=None, load the credential from the string
260 # @param filename If filename!=None, load the credential from the file
261 # FIXME: create and subject are ignored!
262 def __init__(self, create=False, subject=None, string=None,
263 filename=None, cred=None):
264 self.gidCaller = None
265 self.gidObject = None
266 self.expiration = None
267 self.privileges = None
268 self.issuer_privkey = None
269 self.issuer_gid = None
270 self.issuer_pubkey = None
272 self.signature = None
275 self.type = Credential.SFA_CREDENTIAL_TYPE
279 if isinstance(cred, StringType):
281 self.type = Credential.SFA_CREDENTIAL_TYPE
283 elif isinstance(cred, dict):
284 string = cred['geni_value']
285 self.type = cred['geni_type']
286 self.version = cred['geni_version']
288 if string or filename:
292 with open(filename) as infile:
295 # if this is a legacy credential, write error and bail out
296 if isinstance(str, StringType) and str.strip().startswith("-----"):
298 "Legacy credentials not supported any more "
299 "- giving up with {}..."
305 # not strictly necessary but won't hurt either
306 self.get_xmlsec1_path()
309 def get_xmlsec1_path():
310 if not getattr(Credential, 'xmlsec1_path', None):
311 # Find a xmlsec1 binary path
312 Credential.xmlsec1_path = ''
313 paths = ['/usr/bin', '/usr/local/bin',
314 '/bin', '/opt/bin', '/opt/local/bin']
316 paths += os.getenv('PATH').split(':')
320 xmlsec1 = os.path.join(path, 'xmlsec1')
321 if os.path.isfile(xmlsec1):
322 Credential.xmlsec1_path = xmlsec1
324 if not Credential.xmlsec1_path:
326 "Could not locate required binary 'xmlsec1' -"
327 "SFA will be unable to sign stuff !!")
328 return Credential.xmlsec1_path
330 def get_subject(self):
331 if not self.gidObject:
333 return self.gidObject.get_subject()
335 def pretty_subject(self):
337 if not self.gidObject:
340 subject = self.gidObject.pretty_cert()
343 # sounds like this should be __repr__ instead ??
344 def pretty_cred(self):
345 if not self.gidObject:
347 obj = self.gidObject.pretty_cert()
348 caller = self.gidCaller.pretty_cert()
349 exp = self.get_expiration()
350 # Summarize the rights too? The issuer?
351 return "[Cred. for {caller} rights on {obj} until {exp} ]"\
354 def get_signature(self):
355 if not self.signature:
357 return self.signature
359 def set_signature(self, sig):
363 # Need the issuer's private key and name
364 # @param key Keypair object containing the private key of the issuer
365 # @param gid GID of the issuing authority
367 def set_issuer_keys(self, privkey, gid):
368 self.issuer_privkey = privkey
369 self.issuer_gid = gid
372 # Set this credential's parent
373 def set_parent(self, cred):
378 # set the GID of the caller
380 # @param gid GID object of the caller
382 def set_gid_caller(self, gid):
384 # gid origin caller is the caller's gid by default
385 self.gidOriginCaller = gid
388 # get the GID of the object
390 def get_gid_caller(self):
391 if not self.gidCaller:
393 return self.gidCaller
396 # set the GID of the object
398 # @param gid GID object of the object
400 def set_gid_object(self, gid):
404 # get the GID of the object
406 def get_gid_object(self):
407 if not self.gidObject:
409 return self.gidObject
412 # Expiration: an absolute UTC time of expiration (as either an int
413 # or string or datetime)
415 def set_expiration(self, expiration):
416 expiration_datetime = utcparse(expiration)
417 if expiration_datetime is not None:
418 self.expiration = expiration_datetime
421 "unexpected input {} in Credential.set_expiration"
425 # get the lifetime of the credential (always in datetime format)
427 def get_expiration(self):
428 if not self.expiration:
430 # at this point self.expiration is normalized as a datetime - DON'T
431 # call utcparse again
432 return self.expiration
437 # @param privs either a comma-separated list of privileges of a
440 def set_privileges(self, privs):
441 if isinstance(privs, str):
442 self.privileges = Rights(string=privs)
444 self.privileges = privs
447 # return the privileges as a Rights object
449 def get_privileges(self):
450 if not self.privileges:
452 return self.privileges
455 # determine whether the credential allows a particular operation to be
458 # @param op_name string specifying name of operation
459 # ("lookup", "update", etc)
461 def can_perform(self, op_name):
462 rights = self.get_privileges()
467 return rights.can_perform(op_name)
470 # Encode the attributes of the credential into an XML string
471 # This should be done immediately before signing the credential.
473 # In general, a signed credential obtained externally should
474 # not be changed else the signature is no longer valid. So, once
475 # you have loaded an existing signed credential, do not call encode() or
479 # Create the XML document
481 signed_cred = doc.createElement("signed-credential")
484 # Note that credential/policy.xsd are really the PG schemas
486 # Note that delegation of credentials between the 2 only really works
487 # cause those schemas are identical.
488 # Also note these PG schemas talk about PG tickets and CM policies.
489 signed_cred.setAttribute(
490 "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
491 # FIXME: See v2 schema at
492 # www.geni.net/resources/credential/2/credential.xsd
493 signed_cred.setAttribute(
494 "xsi:noNamespaceSchemaLocation",
495 "http://www.planet-lab.org/resources/sfa/credential.xsd")
496 signed_cred.setAttribute(
497 "xsi:schemaLocation",
498 "http://www.planet-lab.org/resources/sfa/ext/policy/1 "
499 "http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
501 # PG says for those last 2:
502 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation",
503 # "http://www.protogeni.net/resources/credential/credential.xsd")
504 # signed_cred.setAttribute("xsi:schemaLocation",
505 # "http://www.protogeni.net/resources/credential/ext/policy/1 "
506 # "http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
508 doc.appendChild(signed_cred)
510 # Fill in the <credential> bit
511 cred = doc.createElement("credential")
512 cred.setAttribute("xml:id", self.get_refid())
513 signed_cred.appendChild(cred)
514 append_sub(doc, cred, "type", "privilege")
515 append_sub(doc, cred, "serial", "8")
516 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
517 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
518 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
519 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
520 append_sub(doc, cred, "uuid", "")
521 if not self.expiration:
522 logger.debug("Creating credential valid for {} s".format(
523 DEFAULT_CREDENTIAL_LIFETIME))
524 self.set_expiration(datetime.datetime.utcnow(
525 ) + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
526 self.expiration = self.expiration.replace(microsecond=0)
527 if self.expiration.tzinfo is not None \
528 and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
529 # TZ aware. Make sure it is UTC - by Aaron Helsinger
530 self.expiration = self.expiration.astimezone(tz.tzutc())
531 append_sub(doc, cred, "expires",
532 self.expiration.strftime(SFATIME_FORMAT))
533 privileges = doc.createElement("privileges")
534 cred.appendChild(privileges)
537 rights = self.get_privileges()
538 for right in rights.rights:
539 priv = doc.createElement("privilege")
540 append_sub(doc, priv, "name", right.kind)
541 append_sub(doc, priv, "can_delegate",
542 str(right.delegate).lower())
543 privileges.appendChild(priv)
545 # Add the parent credential if it exists
547 sdoc = parseString(self.parent.get_xml())
548 # If the root node is a signed-credential (it should be), then
549 # get all its attributes and attach those to our signed_cred
551 # Specifically, PG and PL add attributes for namespaces
552 # (which is reasonable),
553 # and we need to include those again here or else their signature
554 # no longer matches on the credential.
555 # We expect three of these, but here we copy them all:
556 # signed_cred.setAttribute("xmlns:xsi",
557 # "http://www.w3.org/2001/XMLSchema-instance")
558 # and from PG (PL is equivalent, as shown above):
559 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation",
560 # "http://www.protogeni.net/resources/credential/credential.xsd")
561 # signed_cred.setAttribute("xsi:schemaLocation",
562 # "http://www.protogeni.net/resources/credential/ext/policy/1 "
563 # "http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
566 # PL now also declares these, with different URLs, so
567 # the code notices those attributes already existed with
568 # different values, and complains.
569 # This happens regularly on delegation now that PG and
570 # PL both declare the namespace with different URLs.
571 # If the content ever differs this is a problem,
572 # but for now it works - different URLs (values in the attributes)
573 # but the same actual schema, so using the PG schema
574 # on delegated-to-PL credentials works fine.
576 # Note: you could also not copy attributes
577 # which already exist. It appears that both PG and PL
578 # will actually validate a slicecred with a parent
579 # signed using PG namespaces and a child signed with PL
580 # namespaces over the whole thing. But I don't know
581 # if that is a bug in xmlsec1, an accident since
582 # the contents of the schemas are the same,
583 # or something else, but it seems odd. And this works.
584 parentRoot = sdoc.documentElement
585 if parentRoot.tagName == "signed-credential" and \
586 parentRoot.hasAttributes():
587 for attrIx in range(0, parentRoot.attributes.length):
588 attr = parentRoot.attributes.item(attrIx)
589 # returns the old attribute of same name that was
591 # Below throws InUse exception if we forgot to clone the
593 oldAttr = signed_cred.setAttributeNode(
594 attr.cloneNode(True))
595 if oldAttr and oldAttr.value != attr.value:
596 msg = "Delegating cred from owner {} to {} over {}:\n"
597 "- Replaced attribute {} value '{}' with '{}'"\
598 .format(self.parent.gidCaller.get_urn(),
599 self.gidCaller.get_urn(),
600 self.gidObject.get_urn(),
601 oldAttr.name, oldAttr.value, attr.value)
603 # raise CredentialNotVerifiable(
604 # "Can't encode new valid delegated credential: {}"
607 p_cred = doc.importNode(
608 sdoc.getElementsByTagName("credential")[0], True)
609 p = doc.createElement("parent")
610 p.appendChild(p_cred)
612 # done handling parent credential
614 # Create the <signatures> tag
615 signatures = doc.createElement("signatures")
616 signed_cred.appendChild(signatures)
618 # Add any parent signatures
620 for cur_cred in self.get_credential_list()[1:]:
621 sdoc = parseString(cur_cred.get_signature().get_xml())
622 ele = doc.importNode(
623 sdoc.getElementsByTagName("Signature")[0], True)
624 signatures.appendChild(ele)
626 # Get the finished product
627 self.xml = doc.toxml("utf-8")
629 def save_to_random_tmp_file(self):
630 fp, filename = mkstemp(suffix='cred', text=True)
631 fp = os.fdopen(fp, "w")
632 self.save_to_file(filename, save_parents=True, filep=fp)
635 def save_to_file(self, filename, save_parents=True, filep=None):
641 f = open(filename, "w")
642 if PY3 and isinstance(self.xml, bytes):
643 self.xml = self.xml.decode()
647 def save_to_string(self, save_parents=True):
650 if PY3 and isinstance(self.xml, bytes):
651 self.xml = self.xml.decode()
659 def set_refid(self, rid):
663 # Figure out what refids exist, and update this credential's id
664 # so that it doesn't clobber the others. Returns the refids of
667 def updateRefID(self):
669 self.set_refid('ref0')
674 next_cred = self.parent
676 refs.append(next_cred.get_refid())
678 next_cred = next_cred.parent
682 # Find a unique refid for this credential
683 rid = self.get_refid()
686 rid = "ref{}".format(val + 1)
691 # Return the set of parent credential ref ids
700 # Sign the XML file created by encode()
703 # In general, a signed credential obtained externally should
704 # not be changed else the signature is no longer valid. So, once
705 # you have loaded an existing signed credential, do not call encode() or
709 if not self.issuer_privkey:
710 logger.warning("Cannot sign credential (no private key)")
712 if not self.issuer_gid:
713 logger.warning("Cannot sign credential (no issuer gid)")
715 doc = parseString(self.get_xml())
716 sigs = doc.getElementsByTagName("signatures")[0]
718 # Create the signature template to be signed
719 signature = Signature()
720 signature.set_refid(self.get_refid())
721 sdoc = parseString(signature.get_xml())
722 sig_ele = doc.importNode(
723 sdoc.getElementsByTagName("Signature")[0], True)
724 sigs.appendChild(sig_ele)
726 self.xml = doc.toxml("utf-8")
728 # Split the issuer GID into multiple certificates if it's a chain
729 chain = GID(filename=self.issuer_gid)
732 gid_files.append(chain.save_to_random_tmp_file(False))
733 if chain.get_parent():
734 chain = chain.get_parent()
738 # Call out to xmlsec1 to sign it
739 ref = 'Sig_{}'.format(self.get_refid())
740 filename = self.save_to_random_tmp_file()
741 xmlsec1 = self.get_xmlsec1_path()
743 raise Exception("Could not locate required 'xmlsec1' program")
744 command = '{} --sign --node-id "{}" --privkey-pem {},{} {}' \
745 .format(xmlsec1, ref, self.issuer_privkey,
746 ",".join(gid_files), filename)
747 signed = os.popen(command).read()
750 for gid_file in gid_files:
759 # Retrieve the attributes of the credential from the XML.
760 # This is automatically called by the various get_* methods of
761 # this class and should not need to be called explicitly.
769 doc = parseString(self.xml)
770 except ExpatError as e:
771 raise CredentialNotVerifiable("Malformed credential")
772 doc = parseString(self.xml)
774 signed_cred = doc.getElementsByTagName("signed-credential")
776 # Is this a signed-cred or just a cred?
777 if len(signed_cred) > 0:
778 creds = signed_cred[0].getElementsByTagName("credential")
779 signatures = signed_cred[0].getElementsByTagName("signatures")
780 if len(signatures) > 0:
781 sigs = signatures[0].getElementsByTagName("Signature")
783 creds = doc.getElementsByTagName("credential")
785 if creds is None or len(creds) == 0:
786 # malformed cred file
787 raise CredentialNotVerifiable(
788 "Malformed XML: No credential tag found")
790 # Just take the first cred if there are more than one
793 self.set_refid(cred.getAttribute("xml:id"))
794 self.set_expiration(utcparse(getTextNode(cred, "expires")))
795 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
796 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
798 # This code until the end of function rewritten by Aaron Helsinger
801 priv_nodes = cred.getElementsByTagName("privileges")
802 if len(priv_nodes) > 0:
803 privs = priv_nodes[0]
804 for priv in privs.getElementsByTagName("privilege"):
805 kind = getTextNode(priv, "name")
806 deleg = str2bool(getTextNode(priv, "can_delegate"))
808 # Convert * into the default privileges
809 # for the credential's type
810 # Each inherits the delegatability from the * above
811 _, type = urn_to_hrn(self.gidObject.get_urn())
812 rl = determine_rights(type, self.gidObject.get_urn())
817 rlist.add(Right(kind.strip(), deleg))
818 self.set_privileges(rlist)
821 parent = cred.getElementsByTagName("parent")
823 parent_doc = parent[0].getElementsByTagName("credential")[0]
824 parent_xml = parent_doc.toxml("utf-8")
825 if parent_xml is None or parent_xml.strip() == "":
826 raise CredentialNotVerifiable(
827 "Malformed XML: Had parent tag but it is empty")
828 self.parent = Credential(string=parent_xml)
831 # Assign the signatures to the credentials
833 Sig = Signature(string=sig.toxml("utf-8"))
835 for cur_cred in self.get_credential_list():
836 if cur_cred.get_refid() == Sig.get_refid():
837 cur_cred.set_signature(Sig)
841 # trusted_certs: A list of trusted GID filenames (not GID objects!)
842 # Chaining is not supported within the GIDs by xmlsec1.
844 # trusted_certs_required: Should usually be true. Set False means an
845 # empty list of trusted_certs would still let this method pass.
846 # It just skips xmlsec1 verification et al.
847 # Only used by some utils
850 # . All of the signatures are valid and that the issuers trace back
851 # to trusted roots (performed by xmlsec1)
852 # . The XML matches the credential schema
853 # . That the issuer of the credential is the authority in the target's urn
854 # . In the case of a delegated credential, this must be true of the root
855 # . That all of the gids presented in the credential are valid
856 # . Including verifying GID chains, and includ the issuer
857 # . The credential is not expired
859 # -- For Delegates (credentials with parents)
860 # . The privileges must be a subset of the parent credentials
861 # . The privileges must have "can_delegate"
862 # set for each delegated privilege
863 # . The target gid must be the same between child and parents
864 # . The expiry time on the child must be no later than the parent
865 # . The signer of the child must be the owner of the parent
867 # -- Verify does *NOT*
868 # . ensure that an xmlrpc client's gid matches a credential gid, that
869 # must be done elsewhere
871 # @param trusted_certs: The certificates of trusted CA certificates
872 def verify(self, trusted_certs=None, schema=None,
873 trusted_certs_required=True):
877 # validate against RelaxNG schema
879 if schema and os.path.exists(schema):
880 tree = etree.parse(StringIO(self.xml))
881 schema_doc = etree.parse(schema)
882 xmlschema = etree.XMLSchema(schema_doc)
883 if not xmlschema.validate(tree):
884 error = xmlschema.error_log.last_error
885 message = "{}: {} (line {})"\
886 .format(self.pretty_cred(),
887 error.message, error.line)
888 raise CredentialNotVerifiable(message)
890 if trusted_certs_required and trusted_certs is None:
893 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
894 trusted_cert_objects = []
895 ok_trusted_certs = []
896 # If caller explicitly passed in None, that means
897 # skip cert chain validation. Strange and not typical
898 if trusted_certs is not None:
899 for f in trusted_certs:
901 # Failures here include unreadable files
903 trusted_cert_objects.append(GID(filename=f))
904 ok_trusted_certs.append(f)
905 except Exception as exc:
907 "Failed to load trusted cert from {}".format(f))
908 trusted_certs = ok_trusted_certs
910 # make sure it is not expired
911 if self.get_expiration() < datetime.datetime.utcnow():
912 raise CredentialNotVerifiable(
913 "Credential {} expired at {}"
914 .format(self.pretty_cred(),
915 self.expiration.strftime(SFATIME_FORMAT)))
917 # Verify the signatures
918 filename = self.save_to_random_tmp_file()
920 # If caller explicitly passed in None that means
921 # skip cert chain validation. Strange and not typical
922 if trusted_certs is not None:
923 # Verify the caller and object gids of this cred and of its parents
924 for cur_cred in self.get_credential_list():
925 # check both the caller and the subject
926 for gid in (cur_cred.get_gid_object(),
927 cur_cred.get_gid_caller()):
928 logger.debug("Credential.verify: verifying chain {}"
929 .format(gid.pretty_cert()))
930 logger.debug("Credential.verify: against trusted {}"
931 .format(" ".join(trusted_certs)))
932 gid.verify_chain(trusted_cert_objects)
935 refs.append("Sig_{}".format(self.get_refid()))
937 parentRefs = self.updateRefID()
938 for ref in parentRefs:
939 refs.append("Sig_{}".format(ref))
942 # If caller explicitly passed in None that means
943 # skip xmlsec1 validation. Strange and not typical
944 if trusted_certs is None:
948 # up to fedora20 we used os.popen and checked
949 # that the output begins with OK; turns out, with fedora21,
950 # there is extra input before this 'OK' thing
951 # looks like we're better off just using the exit code
952 # that's what it is made for
953 # cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
954 # command = '{} --verify --node-id "{}" {} {} 2>&1'.\
955 # format(self.xmlsec_path, ref, cert_args, filename)
956 xmlsec1 = self.get_xmlsec1_path()
958 raise Exception("Could not locate required 'xmlsec1' program")
959 command = [xmlsec1, '--verify', '--node-id', ref]
960 for trusted in trusted_certs:
961 command += ["--trusted-pem", trusted]
962 command += [filename]
963 logger.debug("Running " + " ".join(command))
965 verified = subprocess.check_output(
966 command, stderr=subprocess.STDOUT)
967 logger.debug("xmlsec command returned {}".format(verified))
968 if "OK\n" not in verified:
970 "WARNING: xmlsec1 seemed to return fine but without a OK in its output")
971 except subprocess.CalledProcessError as e:
973 # xmlsec errors have a msg= which is the interesting bit.
974 mstart = verified.find("msg=")
976 if mstart > -1 and len(verified) > 4:
978 mend = verified.find('\\', mstart)
979 msg = verified[mstart:mend]
981 "Credential.verify - failed - xmlsec1 returned {}"
982 .format(verified.strip()))
983 raise CredentialNotVerifiable(
984 "xmlsec1 error verifying cred {} using Signature ID {}: {}"
985 .format(self.pretty_cred(), ref, msg))
988 # Verify the parents (delegation)
990 self.verify_parent(self.parent)
992 # Make sure the issuer is the target's authority, and is
994 self.verify_issuer(trusted_cert_objects)
998 # Creates a list of the credential and its parents, with the root
999 # (original delegated credential) as the last item in the list
1000 def get_credential_list(self):
1004 list.append(cur_cred)
1006 cur_cred = cur_cred.parent
1012 # Make sure the credential's target gid (a) was signed by or (b)
1013 # is the same as the entity that signed the original credential,
1014 # or (c) is an authority over the target's namespace.
1015 # Also ensure that the credential issuer / signer itself has a valid
1016 # GID signature chain (signed by an authority with namespace rights).
1017 def verify_issuer(self, trusted_gids):
1018 root_cred = self.get_credential_list()[-1]
1019 root_target_gid = root_cred.get_gid_object()
1020 if root_cred.get_signature() is None:
1022 raise CredentialNotVerifiable(
1023 "Could not verify credential owned by {} for object {}. "
1024 "Cred has no signature"
1025 .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
1027 root_cred_signer = root_cred.get_signature().get_issuer_gid()
1030 # Allow non authority to sign target and cred about target.
1032 # Why do we need to allow non authorities to sign?
1033 # If in the target gid validation step we correctly
1034 # checked that the target is only signed by an authority,
1035 # then this is just a special case of case 3.
1036 # This short-circuit is the common case currently -
1037 # and cause GID validation doesn't check 'authority',
1038 # this allows users to generate valid slice credentials.
1039 if root_target_gid.is_signed_by_cert(root_cred_signer):
1040 # cred signer matches target signer, return success
1044 # Allow someone to sign credential about themeselves. Used?
1045 # If not, remove this.
1046 # root_target_gid_str = root_target_gid.save_to_string()
1047 # root_cred_signer_str = root_cred_signer.save_to_string()
1048 # if root_target_gid_str == root_cred_signer_str:
1049 # # cred signer is target, return success
1054 # root_cred_signer is not the target_gid
1055 # So this is a different gid that we have not verified.
1056 # xmlsec1 verified the cert chain on this already, but
1057 # it hasn't verified that the gid meets the HRN namespace
1059 # Below we'll ensure that it is an authority.
1060 # But we haven't verified that it is _signed by_ an authority
1061 # We also don't know if xmlsec1 requires that cert signers
1062 # are marked as CAs.
1064 # Note that if verify() gave us no trusted_gids then this
1065 # call will fail. So skip it if we have no trusted_gids
1066 if trusted_gids and len(trusted_gids) > 0:
1067 root_cred_signer.verify_chain(trusted_gids)
1070 "Cannot verify that cred signer is signed by a trusted authority. "
1071 "No trusted gids. Skipping that check.")
1073 # See if the signer is an authority over the domain of the target.
1074 # There are multiple types of authority - accept them all here
1075 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1076 root_cred_signer_type = root_cred_signer.get_type()
1077 if root_cred_signer_type.find('authority') == 0:
1078 # logger.debug('Cred signer is an authority')
1079 # signer is an authority, see if target is in authority's domain
1080 signerhrn = root_cred_signer.get_hrn()
1081 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1084 # We've required that the credential be signed by an authority
1085 # for that domain. Reasonable and probably correct.
1086 # A looser model would also allow the signer to be an authority
1087 # in my control framework - eg My CA or CH. Even if it is not
1088 # the CH that issued these, eg, user credentials.
1090 # Give up, credential does not pass issuer verification
1092 raise CredentialNotVerifiable(
1093 "Could not verify credential owned by {} for object {}. "
1094 "Cred signer {} not the trusted authority for Cred target {}"
1095 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1096 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1099 # -- For Delegates (credentials with parents) verify that:
1100 # . The privileges must be a subset of the parent credentials
1101 # . The privileges must have "can_delegate" set for each delegated privilege
1102 # . The target gid must be the same between child and parents
1103 # . The expiry time on the child must be no later than the parent
1104 # . The signer of the child must be the owner of the parent
1105 def verify_parent(self, parent_cred):
1106 # make sure the rights given to the child are a subset of the
1107 # parents rights (and check delegate bits)
1108 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1110 "Parent cred {} (ref {}) rights {} "
1111 " not superset of delegated cred {} (ref {}) rights {}"
1112 .format(parent_cred.pretty_cred(), parent_cred.get_refid(),
1113 parent_cred.get_privileges().pretty_rights(),
1114 self.pretty_cred(), self.get_refid(),
1115 self.get_privileges().pretty_rights()))
1116 logger.error(message)
1117 logger.error("parent details {}".format(
1118 parent_cred.get_privileges().save_to_string()))
1119 logger.error("self details {}".format(
1120 self.get_privileges().save_to_string()))
1121 raise ChildRightsNotSubsetOfParent(message)
1123 # make sure my target gid is the same as the parent's
1124 if not parent_cred.get_gid_object().save_to_string() == \
1125 self.get_gid_object().save_to_string():
1127 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1128 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1129 logger.error(message)
1130 logger.error("parent details {}".format(
1131 parent_cred.save_to_string()))
1132 logger.error("self details {}".format(self.save_to_string()))
1133 raise CredentialNotVerifiable(message)
1135 # make sure my expiry time is <= my parent's
1136 if not parent_cred.get_expiration() >= self.get_expiration():
1137 raise CredentialNotVerifiable(
1138 "Delegated credential {} expires after parent {}"
1139 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1141 # make sure my signer is the parent's caller
1142 if not parent_cred.get_gid_caller().save_to_string(False) == \
1143 self.get_signature().get_issuer_gid().save_to_string(False):
1144 message = "Delegated credential {} not signed by parent {}'s caller"\
1145 .format(self.pretty_cred(), parent_cred.pretty_cred())
1146 logger.error(message)
1147 logger.error("compare1 parent {}".format(
1148 parent_cred.get_gid_caller().pretty_cert()))
1149 logger.error("compare1 parent details {}".format(
1150 parent_cred.get_gid_caller().save_to_string()))
1151 logger.error("compare2 self {}".format(
1152 self.get_signature().get_issuer_gid().pretty_crert()))
1153 logger.error("compare2 self details {}".format(
1154 self.get_signature().get_issuer_gid().save_to_string()))
1155 raise CredentialNotVerifiable(message)
1158 if parent_cred.parent:
1159 parent_cred.verify_parent(parent_cred.parent)
1161 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1163 Return a delegated copy of this credential, delegated to the
1164 specified gid's user.
1166 # get the gid of the object we are delegating
1167 object_gid = self.get_gid_object()
1168 object_hrn = object_gid.get_hrn()
1170 # the hrn of the user who will be delegated to
1171 delegee_gid = GID(filename=delegee_gidfile)
1172 delegee_hrn = delegee_gid.get_hrn()
1174 # user_key = Keypair(filename=keyfile)
1175 # user_hrn = self.get_gid_caller().get_hrn()
1176 subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1177 dcred = Credential(subject=subject_string)
1178 dcred.set_gid_caller(delegee_gid)
1179 dcred.set_gid_object(object_gid)
1180 dcred.set_parent(self)
1181 dcred.set_expiration(self.get_expiration())
1182 dcred.set_privileges(self.get_privileges())
1183 dcred.get_privileges().delegate_all_privileges(True)
1184 # dcred.set_issuer_keys(keyfile, delegee_gidfile)
1185 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1192 def get_filename(self):
1193 return getattr(self, 'filename', None)
1195 def actual_caller_hrn(self):
1197 a helper method used by some API calls like e.g. Allocate
1198 to try and find out who really is the original caller
1200 This admittedly is a bit of a hack, please USE IN LAST RESORT
1202 This code uses a heuristic to identify a delegated credential
1204 A first known restriction if for traffic that gets through a
1205 slice manager in this case the hrn reported is the one from
1206 the last SM in the call graph which is not at all what is
1210 caller_hrn, caller_type = urn_to_hrn(self.get_gid_caller().get_urn())
1211 issuer_hrn, issuer_type = urn_to_hrn(
1212 self.get_signature().get_issuer_gid().get_urn())
1213 subject_hrn = self.get_gid_object().get_hrn()
1214 # if the caller is a user and the issuer is not
1215 # it's probably the former
1216 if caller_type == "user" and issuer_type != "user":
1217 actual_caller_hrn = caller_hrn
1218 # if we find that the caller_hrn is an immediate descendant
1219 # of the issuer, then this seems to be a 'regular' credential
1220 elif caller_hrn.startswith(issuer_hrn):
1221 actual_caller_hrn = caller_hrn
1222 # else this looks like a delegated credential, and the real caller is
1225 actual_caller_hrn = issuer_hrn
1227 "actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1228 .format(caller_hrn, issuer_hrn, actual_caller_hrn))
1229 return actual_caller_hrn
1232 # Dump the contents of a credential to stdout in human-readable format
1234 # @param dump_parents If true, also dump the parent certificates
1235 def dump(self, *args, **kwargs):
1236 print(self.dump_string(*args, **kwargs))
1238 # SFA code ignores show_xml and disables printing the cred xml
1239 def dump_string(self, dump_parents=False, show_xml=False):
1241 result += "CREDENTIAL {}\n".format(self.pretty_subject())
1242 filename = self.get_filename()
1244 result += "Filename {}\n".format(filename)
1245 privileges = self.get_privileges()
1247 result += " privs: {}\n".format(privileges.save_to_string())
1249 result += " privs: \n"
1250 gidCaller = self.get_gid_caller()
1252 result += " gidCaller:\n"
1253 result += gidCaller.dump_string(8, dump_parents)
1255 if self.get_signature():
1256 result += " gidIssuer:\n"
1257 result += self.get_signature().get_issuer_gid()\
1258 .dump_string(8, dump_parents)
1261 result += " expiration: " + \
1262 self.expiration.strftime(SFATIME_FORMAT) + "\n"
1264 gidObject = self.get_gid_object()
1266 result += " gidObject:\n"
1267 result += gidObject.dump_string(8, dump_parents)
1269 if self.parent and dump_parents:
1270 result += "\nPARENT"
1271 result += self.parent.dump_string(True)
1273 if show_xml and HAVELXML:
1275 tree = etree.parse(StringIO(self.xml))
1276 aside = etree.tostring(tree, pretty_print=True)
1277 result += "\nXML:\n\n"
1279 result += "\nEnd XML\n"
1282 print("exc. Credential.dump_string / XML")
1283 traceback.print_exc()