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 xml.parsers.expat import ExpatError
\r
45 from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent
\r
46 from sfa.util.sfalogging import logger
\r
47 from sfa.util.sfatime import utcparse
\r
48 from sfa.trust.credential_legacy import CredentialLegacy
\r
49 from sfa.trust.rights import Right, Rights, determine_rights
\r
50 from sfa.trust.gid import GID
\r
51 from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn
\r
53 # 2 weeks, in seconds
\r
54 DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31
\r
58 # . make privs match between PG and PL
\r
59 # . Need to add support for other types of credentials, e.g. tickets
\r
60 # . add namespaces to signed-credential element?
\r
62 signature_template = \
\r
64 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
\r
66 <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
67 <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
68 <Reference URI="#%s">
\r
70 <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
72 <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
73 <DigestValue></DigestValue>
\r
88 # PG formats the template (whitespace) slightly differently.
\r
89 # Note that they don't include the xmlns in the template, but add it later.
\r
90 # Otherwise the two are equivalent.
\r
91 #signature_template_as_in_pg = \
\r
93 #<Signature xml:id="Sig_%s" >
\r
95 # <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
\r
96 # <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
\r
97 # <Reference URI="#%s">
\r
99 # <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
\r
101 # <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
\r
102 # <DigestValue></DigestValue>
\r
105 # <SignatureValue />
\r
108 # <X509SubjectName/>
\r
109 # <X509IssuerSerial/>
\r
110 # <X509Certificate/>
\r
118 # Convert a string into a bool
\r
119 # used to convert an xsd:boolean to a Python boolean
\r
121 if str.lower() in ['true','1']:
\r
127 # Utility function to get the text of an XML element
\r
129 def getTextNode(element, subele):
\r
130 sub = element.getElementsByTagName(subele)[0]
\r
131 if len(sub.childNodes) > 0:
\r
132 return sub.childNodes[0].nodeValue
\r
137 # Utility function to set the text of an XML element
\r
138 # It creates the element, adds the text to it,
\r
139 # and then appends it to the parent.
\r
141 def append_sub(doc, parent, element, text):
\r
142 ele = doc.createElement(element)
\r
143 ele.appendChild(doc.createTextNode(text))
\r
144 parent.appendChild(ele)
\r
147 # Signature contains information about an xmlsec1 signature
\r
148 # for a signed-credential
\r
151 class Signature(object):
\r
153 def __init__(self, string=None):
\r
155 self.issuer_gid = None
\r
162 def get_refid(self):
\r
172 def set_refid(self, id):
\r
175 def get_issuer_gid(self):
\r
180 def set_issuer_gid(self, gid):
\r
185 doc = parseString(self.xml)
\r
186 except ExpatError,e:
\r
187 logger.log_exc ("Failed to parse credential, %s"%self.xml)
\r
189 sig = doc.getElementsByTagName("Signature")[0]
\r
190 self.set_refid(sig.getAttribute("xml:id").strip("Sig_"))
\r
191 keyinfo = sig.getElementsByTagName("X509Data")[0]
\r
192 szgid = getTextNode(keyinfo, "X509Certificate")
\r
193 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
\r
194 self.set_issuer_gid(GID(string=szgid))
\r
197 self.xml = signature_template % (self.get_refid(), self.get_refid())
\r
201 # A credential provides a caller gid with privileges to an object gid.
\r
202 # A signed credential is signed by the object's authority.
\r
204 # Credentials are encoded in one of two ways. The legacy style places
\r
205 # it in the subjectAltName of an X509 certificate. The new credentials
\r
206 # are placed in signed XML.
\r
209 # In general, a signed credential obtained externally should
\r
210 # not be changed else the signature is no longer valid. So, once
\r
211 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
213 def filter_creds_by_caller(creds, caller_hrn_list):
\r
215 Returns a list of creds who's gid caller matches the
\r
216 specified caller hrn
\r
218 if not isinstance(creds, list): creds = [creds]
\r
219 if not isinstance(caller_hrn_list, list):
\r
220 caller_hrn_list = [caller_hrn_list]
\r
224 tmp_cred = Credential(string=cred)
\r
225 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
\r
226 caller_creds.append(cred)
\r
228 return caller_creds
\r
230 class Credential(object):
\r
233 # Create a Credential object
\r
235 # @param create If true, create a blank x509 certificate
\r
236 # @param subject If subject!=None, create an x509 cert with the subject name
\r
237 # @param string If string!=None, load the credential from the string
\r
238 # @param filename If filename!=None, load the credential from the file
\r
239 # FIXME: create and subject are ignored!
\r
240 def __init__(self, create=False, subject=None, string=None, filename=None):
\r
241 self.gidCaller = None
\r
242 self.gidObject = None
\r
243 self.expiration = None
\r
244 self.privileges = None
\r
245 self.issuer_privkey = None
\r
246 self.issuer_gid = None
\r
247 self.issuer_pubkey = None
\r
249 self.signature = None
\r
254 # Check if this is a legacy credential, translate it if so
\r
255 if string or filename:
\r
259 str = file(filename).read()
\r
261 if str.strip().startswith("-----"):
\r
262 self.legacy = CredentialLegacy(False,string=str)
\r
263 self.translate_legacy(str)
\r
268 # Find an xmlsec1 path
\r
269 self.xmlsec_path = ''
\r
270 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
\r
272 if os.path.isfile(path + '/' + 'xmlsec1'):
\r
273 self.xmlsec_path = path + '/' + 'xmlsec1'
\r
276 def get_subject(self):
\r
277 if not self.gidObject:
\r
279 return self.gidObject.get_printable_subject()
\r
281 # sounds like this should be __repr__ instead ??
\r
282 def get_summary_tostring(self):
\r
283 if not self.gidObject:
\r
285 obj = self.gidObject.get_printable_subject()
\r
286 caller = self.gidCaller.get_printable_subject()
\r
287 exp = self.get_expiration()
\r
288 # Summarize the rights too? The issuer?
\r
289 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
\r
291 def get_signature(self):
\r
292 if not self.signature:
\r
294 return self.signature
\r
296 def set_signature(self, sig):
\r
297 self.signature = sig
\r
301 # Translate a legacy credential into a new one
\r
303 # @param String of the legacy credential
\r
305 def translate_legacy(self, str):
\r
306 legacy = CredentialLegacy(False,string=str)
\r
307 self.gidCaller = legacy.get_gid_caller()
\r
308 self.gidObject = legacy.get_gid_object()
\r
309 lifetime = legacy.get_lifetime()
\r
311 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
313 self.set_expiration(int(lifetime))
\r
314 self.lifeTime = legacy.get_lifetime()
\r
315 self.set_privileges(legacy.get_privileges())
\r
316 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
\r
319 # Need the issuer's private key and name
\r
320 # @param key Keypair object containing the private key of the issuer
\r
321 # @param gid GID of the issuing authority
\r
323 def set_issuer_keys(self, privkey, gid):
\r
324 self.issuer_privkey = privkey
\r
325 self.issuer_gid = gid
\r
329 # Set this credential's parent
\r
330 def set_parent(self, cred):
\r
335 # set the GID of the caller
\r
337 # @param gid GID object of the caller
\r
339 def set_gid_caller(self, gid):
\r
340 self.gidCaller = gid
\r
341 # gid origin caller is the caller's gid by default
\r
342 self.gidOriginCaller = gid
\r
345 # get the GID of the object
\r
347 def get_gid_caller(self):
\r
348 if not self.gidCaller:
\r
350 return self.gidCaller
\r
353 # set the GID of the object
\r
355 # @param gid GID object of the object
\r
357 def set_gid_object(self, gid):
\r
358 self.gidObject = gid
\r
361 # get the GID of the object
\r
363 def get_gid_object(self):
\r
364 if not self.gidObject:
\r
366 return self.gidObject
\r
369 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
\r
371 def set_expiration(self, expiration):
\r
372 if isinstance(expiration, (int, float)):
\r
373 self.expiration = datetime.datetime.fromtimestamp(expiration)
\r
374 elif isinstance (expiration, datetime.datetime):
\r
375 self.expiration = expiration
\r
376 elif isinstance (expiration, StringTypes):
\r
377 self.expiration = utcparse (expiration)
\r
379 logger.error ("unexpected input type in Credential.set_expiration")
\r
383 # get the lifetime of the credential (always in datetime format)
\r
385 def get_expiration(self):
\r
386 if not self.expiration:
\r
388 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
\r
389 return self.expiration
\r
393 def get_lifetime(self):
\r
394 return self.get_expiration()
\r
397 # set the privileges
\r
399 # @param privs either a comma-separated list of privileges of a Rights object
\r
401 def set_privileges(self, privs):
\r
402 if isinstance(privs, str):
\r
403 self.privileges = Rights(string = privs)
\r
405 self.privileges = privs
\r
408 # return the privileges as a Rights object
\r
410 def get_privileges(self):
\r
411 if not self.privileges:
\r
413 return self.privileges
\r
416 # determine whether the credential allows a particular operation to be
\r
419 # @param op_name string specifying name of operation ("lookup", "update", etc)
\r
421 def can_perform(self, op_name):
\r
422 rights = self.get_privileges()
\r
427 return rights.can_perform(op_name)
\r
431 # Encode the attributes of the credential into an XML string
\r
432 # This should be done immediately before signing the credential.
\r
434 # In general, a signed credential obtained externally should
\r
435 # not be changed else the signature is no longer valid. So, once
\r
436 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
439 # Create the XML document
\r
441 signed_cred = doc.createElement("signed-credential")
\r
443 # Declare namespaces
\r
444 # Note that credential/policy.xsd are really the PG schemas
\r
445 # in a PL namespace.
\r
446 # Note that delegation of credentials between the 2 only really works
\r
447 # cause those schemas are identical.
\r
448 # Also note these PG schemas talk about PG tickets and CM policies.
\r
449 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
450 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
\r
451 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
453 # PG says for those last 2:
\r
454 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
455 # 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
457 doc.appendChild(signed_cred)
\r
459 # Fill in the <credential> bit
\r
460 cred = doc.createElement("credential")
\r
461 cred.setAttribute("xml:id", self.get_refid())
\r
462 signed_cred.appendChild(cred)
\r
463 append_sub(doc, cred, "type", "privilege")
\r
464 append_sub(doc, cred, "serial", "8")
\r
465 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
\r
466 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
\r
467 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
\r
468 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
\r
469 append_sub(doc, cred, "uuid", "")
\r
470 if not self.expiration:
\r
471 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
472 self.expiration = self.expiration.replace(microsecond=0)
\r
473 append_sub(doc, cred, "expires", self.expiration.isoformat())
\r
474 privileges = doc.createElement("privileges")
\r
475 cred.appendChild(privileges)
\r
477 if self.privileges:
\r
478 rights = self.get_privileges()
\r
479 for right in rights.rights:
\r
480 priv = doc.createElement("privilege")
\r
481 append_sub(doc, priv, "name", right.kind)
\r
482 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
\r
483 privileges.appendChild(priv)
\r
485 # Add the parent credential if it exists
\r
487 sdoc = parseString(self.parent.get_xml())
\r
488 # If the root node is a signed-credential (it should be), then
\r
489 # get all its attributes and attach those to our signed_cred
\r
491 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
\r
492 # and we need to include those again here or else their signature
\r
493 # no longer matches on the credential.
\r
494 # We expect three of these, but here we copy them all:
\r
495 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
496 # and from PG (PL is equivalent, as shown above):
\r
497 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
498 # 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
501 # PL now also declares these, with different URLs, so
\r
502 # the code notices those attributes already existed with
\r
503 # different values, and complains.
\r
504 # This happens regularly on delegation now that PG and
\r
505 # PL both declare the namespace with different URLs.
\r
506 # If the content ever differs this is a problem,
\r
507 # but for now it works - different URLs (values in the attributes)
\r
508 # but the same actual schema, so using the PG schema
\r
509 # on delegated-to-PL credentials works fine.
\r
511 # Note: you could also not copy attributes
\r
512 # which already exist. It appears that both PG and PL
\r
513 # will actually validate a slicecred with a parent
\r
514 # signed using PG namespaces and a child signed with PL
\r
515 # namespaces over the whole thing. But I don't know
\r
516 # if that is a bug in xmlsec1, an accident since
\r
517 # the contents of the schemas are the same,
\r
518 # or something else, but it seems odd. And this works.
\r
519 parentRoot = sdoc.documentElement
\r
520 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
\r
521 for attrIx in range(0, parentRoot.attributes.length):
\r
522 attr = parentRoot.attributes.item(attrIx)
\r
523 # returns the old attribute of same name that was
\r
524 # on the credential
\r
525 # Below throws InUse exception if we forgot to clone the attribute first
\r
526 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
\r
527 if oldAttr and oldAttr.value != attr.value:
\r
528 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
530 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
\r
532 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
\r
533 p = doc.createElement("parent")
\r
534 p.appendChild(p_cred)
\r
535 cred.appendChild(p)
\r
536 # done handling parent credential
\r
538 # Create the <signatures> tag
\r
539 signatures = doc.createElement("signatures")
\r
540 signed_cred.appendChild(signatures)
\r
542 # Add any parent signatures
\r
544 for cur_cred in self.get_credential_list()[1:]:
\r
545 sdoc = parseString(cur_cred.get_signature().get_xml())
\r
546 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
547 signatures.appendChild(ele)
\r
549 # Get the finished product
\r
550 self.xml = doc.toxml()
\r
553 def save_to_random_tmp_file(self):
\r
554 fp, filename = mkstemp(suffix='cred', text=True)
\r
555 fp = os.fdopen(fp, "w")
\r
556 self.save_to_file(filename, save_parents=True, filep=fp)
\r
559 def save_to_file(self, filename, save_parents=True, filep=None):
\r
565 f = open(filename, "w")
\r
569 def save_to_string(self, save_parents=True):
\r
574 def get_refid(self):
\r
576 self.refid = 'ref0'
\r
579 def set_refid(self, rid):
\r
583 # Figure out what refids exist, and update this credential's id
\r
584 # so that it doesn't clobber the others. Returns the refids of
\r
587 def updateRefID(self):
\r
588 if not self.parent:
\r
589 self.set_refid('ref0')
\r
594 next_cred = self.parent
\r
596 refs.append(next_cred.get_refid())
\r
597 if next_cred.parent:
\r
598 next_cred = next_cred.parent
\r
603 # Find a unique refid for this credential
\r
604 rid = self.get_refid()
\r
607 rid = "ref%d" % (val + 1)
\r
609 # Set the new refid
\r
610 self.set_refid(rid)
\r
612 # Return the set of parent credential ref ids
\r
621 # Sign the XML file created by encode()
\r
624 # In general, a signed credential obtained externally should
\r
625 # not be changed else the signature is no longer valid. So, once
\r
626 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
629 if not self.issuer_privkey or not self.issuer_gid:
\r
631 doc = parseString(self.get_xml())
\r
632 sigs = doc.getElementsByTagName("signatures")[0]
\r
634 # Create the signature template to be signed
\r
635 signature = Signature()
\r
636 signature.set_refid(self.get_refid())
\r
637 sdoc = parseString(signature.get_xml())
\r
638 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
639 sigs.appendChild(sig_ele)
\r
641 self.xml = doc.toxml()
\r
644 # Split the issuer GID into multiple certificates if it's a chain
\r
645 chain = GID(filename=self.issuer_gid)
\r
648 gid_files.append(chain.save_to_random_tmp_file(False))
\r
649 if chain.get_parent():
\r
650 chain = chain.get_parent()
\r
655 # Call out to xmlsec1 to sign it
\r
656 ref = 'Sig_%s' % self.get_refid()
\r
657 filename = self.save_to_random_tmp_file()
\r
658 signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
\r
659 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read()
\r
660 os.remove(filename)
\r
662 for gid_file in gid_files:
\r
663 os.remove(gid_file)
\r
667 # This is no longer a legacy credential
\r
671 # Update signatures
\r
676 # Retrieve the attributes of the credential from the XML.
\r
677 # This is automatically called by the various get_* methods of
\r
678 # this class and should not need to be called explicitly.
\r
683 doc = parseString(self.xml)
\r
685 signed_cred = doc.getElementsByTagName("signed-credential")
\r
687 # Is this a signed-cred or just a cred?
\r
688 if len(signed_cred) > 0:
\r
689 creds = signed_cred[0].getElementsByTagName("credential")
\r
690 signatures = signed_cred[0].getElementsByTagName("signatures")
\r
691 if len(signatures) > 0:
\r
692 sigs = signatures[0].getElementsByTagName("Signature")
\r
694 creds = doc.getElementsByTagName("credential")
\r
696 if creds is None or len(creds) == 0:
\r
697 # malformed cred file
\r
698 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
\r
700 # Just take the first cred if there are more than one
\r
703 self.set_refid(cred.getAttribute("xml:id"))
\r
704 self.set_expiration(utcparse(getTextNode(cred, "expires")))
\r
705 self.gidCaller = GID(string=getTextNode(cred, "owner_gid"))
\r
706 self.gidObject = GID(string=getTextNode(cred, "target_gid"))
\r
709 # Process privileges
\r
710 privs = cred.getElementsByTagName("privileges")[0]
\r
712 for priv in privs.getElementsByTagName("privilege"):
\r
713 kind = getTextNode(priv, "name")
\r
714 deleg = str2bool(getTextNode(priv, "can_delegate"))
\r
716 # Convert * into the default privileges for the credential's type
\r
717 # Each inherits the delegatability from the * above
\r
718 _ , type = urn_to_hrn(self.gidObject.get_urn())
\r
719 rl = determine_rights(type, self.gidObject.get_urn())
\r
720 for r in rl.rights:
\r
724 rlist.add(Right(kind.strip(), deleg))
\r
725 self.set_privileges(rlist)
\r
728 # Is there a parent?
\r
729 parent = cred.getElementsByTagName("parent")
\r
730 if len(parent) > 0:
\r
731 parent_doc = parent[0].getElementsByTagName("credential")[0]
\r
732 parent_xml = parent_doc.toxml()
\r
733 self.parent = Credential(string=parent_xml)
\r
736 # Assign the signatures to the credentials
\r
738 Sig = Signature(string=sig.toxml())
\r
740 for cur_cred in self.get_credential_list():
\r
741 if cur_cred.get_refid() == Sig.get_refid():
\r
742 cur_cred.set_signature(Sig)
\r
747 # trusted_certs: A list of trusted GID filenames (not GID objects!)
\r
748 # Chaining is not supported within the GIDs by xmlsec1.
\r
750 # trusted_certs_required: Should usually be true. Set False means an
\r
751 # empty list of trusted_certs would still let this method pass.
\r
752 # It just skips xmlsec1 verification et al. Only used by some utils
\r
755 # . All of the signatures are valid and that the issuers trace back
\r
756 # to trusted roots (performed by xmlsec1)
\r
757 # . The XML matches the credential schema
\r
758 # . That the issuer of the credential is the authority in the target's urn
\r
759 # . In the case of a delegated credential, this must be true of the root
\r
760 # . That all of the gids presented in the credential are valid
\r
761 # . Including verifying GID chains, and includ the issuer
\r
762 # . The credential is not expired
\r
764 # -- For Delegates (credentials with parents)
\r
765 # . The privileges must be a subset of the parent credentials
\r
766 # . The privileges must have "can_delegate" set for each delegated privilege
\r
767 # . The target gid must be the same between child and parents
\r
768 # . The expiry time on the child must be no later than the parent
\r
769 # . The signer of the child must be the owner of the parent
\r
771 # -- Verify does *NOT*
\r
772 # . ensure that an xmlrpc client's gid matches a credential gid, that
\r
773 # must be done elsewhere
\r
775 # @param trusted_certs: The certificates of trusted CA certificates
\r
776 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
\r
780 # validate against RelaxNG schema
\r
781 if HAVELXML and not self.legacy:
\r
782 if schema and os.path.exists(schema):
\r
783 tree = etree.parse(StringIO(self.xml))
\r
784 schema_doc = etree.parse(schema)
\r
785 xmlschema = etree.XMLSchema(schema_doc)
\r
786 if not xmlschema.validate(tree):
\r
787 error = xmlschema.error_log.last_error
\r
788 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
\r
789 raise CredentialNotVerifiable(message)
\r
791 if trusted_certs_required and trusted_certs is None:
\r
794 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
\r
795 trusted_cert_objects = []
\r
796 ok_trusted_certs = []
\r
797 # If caller explicitly passed in None that means skip cert chain validation.
\r
798 # Strange and not typical
\r
799 if trusted_certs is not None:
\r
800 for f in trusted_certs:
\r
802 # Failures here include unreadable files
\r
804 trusted_cert_objects.append(GID(filename=f))
\r
805 ok_trusted_certs.append(f)
\r
806 except Exception, exc:
\r
807 logger.error("Failed to load trusted cert from %s: %r", f, exc)
\r
808 trusted_certs = ok_trusted_certs
\r
810 # Use legacy verification if this is a legacy credential
\r
812 self.legacy.verify_chain(trusted_cert_objects)
\r
813 if self.legacy.client_gid:
\r
814 self.legacy.client_gid.verify_chain(trusted_cert_objects)
\r
815 if self.legacy.object_gid:
\r
816 self.legacy.object_gid.verify_chain(trusted_cert_objects)
\r
819 # make sure it is not expired
\r
820 if self.get_expiration() < datetime.datetime.utcnow():
\r
821 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
\r
823 # Verify the signatures
\r
824 filename = self.save_to_random_tmp_file()
\r
825 if trusted_certs is not None:
\r
826 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
\r
828 # If caller explicitly passed in None that means skip cert chain validation.
\r
829 # - Strange and not typical
\r
830 if trusted_certs is not None:
\r
831 # Verify the gids of this cred and of its parents
\r
832 for cur_cred in self.get_credential_list():
\r
833 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
\r
834 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
\r
837 refs.append("Sig_%s" % self.get_refid())
\r
839 parentRefs = self.updateRefID()
\r
840 for ref in parentRefs:
\r
841 refs.append("Sig_%s" % ref)
\r
844 # If caller explicitly passed in None that means skip xmlsec1 validation.
\r
845 # Strange and not typical
\r
846 if trusted_certs is None:
\r
849 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
\r
850 # (self.xmlsec_path, ref, cert_args, filename)
\r
851 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
\r
852 % (self.xmlsec_path, ref, cert_args, filename)).read()
\r
853 if not verified.strip().startswith("OK"):
\r
854 # xmlsec errors have a msg= which is the interesting bit.
\r
855 mstart = verified.find("msg=")
\r
857 if mstart > -1 and len(verified) > 4:
\r
858 mstart = mstart + 4
\r
859 mend = verified.find('\\', mstart)
\r
860 msg = verified[mstart:mend]
\r
861 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
\r
862 os.remove(filename)
\r
864 # Verify the parents (delegation)
\r
866 self.verify_parent(self.parent)
\r
868 # Make sure the issuer is the target's authority, and is
\r
869 # itself a valid GID
\r
870 self.verify_issuer(trusted_cert_objects)
\r
874 # Creates a list of the credential and its parents, with the root
\r
875 # (original delegated credential) as the last item in the list
\r
876 def get_credential_list(self):
\r
880 list.append(cur_cred)
\r
881 if cur_cred.parent:
\r
882 cur_cred = cur_cred.parent
\r
888 # Make sure the credential's target gid (a) was signed by or (b)
\r
889 # is the same as the entity that signed the original credential,
\r
890 # or (c) is an authority over the target's namespace.
\r
891 # Also ensure that the credential issuer / signer itself has a valid
\r
892 # GID signature chain (signed by an authority with namespace rights).
\r
893 def verify_issuer(self, trusted_gids):
\r
894 root_cred = self.get_credential_list()[-1]
\r
895 root_target_gid = root_cred.get_gid_object()
\r
896 root_cred_signer = root_cred.get_signature().get_issuer_gid()
\r
899 # Allow non authority to sign target and cred about target.
\r
901 # Why do we need to allow non authorities to sign?
\r
902 # If in the target gid validation step we correctly
\r
903 # checked that the target is only signed by an authority,
\r
904 # then this is just a special case of case 3.
\r
905 # This short-circuit is the common case currently -
\r
906 # and cause GID validation doesn't check 'authority',
\r
907 # this allows users to generate valid slice credentials.
\r
908 if root_target_gid.is_signed_by_cert(root_cred_signer):
\r
909 # cred signer matches target signer, return success
\r
913 # Allow someone to sign credential about themeselves. Used?
\r
914 # If not, remove this.
\r
915 #root_target_gid_str = root_target_gid.save_to_string()
\r
916 #root_cred_signer_str = root_cred_signer.save_to_string()
\r
917 #if root_target_gid_str == root_cred_signer_str:
\r
918 # # cred signer is target, return success
\r
923 # root_cred_signer is not the target_gid
\r
924 # So this is a different gid that we have not verified.
\r
925 # xmlsec1 verified the cert chain on this already, but
\r
926 # it hasn't verified that the gid meets the HRN namespace
\r
928 # Below we'll ensure that it is an authority.
\r
929 # But we haven't verified that it is _signed by_ an authority
\r
930 # We also don't know if xmlsec1 requires that cert signers
\r
931 # are marked as CAs.
\r
933 # Note that if verify() gave us no trusted_gids then this
\r
934 # call will fail. So skip it if we have no trusted_gids
\r
935 if trusted_gids and len(trusted_gids) > 0:
\r
936 root_cred_signer.verify_chain(trusted_gids)
\r
938 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
\r
940 # See if the signer is an authority over the domain of the target.
\r
941 # There are multiple types of authority - accept them all here
\r
942 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
\r
943 root_cred_signer_type = root_cred_signer.get_type()
\r
944 if (root_cred_signer_type.find('authority') == 0):
\r
945 #logger.debug('Cred signer is an authority')
\r
946 # signer is an authority, see if target is in authority's domain
\r
947 signerhrn = root_cred_signer.get_hrn()
\r
948 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
\r
951 # We've required that the credential be signed by an authority
\r
952 # for that domain. Reasonable and probably correct.
\r
953 # A looser model would also allow the signer to be an authority
\r
954 # in my control framework - eg My CA or CH. Even if it is not
\r
955 # the CH that issued these, eg, user credentials.
\r
957 # Give up, credential does not pass issuer verification
\r
959 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
963 # -- For Delegates (credentials with parents) verify that:
\r
964 # . The privileges must be a subset of the parent credentials
\r
965 # . The privileges must have "can_delegate" set for each delegated privilege
\r
966 # . The target gid must be the same between child and parents
\r
967 # . The expiry time on the child must be no later than the parent
\r
968 # . The signer of the child must be the owner of the parent
\r
969 def verify_parent(self, parent_cred):
\r
970 # make sure the rights given to the child are a subset of the
\r
971 # parents rights (and check delegate bits)
\r
972 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
\r
973 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
\r
974 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
\r
975 self.get_privileges().save_to_string())
\r
977 # make sure my target gid is the same as the parent's
\r
978 if not parent_cred.get_gid_object().save_to_string() == \
\r
979 self.get_gid_object().save_to_string():
\r
980 raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
\r
982 # make sure my expiry time is <= my parent's
\r
983 if not parent_cred.get_expiration() >= self.get_expiration():
\r
984 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
\r
986 # make sure my signer is the parent's caller
\r
987 if not parent_cred.get_gid_caller().save_to_string(False) == \
\r
988 self.get_signature().get_issuer_gid().save_to_string(False):
\r
989 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
\r
992 if parent_cred.parent:
\r
993 parent_cred.verify_parent(parent_cred.parent)
\r
996 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
\r
998 Return a delegated copy of this credential, delegated to the
\r
999 specified gid's user.
\r
1001 # get the gid of the object we are delegating
\r
1002 object_gid = self.get_gid_object()
\r
1003 object_hrn = object_gid.get_hrn()
\r
1005 # the hrn of the user who will be delegated to
\r
1006 delegee_gid = GID(filename=delegee_gidfile)
\r
1007 delegee_hrn = delegee_gid.get_hrn()
\r
1009 #user_key = Keypair(filename=keyfile)
\r
1010 #user_hrn = self.get_gid_caller().get_hrn()
\r
1011 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
\r
1012 dcred = Credential(subject=subject_string)
\r
1013 dcred.set_gid_caller(delegee_gid)
\r
1014 dcred.set_gid_object(object_gid)
\r
1015 dcred.set_parent(self)
\r
1016 dcred.set_expiration(self.get_expiration())
\r
1017 dcred.set_privileges(self.get_privileges())
\r
1018 dcred.get_privileges().delegate_all_privileges(True)
\r
1019 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
\r
1020 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
\r
1026 # only informative
\r
1027 def get_filename(self):
\r
1028 return getattr(self,'filename',None)
\r
1031 # Dump the contents of a credential to stdout in human-readable format
\r
1033 # @param dump_parents If true, also dump the parent certificates
\r
1034 def dump (self, *args, **kwargs):
\r
1035 print self.dump_string(*args, **kwargs)
\r
1038 def dump_string(self, dump_parents=False):
\r
1040 result += "CREDENTIAL %s\n" % self.get_subject()
\r
1041 filename=self.get_filename()
\r
1042 if filename: result += "Filename %s\n"%filename
\r
1043 result += " privs: %s\n" % self.get_privileges().save_to_string()
\r
1044 gidCaller = self.get_gid_caller()
\r
1046 result += " gidCaller:\n"
\r
1047 result += gidCaller.dump_string(8, dump_parents)
\r
1049 if self.get_signature():
\r
1050 print " gidIssuer:"
\r
1051 self.get_signature().get_issuer_gid().dump(8, dump_parents)
\r
1053 gidObject = self.get_gid_object()
\r
1055 result += " gidObject:\n"
\r
1056 result += gidObject.dump_string(8, dump_parents)
\r
1058 if self.parent and dump_parents:
\r
1059 result += "\nPARENT"
\r
1060 result += self.parent.dump_string(True)
\r