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):
185 doc = parseString(self.xml)
187 logger.log_exc ("Failed to parse credential, %s"%self.xml)
189 sig = doc.getElementsByTagName("Signature")[0]
190 ## This code until the end of function rewritten by Aaron Helsinger
191 ref_id = sig.getAttribute("xml:id").strip().strip("Sig_")
192 # The xml:id tag is optional, and could be in a
193 # Reference xml:id or Reference UID sub element instead
194 if not ref_id or ref_id == '':
195 reference = sig.getElementsByTagName('Reference')[0]
196 ref_id = reference.getAttribute('xml:id').strip().strip('Sig_')
197 if not ref_id or ref_id == '':
198 ref_id = reference.getAttribute('URI').strip().strip('#')
199 self.set_refid(ref_id)
200 keyinfos = sig.getElementsByTagName("X509Data")
202 for keyinfo in keyinfos:
203 certs = keyinfo.getElementsByTagName("X509Certificate")
205 if len(cert.childNodes) > 0:
206 szgid = cert.childNodes[0].nodeValue
207 szgid = szgid.strip()
208 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
214 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
215 self.set_issuer_gid(GID(string=gids))
218 self.xml = signature_template % (self.get_refid(), self.get_refid())
221 # A credential provides a caller gid with privileges to an object gid.
222 # A signed credential is signed by the object's authority.
224 # Credentials are encoded in one of two ways.
225 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
226 # The new credentials are placed in signed XML.
229 # In general, a signed credential obtained externally should
230 # not be changed else the signature is no longer valid. So, once
231 # you have loaded an existing signed credential, do not call encode() or sign() on it.
233 def filter_creds_by_caller(creds, caller_hrn_list):
235 Returns a list of creds who's gid caller matches the
238 if not isinstance(creds, list): creds = [creds]
239 if not isinstance(caller_hrn_list, list):
240 caller_hrn_list = [caller_hrn_list]
244 tmp_cred = Credential(string=cred)
245 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
247 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
248 caller_creds.append(cred)
252 class Credential(object):
254 SFA_CREDENTIAL_TYPE = "geni_sfa"
257 # Create a Credential object
259 # @param create If true, create a blank x509 certificate
260 # @param subject If subject!=None, create an x509 cert with the subject name
261 # @param string If string!=None, load the credential from the string
262 # @param filename If filename!=None, load the credential from the file
263 # FIXME: create and subject are ignored!
264 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
265 self.gidCaller = None
266 self.gidObject = None
267 self.expiration = None
268 self.privileges = None
269 self.issuer_privkey = None
270 self.issuer_gid = None
271 self.issuer_pubkey = None
273 self.signature = None
276 self.type = Credential.SFA_CREDENTIAL_TYPE
280 if isinstance(cred, StringTypes):
282 self.type = Credential.SFA_CREDENTIAL_TYPE
284 elif isinstance(cred, dict):
285 string = cred['geni_value']
286 self.type = cred['geni_type']
287 self.version = cred['geni_version']
289 if string or filename:
293 str = file(filename).read()
295 # if this is a legacy credential, write error and bail out
296 if isinstance (str, StringTypes) and str.strip().startswith("-----"):
297 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
302 # not strictly necessary but won't hurt either
303 self.get_xmlsec1_path()
306 def get_xmlsec1_path():
307 if not getattr(Credential, 'xmlsec1_path', None):
308 # Find a xmlsec1 binary path
309 Credential.xmlsec1_path = ''
310 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
311 try: paths += os.getenv('PATH').split(':')
314 xmlsec1 = os.path.join(path, 'xmlsec1')
315 if os.path.isfile(xmlsec1):
316 Credential.xmlsec1_path = xmlsec1
318 if not Credential.xmlsec1_path:
319 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
320 return Credential.xmlsec1_path
322 def get_subject(self):
323 if not self.gidObject:
325 return self.gidObject.get_subject()
327 def pretty_subject(self):
329 if not self.gidObject:
332 subject = self.gidObject.pretty_cert()
335 # sounds like this should be __repr__ instead ??
336 def pretty_cred(self):
337 if not self.gidObject:
339 obj = self.gidObject.pretty_cert()
340 caller = self.gidCaller.pretty_cert()
341 exp = self.get_expiration()
342 # Summarize the rights too? The issuer?
343 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
345 def get_signature(self):
346 if not self.signature:
348 return self.signature
350 def set_signature(self, sig):
355 # Need the issuer's private key and name
356 # @param key Keypair object containing the private key of the issuer
357 # @param gid GID of the issuing authority
359 def set_issuer_keys(self, privkey, gid):
360 self.issuer_privkey = privkey
361 self.issuer_gid = gid
365 # Set this credential's parent
366 def set_parent(self, cred):
371 # set the GID of the caller
373 # @param gid GID object of the caller
375 def set_gid_caller(self, gid):
377 # gid origin caller is the caller's gid by default
378 self.gidOriginCaller = gid
381 # get the GID of the object
383 def get_gid_caller(self):
384 if not self.gidCaller:
386 return self.gidCaller
389 # set the GID of the object
391 # @param gid GID object of the object
393 def set_gid_object(self, gid):
397 # get the GID of the object
399 def get_gid_object(self):
400 if not self.gidObject:
402 return self.gidObject
405 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
407 def set_expiration(self, expiration):
408 expiration_datetime = utcparse (expiration)
409 if expiration_datetime is not None:
410 self.expiration = expiration_datetime
412 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
415 # get the lifetime of the credential (always in datetime format)
417 def get_expiration(self):
418 if not self.expiration:
420 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
421 return self.expiration
426 # @param privs either a comma-separated list of privileges of a Rights object
428 def set_privileges(self, privs):
429 if isinstance(privs, str):
430 self.privileges = Rights(string = privs)
432 self.privileges = privs
435 # return the privileges as a Rights object
437 def get_privileges(self):
438 if not self.privileges:
440 return self.privileges
443 # determine whether the credential allows a particular operation to be
446 # @param op_name string specifying name of operation ("lookup", "update", etc)
448 def can_perform(self, op_name):
449 rights = self.get_privileges()
454 return rights.can_perform(op_name)
458 # Encode the attributes of the credential into an XML string
459 # This should be done immediately before signing the credential.
461 # In general, a signed credential obtained externally should
462 # not be changed else the signature is no longer valid. So, once
463 # you have loaded an existing signed credential, do not call encode() or sign() on it.
466 # Create the XML document
468 signed_cred = doc.createElement("signed-credential")
471 # Note that credential/policy.xsd are really the PG schemas
473 # Note that delegation of credentials between the 2 only really works
474 # cause those schemas are identical.
475 # Also note these PG schemas talk about PG tickets and CM policies.
476 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
477 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
478 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
479 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")
481 # PG says for those last 2:
482 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
483 # 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")
485 doc.appendChild(signed_cred)
487 # Fill in the <credential> bit
488 cred = doc.createElement("credential")
489 cred.setAttribute("xml:id", self.get_refid())
490 signed_cred.appendChild(cred)
491 append_sub(doc, cred, "type", "privilege")
492 append_sub(doc, cred, "serial", "8")
493 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
494 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
495 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
496 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
497 append_sub(doc, cred, "uuid", "")
498 if not self.expiration:
499 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
500 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
501 self.expiration = self.expiration.replace(microsecond=0)
502 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
503 # TZ aware. Make sure it is UTC - by Aaron Helsinger
504 self.expiration = self.expiration.astimezone(tz.tzutc())
505 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
506 privileges = doc.createElement("privileges")
507 cred.appendChild(privileges)
510 rights = self.get_privileges()
511 for right in rights.rights:
512 priv = doc.createElement("privilege")
513 append_sub(doc, priv, "name", right.kind)
514 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
515 privileges.appendChild(priv)
517 # Add the parent credential if it exists
519 sdoc = parseString(self.parent.get_xml())
520 # If the root node is a signed-credential (it should be), then
521 # get all its attributes and attach those to our signed_cred
523 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
524 # and we need to include those again here or else their signature
525 # no longer matches on the credential.
526 # We expect three of these, but here we copy them all:
527 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
528 # and from PG (PL is equivalent, as shown above):
529 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
530 # 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")
533 # PL now also declares these, with different URLs, so
534 # the code notices those attributes already existed with
535 # different values, and complains.
536 # This happens regularly on delegation now that PG and
537 # PL both declare the namespace with different URLs.
538 # If the content ever differs this is a problem,
539 # but for now it works - different URLs (values in the attributes)
540 # but the same actual schema, so using the PG schema
541 # on delegated-to-PL credentials works fine.
543 # Note: you could also not copy attributes
544 # which already exist. It appears that both PG and PL
545 # will actually validate a slicecred with a parent
546 # signed using PG namespaces and a child signed with PL
547 # namespaces over the whole thing. But I don't know
548 # if that is a bug in xmlsec1, an accident since
549 # the contents of the schemas are the same,
550 # or something else, but it seems odd. And this works.
551 parentRoot = sdoc.documentElement
552 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
553 for attrIx in range(0, parentRoot.attributes.length):
554 attr = parentRoot.attributes.item(attrIx)
555 # returns the old attribute of same name that was
557 # Below throws InUse exception if we forgot to clone the attribute first
558 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
559 if oldAttr and oldAttr.value != attr.value:
560 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
561 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
563 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
565 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
566 p = doc.createElement("parent")
567 p.appendChild(p_cred)
569 # done handling parent credential
571 # Create the <signatures> tag
572 signatures = doc.createElement("signatures")
573 signed_cred.appendChild(signatures)
575 # Add any parent signatures
577 for cur_cred in self.get_credential_list()[1:]:
578 sdoc = parseString(cur_cred.get_signature().get_xml())
579 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
580 signatures.appendChild(ele)
582 # Get the finished product
583 self.xml = doc.toxml("utf-8")
586 def save_to_random_tmp_file(self):
587 fp, filename = mkstemp(suffix='cred', text=True)
588 fp = os.fdopen(fp, "w")
589 self.save_to_file(filename, save_parents=True, filep=fp)
592 def save_to_file(self, filename, save_parents=True, filep=None):
598 f = open(filename, "w")
602 def save_to_string(self, save_parents=True):
612 def set_refid(self, rid):
616 # Figure out what refids exist, and update this credential's id
617 # so that it doesn't clobber the others. Returns the refids of
620 def updateRefID(self):
622 self.set_refid('ref0')
627 next_cred = self.parent
629 refs.append(next_cred.get_refid())
631 next_cred = next_cred.parent
636 # Find a unique refid for this credential
637 rid = self.get_refid()
640 rid = "ref%d" % (val + 1)
645 # Return the set of parent credential ref ids
654 # Sign the XML file created by encode()
657 # In general, a signed credential obtained externally should
658 # not be changed else the signature is no longer valid. So, once
659 # you have loaded an existing signed credential, do not call encode() or sign() on it.
662 if not self.issuer_privkey:
663 logger.warn("Cannot sign credential (no private key)")
665 if not self.issuer_gid:
666 logger.warn("Cannot sign credential (no issuer gid)")
668 doc = parseString(self.get_xml())
669 sigs = doc.getElementsByTagName("signatures")[0]
671 # Create the signature template to be signed
672 signature = Signature()
673 signature.set_refid(self.get_refid())
674 sdoc = parseString(signature.get_xml())
675 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
676 sigs.appendChild(sig_ele)
678 self.xml = doc.toxml("utf-8")
681 # Split the issuer GID into multiple certificates if it's a chain
682 chain = GID(filename=self.issuer_gid)
685 gid_files.append(chain.save_to_random_tmp_file(False))
686 if chain.get_parent():
687 chain = chain.get_parent()
692 # Call out to xmlsec1 to sign it
693 ref = 'Sig_%s' % self.get_refid()
694 filename = self.save_to_random_tmp_file()
695 xmlsec1 = self.get_xmlsec1_path()
697 raise Exception("Could not locate required 'xmlsec1' program")
698 command = '%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
699 % (xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
700 # print 'command',command
701 signed = os.popen(command).read()
704 for gid_file in gid_files:
714 # Retrieve the attributes of the credential from the XML.
715 # This is automatically called by the various get_* methods of
716 # this class and should not need to be called explicitly.
724 doc = parseString(self.xml)
726 raise CredentialNotVerifiable("Malformed credential")
727 doc = parseString(self.xml)
729 signed_cred = doc.getElementsByTagName("signed-credential")
731 # Is this a signed-cred or just a cred?
732 if len(signed_cred) > 0:
733 creds = signed_cred[0].getElementsByTagName("credential")
734 signatures = signed_cred[0].getElementsByTagName("signatures")
735 if len(signatures) > 0:
736 sigs = signatures[0].getElementsByTagName("Signature")
738 creds = doc.getElementsByTagName("credential")
740 if creds is None or len(creds) == 0:
741 # malformed cred file
742 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
744 # Just take the first cred if there are more than one
747 self.set_refid(cred.getAttribute("xml:id"))
748 self.set_expiration(utcparse(getTextNode(cred, "expires")))
749 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
750 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
753 ## This code until the end of function rewritten by Aaron Helsinger
756 priv_nodes = cred.getElementsByTagName("privileges")
757 if len(priv_nodes) > 0:
758 privs = priv_nodes[0]
759 for priv in privs.getElementsByTagName("privilege"):
760 kind = getTextNode(priv, "name")
761 deleg = str2bool(getTextNode(priv, "can_delegate"))
763 # Convert * into the default privileges for the credential's type
764 # Each inherits the delegatability from the * above
765 _ , type = urn_to_hrn(self.gidObject.get_urn())
766 rl = determine_rights(type, self.gidObject.get_urn())
771 rlist.add(Right(kind.strip(), deleg))
772 self.set_privileges(rlist)
776 parent = cred.getElementsByTagName("parent")
778 parent_doc = parent[0].getElementsByTagName("credential")[0]
779 parent_xml = parent_doc.toxml("utf-8")
780 if parent_xml is None or parent_xml.strip() == "":
781 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
782 self.parent = Credential(string=parent_xml)
785 # Assign the signatures to the credentials
787 Sig = Signature(string=sig.toxml("utf-8"))
789 for cur_cred in self.get_credential_list():
790 if cur_cred.get_refid() == Sig.get_refid():
791 cur_cred.set_signature(Sig)
796 # trusted_certs: A list of trusted GID filenames (not GID objects!)
797 # Chaining is not supported within the GIDs by xmlsec1.
799 # trusted_certs_required: Should usually be true. Set False means an
800 # empty list of trusted_certs would still let this method pass.
801 # It just skips xmlsec1 verification et al. Only used by some utils
804 # . All of the signatures are valid and that the issuers trace back
805 # to trusted roots (performed by xmlsec1)
806 # . The XML matches the credential schema
807 # . That the issuer of the credential is the authority in the target's urn
808 # . In the case of a delegated credential, this must be true of the root
809 # . That all of the gids presented in the credential are valid
810 # . Including verifying GID chains, and includ the issuer
811 # . The credential is not expired
813 # -- For Delegates (credentials with parents)
814 # . The privileges must be a subset of the parent credentials
815 # . The privileges must have "can_delegate" set for each delegated privilege
816 # . The target gid must be the same between child and parents
817 # . The expiry time on the child must be no later than the parent
818 # . The signer of the child must be the owner of the parent
820 # -- Verify does *NOT*
821 # . ensure that an xmlrpc client's gid matches a credential gid, that
822 # must be done elsewhere
824 # @param trusted_certs: The certificates of trusted CA certificates
825 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
829 # validate against RelaxNG schema
831 if schema and os.path.exists(schema):
832 tree = etree.parse(StringIO(self.xml))
833 schema_doc = etree.parse(schema)
834 xmlschema = etree.XMLSchema(schema_doc)
835 if not xmlschema.validate(tree):
836 error = xmlschema.error_log.last_error
837 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
838 raise CredentialNotVerifiable(message)
840 if trusted_certs_required and trusted_certs is None:
843 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
844 trusted_cert_objects = []
845 ok_trusted_certs = []
846 # If caller explicitly passed in None that means skip cert chain validation.
847 # Strange and not typical
848 if trusted_certs is not None:
849 for f in trusted_certs:
851 # Failures here include unreadable files
853 trusted_cert_objects.append(GID(filename=f))
854 ok_trusted_certs.append(f)
855 except Exception, exc:
856 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
857 trusted_certs = ok_trusted_certs
859 # make sure it is not expired
860 if self.get_expiration() < datetime.datetime.utcnow():
861 raise CredentialNotVerifiable("Credential %s expired at %s" % \
863 self.expiration.strftime(SFATIME_FORMAT)))
865 # Verify the signatures
866 filename = self.save_to_random_tmp_file()
868 # If caller explicitly passed in None that means skip cert chain validation.
869 # - Strange and not typical
870 if trusted_certs is not None:
871 # Verify the gids of this cred and of its parents
872 for cur_cred in self.get_credential_list():
873 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
874 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
877 refs.append("Sig_%s" % self.get_refid())
879 parentRefs = self.updateRefID()
880 for ref in parentRefs:
881 refs.append("Sig_%s" % ref)
884 # If caller explicitly passed in None that means skip xmlsec1 validation.
885 # Strange and not typical
886 if trusted_certs is None:
890 # up to fedora20 we used os.popen and checked that the output begins with OK
891 # turns out, with fedora21, there is extra input before this 'OK' thing
892 # looks like we're better off just using the exit code - that's what it is made for
893 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
894 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
895 # format(self.xmlsec_path, ref, cert_args, filename)
896 xmlsec1 = cred.get_xmlsec1_path()
898 raise Exception("Could not locate required 'xmlsec1' program")
899 command = [ xmlsec1, '--verify', '--node-id', ref ]
900 for trusted in trusted_certs:
901 command += ["--trusted-pem", trusted ]
902 command += [ filename ]
903 logger.debug("Running " + " ".join(command))
905 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
906 logger.debug("xmlsec command returned {}".format(verified))
907 if "OK\n" not in verified:
908 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
909 except subprocess.CalledProcessError as e:
911 # xmlsec errors have a msg= which is the interesting bit.
912 mstart = verified.find("msg=")
914 if mstart > -1 and len(verified) > 4:
916 mend = verified.find('\\', mstart)
917 msg = verified[mstart:mend]
918 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
919 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
920 (self.pretty_cred(), ref, msg))
923 # Verify the parents (delegation)
925 self.verify_parent(self.parent)
927 # Make sure the issuer is the target's authority, and is
929 self.verify_issuer(trusted_cert_objects)
933 # Creates a list of the credential and its parents, with the root
934 # (original delegated credential) as the last item in the list
935 def get_credential_list(self):
939 list.append(cur_cred)
941 cur_cred = cur_cred.parent
947 # Make sure the credential's target gid (a) was signed by or (b)
948 # is the same as the entity that signed the original credential,
949 # or (c) is an authority over the target's namespace.
950 # Also ensure that the credential issuer / signer itself has a valid
951 # GID signature chain (signed by an authority with namespace rights).
952 def verify_issuer(self, trusted_gids):
953 root_cred = self.get_credential_list()[-1]
954 root_target_gid = root_cred.get_gid_object()
955 if root_cred.get_signature() is None:
957 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
959 root_cred_signer = root_cred.get_signature().get_issuer_gid()
962 # Allow non authority to sign target and cred about target.
964 # Why do we need to allow non authorities to sign?
965 # If in the target gid validation step we correctly
966 # checked that the target is only signed by an authority,
967 # then this is just a special case of case 3.
968 # This short-circuit is the common case currently -
969 # and cause GID validation doesn't check 'authority',
970 # this allows users to generate valid slice credentials.
971 if root_target_gid.is_signed_by_cert(root_cred_signer):
972 # cred signer matches target signer, return success
976 # Allow someone to sign credential about themeselves. Used?
977 # If not, remove this.
978 #root_target_gid_str = root_target_gid.save_to_string()
979 #root_cred_signer_str = root_cred_signer.save_to_string()
980 #if root_target_gid_str == root_cred_signer_str:
981 # # cred signer is target, return success
986 # root_cred_signer is not the target_gid
987 # So this is a different gid that we have not verified.
988 # xmlsec1 verified the cert chain on this already, but
989 # it hasn't verified that the gid meets the HRN namespace
991 # Below we'll ensure that it is an authority.
992 # But we haven't verified that it is _signed by_ an authority
993 # We also don't know if xmlsec1 requires that cert signers
996 # Note that if verify() gave us no trusted_gids then this
997 # call will fail. So skip it if we have no trusted_gids
998 if trusted_gids and len(trusted_gids) > 0:
999 root_cred_signer.verify_chain(trusted_gids)
1001 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1002 "No trusted gids. Skipping that check.")
1004 # See if the signer is an authority over the domain of the target.
1005 # There are multiple types of authority - accept them all here
1006 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1007 root_cred_signer_type = root_cred_signer.get_type()
1008 if root_cred_signer_type.find('authority') == 0:
1009 #logger.debug('Cred signer is an authority')
1010 # signer is an authority, see if target is in authority's domain
1011 signerhrn = root_cred_signer.get_hrn()
1012 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1015 # We've required that the credential be signed by an authority
1016 # for that domain. Reasonable and probably correct.
1017 # A looser model would also allow the signer to be an authority
1018 # in my control framework - eg My CA or CH. Even if it is not
1019 # the CH that issued these, eg, user credentials.
1021 # Give up, credential does not pass issuer verification
1023 raise CredentialNotVerifiable(
1024 "Could not verify credential owned by {} for object {}. "
1025 "Cred signer {} not the trusted authority for Cred target {}"
1026 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1027 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1030 # -- For Delegates (credentials with parents) verify that:
1031 # . The privileges must be a subset of the parent credentials
1032 # . The privileges must have "can_delegate" set for each delegated privilege
1033 # . The target gid must be the same between child and parents
1034 # . The expiry time on the child must be no later than the parent
1035 # . The signer of the child must be the owner of the parent
1036 def verify_parent(self, parent_cred):
1037 # make sure the rights given to the child are a subset of the
1038 # parents rights (and check delegate bits)
1039 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1041 "Parent cred {} (ref {}) rights {} "
1042 " not superset of delegated cred {} (ref {}) rights {}"
1043 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1044 parent_cred.get_privileges().pretty_rights(),
1045 self.pretty_cred(), self.get_refid(),
1046 self.get_privileges().pretty_rights()))
1047 logger.error(message)
1048 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1049 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1050 raise ChildRightsNotSubsetOfParent(message)
1052 # make sure my target gid is the same as the parent's
1053 if not parent_cred.get_gid_object().save_to_string() == \
1054 self.get_gid_object().save_to_string():
1056 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1057 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1058 logger.error(message)
1059 logger.error("parent details {}".format(parent_cred.save_to_string()))
1060 logger.error("self details {}".format(self.save_to_string()))
1061 raise CredentialNotVerifiable(message)
1063 # make sure my expiry time is <= my parent's
1064 if not parent_cred.get_expiration() >= self.get_expiration():
1065 raise CredentialNotVerifiable(
1066 "Delegated credential {} expires after parent {}"
1067 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1069 # make sure my signer is the parent's caller
1070 if not parent_cred.get_gid_caller().save_to_string(False) == \
1071 self.get_signature().get_issuer_gid().save_to_string(False):
1072 message = "Delegated credential {} not signed by parent {}'s caller"\
1073 .format(self.pretty_cred(), parent_cred.pretty_cred())
1074 logger.error(message)
1075 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1076 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1077 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1078 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1079 raise CredentialNotVerifiable(message)
1082 if parent_cred.parent:
1083 parent_cred.verify_parent(parent_cred.parent)
1086 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1088 Return a delegated copy of this credential, delegated to the
1089 specified gid's user.
1091 # get the gid of the object we are delegating
1092 object_gid = self.get_gid_object()
1093 object_hrn = object_gid.get_hrn()
1095 # the hrn of the user who will be delegated to
1096 delegee_gid = GID(filename=delegee_gidfile)
1097 delegee_hrn = delegee_gid.get_hrn()
1099 #user_key = Keypair(filename=keyfile)
1100 #user_hrn = self.get_gid_caller().get_hrn()
1101 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1102 dcred = Credential(subject=subject_string)
1103 dcred.set_gid_caller(delegee_gid)
1104 dcred.set_gid_object(object_gid)
1105 dcred.set_parent(self)
1106 dcred.set_expiration(self.get_expiration())
1107 dcred.set_privileges(self.get_privileges())
1108 dcred.get_privileges().delegate_all_privileges(True)
1109 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1110 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1117 def get_filename(self):
1118 return getattr(self,'filename',None)
1120 def actual_caller_hrn (self):
1121 """a helper method used by some API calls like e.g. Allocate
1122 to try and find out who really is the original caller
1124 This admittedly is a bit of a hack, please USE IN LAST RESORT
1126 This code uses a heuristic to identify a delegated credential
1128 A first known restriction if for traffic that gets through a slice manager
1129 in this case the hrn reported is the one from the last SM in the call graph
1130 which is not at all what is meant here"""
1132 caller_hrn = self.get_gid_caller().get_hrn()
1133 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1134 subject_hrn = self.get_gid_object().get_hrn()
1135 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1136 # this seems to be a 'regular' credential
1137 if caller_hrn.startswith(issuer_hrn):
1138 actual_caller_hrn=caller_hrn
1139 # else this looks like a delegated credential, and the real caller is the issuer
1141 actual_caller_hrn=issuer_hrn
1142 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1143 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1144 return actual_caller_hrn
1147 # Dump the contents of a credential to stdout in human-readable format
1149 # @param dump_parents If true, also dump the parent certificates
1150 def dump (self, *args, **kwargs):
1151 print self.dump_string(*args, **kwargs)
1153 # SFA code ignores show_xml and disables printing the cred xml
1154 def dump_string(self, dump_parents=False, show_xml=False):
1156 result += "CREDENTIAL %s\n" % self.pretty_subject()
1157 filename=self.get_filename()
1158 if filename: result += "Filename %s\n"%filename
1159 privileges = self.get_privileges()
1161 result += " privs: %s\n" % privileges.save_to_string()
1163 result += " privs: \n"
1164 gidCaller = self.get_gid_caller()
1166 result += " gidCaller:\n"
1167 result += gidCaller.dump_string(8, dump_parents)
1169 if self.get_signature():
1170 result += " gidIssuer:\n"
1171 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1174 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1176 gidObject = self.get_gid_object()
1178 result += " gidObject:\n"
1179 result += gidObject.dump_string(8, dump_parents)
1181 if self.parent and dump_parents:
1182 result += "\nPARENT"
1183 result += self.parent.dump_string(True)
1185 if show_xml and HAVELXML:
1187 tree = etree.parse(StringIO(self.xml))
1188 aside = etree.tostring(tree, pretty_print=True)
1189 result += "\nXML:\n\n"
1191 result += "\nEnd XML\n"
1194 print "exc. Credential.dump_string / XML"
1195 traceback.print_exc()