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?
65 signature_template = \
67 <Signature xml:id="Sig_%s" 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"/>
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>
91 # PG formats the template (whitespace) slightly differently.
92 # Note that they don't include the xmlns in the template, but add it later.
93 # Otherwise the two are equivalent.
94 #signature_template_as_in_pg = \
96 #<Signature xml:id="Sig_%s" >
98 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
99 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
100 # <Reference URI="#%s">
102 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
104 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
105 # <DigestValue></DigestValue>
112 # <X509IssuerSerial/>
121 # Convert a string into a bool
122 # used to convert an xsd:boolean to a Python boolean
124 if str.lower() in ['true','1']:
130 # Utility function to get the text of an XML element
132 def getTextNode(element, subele):
133 sub = element.getElementsByTagName(subele)[0]
134 if len(sub.childNodes) > 0:
135 return sub.childNodes[0].nodeValue
140 # Utility function to set the text of an XML element
141 # It creates the element, adds the text to it,
142 # and then appends it to the parent.
144 def append_sub(doc, parent, element, text):
145 ele = doc.createElement(element)
146 ele.appendChild(doc.createTextNode(text))
147 parent.appendChild(ele)
150 # Signature contains information about an xmlsec1 signature
151 # for a signed-credential
154 class Signature(object):
156 def __init__(self, string=None):
158 self.issuer_gid = None
175 def set_refid(self, id):
178 def get_issuer_gid(self):
183 def set_issuer_gid(self, gid):
187 # Helper function to pull characters off the front of a string if present
188 def remove_prefix(text, prefix):
189 if text and prefix and text.startswith(prefix):
190 return text[len(prefix):]
194 doc = parseString(self.xml)
195 except ExpatError as e:
196 logger.log_exc ("Failed to parse credential, %s"%self.xml)
198 sig = doc.getElementsByTagName("Signature")[0]
199 ## This code until the end of function rewritten by Aaron Helsinger
200 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
201 # The xml:id tag is optional, and could be in a
202 # Reference xml:id or Reference UID sub element instead
203 if not ref_id or ref_id == '':
204 reference = sig.getElementsByTagName('Reference')[0]
205 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
206 if not ref_id or ref_id == '':
207 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
208 self.set_refid(ref_id)
209 keyinfos = sig.getElementsByTagName("X509Data")
211 for keyinfo in keyinfos:
212 certs = keyinfo.getElementsByTagName("X509Certificate")
214 if len(cert.childNodes) > 0:
215 szgid = cert.childNodes[0].nodeValue
216 szgid = szgid.strip()
217 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
223 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
224 self.set_issuer_gid(GID(string=gids))
227 self.xml = signature_template % (self.get_refid(), self.get_refid())
230 # A credential provides a caller gid with privileges to an object gid.
231 # A signed credential is signed by the object's authority.
233 # Credentials are encoded in one of two ways.
234 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
235 # The new credentials are placed in signed XML.
238 # In general, a signed credential obtained externally should
239 # not be changed else the signature is no longer valid. So, once
240 # you have loaded an existing signed credential, do not call encode() or sign() on it.
242 def filter_creds_by_caller(creds, caller_hrn_list):
244 Returns a list of creds who's gid caller matches the
247 if not isinstance(creds, list): creds = [creds]
248 if not isinstance(caller_hrn_list, list):
249 caller_hrn_list = [caller_hrn_list]
253 tmp_cred = Credential(string=cred)
254 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
256 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
257 caller_creds.append(cred)
261 class Credential(object):
263 SFA_CREDENTIAL_TYPE = "geni_sfa"
266 # Create a Credential object
268 # @param create If true, create a blank x509 certificate
269 # @param subject If subject!=None, create an x509 cert with the subject name
270 # @param string If string!=None, load the credential from the string
271 # @param filename If filename!=None, load the credential from the file
272 # FIXME: create and subject are ignored!
273 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
274 self.gidCaller = None
275 self.gidObject = None
276 self.expiration = None
277 self.privileges = None
278 self.issuer_privkey = None
279 self.issuer_gid = None
280 self.issuer_pubkey = None
282 self.signature = None
285 self.type = Credential.SFA_CREDENTIAL_TYPE
289 if isinstance(cred, StringType):
291 self.type = Credential.SFA_CREDENTIAL_TYPE
293 elif isinstance(cred, dict):
294 string = cred['geni_value']
295 self.type = cred['geni_type']
296 self.version = cred['geni_version']
298 if string or filename:
302 str = file(filename).read()
304 # if this is a legacy credential, write error and bail out
305 if isinstance (str, StringType) and str.strip().startswith("-----"):
306 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
311 # not strictly necessary but won't hurt either
312 self.get_xmlsec1_path()
315 def get_xmlsec1_path():
316 if not getattr(Credential, 'xmlsec1_path', None):
317 # Find a xmlsec1 binary path
318 Credential.xmlsec1_path = ''
319 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
320 try: paths += os.getenv('PATH').split(':')
323 xmlsec1 = os.path.join(path, 'xmlsec1')
324 if os.path.isfile(xmlsec1):
325 Credential.xmlsec1_path = xmlsec1
327 if not Credential.xmlsec1_path:
328 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
329 return Credential.xmlsec1_path
331 def get_subject(self):
332 if not self.gidObject:
334 return self.gidObject.get_subject()
336 def pretty_subject(self):
338 if not self.gidObject:
341 subject = self.gidObject.pretty_cert()
344 # sounds like this should be __repr__ instead ??
345 def pretty_cred(self):
346 if not self.gidObject:
348 obj = self.gidObject.pretty_cert()
349 caller = self.gidCaller.pretty_cert()
350 exp = self.get_expiration()
351 # Summarize the rights too? The issuer?
352 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
354 def get_signature(self):
355 if not self.signature:
357 return self.signature
359 def set_signature(self, sig):
364 # Need the issuer's private key and name
365 # @param key Keypair object containing the private key of the issuer
366 # @param gid GID of the issuing authority
368 def set_issuer_keys(self, privkey, gid):
369 self.issuer_privkey = privkey
370 self.issuer_gid = gid
374 # Set this credential's parent
375 def set_parent(self, cred):
380 # set the GID of the caller
382 # @param gid GID object of the caller
384 def set_gid_caller(self, gid):
386 # gid origin caller is the caller's gid by default
387 self.gidOriginCaller = gid
390 # get the GID of the object
392 def get_gid_caller(self):
393 if not self.gidCaller:
395 return self.gidCaller
398 # set the GID of the object
400 # @param gid GID object of the object
402 def set_gid_object(self, gid):
406 # get the GID of the object
408 def get_gid_object(self):
409 if not self.gidObject:
411 return self.gidObject
414 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
416 def set_expiration(self, expiration):
417 expiration_datetime = utcparse (expiration)
418 if expiration_datetime is not None:
419 self.expiration = expiration_datetime
421 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
424 # get the lifetime of the credential (always in datetime format)
426 def get_expiration(self):
427 if not self.expiration:
429 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
430 return self.expiration
435 # @param privs either a comma-separated list of privileges of a Rights object
437 def set_privileges(self, privs):
438 if isinstance(privs, str):
439 self.privileges = Rights(string = privs)
441 self.privileges = privs
444 # return the privileges as a Rights object
446 def get_privileges(self):
447 if not self.privileges:
449 return self.privileges
452 # determine whether the credential allows a particular operation to be
455 # @param op_name string specifying name of operation ("lookup", "update", etc)
457 def can_perform(self, op_name):
458 rights = self.get_privileges()
463 return rights.can_perform(op_name)
467 # Encode the attributes of the credential into an XML string
468 # This should be done immediately before signing the credential.
470 # In general, a signed credential obtained externally should
471 # not be changed else the signature is no longer valid. So, once
472 # you have loaded an existing signed credential, do not call encode() or sign() on it.
475 # Create the XML document
477 signed_cred = doc.createElement("signed-credential")
480 # Note that credential/policy.xsd are really the PG schemas
482 # Note that delegation of credentials between the 2 only really works
483 # cause those schemas are identical.
484 # Also note these PG schemas talk about PG tickets and CM policies.
485 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
486 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
487 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
488 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")
490 # PG says for those last 2:
491 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
492 # 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")
494 doc.appendChild(signed_cred)
496 # Fill in the <credential> bit
497 cred = doc.createElement("credential")
498 cred.setAttribute("xml:id", self.get_refid())
499 signed_cred.appendChild(cred)
500 append_sub(doc, cred, "type", "privilege")
501 append_sub(doc, cred, "serial", "8")
502 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
503 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
504 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
505 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
506 append_sub(doc, cred, "uuid", "")
507 if not self.expiration:
508 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
509 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
510 self.expiration = self.expiration.replace(microsecond=0)
511 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
512 # TZ aware. Make sure it is UTC - by Aaron Helsinger
513 self.expiration = self.expiration.astimezone(tz.tzutc())
514 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
515 privileges = doc.createElement("privileges")
516 cred.appendChild(privileges)
519 rights = self.get_privileges()
520 for right in rights.rights:
521 priv = doc.createElement("privilege")
522 append_sub(doc, priv, "name", right.kind)
523 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
524 privileges.appendChild(priv)
526 # Add the parent credential if it exists
528 sdoc = parseString(self.parent.get_xml())
529 # If the root node is a signed-credential (it should be), then
530 # get all its attributes and attach those to our signed_cred
532 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
533 # and we need to include those again here or else their signature
534 # no longer matches on the credential.
535 # We expect three of these, but here we copy them all:
536 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
537 # and from PG (PL is equivalent, as shown above):
538 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
539 # 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")
542 # PL now also declares these, with different URLs, so
543 # the code notices those attributes already existed with
544 # different values, and complains.
545 # This happens regularly on delegation now that PG and
546 # PL both declare the namespace with different URLs.
547 # If the content ever differs this is a problem,
548 # but for now it works - different URLs (values in the attributes)
549 # but the same actual schema, so using the PG schema
550 # on delegated-to-PL credentials works fine.
552 # Note: you could also not copy attributes
553 # which already exist. It appears that both PG and PL
554 # will actually validate a slicecred with a parent
555 # signed using PG namespaces and a child signed with PL
556 # namespaces over the whole thing. But I don't know
557 # if that is a bug in xmlsec1, an accident since
558 # the contents of the schemas are the same,
559 # or something else, but it seems odd. And this works.
560 parentRoot = sdoc.documentElement
561 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
562 for attrIx in range(0, parentRoot.attributes.length):
563 attr = parentRoot.attributes.item(attrIx)
564 # returns the old attribute of same name that was
566 # Below throws InUse exception if we forgot to clone the attribute first
567 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
568 if oldAttr and oldAttr.value != attr.value:
569 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
570 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
572 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
574 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
575 p = doc.createElement("parent")
576 p.appendChild(p_cred)
578 # done handling parent credential
580 # Create the <signatures> tag
581 signatures = doc.createElement("signatures")
582 signed_cred.appendChild(signatures)
584 # Add any parent signatures
586 for cur_cred in self.get_credential_list()[1:]:
587 sdoc = parseString(cur_cred.get_signature().get_xml())
588 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
589 signatures.appendChild(ele)
591 # Get the finished product
592 self.xml = doc.toxml("utf-8")
595 def save_to_random_tmp_file(self):
596 fp, filename = mkstemp(suffix='cred', text=True)
597 fp = os.fdopen(fp, "w")
598 self.save_to_file(filename, save_parents=True, filep=fp)
601 def save_to_file(self, filename, save_parents=True, filep=None):
607 f = open(filename, "w")
611 def save_to_string(self, save_parents=True):
621 def set_refid(self, rid):
625 # Figure out what refids exist, and update this credential's id
626 # so that it doesn't clobber the others. Returns the refids of
629 def updateRefID(self):
631 self.set_refid('ref0')
636 next_cred = self.parent
638 refs.append(next_cred.get_refid())
640 next_cred = next_cred.parent
645 # Find a unique refid for this credential
646 rid = self.get_refid()
649 rid = "ref%d" % (val + 1)
654 # Return the set of parent credential ref ids
663 # Sign the XML file created by encode()
666 # In general, a signed credential obtained externally should
667 # not be changed else the signature is no longer valid. So, once
668 # you have loaded an existing signed credential, do not call encode() or sign() on it.
671 if not self.issuer_privkey:
672 logger.warn("Cannot sign credential (no private key)")
674 if not self.issuer_gid:
675 logger.warn("Cannot sign credential (no issuer gid)")
677 doc = parseString(self.get_xml())
678 sigs = doc.getElementsByTagName("signatures")[0]
680 # Create the signature template to be signed
681 signature = Signature()
682 signature.set_refid(self.get_refid())
683 sdoc = parseString(signature.get_xml())
684 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
685 sigs.appendChild(sig_ele)
687 self.xml = doc.toxml("utf-8")
690 # Split the issuer GID into multiple certificates if it's a chain
691 chain = GID(filename=self.issuer_gid)
694 gid_files.append(chain.save_to_random_tmp_file(False))
695 if chain.get_parent():
696 chain = chain.get_parent()
701 # Call out to xmlsec1 to sign it
702 ref = 'Sig_%s' % self.get_refid()
703 filename = self.save_to_random_tmp_file()
704 xmlsec1 = self.get_xmlsec1_path()
706 raise Exception("Could not locate required 'xmlsec1' program")
707 command = '%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
708 % (xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
709 # print 'command',command
710 signed = os.popen(command).read()
713 for gid_file in gid_files:
723 # Retrieve the attributes of the credential from the XML.
724 # This is automatically called by the various get_* methods of
725 # this class and should not need to be called explicitly.
733 doc = parseString(self.xml)
734 except ExpatError as e:
735 raise CredentialNotVerifiable("Malformed credential")
736 doc = parseString(self.xml)
738 signed_cred = doc.getElementsByTagName("signed-credential")
740 # Is this a signed-cred or just a cred?
741 if len(signed_cred) > 0:
742 creds = signed_cred[0].getElementsByTagName("credential")
743 signatures = signed_cred[0].getElementsByTagName("signatures")
744 if len(signatures) > 0:
745 sigs = signatures[0].getElementsByTagName("Signature")
747 creds = doc.getElementsByTagName("credential")
749 if creds is None or len(creds) == 0:
750 # malformed cred file
751 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
753 # Just take the first cred if there are more than one
756 self.set_refid(cred.getAttribute("xml:id"))
757 self.set_expiration(utcparse(getTextNode(cred, "expires")))
758 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
759 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
762 ## This code until the end of function rewritten by Aaron Helsinger
765 priv_nodes = cred.getElementsByTagName("privileges")
766 if len(priv_nodes) > 0:
767 privs = priv_nodes[0]
768 for priv in privs.getElementsByTagName("privilege"):
769 kind = getTextNode(priv, "name")
770 deleg = str2bool(getTextNode(priv, "can_delegate"))
772 # Convert * into the default privileges for the credential's type
773 # Each inherits the delegatability from the * above
774 _ , type = urn_to_hrn(self.gidObject.get_urn())
775 rl = determine_rights(type, self.gidObject.get_urn())
780 rlist.add(Right(kind.strip(), deleg))
781 self.set_privileges(rlist)
785 parent = cred.getElementsByTagName("parent")
787 parent_doc = parent[0].getElementsByTagName("credential")[0]
788 parent_xml = parent_doc.toxml("utf-8")
789 if parent_xml is None or parent_xml.strip() == "":
790 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
791 self.parent = Credential(string=parent_xml)
794 # Assign the signatures to the credentials
796 Sig = Signature(string=sig.toxml("utf-8"))
798 for cur_cred in self.get_credential_list():
799 if cur_cred.get_refid() == Sig.get_refid():
800 cur_cred.set_signature(Sig)
805 # trusted_certs: A list of trusted GID filenames (not GID objects!)
806 # Chaining is not supported within the GIDs by xmlsec1.
808 # trusted_certs_required: Should usually be true. Set False means an
809 # empty list of trusted_certs would still let this method pass.
810 # It just skips xmlsec1 verification et al. Only used by some utils
813 # . All of the signatures are valid and that the issuers trace back
814 # to trusted roots (performed by xmlsec1)
815 # . The XML matches the credential schema
816 # . That the issuer of the credential is the authority in the target's urn
817 # . In the case of a delegated credential, this must be true of the root
818 # . That all of the gids presented in the credential are valid
819 # . Including verifying GID chains, and includ the issuer
820 # . The credential is not expired
822 # -- For Delegates (credentials with parents)
823 # . The privileges must be a subset of the parent credentials
824 # . The privileges must have "can_delegate" set for each delegated privilege
825 # . The target gid must be the same between child and parents
826 # . The expiry time on the child must be no later than the parent
827 # . The signer of the child must be the owner of the parent
829 # -- Verify does *NOT*
830 # . ensure that an xmlrpc client's gid matches a credential gid, that
831 # must be done elsewhere
833 # @param trusted_certs: The certificates of trusted CA certificates
834 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
838 # validate against RelaxNG schema
840 if schema and os.path.exists(schema):
841 tree = etree.parse(StringIO(self.xml))
842 schema_doc = etree.parse(schema)
843 xmlschema = etree.XMLSchema(schema_doc)
844 if not xmlschema.validate(tree):
845 error = xmlschema.error_log.last_error
846 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
847 raise CredentialNotVerifiable(message)
849 if trusted_certs_required and trusted_certs is None:
852 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
853 trusted_cert_objects = []
854 ok_trusted_certs = []
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 for f in trusted_certs:
860 # Failures here include unreadable files
862 trusted_cert_objects.append(GID(filename=f))
863 ok_trusted_certs.append(f)
864 except Exception as exc:
865 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
866 trusted_certs = ok_trusted_certs
868 # make sure it is not expired
869 if self.get_expiration() < datetime.datetime.utcnow():
870 raise CredentialNotVerifiable("Credential %s expired at %s" % \
872 self.expiration.strftime(SFATIME_FORMAT)))
874 # Verify the signatures
875 filename = self.save_to_random_tmp_file()
877 # If caller explicitly passed in None that means skip cert chain validation.
878 # - Strange and not typical
879 if trusted_certs is not None:
880 # Verify the gids of this cred and of its parents
881 for cur_cred in self.get_credential_list():
882 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
883 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
886 refs.append("Sig_%s" % self.get_refid())
888 parentRefs = self.updateRefID()
889 for ref in parentRefs:
890 refs.append("Sig_%s" % ref)
893 # If caller explicitly passed in None that means skip xmlsec1 validation.
894 # Strange and not typical
895 if trusted_certs is None:
899 # up to fedora20 we used os.popen and checked that the output begins with OK
900 # turns out, with fedora21, there is extra input before this 'OK' thing
901 # looks like we're better off just using the exit code - that's what it is made for
902 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
903 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
904 # format(self.xmlsec_path, ref, cert_args, filename)
905 xmlsec1 = self.get_xmlsec1_path()
907 raise Exception("Could not locate required 'xmlsec1' program")
908 command = [ xmlsec1, '--verify', '--node-id', ref ]
909 for trusted in trusted_certs:
910 command += ["--trusted-pem", trusted ]
911 command += [ filename ]
912 logger.debug("Running " + " ".join(command))
914 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
915 logger.debug("xmlsec command returned {}".format(verified))
916 if "OK\n" not in verified:
917 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
918 except subprocess.CalledProcessError as e:
920 # xmlsec errors have a msg= which is the interesting bit.
921 mstart = verified.find("msg=")
923 if mstart > -1 and len(verified) > 4:
925 mend = verified.find('\\', mstart)
926 msg = verified[mstart:mend]
927 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
928 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
929 (self.pretty_cred(), ref, msg))
932 # Verify the parents (delegation)
934 self.verify_parent(self.parent)
936 # Make sure the issuer is the target's authority, and is
938 self.verify_issuer(trusted_cert_objects)
942 # Creates a list of the credential and its parents, with the root
943 # (original delegated credential) as the last item in the list
944 def get_credential_list(self):
948 list.append(cur_cred)
950 cur_cred = cur_cred.parent
956 # Make sure the credential's target gid (a) was signed by or (b)
957 # is the same as the entity that signed the original credential,
958 # or (c) is an authority over the target's namespace.
959 # Also ensure that the credential issuer / signer itself has a valid
960 # GID signature chain (signed by an authority with namespace rights).
961 def verify_issuer(self, trusted_gids):
962 root_cred = self.get_credential_list()[-1]
963 root_target_gid = root_cred.get_gid_object()
964 if root_cred.get_signature() is None:
966 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
968 root_cred_signer = root_cred.get_signature().get_issuer_gid()
971 # Allow non authority to sign target and cred about target.
973 # Why do we need to allow non authorities to sign?
974 # If in the target gid validation step we correctly
975 # checked that the target is only signed by an authority,
976 # then this is just a special case of case 3.
977 # This short-circuit is the common case currently -
978 # and cause GID validation doesn't check 'authority',
979 # this allows users to generate valid slice credentials.
980 if root_target_gid.is_signed_by_cert(root_cred_signer):
981 # cred signer matches target signer, return success
985 # Allow someone to sign credential about themeselves. Used?
986 # If not, remove this.
987 #root_target_gid_str = root_target_gid.save_to_string()
988 #root_cred_signer_str = root_cred_signer.save_to_string()
989 #if root_target_gid_str == root_cred_signer_str:
990 # # cred signer is target, return success
995 # root_cred_signer is not the target_gid
996 # So this is a different gid that we have not verified.
997 # xmlsec1 verified the cert chain on this already, but
998 # it hasn't verified that the gid meets the HRN namespace
1000 # Below we'll ensure that it is an authority.
1001 # But we haven't verified that it is _signed by_ an authority
1002 # We also don't know if xmlsec1 requires that cert signers
1003 # are marked as CAs.
1005 # Note that if verify() gave us no trusted_gids then this
1006 # call will fail. So skip it if we have no trusted_gids
1007 if trusted_gids and len(trusted_gids) > 0:
1008 root_cred_signer.verify_chain(trusted_gids)
1010 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1011 "No trusted gids. Skipping that check.")
1013 # See if the signer is an authority over the domain of the target.
1014 # There are multiple types of authority - accept them all here
1015 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1016 root_cred_signer_type = root_cred_signer.get_type()
1017 if root_cred_signer_type.find('authority') == 0:
1018 #logger.debug('Cred signer is an authority')
1019 # signer is an authority, see if target is in authority's domain
1020 signerhrn = root_cred_signer.get_hrn()
1021 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1024 # We've required that the credential be signed by an authority
1025 # for that domain. Reasonable and probably correct.
1026 # A looser model would also allow the signer to be an authority
1027 # in my control framework - eg My CA or CH. Even if it is not
1028 # the CH that issued these, eg, user credentials.
1030 # Give up, credential does not pass issuer verification
1032 raise CredentialNotVerifiable(
1033 "Could not verify credential owned by {} for object {}. "
1034 "Cred signer {} not the trusted authority for Cred target {}"
1035 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1036 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1039 # -- For Delegates (credentials with parents) verify that:
1040 # . The privileges must be a subset of the parent credentials
1041 # . The privileges must have "can_delegate" set for each delegated privilege
1042 # . The target gid must be the same between child and parents
1043 # . The expiry time on the child must be no later than the parent
1044 # . The signer of the child must be the owner of the parent
1045 def verify_parent(self, parent_cred):
1046 # make sure the rights given to the child are a subset of the
1047 # parents rights (and check delegate bits)
1048 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1050 "Parent cred {} (ref {}) rights {} "
1051 " not superset of delegated cred {} (ref {}) rights {}"
1052 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1053 parent_cred.get_privileges().pretty_rights(),
1054 self.pretty_cred(), self.get_refid(),
1055 self.get_privileges().pretty_rights()))
1056 logger.error(message)
1057 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1058 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1059 raise ChildRightsNotSubsetOfParent(message)
1061 # make sure my target gid is the same as the parent's
1062 if not parent_cred.get_gid_object().save_to_string() == \
1063 self.get_gid_object().save_to_string():
1065 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1066 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1067 logger.error(message)
1068 logger.error("parent details {}".format(parent_cred.save_to_string()))
1069 logger.error("self details {}".format(self.save_to_string()))
1070 raise CredentialNotVerifiable(message)
1072 # make sure my expiry time is <= my parent's
1073 if not parent_cred.get_expiration() >= self.get_expiration():
1074 raise CredentialNotVerifiable(
1075 "Delegated credential {} expires after parent {}"
1076 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1078 # make sure my signer is the parent's caller
1079 if not parent_cred.get_gid_caller().save_to_string(False) == \
1080 self.get_signature().get_issuer_gid().save_to_string(False):
1081 message = "Delegated credential {} not signed by parent {}'s caller"\
1082 .format(self.pretty_cred(), parent_cred.pretty_cred())
1083 logger.error(message)
1084 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1085 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1086 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1087 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1088 raise CredentialNotVerifiable(message)
1091 if parent_cred.parent:
1092 parent_cred.verify_parent(parent_cred.parent)
1095 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1097 Return a delegated copy of this credential, delegated to the
1098 specified gid's user.
1100 # get the gid of the object we are delegating
1101 object_gid = self.get_gid_object()
1102 object_hrn = object_gid.get_hrn()
1104 # the hrn of the user who will be delegated to
1105 delegee_gid = GID(filename=delegee_gidfile)
1106 delegee_hrn = delegee_gid.get_hrn()
1108 #user_key = Keypair(filename=keyfile)
1109 #user_hrn = self.get_gid_caller().get_hrn()
1110 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1111 dcred = Credential(subject=subject_string)
1112 dcred.set_gid_caller(delegee_gid)
1113 dcred.set_gid_object(object_gid)
1114 dcred.set_parent(self)
1115 dcred.set_expiration(self.get_expiration())
1116 dcred.set_privileges(self.get_privileges())
1117 dcred.get_privileges().delegate_all_privileges(True)
1118 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1119 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1126 def get_filename(self):
1127 return getattr(self,'filename',None)
1129 def actual_caller_hrn (self):
1130 """a helper method used by some API calls like e.g. Allocate
1131 to try and find out who really is the original caller
1133 This admittedly is a bit of a hack, please USE IN LAST RESORT
1135 This code uses a heuristic to identify a delegated credential
1137 A first known restriction if for traffic that gets through a slice manager
1138 in this case the hrn reported is the one from the last SM in the call graph
1139 which is not at all what is meant here"""
1141 caller_hrn = self.get_gid_caller().get_hrn()
1142 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1143 subject_hrn = self.get_gid_object().get_hrn()
1144 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1145 # this seems to be a 'regular' credential
1146 if caller_hrn.startswith(issuer_hrn):
1147 actual_caller_hrn=caller_hrn
1148 # else this looks like a delegated credential, and the real caller is the issuer
1150 actual_caller_hrn=issuer_hrn
1151 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1152 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1153 return actual_caller_hrn
1156 # Dump the contents of a credential to stdout in human-readable format
1158 # @param dump_parents If true, also dump the parent certificates
1159 def dump (self, *args, **kwargs):
1160 print(self.dump_string(*args, **kwargs))
1162 # SFA code ignores show_xml and disables printing the cred xml
1163 def dump_string(self, dump_parents=False, show_xml=False):
1165 result += "CREDENTIAL %s\n" % self.pretty_subject()
1166 filename=self.get_filename()
1167 if filename: result += "Filename %s\n"%filename
1168 privileges = self.get_privileges()
1170 result += " privs: %s\n" % privileges.save_to_string()
1172 result += " privs: \n"
1173 gidCaller = self.get_gid_caller()
1175 result += " gidCaller:\n"
1176 result += gidCaller.dump_string(8, dump_parents)
1178 if self.get_signature():
1179 result += " gidIssuer:\n"
1180 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1183 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1185 gidObject = self.get_gid_object()
1187 result += " gidObject:\n"
1188 result += gidObject.dump_string(8, dump_parents)
1190 if self.parent and dump_parents:
1191 result += "\nPARENT"
1192 result += self.parent.dump_string(True)
1194 if show_xml and HAVELXML:
1196 tree = etree.parse(StringIO(self.xml))
1197 aside = etree.tostring(tree, pretty_print=True)
1198 result += "\nXML:\n\n"
1200 result += "\nEnd XML\n"
1203 print("exc. Credential.dump_string / XML")
1204 traceback.print_exc()