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 sfa.util.sfatime import utcparse
\r
35 from tempfile import mkstemp
\r
36 from xml.dom.minidom import Document, parseString
\r
37 from lxml import etree
\r
38 from dateutil.parser import parse
\r
39 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
46 from sfa.util.xrn import urn_to_hrn
\r
48 # 2 weeks, in seconds
\r
49 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
\r
53 # . make privs match between PG and PL
\r
54 # . Need to add support for other types of credentials, e.g. tickets
\r
55 # . add namespaces to signed-credential element?
\r
57 signature_template = \
\r
59 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
\r
61 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
62 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
63 <Reference URI="#%s">
\r
65 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
67 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
68 <DigestValue></DigestValue>
\r
83 # PG formats the template (whitespace) slightly differently.
\r
84 # Note that they don't include the xmlns in the template, but add it later.
\r
85 # Otherwise the two are equivalent.
\r
86 #signature_template_as_in_pg = \
\r
88 #<Signature xml:id="Sig_%s" >
\r
90 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
91 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
92 # <Reference URI="#%s">
\r
94 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
96 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
97 # <DigestValue></DigestValue>
\r
100 # <SignatureValue />
\r
103 # <X509SubjectName/>
\r
104 # <X509IssuerSerial/>
\r
105 # <X509Certificate/>
\r
113 # Convert a string into a bool
\r
114 # used to convert an xsd:boolean to a Python boolean
\r
116 if str.lower() in ['true','1']:
\r
122 # Utility function to get the text of an XML element
\r
124 def getTextNode(element, subele):
\r
125 sub = element.getElementsByTagName(subele)[0]
\r
126 if len(sub.childNodes) > 0:
\r
127 return sub.childNodes[0].nodeValue
\r
132 # Utility function to set the text of an XML element
\r
133 # It creates the element, adds the text to it,
\r
134 # and then appends it to the parent.
\r
136 def append_sub(doc, parent, element, text):
\r
137 ele = doc.createElement(element)
\r
138 ele.appendChild(doc.createTextNode(text))
\r
139 parent.appendChild(ele)
\r
142 # Signature contains information about an xmlsec1 signature
\r
143 # for a signed-credential
\r
146 class Signature(object):
\r
148 def __init__(self, string=None):
\r
150 self.issuer_gid = None
\r
157 def get_refid(self):
\r
167 def set_refid(self, id):
\r
170 def get_issuer_gid(self):
\r
175 def set_issuer_gid(self, gid):
\r
179 doc = parseString(self.xml)
\r
180 sig = doc.getElementsByTagName("Signature")[0]
\r
181 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
\r
182 keyinfo = sig.getElementsByTagName("X509Data")[0]
\r
183 szgid = getTextNode(keyinfo, "X509Certificate")
\r
184 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
\r
185 self.set_issuer_gid(GID(string=szgid))
\r
188 self.xml = signature_template % (self.get_refid(), self.get_refid())
\r
192 # A credential provides a caller gid with privileges to an object gid.
\r
193 # A signed credential is signed by the object's authority.
\r
195 # Credentials are encoded in one of two ways. The legacy style places
\r
196 # it in the subjectAltName of an X509 certificate. The new credentials
\r
197 # are placed in signed XML.
\r
200 # In general, a signed credential obtained externally should
\r
201 # not be changed else the signature is no longer valid. So, once
\r
202 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
204 def filter_creds_by_caller(creds, caller_hrn):
\r
206 Returns a list of creds who's gid caller matches the
\r
207 specified caller hrn
\r
209 if not isinstance(creds, list): creds = [creds]
\r
213 tmp_cred = Credential(string=cred)
\r
214 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
\r
215 caller_creds.append(cred)
\r
217 return caller_creds
\r
219 class Credential(object):
\r
222 # Create a Credential object
\r
224 # @param create If true, create a blank x509 certificate
\r
225 # @param subject If subject!=None, create an x509 cert with the subject name
\r
226 # @param string If string!=None, load the credential from the string
\r
227 # @param filename If filename!=None, load the credential from the file
\r
228 # FIXME: create and subject are ignored!
\r
229 def __init__(self, create=False, subject=None, string=None, filename=None):
\r
230 self.gidCaller = None
\r
231 self.gidObject = None
\r
232 self.expiration = None
\r
233 self.privileges = None
\r
234 self.issuer_privkey = None
\r
235 self.issuer_gid = None
\r
236 self.issuer_pubkey = None
\r
238 self.signature = None
\r
243 # Check if this is a legacy credential, translate it if so
\r
244 if string or filename:
\r
248 str = file(filename).read()
\r
250 if str.strip().startswith("-----"):
\r
251 self.legacy = CredentialLegacy(False,string=str)
\r
252 self.translate_legacy(str)
\r
257 # Find an xmlsec1 path
\r
258 self.xmlsec_path = ''
\r
259 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
\r
261 if os.path.isfile(path + '/' + 'xmlsec1'):
\r
262 self.xmlsec_path = path + '/' + 'xmlsec1'
\r
265 def get_subject(self):
\r
266 if not self.gidObject:
\r
268 return self.gidObject.get_subject()
\r
270 def get_signature(self):
\r
271 if not self.signature:
\r
273 return self.signature
\r
275 def set_signature(self, sig):
\r
276 self.signature = sig
\r
280 # Translate a legacy credential into a new one
\r
282 # @param String of the legacy credential
\r
284 def translate_legacy(self, str):
\r
285 legacy = CredentialLegacy(False,string=str)
\r
286 self.gidCaller = legacy.get_gid_caller()
\r
287 self.gidObject = legacy.get_gid_object()
\r
288 lifetime = legacy.get_lifetime()
\r
290 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
292 self.set_expiration(int(lifetime))
\r
293 self.lifeTime = legacy.get_lifetime()
\r
294 self.set_privileges(legacy.get_privileges())
\r
295 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
\r
298 # Need the issuer's private key and name
\r
299 # @param key Keypair object containing the private key of the issuer
\r
300 # @param gid GID of the issuing authority
\r
302 def set_issuer_keys(self, privkey, gid):
\r
303 self.issuer_privkey = privkey
\r
304 self.issuer_gid = gid
\r
308 # Set this credential's parent
\r
309 def set_parent(self, cred):
\r
314 # set the GID of the caller
\r
316 # @param gid GID object of the caller
\r
318 def set_gid_caller(self, gid):
\r
319 self.gidCaller = gid
\r
320 # gid origin caller is the caller's gid by default
\r
321 self.gidOriginCaller = gid
\r
324 # get the GID of the object
\r
326 def get_gid_caller(self):
\r
327 if not self.gidCaller:
\r
329 return self.gidCaller
\r
332 # set the GID of the object
\r
334 # @param gid GID object of the object
\r
336 def set_gid_object(self, gid):
\r
337 self.gidObject = gid
\r
340 # get the GID of the object
\r
342 def get_gid_object(self):
\r
343 if not self.gidObject:
\r
345 return self.gidObject
\r
350 # Expiration: an absolute UTC time of expiration (as either an int or datetime)
\r
352 def set_expiration(self, expiration):
\r
353 if isinstance(expiration, int):
\r
354 self.expiration = datetime.datetime.fromtimestamp(expiration)
\r
356 self.expiration = expiration
\r
360 # get the lifetime of the credential (in datetime format)
\r
362 def get_expiration(self):
\r
363 if not self.expiration:
\r
365 return utcparse(self.expiration)
\r
369 def get_lifetime(self):
\r
370 return self.get_expiration()
\r
373 # set the privileges
\r
375 # @param privs either a comma-separated list of privileges of a Rights object
\r
377 def set_privileges(self, privs):
\r
378 if isinstance(privs, str):
\r
379 self.privileges = Rights(string = privs)
\r
381 self.privileges = privs
\r
385 # return the privileges as a Rights object
\r
387 def get_privileges(self):
\r
388 if not self.privileges:
\r
390 return self.privileges
\r
393 # determine whether the credential allows a particular operation to be
\r
396 # @param op_name string specifying name of operation ("lookup", "update", etc)
\r
398 def can_perform(self, op_name):
\r
399 rights = self.get_privileges()
\r
404 return rights.can_perform(op_name)
\r
408 # Encode the attributes of the credential into an XML string
\r
409 # This should be done immediately before signing the credential.
\r
411 # In general, a signed credential obtained externally should
\r
412 # not be changed else the signature is no longer valid. So, once
\r
413 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
416 # Create the XML document
\r
418 signed_cred = doc.createElement("signed-credential")
\r
420 # PG adds these. It would be nice to be consistent.
\r
421 # But it's kind of odd for PL to use PG schemas that talk
\r
422 # about tickets, and the PG CM policies.
\r
423 # Note the careful addition of attributes from the parent below...
\r
424 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
425 # signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
426 # 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
428 doc.appendChild(signed_cred)
\r
430 # Fill in the <credential> bit
\r
431 cred = doc.createElement("credential")
\r
432 cred.setAttribute("xml:id", self.get_refid())
\r
433 signed_cred.appendChild(cred)
\r
434 append_sub(doc, cred, "type", "privilege")
\r
435 append_sub(doc, cred, "serial", "8")
\r
436 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
\r
437 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
\r
438 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
\r
439 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
\r
440 append_sub(doc, cred, "uuid", "")
\r
441 if not self.expiration:
\r
442 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
443 self.expiration = self.expiration.replace(microsecond=0)
\r
444 append_sub(doc, cred, "expires", self.expiration.isoformat())
\r
445 privileges = doc.createElement("privileges")
\r
446 cred.appendChild(privileges)
\r
448 if self.privileges:
\r
449 rights = self.get_privileges()
\r
450 for right in rights.rights:
\r
451 priv = doc.createElement("privilege")
\r
452 append_sub(doc, priv, "name", right.kind)
\r
453 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
\r
454 privileges.appendChild(priv)
\r
456 # Add the parent credential if it exists
\r
458 sdoc = parseString(self.parent.get_xml())
\r
459 # If the root node is a signed-credential (it should be), then
\r
460 # get all its attributes and attach those to our signed_cred
\r
462 # Specifically, PG adds attributes for namespaces (which is reasonable),
\r
463 # and we need to include those again here or else their signature
\r
464 # no longer matches on the credential.
\r
465 # We expect three of these, but here we copy them all:
\r
466 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
467 # signed_cred.setAttribute("xsinoNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
468 # 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
469 parentRoot = sdoc.documentElement
\r
470 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
\r
471 for attrIx in range(0, parentRoot.attributes.length):
\r
472 attr = parentRoot.attributes.item(attrIx)
\r
473 # returns the old attribute of same name that was
\r
474 # on the credential
\r
475 # Below throws InUse exception if we forgot to clone the attribute first
\r
476 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
\r
477 if oldAttr and oldAttr.value != attr.value:
\r
478 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
480 raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
\r
482 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
\r
483 p = doc.createElement("parent")
\r
484 p.appendChild(p_cred)
\r
485 cred.appendChild(p)
\r
486 # done handling parent credential
\r
488 # Create the <signatures> tag
\r
489 signatures = doc.createElement("signatures")
\r
490 signed_cred.appendChild(signatures)
\r
492 # Add any parent signatures
\r
494 for cur_cred in self.get_credential_list()[1:]:
\r
495 sdoc = parseString(cur_cred.get_signature().get_xml())
\r
496 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
497 signatures.appendChild(ele)
\r
499 # Get the finished product
\r
500 self.xml = doc.toxml()
\r
503 def save_to_random_tmp_file(self):
\r
504 fp, filename = mkstemp(suffix='cred', text=True)
\r
505 fp = os.fdopen(fp, "w")
\r
506 self.save_to_file(filename, save_parents=True, filep=fp)
\r
509 def save_to_file(self, filename, save_parents=True, filep=None):
\r
515 f = open(filename, "w")
\r
519 def save_to_string(self, save_parents=True):
\r
524 def get_refid(self):
\r
526 self.refid = 'ref0'
\r
529 def set_refid(self, rid):
\r
533 # Figure out what refids exist, and update this credential's id
\r
534 # so that it doesn't clobber the others. Returns the refids of
\r
537 def updateRefID(self):
\r
538 if not self.parent:
\r
539 self.set_refid('ref0')
\r
544 next_cred = self.parent
\r
546 refs.append(next_cred.get_refid())
\r
547 if next_cred.parent:
\r
548 next_cred = next_cred.parent
\r
553 # Find a unique refid for this credential
\r
554 rid = self.get_refid()
\r
557 rid = "ref%d" % (val + 1)
\r
559 # Set the new refid
\r
560 self.set_refid(rid)
\r
562 # Return the set of parent credential ref ids
\r
571 # Sign the XML file created by encode()
\r
574 # In general, a signed credential obtained externally should
\r
575 # not be changed else the signature is no longer valid. So, once
\r
576 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
579 if not self.issuer_privkey or not self.issuer_gid:
\r
581 doc = parseString(self.get_xml())
\r
582 sigs = doc.getElementsByTagName("signatures")[0]
\r
584 # Create the signature template to be signed
\r
585 signature = Signature()
\r
586 signature.set_refid(self.get_refid())
\r
587 sdoc = parseString(signature.get_xml())
\r
588 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
589 sigs.appendChild(sig_ele)
\r
591 self.xml = doc.toxml()
\r
594 # Split the issuer GID into multiple certificates if it's a chain
\r
595 chain = GID(filename=self.issuer_gid)
\r
598 gid_files.append(chain.save_to_random_tmp_file(False))
\r
599 if chain.get_parent():
\r
600 chain = chain.get_parent()
\r
605 # Call out to xmlsec1 to sign it
\r
606 ref = 'Sig_%s' % self.get_refid()
\r
607 filename = self.save_to_random_tmp_file()
\r
608 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
\r
609 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
\r
610 os.remove(filename)
\r
612 for gid_file in gid_files:
\r
613 os.remove(gid_file)
\r
617 # This is no longer a legacy credential
\r
621 # Update signatures
\r
626 # Retrieve the attributes of the credential from the XML.
\r
627 # This is automatically called by the various get_* methods of
\r
628 # this class and should not need to be called explicitly.
\r
633 doc = parseString(self.xml)
\r
635 signed_cred = doc.getElementsByTagName("signed-credential")
\r
637 # Is this a signed-cred or just a cred?
\r
638 if len(signed_cred) > 0:
\r
639 cred = signed_cred[0].getElementsByTagName("credential")[0]
\r
640 signatures = signed_cred[0].getElementsByTagName("signatures")
\r
641 if len(signatures) > 0:
\r
642 sigs = signatures[0].getElementsByTagName("Signature")
\r
644 cred = doc.getElementsByTagName("credential")[0]
\r
647 self.set_refid(cred.getAttribute("xml:id"))
\r
648 self.set_expiration(parse(getTextNode(cred, "expires")))
\r
649 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
\r
650 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
\r
653 # Process privileges
\r
654 privs = cred.getElementsByTagName("privileges")[0]
\r
656 for priv in privs.getElementsByTagName("privilege"):
\r
657 kind = getTextNode(priv, "name")
\r
658 deleg = str2bool(getTextNode(priv, "can_delegate"))
\r
660 # Convert * into the default privileges for the credential's type
\r
661 # Each inherits the delegatability from the * above
\r
662 _ , type = urn_to_hrn(self.gidObject.get_urn())
\r
663 rl = rlist.determine_rights(type, self.gidObject.get_urn())
\r
664 for r in rl.rights:
\r
668 rlist.add(Right(kind.strip(), deleg))
\r
669 self.set_privileges(rlist)
\r
672 # Is there a parent?
\r
673 parent = cred.getElementsByTagName("parent")
\r
674 if len(parent) > 0:
\r
675 parent_doc = parent[0].getElementsByTagName("credential")[0]
\r
676 parent_xml = parent_doc.toxml()
\r
677 self.parent = Credential(string=parent_xml)
\r
680 # Assign the signatures to the credentials
\r
682 Sig = Signature(string=sig.toxml())
\r
684 for cur_cred in self.get_credential_list():
\r
685 if cur_cred.get_refid() == Sig.get_refid():
\r
686 cur_cred.set_signature(Sig)
\r
691 # trusted_certs: A list of trusted GID filenames (not GID objects!)
\r
692 # Chaining is not supported within the GIDs by xmlsec1.
\r
694 # trusted_certs_required: Should usually be true. Set False means an
\r
695 # empty list of trusted_certs would still let this method pass.
\r
696 # It just skips xmlsec1 verification et al. Only used by some utils
\r
699 # . All of the signatures are valid and that the issuers trace back
\r
700 # to trusted roots (performed by xmlsec1)
\r
701 # . The XML matches the credential schema
\r
702 # . That the issuer of the credential is the authority in the target's urn
\r
703 # . In the case of a delegated credential, this must be true of the root
\r
704 # . That all of the gids presented in the credential are valid
\r
705 # . The credential is not expired
\r
707 # -- For Delegates (credentials with parents)
\r
708 # . The privileges must be a subset of the parent credentials
\r
709 # . The privileges must have "can_delegate" set for each delegated privilege
\r
710 # . The target gid must be the same between child and parents
\r
711 # . The expiry time on the child must be no later than the parent
\r
712 # . The signer of the child must be the owner of the parent
\r
714 # -- Verify does *NOT*
\r
715 # . ensure that an xmlrpc client's gid matches a credential gid, that
\r
716 # must be done elsewhere
\r
718 # @param trusted_certs: The certificates of trusted CA certificates
\r
719 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
\r
723 # validate against RelaxNG schema
\r
724 if not self.legacy:
\r
725 if schema and os.path.exists(schema):
\r
726 tree = etree.parse(StringIO(self.xml))
\r
727 schema_doc = etree.parse(schema)
\r
728 xmlschema = etree.XMLSchema(schema_doc)
\r
729 if not xmlschema.validate(tree):
\r
730 error = xmlschema.error_log.last_error
\r
731 message = "%s (line %s)" % (error.message, error.line)
\r
732 raise CredentialNotVerifiable(message)
\r
734 if trusted_certs_required and trusted_certs is None:
\r
737 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
\r
738 trusted_cert_objects = []
\r
739 ok_trusted_certs = []
\r
740 # If caller explicitly passed in None that means skip cert chain validation.
\r
741 # Strange and not typical
\r
742 if trusted_certs is not None:
\r
743 for f in trusted_certs:
\r
745 # Failures here include unreadable files
\r
747 trusted_cert_objects.append(GID(filename=f))
\r
748 ok_trusted_certs.append(f)
\r
749 except Exception, exc:
\r
750 logger.error("Failed to load trusted cert from %s: %r", f, exc)
\r
751 trusted_certs = ok_trusted_certs
\r
753 # Use legacy verification if this is a legacy credential
\r
755 self.legacy.verify_chain(trusted_cert_objects)
\r
756 if self.legacy.client_gid:
\r
757 self.legacy.client_gid.verify_chain(trusted_cert_objects)
\r
758 if self.legacy.object_gid:
\r
759 self.legacy.object_gid.verify_chain(trusted_cert_objects)
\r
762 # make sure it is not expired
\r
763 if self.get_expiration() < datetime.datetime.utcnow():
\r
764 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
\r
766 # Verify the signatures
\r
767 filename = self.save_to_random_tmp_file()
\r
768 if trusted_certs is not None:
\r
769 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
\r
771 # If caller explicitly passed in None that means skip cert chain validation.
\r
772 # Strange and not typical
\r
773 if trusted_certs is not None:
\r
774 # Verify the gids of this cred and of its parents
\r
775 for cur_cred in self.get_credential_list():
\r
776 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
\r
777 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
\r
780 refs.append("Sig_%s" % self.get_refid())
\r
782 parentRefs = self.updateRefID()
\r
783 for ref in parentRefs:
\r
784 refs.append("Sig_%s" % ref)
\r
787 # If caller explicitly passed in None that means skip xmlsec1 validation.
\r
788 # Strange and not typical
\r
789 if trusted_certs is None:
\r
792 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
\r
793 # (self.xmlsec_path, ref, cert_args, filename)
\r
794 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
\r
795 % (self.xmlsec_path, ref, cert_args, filename)).read()
\r
796 if not verified.strip().startswith("OK"):
\r
797 # xmlsec errors have a msg= which is the interesting bit.
\r
798 mstart = verified.find("msg=")
\r
800 if mstart > -1 and len(verified) > 4:
\r
801 mstart = mstart + 4
\r
802 mend = verified.find('\\', mstart)
\r
803 msg = verified[mstart:mend]
\r
804 raise CredentialNotVerifiable("xmlsec1 error verifying cred using Signature ID %s: %s %s" % (ref, msg, verified.strip()))
\r
805 os.remove(filename)
\r
807 # Verify the parents (delegation)
\r
809 self.verify_parent(self.parent)
\r
811 # Make sure the issuer is the target's authority
\r
812 self.verify_issuer()
\r
816 # Creates a list of the credential and its parents, with the root
\r
817 # (original delegated credential) as the last item in the list
\r
818 def get_credential_list(self):
\r
822 list.append(cur_cred)
\r
823 if cur_cred.parent:
\r
824 cur_cred = cur_cred.parent
\r
830 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
\r
831 # the original credential or an authority over that namespace.
\r
832 def verify_issuer(self):
\r
833 root_cred = self.get_credential_list()[-1]
\r
834 root_target_gid = root_cred.get_gid_object()
\r
835 root_cred_signer = root_cred.get_signature().get_issuer_gid()
\r
837 if root_target_gid.is_signed_by_cert(root_cred_signer):
\r
838 # cred signer matches target signer, return success
\r
841 root_target_gid_str = root_target_gid.save_to_string()
\r
842 root_cred_signer_str = root_cred_signer.save_to_string()
\r
843 if root_target_gid_str == root_cred_signer_str:
\r
844 # cred signer is target, return success
\r
847 # See if it the signer is an authority over the domain of the target
\r
848 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
\r
849 root_cred_signer_type = root_cred_signer.get_type()
\r
850 if (root_cred_signer_type == 'authority'):
\r
851 #sfa_logger.debug('Cred signer is an authority')
\r
852 # signer is an authority, see if target is in authority's domain
\r
853 hrn = root_cred_signer.get_hrn()
\r
854 if root_target_gid.get_hrn().startswith(hrn):
\r
857 # We've required that the credential be signed by an authority
\r
858 # for that domain. Reasonable and probably correct.
\r
859 # A looser model would also allow the signer to be an authority
\r
860 # in my control framework - eg My CA or CH. Even if it is not
\r
861 # the CH that issued these, eg, user credentials.
\r
863 # Give up, credential does not pass issuer verification
\r
865 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
869 # -- For Delegates (credentials with parents) verify that:
\r
870 # . The privileges must be a subset of the parent credentials
\r
871 # . The privileges must have "can_delegate" set for each delegated privilege
\r
872 # . The target gid must be the same between child and parents
\r
873 # . The expiry time on the child must be no later than the parent
\r
874 # . The signer of the child must be the owner of the parent
\r
875 def verify_parent(self, parent_cred):
\r
876 # make sure the rights given to the child are a subset of the
\r
877 # parents rights (and check delegate bits)
\r
878 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
\r
879 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % self.parent.get_refid()) +
\r
880 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred ref %s rights " % self.get_refid()) +
\r
881 self.get_privileges().save_to_string())
\r
883 # make sure my target gid is the same as the parent's
\r
884 if not parent_cred.get_gid_object().save_to_string() == \
\r
885 self.get_gid_object().save_to_string():
\r
886 raise CredentialNotVerifiable("Target gid not equal between parent and child")
\r
888 # make sure my expiry time is <= my parent's
\r
889 if not parent_cred.get_expiration() >= self.get_expiration():
\r
890 raise CredentialNotVerifiable("Delegated credential expires after parent")
\r
892 # make sure my signer is the parent's caller
\r
893 if not parent_cred.get_gid_caller().save_to_string(False) == \
\r
894 self.get_signature().get_issuer_gid().save_to_string(False):
\r
895 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
\r
898 if parent_cred.parent:
\r
899 parent_cred.verify_parent(parent_cred.parent)
\r
902 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
\r
904 Return a delegated copy of this credential, delegated to the
\r
905 specified gid's user.
\r
907 # get the gid of the object we are delegating
\r
908 object_gid = self.get_gid_object()
\r
909 object_hrn = object_gid.get_hrn()
\r
911 # the hrn of the user who will be delegated to
\r
912 delegee_gid = GID(filename=delegee_gidfile)
\r
913 delegee_hrn = delegee_gid.get_hrn()
\r
915 #user_key = Keypair(filename=keyfile)
\r
916 #user_hrn = self.get_gid_caller().get_hrn()
\r
917 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
\r
918 dcred = Credential(subject=subject_string)
\r
919 dcred.set_gid_caller(delegee_gid)
\r
920 dcred.set_gid_object(object_gid)
\r
921 dcred.set_parent(self)
\r
922 dcred.set_expiration(self.get_expiration())
\r
923 dcred.set_privileges(self.get_privileges())
\r
924 dcred.get_privileges().delegate_all_privileges(True)
\r
925 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
\r
926 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
\r
933 def get_filename(self):
\r
934 return getattr(self,'filename',None)
\r
937 # Dump the contents of a credential to stdout in human-readable format
\r
939 # @param dump_parents If true, also dump the parent certificates
\r
940 def dump (self, *args, **kwargs):
\r
941 print self.dump_string(*args, **kwargs)
\r
944 def dump_string(self, dump_parents=False):
\r
946 result += "CREDENTIAL %s\n" % self.get_subject()
\r
947 filename=self.get_filename()
\r
948 if filename: result += "Filename %s\n"%filename
\r
949 result += " privs: %s\n" % self.get_privileges().save_to_string()
\r
950 gidCaller = self.get_gid_caller()
\r
952 result += " gidCaller:\n"
\r
953 result += gidCaller.dump_string(8, dump_parents)
\r
955 if self.get_signature():
\r
956 print " gidIssuer:"
\r
957 self.get_signature().get_issuer_gid().dump(8, dump_parents)
\r
959 gidObject = self.get_gid_object()
\r
961 result += " gidObject:\n"
\r
962 result += gidObject.dump_string(8, dump_parents)
\r
964 if self.parent and dump_parents:
\r
965 result += "\nPARENT"
\r
966 result += self.parent.dump(True)
\r