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 StringType
38 from sfa.util.py23 import 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
95 if str.lower() in ['true','1']:
101 # Utility function to get the text of an XML element
103 def getTextNode(element, subele):
104 sub = element.getElementsByTagName(subele)[0]
105 if len(sub.childNodes) > 0:
106 return sub.childNodes[0].nodeValue
111 # Utility function to set the text of an XML element
112 # It creates the element, adds the text to it,
113 # and then appends it to the parent.
115 def append_sub(doc, parent, element, text):
116 ele = doc.createElement(element)
117 ele.appendChild(doc.createTextNode(text))
118 parent.appendChild(ele)
121 # Signature contains information about an xmlsec1 signature
122 # for a signed-credential
125 class Signature(object):
127 def __init__(self, string=None):
129 self.issuer_gid = None
146 def set_refid(self, id):
149 def get_issuer_gid(self):
154 def set_issuer_gid(self, gid):
158 # Helper function to pull characters off the front of a string if present
159 def remove_prefix(text, prefix):
160 if text and prefix and text.startswith(prefix):
161 return text[len(prefix):]
165 doc = parseString(self.xml)
166 except ExpatError as e:
167 logger.log_exc("Failed to parse credential, {}".format(self.xml))
169 sig = doc.getElementsByTagName("Signature")[0]
170 ## This code until the end of function rewritten by Aaron Helsinger
171 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
172 # The xml:id tag is optional, and could be in a
173 # Reference xml:id or Reference UID sub element instead
174 if not ref_id or ref_id == '':
175 reference = sig.getElementsByTagName('Reference')[0]
176 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
177 if not ref_id or ref_id == '':
178 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
179 self.set_refid(ref_id)
180 keyinfos = sig.getElementsByTagName("X509Data")
182 for keyinfo in keyinfos:
183 certs = keyinfo.getElementsByTagName("X509Certificate")
185 if len(cert.childNodes) > 0:
186 szgid = cert.childNodes[0].nodeValue
187 szgid = szgid.strip()
188 szgid = "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----".format(szgid)
194 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
195 self.set_issuer_gid(GID(string=gids))
198 self.xml = signature_format.format(refid=self.get_refid())
201 # A credential provides a caller gid with privileges to an object gid.
202 # A signed credential is signed by the object's authority.
204 # Credentials are encoded in one of two ways.
205 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
206 # The new credentials are placed in signed XML.
209 # In general, a signed credential obtained externally should
210 # not be changed else the signature is no longer valid. So, once
211 # you have loaded an existing signed credential, do not call encode() or sign() on it.
213 def filter_creds_by_caller(creds, caller_hrn_list):
215 Returns a list of creds who's gid caller matches the
218 if not isinstance(creds, list): creds = [creds]
219 if not isinstance(caller_hrn_list, list):
220 caller_hrn_list = [caller_hrn_list]
224 tmp_cred = Credential(string=cred)
225 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
227 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
228 caller_creds.append(cred)
232 class Credential(object):
234 SFA_CREDENTIAL_TYPE = "geni_sfa"
237 # Create a Credential object
239 # @param create If true, create a blank x509 certificate
240 # @param subject If subject!=None, create an x509 cert with the subject name
241 # @param string If string!=None, load the credential from the string
242 # @param filename If filename!=None, load the credential from the file
243 # FIXME: create and subject are ignored!
244 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
245 self.gidCaller = None
246 self.gidObject = None
247 self.expiration = None
248 self.privileges = None
249 self.issuer_privkey = None
250 self.issuer_gid = None
251 self.issuer_pubkey = None
253 self.signature = None
256 self.type = Credential.SFA_CREDENTIAL_TYPE
260 if isinstance(cred, StringType):
262 self.type = Credential.SFA_CREDENTIAL_TYPE
264 elif isinstance(cred, dict):
265 string = cred['geni_value']
266 self.type = cred['geni_type']
267 self.version = cred['geni_version']
269 if string or filename:
273 with open(filename) as infile:
276 # if this is a legacy credential, write error and bail out
277 if isinstance(str, StringType) and str.strip().startswith("-----"):
278 logger.error("Legacy credentials not supported any more - giving up with {}...".format(str[:10]))
283 # not strictly necessary but won't hurt either
284 self.get_xmlsec1_path()
287 def get_xmlsec1_path():
288 if not getattr(Credential, 'xmlsec1_path', None):
289 # Find a xmlsec1 binary path
290 Credential.xmlsec1_path = ''
291 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
292 try: paths += os.getenv('PATH').split(':')
295 xmlsec1 = os.path.join(path, 'xmlsec1')
296 if os.path.isfile(xmlsec1):
297 Credential.xmlsec1_path = xmlsec1
299 if not Credential.xmlsec1_path:
300 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
301 return Credential.xmlsec1_path
303 def get_subject(self):
304 if not self.gidObject:
306 return self.gidObject.get_subject()
308 def pretty_subject(self):
310 if not self.gidObject:
313 subject = self.gidObject.pretty_cert()
316 # sounds like this should be __repr__ instead ??
317 def pretty_cred(self):
318 if not self.gidObject:
320 obj = self.gidObject.pretty_cert()
321 caller = self.gidCaller.pretty_cert()
322 exp = self.get_expiration()
323 # Summarize the rights too? The issuer?
324 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
326 def get_signature(self):
327 if not self.signature:
329 return self.signature
331 def set_signature(self, sig):
336 # Need the issuer's private key and name
337 # @param key Keypair object containing the private key of the issuer
338 # @param gid GID of the issuing authority
340 def set_issuer_keys(self, privkey, gid):
341 self.issuer_privkey = privkey
342 self.issuer_gid = gid
346 # Set this credential's parent
347 def set_parent(self, cred):
352 # set the GID of the caller
354 # @param gid GID object of the caller
356 def set_gid_caller(self, gid):
358 # gid origin caller is the caller's gid by default
359 self.gidOriginCaller = gid
362 # get the GID of the object
364 def get_gid_caller(self):
365 if not self.gidCaller:
367 return self.gidCaller
370 # set the GID of the object
372 # @param gid GID object of the object
374 def set_gid_object(self, gid):
378 # get the GID of the object
380 def get_gid_object(self):
381 if not self.gidObject:
383 return self.gidObject
386 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
388 def set_expiration(self, expiration):
389 expiration_datetime = utcparse(expiration)
390 if expiration_datetime is not None:
391 self.expiration = expiration_datetime
393 logger.error("unexpected input {} in Credential.set_expiration".format(expiration))
396 # get the lifetime of the credential (always in datetime format)
398 def get_expiration(self):
399 if not self.expiration:
401 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
402 return self.expiration
407 # @param privs either a comma-separated list of privileges of a Rights object
409 def set_privileges(self, privs):
410 if isinstance(privs, str):
411 self.privileges = Rights(string = privs)
413 self.privileges = privs
416 # return the privileges as a Rights object
418 def get_privileges(self):
419 if not self.privileges:
421 return self.privileges
424 # determine whether the credential allows a particular operation to be
427 # @param op_name string specifying name of operation ("lookup", "update", etc)
429 def can_perform(self, op_name):
430 rights = self.get_privileges()
435 return rights.can_perform(op_name)
439 # Encode the attributes of the credential into an XML string
440 # This should be done immediately before signing the credential.
442 # In general, a signed credential obtained externally should
443 # not be changed else the signature is no longer valid. So, once
444 # you have loaded an existing signed credential, do not call encode() or sign() on it.
447 # Create the XML document
449 signed_cred = doc.createElement("signed-credential")
452 # Note that credential/policy.xsd are really the PG schemas
454 # Note that delegation of credentials between the 2 only really works
455 # cause those schemas are identical.
456 # Also note these PG schemas talk about PG tickets and CM policies.
457 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
458 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
459 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
460 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")
462 # PG says for those last 2:
463 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
464 # 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")
466 doc.appendChild(signed_cred)
468 # Fill in the <credential> bit
469 cred = doc.createElement("credential")
470 cred.setAttribute("xml:id", self.get_refid())
471 signed_cred.appendChild(cred)
472 append_sub(doc, cred, "type", "privilege")
473 append_sub(doc, cred, "serial", "8")
474 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
475 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
476 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
477 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
478 append_sub(doc, cred, "uuid", "")
479 if not self.expiration:
480 logger.debug("Creating credential valid for {} s".format(DEFAULT_CREDENTIAL_LIFETIME))
481 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
482 self.expiration = self.expiration.replace(microsecond=0)
483 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
484 # TZ aware. Make sure it is UTC - by Aaron Helsinger
485 self.expiration = self.expiration.astimezone(tz.tzutc())
486 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
487 privileges = doc.createElement("privileges")
488 cred.appendChild(privileges)
491 rights = self.get_privileges()
492 for right in rights.rights:
493 priv = doc.createElement("privilege")
494 append_sub(doc, priv, "name", right.kind)
495 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
496 privileges.appendChild(priv)
498 # Add the parent credential if it exists
500 sdoc = parseString(self.parent.get_xml())
501 # If the root node is a signed-credential (it should be), then
502 # get all its attributes and attach those to our signed_cred
504 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
505 # and we need to include those again here or else their signature
506 # no longer matches on the credential.
507 # We expect three of these, but here we copy them all:
508 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
509 # and from PG (PL is equivalent, as shown above):
510 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
511 # 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")
514 # PL now also declares these, with different URLs, so
515 # the code notices those attributes already existed with
516 # different values, and complains.
517 # This happens regularly on delegation now that PG and
518 # PL both declare the namespace with different URLs.
519 # If the content ever differs this is a problem,
520 # but for now it works - different URLs (values in the attributes)
521 # but the same actual schema, so using the PG schema
522 # on delegated-to-PL credentials works fine.
524 # Note: you could also not copy attributes
525 # which already exist. It appears that both PG and PL
526 # will actually validate a slicecred with a parent
527 # signed using PG namespaces and a child signed with PL
528 # namespaces over the whole thing. But I don't know
529 # if that is a bug in xmlsec1, an accident since
530 # the contents of the schemas are the same,
531 # or something else, but it seems odd. And this works.
532 parentRoot = sdoc.documentElement
533 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
534 for attrIx in range(0, parentRoot.attributes.length):
535 attr = parentRoot.attributes.item(attrIx)
536 # returns the old attribute of same name that was
538 # Below throws InUse exception if we forgot to clone the attribute first
539 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
540 if oldAttr and oldAttr.value != attr.value:
541 msg = "Delegating cred from owner {} to {} over {}:\n"
542 "- Replaced attribute {} value '{}' with '{}'"\
543 .format(self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(),
544 self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
546 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: {}".format(msg))
548 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
549 p = doc.createElement("parent")
550 p.appendChild(p_cred)
552 # done handling parent credential
554 # Create the <signatures> tag
555 signatures = doc.createElement("signatures")
556 signed_cred.appendChild(signatures)
558 # Add any parent signatures
560 for cur_cred in self.get_credential_list()[1:]:
561 sdoc = parseString(cur_cred.get_signature().get_xml())
562 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
563 signatures.appendChild(ele)
565 # Get the finished product
566 self.xml = doc.toxml("utf-8")
569 def save_to_random_tmp_file(self):
570 fp, filename = mkstemp(suffix='cred', text=True)
571 fp = os.fdopen(fp, "w")
572 self.save_to_file(filename, save_parents=True, filep=fp)
575 def save_to_file(self, filename, save_parents=True, filep=None):
581 f = open(filename, "w")
582 if isinstance(self.xml, bytes):
583 self.xml = self.xml.decode()
587 def save_to_string(self, save_parents=True):
590 if isinstance(self.xml, bytes):
591 self.xml = self.xml.decode()
599 def set_refid(self, rid):
603 # Figure out what refids exist, and update this credential's id
604 # so that it doesn't clobber the others. Returns the refids of
607 def updateRefID(self):
609 self.set_refid('ref0')
614 next_cred = self.parent
616 refs.append(next_cred.get_refid())
618 next_cred = next_cred.parent
623 # Find a unique refid for this credential
624 rid = self.get_refid()
627 rid = "ref{}".format(val + 1)
632 # Return the set of parent credential ref ids
641 # Sign the XML file created by encode()
644 # In general, a signed credential obtained externally should
645 # not be changed else the signature is no longer valid. So, once
646 # you have loaded an existing signed credential, do not call encode() or sign() on it.
649 if not self.issuer_privkey:
650 logger.warn("Cannot sign credential (no private key)")
652 if not self.issuer_gid:
653 logger.warn("Cannot sign credential (no issuer gid)")
655 doc = parseString(self.get_xml())
656 sigs = doc.getElementsByTagName("signatures")[0]
658 # Create the signature template to be signed
659 signature = Signature()
660 signature.set_refid(self.get_refid())
661 sdoc = parseString(signature.get_xml())
662 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
663 sigs.appendChild(sig_ele)
665 self.xml = doc.toxml("utf-8")
668 # Split the issuer GID into multiple certificates if it's a chain
669 chain = GID(filename=self.issuer_gid)
672 gid_files.append(chain.save_to_random_tmp_file(False))
673 if chain.get_parent():
674 chain = chain.get_parent()
679 # Call out to xmlsec1 to sign it
680 ref = 'Sig_{}'.format(self.get_refid())
681 filename = self.save_to_random_tmp_file()
682 xmlsec1 = self.get_xmlsec1_path()
684 raise Exception("Could not locate required 'xmlsec1' program")
685 command = '{} --sign --node-id "{}" --privkey-pem {},{} {}' \
686 .format(xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
687 signed = os.popen(command).read()
690 for gid_file in gid_files:
700 # Retrieve the attributes of the credential from the XML.
701 # This is automatically called by the various get_* methods of
702 # this class and should not need to be called explicitly.
710 doc = parseString(self.xml)
711 except ExpatError as e:
712 raise CredentialNotVerifiable("Malformed credential")
713 doc = parseString(self.xml)
715 signed_cred = doc.getElementsByTagName("signed-credential")
717 # Is this a signed-cred or just a cred?
718 if len(signed_cred) > 0:
719 creds = signed_cred[0].getElementsByTagName("credential")
720 signatures = signed_cred[0].getElementsByTagName("signatures")
721 if len(signatures) > 0:
722 sigs = signatures[0].getElementsByTagName("Signature")
724 creds = doc.getElementsByTagName("credential")
726 if creds is None or len(creds) == 0:
727 # malformed cred file
728 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
730 # Just take the first cred if there are more than one
733 self.set_refid(cred.getAttribute("xml:id"))
734 self.set_expiration(utcparse(getTextNode(cred, "expires")))
735 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
736 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
739 ## This code until the end of function rewritten by Aaron Helsinger
742 priv_nodes = cred.getElementsByTagName("privileges")
743 if len(priv_nodes) > 0:
744 privs = priv_nodes[0]
745 for priv in privs.getElementsByTagName("privilege"):
746 kind = getTextNode(priv, "name")
747 deleg = str2bool(getTextNode(priv, "can_delegate"))
749 # Convert * into the default privileges for the credential's type
750 # Each inherits the delegatability from the * above
751 _ , type = urn_to_hrn(self.gidObject.get_urn())
752 rl = determine_rights(type, self.gidObject.get_urn())
757 rlist.add(Right(kind.strip(), deleg))
758 self.set_privileges(rlist)
762 parent = cred.getElementsByTagName("parent")
764 parent_doc = parent[0].getElementsByTagName("credential")[0]
765 parent_xml = parent_doc.toxml("utf-8")
766 if parent_xml is None or parent_xml.strip() == "":
767 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
768 self.parent = Credential(string=parent_xml)
771 # Assign the signatures to the credentials
773 Sig = Signature(string=sig.toxml("utf-8"))
775 for cur_cred in self.get_credential_list():
776 if cur_cred.get_refid() == Sig.get_refid():
777 cur_cred.set_signature(Sig)
782 # trusted_certs: A list of trusted GID filenames (not GID objects!)
783 # Chaining is not supported within the GIDs by xmlsec1.
785 # trusted_certs_required: Should usually be true. Set False means an
786 # empty list of trusted_certs would still let this method pass.
787 # It just skips xmlsec1 verification et al. Only used by some utils
790 # . All of the signatures are valid and that the issuers trace back
791 # to trusted roots (performed by xmlsec1)
792 # . The XML matches the credential schema
793 # . That the issuer of the credential is the authority in the target's urn
794 # . In the case of a delegated credential, this must be true of the root
795 # . That all of the gids presented in the credential are valid
796 # . Including verifying GID chains, and includ the issuer
797 # . The credential is not expired
799 # -- For Delegates (credentials with parents)
800 # . The privileges must be a subset of the parent credentials
801 # . The privileges must have "can_delegate" set for each delegated privilege
802 # . The target gid must be the same between child and parents
803 # . The expiry time on the child must be no later than the parent
804 # . The signer of the child must be the owner of the parent
806 # -- Verify does *NOT*
807 # . ensure that an xmlrpc client's gid matches a credential gid, that
808 # must be done elsewhere
810 # @param trusted_certs: The certificates of trusted CA certificates
811 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
815 # validate against RelaxNG schema
817 if schema and os.path.exists(schema):
818 tree = etree.parse(StringIO(self.xml))
819 schema_doc = etree.parse(schema)
820 xmlschema = etree.XMLSchema(schema_doc)
821 if not xmlschema.validate(tree):
822 error = xmlschema.error_log.last_error
823 message = "{}: {} (line {})".format(self.pretty_cred(),
824 error.message, error.line)
825 raise CredentialNotVerifiable(message)
827 if trusted_certs_required and trusted_certs is None:
830 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
831 trusted_cert_objects = []
832 ok_trusted_certs = []
833 # If caller explicitly passed in None that means skip cert chain validation.
834 # Strange and not typical
835 if trusted_certs is not None:
836 for f in trusted_certs:
838 # Failures here include unreadable files
840 trusted_cert_objects.append(GID(filename=f))
841 ok_trusted_certs.append(f)
842 except Exception as exc:
843 logger.error("Failed to load trusted cert from {}: {}".format(f, exc))
844 trusted_certs = ok_trusted_certs
846 # make sure it is not expired
847 if self.get_expiration() < datetime.datetime.utcnow():
848 raise CredentialNotVerifiable("Credential {} expired at {}" \
849 .format(self.pretty_cred(),
850 self.expiration.strftime(SFATIME_FORMAT)))
852 # Verify the signatures
853 filename = self.save_to_random_tmp_file()
855 # If caller explicitly passed in None that means skip cert chain validation.
856 # - Strange and not typical
857 if trusted_certs is not None:
858 # Verify the gids of this cred and of its parents
859 for cur_cred in self.get_credential_list():
860 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
861 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
864 refs.append("Sig_{}".format(self.get_refid()))
866 parentRefs = self.updateRefID()
867 for ref in parentRefs:
868 refs.append("Sig_{}".format(ref))
871 # If caller explicitly passed in None that means skip xmlsec1 validation.
872 # Strange and not typical
873 if trusted_certs is None:
877 # up to fedora20 we used os.popen and checked that the output begins with OK
878 # turns out, with fedora21, there is extra input before this 'OK' thing
879 # looks like we're better off just using the exit code - that's what it is made for
880 #cert_args = " ".join(['--trusted-pem {}'.format(x) for x in trusted_certs])
881 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
882 # format(self.xmlsec_path, ref, cert_args, filename)
883 xmlsec1 = self.get_xmlsec1_path()
885 raise Exception("Could not locate required 'xmlsec1' program")
886 command = [ xmlsec1, '--verify', '--node-id', ref ]
887 for trusted in trusted_certs:
888 command += ["--trusted-pem", trusted ]
889 command += [ filename ]
890 logger.debug("Running " + " ".join(command))
892 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
893 logger.debug("xmlsec command returned {}".format(verified))
894 if "OK\n" not in verified:
895 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
896 except subprocess.CalledProcessError as e:
898 # xmlsec errors have a msg= which is the interesting bit.
899 mstart = verified.find("msg=")
901 if mstart > -1 and len(verified) > 4:
903 mend = verified.find('\\', mstart)
904 msg = verified[mstart:mend]
905 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
906 raise CredentialNotVerifiable("xmlsec1 error verifying cred {} using Signature ID {}: {}"\
907 .format(self.pretty_cred(), ref, msg))
910 # Verify the parents (delegation)
912 self.verify_parent(self.parent)
914 # Make sure the issuer is the target's authority, and is
916 self.verify_issuer(trusted_cert_objects)
920 # Creates a list of the credential and its parents, with the root
921 # (original delegated credential) as the last item in the list
922 def get_credential_list(self):
926 list.append(cur_cred)
928 cur_cred = cur_cred.parent
934 # Make sure the credential's target gid (a) was signed by or (b)
935 # is the same as the entity that signed the original credential,
936 # or (c) is an authority over the target's namespace.
937 # Also ensure that the credential issuer / signer itself has a valid
938 # GID signature chain (signed by an authority with namespace rights).
939 def verify_issuer(self, trusted_gids):
940 root_cred = self.get_credential_list()[-1]
941 root_target_gid = root_cred.get_gid_object()
942 if root_cred.get_signature() is None:
944 raise CredentialNotVerifiable("Could not verify credential owned by {} for object {}. "
945 "Cred has no signature" \
946 .format(self.gidCaller.get_urn(), self.gidObject.get_urn()))
948 root_cred_signer = root_cred.get_signature().get_issuer_gid()
951 # Allow non authority to sign target and cred about target.
953 # Why do we need to allow non authorities to sign?
954 # If in the target gid validation step we correctly
955 # checked that the target is only signed by an authority,
956 # then this is just a special case of case 3.
957 # This short-circuit is the common case currently -
958 # and cause GID validation doesn't check 'authority',
959 # this allows users to generate valid slice credentials.
960 if root_target_gid.is_signed_by_cert(root_cred_signer):
961 # cred signer matches target signer, return success
965 # Allow someone to sign credential about themeselves. Used?
966 # If not, remove this.
967 #root_target_gid_str = root_target_gid.save_to_string()
968 #root_cred_signer_str = root_cred_signer.save_to_string()
969 #if root_target_gid_str == root_cred_signer_str:
970 # # cred signer is target, return success
975 # root_cred_signer is not the target_gid
976 # So this is a different gid that we have not verified.
977 # xmlsec1 verified the cert chain on this already, but
978 # it hasn't verified that the gid meets the HRN namespace
980 # Below we'll ensure that it is an authority.
981 # But we haven't verified that it is _signed by_ an authority
982 # We also don't know if xmlsec1 requires that cert signers
985 # Note that if verify() gave us no trusted_gids then this
986 # call will fail. So skip it if we have no trusted_gids
987 if trusted_gids and len(trusted_gids) > 0:
988 root_cred_signer.verify_chain(trusted_gids)
990 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
991 "No trusted gids. Skipping that check.")
993 # See if the signer is an authority over the domain of the target.
994 # There are multiple types of authority - accept them all here
995 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
996 root_cred_signer_type = root_cred_signer.get_type()
997 if root_cred_signer_type.find('authority') == 0:
998 #logger.debug('Cred signer is an authority')
999 # signer is an authority, see if target is in authority's domain
1000 signerhrn = root_cred_signer.get_hrn()
1001 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1004 # We've required that the credential be signed by an authority
1005 # for that domain. Reasonable and probably correct.
1006 # A looser model would also allow the signer to be an authority
1007 # in my control framework - eg My CA or CH. Even if it is not
1008 # the CH that issued these, eg, user credentials.
1010 # Give up, credential does not pass issuer verification
1012 raise CredentialNotVerifiable(
1013 "Could not verify credential owned by {} for object {}. "
1014 "Cred signer {} not the trusted authority for Cred target {}"
1015 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1016 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1019 # -- For Delegates (credentials with parents) verify that:
1020 # . The privileges must be a subset of the parent credentials
1021 # . The privileges must have "can_delegate" set for each delegated privilege
1022 # . The target gid must be the same between child and parents
1023 # . The expiry time on the child must be no later than the parent
1024 # . The signer of the child must be the owner of the parent
1025 def verify_parent(self, parent_cred):
1026 # make sure the rights given to the child are a subset of the
1027 # parents rights (and check delegate bits)
1028 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1030 "Parent cred {} (ref {}) rights {} "
1031 " not superset of delegated cred {} (ref {}) rights {}"
1032 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1033 parent_cred.get_privileges().pretty_rights(),
1034 self.pretty_cred(), self.get_refid(),
1035 self.get_privileges().pretty_rights()))
1036 logger.error(message)
1037 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1038 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1039 raise ChildRightsNotSubsetOfParent(message)
1041 # make sure my target gid is the same as the parent's
1042 if not parent_cred.get_gid_object().save_to_string() == \
1043 self.get_gid_object().save_to_string():
1045 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1046 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1047 logger.error(message)
1048 logger.error("parent details {}".format(parent_cred.save_to_string()))
1049 logger.error("self details {}".format(self.save_to_string()))
1050 raise CredentialNotVerifiable(message)
1052 # make sure my expiry time is <= my parent's
1053 if not parent_cred.get_expiration() >= self.get_expiration():
1054 raise CredentialNotVerifiable(
1055 "Delegated credential {} expires after parent {}"
1056 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1058 # make sure my signer is the parent's caller
1059 if not parent_cred.get_gid_caller().save_to_string(False) == \
1060 self.get_signature().get_issuer_gid().save_to_string(False):
1061 message = "Delegated credential {} not signed by parent {}'s caller"\
1062 .format(self.pretty_cred(), parent_cred.pretty_cred())
1063 logger.error(message)
1064 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1065 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1066 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1067 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1068 raise CredentialNotVerifiable(message)
1071 if parent_cred.parent:
1072 parent_cred.verify_parent(parent_cred.parent)
1075 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1077 Return a delegated copy of this credential, delegated to the
1078 specified gid's user.
1080 # get the gid of the object we are delegating
1081 object_gid = self.get_gid_object()
1082 object_hrn = object_gid.get_hrn()
1084 # the hrn of the user who will be delegated to
1085 delegee_gid = GID(filename=delegee_gidfile)
1086 delegee_hrn = delegee_gid.get_hrn()
1088 #user_key = Keypair(filename=keyfile)
1089 #user_hrn = self.get_gid_caller().get_hrn()
1090 subject_string = "{} delegated to {}".format(object_hrn, delegee_hrn)
1091 dcred = Credential(subject=subject_string)
1092 dcred.set_gid_caller(delegee_gid)
1093 dcred.set_gid_object(object_gid)
1094 dcred.set_parent(self)
1095 dcred.set_expiration(self.get_expiration())
1096 dcred.set_privileges(self.get_privileges())
1097 dcred.get_privileges().delegate_all_privileges(True)
1098 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1099 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1106 def get_filename(self):
1107 return getattr(self,'filename',None)
1109 def actual_caller_hrn(self):
1110 """a helper method used by some API calls like e.g. Allocate
1111 to try and find out who really is the original caller
1113 This admittedly is a bit of a hack, please USE IN LAST RESORT
1115 This code uses a heuristic to identify a delegated credential
1117 A first known restriction if for traffic that gets through a slice manager
1118 in this case the hrn reported is the one from the last SM in the call graph
1119 which is not at all what is meant here"""
1121 caller_hrn = self.get_gid_caller().get_hrn()
1122 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1123 subject_hrn = self.get_gid_object().get_hrn()
1124 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1125 # this seems to be a 'regular' credential
1126 if caller_hrn.startswith(issuer_hrn):
1127 actual_caller_hrn=caller_hrn
1128 # else this looks like a delegated credential, and the real caller is the issuer
1130 actual_caller_hrn=issuer_hrn
1131 logger.info("actual_caller_hrn: caller_hrn={}, issuer_hrn={}, returning {}"
1132 .format(caller_hrn,issuer_hrn,actual_caller_hrn))
1133 return actual_caller_hrn
1136 # Dump the contents of a credential to stdout in human-readable format
1138 # @param dump_parents If true, also dump the parent certificates
1139 def dump(self, *args, **kwargs):
1140 print(self.dump_string(*args, **kwargs))
1142 # SFA code ignores show_xml and disables printing the cred xml
1143 def dump_string(self, dump_parents=False, show_xml=False):
1145 result += "CREDENTIAL {}\n".format(self.pretty_subject())
1146 filename=self.get_filename()
1147 if filename: result += "Filename {}\n".format(filename)
1148 privileges = self.get_privileges()
1150 result += " privs: {}\n".format(privileges.save_to_string())
1152 result += " privs: \n"
1153 gidCaller = self.get_gid_caller()
1155 result += " gidCaller:\n"
1156 result += gidCaller.dump_string(8, dump_parents)
1158 if self.get_signature():
1159 result += " gidIssuer:\n"
1160 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1163 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1165 gidObject = self.get_gid_object()
1167 result += " gidObject:\n"
1168 result += gidObject.dump_string(8, dump_parents)
1170 if self.parent and dump_parents:
1171 result += "\nPARENT"
1172 result += self.parent.dump_string(True)
1174 if show_xml and HAVELXML:
1176 tree = etree.parse(StringIO(self.xml))
1177 aside = etree.tostring(tree, pretty_print=True)
1178 result += "\nXML:\n\n"
1180 result += "\nEnd XML\n"
1183 print("exc. Credential.dump_string / XML")
1184 traceback.print_exc()