1 #----------------------------------------------------------------------
\r
2 # Copyright (c) 2008 Board of Trustees, Princeton University
\r
4 # Permission is hereby granted, free of charge, to any person obtaining
\r
5 # a copy of this software and/or hardware specification (the "Work") to
\r
6 # deal in the Work without restriction, including without limitation the
\r
7 # rights to use, copy, modify, merge, publish, distribute, sublicense,
\r
8 # and/or sell copies of the Work, and to permit persons to whom the Work
\r
9 # is furnished to do so, subject to the following conditions:
\r
11 # The above copyright notice and this permission notice shall be
\r
12 # included in all copies or substantial portions of the Work.
\r
14 # THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
\r
15 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
\r
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
\r
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
\r
18 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
\r
19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\r
20 # OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS
\r
22 #----------------------------------------------------------------------
\r
24 # Implements SFA Credentials
\r
26 # Credentials are signed XML files that assign a subject gid privileges to an object gid
\r
34 from tempfile import mkstemp
\r
35 from xml.dom.minidom import Document, parseString
\r
36 from lxml import etree
\r
37 from dateutil.parser import parse
\r
38 from StringIO import StringIO
\r
40 from sfa.util.faults import *
\r
41 from sfa.util.sfalogging import logger
\r
42 from sfa.trust.certificate import Keypair
\r
43 from sfa.trust.credential_legacy import CredentialLegacy
\r
44 from sfa.trust.rights import Right, Rights
\r
45 from sfa.trust.gid import GID
\r
47 # 2 weeks, in seconds
\r
48 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
\r
52 # . make privs match between PG and PL
\r
53 # . Need to add support for other types of credentials, e.g. tickets
\r
54 # . add namespaces to signed-credential element?
\r
56 signature_template = \
\r
58 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
\r
60 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
61 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
62 <Reference URI="#%s">
\r
64 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
66 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
67 <DigestValue></DigestValue>
\r
82 # PG formats the template (whitespace) slightly differently.
\r
83 # Note that they don't include the xmlns in the template, but add it later.
\r
84 # Otherwise the two are equivalent.
\r
85 #signature_template_as_in_pg = \
\r
87 #<Signature xml:id="Sig_%s" >
\r
89 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
90 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
91 # <Reference URI="#%s">
\r
93 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
95 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
96 # <DigestValue></DigestValue>
\r
99 # <SignatureValue />
\r
102 # <X509SubjectName/>
\r
103 # <X509IssuerSerial/>
\r
104 # <X509Certificate/>
\r
112 # Convert a string into a bool
\r
113 # used to convert an xsd:boolean to a Python boolean
\r
115 if str.lower() in ['true','1']:
\r
121 # Utility function to get the text of an XML element
\r
123 def getTextNode(element, subele):
\r
124 sub = element.getElementsByTagName(subele)[0]
\r
125 if len(sub.childNodes) > 0:
\r
126 return sub.childNodes[0].nodeValue
\r
131 # Utility function to set the text of an XML element
\r
132 # It creates the element, adds the text to it,
\r
133 # and then appends it to the parent.
\r
135 def append_sub(doc, parent, element, text):
\r
136 ele = doc.createElement(element)
\r
137 ele.appendChild(doc.createTextNode(text))
\r
138 parent.appendChild(ele)
\r
141 # Signature contains information about an xmlsec1 signature
\r
142 # for a signed-credential
\r
145 class Signature(object):
\r
147 def __init__(self, string=None):
\r
149 self.issuer_gid = None
\r
156 def get_refid(self):
\r
166 def set_refid(self, id):
\r
169 def get_issuer_gid(self):
\r
174 def set_issuer_gid(self, gid):
\r
178 doc = parseString(self.xml)
\r
179 sig = doc.getElementsByTagName("Signature")[0]
\r
180 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
\r
181 keyinfo = sig.getElementsByTagName("X509Data")[0]
\r
182 szgid = getTextNode(keyinfo, "X509Certificate")
\r
183 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
\r
184 self.set_issuer_gid(GID(string=szgid))
\r
187 self.xml = signature_template % (self.get_refid(), self.get_refid())
\r
191 # A credential provides a caller gid with privileges to an object gid.
\r
192 # A signed credential is signed by the object's authority.
\r
194 # Credentials are encoded in one of two ways. The legacy style places
\r
195 # it in the subjectAltName of an X509 certificate. The new credentials
\r
196 # are placed in signed XML.
\r
199 # In general, a signed credential obtained externally should
\r
200 # not be changed else the signature is no longer valid. So, once
\r
201 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
203 def filter_creds_by_caller(creds, caller_hrn):
\r
205 Returns a list of creds who's gid caller matches the
\r
206 specified caller hrn
\r
208 if not isinstance(creds, list): creds = [creds]
\r
212 tmp_cred = Credential(string=cred)
\r
213 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
\r
214 caller_creds.append(cred)
\r
216 return caller_creds
\r
218 class Credential(object):
\r
221 # Create a Credential object
\r
223 # @param create If true, create a blank x509 certificate
\r
224 # @param subject If subject!=None, create an x509 cert with the subject name
\r
225 # @param string If string!=None, load the credential from the string
\r
226 # @param filename If filename!=None, load the credential from the file
\r
227 # FIXME: create and subject are ignored!
\r
228 def __init__(self, create=False, subject=None, string=None, filename=None):
\r
229 self.gidCaller = None
\r
230 self.gidObject = None
\r
231 self.expiration = None
\r
232 self.privileges = None
\r
233 self.issuer_privkey = None
\r
234 self.issuer_gid = None
\r
235 self.issuer_pubkey = None
\r
237 self.signature = None
\r
242 # Check if this is a legacy credential, translate it if so
\r
243 if string or filename:
\r
247 str = file(filename).read()
\r
249 if str.strip().startswith("-----"):
\r
250 self.legacy = CredentialLegacy(False,string=str)
\r
251 self.translate_legacy(str)
\r
256 # Find an xmlsec1 path
\r
257 self.xmlsec_path = ''
\r
258 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
\r
260 if os.path.isfile(path + '/' + 'xmlsec1'):
\r
261 self.xmlsec_path = path + '/' + 'xmlsec1'
\r
264 def get_subject(self):
\r
265 if not self.gidObject:
\r
267 return self.gidObject.get_subject()
\r
269 def get_signature(self):
\r
270 if not self.signature:
\r
272 return self.signature
\r
274 def set_signature(self, sig):
\r
275 self.signature = sig
\r
279 # Translate a legacy credential into a new one
\r
281 # @param String of the legacy credential
\r
283 def translate_legacy(self, str):
\r
284 legacy = CredentialLegacy(False,string=str)
\r
285 self.gidCaller = legacy.get_gid_caller()
\r
286 self.gidObject = legacy.get_gid_object()
\r
287 lifetime = legacy.get_lifetime()
\r
289 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
291 self.set_expiration(int(lifetime))
\r
292 self.lifeTime = legacy.get_lifetime()
\r
293 self.set_privileges(legacy.get_privileges())
\r
294 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
\r
297 # Need the issuer's private key and name
\r
298 # @param key Keypair object containing the private key of the issuer
\r
299 # @param gid GID of the issuing authority
\r
301 def set_issuer_keys(self, privkey, gid):
\r
302 self.issuer_privkey = privkey
\r
303 self.issuer_gid = gid
\r
307 # Set this credential's parent
\r
308 def set_parent(self, cred):
\r
313 # set the GID of the caller
\r
315 # @param gid GID object of the caller
\r
317 def set_gid_caller(self, gid):
\r
318 self.gidCaller = gid
\r
319 # gid origin caller is the caller's gid by default
\r
320 self.gidOriginCaller = gid
\r
323 # get the GID of the object
\r
325 def get_gid_caller(self):
\r
326 if not self.gidCaller:
\r
328 return self.gidCaller
\r
331 # set the GID of the object
\r
333 # @param gid GID object of the object
\r
335 def set_gid_object(self, gid):
\r
336 self.gidObject = gid
\r
339 # get the GID of the object
\r
341 def get_gid_object(self):
\r
342 if not self.gidObject:
\r
344 return self.gidObject
\r
349 # Expiration: an absolute UTC time of expiration (as either an int or datetime)
\r
351 def set_expiration(self, expiration):
\r
352 if isinstance(expiration, int):
\r
353 self.expiration = datetime.datetime.fromtimestamp(expiration)
\r
355 self.expiration = expiration
\r
359 # get the lifetime of the credential (in datetime format)
\r
361 def get_expiration(self):
\r
362 if not self.expiration:
\r
364 return self.expiration
\r
368 def get_lifetime(self):
\r
369 return self.get_expiration()
\r
372 # set the privileges
\r
374 # @param privs either a comma-separated list of privileges of a Rights object
\r
376 def set_privileges(self, privs):
\r
377 if isinstance(privs, str):
\r
378 self.privileges = Rights(string = privs)
\r
380 self.privileges = privs
\r
384 # return the privileges as a Rights object
\r
386 def get_privileges(self):
\r
387 if not self.privileges:
\r
389 return self.privileges
\r
392 # determine whether the credential allows a particular operation to be
\r
395 # @param op_name string specifying name of operation ("lookup", "update", etc)
\r
397 def can_perform(self, op_name):
\r
398 rights = self.get_privileges()
\r
403 return rights.can_perform(op_name)
\r
407 # Encode the attributes of the credential into an XML string
\r
408 # This should be done immediately before signing the credential.
\r
410 # In general, a signed credential obtained externally should
\r
411 # not be changed else the signature is no longer valid. So, once
\r
412 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
415 # Create the XML document
\r
417 signed_cred = doc.createElement("signed-credential")
\r
419 # PG adds these. It would be nice to be consistent.
\r
420 # But it's kind of odd for PL to use PG schemas that talk
\r
421 # about tickets, and the PG CM policies.
\r
422 # Note the careful addition of attributes from the parent below...
\r
423 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
424 # signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
425 # 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")
\r
427 doc.appendChild(signed_cred)
\r
429 # Fill in the <credential> bit
\r
430 cred = doc.createElement("credential")
\r
431 cred.setAttribute("xml:id", self.get_refid())
\r
432 signed_cred.appendChild(cred)
\r
433 append_sub(doc, cred, "type", "privilege")
\r
434 append_sub(doc, cred, "serial", "8")
\r
435 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
\r
436 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
\r
437 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
\r
438 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
\r
439 append_sub(doc, cred, "uuid", "")
\r
440 if not self.expiration:
\r
441 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
442 self.expiration = self.expiration.replace(microsecond=0)
\r
443 append_sub(doc, cred, "expires", self.expiration.isoformat())
\r
444 privileges = doc.createElement("privileges")
\r
445 cred.appendChild(privileges)
\r
447 if self.privileges:
\r
448 rights = self.get_privileges()
\r
449 for right in rights.rights:
\r
450 priv = doc.createElement("privilege")
\r
451 append_sub(doc, priv, "name", right.kind)
\r
452 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
\r
453 privileges.appendChild(priv)
\r
455 # Add the parent credential if it exists
\r
457 sdoc = parseString(self.parent.get_xml())
\r
458 # If the root node is a signed-credential (it should be), then
\r
459 # get all its attributes and attach those to our signed_cred
\r
461 # Specifically, PG adds attributes for namespaces (which is reasonable),
\r
462 # and we need to include those again here or else their signature
\r
463 # no longer matches on the credential.
\r
464 # We expect three of these, but here we copy them all:
\r
465 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
466 # signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
467 # 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")
\r
468 parentRoot = sdoc.documentElement
\r
469 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
\r
470 for attrIx in range(0, parentRoot.attributes.length):
\r
471 attr = parentRoot.attributes.item(attrIx)
\r
472 # returns the old attribute of same name that was
\r
473 # on the credential
\r
474 # Below throws InUse exception if we forgot to clone the attribute first
\r
475 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
\r
476 if oldAttr and oldAttr.value != attr.value:
\r
477 msg = "Delegating cred from owner %s to %s over %s replaced attribute %s value %s with %s" % (self.parent.gidCaller.get_urn(), self.gidCaller.get_urn(), self.gidObject.get_urn(), oldAttr.name, oldAttr.value, attr.value)
\r
479 raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
\r
481 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
\r
482 p = doc.createElement("parent")
\r
483 p.appendChild(p_cred)
\r
484 cred.appendChild(p)
\r
485 # done handling parent credential
\r
487 # Create the <signatures> tag
\r
488 signatures = doc.createElement("signatures")
\r
489 signed_cred.appendChild(signatures)
\r
491 # Add any parent signatures
\r
493 for cur_cred in self.get_credential_list()[1:]:
\r
494 sdoc = parseString(cur_cred.get_signature().get_xml())
\r
495 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
496 signatures.appendChild(ele)
\r
498 # Get the finished product
\r
499 self.xml = doc.toxml()
\r
502 def save_to_random_tmp_file(self):
\r
503 fp, filename = mkstemp(suffix='cred', text=True)
\r
504 fp = os.fdopen(fp, "w")
\r
505 self.save_to_file(filename, save_parents=True, filep=fp)
\r
508 def save_to_file(self, filename, save_parents=True, filep=None):
\r
514 f = open(filename, "w")
\r
518 def save_to_string(self, save_parents=True):
\r
523 def get_refid(self):
\r
525 self.refid = 'ref0'
\r
528 def set_refid(self, rid):
\r
532 # Figure out what refids exist, and update this credential's id
\r
533 # so that it doesn't clobber the others. Returns the refids of
\r
536 def updateRefID(self):
\r
537 if not self.parent:
\r
538 self.set_refid('ref0')
\r
543 next_cred = self.parent
\r
545 refs.append(next_cred.get_refid())
\r
546 if next_cred.parent:
\r
547 next_cred = next_cred.parent
\r
552 # Find a unique refid for this credential
\r
553 rid = self.get_refid()
\r
556 rid = "ref%d" % (val + 1)
\r
558 # Set the new refid
\r
559 self.set_refid(rid)
\r
561 # Return the set of parent credential ref ids
\r
570 # Sign the XML file created by encode()
\r
573 # In general, a signed credential obtained externally should
\r
574 # not be changed else the signature is no longer valid. So, once
\r
575 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
578 if not self.issuer_privkey or not self.issuer_gid:
\r
580 doc = parseString(self.get_xml())
\r
581 sigs = doc.getElementsByTagName("signatures")[0]
\r
583 # Create the signature template to be signed
\r
584 signature = Signature()
\r
585 signature.set_refid(self.get_refid())
\r
586 sdoc = parseString(signature.get_xml())
\r
587 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
588 sigs.appendChild(sig_ele)
\r
590 self.xml = doc.toxml()
\r
593 # Split the issuer GID into multiple certificates if it's a chain
\r
594 chain = GID(filename=self.issuer_gid)
\r
597 gid_files.append(chain.save_to_random_tmp_file(False))
\r
598 if chain.get_parent():
\r
599 chain = chain.get_parent()
\r
604 # Call out to xmlsec1 to sign it
\r
605 ref = 'Sig_%s' % self.get_refid()
\r
606 filename = self.save_to_random_tmp_file()
\r
607 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
\r
608 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
\r
609 os.remove(filename)
\r
611 for gid_file in gid_files:
\r
612 os.remove(gid_file)
\r
616 # This is no longer a legacy credential
\r
620 # Update signatures
\r
625 # Retrieve the attributes of the credential from the XML.
\r
626 # This is automatically called by the various get_* methods of
\r
627 # this class and should not need to be called explicitly.
\r
632 doc = parseString(self.xml)
\r
634 signed_cred = doc.getElementsByTagName("signed-credential")
\r
636 # Is this a signed-cred or just a cred?
\r
637 if len(signed_cred) > 0:
\r
638 cred = signed_cred[0].getElementsByTagName("credential")[0]
\r
639 signatures = signed_cred[0].getElementsByTagName("signatures")
\r
640 if len(signatures) > 0:
\r
641 sigs = signatures[0].getElementsByTagName("Signature")
\r
643 cred = doc.getElementsByTagName("credential")[0]
\r
646 self.set_refid(cred.getAttribute("xml:id"))
\r
647 self.set_expiration(parse(getTextNode(cred, "expires")))
\r
648 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
\r
649 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
\r
652 # Process privileges
\r
653 privs = cred.getElementsByTagName("privileges")[0]
\r
655 for priv in privs.getElementsByTagName("privilege"):
\r
656 kind = getTextNode(priv, "name")
\r
657 deleg = str2bool(getTextNode(priv, "can_delegate"))
\r
659 # Convert * into the default privileges for the credential's type
\r
660 # Each inherits the delegatability from the * above
\r
661 _ , type = urn_to_hrn(self.gidObject.get_urn())
\r
662 rl = rlist.determine_rights(type, self.gidObject.get_urn())
\r
663 for r in rl.rights:
\r
667 rlist.add(Right(kind.strip(), deleg))
\r
668 self.set_privileges(rlist)
\r
671 # Is there a parent?
\r
672 parent = cred.getElementsByTagName("parent")
\r
673 if len(parent) > 0:
\r
674 parent_doc = parent[0].getElementsByTagName("credential")[0]
\r
675 parent_xml = parent_doc.toxml()
\r
676 self.parent = Credential(string=parent_xml)
\r
679 # Assign the signatures to the credentials
\r
681 Sig = Signature(string=sig.toxml())
\r
683 for cur_cred in self.get_credential_list():
\r
684 if cur_cred.get_refid() == Sig.get_refid():
\r
685 cur_cred.set_signature(Sig)
\r
690 # trusted_certs: A list of trusted GID filenames (not GID objects!)
\r
691 # Chaining is not supported within the GIDs by xmlsec1.
\r
693 # trusted_certs_required: Should usually be true. Set False means an
\r
694 # empty list of trusted_certs would still let this method pass.
\r
695 # It just skips xmlsec1 verification et al. Only used by some utils
\r
698 # . All of the signatures are valid and that the issuers trace back
\r
699 # to trusted roots (performed by xmlsec1)
\r
700 # . The XML matches the credential schema
\r
701 # . That the issuer of the credential is the authority in the target's urn
\r
702 # . In the case of a delegated credential, this must be true of the root
\r
703 # . That all of the gids presented in the credential are valid
\r
704 # . The credential is not expired
\r
706 # -- For Delegates (credentials with parents)
\r
707 # . The privileges must be a subset of the parent credentials
\r
708 # . The privileges must have "can_delegate" set for each delegated privilege
\r
709 # . The target gid must be the same between child and parents
\r
710 # . The expiry time on the child must be no later than the parent
\r
711 # . The signer of the child must be the owner of the parent
\r
713 # -- Verify does *NOT*
\r
714 # . ensure that an xmlrpc client's gid matches a credential gid, that
\r
715 # must be done elsewhere
\r
717 # @param trusted_certs: The certificates of trusted CA certificates
\r
718 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
\r
722 # validate against RelaxNG schema
\r
723 if not self.legacy:
\r
724 if schema and os.path.exists(schema):
\r
725 tree = etree.parse(StringIO(self.xml))
\r
726 schema_doc = etree.parse(schema)
\r
727 xmlschema = etree.XMLSchema(schema_doc)
\r
728 if not xmlschema.validate(tree):
\r
729 error = xmlschema.error_log.last_error
\r
730 message = "%s (line %s)" % (error.message, error.line)
\r
731 raise CredentialNotVerifiable(message)
\r
733 if trusted_certs_required and trusted_certs is None:
\r
736 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
\r
737 trusted_cert_objects = []
\r
738 ok_trusted_certs = []
\r
739 # If caller explicitly passed in None that means skip cert chain validation.
\r
740 # Strange and not typical
\r
741 if trusted_certs is not None:
\r
742 for f in trusted_certs:
\r
744 # Failures here include unreadable files
\r
746 trusted_cert_objects.append(GID(filename=f))
\r
747 ok_trusted_certs.append(f)
\r
748 except Exception, exc:
\r
749 logger.error("Failed to load trusted cert from %s: %r", f, exc)
\r
750 trusted_certs = ok_trusted_certs
\r
752 # Use legacy verification if this is a legacy credential
\r
754 self.legacy.verify_chain(trusted_cert_objects)
\r
755 if self.legacy.client_gid:
\r
756 self.legacy.client_gid.verify_chain(trusted_cert_objects)
\r
757 if self.legacy.object_gid:
\r
758 self.legacy.object_gid.verify_chain(trusted_cert_objects)
\r
761 # make sure it is not expired
\r
762 if self.get_expiration() < datetime.datetime.utcnow():
\r
763 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
\r
765 # Verify the signatures
\r
766 filename = self.save_to_random_tmp_file()
\r
767 if trusted_certs is not None:
\r
768 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
\r
770 # If caller explicitly passed in None that means skip cert chain validation.
\r
771 # Strange and not typical
\r
772 if trusted_certs is not None:
\r
773 # Verify the gids of this cred and of its parents
\r
774 for cur_cred in self.get_credential_list():
\r
775 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
\r
776 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
\r
779 refs.append("Sig_%s" % self.get_refid())
\r
781 parentRefs = self.updateRefID()
\r
782 for ref in parentRefs:
\r
783 refs.append("Sig_%s" % ref)
\r
786 # If caller explicitly passed in None that means skip xmlsec1 validation.
\r
787 # Strange and not typical
\r
788 if trusted_certs is None:
\r
791 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
\r
792 # (self.xmlsec_path, ref, cert_args, filename)
\r
793 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
\r
794 % (self.xmlsec_path, ref, cert_args, filename)).read()
\r
795 if not verified.strip().startswith("OK"):
\r
796 # xmlsec errors have a msg= which is the interesting bit.
\r
797 mstart = verified.find("msg=")
\r
799 if mstart > -1 and len(verified) > 4:
\r
800 mstart = mstart + 4
\r
801 mend = verified.find('\\', mstart)
\r
802 msg = verified[mstart:mend]
\r
803 raise CredentialNotVerifiable("xmlsec1 error verifying cred using Signature ID %s: %s %s" % (ref, msg, verified.strip()))
\r
804 os.remove(filename)
\r
806 # Verify the parents (delegation)
\r
808 self.verify_parent(self.parent)
\r
810 # Make sure the issuer is the target's authority
\r
811 self.verify_issuer()
\r
815 # Creates a list of the credential and its parents, with the root
\r
816 # (original delegated credential) as the last item in the list
\r
817 def get_credential_list(self):
\r
821 list.append(cur_cred)
\r
822 if cur_cred.parent:
\r
823 cur_cred = cur_cred.parent
\r
829 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
\r
830 # the original credential or an authority over that namespace.
\r
831 def verify_issuer(self):
\r
832 root_cred = self.get_credential_list()[-1]
\r
833 root_target_gid = root_cred.get_gid_object()
\r
834 root_cred_signer = root_cred.get_signature().get_issuer_gid()
\r
836 if root_target_gid.is_signed_by_cert(root_cred_signer):
\r
837 # cred signer matches target signer, return success
\r
840 root_target_gid_str = root_target_gid.save_to_string()
\r
841 root_cred_signer_str = root_cred_signer.save_to_string()
\r
842 if root_target_gid_str == root_cred_signer_str:
\r
843 # cred signer is target, return success
\r
846 # See if it the signer is an authority over the domain of the target
\r
847 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
\r
848 root_cred_signer_type = root_cred_signer.get_type()
\r
849 if (root_cred_signer_type == 'authority'):
\r
850 #sfa_logger.debug('Cred signer is an authority')
\r
851 # signer is an authority, see if target is in authority's domain
\r
852 hrn = root_cred_signer.get_hrn()
\r
853 if root_target_gid.get_hrn().startswith(hrn):
\r
856 # We've required that the credential be signed by an authority
\r
857 # for that domain. Reasonable and probably correct.
\r
858 # A looser model would also allow the signer to be an authority
\r
859 # in my control framework - eg My CA or CH. Even if it is not
\r
860 # the CH that issued these, eg, user credentials.
\r
862 # Give up, credential does not pass issuer verification
\r
864 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred signer %s not the trusted authority for Cred target %s" % (self.gidCaller.get_urn(), self.gidObject.get_urn(), root_cred_signer.get_hrn(), root_target_gid.get_hrn()))
\r
868 # -- For Delegates (credentials with parents) verify that:
\r
869 # . The privileges must be a subset of the parent credentials
\r
870 # . The privileges must have "can_delegate" set for each delegated privilege
\r
871 # . The target gid must be the same between child and parents
\r
872 # . The expiry time on the child must be no later than the parent
\r
873 # . The signer of the child must be the owner of the parent
\r
874 def verify_parent(self, parent_cred):
\r
875 # make sure the rights given to the child are a subset of the
\r
876 # parents rights (and check delegate bits)
\r
877 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
\r
878 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % self.parent.get_refid()) +
\r
879 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred ref %s rights " % self.get_refid()) +
\r
880 self.get_privileges().save_to_string())
\r
882 # make sure my target gid is the same as the parent's
\r
883 if not parent_cred.get_gid_object().save_to_string() == \
\r
884 self.get_gid_object().save_to_string():
\r
885 raise CredentialNotVerifiable("Target gid not equal between parent and child")
\r
887 # make sure my expiry time is <= my parent's
\r
888 if not parent_cred.get_expiration() >= self.get_expiration():
\r
889 raise CredentialNotVerifiable("Delegated credential expires after parent")
\r
891 # make sure my signer is the parent's caller
\r
892 if not parent_cred.get_gid_caller().save_to_string(False) == \
\r
893 self.get_signature().get_issuer_gid().save_to_string(False):
\r
894 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
\r
897 if parent_cred.parent:
\r
898 parent_cred.verify_parent(parent_cred.parent)
\r
901 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
\r
903 Return a delegated copy of this credential, delegated to the
\r
904 specified gid's user.
\r
906 # get the gid of the object we are delegating
\r
907 object_gid = self.get_gid_object()
\r
908 object_hrn = object_gid.get_hrn()
\r
910 # the hrn of the user who will be delegated to
\r
911 delegee_gid = GID(filename=delegee_gidfile)
\r
912 delegee_hrn = delegee_gid.get_hrn()
\r
914 #user_key = Keypair(filename=keyfile)
\r
915 #user_hrn = self.get_gid_caller().get_hrn()
\r
916 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
\r
917 dcred = Credential(subject=subject_string)
\r
918 dcred.set_gid_caller(delegee_gid)
\r
919 dcred.set_gid_object(object_gid)
\r
920 dcred.set_parent(self)
\r
921 dcred.set_expiration(self.get_expiration())
\r
922 dcred.set_privileges(self.get_privileges())
\r
923 dcred.get_privileges().delegate_all_privileges(True)
\r
924 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
\r
925 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
\r
932 def get_filename(self):
\r
933 return getattr(self,'filename',None)
\r
936 # Dump the contents of a credential to stdout in human-readable format
\r
938 # @param dump_parents If true, also dump the parent certificates
\r
939 def dump (self, *args, **kwargs):
\r
940 print self.dump_string(*args, **kwargs)
\r
943 def dump_string(self, dump_parents=False):
\r
945 result += "CREDENTIAL %s\n" % self.get_subject()
\r
946 filename=self.get_filename()
\r
947 if filename: result += "Filename %s\n"%filename
\r
948 result += " privs: %s\n" % self.get_privileges().save_to_string()
\r
949 gidCaller = self.get_gid_caller()
\r
951 result += " gidCaller:\n"
\r
952 result += gidCaller.dump_string(8, dump_parents)
\r
954 if self.get_signature():
\r
955 print " gidIssuer:"
\r
956 self.get_signature().get_issuer_gid().dump(8, dump_parents)
\r
958 gidObject = self.get_gid_object()
\r
960 result += " gidObject:\n"
\r
961 result += gidObject.dump_string(8, dump_parents)
\r
963 if self.parent and dump_parents:
\r
964 result += "\nPARENT"
\r
965 result += self.parent.dump(True)
\r