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
31 from types import StringTypes
33 from StringIO import StringIO
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
39 from lxml import etree
44 from xml.parsers.expat import ExpatError
46 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
47 from sfa.util.sfalogging import logger
48 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
49 from sfa.trust.rights import Right, Rights, determine_rights
50 from sfa.trust.gid import GID
51 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
54 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
58 # . make privs match between PG and PL
59 # . Need to add support for other types of credentials, e.g. tickets
60 # . add namespaces to signed-credential element?
62 signature_template = \
64 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
66 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
67 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
70 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
72 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
73 <DigestValue></DigestValue>
88 # PG formats the template (whitespace) slightly differently.
89 # Note that they don't include the xmlns in the template, but add it later.
90 # Otherwise the two are equivalent.
91 #signature_template_as_in_pg = \
93 #<Signature xml:id="Sig_%s" >
95 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
96 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
97 # <Reference URI="#%s">
99 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
101 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
102 # <DigestValue></DigestValue>
109 # <X509IssuerSerial/>
118 # Convert a string into a bool
119 # used to convert an xsd:boolean to a Python boolean
121 if str.lower() in ['true','1']:
127 # Utility function to get the text of an XML element
129 def getTextNode(element, subele):
130 sub = element.getElementsByTagName(subele)[0]
131 if len(sub.childNodes) > 0:
132 return sub.childNodes[0].nodeValue
137 # Utility function to set the text of an XML element
138 # It creates the element, adds the text to it,
139 # and then appends it to the parent.
141 def append_sub(doc, parent, element, text):
142 ele = doc.createElement(element)
143 ele.appendChild(doc.createTextNode(text))
144 parent.appendChild(ele)
147 # Signature contains information about an xmlsec1 signature
148 # for a signed-credential
151 class Signature(object):
153 def __init__(self, string=None):
155 self.issuer_gid = None
172 def set_refid(self, id):
175 def get_issuer_gid(self):
180 def set_issuer_gid(self, gid):
184 # Helper function to pull characters off the front of a string if present
185 def remove_prefix(text, prefix):
186 if text and prefix and text.startswith(prefix):
187 return text[len(prefix):]
191 doc = parseString(self.xml)
193 logger.log_exc ("Failed to parse credential, %s"%self.xml)
195 sig = doc.getElementsByTagName("Signature")[0]
196 ## This code until the end of function rewritten by Aaron Helsinger
197 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
198 # The xml:id tag is optional, and could be in a
199 # Reference xml:id or Reference UID sub element instead
200 if not ref_id or ref_id == '':
201 reference = sig.getElementsByTagName('Reference')[0]
202 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
203 if not ref_id or ref_id == '':
204 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
205 self.set_refid(ref_id)
206 keyinfos = sig.getElementsByTagName("X509Data")
208 for keyinfo in keyinfos:
209 certs = keyinfo.getElementsByTagName("X509Certificate")
211 if len(cert.childNodes) > 0:
212 szgid = cert.childNodes[0].nodeValue
213 szgid = szgid.strip()
214 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
220 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
221 self.set_issuer_gid(GID(string=gids))
224 self.xml = signature_template % (self.get_refid(), self.get_refid())
227 # A credential provides a caller gid with privileges to an object gid.
228 # A signed credential is signed by the object's authority.
230 # Credentials are encoded in one of two ways.
231 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
232 # The new credentials are placed in signed XML.
235 # In general, a signed credential obtained externally should
236 # not be changed else the signature is no longer valid. So, once
237 # you have loaded an existing signed credential, do not call encode() or sign() on it.
239 def filter_creds_by_caller(creds, caller_hrn_list):
241 Returns a list of creds who's gid caller matches the
244 if not isinstance(creds, list): creds = [creds]
245 if not isinstance(caller_hrn_list, list):
246 caller_hrn_list = [caller_hrn_list]
250 tmp_cred = Credential(string=cred)
251 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
253 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
254 caller_creds.append(cred)
258 class Credential(object):
260 SFA_CREDENTIAL_TYPE = "geni_sfa"
263 # Create a Credential object
265 # @param create If true, create a blank x509 certificate
266 # @param subject If subject!=None, create an x509 cert with the subject name
267 # @param string If string!=None, load the credential from the string
268 # @param filename If filename!=None, load the credential from the file
269 # FIXME: create and subject are ignored!
270 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
271 self.gidCaller = None
272 self.gidObject = None
273 self.expiration = None
274 self.privileges = None
275 self.issuer_privkey = None
276 self.issuer_gid = None
277 self.issuer_pubkey = None
279 self.signature = None
282 self.type = Credential.SFA_CREDENTIAL_TYPE
286 if isinstance(cred, StringTypes):
288 self.type = Credential.SFA_CREDENTIAL_TYPE
290 elif isinstance(cred, dict):
291 string = cred['geni_value']
292 self.type = cred['geni_type']
293 self.version = cred['geni_version']
295 if string or filename:
299 str = file(filename).read()
301 # if this is a legacy credential, write error and bail out
302 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
303 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
308 # not strictly necessary but won't hurt either
309 self.get_xmlsec1_path()
312 def get_xmlsec1_path():
313 if not getattr(Credential, 'xmlsec1_path', None):
314 # Find a xmlsec1 binary path
315 Credential.xmlsec1_path = ''
316 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
317 try: paths += os.getenv('PATH').split(':')
320 xmlsec1 = os.path.join(path, 'xmlsec1')
321 if os.path.isfile(xmlsec1):
322 Credential.xmlsec1_path = xmlsec1
324 if not Credential.xmlsec1_path:
325 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
326 return Credential.xmlsec1_path
328 def get_subject(self):
329 if not self.gidObject:
331 return self.gidObject.get_subject()
333 def pretty_subject(self):
335 if not self.gidObject:
338 subject = self.gidObject.pretty_cert()
341 # sounds like this should be __repr__ instead ??
342 def pretty_cred(self):
343 if not self.gidObject:
345 obj = self.gidObject.pretty_cert()
346 caller = self.gidCaller.pretty_cert()
347 exp = self.get_expiration()
348 # Summarize the rights too? The issuer?
349 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
351 def get_signature(self):
352 if not self.signature:
354 return self.signature
356 def set_signature(self, sig):
361 # Need the issuer's private key and name
362 # @param key Keypair object containing the private key of the issuer
363 # @param gid GID of the issuing authority
365 def set_issuer_keys(self, privkey, gid):
366 self.issuer_privkey = privkey
367 self.issuer_gid = gid
371 # Set this credential's parent
372 def set_parent(self, cred):
377 # set the GID of the caller
379 # @param gid GID object of the caller
381 def set_gid_caller(self, gid):
383 # gid origin caller is the caller's gid by default
384 self.gidOriginCaller = gid
387 # get the GID of the object
389 def get_gid_caller(self):
390 if not self.gidCaller:
392 return self.gidCaller
395 # set the GID of the object
397 # @param gid GID object of the object
399 def set_gid_object(self, gid):
403 # get the GID of the object
405 def get_gid_object(self):
406 if not self.gidObject:
408 return self.gidObject
411 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
413 def set_expiration(self, expiration):
414 expiration_datetime = utcparse (expiration)
415 if expiration_datetime is not None:
416 self.expiration = expiration_datetime
418 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
421 # get the lifetime of the credential (always in datetime format)
423 def get_expiration(self):
424 if not self.expiration:
426 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
427 return self.expiration
432 # @param privs either a comma-separated list of privileges of a Rights object
434 def set_privileges(self, privs):
435 if isinstance(privs, str):
436 self.privileges = Rights(string = privs)
438 self.privileges = privs
441 # return the privileges as a Rights object
443 def get_privileges(self):
444 if not self.privileges:
446 return self.privileges
449 # determine whether the credential allows a particular operation to be
452 # @param op_name string specifying name of operation ("lookup", "update", etc)
454 def can_perform(self, op_name):
455 rights = self.get_privileges()
460 return rights.can_perform(op_name)
464 # Encode the attributes of the credential into an XML string
465 # This should be done immediately before signing the credential.
467 # In general, a signed credential obtained externally should
468 # not be changed else the signature is no longer valid. So, once
469 # you have loaded an existing signed credential, do not call encode() or sign() on it.
472 # Create the XML document
474 signed_cred = doc.createElement("signed-credential")
477 # Note that credential/policy.xsd are really the PG schemas
479 # Note that delegation of credentials between the 2 only really works
480 # cause those schemas are identical.
481 # Also note these PG schemas talk about PG tickets and CM policies.
482 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
483 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
484 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
485 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")
487 # PG says for those last 2:
488 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
489 # 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")
491 doc.appendChild(signed_cred)
493 # Fill in the <credential> bit
494 cred = doc.createElement("credential")
495 cred.setAttribute("xml:id", self.get_refid())
496 signed_cred.appendChild(cred)
497 append_sub(doc, cred, "type", "privilege")
498 append_sub(doc, cred, "serial", "8")
499 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
500 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
501 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
502 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
503 append_sub(doc, cred, "uuid", "")
504 if not self.expiration:
505 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
506 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
507 self.expiration = self.expiration.replace(microsecond=0)
508 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
509 # TZ aware. Make sure it is UTC - by Aaron Helsinger
510 self.expiration = self.expiration.astimezone(tz.tzutc())
511 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
512 privileges = doc.createElement("privileges")
513 cred.appendChild(privileges)
516 rights = self.get_privileges()
517 for right in rights.rights:
518 priv = doc.createElement("privilege")
519 append_sub(doc, priv, "name", right.kind)
520 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
521 privileges.appendChild(priv)
523 # Add the parent credential if it exists
525 sdoc = parseString(self.parent.get_xml())
526 # If the root node is a signed-credential (it should be), then
527 # get all its attributes and attach those to our signed_cred
529 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
530 # and we need to include those again here or else their signature
531 # no longer matches on the credential.
532 # We expect three of these, but here we copy them all:
533 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
534 # and from PG (PL is equivalent, as shown above):
535 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
536 # 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")
539 # PL now also declares these, with different URLs, so
540 # the code notices those attributes already existed with
541 # different values, and complains.
542 # This happens regularly on delegation now that PG and
543 # PL both declare the namespace with different URLs.
544 # If the content ever differs this is a problem,
545 # but for now it works - different URLs (values in the attributes)
546 # but the same actual schema, so using the PG schema
547 # on delegated-to-PL credentials works fine.
549 # Note: you could also not copy attributes
550 # which already exist. It appears that both PG and PL
551 # will actually validate a slicecred with a parent
552 # signed using PG namespaces and a child signed with PL
553 # namespaces over the whole thing. But I don't know
554 # if that is a bug in xmlsec1, an accident since
555 # the contents of the schemas are the same,
556 # or something else, but it seems odd. And this works.
557 parentRoot = sdoc.documentElement
558 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
559 for attrIx in range(0, parentRoot.attributes.length):
560 attr = parentRoot.attributes.item(attrIx)
561 # returns the old attribute of same name that was
563 # Below throws InUse exception if we forgot to clone the attribute first
564 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
565 if oldAttr and oldAttr.value != attr.value:
566 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
567 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
569 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
571 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
572 p = doc.createElement("parent")
573 p.appendChild(p_cred)
575 # done handling parent credential
577 # Create the <signatures> tag
578 signatures = doc.createElement("signatures")
579 signed_cred.appendChild(signatures)
581 # Add any parent signatures
583 for cur_cred in self.get_credential_list()[1:]:
584 sdoc = parseString(cur_cred.get_signature().get_xml())
585 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
586 signatures.appendChild(ele)
588 # Get the finished product
589 self.xml = doc.toxml("utf-8")
592 def save_to_random_tmp_file(self):
593 fp, filename = mkstemp(suffix='cred', text=True)
594 fp = os.fdopen(fp, "w")
595 self.save_to_file(filename, save_parents=True, filep=fp)
598 def save_to_file(self, filename, save_parents=True, filep=None):
604 f = open(filename, "w")
608 def save_to_string(self, save_parents=True):
618 def set_refid(self, rid):
622 # Figure out what refids exist, and update this credential's id
623 # so that it doesn't clobber the others. Returns the refids of
626 def updateRefID(self):
628 self.set_refid('ref0')
633 next_cred = self.parent
635 refs.append(next_cred.get_refid())
637 next_cred = next_cred.parent
642 # Find a unique refid for this credential
643 rid = self.get_refid()
646 rid = "ref%d" % (val + 1)
651 # Return the set of parent credential ref ids
660 # Sign the XML file created by encode()
663 # In general, a signed credential obtained externally should
664 # not be changed else the signature is no longer valid. So, once
665 # you have loaded an existing signed credential, do not call encode() or sign() on it.
668 if not self.issuer_privkey:
669 logger.warn("Cannot sign credential (no private key)")
671 if not self.issuer_gid:
672 logger.warn("Cannot sign credential (no issuer gid)")
674 doc = parseString(self.get_xml())
675 sigs = doc.getElementsByTagName("signatures")[0]
677 # Create the signature template to be signed
678 signature = Signature()
679 signature.set_refid(self.get_refid())
680 sdoc = parseString(signature.get_xml())
681 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
682 sigs.appendChild(sig_ele)
684 self.xml = doc.toxml("utf-8")
687 # Split the issuer GID into multiple certificates if it's a chain
688 chain = GID(filename=self.issuer_gid)
691 gid_files.append(chain.save_to_random_tmp_file(False))
692 if chain.get_parent():
693 chain = chain.get_parent()
698 # Call out to xmlsec1 to sign it
699 ref = 'Sig_%s' % self.get_refid()
700 filename = self.save_to_random_tmp_file()
701 xmlsec1 = self.get_xmlsec1_path()
703 raise Exception("Could not locate required 'xmlsec1' program")
704 command = '%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
705 % (xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
706 # print 'command',command
707 signed = os.popen(command).read()
710 for gid_file in gid_files:
720 # Retrieve the attributes of the credential from the XML.
721 # This is automatically called by the various get_* methods of
722 # this class and should not need to be called explicitly.
730 doc = parseString(self.xml)
732 raise CredentialNotVerifiable("Malformed credential")
733 doc = parseString(self.xml)
735 signed_cred = doc.getElementsByTagName("signed-credential")
737 # Is this a signed-cred or just a cred?
738 if len(signed_cred) > 0:
739 creds = signed_cred[0].getElementsByTagName("credential")
740 signatures = signed_cred[0].getElementsByTagName("signatures")
741 if len(signatures) > 0:
742 sigs = signatures[0].getElementsByTagName("Signature")
744 creds = doc.getElementsByTagName("credential")
746 if creds is None or len(creds) == 0:
747 # malformed cred file
748 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
750 # Just take the first cred if there are more than one
753 self.set_refid(cred.getAttribute("xml:id"))
754 self.set_expiration(utcparse(getTextNode(cred, "expires")))
755 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
756 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
759 ## This code until the end of function rewritten by Aaron Helsinger
762 priv_nodes = cred.getElementsByTagName("privileges")
763 if len(priv_nodes) > 0:
764 privs = priv_nodes[0]
765 for priv in privs.getElementsByTagName("privilege"):
766 kind = getTextNode(priv, "name")
767 deleg = str2bool(getTextNode(priv, "can_delegate"))
769 # Convert * into the default privileges for the credential's type
770 # Each inherits the delegatability from the * above
771 _ , type = urn_to_hrn(self.gidObject.get_urn())
772 rl = determine_rights(type, self.gidObject.get_urn())
777 rlist.add(Right(kind.strip(), deleg))
778 self.set_privileges(rlist)
782 parent = cred.getElementsByTagName("parent")
784 parent_doc = parent[0].getElementsByTagName("credential")[0]
785 parent_xml = parent_doc.toxml("utf-8")
786 if parent_xml is None or parent_xml.strip() == "":
787 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
788 self.parent = Credential(string=parent_xml)
791 # Assign the signatures to the credentials
793 Sig = Signature(string=sig.toxml("utf-8"))
795 for cur_cred in self.get_credential_list():
796 if cur_cred.get_refid() == Sig.get_refid():
797 cur_cred.set_signature(Sig)
802 # trusted_certs: A list of trusted GID filenames (not GID objects!)
803 # Chaining is not supported within the GIDs by xmlsec1.
805 # trusted_certs_required: Should usually be true. Set False means an
806 # empty list of trusted_certs would still let this method pass.
807 # It just skips xmlsec1 verification et al. Only used by some utils
810 # . All of the signatures are valid and that the issuers trace back
811 # to trusted roots (performed by xmlsec1)
812 # . The XML matches the credential schema
813 # . That the issuer of the credential is the authority in the target's urn
814 # . In the case of a delegated credential, this must be true of the root
815 # . That all of the gids presented in the credential are valid
816 # . Including verifying GID chains, and includ the issuer
817 # . The credential is not expired
819 # -- For Delegates (credentials with parents)
820 # . The privileges must be a subset of the parent credentials
821 # . The privileges must have "can_delegate" set for each delegated privilege
822 # . The target gid must be the same between child and parents
823 # . The expiry time on the child must be no later than the parent
824 # . The signer of the child must be the owner of the parent
826 # -- Verify does *NOT*
827 # . ensure that an xmlrpc client's gid matches a credential gid, that
828 # must be done elsewhere
830 # @param trusted_certs: The certificates of trusted CA certificates
831 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
835 # validate against RelaxNG schema
837 if schema and os.path.exists(schema):
838 tree = etree.parse(StringIO(self.xml))
839 schema_doc = etree.parse(schema)
840 xmlschema = etree.XMLSchema(schema_doc)
841 if not xmlschema.validate(tree):
842 error = xmlschema.error_log.last_error
843 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
844 raise CredentialNotVerifiable(message)
846 if trusted_certs_required and trusted_certs is None:
849 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
850 trusted_cert_objects = []
851 ok_trusted_certs = []
852 # If caller explicitly passed in None that means skip cert chain validation.
853 # Strange and not typical
854 if trusted_certs is not None:
855 for f in trusted_certs:
857 # Failures here include unreadable files
859 trusted_cert_objects.append(GID(filename=f))
860 ok_trusted_certs.append(f)
861 except Exception, exc:
862 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
863 trusted_certs = ok_trusted_certs
865 # make sure it is not expired
866 if self.get_expiration() < datetime.datetime.utcnow():
867 raise CredentialNotVerifiable("Credential %s expired at %s" % \
869 self.expiration.strftime(SFATIME_FORMAT)))
871 # Verify the signatures
872 filename = self.save_to_random_tmp_file()
874 # If caller explicitly passed in None that means skip cert chain validation.
875 # - Strange and not typical
876 if trusted_certs is not None:
877 # Verify the gids of this cred and of its parents
878 for cur_cred in self.get_credential_list():
879 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
880 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
883 refs.append("Sig_%s" % self.get_refid())
885 parentRefs = self.updateRefID()
886 for ref in parentRefs:
887 refs.append("Sig_%s" % ref)
890 # If caller explicitly passed in None that means skip xmlsec1 validation.
891 # Strange and not typical
892 if trusted_certs is None:
896 # up to fedora20 we used os.popen and checked that the output begins with OK
897 # turns out, with fedora21, there is extra input before this 'OK' thing
898 # looks like we're better off just using the exit code - that's what it is made for
899 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
900 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
901 # format(self.xmlsec_path, ref, cert_args, filename)
902 xmlsec1 = self.get_xmlsec1_path()
904 raise Exception("Could not locate required 'xmlsec1' program")
905 command = [ xmlsec1, '--verify', '--node-id', ref ]
906 for trusted in trusted_certs:
907 command += ["--trusted-pem", trusted ]
908 command += [ filename ]
909 logger.debug("Running " + " ".join(command))
911 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
912 logger.debug("xmlsec command returned {}".format(verified))
913 if "OK\n" not in verified:
914 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
915 except subprocess.CalledProcessError as e:
917 # xmlsec errors have a msg= which is the interesting bit.
918 mstart = verified.find("msg=")
920 if mstart > -1 and len(verified) > 4:
922 mend = verified.find('\\', mstart)
923 msg = verified[mstart:mend]
924 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
925 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
926 (self.pretty_cred(), ref, msg))
929 # Verify the parents (delegation)
931 self.verify_parent(self.parent)
933 # Make sure the issuer is the target's authority, and is
935 self.verify_issuer(trusted_cert_objects)
939 # Creates a list of the credential and its parents, with the root
940 # (original delegated credential) as the last item in the list
941 def get_credential_list(self):
945 list.append(cur_cred)
947 cur_cred = cur_cred.parent
953 # Make sure the credential's target gid (a) was signed by or (b)
954 # is the same as the entity that signed the original credential,
955 # or (c) is an authority over the target's namespace.
956 # Also ensure that the credential issuer / signer itself has a valid
957 # GID signature chain (signed by an authority with namespace rights).
958 def verify_issuer(self, trusted_gids):
959 root_cred = self.get_credential_list()[-1]
960 root_target_gid = root_cred.get_gid_object()
961 if root_cred.get_signature() is None:
963 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
965 root_cred_signer = root_cred.get_signature().get_issuer_gid()
968 # Allow non authority to sign target and cred about target.
970 # Why do we need to allow non authorities to sign?
971 # If in the target gid validation step we correctly
972 # checked that the target is only signed by an authority,
973 # then this is just a special case of case 3.
974 # This short-circuit is the common case currently -
975 # and cause GID validation doesn't check 'authority',
976 # this allows users to generate valid slice credentials.
977 if root_target_gid.is_signed_by_cert(root_cred_signer):
978 # cred signer matches target signer, return success
982 # Allow someone to sign credential about themeselves. Used?
983 # If not, remove this.
984 #root_target_gid_str = root_target_gid.save_to_string()
985 #root_cred_signer_str = root_cred_signer.save_to_string()
986 #if root_target_gid_str == root_cred_signer_str:
987 # # cred signer is target, return success
992 # root_cred_signer is not the target_gid
993 # So this is a different gid that we have not verified.
994 # xmlsec1 verified the cert chain on this already, but
995 # it hasn't verified that the gid meets the HRN namespace
997 # Below we'll ensure that it is an authority.
998 # But we haven't verified that it is _signed by_ an authority
999 # We also don't know if xmlsec1 requires that cert signers
1000 # are marked as CAs.
1002 # Note that if verify() gave us no trusted_gids then this
1003 # call will fail. So skip it if we have no trusted_gids
1004 if trusted_gids and len(trusted_gids) > 0:
1005 root_cred_signer.verify_chain(trusted_gids)
1007 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1008 "No trusted gids. Skipping that check.")
1010 # See if the signer is an authority over the domain of the target.
1011 # There are multiple types of authority - accept them all here
1012 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1013 root_cred_signer_type = root_cred_signer.get_type()
1014 if root_cred_signer_type.find('authority') == 0:
1015 #logger.debug('Cred signer is an authority')
1016 # signer is an authority, see if target is in authority's domain
1017 signerhrn = root_cred_signer.get_hrn()
1018 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1021 # We've required that the credential be signed by an authority
1022 # for that domain. Reasonable and probably correct.
1023 # A looser model would also allow the signer to be an authority
1024 # in my control framework - eg My CA or CH. Even if it is not
1025 # the CH that issued these, eg, user credentials.
1027 # Give up, credential does not pass issuer verification
1029 raise CredentialNotVerifiable(
1030 "Could not verify credential owned by {} for object {}. "
1031 "Cred signer {} not the trusted authority for Cred target {}"
1032 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1033 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1036 # -- For Delegates (credentials with parents) verify that:
1037 # . The privileges must be a subset of the parent credentials
1038 # . The privileges must have "can_delegate" set for each delegated privilege
1039 # . The target gid must be the same between child and parents
1040 # . The expiry time on the child must be no later than the parent
1041 # . The signer of the child must be the owner of the parent
1042 def verify_parent(self, parent_cred):
1043 # make sure the rights given to the child are a subset of the
1044 # parents rights (and check delegate bits)
1045 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1047 "Parent cred {} (ref {}) rights {} "
1048 " not superset of delegated cred {} (ref {}) rights {}"
1049 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1050 parent_cred.get_privileges().pretty_rights(),
1051 self.pretty_cred(), self.get_refid(),
1052 self.get_privileges().pretty_rights()))
1053 logger.error(message)
1054 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1055 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1056 raise ChildRightsNotSubsetOfParent(message)
1058 # make sure my target gid is the same as the parent's
1059 if not parent_cred.get_gid_object().save_to_string() == \
1060 self.get_gid_object().save_to_string():
1062 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1063 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1064 logger.error(message)
1065 logger.error("parent details {}".format(parent_cred.save_to_string()))
1066 logger.error("self details {}".format(self.save_to_string()))
1067 raise CredentialNotVerifiable(message)
1069 # make sure my expiry time is <= my parent's
1070 if not parent_cred.get_expiration() >= self.get_expiration():
1071 raise CredentialNotVerifiable(
1072 "Delegated credential {} expires after parent {}"
1073 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1075 # make sure my signer is the parent's caller
1076 if not parent_cred.get_gid_caller().save_to_string(False) == \
1077 self.get_signature().get_issuer_gid().save_to_string(False):
1078 message = "Delegated credential {} not signed by parent {}'s caller"\
1079 .format(self.pretty_cred(), parent_cred.pretty_cred())
1080 logger.error(message)
1081 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1082 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1083 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1084 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1085 raise CredentialNotVerifiable(message)
1088 if parent_cred.parent:
1089 parent_cred.verify_parent(parent_cred.parent)
1092 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1094 Return a delegated copy of this credential, delegated to the
1095 specified gid's user.
1097 # get the gid of the object we are delegating
1098 object_gid = self.get_gid_object()
1099 object_hrn = object_gid.get_hrn()
1101 # the hrn of the user who will be delegated to
1102 delegee_gid = GID(filename=delegee_gidfile)
1103 delegee_hrn = delegee_gid.get_hrn()
1105 #user_key = Keypair(filename=keyfile)
1106 #user_hrn = self.get_gid_caller().get_hrn()
1107 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1108 dcred = Credential(subject=subject_string)
1109 dcred.set_gid_caller(delegee_gid)
1110 dcred.set_gid_object(object_gid)
1111 dcred.set_parent(self)
1112 dcred.set_expiration(self.get_expiration())
1113 dcred.set_privileges(self.get_privileges())
1114 dcred.get_privileges().delegate_all_privileges(True)
1115 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1116 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1123 def get_filename(self):
1124 return getattr(self,'filename',None)
1126 def actual_caller_hrn (self):
1127 """a helper method used by some API calls like e.g. Allocate
1128 to try and find out who really is the original caller
1130 This admittedly is a bit of a hack, please USE IN LAST RESORT
1132 This code uses a heuristic to identify a delegated credential
1134 A first known restriction if for traffic that gets through a slice manager
1135 in this case the hrn reported is the one from the last SM in the call graph
1136 which is not at all what is meant here"""
1138 caller_hrn = self.get_gid_caller().get_hrn()
1139 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1140 subject_hrn = self.get_gid_object().get_hrn()
1141 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1142 # this seems to be a 'regular' credential
1143 if caller_hrn.startswith(issuer_hrn):
1144 actual_caller_hrn=caller_hrn
1145 # else this looks like a delegated credential, and the real caller is the issuer
1147 actual_caller_hrn=issuer_hrn
1148 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1149 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1150 return actual_caller_hrn
1153 # Dump the contents of a credential to stdout in human-readable format
1155 # @param dump_parents If true, also dump the parent certificates
1156 def dump (self, *args, **kwargs):
1157 print self.dump_string(*args, **kwargs)
1159 # SFA code ignores show_xml and disables printing the cred xml
1160 def dump_string(self, dump_parents=False, show_xml=False):
1162 result += "CREDENTIAL %s\n" % self.pretty_subject()
1163 filename=self.get_filename()
1164 if filename: result += "Filename %s\n"%filename
1165 privileges = self.get_privileges()
1167 result += " privs: %s\n" % privileges.save_to_string()
1169 result += " privs: \n"
1170 gidCaller = self.get_gid_caller()
1172 result += " gidCaller:\n"
1173 result += gidCaller.dump_string(8, dump_parents)
1175 if self.get_signature():
1176 result += " gidIssuer:\n"
1177 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1180 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1182 gidObject = self.get_gid_object()
1184 result += " gidObject:\n"
1185 result += gidObject.dump_string(8, dump_parents)
1187 if self.parent and dump_parents:
1188 result += "\nPARENT"
1189 result += self.parent.dump_string(True)
1191 if show_xml and HAVELXML:
1193 tree = etree.parse(StringIO(self.xml))
1194 aside = etree.tostring(tree, pretty_print=True)
1195 result += "\nXML:\n\n"
1197 result += "\nEnd XML\n"
1200 print "exc. Credential.dump_string / XML"
1201 traceback.print_exc()