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.get_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.cred_type = Credential.SFA_CREDENTIAL_TYPE
280 if isinstance(cred, StringTypes):
282 self.cred_type = Credential.SFA_CREDENTIAL_TYPE
284 elif isinstance(cred, dict):
285 string = cred['geni_value']
286 self.cred_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])
303 # Find an xmlsec1 path
304 self.xmlsec_path = ''
305 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
307 if os.path.isfile(path + '/' + 'xmlsec1'):
308 self.xmlsec_path = path + '/' + 'xmlsec1'
310 if not self.xmlsec_path:
311 logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!")
313 def get_cred_type(self):
314 return self.cred_type
316 def get_subject(self):
317 if not self.gidObject:
319 return self.gidObject.get_subject()
321 def pretty_subject(self):
323 if not self.gidObject:
326 subject = self.gidObject.pretty_cert()
329 # sounds like this should be __repr__ instead ??
330 def pretty_cred(self):
331 if not self.gidObject:
333 obj = self.gidObject.pretty_cert()
334 caller = self.gidCaller.pretty_cert()
335 exp = self.get_expiration()
336 # Summarize the rights too? The issuer?
337 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
339 def get_signature(self):
340 if not self.signature:
342 return self.signature
344 def set_signature(self, sig):
349 # Need the issuer's private key and name
350 # @param key Keypair object containing the private key of the issuer
351 # @param gid GID of the issuing authority
353 def set_issuer_keys(self, privkey, gid):
354 self.issuer_privkey = privkey
355 self.issuer_gid = gid
359 # Set this credential's parent
360 def set_parent(self, cred):
365 # set the GID of the caller
367 # @param gid GID object of the caller
369 def set_gid_caller(self, gid):
371 # gid origin caller is the caller's gid by default
372 self.gidOriginCaller = gid
375 # get the GID of the object
377 def get_gid_caller(self):
378 if not self.gidCaller:
380 return self.gidCaller
383 # set the GID of the object
385 # @param gid GID object of the object
387 def set_gid_object(self, gid):
391 # get the GID of the object
393 def get_gid_object(self):
394 if not self.gidObject:
396 return self.gidObject
399 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
401 def set_expiration(self, expiration):
402 expiration_datetime = utcparse (expiration)
403 if expiration_datetime is not None:
404 self.expiration = expiration_datetime
406 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
409 # get the lifetime of the credential (always in datetime format)
411 def get_expiration(self):
412 if not self.expiration:
414 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
415 return self.expiration
420 # @param privs either a comma-separated list of privileges of a Rights object
422 def set_privileges(self, privs):
423 if isinstance(privs, str):
424 self.privileges = Rights(string = privs)
426 self.privileges = privs
429 # return the privileges as a Rights object
431 def get_privileges(self):
432 if not self.privileges:
434 return self.privileges
437 # determine whether the credential allows a particular operation to be
440 # @param op_name string specifying name of operation ("lookup", "update", etc)
442 def can_perform(self, op_name):
443 rights = self.get_privileges()
448 return rights.can_perform(op_name)
452 # Encode the attributes of the credential into an XML string
453 # This should be done immediately before signing the credential.
455 # In general, a signed credential obtained externally should
456 # not be changed else the signature is no longer valid. So, once
457 # you have loaded an existing signed credential, do not call encode() or sign() on it.
460 # Create the XML document
462 signed_cred = doc.createElement("signed-credential")
465 # Note that credential/policy.xsd are really the PG schemas
467 # Note that delegation of credentials between the 2 only really works
468 # cause those schemas are identical.
469 # Also note these PG schemas talk about PG tickets and CM policies.
470 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
471 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
472 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
473 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")
475 # PG says for those last 2:
476 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
477 # 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")
479 doc.appendChild(signed_cred)
481 # Fill in the <credential> bit
482 cred = doc.createElement("credential")
483 cred.setAttribute("xml:id", self.get_refid())
484 signed_cred.appendChild(cred)
485 append_sub(doc, cred, "type", "privilege")
486 append_sub(doc, cred, "serial", "8")
487 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
488 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
489 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
490 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
491 append_sub(doc, cred, "uuid", "")
492 if not self.expiration:
493 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
494 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
495 self.expiration = self.expiration.replace(microsecond=0)
496 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
497 # TZ aware. Make sure it is UTC - by Aaron Helsinger
498 self.expiration = self.expiration.astimezone(tz.tzutc())
499 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
500 privileges = doc.createElement("privileges")
501 cred.appendChild(privileges)
504 rights = self.get_privileges()
505 for right in rights.rights:
506 priv = doc.createElement("privilege")
507 append_sub(doc, priv, "name", right.kind)
508 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
509 privileges.appendChild(priv)
511 # Add the parent credential if it exists
513 sdoc = parseString(self.parent.get_xml())
514 # If the root node is a signed-credential (it should be), then
515 # get all its attributes and attach those to our signed_cred
517 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
518 # and we need to include those again here or else their signature
519 # no longer matches on the credential.
520 # We expect three of these, but here we copy them all:
521 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
522 # and from PG (PL is equivalent, as shown above):
523 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
524 # 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")
527 # PL now also declares these, with different URLs, so
528 # the code notices those attributes already existed with
529 # different values, and complains.
530 # This happens regularly on delegation now that PG and
531 # PL both declare the namespace with different URLs.
532 # If the content ever differs this is a problem,
533 # but for now it works - different URLs (values in the attributes)
534 # but the same actual schema, so using the PG schema
535 # on delegated-to-PL credentials works fine.
537 # Note: you could also not copy attributes
538 # which already exist. It appears that both PG and PL
539 # will actually validate a slicecred with a parent
540 # signed using PG namespaces and a child signed with PL
541 # namespaces over the whole thing. But I don't know
542 # if that is a bug in xmlsec1, an accident since
543 # the contents of the schemas are the same,
544 # or something else, but it seems odd. And this works.
545 parentRoot = sdoc.documentElement
546 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
547 for attrIx in range(0, parentRoot.attributes.length):
548 attr = parentRoot.attributes.item(attrIx)
549 # returns the old attribute of same name that was
551 # Below throws InUse exception if we forgot to clone the attribute first
552 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
553 if oldAttr and oldAttr.value != attr.value:
554 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
555 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
557 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
559 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
560 p = doc.createElement("parent")
561 p.appendChild(p_cred)
563 # done handling parent credential
565 # Create the <signatures> tag
566 signatures = doc.createElement("signatures")
567 signed_cred.appendChild(signatures)
569 # Add any parent signatures
571 for cur_cred in self.get_credential_list()[1:]:
572 sdoc = parseString(cur_cred.get_signature().get_xml())
573 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
574 signatures.appendChild(ele)
576 # Get the finished product
577 self.xml = doc.toxml("utf-8")
580 def save_to_random_tmp_file(self):
581 fp, filename = mkstemp(suffix='cred', text=True)
582 fp = os.fdopen(fp, "w")
583 self.save_to_file(filename, save_parents=True, filep=fp)
586 def save_to_file(self, filename, save_parents=True, filep=None):
592 f = open(filename, "w")
596 def save_to_string(self, save_parents=True):
606 def set_refid(self, rid):
610 # Figure out what refids exist, and update this credential's id
611 # so that it doesn't clobber the others. Returns the refids of
614 def updateRefID(self):
616 self.set_refid('ref0')
621 next_cred = self.parent
623 refs.append(next_cred.get_refid())
625 next_cred = next_cred.parent
630 # Find a unique refid for this credential
631 rid = self.get_refid()
634 rid = "ref%d" % (val + 1)
639 # Return the set of parent credential ref ids
648 # Sign the XML file created by encode()
651 # In general, a signed credential obtained externally should
652 # not be changed else the signature is no longer valid. So, once
653 # you have loaded an existing signed credential, do not call encode() or sign() on it.
656 if not self.issuer_privkey:
657 logger.warn("Cannot sign credential (no private key)")
659 if not self.issuer_gid:
660 logger.warn("Cannot sign credential (no issuer gid)")
662 doc = parseString(self.get_xml())
663 sigs = doc.getElementsByTagName("signatures")[0]
665 # Create the signature template to be signed
666 signature = Signature()
667 signature.set_refid(self.get_refid())
668 sdoc = parseString(signature.get_xml())
669 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
670 sigs.appendChild(sig_ele)
672 self.xml = doc.toxml("utf-8")
675 # Split the issuer GID into multiple certificates if it's a chain
676 chain = GID(filename=self.issuer_gid)
679 gid_files.append(chain.save_to_random_tmp_file(False))
680 if chain.get_parent():
681 chain = chain.get_parent()
686 # Call out to xmlsec1 to sign it
687 ref = 'Sig_%s' % self.get_refid()
688 filename = self.save_to_random_tmp_file()
689 command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
690 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)
691 # print 'command',command
692 signed = os.popen(command).read()
695 for gid_file in gid_files:
705 # Retrieve the attributes of the credential from the XML.
706 # This is automatically called by the various get_* methods of
707 # this class and should not need to be called explicitly.
715 doc = parseString(self.xml)
717 raise CredentialNotVerifiable("Malformed credential")
718 doc = parseString(self.xml)
720 signed_cred = doc.getElementsByTagName("signed-credential")
722 # Is this a signed-cred or just a cred?
723 if len(signed_cred) > 0:
724 creds = signed_cred[0].getElementsByTagName("credential")
725 signatures = signed_cred[0].getElementsByTagName("signatures")
726 if len(signatures) > 0:
727 sigs = signatures[0].getElementsByTagName("Signature")
729 creds = doc.getElementsByTagName("credential")
731 if creds is None or len(creds) == 0:
732 # malformed cred file
733 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
735 # Just take the first cred if there are more than one
738 self.set_refid(cred.getAttribute("xml:id"))
739 self.set_expiration(utcparse(getTextNode(cred, "expires")))
740 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
741 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
744 ## This code until the end of function rewritten by Aaron Helsinger
747 priv_nodes = cred.getElementsByTagName("privileges")
748 if len(priv_nodes) > 0:
749 privs = priv_nodes[0]
750 for priv in privs.getElementsByTagName("privilege"):
751 kind = getTextNode(priv, "name")
752 deleg = str2bool(getTextNode(priv, "can_delegate"))
754 # Convert * into the default privileges for the credential's type
755 # Each inherits the delegatability from the * above
756 _ , type = urn_to_hrn(self.gidObject.get_urn())
757 rl = determine_rights(type, self.gidObject.get_urn())
762 rlist.add(Right(kind.strip(), deleg))
763 self.set_privileges(rlist)
767 parent = cred.getElementsByTagName("parent")
769 parent_doc = parent[0].getElementsByTagName("credential")[0]
770 parent_xml = parent_doc.toxml("utf-8")
771 if parent_xml is None or parent_xml.strip() == "":
772 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
773 self.parent = Credential(string=parent_xml)
776 # Assign the signatures to the credentials
778 Sig = Signature(string=sig.toxml("utf-8"))
780 for cur_cred in self.get_credential_list():
781 if cur_cred.get_refid() == Sig.get_refid():
782 cur_cred.set_signature(Sig)
787 # trusted_certs: A list of trusted GID filenames (not GID objects!)
788 # Chaining is not supported within the GIDs by xmlsec1.
790 # trusted_certs_required: Should usually be true. Set False means an
791 # empty list of trusted_certs would still let this method pass.
792 # It just skips xmlsec1 verification et al. Only used by some utils
795 # . All of the signatures are valid and that the issuers trace back
796 # to trusted roots (performed by xmlsec1)
797 # . The XML matches the credential schema
798 # . That the issuer of the credential is the authority in the target's urn
799 # . In the case of a delegated credential, this must be true of the root
800 # . That all of the gids presented in the credential are valid
801 # . Including verifying GID chains, and includ the issuer
802 # . The credential is not expired
804 # -- For Delegates (credentials with parents)
805 # . The privileges must be a subset of the parent credentials
806 # . The privileges must have "can_delegate" set for each delegated privilege
807 # . The target gid must be the same between child and parents
808 # . The expiry time on the child must be no later than the parent
809 # . The signer of the child must be the owner of the parent
811 # -- Verify does *NOT*
812 # . ensure that an xmlrpc client's gid matches a credential gid, that
813 # must be done elsewhere
815 # @param trusted_certs: The certificates of trusted CA certificates
816 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
820 # validate against RelaxNG schema
822 if schema and os.path.exists(schema):
823 tree = etree.parse(StringIO(self.xml))
824 schema_doc = etree.parse(schema)
825 xmlschema = etree.XMLSchema(schema_doc)
826 if not xmlschema.validate(tree):
827 error = xmlschema.error_log.last_error
828 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
829 raise CredentialNotVerifiable(message)
831 if trusted_certs_required and trusted_certs is None:
834 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
835 trusted_cert_objects = []
836 ok_trusted_certs = []
837 # If caller explicitly passed in None that means skip cert chain validation.
838 # Strange and not typical
839 if trusted_certs is not None:
840 for f in trusted_certs:
842 # Failures here include unreadable files
844 trusted_cert_objects.append(GID(filename=f))
845 ok_trusted_certs.append(f)
846 except Exception, exc:
847 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
848 trusted_certs = ok_trusted_certs
850 # make sure it is not expired
851 if self.get_expiration() < datetime.datetime.utcnow():
852 raise CredentialNotVerifiable("Credential %s expired at %s" % \
854 self.expiration.strftime(SFATIME_FORMAT)))
856 # Verify the signatures
857 filename = self.save_to_random_tmp_file()
859 # If caller explicitly passed in None that means skip cert chain validation.
860 # - Strange and not typical
861 if trusted_certs is not None:
862 # Verify the gids of this cred and of its parents
863 for cur_cred in self.get_credential_list():
864 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
865 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
868 refs.append("Sig_%s" % self.get_refid())
870 parentRefs = self.updateRefID()
871 for ref in parentRefs:
872 refs.append("Sig_%s" % ref)
875 # If caller explicitly passed in None that means skip xmlsec1 validation.
876 # Strange and not typical
877 if trusted_certs is None:
881 # up to fedora20 we used os.popen and checked that the output begins with OK
882 # turns out, with fedora21, there is extra input before this 'OK' thing
883 # looks like we're better off just using the exit code - that's what it is made for
884 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
885 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
886 # format(self.xmlsec_path, ref, cert_args, filename)
887 command = [ self.xmlsec_path, '--verify', '--node-id', ref ]
888 for trusted in trusted_certs:
889 command += ["--trusted-pem", trusted ]
890 command += [ filename ]
891 logger.debug("Running " + " ".join(command))
893 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
894 logger.debug("xmlsec command returned {}".format(verified))
895 if "OK\n" not in verified:
896 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
897 except subprocess.CalledProcessError as e:
899 # xmlsec errors have a msg= which is the interesting bit.
900 mstart = verified.find("msg=")
902 if mstart > -1 and len(verified) > 4:
904 mend = verified.find('\\', mstart)
905 msg = verified[mstart:mend]
906 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
907 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
908 (self.pretty_cred(), ref, msg))
911 # Verify the parents (delegation)
913 self.verify_parent(self.parent)
915 # Make sure the issuer is the target's authority, and is
917 self.verify_issuer(trusted_cert_objects)
921 # Creates a list of the credential and its parents, with the root
922 # (original delegated credential) as the last item in the list
923 def get_credential_list(self):
927 list.append(cur_cred)
929 cur_cred = cur_cred.parent
935 # Make sure the credential's target gid (a) was signed by or (b)
936 # is the same as the entity that signed the original credential,
937 # or (c) is an authority over the target's namespace.
938 # Also ensure that the credential issuer / signer itself has a valid
939 # GID signature chain (signed by an authority with namespace rights).
940 def verify_issuer(self, trusted_gids):
941 root_cred = self.get_credential_list()[-1]
942 root_target_gid = root_cred.get_gid_object()
943 if root_cred.get_signature() is None:
945 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
947 root_cred_signer = root_cred.get_signature().get_issuer_gid()
950 # Allow non authority to sign target and cred about target.
952 # Why do we need to allow non authorities to sign?
953 # If in the target gid validation step we correctly
954 # checked that the target is only signed by an authority,
955 # then this is just a special case of case 3.
956 # This short-circuit is the common case currently -
957 # and cause GID validation doesn't check 'authority',
958 # this allows users to generate valid slice credentials.
959 if root_target_gid.is_signed_by_cert(root_cred_signer):
960 # cred signer matches target signer, return success
964 # Allow someone to sign credential about themeselves. Used?
965 # If not, remove this.
966 #root_target_gid_str = root_target_gid.save_to_string()
967 #root_cred_signer_str = root_cred_signer.save_to_string()
968 #if root_target_gid_str == root_cred_signer_str:
969 # # cred signer is target, return success
974 # root_cred_signer is not the target_gid
975 # So this is a different gid that we have not verified.
976 # xmlsec1 verified the cert chain on this already, but
977 # it hasn't verified that the gid meets the HRN namespace
979 # Below we'll ensure that it is an authority.
980 # But we haven't verified that it is _signed by_ an authority
981 # We also don't know if xmlsec1 requires that cert signers
984 # Note that if verify() gave us no trusted_gids then this
985 # call will fail. So skip it if we have no trusted_gids
986 if trusted_gids and len(trusted_gids) > 0:
987 root_cred_signer.verify_chain(trusted_gids)
989 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
990 "No trusted gids. Skipping that check.")
992 # See if the signer is an authority over the domain of the target.
993 # There are multiple types of authority - accept them all here
994 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
995 root_cred_signer_type = root_cred_signer.get_type()
996 if root_cred_signer_type.find('authority') == 0:
997 #logger.debug('Cred signer is an authority')
998 # signer is an authority, see if target is in authority's domain
999 signerhrn = root_cred_signer.get_hrn()
1000 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1003 # We've required that the credential be signed by an authority
1004 # for that domain. Reasonable and probably correct.
1005 # A looser model would also allow the signer to be an authority
1006 # in my control framework - eg My CA or CH. Even if it is not
1007 # the CH that issued these, eg, user credentials.
1009 # Give up, credential does not pass issuer verification
1011 raise CredentialNotVerifiable(
1012 "Could not verify credential owned by {} for object {}. "
1013 "Cred signer {} not the trusted authority for Cred target {}"
1014 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1015 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1018 # -- For Delegates (credentials with parents) verify that:
1019 # . The privileges must be a subset of the parent credentials
1020 # . The privileges must have "can_delegate" set for each delegated privilege
1021 # . The target gid must be the same between child and parents
1022 # . The expiry time on the child must be no later than the parent
1023 # . The signer of the child must be the owner of the parent
1024 def verify_parent(self, parent_cred):
1025 # make sure the rights given to the child are a subset of the
1026 # parents rights (and check delegate bits)
1027 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1029 "Parent cred {} (ref {}) rights {} "
1030 " not superset of delegated cred {} (ref {}) rights {}"
1031 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1032 parent_cred.get_privileges().pretty_rights(),
1033 self.pretty_cred(), self.get_refid(),
1034 self.get_privileges().pretty_rights()))
1035 logger.error(message)
1036 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1037 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1038 raise ChildRightsNotSubsetOfParent(message)
1040 # make sure my target gid is the same as the parent's
1041 if not parent_cred.get_gid_object().save_to_string() == \
1042 self.get_gid_object().save_to_string():
1044 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1045 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1046 logger.error(message)
1047 logger.error("parent details {}".format(parent_cred.save_to_string()))
1048 logger.error("self details {}".format(self.save_to_string()))
1049 raise CredentialNotVerifiable(message)
1051 # make sure my expiry time is <= my parent's
1052 if not parent_cred.get_expiration() >= self.get_expiration():
1053 raise CredentialNotVerifiable(
1054 "Delegated credential {} expires after parent {}"
1055 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1057 # make sure my signer is the parent's caller
1058 if not parent_cred.get_gid_caller().save_to_string(False) == \
1059 self.get_signature().get_issuer_gid().save_to_string(False):
1060 message = "Delegated credential {} not signed by parent {}'s caller"\
1061 .format(self.pretty_cred(), parent_cred.pretty_cred())
1062 logger.error(message)
1063 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1064 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1065 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1066 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1067 raise CredentialNotVerifiable(message)
1070 if parent_cred.parent:
1071 parent_cred.verify_parent(parent_cred.parent)
1074 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1076 Return a delegated copy of this credential, delegated to the
1077 specified gid's user.
1079 # get the gid of the object we are delegating
1080 object_gid = self.get_gid_object()
1081 object_hrn = object_gid.get_hrn()
1083 # the hrn of the user who will be delegated to
1084 delegee_gid = GID(filename=delegee_gidfile)
1085 delegee_hrn = delegee_gid.get_hrn()
1087 #user_key = Keypair(filename=keyfile)
1088 #user_hrn = self.get_gid_caller().get_hrn()
1089 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1090 dcred = Credential(subject=subject_string)
1091 dcred.set_gid_caller(delegee_gid)
1092 dcred.set_gid_object(object_gid)
1093 dcred.set_parent(self)
1094 dcred.set_expiration(self.get_expiration())
1095 dcred.set_privileges(self.get_privileges())
1096 dcred.get_privileges().delegate_all_privileges(True)
1097 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1098 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1105 def get_filename(self):
1106 return getattr(self,'filename',None)
1108 def actual_caller_hrn (self):
1109 """a helper method used by some API calls like e.g. Allocate
1110 to try and find out who really is the original caller
1112 This admittedly is a bit of a hack, please USE IN LAST RESORT
1114 This code uses a heuristic to identify a delegated credential
1116 A first known restriction if for traffic that gets through a slice manager
1117 in this case the hrn reported is the one from the last SM in the call graph
1118 which is not at all what is meant here"""
1120 caller_hrn = self.get_gid_caller().get_hrn()
1121 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1122 subject_hrn = self.get_gid_object().get_hrn()
1123 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1124 # this seems to be a 'regular' credential
1125 if caller_hrn.startswith(issuer_hrn):
1126 actual_caller_hrn=caller_hrn
1127 # else this looks like a delegated credential, and the real caller is the issuer
1129 actual_caller_hrn=issuer_hrn
1130 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1131 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1132 return actual_caller_hrn
1135 # Dump the contents of a credential to stdout in human-readable format
1137 # @param dump_parents If true, also dump the parent certificates
1138 def dump (self, *args, **kwargs):
1139 print self.dump_string(*args, **kwargs)
1141 # SFA code ignores show_xml and disables printing the cred xml
1142 def dump_string(self, dump_parents=False, show_xml=False):
1144 result += "CREDENTIAL %s\n" % self.pretty_subject()
1145 filename=self.get_filename()
1146 if filename: result += "Filename %s\n"%filename
1147 privileges = self.get_privileges()
1149 result += " privs: %s\n" % privileges.save_to_string()
1151 result += " privs: \n"
1152 gidCaller = self.get_gid_caller()
1154 result += " gidCaller:\n"
1155 result += gidCaller.dump_string(8, dump_parents)
1157 if self.get_signature():
1158 result += " gidIssuer:\n"
1159 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1162 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1164 gidObject = self.get_gid_object()
1166 result += " gidObject:\n"
1167 result += gidObject.dump_string(8, dump_parents)
1169 if self.parent and dump_parents:
1170 result += "\nPARENT"
1171 result += self.parent.dump_string(True)
1173 if show_xml and HAVELXML:
1175 tree = etree.parse(StringIO(self.xml))
1176 aside = etree.tostring(tree, pretty_print=True)
1177 result += "\nXML:\n\n"
1179 result += "\nEnd XML\n"
1182 print "exc. Credential.dump_string / XML"
1183 traceback.print_exc()