1 #----------------------------------------------------------------------
2 # Copyright (c) 2008 Board of Trustees, Princeton University
4 # Permission is hereby granted, free of charge, to any person obtaining
5 # a copy of this software and/or hardware specification (the "Work") to
6 # deal in the Work without restriction, including without limitation the
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
8 # and/or sell copies of the Work, and to permit persons to whom the Work
9 # is furnished to do so, subject to the following conditions:
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Work.
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
22 #----------------------------------------------------------------------
24 # Implements SFA Credentials
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
29 from __future__ import print_function
34 from tempfile import mkstemp
35 from xml.dom.minidom import Document, parseString
37 from sfa.util.py23 import StringType
38 from sfa.util.py23 import StringIO
42 from lxml import etree
47 from xml.parsers.expat import ExpatError
49 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
50 from sfa.util.sfalogging import logger
51 from sfa.util.sfatime import utcparse, SFATIME_FORMAT
52 from sfa.trust.rights import Right, Rights, determine_rights
53 from sfa.trust.gid import GID
54 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
57 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
61 # . make privs match between PG and PL
62 # . Need to add support for other types of credentials, e.g. tickets
63 # . add namespaces to signed-credential element?
65 signature_template = \
67 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
69 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
70 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
73 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
75 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
76 <DigestValue></DigestValue>
91 # PG formats the template (whitespace) slightly differently.
92 # Note that they don't include the xmlns in the template, but add it later.
93 # Otherwise the two are equivalent.
94 #signature_template_as_in_pg = \
96 #<Signature xml:id="Sig_%s" >
98 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
99 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
100 # <Reference URI="#%s">
102 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
104 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
105 # <DigestValue></DigestValue>
112 # <X509IssuerSerial/>
121 # Convert a string into a bool
122 # used to convert an xsd:boolean to a Python boolean
124 if str.lower() in ['true','1']:
130 # Utility function to get the text of an XML element
132 def getTextNode(element, subele):
133 sub = element.getElementsByTagName(subele)[0]
134 if len(sub.childNodes) > 0:
135 return sub.childNodes[0].nodeValue
140 # Utility function to set the text of an XML element
141 # It creates the element, adds the text to it,
142 # and then appends it to the parent.
144 def append_sub(doc, parent, element, text):
145 ele = doc.createElement(element)
146 ele.appendChild(doc.createTextNode(text))
147 parent.appendChild(ele)
150 # Signature contains information about an xmlsec1 signature
151 # for a signed-credential
154 class Signature(object):
156 def __init__(self, string=None):
158 self.issuer_gid = None
175 def set_refid(self, id):
178 def get_issuer_gid(self):
183 def set_issuer_gid(self, gid):
187 # Helper function to pull characters off the front of a string if present
188 def remove_prefix(text, prefix):
189 if text and prefix and text.startswith(prefix):
190 return text[len(prefix):]
194 doc = parseString(self.xml)
195 except ExpatError as e:
196 logger.log_exc ("Failed to parse credential, %s"%self.xml)
198 sig = doc.getElementsByTagName("Signature")[0]
199 ## This code until the end of function rewritten by Aaron Helsinger
200 ref_id = remove_prefix(sig.getAttribute("xml:id").strip(), "Sig_")
201 # The xml:id tag is optional, and could be in a
202 # Reference xml:id or Reference UID sub element instead
203 if not ref_id or ref_id == '':
204 reference = sig.getElementsByTagName('Reference')[0]
205 ref_id = remove_prefix(reference.getAttribute('xml:id').strip(), "Sig_")
206 if not ref_id or ref_id == '':
207 ref_id = remove_prefix(reference.getAttribute('URI').strip(), "#")
208 self.set_refid(ref_id)
209 keyinfos = sig.getElementsByTagName("X509Data")
211 for keyinfo in keyinfos:
212 certs = keyinfo.getElementsByTagName("X509Certificate")
214 if len(cert.childNodes) > 0:
215 szgid = cert.childNodes[0].nodeValue
216 szgid = szgid.strip()
217 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
223 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
224 self.set_issuer_gid(GID(string=gids))
227 self.xml = signature_template % (self.get_refid(), self.get_refid())
230 # A credential provides a caller gid with privileges to an object gid.
231 # A signed credential is signed by the object's authority.
233 # Credentials are encoded in one of two ways.
234 # The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate.
235 # The new credentials are placed in signed XML.
238 # In general, a signed credential obtained externally should
239 # not be changed else the signature is no longer valid. So, once
240 # you have loaded an existing signed credential, do not call encode() or sign() on it.
242 def filter_creds_by_caller(creds, caller_hrn_list):
244 Returns a list of creds who's gid caller matches the
247 if not isinstance(creds, list): creds = [creds]
248 if not isinstance(caller_hrn_list, list):
249 caller_hrn_list = [caller_hrn_list]
253 tmp_cred = Credential(string=cred)
254 if tmp_cred.type != Credential.SFA_CREDENTIAL_TYPE:
256 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
257 caller_creds.append(cred)
261 class Credential(object):
263 SFA_CREDENTIAL_TYPE = "geni_sfa"
266 # Create a Credential object
268 # @param create If true, create a blank x509 certificate
269 # @param subject If subject!=None, create an x509 cert with the subject name
270 # @param string If string!=None, load the credential from the string
271 # @param filename If filename!=None, load the credential from the file
272 # FIXME: create and subject are ignored!
273 def __init__(self, create=False, subject=None, string=None, filename=None, cred=None):
274 self.gidCaller = None
275 self.gidObject = None
276 self.expiration = None
277 self.privileges = None
278 self.issuer_privkey = None
279 self.issuer_gid = None
280 self.issuer_pubkey = None
282 self.signature = None
285 self.type = Credential.SFA_CREDENTIAL_TYPE
289 if isinstance(cred, StringType):
291 self.type = Credential.SFA_CREDENTIAL_TYPE
293 elif isinstance(cred, dict):
294 string = cred['geni_value']
295 self.type = cred['geni_type']
296 self.version = cred['geni_version']
298 if string or filename:
302 with open(filename) as infile:
305 # if this is a legacy credential, write error and bail out
306 if isinstance (str, StringType) and str.strip().startswith("-----"):
307 logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10])
312 # not strictly necessary but won't hurt either
313 self.get_xmlsec1_path()
316 def get_xmlsec1_path():
317 if not getattr(Credential, 'xmlsec1_path', None):
318 # Find a xmlsec1 binary path
319 Credential.xmlsec1_path = ''
320 paths = ['/usr/bin', '/usr/local/bin', '/bin', '/opt/bin', '/opt/local/bin']
321 try: paths += os.getenv('PATH').split(':')
324 xmlsec1 = os.path.join(path, 'xmlsec1')
325 if os.path.isfile(xmlsec1):
326 Credential.xmlsec1_path = xmlsec1
328 if not Credential.xmlsec1_path:
329 logger.error("Could not locate required binary 'xmlsec1' - SFA will be unable to sign stuff !!")
330 return Credential.xmlsec1_path
332 def get_subject(self):
333 if not self.gidObject:
335 return self.gidObject.get_subject()
337 def pretty_subject(self):
339 if not self.gidObject:
342 subject = self.gidObject.pretty_cert()
345 # sounds like this should be __repr__ instead ??
346 def pretty_cred(self):
347 if not self.gidObject:
349 obj = self.gidObject.pretty_cert()
350 caller = self.gidCaller.pretty_cert()
351 exp = self.get_expiration()
352 # Summarize the rights too? The issuer?
353 return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals())
355 def get_signature(self):
356 if not self.signature:
358 return self.signature
360 def set_signature(self, sig):
365 # Need the issuer's private key and name
366 # @param key Keypair object containing the private key of the issuer
367 # @param gid GID of the issuing authority
369 def set_issuer_keys(self, privkey, gid):
370 self.issuer_privkey = privkey
371 self.issuer_gid = gid
375 # Set this credential's parent
376 def set_parent(self, cred):
381 # set the GID of the caller
383 # @param gid GID object of the caller
385 def set_gid_caller(self, gid):
387 # gid origin caller is the caller's gid by default
388 self.gidOriginCaller = gid
391 # get the GID of the object
393 def get_gid_caller(self):
394 if not self.gidCaller:
396 return self.gidCaller
399 # set the GID of the object
401 # @param gid GID object of the object
403 def set_gid_object(self, gid):
407 # get the GID of the object
409 def get_gid_object(self):
410 if not self.gidObject:
412 return self.gidObject
415 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
417 def set_expiration(self, expiration):
418 expiration_datetime = utcparse (expiration)
419 if expiration_datetime is not None:
420 self.expiration = expiration_datetime
422 logger.error ("unexpected input %s in Credential.set_expiration"%expiration)
425 # get the lifetime of the credential (always in datetime format)
427 def get_expiration(self):
428 if not self.expiration:
430 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
431 return self.expiration
436 # @param privs either a comma-separated list of privileges of a Rights object
438 def set_privileges(self, privs):
439 if isinstance(privs, str):
440 self.privileges = Rights(string = privs)
442 self.privileges = privs
445 # return the privileges as a Rights object
447 def get_privileges(self):
448 if not self.privileges:
450 return self.privileges
453 # determine whether the credential allows a particular operation to be
456 # @param op_name string specifying name of operation ("lookup", "update", etc)
458 def can_perform(self, op_name):
459 rights = self.get_privileges()
464 return rights.can_perform(op_name)
468 # Encode the attributes of the credential into an XML string
469 # This should be done immediately before signing the credential.
471 # In general, a signed credential obtained externally should
472 # not be changed else the signature is no longer valid. So, once
473 # you have loaded an existing signed credential, do not call encode() or sign() on it.
476 # Create the XML document
478 signed_cred = doc.createElement("signed-credential")
481 # Note that credential/policy.xsd are really the PG schemas
483 # Note that delegation of credentials between the 2 only really works
484 # cause those schemas are identical.
485 # Also note these PG schemas talk about PG tickets and CM policies.
486 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
487 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
488 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
489 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")
491 # PG says for those last 2:
492 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
493 # 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")
495 doc.appendChild(signed_cred)
497 # Fill in the <credential> bit
498 cred = doc.createElement("credential")
499 cred.setAttribute("xml:id", self.get_refid())
500 signed_cred.appendChild(cred)
501 append_sub(doc, cred, "type", "privilege")
502 append_sub(doc, cred, "serial", "8")
503 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
504 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
505 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
506 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
507 append_sub(doc, cred, "uuid", "")
508 if not self.expiration:
509 logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME)
510 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
511 self.expiration = self.expiration.replace(microsecond=0)
512 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
513 # TZ aware. Make sure it is UTC - by Aaron Helsinger
514 self.expiration = self.expiration.astimezone(tz.tzutc())
515 append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT))
516 privileges = doc.createElement("privileges")
517 cred.appendChild(privileges)
520 rights = self.get_privileges()
521 for right in rights.rights:
522 priv = doc.createElement("privilege")
523 append_sub(doc, priv, "name", right.kind)
524 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
525 privileges.appendChild(priv)
527 # Add the parent credential if it exists
529 sdoc = parseString(self.parent.get_xml())
530 # If the root node is a signed-credential (it should be), then
531 # get all its attributes and attach those to our signed_cred
533 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
534 # and we need to include those again here or else their signature
535 # no longer matches on the credential.
536 # We expect three of these, but here we copy them all:
537 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
538 # and from PG (PL is equivalent, as shown above):
539 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
540 # 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")
543 # PL now also declares these, with different URLs, so
544 # the code notices those attributes already existed with
545 # different values, and complains.
546 # This happens regularly on delegation now that PG and
547 # PL both declare the namespace with different URLs.
548 # If the content ever differs this is a problem,
549 # but for now it works - different URLs (values in the attributes)
550 # but the same actual schema, so using the PG schema
551 # on delegated-to-PL credentials works fine.
553 # Note: you could also not copy attributes
554 # which already exist. It appears that both PG and PL
555 # will actually validate a slicecred with a parent
556 # signed using PG namespaces and a child signed with PL
557 # namespaces over the whole thing. But I don't know
558 # if that is a bug in xmlsec1, an accident since
559 # the contents of the schemas are the same,
560 # or something else, but it seems odd. And this works.
561 parentRoot = sdoc.documentElement
562 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
563 for attrIx in range(0, parentRoot.attributes.length):
564 attr = parentRoot.attributes.item(attrIx)
565 # returns the old attribute of same name that was
567 # Below throws InUse exception if we forgot to clone the attribute first
568 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
569 if oldAttr and oldAttr.value != attr.value:
570 msg = "Delegating cred from owner %s to %s over %s:\n - Replaced attribute %s value '%s' with '%s'" % \
571 (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
573 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
575 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
576 p = doc.createElement("parent")
577 p.appendChild(p_cred)
579 # done handling parent credential
581 # Create the <signatures> tag
582 signatures = doc.createElement("signatures")
583 signed_cred.appendChild(signatures)
585 # Add any parent signatures
587 for cur_cred in self.get_credential_list()[1:]:
588 sdoc = parseString(cur_cred.get_signature().get_xml())
589 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
590 signatures.appendChild(ele)
592 # Get the finished product
593 self.xml = doc.toxml("utf-8")
596 def save_to_random_tmp_file(self):
597 fp, filename = mkstemp(suffix='cred', text=True)
598 fp = os.fdopen(fp, "w")
599 self.save_to_file(filename, save_parents=True, filep=fp)
602 def save_to_file(self, filename, save_parents=True, filep=None):
608 f = open(filename, "w")
612 def save_to_string(self, save_parents=True):
622 def set_refid(self, rid):
626 # Figure out what refids exist, and update this credential's id
627 # so that it doesn't clobber the others. Returns the refids of
630 def updateRefID(self):
632 self.set_refid('ref0')
637 next_cred = self.parent
639 refs.append(next_cred.get_refid())
641 next_cred = next_cred.parent
646 # Find a unique refid for this credential
647 rid = self.get_refid()
650 rid = "ref%d" % (val + 1)
655 # Return the set of parent credential ref ids
664 # Sign the XML file created by encode()
667 # In general, a signed credential obtained externally should
668 # not be changed else the signature is no longer valid. So, once
669 # you have loaded an existing signed credential, do not call encode() or sign() on it.
672 if not self.issuer_privkey:
673 logger.warn("Cannot sign credential (no private key)")
675 if not self.issuer_gid:
676 logger.warn("Cannot sign credential (no issuer gid)")
678 doc = parseString(self.get_xml())
679 sigs = doc.getElementsByTagName("signatures")[0]
681 # Create the signature template to be signed
682 signature = Signature()
683 signature.set_refid(self.get_refid())
684 sdoc = parseString(signature.get_xml())
685 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
686 sigs.appendChild(sig_ele)
688 self.xml = doc.toxml("utf-8")
691 # Split the issuer GID into multiple certificates if it's a chain
692 chain = GID(filename=self.issuer_gid)
695 gid_files.append(chain.save_to_random_tmp_file(False))
696 if chain.get_parent():
697 chain = chain.get_parent()
702 # Call out to xmlsec1 to sign it
703 ref = 'Sig_%s' % self.get_refid()
704 filename = self.save_to_random_tmp_file()
705 xmlsec1 = self.get_xmlsec1_path()
707 raise Exception("Could not locate required 'xmlsec1' program")
708 command = '%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
709 % (xmlsec1, ref, self.issuer_privkey, ",".join(gid_files), filename)
710 # print 'command',command
711 signed = os.popen(command).read()
714 for gid_file in gid_files:
724 # Retrieve the attributes of the credential from the XML.
725 # This is automatically called by the various get_* methods of
726 # this class and should not need to be called explicitly.
734 doc = parseString(self.xml)
735 except ExpatError as e:
736 raise CredentialNotVerifiable("Malformed credential")
737 doc = parseString(self.xml)
739 signed_cred = doc.getElementsByTagName("signed-credential")
741 # Is this a signed-cred or just a cred?
742 if len(signed_cred) > 0:
743 creds = signed_cred[0].getElementsByTagName("credential")
744 signatures = signed_cred[0].getElementsByTagName("signatures")
745 if len(signatures) > 0:
746 sigs = signatures[0].getElementsByTagName("Signature")
748 creds = doc.getElementsByTagName("credential")
750 if creds is None or len(creds) == 0:
751 # malformed cred file
752 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
754 # Just take the first cred if there are more than one
757 self.set_refid(cred.getAttribute("xml:id"))
758 self.set_expiration(utcparse(getTextNode(cred, "expires")))
759 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
760 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
763 ## This code until the end of function rewritten by Aaron Helsinger
766 priv_nodes = cred.getElementsByTagName("privileges")
767 if len(priv_nodes) > 0:
768 privs = priv_nodes[0]
769 for priv in privs.getElementsByTagName("privilege"):
770 kind = getTextNode(priv, "name")
771 deleg = str2bool(getTextNode(priv, "can_delegate"))
773 # Convert * into the default privileges for the credential's type
774 # Each inherits the delegatability from the * above
775 _ , type = urn_to_hrn(self.gidObject.get_urn())
776 rl = determine_rights(type, self.gidObject.get_urn())
781 rlist.add(Right(kind.strip(), deleg))
782 self.set_privileges(rlist)
786 parent = cred.getElementsByTagName("parent")
788 parent_doc = parent[0].getElementsByTagName("credential")[0]
789 parent_xml = parent_doc.toxml("utf-8")
790 if parent_xml is None or parent_xml.strip() == "":
791 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
792 self.parent = Credential(string=parent_xml)
795 # Assign the signatures to the credentials
797 Sig = Signature(string=sig.toxml("utf-8"))
799 for cur_cred in self.get_credential_list():
800 if cur_cred.get_refid() == Sig.get_refid():
801 cur_cred.set_signature(Sig)
806 # trusted_certs: A list of trusted GID filenames (not GID objects!)
807 # Chaining is not supported within the GIDs by xmlsec1.
809 # trusted_certs_required: Should usually be true. Set False means an
810 # empty list of trusted_certs would still let this method pass.
811 # It just skips xmlsec1 verification et al. Only used by some utils
814 # . All of the signatures are valid and that the issuers trace back
815 # to trusted roots (performed by xmlsec1)
816 # . The XML matches the credential schema
817 # . That the issuer of the credential is the authority in the target's urn
818 # . In the case of a delegated credential, this must be true of the root
819 # . That all of the gids presented in the credential are valid
820 # . Including verifying GID chains, and includ the issuer
821 # . The credential is not expired
823 # -- For Delegates (credentials with parents)
824 # . The privileges must be a subset of the parent credentials
825 # . The privileges must have "can_delegate" set for each delegated privilege
826 # . The target gid must be the same between child and parents
827 # . The expiry time on the child must be no later than the parent
828 # . The signer of the child must be the owner of the parent
830 # -- Verify does *NOT*
831 # . ensure that an xmlrpc client's gid matches a credential gid, that
832 # must be done elsewhere
834 # @param trusted_certs: The certificates of trusted CA certificates
835 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
839 # validate against RelaxNG schema
841 if schema and os.path.exists(schema):
842 tree = etree.parse(StringIO(self.xml))
843 schema_doc = etree.parse(schema)
844 xmlschema = etree.XMLSchema(schema_doc)
845 if not xmlschema.validate(tree):
846 error = xmlschema.error_log.last_error
847 message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line)
848 raise CredentialNotVerifiable(message)
850 if trusted_certs_required and trusted_certs is None:
853 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
854 trusted_cert_objects = []
855 ok_trusted_certs = []
856 # If caller explicitly passed in None that means skip cert chain validation.
857 # Strange and not typical
858 if trusted_certs is not None:
859 for f in trusted_certs:
861 # Failures here include unreadable files
863 trusted_cert_objects.append(GID(filename=f))
864 ok_trusted_certs.append(f)
865 except Exception as exc:
866 logger.error("Failed to load trusted cert from %s: %r"%( f, exc))
867 trusted_certs = ok_trusted_certs
869 # make sure it is not expired
870 if self.get_expiration() < datetime.datetime.utcnow():
871 raise CredentialNotVerifiable("Credential %s expired at %s" % \
873 self.expiration.strftime(SFATIME_FORMAT)))
875 # Verify the signatures
876 filename = self.save_to_random_tmp_file()
878 # If caller explicitly passed in None that means skip cert chain validation.
879 # - Strange and not typical
880 if trusted_certs is not None:
881 # Verify the gids of this cred and of its parents
882 for cur_cred in self.get_credential_list():
883 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
884 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
887 refs.append("Sig_%s" % self.get_refid())
889 parentRefs = self.updateRefID()
890 for ref in parentRefs:
891 refs.append("Sig_%s" % ref)
894 # If caller explicitly passed in None that means skip xmlsec1 validation.
895 # Strange and not typical
896 if trusted_certs is None:
900 # up to fedora20 we used os.popen and checked that the output begins with OK
901 # turns out, with fedora21, there is extra input before this 'OK' thing
902 # looks like we're better off just using the exit code - that's what it is made for
903 #cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
904 #command = '{} --verify --node-id "{}" {} {} 2>&1'.\
905 # format(self.xmlsec_path, ref, cert_args, filename)
906 xmlsec1 = self.get_xmlsec1_path()
908 raise Exception("Could not locate required 'xmlsec1' program")
909 command = [ xmlsec1, '--verify', '--node-id', ref ]
910 for trusted in trusted_certs:
911 command += ["--trusted-pem", trusted ]
912 command += [ filename ]
913 logger.debug("Running " + " ".join(command))
915 verified = subprocess.check_output(command, stderr=subprocess.STDOUT)
916 logger.debug("xmlsec command returned {}".format(verified))
917 if "OK\n" not in verified:
918 logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output")
919 except subprocess.CalledProcessError as e:
921 # xmlsec errors have a msg= which is the interesting bit.
922 mstart = verified.find("msg=")
924 if mstart > -1 and len(verified) > 4:
926 mend = verified.find('\\', mstart)
927 msg = verified[mstart:mend]
928 logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip()))
929 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \
930 (self.pretty_cred(), ref, msg))
933 # Verify the parents (delegation)
935 self.verify_parent(self.parent)
937 # Make sure the issuer is the target's authority, and is
939 self.verify_issuer(trusted_cert_objects)
943 # Creates a list of the credential and its parents, with the root
944 # (original delegated credential) as the last item in the list
945 def get_credential_list(self):
949 list.append(cur_cred)
951 cur_cred = cur_cred.parent
957 # Make sure the credential's target gid (a) was signed by or (b)
958 # is the same as the entity that signed the original credential,
959 # or (c) is an authority over the target's namespace.
960 # Also ensure that the credential issuer / signer itself has a valid
961 # GID signature chain (signed by an authority with namespace rights).
962 def verify_issuer(self, trusted_gids):
963 root_cred = self.get_credential_list()[-1]
964 root_target_gid = root_cred.get_gid_object()
965 if root_cred.get_signature() is None:
967 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
969 root_cred_signer = root_cred.get_signature().get_issuer_gid()
972 # Allow non authority to sign target and cred about target.
974 # Why do we need to allow non authorities to sign?
975 # If in the target gid validation step we correctly
976 # checked that the target is only signed by an authority,
977 # then this is just a special case of case 3.
978 # This short-circuit is the common case currently -
979 # and cause GID validation doesn't check 'authority',
980 # this allows users to generate valid slice credentials.
981 if root_target_gid.is_signed_by_cert(root_cred_signer):
982 # cred signer matches target signer, return success
986 # Allow someone to sign credential about themeselves. Used?
987 # If not, remove this.
988 #root_target_gid_str = root_target_gid.save_to_string()
989 #root_cred_signer_str = root_cred_signer.save_to_string()
990 #if root_target_gid_str == root_cred_signer_str:
991 # # cred signer is target, return success
996 # root_cred_signer is not the target_gid
997 # So this is a different gid that we have not verified.
998 # xmlsec1 verified the cert chain on this already, but
999 # it hasn't verified that the gid meets the HRN namespace
1001 # Below we'll ensure that it is an authority.
1002 # But we haven't verified that it is _signed by_ an authority
1003 # We also don't know if xmlsec1 requires that cert signers
1004 # are marked as CAs.
1006 # Note that if verify() gave us no trusted_gids then this
1007 # call will fail. So skip it if we have no trusted_gids
1008 if trusted_gids and len(trusted_gids) > 0:
1009 root_cred_signer.verify_chain(trusted_gids)
1011 logger.debug("Cannot verify that cred signer is signed by a trusted authority. "
1012 "No trusted gids. Skipping that check.")
1014 # See if the signer is an authority over the domain of the target.
1015 # There are multiple types of authority - accept them all here
1016 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
1017 root_cred_signer_type = root_cred_signer.get_type()
1018 if root_cred_signer_type.find('authority') == 0:
1019 #logger.debug('Cred signer is an authority')
1020 # signer is an authority, see if target is in authority's domain
1021 signerhrn = root_cred_signer.get_hrn()
1022 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
1025 # We've required that the credential be signed by an authority
1026 # for that domain. Reasonable and probably correct.
1027 # A looser model would also allow the signer to be an authority
1028 # in my control framework - eg My CA or CH. Even if it is not
1029 # the CH that issued these, eg, user credentials.
1031 # Give up, credential does not pass issuer verification
1033 raise CredentialNotVerifiable(
1034 "Could not verify credential owned by {} for object {}. "
1035 "Cred signer {} not the trusted authority for Cred target {}"
1036 .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(),
1037 root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
1040 # -- For Delegates (credentials with parents) verify that:
1041 # . The privileges must be a subset of the parent credentials
1042 # . The privileges must have "can_delegate" set for each delegated privilege
1043 # . The target gid must be the same between child and parents
1044 # . The expiry time on the child must be no later than the parent
1045 # . The signer of the child must be the owner of the parent
1046 def verify_parent(self, parent_cred):
1047 # make sure the rights given to the child are a subset of the
1048 # parents rights (and check delegate bits)
1049 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
1051 "Parent cred {} (ref {}) rights {} "
1052 " not superset of delegated cred {} (ref {}) rights {}"
1053 .format(parent_cred.pretty_cred(),parent_cred.get_refid(),
1054 parent_cred.get_privileges().pretty_rights(),
1055 self.pretty_cred(), self.get_refid(),
1056 self.get_privileges().pretty_rights()))
1057 logger.error(message)
1058 logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string()))
1059 logger.error("self details {}".format(self.get_privileges().save_to_string()))
1060 raise ChildRightsNotSubsetOfParent(message)
1062 # make sure my target gid is the same as the parent's
1063 if not parent_cred.get_gid_object().save_to_string() == \
1064 self.get_gid_object().save_to_string():
1066 "Delegated cred {}: Target gid not equal between parent and child. Parent {}"
1067 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1068 logger.error(message)
1069 logger.error("parent details {}".format(parent_cred.save_to_string()))
1070 logger.error("self details {}".format(self.save_to_string()))
1071 raise CredentialNotVerifiable(message)
1073 # make sure my expiry time is <= my parent's
1074 if not parent_cred.get_expiration() >= self.get_expiration():
1075 raise CredentialNotVerifiable(
1076 "Delegated credential {} expires after parent {}"
1077 .format(self.pretty_cred(), parent_cred.pretty_cred()))
1079 # make sure my signer is the parent's caller
1080 if not parent_cred.get_gid_caller().save_to_string(False) == \
1081 self.get_signature().get_issuer_gid().save_to_string(False):
1082 message = "Delegated credential {} not signed by parent {}'s caller"\
1083 .format(self.pretty_cred(), parent_cred.pretty_cred())
1084 logger.error(message)
1085 logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred()))
1086 logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string()))
1087 logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred()))
1088 logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string()))
1089 raise CredentialNotVerifiable(message)
1092 if parent_cred.parent:
1093 parent_cred.verify_parent(parent_cred.parent)
1096 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
1098 Return a delegated copy of this credential, delegated to the
1099 specified gid's user.
1101 # get the gid of the object we are delegating
1102 object_gid = self.get_gid_object()
1103 object_hrn = object_gid.get_hrn()
1105 # the hrn of the user who will be delegated to
1106 delegee_gid = GID(filename=delegee_gidfile)
1107 delegee_hrn = delegee_gid.get_hrn()
1109 #user_key = Keypair(filename=keyfile)
1110 #user_hrn = self.get_gid_caller().get_hrn()
1111 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
1112 dcred = Credential(subject=subject_string)
1113 dcred.set_gid_caller(delegee_gid)
1114 dcred.set_gid_object(object_gid)
1115 dcred.set_parent(self)
1116 dcred.set_expiration(self.get_expiration())
1117 dcred.set_privileges(self.get_privileges())
1118 dcred.get_privileges().delegate_all_privileges(True)
1119 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
1120 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
1127 def get_filename(self):
1128 return getattr(self,'filename',None)
1130 def actual_caller_hrn (self):
1131 """a helper method used by some API calls like e.g. Allocate
1132 to try and find out who really is the original caller
1134 This admittedly is a bit of a hack, please USE IN LAST RESORT
1136 This code uses a heuristic to identify a delegated credential
1138 A first known restriction if for traffic that gets through a slice manager
1139 in this case the hrn reported is the one from the last SM in the call graph
1140 which is not at all what is meant here"""
1142 caller_hrn = self.get_gid_caller().get_hrn()
1143 issuer_hrn = self.get_signature().get_issuer_gid().get_hrn()
1144 subject_hrn = self.get_gid_object().get_hrn()
1145 # if we find that the caller_hrn is an immediate descendant of the issuer, then
1146 # this seems to be a 'regular' credential
1147 if caller_hrn.startswith(issuer_hrn):
1148 actual_caller_hrn=caller_hrn
1149 # else this looks like a delegated credential, and the real caller is the issuer
1151 actual_caller_hrn=issuer_hrn
1152 logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s"
1153 %(caller_hrn,issuer_hrn,actual_caller_hrn))
1154 return actual_caller_hrn
1157 # Dump the contents of a credential to stdout in human-readable format
1159 # @param dump_parents If true, also dump the parent certificates
1160 def dump (self, *args, **kwargs):
1161 print(self.dump_string(*args, **kwargs))
1163 # SFA code ignores show_xml and disables printing the cred xml
1164 def dump_string(self, dump_parents=False, show_xml=False):
1166 result += "CREDENTIAL %s\n" % self.pretty_subject()
1167 filename=self.get_filename()
1168 if filename: result += "Filename %s\n"%filename
1169 privileges = self.get_privileges()
1171 result += " privs: %s\n" % privileges.save_to_string()
1173 result += " privs: \n"
1174 gidCaller = self.get_gid_caller()
1176 result += " gidCaller:\n"
1177 result += gidCaller.dump_string(8, dump_parents)
1179 if self.get_signature():
1180 result += " gidIssuer:\n"
1181 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
1184 result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n"
1186 gidObject = self.get_gid_object()
1188 result += " gidObject:\n"
1189 result += gidObject.dump_string(8, dump_parents)
1191 if self.parent and dump_parents:
1192 result += "\nPARENT"
1193 result += self.parent.dump_string(True)
1195 if show_xml and HAVELXML:
1197 tree = etree.parse(StringIO(self.xml))
1198 aside = etree.tostring(tree, pretty_print=True)
1199 result += "\nXML:\n\n"
1201 result += "\nEnd XML\n"
1204 print("exc. Credential.dump_string / XML")
1205 traceback.print_exc()