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
29 from __future__ import print_function
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
37 from sfa.util.py23 import PY3, StringType, StringIO
41 from lxml import etree
46 from xml.parsers.expat import ExpatError
48 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
49 from sfa.util.sfalogging import logger
50 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
51 from sfa.trust.rights import Right, Rights, determine_rights
52 from sfa.trust.gid import GID
53 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
56 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
60 # . make privs match between PG and PL
61 # . Need to add support for other types of credentials, e.g. tickets
62 # . add namespaces to signed-credential element?
66 <Signature xml:id="Sig_{refid}" xmlns="http://www.w3.org/2000/09/xmldsig#">
68 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
69 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
70 <Reference URI="#{refid}">
72 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
74 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
75 <DigestValue></DigestValue>
91 # Convert a string into a bool
92 # used to convert an xsd:boolean to a Python boolean
94 if str.lower() in ['true','1']:
100 # Utility function to get the text of an XML element
102 def getTextNode(element, subele):
103 sub = element.getElementsByTagName(subele)[0]
104 if len(sub.childNodes) > 0:
105 return sub.childNodes[0].nodeValue
110 # Utility function to set the text of an XML element
111 # It creates the element, adds the text to it,
112 # and then appends it to the parent.
114 def append_sub(doc, parent, element, text):
115 ele = doc.createElement(element)
116 ele.appendChild(doc.createTextNode(text))
117 parent.appendChild(ele)
120 # Signature contains information about an xmlsec1 signature
121 # for a signed-credential
124 class Signature(object):
126 def __init__(self, string=None):
128 self.issuer_gid = None
145 def set_refid(self, id):
148 def get_issuer_gid(self):
153 def set_issuer_gid(self, gid):
157 # Helper function to pull characters off the front of a string if present
158 def remove_prefix(text, prefix):
159 if text and prefix and text.startswith(prefix):
160 return text[len(prefix):]
164 doc = parseString(self.xml)
165 except ExpatError as e:
166 logger.log_exc("Failed to parse credential, {}".format(self.xml))
168 sig = doc.getElementsByTagName("Signature")[0]
169 ## This code until the end of function rewritten by Aaron Helsinger
170 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
171 # The xml:id tag is optional, and could be in a
172 # Reference xml:id or Reference UID sub element instead
173 if not ref_id or ref_id == '':
174 reference = sig.getElementsByTagName('Reference')[0]
175 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
176 if not ref_id or ref_id == '':
177 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
178 self.set_refid(ref_id)
179 keyinfos = sig.getElementsByTagName("X509Data")
181 for keyinfo in keyinfos:
182 certs = keyinfo.getElementsByTagName("X509Certificate")
184 if len(cert.childNodes) > 0:
185 szgid = cert.childNodes[0].nodeValue
186 szgid = szgid.strip()
187 szgid = "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----".format(szgid)
193 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
194 self.set_issuer_gid(GID(string=gids))
197 self.xml = signature_format.format(refid=self.get_refid())
200 # A credential provides a caller gid with privileges to an object gid.
201 # A signed credential is signed by the object's authority.
203 # Credentials are encoded in one of two ways.
204 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
205 # The new credentials are placed in signed XML.
208 # In general, a signed credential obtained externally should
209 # not be changed else the signature is no longer valid. So, once
210 # you have loaded an existing signed credential, do not call encode() or sign() on it.
212 def filter_creds_by_caller(creds, caller_hrn_list):
214 Returns a list of creds who's gid caller matches the
217 if not isinstance(creds, list): creds = [creds]
218 if not isinstance(caller_hrn_list, list):
219 caller_hrn_list = [caller_hrn_list]
223 tmp_cred = Credential(string=cred)
224 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
226 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
227 caller_creds.append(cred)
231 class Credential(object):
233 SFA_CREDENTIAL_TYPE = "geni_sfa"
236 # Create a Credential object
238 # @param create If true, create a blank x509 certificate
239 # @param subject If subject!=None, create an x509 cert with the subject name
240 # @param string If string!=None, load the credential from the string
241 # @param filename If filename!=None, load the credential from the file
242 # FIXME: create and subject are ignored!
243 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
244 self.gidCaller = None
245 self.gidObject = None
246 self.expiration = None
247 self.privileges = None
248 self.issuer_privkey = None
249 self.issuer_gid = None
250 self.issuer_pubkey = None
252 self.signature = None
255 self.type = Credential.SFA_CREDENTIAL_TYPE
259 if isinstance(cred, StringType):
261 self.type = Credential.SFA_CREDENTIAL_TYPE
263 elif isinstance(cred, dict):
264 string = cred['geni_value']
265 self.type = cred['geni_type']
266 self.version = cred['geni_version']
268 if string or filename:
272 with open(filename) as infile:
275 # if this is a legacy credential, write error and bail out
276 if isinstance(str, StringType) and str.strip().startswith("-----"):
277 logger.error("Legacy credentials not supported any more - giving up with {}...".format(str[:10]))
282 # not strictly necessary but won't hurt either
283 self.get_xmlsec1_path()
286 def get_xmlsec1_path():
287 if not getattr(Credential, 'xmlsec1_path', None):
288 # Find a xmlsec1 binary path
289 Credential.xmlsec1_path = ''
290 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
291 try: paths += os.getenv('PATH').split(':')
294 xmlsec1 = os.path.join(path, 'xmlsec1')
295 if os.path.isfile(xmlsec1):
296 Credential.xmlsec1_path = xmlsec1
298 if not Credential.xmlsec1_path:
299 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
300 return Credential.xmlsec1_path
302 def get_subject(self):
303 if not self.gidObject:
305 return self.gidObject.get_subject()
307 def pretty_subject(self):
309 if not self.gidObject:
312 subject = self.gidObject.pretty_cert()
315 # sounds like this should be __repr__ instead ??
316 def pretty_cred(self):
317 if not self.gidObject:
319 obj = self.gidObject.pretty_cert()
320 caller = self.gidCaller.pretty_cert()
321 exp = self.get_expiration()
322 # Summarize the rights too? The issuer?
323 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
325 def get_signature(self):
326 if not self.signature:
328 return self.signature
330 def set_signature(self, sig):
335 # Need the issuer's private key and name
336 # @param key Keypair object containing the private key of the issuer
337 # @param gid GID of the issuing authority
339 def set_issuer_keys(self, privkey, gid):
340 self.issuer_privkey = privkey
341 self.issuer_gid = gid
345 # Set this credential's parent
346 def set_parent(self, cred):
351 # set the GID of the caller
353 # @param gid GID object of the caller
355 def set_gid_caller(self, gid):
357 # gid origin caller is the caller's gid by default
358 self.gidOriginCaller = gid
361 # get the GID of the object
363 def get_gid_caller(self):
364 if not self.gidCaller:
366 return self.gidCaller
369 # set the GID of the object
371 # @param gid GID object of the object
373 def set_gid_object(self, gid):
377 # get the GID of the object
379 def get_gid_object(self):
380 if not self.gidObject:
382 return self.gidObject
385 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
387 def set_expiration(self, expiration):
388 expiration_datetime = utcparse(expiration)
389 if expiration_datetime is not None:
390 self.expiration = expiration_datetime
392 logger.error("unexpected input {} in Credential.set_expiration".format(expiration))
395 # get the lifetime of the credential (always in datetime format)
397 def get_expiration(self):
398 if not self.expiration:
400 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
401 return self.expiration
406 # @param privs either a comma-separated list of privileges of a Rights object
408 def set_privileges(self, privs):
409 if isinstance(privs, str):
410 self.privileges = Rights(string = privs)
412 self.privileges = privs
415 # return the privileges as a Rights object
417 def get_privileges(self):
418 if not self.privileges:
420 return self.privileges
423 # determine whether the credential allows a particular operation to be
426 # @param op_name string specifying name of operation ("lookup", "update", etc)
428 def can_perform(self, op_name):
429 rights = self.get_privileges()
434 return rights.can_perform(op_name)
438 # Encode the attributes of the credential into an XML string
439 # This should be done immediately before signing the credential.
441 # In general, a signed credential obtained externally should
442 # not be changed else the signature is no longer valid. So, once
443 # you have loaded an existing signed credential, do not call encode() or sign() on it.
446 # Create the XML document
448 signed_cred = doc.createElement("signed-credential")
451 # Note that credential/policy.xsd are really the PG schemas
453 # Note that delegation of credentials between the 2 only really works
454 # cause those schemas are identical.
455 # Also note these PG schemas talk about PG tickets and CM policies.
456 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
457 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
458 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
459 signed_cred.setAttribute("xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
461 # PG says for those last 2:
462 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
463 # signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
465 doc.appendChild(signed_cred)
467 # Fill in the <credential> bit
468 cred = doc.createElement("credential")
469 cred.setAttribute("xml:id", self.get_refid())
470 signed_cred.appendChild(cred)
471 append_sub(doc, cred, "type", "privilege")
472 append_sub(doc, cred, "serial", "8")
473 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
474 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
475 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
476 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
477 append_sub(doc, cred, "uuid", "")
478 if not self.expiration:
479 logger.debug("Creating credential valid for {} s".format(DEFAULT_CREDENTIAL_LIFETIME))
480 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
481 self.expiration = self.expiration.replace(microsecond=0)
482 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
483 # TZ aware. Make sure it is UTC - by Aaron Helsinger
484 self.expiration = self.expiration.astimezone(tz.tzutc())
485 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
486 privileges = doc.createElement("privileges")
487 cred.appendChild(privileges)
490 rights = self.get_privileges()
491 for right in rights.rights:
492 priv = doc.createElement("privilege")
493 append_sub(doc, priv, "name", right.kind)
494 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
495 privileges.appendChild(priv)
497 # Add the parent credential if it exists
499 sdoc = parseString(self.parent.get_xml())
500 # If the root node is a signed-credential (it should be), then
501 # get all its attributes and attach those to our signed_cred
503 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
504 # and we need to include those again here or else their signature
505 # no longer matches on the credential.
506 # We expect three of these, but here we copy them all:
507 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
508 # and from PG (PL is equivalent, as shown above):
509 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
510 # signed_cred.setAttribute("xsi:schemaLocation", "http://www.protogeni.net/resources/credential/ext/policy/1 http://www.protogeni.net/resources/credential/ext/policy/1/policy.xsd")
513 # PL now also declares these, with different URLs, so
514 # the code notices those attributes already existed with
515 # different values, and complains.
516 # This happens regularly on delegation now that PG and
517 # PL both declare the namespace with different URLs.
518 # If the content ever differs this is a problem,
519 # but for now it works - different URLs (values in the attributes)
520 # but the same actual schema, so using the PG schema
521 # on delegated-to-PL credentials works fine.
523 # Note: you could also not copy attributes
524 # which already exist. It appears that both PG and PL
525 # will actually validate a slicecred with a parent
526 # signed using PG namespaces and a child signed with PL
527 # namespaces over the whole thing. But I don't know
528 # if that is a bug in xmlsec1, an accident since
529 # the contents of the schemas are the same,
530 # or something else, but it seems odd. And this works.
531 parentRoot = sdoc.documentElement
532 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
533 for attrIx in range(0, parentRoot.attributes.length):
534 attr = parentRoot.attributes.item(attrIx)
535 # returns the old attribute of same name that was
537 # Below throws InUse exception if we forgot to clone the attribute first
538 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
539 if oldAttr and oldAttr.value != attr.value:
540 msg = "Delegating cred from owner {} to {} over {}:\n"
541 "- Replaced attribute {} value '{}' with '{}'"\
542 .format(self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(),
543 self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
545 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: {}".format(msg))
547 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
548 p = doc.createElement("parent")
549 p.appendChild(p_cred)
551 # done handling parent credential
553 # Create the <signatures> tag
554 signatures = doc.createElement("signatures")
555 signed_cred.appendChild(signatures)
557 # Add any parent signatures
559 for cur_cred in self.get_credential_list()[1:]:
560 sdoc = parseString(cur_cred.get_signature().get_xml())
561 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
562 signatures.appendChild(ele)
564 # Get the finished product
565 self.xml = doc.toxml("utf-8")
568 def save_to_random_tmp_file(self):
569 fp, filename = mkstemp(suffix='cred', text=True)
570 fp = os.fdopen(fp, "w")
571 self.save_to_file(filename, save_parents=True, filep=fp)
574 def save_to_file(self, filename, save_parents=True, filep=None):
580 f = open(filename, "w")
581 if PY3 and isinstance(self.xml, bytes):
582 self.xml = self.xml.decode()
586 def save_to_string(self, save_parents=True):
589 if PY3 and isinstance(self.xml, bytes):
590 self.xml = self.xml.decode()
598 def set_refid(self, rid):
602 # Figure out what refids exist, and update this credential's id
603 # so that it doesn't clobber the others. Returns the refids of
606 def updateRefID(self):
608 self.set_refid('ref0')
613 next_cred = self.parent
615 refs.append(next_cred.get_refid())
617 next_cred = next_cred.parent
622 # Find a unique refid for this credential
623 rid = self.get_refid()
626 rid = "ref{}".format(val + 1)
631 # Return the set of parent credential ref ids
640 # Sign the XML file created by encode()
643 # In general, a signed credential obtained externally should
644 # not be changed else the signature is no longer valid. So, once
645 # you have loaded an existing signed credential, do not call encode() or sign() on it.
648 if not self.issuer_privkey:
649 logger.warn("Cannot sign credential (no private key)")
651 if not self.issuer_gid:
652 logger.warn("Cannot sign credential (no issuer gid)")
654 doc = parseString(self.get_xml())
655 sigs = doc.getElementsByTagName("signatures")[0]
657 # Create the signature template to be signed
658 signature = Signature()
659 signature.set_refid(self.get_refid())
660 sdoc = parseString(signature.get_xml())
661 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
662 sigs.appendChild(sig_ele)
664 self.xml = doc.toxml("utf-8")
667 # Split the issuer GID into multiple certificates if it's a chain
668 chain = GID(filename=self.issuer_gid)
671 gid_files.append(chain.save_to_random_tmp_file(False))
672 if chain.get_parent():
673 chain = chain.get_parent()
678 # Call out to xmlsec1 to sign it
679 ref = 'Sig_{}'.format(self.get_refid())
680 filename = self.save_to_random_tmp_file()
681 xmlsec1 = self.get_xmlsec1_path()
683 raise Exception("Could not locate required 'xmlsec1' program")
684 command = '{} --sign --node-id "{}" --privkey-pem {},{} {}' \
685 .format(xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
686 signed = os.popen(command).read()
689 for gid_file in gid_files:
699 # Retrieve the attributes of the credential from the XML.
700 # This is automatically called by the various get_* methods of
701 # this class and should not need to be called explicitly.
709 doc = parseString(self.xml)
710 except ExpatError as e:
711 raise CredentialNotVerifiable("Malformed credential")
712 doc = parseString(self.xml)
714 signed_cred = doc.getElementsByTagName("signed-credential")
716 # Is this a signed-cred or just a cred?
717 if len(signed_cred) > 0:
718 creds = signed_cred[0].getElementsByTagName("credential")
719 signatures = signed_cred[0].getElementsByTagName("signatures")
720 if len(signatures) > 0:
721 sigs = signatures[0].getElementsByTagName("Signature")
723 creds = doc.getElementsByTagName("credential")
725 if creds is None or len(creds) == 0:
726 # malformed cred file
727 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
729 # Just take the first cred if there are more than one
732 self.set_refid(cred.getAttribute("xml:id"))
733 self.set_expiration(utcparse(getTextNode(cred, "expires")))
734 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
735 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
738 ## This code until the end of function rewritten by Aaron Helsinger
741 priv_nodes = cred.getElementsByTagName("privileges")
742 if len(priv_nodes) > 0:
743 privs = priv_nodes[0]
744 for priv in privs.getElementsByTagName("privilege"):
745 kind = getTextNode(priv, "name")
746 deleg = str2bool(getTextNode(priv, "can_delegate"))
748 # Convert * into the default privileges for the credential's type
749 # Each inherits the delegatability from the * above
750 _ , type = urn_to_hrn(self.gidObject.get_urn())
751 rl = determine_rights(type, self.gidObject.get_urn())
756 rlist.add(Right(kind.strip(), deleg))
757 self.set_privileges(rlist)
761 parent = cred.getElementsByTagName("parent")
763 parent_doc = parent[0].getElementsByTagName("credential")[0]
764 parent_xml = parent_doc.toxml("utf-8")
765 if parent_xml is None or parent_xml.strip() == "":
766 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
767 self.parent = Credential(string=parent_xml)
770 # Assign the signatures to the credentials
772 Sig = Signature(string=sig.toxml("utf-8"))
774 for cur_cred in self.get_credential_list():
775 if cur_cred.get_refid() == Sig.get_refid():
776 cur_cred.set_signature(Sig)
781 # trusted_certs: A list of trusted GID filenames (not GID objects!)
782 # Chaining is not supported within the GIDs by xmlsec1.
784 # trusted_certs_required: Should usually be true. Set False means an
785 # empty list of trusted_certs would still let this method pass.
786 # It just skips xmlsec1 verification et al. Only used by some utils
789 # . All of the signatures are valid and that the issuers trace back
790 # to trusted roots (performed by xmlsec1)
791 # . The XML matches the credential schema
792 # . That the issuer of the credential is the authority in the target's urn
793 # . In the case of a delegated credential, this must be true of the root
794 # . That all of the gids presented in the credential are valid
795 # . Including verifying GID chains, and includ the issuer
796 # . The credential is not expired
798 # -- For Delegates (credentials with parents)
799 # . The privileges must be a subset of the parent credentials
800 # . The privileges must have "can_delegate" set for each delegated privilege
801 # . The target gid must be the same between child and parents
802 # . The expiry time on the child must be no later than the parent
803 # . The signer of the child must be the owner of the parent
805 # -- Verify does *NOT*
806 # . ensure that an xmlrpc client's gid matches a credential gid, that
807 # must be done elsewhere
809 # @param trusted_certs: The certificates of trusted CA certificates
810 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
814 # validate against RelaxNG schema
816 if schema and os.path.exists(schema):
817 tree = etree.parse(StringIO(self.xml))
818 schema_doc = etree.parse(schema)
819 xmlschema = etree.XMLSchema(schema_doc)
820 if not xmlschema.validate(tree):
821 error = xmlschema.error_log.last_error
822 message = "{}: {} (line {})".format(self.pretty_cred(),
823 error.message, error.line)
824 raise CredentialNotVerifiable(message)
826 if trusted_certs_required and trusted_certs is None:
829 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
830 trusted_cert_objects = []
831 ok_trusted_certs = []
832 # If caller explicitly passed in None that means skip cert chain validation.
833 # Strange and not typical
834 if trusted_certs is not None:
835 for f in trusted_certs:
837 # Failures here include unreadable files
839 trusted_cert_objects.append(GID(filename=f))
840 ok_trusted_certs.append(f)
841 except Exception as exc:
842 logger.error("Failed to load trusted cert from {}: {}".format(f, exc))
843 trusted_certs = ok_trusted_certs
845 # make sure it is not expired
846 if self.get_expiration() < datetime.datetime.utcnow():
847 raise CredentialNotVerifiable("Credential {} expired at {}" \
848 .format(self.pretty_cred(),
849 self.expiration.strftime(SFATIME_FORMAT)))
851 # Verify the signatures
852 filename = self.save_to_random_tmp_file()
854 # If caller explicitly passed in None that means skip cert chain validation.
855 # - Strange and not typical
856 if trusted_certs is not None:
857 # Verify the caller and object gids of this cred and of its parents
858 for cur_cred in self.get_credential_list():
859 # check both the caller and the subject
860 for gid in cur_cred.get_gid_object(), cur_cred.get_gid_caller():
861 logger.debug("Credential.verify: verifying chain {}"
862 .format(gid.pretty_cert()))
863 logger.debug("Credential.verify: against trusted {}"
864 .format(" ".join(trusted_certs)))
865 gid.verify_chain(trusted_cert_objects)
868 refs.append("Sig_{}".format(self.get_refid()))
870 parentRefs = self.updateRefID()
871 for ref in parentRefs:
872 refs.append("Sig_{}".format(ref))
875 # If caller explicitly passed in None that means skip xmlsec1 validation.
876 # Strange and not typical
877 if trusted_certs is None:
881 # up to fedora20 we used os.popen and checked that the output begins with OK
882 # turns out, with fedora21, there is extra input before this 'OK' thing
883 # looks like we're better off just using the exit code - that's what it is made for
884 #cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
885 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
886 # format(self.xmlsec_path, ref, cert_args, filename)
887 xmlsec1 = self.get_xmlsec1_path()
889 raise Exception("Could not locate required 'xmlsec1' program")
890 command = [ xmlsec1, '--verify', '--node-id', ref ]
891 for trusted in trusted_certs:
892 command += ["--trusted-pem", trusted ]
893 command += [ filename ]
894 logger.debug("Running " + " ".join(command))
896 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
897 logger.debug("xmlsec command returned {}".format(verified))
898 if "OK\n" not in verified:
899 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
900 except subprocess.CalledProcessError as e:
902 # xmlsec errors have a msg= which is the interesting bit.
903 mstart = verified.find("msg=")
905 if mstart > -1 and len(verified) > 4:
907 mend = verified.find('\\', mstart)
908 msg = verified[mstart:mend]
909 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
910 raise CredentialNotVerifiable("xmlsec1 error verifying cred {} using Signature ID {}: {}"\
911 .format(self.pretty_cred(), ref, msg))
914 # Verify the parents (delegation)
916 self.verify_parent(self.parent)
918 # Make sure the issuer is the target's authority, and is
920 self.verify_issuer(trusted_cert_objects)
924 # Creates a list of the credential and its parents, with the root
925 # (original delegated credential) as the last item in the list
926 def get_credential_list(self):
930 list.append(cur_cred)
932 cur_cred = cur_cred.parent
938 # Make sure the credential's target gid (a) was signed by or (b)
939 # is the same as the entity that signed the original credential,
940 # or (c) is an authority over the target's namespace.
941 # Also ensure that the credential issuer / signer itself has a valid
942 # GID signature chain (signed by an authority with namespace rights).
943 def verify_issuer(self, trusted_gids):
944 root_cred = self.get_credential_list()[-1]
945 root_target_gid = root_cred.get_gid_object()
946 if root_cred.get_signature() is None:
948 raise CredentialNotVerifiable("Could not verify credential owned by {} for object {}. "
949 "Cred has no signature" \
950 .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
952 root_cred_signer = root_cred.get_signature().get_issuer_gid()
955 # Allow non authority to sign target and cred about target.
957 # Why do we need to allow non authorities to sign?
958 # If in the target gid validation step we correctly
959 # checked that the target is only signed by an authority,
960 # then this is just a special case of case 3.
961 # This short-circuit is the common case currently -
962 # and cause GID validation doesn't check 'authority',
963 # this allows users to generate valid slice credentials.
964 if root_target_gid.is_signed_by_cert(root_cred_signer):
965 # cred signer matches target signer, return success
969 # Allow someone to sign credential about themeselves. Used?
970 # If not, remove this.
971 #root_target_gid_str = root_target_gid.save_to_string()
972 #root_cred_signer_str = root_cred_signer.save_to_string()
973 #if root_target_gid_str == root_cred_signer_str:
974 # # cred signer is target, return success
979 # root_cred_signer is not the target_gid
980 # So this is a different gid that we have not verified.
981 # xmlsec1 verified the cert chain on this already, but
982 # it hasn't verified that the gid meets the HRN namespace
984 # Below we'll ensure that it is an authority.
985 # But we haven't verified that it is _signed by_ an authority
986 # We also don't know if xmlsec1 requires that cert signers
989 # Note that if verify() gave us no trusted_gids then this
990 # call will fail. So skip it if we have no trusted_gids
991 if trusted_gids and len(trusted_gids) > 0:
992 root_cred_signer.verify_chain(trusted_gids)
994 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
995 "No trusted gids. Skipping that check.")
997 # See if the signer is an authority over the domain of the target.
998 # There are multiple types of authority - accept them all here
999 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1000 root_cred_signer_type = root_cred_signer.get_type()
1001 if root_cred_signer_type.find('authority') == 0:
1002 #logger.debug('Cred signer is an authority')
1003 # signer is an authority, see if target is in authority's domain
1004 signerhrn = root_cred_signer.get_hrn()
1005 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1008 # We've required that the credential be signed by an authority
1009 # for that domain. Reasonable and probably correct.
1010 # A looser model would also allow the signer to be an authority
1011 # in my control framework - eg My CA or CH. Even if it is not
1012 # the CH that issued these, eg, user credentials.
1014 # Give up, credential does not pass issuer verification
1016 raise CredentialNotVerifiable(
1017 "Could not verify credential owned by {} for object {}. "
1018 "Cred signer {} not the trusted authority for Cred target {}"
1019 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1020 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1023 # -- For Delegates (credentials with parents) verify that:
1024 # . The privileges must be a subset of the parent credentials
1025 # . The privileges must have "can_delegate" set for each delegated privilege
1026 # . The target gid must be the same between child and parents
1027 # . The expiry time on the child must be no later than the parent
1028 # . The signer of the child must be the owner of the parent
1029 def verify_parent(self, parent_cred):
1030 # make sure the rights given to the child are a subset of the
1031 # parents rights (and check delegate bits)
1032 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1034 "Parent cred {} (ref {}) rights {} "
1035 " not superset of delegated cred {} (ref {}) rights {}"
1036 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1037 parent_cred.get_privileges().pretty_rights(),
1038 self.pretty_cred(), self.get_refid(),
1039 self.get_privileges().pretty_rights()))
1040 logger.error(message)
1041 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1042 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1043 raise ChildRightsNotSubsetOfParent(message)
1045 # make sure my target gid is the same as the parent's
1046 if not parent_cred.get_gid_object().save_to_string() == \
1047 self.get_gid_object().save_to_string():
1049 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1050 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1051 logger.error(message)
1052 logger.error("parent details {}".format(parent_cred.save_to_string()))
1053 logger.error("self details {}".format(self.save_to_string()))
1054 raise CredentialNotVerifiable(message)
1056 # make sure my expiry time is <= my parent's
1057 if not parent_cred.get_expiration() >= self.get_expiration():
1058 raise CredentialNotVerifiable(
1059 "Delegated credential {} expires after parent {}"
1060 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1062 # make sure my signer is the parent's caller
1063 if not parent_cred.get_gid_caller().save_to_string(False) == \
1064 self.get_signature().get_issuer_gid().save_to_string(False):
1065 message = "Delegated credential {} not signed by parent {}'s caller"\
1066 .format(self.pretty_cred(), parent_cred.pretty_cred())
1067 logger.error(message)
1068 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cert()))
1069 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1070 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_crert()))
1071 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1072 raise CredentialNotVerifiable(message)
1075 if parent_cred.parent:
1076 parent_cred.verify_parent(parent_cred.parent)
1079 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1081 Return a delegated copy of this credential, delegated to the
1082 specified gid's user.
1084 # get the gid of the object we are delegating
1085 object_gid = self.get_gid_object()
1086 object_hrn = object_gid.get_hrn()
1088 # the hrn of the user who will be delegated to
1089 delegee_gid = GID(filename=delegee_gidfile)
1090 delegee_hrn = delegee_gid.get_hrn()
1092 #user_key = Keypair(filename=keyfile)
1093 #user_hrn = self.get_gid_caller().get_hrn()
1094 subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1095 dcred = Credential(subject=subject_string)
1096 dcred.set_gid_caller(delegee_gid)
1097 dcred.set_gid_object(object_gid)
1098 dcred.set_parent(self)
1099 dcred.set_expiration(self.get_expiration())
1100 dcred.set_privileges(self.get_privileges())
1101 dcred.get_privileges().delegate_all_privileges(True)
1102 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1103 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1110 def get_filename(self):
1111 return getattr(self,'filename',None)
1113 def actual_caller_hrn(self):
1115 a helper method used by some API calls like e.g. Allocate
1116 to try and find out who really is the original caller
1118 This admittedly is a bit of a hack, please USE IN LAST RESORT
1120 This code uses a heuristic to identify a delegated credential
1122 A first known restriction if for traffic that gets through a
1123 slice manager in this case the hrn reported is the one from
1124 the last SM in the call graph which is not at all what is
1128 caller_hrn, caller_type = urn_to_hrn(self.get_gid_caller().get_urn())
1129 issuer_hrn, issuer_type = urn_to_hrn(self.get_signature().get_issuer_gid().get_urn())
1130 subject_hrn = self.get_gid_object().get_hrn()
1131 # if the caller is a user and the issuer is not
1132 # it's probably the former
1133 if caller_type == "user" and issuer_type != "user":
1134 actual_caller_hrn = caller_hrn
1135 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1136 # this seems to be a 'regular' credential
1137 elif caller_hrn.startswith(issuer_hrn):
1138 actual_caller_hrn = caller_hrn
1139 # else this looks like a delegated credential, and the real caller is the issuer
1141 actual_caller_hrn = issuer_hrn
1142 logger.info("actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1143 .format(caller_hrn, issuer_hrn, actual_caller_hrn))
1144 return actual_caller_hrn
1147 # Dump the contents of a credential to stdout in human-readable format
1149 # @param dump_parents If true, also dump the parent certificates
1150 def dump(self, *args, **kwargs):
1151 print(self.dump_string(*args, **kwargs))
1153 # SFA code ignores show_xml and disables printing the cred xml
1154 def dump_string(self, dump_parents=False, show_xml=False):
1156 result += "CREDENTIAL {}\n".format(self.pretty_subject())
1157 filename=self.get_filename()
1158 if filename: result += "Filename {}\n".format(filename)
1159 privileges = self.get_privileges()
1161 result += " privs: {}\n".format(privileges.save_to_string())
1163 result += " privs: \n"
1164 gidCaller = self.get_gid_caller()
1166 result += " gidCaller:\n"
1167 result += gidCaller.dump_string(8, dump_parents)
1169 if self.get_signature():
1170 result += " gidIssuer:\n"
1171 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1174 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1176 gidObject = self.get_gid_object()
1178 result += " gidObject:\n"
1179 result += gidObject.dump_string(8, dump_parents)
1181 if self.parent and dump_parents:
1182 result += "\nPARENT"
1183 result += self.parent.dump_string(True)
1185 if show_xml and HAVELXML:
1187 tree = etree.parse(StringIO(self.xml))
1188 aside = etree.tostring(tree, pretty_print=True)
1189 result += "\nXML:\n\n"
1191 result += "\nEnd XML\n"
1194 print("exc. Credential.dump_string / XML")
1195 traceback.print_exc()