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
30 from types import StringTypes
\r
32 from StringIO import StringIO
\r
33 from tempfile import mkstemp
\r
34 from xml.dom.minidom import Document, parseString
\r
38 from lxml import etree
\r
43 from sfa.util.faults import *
\r
44 from sfa.util.sfalogging import logger
\r
45 from sfa.util.sfatime import utcparse
\r
46 from sfa.trust.certificate import Keypair
\r
47 from sfa.trust.credential_legacy import CredentialLegacy
\r
48 from sfa.trust.rights import Right, Rights, determine_rights
\r
49 from sfa.trust.gid import GID
\r
50 from sfa.util.xrn import urn_to_hrn
\r
52 # 2 weeks, in seconds
\r
53 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
\r
57 # . make privs match between PG and PL
\r
58 # . Need to add support for other types of credentials, e.g. tickets
\r
59 # . add namespaces to signed-credential element?
\r
61 signature_template = \
\r
63 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
\r
65 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
66 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
67 <Reference URI="#%s">
\r
69 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
71 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
72 <DigestValue></DigestValue>
\r
87 # PG formats the template (whitespace) slightly differently.
\r
88 # Note that they don't include the xmlns in the template, but add it later.
\r
89 # Otherwise the two are equivalent.
\r
90 #signature_template_as_in_pg = \
\r
92 #<Signature xml:id="Sig_%s" >
\r
94 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
95 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
96 # <Reference URI="#%s">
\r
98 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
100 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
101 # <DigestValue></DigestValue>
\r
104 # <SignatureValue />
\r
107 # <X509SubjectName/>
\r
108 # <X509IssuerSerial/>
\r
109 # <X509Certificate/>
\r
117 # Convert a string into a bool
\r
118 # used to convert an xsd:boolean to a Python boolean
\r
120 if str.lower() in ['true','1']:
\r
126 # Utility function to get the text of an XML element
\r
128 def getTextNode(element, subele):
\r
129 sub = element.getElementsByTagName(subele)[0]
\r
130 if len(sub.childNodes) > 0:
\r
131 return sub.childNodes[0].nodeValue
\r
136 # Utility function to set the text of an XML element
\r
137 # It creates the element, adds the text to it,
\r
138 # and then appends it to the parent.
\r
140 def append_sub(doc, parent, element, text):
\r
141 ele = doc.createElement(element)
\r
142 ele.appendChild(doc.createTextNode(text))
\r
143 parent.appendChild(ele)
\r
146 # Signature contains information about an xmlsec1 signature
\r
147 # for a signed-credential
\r
150 class Signature(object):
\r
152 def __init__(self, string=None):
\r
154 self.issuer_gid = None
\r
161 def get_refid(self):
\r
171 def set_refid(self, id):
\r
174 def get_issuer_gid(self):
\r
179 def set_issuer_gid(self, gid):
\r
183 doc = parseString(self.xml)
\r
184 sig = doc.getElementsByTagName("Signature")[0]
\r
185 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
\r
186 keyinfo = sig.getElementsByTagName("X509Data")[0]
\r
187 szgid = getTextNode(keyinfo, "X509Certificate")
\r
188 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
\r
189 self.set_issuer_gid(GID(string=szgid))
\r
192 self.xml = signature_template % (self.get_refid(), self.get_refid())
\r
196 # A credential provides a caller gid with privileges to an object gid.
\r
197 # A signed credential is signed by the object's authority.
\r
199 # Credentials are encoded in one of two ways. The legacy style places
\r
200 # it in the subjectAltName of an X509 certificate. The new credentials
\r
201 # are placed in signed XML.
\r
204 # In general, a signed credential obtained externally should
\r
205 # not be changed else the signature is no longer valid. So, once
\r
206 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
208 def filter_creds_by_caller(creds, caller_hrn):
\r
210 Returns a list of creds who's gid caller matches the
\r
211 specified caller hrn
\r
213 if not isinstance(creds, list): creds = [creds]
\r
217 tmp_cred = Credential(string=cred)
\r
218 if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
\r
219 caller_creds.append(cred)
\r
221 return caller_creds
\r
223 class Credential(object):
\r
226 # Create a Credential object
\r
228 # @param create If true, create a blank x509 certificate
\r
229 # @param subject If subject!=None, create an x509 cert with the subject name
\r
230 # @param string If string!=None, load the credential from the string
\r
231 # @param filename If filename!=None, load the credential from the file
\r
232 # FIXME: create and subject are ignored!
\r
233 def __init__(self, create=False, subject=None, string=None, filename=None):
\r
234 self.gidCaller = None
\r
235 self.gidObject = None
\r
236 self.expiration = None
\r
237 self.privileges = None
\r
238 self.issuer_privkey = None
\r
239 self.issuer_gid = None
\r
240 self.issuer_pubkey = None
\r
242 self.signature = None
\r
247 # Check if this is a legacy credential, translate it if so
\r
248 if string or filename:
\r
252 str = file(filename).read()
\r
254 if str.strip().startswith("-----"):
\r
255 self.legacy = CredentialLegacy(False,string=str)
\r
256 self.translate_legacy(str)
\r
261 # Find an xmlsec1 path
\r
262 self.xmlsec_path = ''
\r
263 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
\r
265 if os.path.isfile(path + '/' + 'xmlsec1'):
\r
266 self.xmlsec_path = path + '/' + 'xmlsec1'
\r
269 def get_subject(self):
\r
270 if not self.gidObject:
\r
272 return self.gidObject.get_subject()
\r
274 def get_signature(self):
\r
275 if not self.signature:
\r
277 return self.signature
\r
279 def set_signature(self, sig):
\r
280 self.signature = sig
\r
284 # Translate a legacy credential into a new one
\r
286 # @param String of the legacy credential
\r
288 def translate_legacy(self, str):
\r
289 legacy = CredentialLegacy(False,string=str)
\r
290 self.gidCaller = legacy.get_gid_caller()
\r
291 self.gidObject = legacy.get_gid_object()
\r
292 lifetime = legacy.get_lifetime()
\r
294 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
296 self.set_expiration(int(lifetime))
\r
297 self.lifeTime = legacy.get_lifetime()
\r
298 self.set_privileges(legacy.get_privileges())
\r
299 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
\r
302 # Need the issuer's private key and name
\r
303 # @param key Keypair object containing the private key of the issuer
\r
304 # @param gid GID of the issuing authority
\r
306 def set_issuer_keys(self, privkey, gid):
\r
307 self.issuer_privkey = privkey
\r
308 self.issuer_gid = gid
\r
312 # Set this credential's parent
\r
313 def set_parent(self, cred):
\r
318 # set the GID of the caller
\r
320 # @param gid GID object of the caller
\r
322 def set_gid_caller(self, gid):
\r
323 self.gidCaller = gid
\r
324 # gid origin caller is the caller's gid by default
\r
325 self.gidOriginCaller = gid
\r
328 # get the GID of the object
\r
330 def get_gid_caller(self):
\r
331 if not self.gidCaller:
\r
333 return self.gidCaller
\r
336 # set the GID of the object
\r
338 # @param gid GID object of the object
\r
340 def set_gid_object(self, gid):
\r
341 self.gidObject = gid
\r
344 # get the GID of the object
\r
346 def get_gid_object(self):
\r
347 if not self.gidObject:
\r
349 return self.gidObject
\r
354 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
\r
356 def set_expiration(self, expiration):
\r
357 if isinstance(expiration, (int, float)):
\r
358 self.expiration = datetime.datetime.fromtimestamp(expiration)
\r
359 elif isinstance (expiration, datetime.datetime):
\r
360 self.expiration = expiration
\r
361 elif isinstance (expiration, StringTypes):
\r
362 self.expiration = utcparse (expiration)
\r
364 logger.error ("unexpected input type in Credential.set_expiration")
\r
368 # get the lifetime of the credential (always in datetime format)
\r
370 def get_expiration(self):
\r
371 if not self.expiration:
\r
373 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
\r
374 return self.expiration
\r
378 def get_lifetime(self):
\r
379 return self.get_expiration()
\r
382 # set the privileges
\r
384 # @param privs either a comma-separated list of privileges of a Rights object
\r
386 def set_privileges(self, privs):
\r
387 if isinstance(privs, str):
\r
388 self.privileges = Rights(string = privs)
\r
390 self.privileges = privs
\r
394 # return the privileges as a Rights object
\r
396 def get_privileges(self):
\r
397 if not self.privileges:
\r
399 return self.privileges
\r
402 # determine whether the credential allows a particular operation to be
\r
405 # @param op_name string specifying name of operation ("lookup", "update", etc)
\r
407 def can_perform(self, op_name):
\r
408 rights = self.get_privileges()
\r
413 return rights.can_perform(op_name)
\r
417 # Encode the attributes of the credential into an XML string
\r
418 # This should be done immediately before signing the credential.
\r
420 # In general, a signed credential obtained externally should
\r
421 # not be changed else the signature is no longer valid. So, once
\r
422 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
425 # Create the XML document
\r
427 signed_cred = doc.createElement("signed-credential")
\r
429 # Declare namespaces
\r
430 # Note that credential/policy.xsd are really the PG schemas
\r
431 # in a PL namespace.
\r
432 # Note that delegation of credentials between the 2 only really works
\r
433 # cause those schemas are identical.
\r
434 # Also note these PG schemas talk about PG tickets and CM policies.
\r
435 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
436 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
\r
437 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")
\r
439 # PG says for those last 2:
\r
440 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
441 # 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
443 doc.appendChild(signed_cred)
\r
445 # Fill in the <credential> bit
\r
446 cred = doc.createElement("credential")
\r
447 cred.setAttribute("xml:id", self.get_refid())
\r
448 signed_cred.appendChild(cred)
\r
449 append_sub(doc, cred, "type", "privilege")
\r
450 append_sub(doc, cred, "serial", "8")
\r
451 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
\r
452 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
\r
453 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
\r
454 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
\r
455 append_sub(doc, cred, "uuid", "")
\r
456 if not self.expiration:
\r
457 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
458 self.expiration = self.expiration.replace(microsecond=0)
\r
459 append_sub(doc, cred, "expires", self.expiration.isoformat())
\r
460 privileges = doc.createElement("privileges")
\r
461 cred.appendChild(privileges)
\r
463 if self.privileges:
\r
464 rights = self.get_privileges()
\r
465 for right in rights.rights:
\r
466 priv = doc.createElement("privilege")
\r
467 append_sub(doc, priv, "name", right.kind)
\r
468 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
\r
469 privileges.appendChild(priv)
\r
471 # Add the parent credential if it exists
\r
473 sdoc = parseString(self.parent.get_xml())
\r
474 # If the root node is a signed-credential (it should be), then
\r
475 # get all its attributes and attach those to our signed_cred
\r
477 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
\r
478 # and we need to include those again here or else their signature
\r
479 # no longer matches on the credential.
\r
480 # We expect three of these, but here we copy them all:
\r
481 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
482 # and from PG (PL is equivalent, as shown above):
\r
483 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
484 # 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
487 # PL now also declares these, with different URLs, so
\r
488 # the code notices those attributes already existed with
\r
489 # different values, and complains.
\r
490 # This happens regularly on delegation now that PG and
\r
491 # PL both declare the namespace with different URLs.
\r
492 # If the content ever differs this is a problem,
\r
493 # but for now it works - different URLs (values in the attributes)
\r
494 # but the same actual schema, so using the PG schema
\r
495 # on delegated-to-PL credentials works fine.
\r
497 # Note: you could also not copy attributes
\r
498 # which already exist. It appears that both PG and PL
\r
499 # will actually validate a slicecred with a parent
\r
500 # signed using PG namespaces and a child signed with PL
\r
501 # namespaces over the whole thing. But I don't know
\r
502 # if that is a bug in xmlsec1, an accident since
\r
503 # the contents of the schemas are the same,
\r
504 # or something else, but it seems odd. And this works.
\r
505 parentRoot = sdoc.documentElement
\r
506 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
\r
507 for attrIx in range(0, parentRoot.attributes.length):
\r
508 attr = parentRoot.attributes.item(attrIx)
\r
509 # returns the old attribute of same name that was
\r
510 # on the credential
\r
511 # Below throws InUse exception if we forgot to clone the attribute first
\r
512 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
\r
513 if oldAttr and oldAttr.value != attr.value:
\r
514 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
516 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
\r
518 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
\r
519 p = doc.createElement("parent")
\r
520 p.appendChild(p_cred)
\r
521 cred.appendChild(p)
\r
522 # done handling parent credential
\r
524 # Create the <signatures> tag
\r
525 signatures = doc.createElement("signatures")
\r
526 signed_cred.appendChild(signatures)
\r
528 # Add any parent signatures
\r
530 for cur_cred in self.get_credential_list()[1:]:
\r
531 sdoc = parseString(cur_cred.get_signature().get_xml())
\r
532 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
533 signatures.appendChild(ele)
\r
535 # Get the finished product
\r
536 self.xml = doc.toxml()
\r
539 def save_to_random_tmp_file(self):
\r
540 fp, filename = mkstemp(suffix='cred', text=True)
\r
541 fp = os.fdopen(fp, "w")
\r
542 self.save_to_file(filename, save_parents=True, filep=fp)
\r
545 def save_to_file(self, filename, save_parents=True, filep=None):
\r
551 f = open(filename, "w")
\r
555 def save_to_string(self, save_parents=True):
\r
560 def get_refid(self):
\r
562 self.refid = 'ref0'
\r
565 def set_refid(self, rid):
\r
569 # Figure out what refids exist, and update this credential's id
\r
570 # so that it doesn't clobber the others. Returns the refids of
\r
573 def updateRefID(self):
\r
574 if not self.parent:
\r
575 self.set_refid('ref0')
\r
580 next_cred = self.parent
\r
582 refs.append(next_cred.get_refid())
\r
583 if next_cred.parent:
\r
584 next_cred = next_cred.parent
\r
589 # Find a unique refid for this credential
\r
590 rid = self.get_refid()
\r
593 rid = "ref%d" % (val + 1)
\r
595 # Set the new refid
\r
596 self.set_refid(rid)
\r
598 # Return the set of parent credential ref ids
\r
607 # Sign the XML file created by encode()
\r
610 # In general, a signed credential obtained externally should
\r
611 # not be changed else the signature is no longer valid. So, once
\r
612 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
615 if not self.issuer_privkey or not self.issuer_gid:
\r
617 doc = parseString(self.get_xml())
\r
618 sigs = doc.getElementsByTagName("signatures")[0]
\r
620 # Create the signature template to be signed
\r
621 signature = Signature()
\r
622 signature.set_refid(self.get_refid())
\r
623 sdoc = parseString(signature.get_xml())
\r
624 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
625 sigs.appendChild(sig_ele)
\r
627 self.xml = doc.toxml()
\r
630 # Split the issuer GID into multiple certificates if it's a chain
\r
631 chain = GID(filename=self.issuer_gid)
\r
634 gid_files.append(chain.save_to_random_tmp_file(False))
\r
635 if chain.get_parent():
\r
636 chain = chain.get_parent()
\r
641 # Call out to xmlsec1 to sign it
\r
642 ref = 'Sig_%s' % self.get_refid()
\r
643 filename = self.save_to_random_tmp_file()
\r
644 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
\r
645 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
\r
646 os.remove(filename)
\r
648 for gid_file in gid_files:
\r
649 os.remove(gid_file)
\r
653 # This is no longer a legacy credential
\r
657 # Update signatures
\r
662 # Retrieve the attributes of the credential from the XML.
\r
663 # This is automatically called by the various get_* methods of
\r
664 # this class and should not need to be called explicitly.
\r
669 doc = parseString(self.xml)
\r
671 signed_cred = doc.getElementsByTagName("signed-credential")
\r
673 # Is this a signed-cred or just a cred?
\r
674 if len(signed_cred) > 0:
\r
675 cred = signed_cred[0].getElementsByTagName("credential")[0]
\r
676 signatures = signed_cred[0].getElementsByTagName("signatures")
\r
677 if len(signatures) > 0:
\r
678 sigs = signatures[0].getElementsByTagName("Signature")
\r
680 cred = doc.getElementsByTagName("credential")[0]
\r
683 self.set_refid(cred.getAttribute("xml:id"))
\r
684 self.set_expiration(utcparse(getTextNode(cred, "expires")))
\r
685 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
\r
686 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
\r
689 # Process privileges
\r
690 privs = cred.getElementsByTagName("privileges")[0]
\r
692 for priv in privs.getElementsByTagName("privilege"):
\r
693 kind = getTextNode(priv, "name")
\r
694 deleg = str2bool(getTextNode(priv, "can_delegate"))
\r
696 # Convert * into the default privileges for the credential's type
\r
697 # Each inherits the delegatability from the * above
\r
698 _ , type = urn_to_hrn(self.gidObject.get_urn())
\r
699 rl = determine_rights(type, self.gidObject.get_urn())
\r
700 for r in rl.rights:
\r
704 rlist.add(Right(kind.strip(), deleg))
\r
705 self.set_privileges(rlist)
\r
708 # Is there a parent?
\r
709 parent = cred.getElementsByTagName("parent")
\r
710 if len(parent) > 0:
\r
711 parent_doc = parent[0].getElementsByTagName("credential")[0]
\r
712 parent_xml = parent_doc.toxml()
\r
713 self.parent = Credential(string=parent_xml)
\r
716 # Assign the signatures to the credentials
\r
718 Sig = Signature(string=sig.toxml())
\r
720 for cur_cred in self.get_credential_list():
\r
721 if cur_cred.get_refid() == Sig.get_refid():
\r
722 cur_cred.set_signature(Sig)
\r
727 # trusted_certs: A list of trusted GID filenames (not GID objects!)
\r
728 # Chaining is not supported within the GIDs by xmlsec1.
\r
730 # trusted_certs_required: Should usually be true. Set False means an
\r
731 # empty list of trusted_certs would still let this method pass.
\r
732 # It just skips xmlsec1 verification et al. Only used by some utils
\r
735 # . All of the signatures are valid and that the issuers trace back
\r
736 # to trusted roots (performed by xmlsec1)
\r
737 # . The XML matches the credential schema
\r
738 # . That the issuer of the credential is the authority in the target's urn
\r
739 # . In the case of a delegated credential, this must be true of the root
\r
740 # . That all of the gids presented in the credential are valid
\r
741 # . The credential is not expired
\r
743 # -- For Delegates (credentials with parents)
\r
744 # . The privileges must be a subset of the parent credentials
\r
745 # . The privileges must have "can_delegate" set for each delegated privilege
\r
746 # . The target gid must be the same between child and parents
\r
747 # . The expiry time on the child must be no later than the parent
\r
748 # . The signer of the child must be the owner of the parent
\r
750 # -- Verify does *NOT*
\r
751 # . ensure that an xmlrpc client's gid matches a credential gid, that
\r
752 # must be done elsewhere
\r
754 # @param trusted_certs: The certificates of trusted CA certificates
\r
755 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
\r
759 # validate against RelaxNG schema
\r
760 if HAVELXML and not self.legacy:
\r
761 if schema and os.path.exists(schema):
\r
762 tree = etree.parse(StringIO(self.xml))
\r
763 schema_doc = etree.parse(schema)
\r
764 xmlschema = etree.XMLSchema(schema_doc)
\r
765 if not xmlschema.validate(tree):
\r
766 error = xmlschema.error_log.last_error
\r
767 message = "%s (line %s)" % (error.message, error.line)
\r
768 raise CredentialNotVerifiable(message)
\r
770 if trusted_certs_required and trusted_certs is None:
\r
773 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
\r
774 trusted_cert_objects = []
\r
775 ok_trusted_certs = []
\r
776 # If caller explicitly passed in None that means skip cert chain validation.
\r
777 # Strange and not typical
\r
778 if trusted_certs is not None:
\r
779 for f in trusted_certs:
\r
781 # Failures here include unreadable files
\r
783 trusted_cert_objects.append(GID(filename=f))
\r
784 ok_trusted_certs.append(f)
\r
785 except Exception, exc:
\r
786 logger.error("Failed to load trusted cert from %s: %r", f, exc)
\r
787 trusted_certs = ok_trusted_certs
\r
789 # Use legacy verification if this is a legacy credential
\r
791 self.legacy.verify_chain(trusted_cert_objects)
\r
792 if self.legacy.client_gid:
\r
793 self.legacy.client_gid.verify_chain(trusted_cert_objects)
\r
794 if self.legacy.object_gid:
\r
795 self.legacy.object_gid.verify_chain(trusted_cert_objects)
\r
798 # make sure it is not expired
\r
799 if self.get_expiration() < datetime.datetime.utcnow():
\r
800 raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
\r
802 # Verify the signatures
\r
803 filename = self.save_to_random_tmp_file()
\r
804 if trusted_certs is not None:
\r
805 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
\r
807 # If caller explicitly passed in None that means skip cert chain validation.
\r
808 # Strange and not typical
\r
809 if trusted_certs is not None:
\r
810 # Verify the gids of this cred and of its parents
\r
811 for cur_cred in self.get_credential_list():
\r
812 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
\r
813 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
\r
816 refs.append("Sig_%s" % self.get_refid())
\r
818 parentRefs = self.updateRefID()
\r
819 for ref in parentRefs:
\r
820 refs.append("Sig_%s" % ref)
\r
823 # If caller explicitly passed in None that means skip xmlsec1 validation.
\r
824 # Strange and not typical
\r
825 if trusted_certs is None:
\r
828 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
\r
829 # (self.xmlsec_path, ref, cert_args, filename)
\r
830 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
\r
831 % (self.xmlsec_path, ref, cert_args, filename)).read()
\r
832 if not verified.strip().startswith("OK"):
\r
833 # xmlsec errors have a msg= which is the interesting bit.
\r
834 mstart = verified.find("msg=")
\r
836 if mstart > -1 and len(verified) > 4:
\r
837 mstart = mstart + 4
\r
838 mend = verified.find('\\', mstart)
\r
839 msg = verified[mstart:mend]
\r
840 raise CredentialNotVerifiable("xmlsec1 error verifying cred using Signature ID %s: %s %s" % (ref, msg, verified.strip()))
\r
841 os.remove(filename)
\r
843 # Verify the parents (delegation)
\r
845 self.verify_parent(self.parent)
\r
847 # Make sure the issuer is the target's authority
\r
848 self.verify_issuer()
\r
852 # Creates a list of the credential and its parents, with the root
\r
853 # (original delegated credential) as the last item in the list
\r
854 def get_credential_list(self):
\r
858 list.append(cur_cred)
\r
859 if cur_cred.parent:
\r
860 cur_cred = cur_cred.parent
\r
866 # Make sure the credential's target gid was signed by (or is the same) the entity that signed
\r
867 # the original credential or an authority over that namespace.
\r
868 def verify_issuer(self):
\r
869 root_cred = self.get_credential_list()[-1]
\r
870 root_target_gid = root_cred.get_gid_object()
\r
871 root_cred_signer = root_cred.get_signature().get_issuer_gid()
\r
873 if root_target_gid.is_signed_by_cert(root_cred_signer):
\r
874 # cred signer matches target signer, return success
\r
877 root_target_gid_str = root_target_gid.save_to_string()
\r
878 root_cred_signer_str = root_cred_signer.save_to_string()
\r
879 if root_target_gid_str == root_cred_signer_str:
\r
880 # cred signer is target, return success
\r
883 # root_cred_signer is not the target_gid
\r
884 # So this is a different gid that we have not verified
\r
885 # Did xmlsec1 verify the cert chain on this already?
\r
886 # Regardless, it hasn't verified that the gid meets the HRN namespace
\r
888 # FIXME: Uncomment once we verify this is right
\r
889 # root_cred_signer.verify_chain(trusted_cert_objects)
\r
891 # See if it the signer is an authority over the domain of the target
\r
892 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
\r
893 root_cred_signer_type = root_cred_signer.get_type()
\r
894 if (root_cred_signer_type == 'authority'):
\r
895 #logger.debug('Cred signer is an authority')
\r
896 # signer is an authority, see if target is in authority's domain
\r
897 hrn = root_cred_signer.get_hrn()
\r
898 if root_target_gid.get_hrn().startswith(hrn):
\r
901 # We've required that the credential be signed by an authority
\r
902 # for that domain. Reasonable and probably correct.
\r
903 # A looser model would also allow the signer to be an authority
\r
904 # in my control framework - eg My CA or CH. Even if it is not
\r
905 # the CH that issued these, eg, user credentials.
\r
907 # Give up, credential does not pass issuer verification
\r
909 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
913 # -- For Delegates (credentials with parents) verify that:
\r
914 # . The privileges must be a subset of the parent credentials
\r
915 # . The privileges must have "can_delegate" set for each delegated privilege
\r
916 # . The target gid must be the same between child and parents
\r
917 # . The expiry time on the child must be no later than the parent
\r
918 # . The signer of the child must be the owner of the parent
\r
919 def verify_parent(self, parent_cred):
\r
920 # make sure the rights given to the child are a subset of the
\r
921 # parents rights (and check delegate bits)
\r
922 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
\r
923 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % self.parent.get_refid()) +
\r
924 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred ref %s rights " % self.get_refid()) +
\r
925 self.get_privileges().save_to_string())
\r
927 # make sure my target gid is the same as the parent's
\r
928 if not parent_cred.get_gid_object().save_to_string() == \
\r
929 self.get_gid_object().save_to_string():
\r
930 raise CredentialNotVerifiable("Target gid not equal between parent and child")
\r
932 # make sure my expiry time is <= my parent's
\r
933 if not parent_cred.get_expiration() >= self.get_expiration():
\r
934 raise CredentialNotVerifiable("Delegated credential expires after parent")
\r
936 # make sure my signer is the parent's caller
\r
937 if not parent_cred.get_gid_caller().save_to_string(False) == \
\r
938 self.get_signature().get_issuer_gid().save_to_string(False):
\r
939 raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
\r
942 if parent_cred.parent:
\r
943 parent_cred.verify_parent(parent_cred.parent)
\r
946 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
\r
948 Return a delegated copy of this credential, delegated to the
\r
949 specified gid's user.
\r
951 # get the gid of the object we are delegating
\r
952 object_gid = self.get_gid_object()
\r
953 object_hrn = object_gid.get_hrn()
\r
955 # the hrn of the user who will be delegated to
\r
956 delegee_gid = GID(filename=delegee_gidfile)
\r
957 delegee_hrn = delegee_gid.get_hrn()
\r
959 #user_key = Keypair(filename=keyfile)
\r
960 #user_hrn = self.get_gid_caller().get_hrn()
\r
961 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
\r
962 dcred = Credential(subject=subject_string)
\r
963 dcred.set_gid_caller(delegee_gid)
\r
964 dcred.set_gid_object(object_gid)
\r
965 dcred.set_parent(self)
\r
966 dcred.set_expiration(self.get_expiration())
\r
967 dcred.set_privileges(self.get_privileges())
\r
968 dcred.get_privileges().delegate_all_privileges(True)
\r
969 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
\r
970 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
\r
977 def get_filename(self):
\r
978 return getattr(self,'filename',None)
\r
981 # Dump the contents of a credential to stdout in human-readable format
\r
983 # @param dump_parents If true, also dump the parent certificates
\r
984 def dump (self, *args, **kwargs):
\r
985 print self.dump_string(*args, **kwargs)
\r
988 def dump_string(self, dump_parents=False):
\r
990 result += "CREDENTIAL %s\n" % self.get_subject()
\r
991 filename=self.get_filename()
\r
992 if filename: result += "Filename %s\n"%filename
\r
993 result += " privs: %s\n" % self.get_privileges().save_to_string()
\r
994 gidCaller = self.get_gid_caller()
\r
996 result += " gidCaller:\n"
\r
997 result += gidCaller.dump_string(8, dump_parents)
\r
999 if self.get_signature():
\r
1000 print " gidIssuer:"
\r
1001 self.get_signature().get_issuer_gid().dump(8, dump_parents)
\r
1003 gidObject = self.get_gid_object()
\r
1005 result += " gidObject:\n"
\r
1006 result += gidObject.dump_string(8, dump_parents)
\r
1008 if self.parent and dump_parents:
\r
1009 result += "\nPARENT"
\r
1010 result += self.parent.dump_string(True)
\r