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
33 from types import StringTypes
35 from StringIO import StringIO
36 from tempfile import mkstemp
37 from xml.dom.minidom import Document, parseString
41 from lxml import etree
46 from xml.parsers.expat import ExpatError
48 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
49 from sfa.util.sfalogging import logger
50 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
51 from sfa.trust.rights import Right, Rights, determine_rights
52 from sfa.trust.gid import GID
53 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
56 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
60 # . make privs match between PG and PL
61 # . Need to add support for other types of credentials, e.g. tickets
62 # . add namespaces to signed-credential element?
64 signature_template = \
66 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
68 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
69 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
72 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
74 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
75 <DigestValue></DigestValue>
90 # PG formats the template (whitespace) slightly differently.
91 # Note that they don't include the xmlns in the template, but add it later.
92 # Otherwise the two are equivalent.
93 #signature_template_as_in_pg = \
95 #<Signature xml:id="Sig_%s" >
97 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
98 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
99 # <Reference URI="#%s">
101 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
103 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
104 # <DigestValue></DigestValue>
111 # <X509IssuerSerial/>
120 # Convert a string into a bool
121 # used to convert an xsd:boolean to a Python boolean
123 if str.lower() in ['true','1']:
129 # Utility function to get the text of an XML element
131 def getTextNode(element, subele):
132 sub = element.getElementsByTagName(subele)[0]
133 if len(sub.childNodes) > 0:
134 return sub.childNodes[0].nodeValue
139 # Utility function to set the text of an XML element
140 # It creates the element, adds the text to it,
141 # and then appends it to the parent.
143 def append_sub(doc, parent, element, text):
144 ele = doc.createElement(element)
145 ele.appendChild(doc.createTextNode(text))
146 parent.appendChild(ele)
149 # Signature contains information about an xmlsec1 signature
150 # for a signed-credential
153 class Signature(object):
155 def __init__(self, string=None):
157 self.issuer_gid = None
174 def set_refid(self, id):
177 def get_issuer_gid(self):
182 def set_issuer_gid(self, gid):
186 # Helper function to pull characters off the front of a string if present
187 def remove_prefix(text, prefix):
188 if text and prefix and text.startswith(prefix):
189 return text[len(prefix):]
193 doc = parseString(self.xml)
194 except ExpatError as e:
195 logger.log_exc ("Failed to parse credential, %s"%self.xml)
197 sig = doc.getElementsByTagName("Signature")[0]
198 ## This code until the end of function rewritten by Aaron Helsinger
199 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
200 # The xml:id tag is optional, and could be in a
201 # Reference xml:id or Reference UID sub element instead
202 if not ref_id or ref_id == '':
203 reference = sig.getElementsByTagName('Reference')[0]
204 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
205 if not ref_id or ref_id == '':
206 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
207 self.set_refid(ref_id)
208 keyinfos = sig.getElementsByTagName("X509Data")
210 for keyinfo in keyinfos:
211 certs = keyinfo.getElementsByTagName("X509Certificate")
213 if len(cert.childNodes) > 0:
214 szgid = cert.childNodes[0].nodeValue
215 szgid = szgid.strip()
216 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
222 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
223 self.set_issuer_gid(GID(string=gids))
226 self.xml = signature_template % (self.get_refid(), self.get_refid())
229 # A credential provides a caller gid with privileges to an object gid.
230 # A signed credential is signed by the object's authority.
232 # Credentials are encoded in one of two ways.
233 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
234 # The new credentials are placed in signed XML.
237 # In general, a signed credential obtained externally should
238 # not be changed else the signature is no longer valid. So, once
239 # you have loaded an existing signed credential, do not call encode() or sign() on it.
241 def filter_creds_by_caller(creds, caller_hrn_list):
243 Returns a list of creds who's gid caller matches the
246 if not isinstance(creds, list): creds = [creds]
247 if not isinstance(caller_hrn_list, list):
248 caller_hrn_list = [caller_hrn_list]
252 tmp_cred = Credential(string=cred)
253 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
255 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
256 caller_creds.append(cred)
260 class Credential(object):
262 SFA_CREDENTIAL_TYPE = "geni_sfa"
265 # Create a Credential object
267 # @param create If true, create a blank x509 certificate
268 # @param subject If subject!=None, create an x509 cert with the subject name
269 # @param string If string!=None, load the credential from the string
270 # @param filename If filename!=None, load the credential from the file
271 # FIXME: create and subject are ignored!
272 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
273 self.gidCaller = None
274 self.gidObject = None
275 self.expiration = None
276 self.privileges = None
277 self.issuer_privkey = None
278 self.issuer_gid = None
279 self.issuer_pubkey = None
281 self.signature = None
284 self.type = Credential.SFA_CREDENTIAL_TYPE
288 if isinstance(cred, StringTypes):
290 self.type = Credential.SFA_CREDENTIAL_TYPE
292 elif isinstance(cred, dict):
293 string = cred['geni_value']
294 self.type = cred['geni_type']
295 self.version = cred['geni_version']
297 if string or filename:
301 str = file(filename).read()
303 # if this is a legacy credential, write error and bail out
304 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
305 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
310 # not strictly necessary but won't hurt either
311 self.get_xmlsec1_path()
314 def get_xmlsec1_path():
315 if not getattr(Credential, 'xmlsec1_path', None):
316 # Find a xmlsec1 binary path
317 Credential.xmlsec1_path = ''
318 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
319 try: paths += os.getenv('PATH').split(':')
322 xmlsec1 = os.path.join(path, 'xmlsec1')
323 if os.path.isfile(xmlsec1):
324 Credential.xmlsec1_path = xmlsec1
326 if not Credential.xmlsec1_path:
327 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
328 return Credential.xmlsec1_path
330 def get_subject(self):
331 if not self.gidObject:
333 return self.gidObject.get_subject()
335 def pretty_subject(self):
337 if not self.gidObject:
340 subject = self.gidObject.pretty_cert()
343 # sounds like this should be __repr__ instead ??
344 def pretty_cred(self):
345 if not self.gidObject:
347 obj = self.gidObject.pretty_cert()
348 caller = self.gidCaller.pretty_cert()
349 exp = self.get_expiration()
350 # Summarize the rights too? The issuer?
351 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
353 def get_signature(self):
354 if not self.signature:
356 return self.signature
358 def set_signature(self, sig):
363 # Need the issuer's private key and name
364 # @param key Keypair object containing the private key of the issuer
365 # @param gid GID of the issuing authority
367 def set_issuer_keys(self, privkey, gid):
368 self.issuer_privkey = privkey
369 self.issuer_gid = gid
373 # Set this credential's parent
374 def set_parent(self, cred):
379 # set the GID of the caller
381 # @param gid GID object of the caller
383 def set_gid_caller(self, gid):
385 # gid origin caller is the caller's gid by default
386 self.gidOriginCaller = gid
389 # get the GID of the object
391 def get_gid_caller(self):
392 if not self.gidCaller:
394 return self.gidCaller
397 # set the GID of the object
399 # @param gid GID object of the object
401 def set_gid_object(self, gid):
405 # get the GID of the object
407 def get_gid_object(self):
408 if not self.gidObject:
410 return self.gidObject
413 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
415 def set_expiration(self, expiration):
416 expiration_datetime = utcparse (expiration)
417 if expiration_datetime is not None:
418 self.expiration = expiration_datetime
420 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
423 # get the lifetime of the credential (always in datetime format)
425 def get_expiration(self):
426 if not self.expiration:
428 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
429 return self.expiration
434 # @param privs either a comma-separated list of privileges of a Rights object
436 def set_privileges(self, privs):
437 if isinstance(privs, str):
438 self.privileges = Rights(string = privs)
440 self.privileges = privs
443 # return the privileges as a Rights object
445 def get_privileges(self):
446 if not self.privileges:
448 return self.privileges
451 # determine whether the credential allows a particular operation to be
454 # @param op_name string specifying name of operation ("lookup", "update", etc)
456 def can_perform(self, op_name):
457 rights = self.get_privileges()
462 return rights.can_perform(op_name)
466 # Encode the attributes of the credential into an XML string
467 # This should be done immediately before signing the credential.
469 # In general, a signed credential obtained externally should
470 # not be changed else the signature is no longer valid. So, once
471 # you have loaded an existing signed credential, do not call encode() or sign() on it.
474 # Create the XML document
476 signed_cred = doc.createElement("signed-credential")
479 # Note that credential/policy.xsd are really the PG schemas
481 # Note that delegation of credentials between the 2 only really works
482 # cause those schemas are identical.
483 # Also note these PG schemas talk about PG tickets and CM policies.
484 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
485 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
486 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
487 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")
489 # PG says for those last 2:
490 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
491 # 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")
493 doc.appendChild(signed_cred)
495 # Fill in the <credential> bit
496 cred = doc.createElement("credential")
497 cred.setAttribute("xml:id", self.get_refid())
498 signed_cred.appendChild(cred)
499 append_sub(doc, cred, "type", "privilege")
500 append_sub(doc, cred, "serial", "8")
501 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
502 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
503 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
504 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
505 append_sub(doc, cred, "uuid", "")
506 if not self.expiration:
507 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
508 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
509 self.expiration = self.expiration.replace(microsecond=0)
510 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
511 # TZ aware. Make sure it is UTC - by Aaron Helsinger
512 self.expiration = self.expiration.astimezone(tz.tzutc())
513 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
514 privileges = doc.createElement("privileges")
515 cred.appendChild(privileges)
518 rights = self.get_privileges()
519 for right in rights.rights:
520 priv = doc.createElement("privilege")
521 append_sub(doc, priv, "name", right.kind)
522 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
523 privileges.appendChild(priv)
525 # Add the parent credential if it exists
527 sdoc = parseString(self.parent.get_xml())
528 # If the root node is a signed-credential (it should be), then
529 # get all its attributes and attach those to our signed_cred
531 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
532 # and we need to include those again here or else their signature
533 # no longer matches on the credential.
534 # We expect three of these, but here we copy them all:
535 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
536 # and from PG (PL is equivalent, as shown above):
537 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
538 # 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")
541 # PL now also declares these, with different URLs, so
542 # the code notices those attributes already existed with
543 # different values, and complains.
544 # This happens regularly on delegation now that PG and
545 # PL both declare the namespace with different URLs.
546 # If the content ever differs this is a problem,
547 # but for now it works - different URLs (values in the attributes)
548 # but the same actual schema, so using the PG schema
549 # on delegated-to-PL credentials works fine.
551 # Note: you could also not copy attributes
552 # which already exist. It appears that both PG and PL
553 # will actually validate a slicecred with a parent
554 # signed using PG namespaces and a child signed with PL
555 # namespaces over the whole thing. But I don't know
556 # if that is a bug in xmlsec1, an accident since
557 # the contents of the schemas are the same,
558 # or something else, but it seems odd. And this works.
559 parentRoot = sdoc.documentElement
560 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
561 for attrIx in range(0, parentRoot.attributes.length):
562 attr = parentRoot.attributes.item(attrIx)
563 # returns the old attribute of same name that was
565 # Below throws InUse exception if we forgot to clone the attribute first
566 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
567 if oldAttr and oldAttr.value != attr.value:
568 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
569 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
571 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
573 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
574 p = doc.createElement("parent")
575 p.appendChild(p_cred)
577 # done handling parent credential
579 # Create the <signatures> tag
580 signatures = doc.createElement("signatures")
581 signed_cred.appendChild(signatures)
583 # Add any parent signatures
585 for cur_cred in self.get_credential_list()[1:]:
586 sdoc = parseString(cur_cred.get_signature().get_xml())
587 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
588 signatures.appendChild(ele)
590 # Get the finished product
591 self.xml = doc.toxml("utf-8")
594 def save_to_random_tmp_file(self):
595 fp, filename = mkstemp(suffix='cred', text=True)
596 fp = os.fdopen(fp, "w")
597 self.save_to_file(filename, save_parents=True, filep=fp)
600 def save_to_file(self, filename, save_parents=True, filep=None):
606 f = open(filename, "w")
610 def save_to_string(self, save_parents=True):
620 def set_refid(self, rid):
624 # Figure out what refids exist, and update this credential's id
625 # so that it doesn't clobber the others. Returns the refids of
628 def updateRefID(self):
630 self.set_refid('ref0')
635 next_cred = self.parent
637 refs.append(next_cred.get_refid())
639 next_cred = next_cred.parent
644 # Find a unique refid for this credential
645 rid = self.get_refid()
648 rid = "ref%d" % (val + 1)
653 # Return the set of parent credential ref ids
662 # Sign the XML file created by encode()
665 # In general, a signed credential obtained externally should
666 # not be changed else the signature is no longer valid. So, once
667 # you have loaded an existing signed credential, do not call encode() or sign() on it.
670 if not self.issuer_privkey:
671 logger.warn("Cannot sign credential (no private key)")
673 if not self.issuer_gid:
674 logger.warn("Cannot sign credential (no issuer gid)")
676 doc = parseString(self.get_xml())
677 sigs = doc.getElementsByTagName("signatures")[0]
679 # Create the signature template to be signed
680 signature = Signature()
681 signature.set_refid(self.get_refid())
682 sdoc = parseString(signature.get_xml())
683 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
684 sigs.appendChild(sig_ele)
686 self.xml = doc.toxml("utf-8")
689 # Split the issuer GID into multiple certificates if it's a chain
690 chain = GID(filename=self.issuer_gid)
693 gid_files.append(chain.save_to_random_tmp_file(False))
694 if chain.get_parent():
695 chain = chain.get_parent()
700 # Call out to xmlsec1 to sign it
701 ref = 'Sig_%s' % self.get_refid()
702 filename = self.save_to_random_tmp_file()
703 xmlsec1 = self.get_xmlsec1_path()
705 raise Exception("Could not locate required 'xmlsec1' program")
706 command = '%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
707 % (xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
708 # print 'command',command
709 signed = os.popen(command).read()
712 for gid_file in gid_files:
722 # Retrieve the attributes of the credential from the XML.
723 # This is automatically called by the various get_* methods of
724 # this class and should not need to be called explicitly.
732 doc = parseString(self.xml)
733 except ExpatError as e:
734 raise CredentialNotVerifiable("Malformed credential")
735 doc = parseString(self.xml)
737 signed_cred = doc.getElementsByTagName("signed-credential")
739 # Is this a signed-cred or just a cred?
740 if len(signed_cred) > 0:
741 creds = signed_cred[0].getElementsByTagName("credential")
742 signatures = signed_cred[0].getElementsByTagName("signatures")
743 if len(signatures) > 0:
744 sigs = signatures[0].getElementsByTagName("Signature")
746 creds = doc.getElementsByTagName("credential")
748 if creds is None or len(creds) == 0:
749 # malformed cred file
750 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
752 # Just take the first cred if there are more than one
755 self.set_refid(cred.getAttribute("xml:id"))
756 self.set_expiration(utcparse(getTextNode(cred, "expires")))
757 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
758 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
761 ## This code until the end of function rewritten by Aaron Helsinger
764 priv_nodes = cred.getElementsByTagName("privileges")
765 if len(priv_nodes) > 0:
766 privs = priv_nodes[0]
767 for priv in privs.getElementsByTagName("privilege"):
768 kind = getTextNode(priv, "name")
769 deleg = str2bool(getTextNode(priv, "can_delegate"))
771 # Convert * into the default privileges for the credential's type
772 # Each inherits the delegatability from the * above
773 _ , type = urn_to_hrn(self.gidObject.get_urn())
774 rl = determine_rights(type, self.gidObject.get_urn())
779 rlist.add(Right(kind.strip(), deleg))
780 self.set_privileges(rlist)
784 parent = cred.getElementsByTagName("parent")
786 parent_doc = parent[0].getElementsByTagName("credential")[0]
787 parent_xml = parent_doc.toxml("utf-8")
788 if parent_xml is None or parent_xml.strip() == "":
789 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
790 self.parent = Credential(string=parent_xml)
793 # Assign the signatures to the credentials
795 Sig = Signature(string=sig.toxml("utf-8"))
797 for cur_cred in self.get_credential_list():
798 if cur_cred.get_refid() == Sig.get_refid():
799 cur_cred.set_signature(Sig)
804 # trusted_certs: A list of trusted GID filenames (not GID objects!)
805 # Chaining is not supported within the GIDs by xmlsec1.
807 # trusted_certs_required: Should usually be true. Set False means an
808 # empty list of trusted_certs would still let this method pass.
809 # It just skips xmlsec1 verification et al. Only used by some utils
812 # . All of the signatures are valid and that the issuers trace back
813 # to trusted roots (performed by xmlsec1)
814 # . The XML matches the credential schema
815 # . That the issuer of the credential is the authority in the target's urn
816 # . In the case of a delegated credential, this must be true of the root
817 # . That all of the gids presented in the credential are valid
818 # . Including verifying GID chains, and includ the issuer
819 # . The credential is not expired
821 # -- For Delegates (credentials with parents)
822 # . The privileges must be a subset of the parent credentials
823 # . The privileges must have "can_delegate" set for each delegated privilege
824 # . The target gid must be the same between child and parents
825 # . The expiry time on the child must be no later than the parent
826 # . The signer of the child must be the owner of the parent
828 # -- Verify does *NOT*
829 # . ensure that an xmlrpc client's gid matches a credential gid, that
830 # must be done elsewhere
832 # @param trusted_certs: The certificates of trusted CA certificates
833 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
837 # validate against RelaxNG schema
839 if schema and os.path.exists(schema):
840 tree = etree.parse(StringIO(self.xml))
841 schema_doc = etree.parse(schema)
842 xmlschema = etree.XMLSchema(schema_doc)
843 if not xmlschema.validate(tree):
844 error = xmlschema.error_log.last_error
845 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
846 raise CredentialNotVerifiable(message)
848 if trusted_certs_required and trusted_certs is None:
851 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
852 trusted_cert_objects = []
853 ok_trusted_certs = []
854 # If caller explicitly passed in None that means skip cert chain validation.
855 # Strange and not typical
856 if trusted_certs is not None:
857 for f in trusted_certs:
859 # Failures here include unreadable files
861 trusted_cert_objects.append(GID(filename=f))
862 ok_trusted_certs.append(f)
863 except Exception as exc:
864 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
865 trusted_certs = ok_trusted_certs
867 # make sure it is not expired
868 if self.get_expiration() < datetime.datetime.utcnow():
869 raise CredentialNotVerifiable("Credential %s expired at %s" % \
871 self.expiration.strftime(SFATIME_FORMAT)))
873 # Verify the signatures
874 filename = self.save_to_random_tmp_file()
876 # If caller explicitly passed in None that means skip cert chain validation.
877 # - Strange and not typical
878 if trusted_certs is not None:
879 # Verify the gids of this cred and of its parents
880 for cur_cred in self.get_credential_list():
881 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
882 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
885 refs.append("Sig_%s" % self.get_refid())
887 parentRefs = self.updateRefID()
888 for ref in parentRefs:
889 refs.append("Sig_%s" % ref)
892 # If caller explicitly passed in None that means skip xmlsec1 validation.
893 # Strange and not typical
894 if trusted_certs is None:
898 # up to fedora20 we used os.popen and checked that the output begins with OK
899 # turns out, with fedora21, there is extra input before this 'OK' thing
900 # looks like we're better off just using the exit code - that's what it is made for
901 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
902 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
903 # format(self.xmlsec_path, ref, cert_args, filename)
904 xmlsec1 = self.get_xmlsec1_path()
906 raise Exception("Could not locate required 'xmlsec1' program")
907 command = [ xmlsec1, '--verify', '--node-id', ref ]
908 for trusted in trusted_certs:
909 command += ["--trusted-pem", trusted ]
910 command += [ filename ]
911 logger.debug("Running " + " ".join(command))
913 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
914 logger.debug("xmlsec command returned {}".format(verified))
915 if "OK\n" not in verified:
916 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
917 except subprocess.CalledProcessError as e:
919 # xmlsec errors have a msg= which is the interesting bit.
920 mstart = verified.find("msg=")
922 if mstart > -1 and len(verified) > 4:
924 mend = verified.find('\\', mstart)
925 msg = verified[mstart:mend]
926 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
927 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
928 (self.pretty_cred(), ref, msg))
931 # Verify the parents (delegation)
933 self.verify_parent(self.parent)
935 # Make sure the issuer is the target's authority, and is
937 self.verify_issuer(trusted_cert_objects)
941 # Creates a list of the credential and its parents, with the root
942 # (original delegated credential) as the last item in the list
943 def get_credential_list(self):
947 list.append(cur_cred)
949 cur_cred = cur_cred.parent
955 # Make sure the credential's target gid (a) was signed by or (b)
956 # is the same as the entity that signed the original credential,
957 # or (c) is an authority over the target's namespace.
958 # Also ensure that the credential issuer / signer itself has a valid
959 # GID signature chain (signed by an authority with namespace rights).
960 def verify_issuer(self, trusted_gids):
961 root_cred = self.get_credential_list()[-1]
962 root_target_gid = root_cred.get_gid_object()
963 if root_cred.get_signature() is None:
965 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
967 root_cred_signer = root_cred.get_signature().get_issuer_gid()
970 # Allow non authority to sign target and cred about target.
972 # Why do we need to allow non authorities to sign?
973 # If in the target gid validation step we correctly
974 # checked that the target is only signed by an authority,
975 # then this is just a special case of case 3.
976 # This short-circuit is the common case currently -
977 # and cause GID validation doesn't check 'authority',
978 # this allows users to generate valid slice credentials.
979 if root_target_gid.is_signed_by_cert(root_cred_signer):
980 # cred signer matches target signer, return success
984 # Allow someone to sign credential about themeselves. Used?
985 # If not, remove this.
986 #root_target_gid_str = root_target_gid.save_to_string()
987 #root_cred_signer_str = root_cred_signer.save_to_string()
988 #if root_target_gid_str == root_cred_signer_str:
989 # # cred signer is target, return success
994 # root_cred_signer is not the target_gid
995 # So this is a different gid that we have not verified.
996 # xmlsec1 verified the cert chain on this already, but
997 # it hasn't verified that the gid meets the HRN namespace
999 # Below we'll ensure that it is an authority.
1000 # But we haven't verified that it is _signed by_ an authority
1001 # We also don't know if xmlsec1 requires that cert signers
1002 # are marked as CAs.
1004 # Note that if verify() gave us no trusted_gids then this
1005 # call will fail. So skip it if we have no trusted_gids
1006 if trusted_gids and len(trusted_gids) > 0:
1007 root_cred_signer.verify_chain(trusted_gids)
1009 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1010 "No trusted gids. Skipping that check.")
1012 # See if the signer is an authority over the domain of the target.
1013 # There are multiple types of authority - accept them all here
1014 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1015 root_cred_signer_type = root_cred_signer.get_type()
1016 if root_cred_signer_type.find('authority') == 0:
1017 #logger.debug('Cred signer is an authority')
1018 # signer is an authority, see if target is in authority's domain
1019 signerhrn = root_cred_signer.get_hrn()
1020 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1023 # We've required that the credential be signed by an authority
1024 # for that domain. Reasonable and probably correct.
1025 # A looser model would also allow the signer to be an authority
1026 # in my control framework - eg My CA or CH. Even if it is not
1027 # the CH that issued these, eg, user credentials.
1029 # Give up, credential does not pass issuer verification
1031 raise CredentialNotVerifiable(
1032 "Could not verify credential owned by {} for object {}. "
1033 "Cred signer {} not the trusted authority for Cred target {}"
1034 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1035 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1038 # -- For Delegates (credentials with parents) verify that:
1039 # . The privileges must be a subset of the parent credentials
1040 # . The privileges must have "can_delegate" set for each delegated privilege
1041 # . The target gid must be the same between child and parents
1042 # . The expiry time on the child must be no later than the parent
1043 # . The signer of the child must be the owner of the parent
1044 def verify_parent(self, parent_cred):
1045 # make sure the rights given to the child are a subset of the
1046 # parents rights (and check delegate bits)
1047 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1049 "Parent cred {} (ref {}) rights {} "
1050 " not superset of delegated cred {} (ref {}) rights {}"
1051 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1052 parent_cred.get_privileges().pretty_rights(),
1053 self.pretty_cred(), self.get_refid(),
1054 self.get_privileges().pretty_rights()))
1055 logger.error(message)
1056 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1057 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1058 raise ChildRightsNotSubsetOfParent(message)
1060 # make sure my target gid is the same as the parent's
1061 if not parent_cred.get_gid_object().save_to_string() == \
1062 self.get_gid_object().save_to_string():
1064 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1065 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1066 logger.error(message)
1067 logger.error("parent details {}".format(parent_cred.save_to_string()))
1068 logger.error("self details {}".format(self.save_to_string()))
1069 raise CredentialNotVerifiable(message)
1071 # make sure my expiry time is <= my parent's
1072 if not parent_cred.get_expiration() >= self.get_expiration():
1073 raise CredentialNotVerifiable(
1074 "Delegated credential {} expires after parent {}"
1075 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1077 # make sure my signer is the parent's caller
1078 if not parent_cred.get_gid_caller().save_to_string(False) == \
1079 self.get_signature().get_issuer_gid().save_to_string(False):
1080 message = "Delegated credential {} not signed by parent {}'s caller"\
1081 .format(self.pretty_cred(), parent_cred.pretty_cred())
1082 logger.error(message)
1083 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1084 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1085 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1086 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1087 raise CredentialNotVerifiable(message)
1090 if parent_cred.parent:
1091 parent_cred.verify_parent(parent_cred.parent)
1094 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1096 Return a delegated copy of this credential, delegated to the
1097 specified gid's user.
1099 # get the gid of the object we are delegating
1100 object_gid = self.get_gid_object()
1101 object_hrn = object_gid.get_hrn()
1103 # the hrn of the user who will be delegated to
1104 delegee_gid = GID(filename=delegee_gidfile)
1105 delegee_hrn = delegee_gid.get_hrn()
1107 #user_key = Keypair(filename=keyfile)
1108 #user_hrn = self.get_gid_caller().get_hrn()
1109 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1110 dcred = Credential(subject=subject_string)
1111 dcred.set_gid_caller(delegee_gid)
1112 dcred.set_gid_object(object_gid)
1113 dcred.set_parent(self)
1114 dcred.set_expiration(self.get_expiration())
1115 dcred.set_privileges(self.get_privileges())
1116 dcred.get_privileges().delegate_all_privileges(True)
1117 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1118 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1125 def get_filename(self):
1126 return getattr(self,'filename',None)
1128 def actual_caller_hrn (self):
1129 """a helper method used by some API calls like e.g. Allocate
1130 to try and find out who really is the original caller
1132 This admittedly is a bit of a hack, please USE IN LAST RESORT
1134 This code uses a heuristic to identify a delegated credential
1136 A first known restriction if for traffic that gets through a slice manager
1137 in this case the hrn reported is the one from the last SM in the call graph
1138 which is not at all what is meant here"""
1140 caller_hrn = self.get_gid_caller().get_hrn()
1141 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1142 subject_hrn = self.get_gid_object().get_hrn()
1143 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1144 # this seems to be a 'regular' credential
1145 if caller_hrn.startswith(issuer_hrn):
1146 actual_caller_hrn=caller_hrn
1147 # else this looks like a delegated credential, and the real caller is the issuer
1149 actual_caller_hrn=issuer_hrn
1150 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1151 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1152 return actual_caller_hrn
1155 # Dump the contents of a credential to stdout in human-readable format
1157 # @param dump_parents If true, also dump the parent certificates
1158 def dump (self, *args, **kwargs):
1159 print(self.dump_string(*args, **kwargs))
1161 # SFA code ignores show_xml and disables printing the cred xml
1162 def dump_string(self, dump_parents=False, show_xml=False):
1164 result += "CREDENTIAL %s\n" % self.pretty_subject()
1165 filename=self.get_filename()
1166 if filename: result += "Filename %s\n"%filename
1167 privileges = self.get_privileges()
1169 result += " privs: %s\n" % privileges.save_to_string()
1171 result += " privs: \n"
1172 gidCaller = self.get_gid_caller()
1174 result += " gidCaller:\n"
1175 result += gidCaller.dump_string(8, dump_parents)
1177 if self.get_signature():
1178 result += " gidIssuer:\n"
1179 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1182 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1184 gidObject = self.get_gid_object()
1186 result += " gidObject:\n"
1187 result += gidObject.dump_string(8, dump_parents)
1189 if self.parent and dump_parents:
1190 result += "\nPARENT"
1191 result += self.parent.dump_string(True)
1193 if show_xml and HAVELXML:
1195 tree = etree.parse(StringIO(self.xml))
1196 aside = etree.tostring(tree, pretty_print=True)
1197 result += "\nXML:\n\n"
1199 result += "\nEnd XML\n"
1202 print("exc. Credential.dump_string / XML")
1203 traceback.print_exc()