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
35 from tempfile import mkstemp
36 from xml.dom.minidom import Document, parseString
38 from sfa.util.py23 import PY3, StringType, StringIO
42 from lxml import etree
47 from xml.parsers.expat import ExpatError
49 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
50 from sfa.util.sfalogging import logger
51 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
52 from sfa.trust.rights import Right, Rights, determine_rights
53 from sfa.trust.gid import GID
54 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
57 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
61 # . make privs match between PG and PL
62 # . Need to add support for other types of credentials, e.g. tickets
63 # . add namespaces to signed-credential element?
67 <Signature xml:id="Sig_{refid}" xmlns="http://www.w3.org/2000/09/xmldsig#">
69 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
70 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
71 <Reference URI="#{refid}">
73 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
75 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
76 <DigestValue></DigestValue>
92 # Convert a string into a bool
93 # used to convert an xsd:boolean to a Python boolean
97 if str.lower() in ('true', '1'):
103 # Utility function to get the text of an XML element
105 def getTextNode(element, subele):
106 sub = element.getElementsByTagName(subele)[0]
107 if len(sub.childNodes) > 0:
108 return sub.childNodes[0].nodeValue
113 # Utility function to set the text of an XML element
114 # It creates the element, adds the text to it,
115 # and then appends it to the parent.
118 def append_sub(doc, parent, element, text):
119 ele = doc.createElement(element)
120 ele.appendChild(doc.createTextNode(text))
121 parent.appendChild(ele)
124 # Signature contains information about an xmlsec1 signature
125 # for a signed-credential
129 class Signature(object):
131 def __init__(self, string=None):
133 self.issuer_gid = None
149 def set_refid(self, id):
152 def get_issuer_gid(self):
157 def set_issuer_gid(self, gid):
161 # Helper function to pull characters off the front of a string if
163 def remove_prefix(text, prefix):
164 if text and prefix and text.startswith(prefix):
165 return text[len(prefix):]
169 doc = parseString(self.xml)
170 except ExpatError as e:
171 logger.log_exc("Failed to parse credential, {}".format(self.xml))
173 sig = doc.getElementsByTagName("Signature")[0]
174 # This code until the end of function rewritten by Aaron Helsinger
175 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
176 # The xml:id tag is optional, and could be in a
177 # Reference xml:id or Reference UID sub element instead
178 if not ref_id or ref_id == '':
179 reference = sig.getElementsByTagName('Reference')[0]
180 ref_id = remove_prefix(
181 reference.getAttribute('xml:id').strip(), "Sig_")
182 if not ref_id or ref_id == '':
183 ref_id = remove_prefix(
184 reference.getAttribute('URI').strip(), "#")
185 self.set_refid(ref_id)
186 keyinfos = sig.getElementsByTagName("X509Data")
188 for keyinfo in keyinfos:
189 certs = keyinfo.getElementsByTagName("X509Certificate")
191 if len(cert.childNodes) > 0:
192 szgid = cert.childNodes[0].nodeValue
193 szgid = szgid.strip()
194 szgid = "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----".format(
201 raise CredentialNotVerifiable(
202 "Malformed XML: No certificate found in signature")
203 self.set_issuer_gid(GID(string=gids))
206 self.xml = signature_format.format(refid=self.get_refid())
209 # A credential provides a caller gid with privileges to an object gid.
210 # A signed credential is signed by the object's authority.
212 # Credentials are encoded in one of two ways.
213 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
214 # The new credentials are placed in signed XML.
217 # In general, a signed credential obtained externally should
218 # not be changed else the signature is no longer valid. So, once
219 # you have loaded an existing signed credential, do not call encode() or
223 def filter_creds_by_caller(creds, caller_hrn_list):
225 Returns a list of creds who's gid caller matches the
228 if not isinstance(creds, list):
230 if not isinstance(caller_hrn_list, list):
231 caller_hrn_list = [caller_hrn_list]
235 tmp_cred = Credential(string=cred)
236 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
238 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
239 caller_creds.append(cred)
245 class Credential(object):
247 SFA_CREDENTIAL_TYPE = "geni_sfa"
250 # Create a Credential object
252 # @param create If true, create a blank x509 certificate
253 # @param subject If subject!=None, create an x509 cert with the subject name
254 # @param string If string!=None, load the credential from the string
255 # @param filename If filename!=None, load the credential from the file
256 # FIXME: create and subject are ignored!
257 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
258 self.gidCaller = None
259 self.gidObject = None
260 self.expiration = None
261 self.privileges = None
262 self.issuer_privkey = None
263 self.issuer_gid = None
264 self.issuer_pubkey = None
266 self.signature = None
269 self.type = Credential.SFA_CREDENTIAL_TYPE
273 if isinstance(cred, StringType):
275 self.type = Credential.SFA_CREDENTIAL_TYPE
277 elif isinstance(cred, dict):
278 string = cred['geni_value']
279 self.type = cred['geni_type']
280 self.version = cred['geni_version']
282 if string or filename:
286 with open(filename) as infile:
289 # if this is a legacy credential, write error and bail out
290 if isinstance(str, StringType) and str.strip().startswith("-----"):
292 "Legacy credentials not supported any more - giving up with {}...".format(str[:10]))
297 # not strictly necessary but won't hurt either
298 self.get_xmlsec1_path()
301 def get_xmlsec1_path():
302 if not getattr(Credential, 'xmlsec1_path', None):
303 # Find a xmlsec1 binary path
304 Credential.xmlsec1_path = ''
305 paths = ['/usr/bin', '/usr/local/bin',
306 '/bin', '/opt/bin', '/opt/local/bin']
308 paths += os.getenv('PATH').split(':')
312 xmlsec1 = os.path.join(path, 'xmlsec1')
313 if os.path.isfile(xmlsec1):
314 Credential.xmlsec1_path = xmlsec1
316 if not Credential.xmlsec1_path:
318 "Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
319 return Credential.xmlsec1_path
321 def get_subject(self):
322 if not self.gidObject:
324 return self.gidObject.get_subject()
326 def pretty_subject(self):
328 if not self.gidObject:
331 subject = self.gidObject.pretty_cert()
334 # sounds like this should be __repr__ instead ??
335 def pretty_cred(self):
336 if not self.gidObject:
338 obj = self.gidObject.pretty_cert()
339 caller = self.gidCaller.pretty_cert()
340 exp = self.get_expiration()
341 # Summarize the rights too? The issuer?
342 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
344 def get_signature(self):
345 if not self.signature:
347 return self.signature
349 def set_signature(self, sig):
353 # Need the issuer's private key and name
354 # @param key Keypair object containing the private key of the issuer
355 # @param gid GID of the issuing authority
357 def set_issuer_keys(self, privkey, gid):
358 self.issuer_privkey = privkey
359 self.issuer_gid = gid
362 # Set this credential's parent
363 def set_parent(self, cred):
368 # set the GID of the caller
370 # @param gid GID object of the caller
372 def set_gid_caller(self, gid):
374 # gid origin caller is the caller's gid by default
375 self.gidOriginCaller = gid
378 # get the GID of the object
380 def get_gid_caller(self):
381 if not self.gidCaller:
383 return self.gidCaller
386 # set the GID of the object
388 # @param gid GID object of the object
390 def set_gid_object(self, gid):
394 # get the GID of the object
396 def get_gid_object(self):
397 if not self.gidObject:
399 return self.gidObject
402 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
404 def set_expiration(self, expiration):
405 expiration_datetime = utcparse(expiration)
406 if expiration_datetime is not None:
407 self.expiration = expiration_datetime
410 "unexpected input {} in Credential.set_expiration".format(expiration))
413 # get the lifetime of the credential (always in datetime format)
415 def get_expiration(self):
416 if not self.expiration:
418 # at this point self.expiration is normalized as a datetime - DON'T
419 # call utcparse again
420 return self.expiration
425 # @param privs either a comma-separated list of privileges of a Rights object
427 def set_privileges(self, privs):
428 if isinstance(privs, str):
429 self.privileges = Rights(string=privs)
431 self.privileges = privs
434 # return the privileges as a Rights object
436 def get_privileges(self):
437 if not self.privileges:
439 return self.privileges
442 # determine whether the credential allows a particular operation to be
445 # @param op_name string specifying name of operation ("lookup", "update", etc)
447 def can_perform(self, op_name):
448 rights = self.get_privileges()
453 return rights.can_perform(op_name)
456 # Encode the attributes of the credential into an XML string
457 # This should be done immediately before signing the credential.
459 # In general, a signed credential obtained externally should
460 # not be changed else the signature is no longer valid. So, once
461 # you have loaded an existing signed credential, do not call encode() or
465 # Create the XML document
467 signed_cred = doc.createElement("signed-credential")
470 # Note that credential/policy.xsd are really the PG schemas
472 # Note that delegation of credentials between the 2 only really works
473 # cause those schemas are identical.
474 # Also note these PG schemas talk about PG tickets and CM policies.
475 signed_cred.setAttribute(
476 "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
477 # FIXME: See v2 schema at
478 # www.geni.net/resources/credential/2/credential.xsd
479 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation",
480 "http://www.planet-lab.org/resources/sfa/credential.xsd")
481 signed_cred.setAttribute(
482 "xsi:schemaLocation", "http://www.planet-lab.org/resources/sfa/ext/policy/1 http://www.planet-lab.org/resources/sfa/ext/policy/1/policy.xsd")
484 # PG says for those last 2:
485 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
486 # 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")
488 doc.appendChild(signed_cred)
490 # Fill in the <credential> bit
491 cred = doc.createElement("credential")
492 cred.setAttribute("xml:id", self.get_refid())
493 signed_cred.appendChild(cred)
494 append_sub(doc, cred, "type", "privilege")
495 append_sub(doc, cred, "serial", "8")
496 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
497 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
498 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
499 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
500 append_sub(doc, cred, "uuid", "")
501 if not self.expiration:
502 logger.debug("Creating credential valid for {} s".format(
503 DEFAULT_CREDENTIAL_LIFETIME))
504 self.set_expiration(datetime.datetime.utcnow(
505 ) + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
506 self.expiration = self.expiration.replace(microsecond=0)
507 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
508 # TZ aware. Make sure it is UTC - by Aaron Helsinger
509 self.expiration = self.expiration.astimezone(tz.tzutc())
510 append_sub(doc, cred, "expires",
511 self.expiration.strftime(SFATIME_FORMAT))
512 privileges = doc.createElement("privileges")
513 cred.appendChild(privileges)
516 rights = self.get_privileges()
517 for right in rights.rights:
518 priv = doc.createElement("privilege")
519 append_sub(doc, priv, "name", right.kind)
520 append_sub(doc, priv, "can_delegate",
521 str(right.delegate).lower())
522 privileges.appendChild(priv)
524 # Add the parent credential if it exists
526 sdoc = parseString(self.parent.get_xml())
527 # If the root node is a signed-credential (it should be), then
528 # get all its attributes and attach those to our signed_cred
530 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
531 # and we need to include those again here or else their signature
532 # no longer matches on the credential.
533 # We expect three of these, but here we copy them all:
534 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
535 # and from PG (PL is equivalent, as shown above):
536 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
537 # 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")
540 # PL now also declares these, with different URLs, so
541 # the code notices those attributes already existed with
542 # different values, and complains.
543 # This happens regularly on delegation now that PG and
544 # PL both declare the namespace with different URLs.
545 # If the content ever differs this is a problem,
546 # but for now it works - different URLs (values in the attributes)
547 # but the same actual schema, so using the PG schema
548 # on delegated-to-PL credentials works fine.
550 # Note: you could also not copy attributes
551 # which already exist. It appears that both PG and PL
552 # will actually validate a slicecred with a parent
553 # signed using PG namespaces and a child signed with PL
554 # namespaces over the whole thing. But I don't know
555 # if that is a bug in xmlsec1, an accident since
556 # the contents of the schemas are the same,
557 # or something else, but it seems odd. And this works.
558 parentRoot = sdoc.documentElement
559 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
560 for attrIx in range(0, parentRoot.attributes.length):
561 attr = parentRoot.attributes.item(attrIx)
562 # returns the old attribute of same name that was
564 # Below throws InUse exception if we forgot to clone the
566 oldAttr = signed_cred.setAttributeNode(
567 attr.cloneNode(True))
568 if oldAttr and oldAttr.value != attr.value:
569 msg = "Delegating cred from owner {} to {} over {}:\n"
570 "- Replaced attribute {} value '{}' with '{}'"\
571 .format(self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(),
572 self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
574 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: {}".format(msg))
576 p_cred = doc.importNode(
577 sdoc.getElementsByTagName("credential")[0], True)
578 p = doc.createElement("parent")
579 p.appendChild(p_cred)
581 # done handling parent credential
583 # Create the <signatures> tag
584 signatures = doc.createElement("signatures")
585 signed_cred.appendChild(signatures)
587 # Add any parent signatures
589 for cur_cred in self.get_credential_list()[1:]:
590 sdoc = parseString(cur_cred.get_signature().get_xml())
591 ele = doc.importNode(
592 sdoc.getElementsByTagName("Signature")[0], True)
593 signatures.appendChild(ele)
595 # Get the finished product
596 self.xml = doc.toxml("utf-8")
598 def save_to_random_tmp_file(self):
599 fp, filename = mkstemp(suffix='cred', text=True)
600 fp = os.fdopen(fp, "w")
601 self.save_to_file(filename, save_parents=True, filep=fp)
604 def save_to_file(self, filename, save_parents=True, filep=None):
610 f = open(filename, "w")
611 if PY3 and isinstance(self.xml, bytes):
612 self.xml = self.xml.decode()
616 def save_to_string(self, save_parents=True):
619 if PY3 and isinstance(self.xml, bytes):
620 self.xml = self.xml.decode()
628 def set_refid(self, rid):
632 # Figure out what refids exist, and update this credential's id
633 # so that it doesn't clobber the others. Returns the refids of
636 def updateRefID(self):
638 self.set_refid('ref0')
643 next_cred = self.parent
645 refs.append(next_cred.get_refid())
647 next_cred = next_cred.parent
651 # Find a unique refid for this credential
652 rid = self.get_refid()
655 rid = "ref{}".format(val + 1)
660 # Return the set of parent credential ref ids
669 # Sign the XML file created by encode()
672 # In general, a signed credential obtained externally should
673 # not be changed else the signature is no longer valid. So, once
674 # you have loaded an existing signed credential, do not call encode() or
678 if not self.issuer_privkey:
679 logger.warn("Cannot sign credential (no private key)")
681 if not self.issuer_gid:
682 logger.warn("Cannot sign credential (no issuer gid)")
684 doc = parseString(self.get_xml())
685 sigs = doc.getElementsByTagName("signatures")[0]
687 # Create the signature template to be signed
688 signature = Signature()
689 signature.set_refid(self.get_refid())
690 sdoc = parseString(signature.get_xml())
691 sig_ele = doc.importNode(
692 sdoc.getElementsByTagName("Signature")[0], True)
693 sigs.appendChild(sig_ele)
695 self.xml = doc.toxml("utf-8")
697 # Split the issuer GID into multiple certificates if it's a chain
698 chain = GID(filename=self.issuer_gid)
701 gid_files.append(chain.save_to_random_tmp_file(False))
702 if chain.get_parent():
703 chain = chain.get_parent()
707 # Call out to xmlsec1 to sign it
708 ref = 'Sig_{}'.format(self.get_refid())
709 filename = self.save_to_random_tmp_file()
710 xmlsec1 = self.get_xmlsec1_path()
712 raise Exception("Could not locate required 'xmlsec1' program")
713 command = '{} --sign --node-id "{}" --privkey-pem {},{} {}' \
714 .format(xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
715 signed = os.popen(command).read()
718 for gid_file in gid_files:
727 # Retrieve the attributes of the credential from the XML.
728 # This is automatically called by the various get_* methods of
729 # this class and should not need to be called explicitly.
737 doc = parseString(self.xml)
738 except ExpatError as e:
739 raise CredentialNotVerifiable("Malformed credential")
740 doc = parseString(self.xml)
742 signed_cred = doc.getElementsByTagName("signed-credential")
744 # Is this a signed-cred or just a cred?
745 if len(signed_cred) > 0:
746 creds = signed_cred[0].getElementsByTagName("credential")
747 signatures = signed_cred[0].getElementsByTagName("signatures")
748 if len(signatures) > 0:
749 sigs = signatures[0].getElementsByTagName("Signature")
751 creds = doc.getElementsByTagName("credential")
753 if creds is None or len(creds) == 0:
754 # malformed cred file
755 raise CredentialNotVerifiable(
756 "Malformed XML: No credential tag found")
758 # Just take the first cred if there are more than one
761 self.set_refid(cred.getAttribute("xml:id"))
762 self.set_expiration(utcparse(getTextNode(cred, "expires")))
763 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
764 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
766 # This code until the end of function rewritten by Aaron Helsinger
769 priv_nodes = cred.getElementsByTagName("privileges")
770 if len(priv_nodes) > 0:
771 privs = priv_nodes[0]
772 for priv in privs.getElementsByTagName("privilege"):
773 kind = getTextNode(priv, "name")
774 deleg = str2bool(getTextNode(priv, "can_delegate"))
776 # Convert * into the default privileges for the credential's type
777 # Each inherits the delegatability from the * above
778 _, type = urn_to_hrn(self.gidObject.get_urn())
779 rl = determine_rights(type, self.gidObject.get_urn())
784 rlist.add(Right(kind.strip(), deleg))
785 self.set_privileges(rlist)
788 parent = cred.getElementsByTagName("parent")
790 parent_doc = parent[0].getElementsByTagName("credential")[0]
791 parent_xml = parent_doc.toxml("utf-8")
792 if parent_xml is None or parent_xml.strip() == "":
793 raise CredentialNotVerifiable(
794 "Malformed XML: Had parent tag but it is empty")
795 self.parent = Credential(string=parent_xml)
798 # Assign the signatures to the credentials
800 Sig = Signature(string=sig.toxml("utf-8"))
802 for cur_cred in self.get_credential_list():
803 if cur_cred.get_refid() == Sig.get_refid():
804 cur_cred.set_signature(Sig)
808 # trusted_certs: A list of trusted GID filenames (not GID objects!)
809 # Chaining is not supported within the GIDs by xmlsec1.
811 # trusted_certs_required: Should usually be true. Set False means an
812 # empty list of trusted_certs would still let this method pass.
813 # It just skips xmlsec1 verification et al. Only used by some utils
816 # . All of the signatures are valid and that the issuers trace back
817 # to trusted roots (performed by xmlsec1)
818 # . The XML matches the credential schema
819 # . That the issuer of the credential is the authority in the target's urn
820 # . In the case of a delegated credential, this must be true of the root
821 # . That all of the gids presented in the credential are valid
822 # . Including verifying GID chains, and includ the issuer
823 # . The credential is not expired
825 # -- For Delegates (credentials with parents)
826 # . The privileges must be a subset of the parent credentials
827 # . The privileges must have "can_delegate" set for each delegated privilege
828 # . The target gid must be the same between child and parents
829 # . The expiry time on the child must be no later than the parent
830 # . The signer of the child must be the owner of the parent
832 # -- Verify does *NOT*
833 # . ensure that an xmlrpc client's gid matches a credential gid, that
834 # must be done elsewhere
836 # @param trusted_certs: The certificates of trusted CA certificates
837 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
841 # validate against RelaxNG schema
843 if schema and os.path.exists(schema):
844 tree = etree.parse(StringIO(self.xml))
845 schema_doc = etree.parse(schema)
846 xmlschema = etree.XMLSchema(schema_doc)
847 if not xmlschema.validate(tree):
848 error = xmlschema.error_log.last_error
849 message = "{}: {} (line {})".format(self.pretty_cred(),
850 error.message, error.line)
851 raise CredentialNotVerifiable(message)
853 if trusted_certs_required and trusted_certs is None:
856 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
857 trusted_cert_objects = []
858 ok_trusted_certs = []
859 # If caller explicitly passed in None that means skip cert chain validation.
860 # Strange and not typical
861 if trusted_certs is not None:
862 for f in trusted_certs:
864 # Failures here include unreadable files
866 trusted_cert_objects.append(GID(filename=f))
867 ok_trusted_certs.append(f)
868 except Exception as exc:
870 "Failed to load trusted cert from {}: {}".format(f, exc))
871 trusted_certs = ok_trusted_certs
873 # make sure it is not expired
874 if self.get_expiration() < datetime.datetime.utcnow():
875 raise CredentialNotVerifiable("Credential {} expired at {}"
876 .format(self.pretty_cred(),
877 self.expiration.strftime(SFATIME_FORMAT)))
879 # Verify the signatures
880 filename = self.save_to_random_tmp_file()
882 # If caller explicitly passed in None that means skip cert chain validation.
883 # - Strange and not typical
884 if trusted_certs is not None:
885 # Verify the caller and object gids of this cred and of its parents
886 for cur_cred in self.get_credential_list():
887 # check both the caller and the subject
888 for gid in cur_cred.get_gid_object(), cur_cred.get_gid_caller():
889 logger.debug("Credential.verify: verifying chain {}"
890 .format(gid.pretty_cert()))
891 logger.debug("Credential.verify: against trusted {}"
892 .format(" ".join(trusted_certs)))
893 gid.verify_chain(trusted_cert_objects)
896 refs.append("Sig_{}".format(self.get_refid()))
898 parentRefs = self.updateRefID()
899 for ref in parentRefs:
900 refs.append("Sig_{}".format(ref))
903 # If caller explicitly passed in None that means skip xmlsec1 validation.
904 # Strange and not typical
905 if trusted_certs is None:
909 # up to fedora20 we used os.popen and checked that the output begins with OK
910 # turns out, with fedora21, there is extra input before this 'OK' thing
911 # looks like we're better off just using the exit code - that's what it is made for
912 #cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
913 # command = '{} --verify --node-id "{}" {} {} 2>&1'.\
914 # format(self.xmlsec_path, ref, cert_args, filename)
915 xmlsec1 = self.get_xmlsec1_path()
917 raise Exception("Could not locate required 'xmlsec1' program")
918 command = [xmlsec1, '--verify', '--node-id', ref]
919 for trusted in trusted_certs:
920 command += ["--trusted-pem", trusted]
921 command += [filename]
922 logger.debug("Running " + " ".join(command))
924 verified = subprocess.check_output(
925 command, stderr=subprocess.STDOUT)
926 logger.debug("xmlsec command returned {}".format(verified))
927 if "OK\n" not in verified:
929 "WARNING: xmlsec1 seemed to return fine but without a OK in its output")
930 except subprocess.CalledProcessError as e:
932 # xmlsec errors have a msg= which is the interesting bit.
933 mstart = verified.find("msg=")
935 if mstart > -1 and len(verified) > 4:
937 mend = verified.find('\\', mstart)
938 msg = verified[mstart:mend]
940 "Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
941 raise CredentialNotVerifiable("xmlsec1 error verifying cred {} using Signature ID {}: {}"
942 .format(self.pretty_cred(), ref, msg))
945 # Verify the parents (delegation)
947 self.verify_parent(self.parent)
949 # Make sure the issuer is the target's authority, and is
951 self.verify_issuer(trusted_cert_objects)
955 # Creates a list of the credential and its parents, with the root
956 # (original delegated credential) as the last item in the list
957 def get_credential_list(self):
961 list.append(cur_cred)
963 cur_cred = cur_cred.parent
969 # Make sure the credential's target gid (a) was signed by or (b)
970 # is the same as the entity that signed the original credential,
971 # or (c) is an authority over the target's namespace.
972 # Also ensure that the credential issuer / signer itself has a valid
973 # GID signature chain (signed by an authority with namespace rights).
974 def verify_issuer(self, trusted_gids):
975 root_cred = self.get_credential_list()[-1]
976 root_target_gid = root_cred.get_gid_object()
977 if root_cred.get_signature() is None:
979 raise CredentialNotVerifiable("Could not verify credential owned by {} for object {}. "
980 "Cred has no signature"
981 .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
983 root_cred_signer = root_cred.get_signature().get_issuer_gid()
986 # Allow non authority to sign target and cred about target.
988 # Why do we need to allow non authorities to sign?
989 # If in the target gid validation step we correctly
990 # checked that the target is only signed by an authority,
991 # then this is just a special case of case 3.
992 # This short-circuit is the common case currently -
993 # and cause GID validation doesn't check 'authority',
994 # this allows users to generate valid slice credentials.
995 if root_target_gid.is_signed_by_cert(root_cred_signer):
996 # cred signer matches target signer, return success
1000 # Allow someone to sign credential about themeselves. Used?
1001 # If not, remove this.
1002 #root_target_gid_str = root_target_gid.save_to_string()
1003 #root_cred_signer_str = root_cred_signer.save_to_string()
1004 # if root_target_gid_str == root_cred_signer_str:
1005 # # cred signer is target, return success
1010 # root_cred_signer is not the target_gid
1011 # So this is a different gid that we have not verified.
1012 # xmlsec1 verified the cert chain on this already, but
1013 # it hasn't verified that the gid meets the HRN namespace
1015 # Below we'll ensure that it is an authority.
1016 # But we haven't verified that it is _signed by_ an authority
1017 # We also don't know if xmlsec1 requires that cert signers
1018 # are marked as CAs.
1020 # Note that if verify() gave us no trusted_gids then this
1021 # call will fail. So skip it if we have no trusted_gids
1022 if trusted_gids and len(trusted_gids) > 0:
1023 root_cred_signer.verify_chain(trusted_gids)
1025 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1026 "No trusted gids. Skipping that check.")
1028 # See if the signer is an authority over the domain of the target.
1029 # There are multiple types of authority - accept them all here
1030 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1031 root_cred_signer_type = root_cred_signer.get_type()
1032 if root_cred_signer_type.find('authority') == 0:
1033 #logger.debug('Cred signer is an authority')
1034 # signer is an authority, see if target is in authority's domain
1035 signerhrn = root_cred_signer.get_hrn()
1036 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1039 # We've required that the credential be signed by an authority
1040 # for that domain. Reasonable and probably correct.
1041 # A looser model would also allow the signer to be an authority
1042 # in my control framework - eg My CA or CH. Even if it is not
1043 # the CH that issued these, eg, user credentials.
1045 # Give up, credential does not pass issuer verification
1047 raise CredentialNotVerifiable(
1048 "Could not verify credential owned by {} for object {}. "
1049 "Cred signer {} not the trusted authority for Cred target {}"
1050 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1051 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1054 # -- For Delegates (credentials with parents) verify that:
1055 # . The privileges must be a subset of the parent credentials
1056 # . The privileges must have "can_delegate" set for each delegated privilege
1057 # . The target gid must be the same between child and parents
1058 # . The expiry time on the child must be no later than the parent
1059 # . The signer of the child must be the owner of the parent
1060 def verify_parent(self, parent_cred):
1061 # make sure the rights given to the child are a subset of the
1062 # parents rights (and check delegate bits)
1063 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1065 "Parent cred {} (ref {}) rights {} "
1066 " not superset of delegated cred {} (ref {}) rights {}"
1067 .format(parent_cred.pretty_cred(), parent_cred.get_refid(),
1068 parent_cred.get_privileges().pretty_rights(),
1069 self.pretty_cred(), self.get_refid(),
1070 self.get_privileges().pretty_rights()))
1071 logger.error(message)
1072 logger.error("parent details {}".format(
1073 parent_cred.get_privileges().save_to_string()))
1074 logger.error("self details {}".format(
1075 self.get_privileges().save_to_string()))
1076 raise ChildRightsNotSubsetOfParent(message)
1078 # make sure my target gid is the same as the parent's
1079 if not parent_cred.get_gid_object().save_to_string() == \
1080 self.get_gid_object().save_to_string():
1082 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1083 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1084 logger.error(message)
1085 logger.error("parent details {}".format(
1086 parent_cred.save_to_string()))
1087 logger.error("self details {}".format(self.save_to_string()))
1088 raise CredentialNotVerifiable(message)
1090 # make sure my expiry time is <= my parent's
1091 if not parent_cred.get_expiration() >= self.get_expiration():
1092 raise CredentialNotVerifiable(
1093 "Delegated credential {} expires after parent {}"
1094 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1096 # make sure my signer is the parent's caller
1097 if not parent_cred.get_gid_caller().save_to_string(False) == \
1098 self.get_signature().get_issuer_gid().save_to_string(False):
1099 message = "Delegated credential {} not signed by parent {}'s caller"\
1100 .format(self.pretty_cred(), parent_cred.pretty_cred())
1101 logger.error(message)
1102 logger.error("compare1 parent {}".format(
1103 parent_cred.get_gid_caller().pretty_cert()))
1104 logger.error("compare1 parent details {}".format(
1105 parent_cred.get_gid_caller().save_to_string()))
1106 logger.error("compare2 self {}".format(
1107 self.get_signature().get_issuer_gid().pretty_crert()))
1108 logger.error("compare2 self details {}".format(
1109 self.get_signature().get_issuer_gid().save_to_string()))
1110 raise CredentialNotVerifiable(message)
1113 if parent_cred.parent:
1114 parent_cred.verify_parent(parent_cred.parent)
1116 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1118 Return a delegated copy of this credential, delegated to the
1119 specified gid's user.
1121 # get the gid of the object we are delegating
1122 object_gid = self.get_gid_object()
1123 object_hrn = object_gid.get_hrn()
1125 # the hrn of the user who will be delegated to
1126 delegee_gid = GID(filename=delegee_gidfile)
1127 delegee_hrn = delegee_gid.get_hrn()
1129 #user_key = Keypair(filename=keyfile)
1130 #user_hrn = self.get_gid_caller().get_hrn()
1131 subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1132 dcred = Credential(subject=subject_string)
1133 dcred.set_gid_caller(delegee_gid)
1134 dcred.set_gid_object(object_gid)
1135 dcred.set_parent(self)
1136 dcred.set_expiration(self.get_expiration())
1137 dcred.set_privileges(self.get_privileges())
1138 dcred.get_privileges().delegate_all_privileges(True)
1139 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1140 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1147 def get_filename(self):
1148 return getattr(self, 'filename', None)
1150 def actual_caller_hrn(self):
1152 a helper method used by some API calls like e.g. Allocate
1153 to try and find out who really is the original caller
1155 This admittedly is a bit of a hack, please USE IN LAST RESORT
1157 This code uses a heuristic to identify a delegated credential
1159 A first known restriction if for traffic that gets through a
1160 slice manager in this case the hrn reported is the one from
1161 the last SM in the call graph which is not at all what is
1165 caller_hrn, caller_type = urn_to_hrn(self.get_gid_caller().get_urn())
1166 issuer_hrn, issuer_type = urn_to_hrn(
1167 self.get_signature().get_issuer_gid().get_urn())
1168 subject_hrn = self.get_gid_object().get_hrn()
1169 # if the caller is a user and the issuer is not
1170 # it's probably the former
1171 if caller_type == "user" and issuer_type != "user":
1172 actual_caller_hrn = caller_hrn
1173 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1174 # this seems to be a 'regular' credential
1175 elif caller_hrn.startswith(issuer_hrn):
1176 actual_caller_hrn = caller_hrn
1177 # else this looks like a delegated credential, and the real caller is
1180 actual_caller_hrn = issuer_hrn
1181 logger.info("actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1182 .format(caller_hrn, issuer_hrn, actual_caller_hrn))
1183 return actual_caller_hrn
1186 # Dump the contents of a credential to stdout in human-readable format
1188 # @param dump_parents If true, also dump the parent certificates
1189 def dump(self, *args, **kwargs):
1190 print(self.dump_string(*args, **kwargs))
1192 # SFA code ignores show_xml and disables printing the cred xml
1193 def dump_string(self, dump_parents=False, show_xml=False):
1195 result += "CREDENTIAL {}\n".format(self.pretty_subject())
1196 filename = self.get_filename()
1198 result += "Filename {}\n".format(filename)
1199 privileges = self.get_privileges()
1201 result += " privs: {}\n".format(privileges.save_to_string())
1203 result += " privs: \n"
1204 gidCaller = self.get_gid_caller()
1206 result += " gidCaller:\n"
1207 result += gidCaller.dump_string(8, dump_parents)
1209 if self.get_signature():
1210 result += " gidIssuer:\n"
1211 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1214 result += " expiration: " + \
1215 self.expiration.strftime(SFATIME_FORMAT) + "\n"
1217 gidObject = self.get_gid_object()
1219 result += " gidObject:\n"
1220 result += gidObject.dump_string(8, dump_parents)
1222 if self.parent and dump_parents:
1223 result += "\nPARENT"
1224 result += self.parent.dump_string(True)
1226 if show_xml and HAVELXML:
1228 tree = etree.parse(StringIO(self.xml))
1229 aside = etree.tostring(tree, pretty_print=True)
1230 result += "\nXML:\n\n"
1232 result += "\nEnd XML\n"
1235 print("exc. Credential.dump_string / XML")
1236 traceback.print_exc()