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 ref_id = sig.getAttribute("xml:id").strip().strip("Sig_")
\r
191 # The xml:id tag is optional, and could be in a
\r
192 # Reference xml:id or Reference UID sub element instead
\r
193 if not ref_id or ref_id == '':
\r
194 reference = sig.getElementsByTagName('Reference')[0]
\r
195 ref_id = reference.getAttribute('xml:id').strip().strip('Sig_')
\r
196 if not ref_id or ref_id == '':
\r
197 ref_id = reference.getAttribute('URI').strip().strip('#')
\r
198 self.set_refid(ref_id)
\r
199 keyinfos = sig.getElementsByTagName("X509Data")
\r
201 for keyinfo in keyinfos:
\r
202 certs = keyinfo.getElementsByTagName("X509Certificate")
\r
204 if len(cert.childNodes) > 0:
\r
205 szgid = cert.childNodes[0].nodeValue
\r
206 szgid = szgid.strip()
\r
207 szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid
\r
211 gids += "\n" + szgid
\r
213 raise CredentialNotVerifiable("Malformed XML: No certificate found in signature")
\r
214 self.set_issuer_gid(GID(string=gids))
\r
217 self.xml = signature_template % (self.get_refid(), self.get_refid())
\r
221 # A credential provides a caller gid with privileges to an object gid.
\r
222 # A signed credential is signed by the object's authority.
\r
224 # Credentials are encoded in one of two ways. The legacy style places
\r
225 # it in the subjectAltName of an X509 certificate. The new credentials
\r
226 # are placed in signed XML.
\r
229 # In general, a signed credential obtained externally should
\r
230 # not be changed else the signature is no longer valid. So, once
\r
231 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
233 def filter_creds_by_caller(creds, caller_hrn_list):
\r
235 Returns a list of creds who's gid caller matches the
\r
236 specified caller hrn
\r
238 if not isinstance(creds, list): creds = [creds]
\r
239 if not isinstance(caller_hrn_list, list):
\r
240 caller_hrn_list = [caller_hrn_list]
\r
244 tmp_cred = Credential(string=cred)
\r
245 if tmp_cred.get_cred_type() != Credential.SFA_CREDENTIAL_TYPE:
\r
247 if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list:
\r
248 caller_creds.append(cred)
\r
250 return caller_creds
\r
252 class Credential(object):
\r
254 SFA_CREDENTIAL_TYPE = "geni_sfa"
\r
257 # Create a Credential object
\r
259 # @param create If true, create a blank x509 certificate
\r
260 # @param subject If subject!=None, create an x509 cert with the subject name
\r
261 # @param string If string!=None, load the credential from the string
\r
262 # @param filename If filename!=None, load the credential from the file
\r
263 # FIXME: create and subject are ignored!
\r
264 def __init__(self, create=False, subject=None, string=None, filename=None):
\r
265 self.gidCaller = None
\r
266 self.gidObject = None
\r
267 self.expiration = None
\r
268 self.privileges = None
\r
269 self.issuer_privkey = None
\r
270 self.issuer_gid = None
\r
271 self.issuer_pubkey = None
\r
273 self.signature = None
\r
277 self.cred_type = Credential.SFA_CREDENTIAL_TYPE
\r
279 # Check if this is a legacy credential, translate it if so
\r
280 if string or filename:
\r
284 str = file(filename).read()
\r
286 if str.strip().startswith("-----"):
\r
287 self.legacy = CredentialLegacy(False,string=str)
\r
288 self.translate_legacy(str)
\r
293 # Find an xmlsec1 path
\r
294 self.xmlsec_path = ''
\r
295 paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin']
\r
297 if os.path.isfile(path + '/' + 'xmlsec1'):
\r
298 self.xmlsec_path = path + '/' + 'xmlsec1'
\r
300 if not self.xmlsec_path:
\r
301 logger.warn("Could not locate binary for xmlsec1 - SFA will be unable to sign stuff !!")
\r
303 def get_cred_type(self):
\r
304 return self.cred_type
\r
306 def get_subject(self):
\r
307 if not self.gidObject:
\r
309 return self.gidObject.get_subject()
\r
311 # sounds like this should be __repr__ instead ??
\r
312 def get_summary_tostring(self):
\r
313 if not self.gidObject:
\r
315 obj = self.gidObject.get_printable_subject()
\r
316 caller = self.gidCaller.get_printable_subject()
\r
317 exp = self.get_expiration()
\r
318 # Summarize the rights too? The issuer?
\r
319 return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp)
\r
321 def get_signature(self):
\r
322 if not self.signature:
\r
324 return self.signature
\r
326 def set_signature(self, sig):
\r
327 self.signature = sig
\r
331 # Translate a legacy credential into a new one
\r
333 # @param String of the legacy credential
\r
335 def translate_legacy(self, str):
\r
336 legacy = CredentialLegacy(False,string=str)
\r
337 self.gidCaller = legacy.get_gid_caller()
\r
338 self.gidObject = legacy.get_gid_object()
\r
339 lifetime = legacy.get_lifetime()
\r
341 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
343 self.set_expiration(int(lifetime))
\r
344 self.lifeTime = legacy.get_lifetime()
\r
345 self.set_privileges(legacy.get_privileges())
\r
346 self.get_privileges().delegate_all_privileges(legacy.get_delegate())
\r
349 # Need the issuer's private key and name
\r
350 # @param key Keypair object containing the private key of the issuer
\r
351 # @param gid GID of the issuing authority
\r
353 def set_issuer_keys(self, privkey, gid):
\r
354 self.issuer_privkey = privkey
\r
355 self.issuer_gid = gid
\r
359 # Set this credential's parent
\r
360 def set_parent(self, cred):
\r
365 # set the GID of the caller
\r
367 # @param gid GID object of the caller
\r
369 def set_gid_caller(self, gid):
\r
370 self.gidCaller = gid
\r
371 # gid origin caller is the caller's gid by default
\r
372 self.gidOriginCaller = gid
\r
375 # get the GID of the object
\r
377 def get_gid_caller(self):
\r
378 if not self.gidCaller:
\r
380 return self.gidCaller
\r
383 # set the GID of the object
\r
385 # @param gid GID object of the object
\r
387 def set_gid_object(self, gid):
\r
388 self.gidObject = gid
\r
391 # get the GID of the object
\r
393 def get_gid_object(self):
\r
394 if not self.gidObject:
\r
396 return self.gidObject
\r
399 # Expiration: an absolute UTC time of expiration (as either an int or string or datetime)
\r
401 def set_expiration(self, expiration):
\r
402 if isinstance(expiration, (int, float)):
\r
403 self.expiration = datetime.datetime.fromtimestamp(expiration)
\r
404 elif isinstance (expiration, datetime.datetime):
\r
405 self.expiration = expiration
\r
406 elif isinstance (expiration, StringTypes):
\r
407 self.expiration = utcparse (expiration)
\r
409 logger.error ("unexpected input type in Credential.set_expiration")
\r
413 # get the lifetime of the credential (always in datetime format)
\r
415 def get_expiration(self):
\r
416 if not self.expiration:
\r
418 # at this point self.expiration is normalized as a datetime - DON'T call utcparse again
\r
419 return self.expiration
\r
423 def get_lifetime(self):
\r
424 return self.get_expiration()
\r
427 # set the privileges
\r
429 # @param privs either a comma-separated list of privileges of a Rights object
\r
431 def set_privileges(self, privs):
\r
432 if isinstance(privs, str):
\r
433 self.privileges = Rights(string = privs)
\r
435 self.privileges = privs
\r
438 # return the privileges as a Rights object
\r
440 def get_privileges(self):
\r
441 if not self.privileges:
\r
443 return self.privileges
\r
446 # determine whether the credential allows a particular operation to be
\r
449 # @param op_name string specifying name of operation ("lookup", "update", etc)
\r
451 def can_perform(self, op_name):
\r
452 rights = self.get_privileges()
\r
457 return rights.can_perform(op_name)
\r
461 # Encode the attributes of the credential into an XML string
\r
462 # This should be done immediately before signing the credential.
\r
464 # In general, a signed credential obtained externally should
\r
465 # not be changed else the signature is no longer valid. So, once
\r
466 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
469 # Create the XML document
\r
471 signed_cred = doc.createElement("signed-credential")
\r
473 # Declare namespaces
\r
474 # Note that credential/policy.xsd are really the PG schemas
\r
475 # in a PL namespace.
\r
476 # Note that delegation of credentials between the 2 only really works
\r
477 # cause those schemas are identical.
\r
478 # Also note these PG schemas talk about PG tickets and CM policies.
\r
479 signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
480 # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd
\r
481 signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd")
\r
482 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
484 # PG says for those last 2:
\r
485 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
486 # 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
488 doc.appendChild(signed_cred)
\r
490 # Fill in the <credential> bit
\r
491 cred = doc.createElement("credential")
\r
492 cred.setAttribute("xml:id", self.get_refid())
\r
493 signed_cred.appendChild(cred)
\r
494 append_sub(doc, cred, "type", "privilege")
\r
495 append_sub(doc, cred, "serial", "8")
\r
496 append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string())
\r
497 append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn())
\r
498 append_sub(doc, cred, "target_gid", self.gidObject.save_to_string())
\r
499 append_sub(doc, cred, "target_urn", self.gidObject.get_urn())
\r
500 append_sub(doc, cred, "uuid", "")
\r
501 if not self.expiration:
\r
502 self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
\r
503 self.expiration = self.expiration.replace(microsecond=0)
\r
504 if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None:
\r
505 # TZ aware. Make sure it is UTC
\r
506 self.expiration = self.expiration.astimezone(tz.tzutc())
\r
507 append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339
\r
508 privileges = doc.createElement("privileges")
\r
509 cred.appendChild(privileges)
\r
511 if self.privileges:
\r
512 rights = self.get_privileges()
\r
513 for right in rights.rights:
\r
514 priv = doc.createElement("privilege")
\r
515 append_sub(doc, priv, "name", right.kind)
\r
516 append_sub(doc, priv, "can_delegate", str(right.delegate).lower())
\r
517 privileges.appendChild(priv)
\r
519 # Add the parent credential if it exists
\r
521 sdoc = parseString(self.parent.get_xml())
\r
522 # If the root node is a signed-credential (it should be), then
\r
523 # get all its attributes and attach those to our signed_cred
\r
525 # Specifically, PG and PLadd attributes for namespaces (which is reasonable),
\r
526 # and we need to include those again here or else their signature
\r
527 # no longer matches on the credential.
\r
528 # We expect three of these, but here we copy them all:
\r
529 # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
\r
530 # and from PG (PL is equivalent, as shown above):
\r
531 # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd")
\r
532 # 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
535 # PL now also declares these, with different URLs, so
\r
536 # the code notices those attributes already existed with
\r
537 # different values, and complains.
\r
538 # This happens regularly on delegation now that PG and
\r
539 # PL both declare the namespace with different URLs.
\r
540 # If the content ever differs this is a problem,
\r
541 # but for now it works - different URLs (values in the attributes)
\r
542 # but the same actual schema, so using the PG schema
\r
543 # on delegated-to-PL credentials works fine.
\r
545 # Note: you could also not copy attributes
\r
546 # which already exist. It appears that both PG and PL
\r
547 # will actually validate a slicecred with a parent
\r
548 # signed using PG namespaces and a child signed with PL
\r
549 # namespaces over the whole thing. But I don't know
\r
550 # if that is a bug in xmlsec1, an accident since
\r
551 # the contents of the schemas are the same,
\r
552 # or something else, but it seems odd. And this works.
\r
553 parentRoot = sdoc.documentElement
\r
554 if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes():
\r
555 for attrIx in range(0, parentRoot.attributes.length):
\r
556 attr = parentRoot.attributes.item(attrIx)
\r
557 # returns the old attribute of same name that was
\r
558 # on the credential
\r
559 # Below throws InUse exception if we forgot to clone the attribute first
\r
560 oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True))
\r
561 if oldAttr and oldAttr.value != attr.value:
\r
562 msg = "Delegating cred from owner %s to %s over %s:\n - 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
564 #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg)
\r
566 p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True)
\r
567 p = doc.createElement("parent")
\r
568 p.appendChild(p_cred)
\r
569 cred.appendChild(p)
\r
570 # done handling parent credential
\r
572 # Create the <signatures> tag
\r
573 signatures = doc.createElement("signatures")
\r
574 signed_cred.appendChild(signatures)
\r
576 # Add any parent signatures
\r
578 for cur_cred in self.get_credential_list()[1:]:
\r
579 sdoc = parseString(cur_cred.get_signature().get_xml())
\r
580 ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
581 signatures.appendChild(ele)
\r
583 # Get the finished product
\r
584 self.xml = doc.toxml("utf-8")
\r
587 def save_to_random_tmp_file(self):
\r
588 fp, filename = mkstemp(suffix='cred', text=True)
\r
589 fp = os.fdopen(fp, "w")
\r
590 self.save_to_file(filename, save_parents=True, filep=fp)
\r
593 def save_to_file(self, filename, save_parents=True, filep=None):
\r
599 f = open(filename, "w")
\r
603 def save_to_string(self, save_parents=True):
\r
608 def get_refid(self):
\r
610 self.refid = 'ref0'
\r
613 def set_refid(self, rid):
\r
617 # Figure out what refids exist, and update this credential's id
\r
618 # so that it doesn't clobber the others. Returns the refids of
\r
621 def updateRefID(self):
\r
622 if not self.parent:
\r
623 self.set_refid('ref0')
\r
628 next_cred = self.parent
\r
630 refs.append(next_cred.get_refid())
\r
631 if next_cred.parent:
\r
632 next_cred = next_cred.parent
\r
637 # Find a unique refid for this credential
\r
638 rid = self.get_refid()
\r
641 rid = "ref%d" % (val + 1)
\r
643 # Set the new refid
\r
644 self.set_refid(rid)
\r
646 # Return the set of parent credential ref ids
\r
655 # Sign the XML file created by encode()
\r
658 # In general, a signed credential obtained externally should
\r
659 # not be changed else the signature is no longer valid. So, once
\r
660 # you have loaded an existing signed credential, do not call encode() or sign() on it.
\r
663 if not self.issuer_privkey:
\r
664 logger.warn("Cannot sign credential (no private key)")
\r
666 if not self.issuer_gid:
\r
667 logger.warn("Cannot sign credential (no issuer gid)")
\r
669 doc = parseString(self.get_xml())
\r
670 sigs = doc.getElementsByTagName("signatures")[0]
\r
672 # Create the signature template to be signed
\r
673 signature = Signature()
\r
674 signature.set_refid(self.get_refid())
\r
675 sdoc = parseString(signature.get_xml())
\r
676 sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True)
\r
677 sigs.appendChild(sig_ele)
\r
679 self.xml = doc.toxml("utf-8")
\r
682 # Split the issuer GID into multiple certificates if it's a chain
\r
683 chain = GID(filename=self.issuer_gid)
\r
686 gid_files.append(chain.save_to_random_tmp_file(False))
\r
687 if chain.get_parent():
\r
688 chain = chain.get_parent()
\r
693 # Call out to xmlsec1 to sign it
\r
694 ref = 'Sig_%s' % self.get_refid()
\r
695 filename = self.save_to_random_tmp_file()
\r
696 command='%s --sign --node-id "%s" --privkey-pem %s,%s %s' \
\r
697 % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)
\r
698 # print 'command',command
\r
699 signed = os.popen(command).read()
\r
700 os.remove(filename)
\r
702 for gid_file in gid_files:
\r
703 os.remove(gid_file)
\r
707 # This is no longer a legacy credential
\r
711 # Update signatures
\r
716 # Retrieve the attributes of the credential from the XML.
\r
717 # This is automatically called by the various get_* methods of
\r
718 # this class and should not need to be called explicitly.
\r
723 doc = parseString(self.xml)
\r
725 signed_cred = doc.getElementsByTagName("signed-credential")
\r
727 # Is this a signed-cred or just a cred?
\r
728 if len(signed_cred) > 0:
\r
729 creds = signed_cred[0].getElementsByTagName("credential")
\r
730 signatures = signed_cred[0].getElementsByTagName("signatures")
\r
731 if len(signatures) > 0:
\r
732 sigs = signatures[0].getElementsByTagName("Signature")
\r
734 creds = doc.getElementsByTagName("credential")
\r
736 if creds is None or len(creds) == 0:
\r
737 # malformed cred file
\r
738 raise CredentialNotVerifiable("Malformed XML: No credential tag found")
\r
740 # Just take the first cred if there are more than one
\r
743 self.set_refid(cred.getAttribute("xml:id"))
\r
744 self.set_expiration(utcparse(getTextNode(cred, "expires")))
\r
747 # stack = traceback.extract_stack()
\r
749 og = getTextNode(cred, "owner_gid")
\r
750 # ABAC creds will have this be None and use this method
\r
753 # for frame in stack:
\r
754 # if 'super(ABACCredential, self).decode()' in frame:
\r
758 # raise CredentialNotVerifiable("Malformed XML: No owner_gid found")
\r
759 self.gidCaller = GID(string=og)
\r
760 tg = getTextNode(cred, "target_gid")
\r
763 # for frame in stack:
\r
764 # if 'super(ABACCredential, self).decode()' in frame:
\r
768 # raise CredentialNotVerifiable("Malformed XML: No target_gid found")
\r
769 self.gidObject = GID(string=tg)
\r
771 # Process privileges
\r
773 priv_nodes = cred.getElementsByTagName("privileges")
\r
774 if len(priv_nodes) > 0:
\r
775 privs = priv_nodes[0]
\r
776 for priv in privs.getElementsByTagName("privilege"):
\r
777 kind = getTextNode(priv, "name")
\r
778 deleg = str2bool(getTextNode(priv, "can_delegate"))
\r
780 # Convert * into the default privileges for the credential's type
\r
781 # Each inherits the delegatability from the * above
\r
782 _ , type = urn_to_hrn(self.gidObject.get_urn())
\r
783 rl = determine_rights(type, self.gidObject.get_urn())
\r
784 for r in rl.rights:
\r
788 rlist.add(Right(kind.strip(), deleg))
\r
789 self.set_privileges(rlist)
\r
792 # Is there a parent?
\r
793 parent = cred.getElementsByTagName("parent")
\r
794 if len(parent) > 0:
\r
795 parent_doc = parent[0].getElementsByTagName("credential")[0]
\r
796 parent_xml = parent_doc.toxml("utf-8")
\r
797 if parent_xml is None or parent_xml.strip() == "":
\r
798 raise CredentialNotVerifiable("Malformed XML: Had parent tag but it is empty")
\r
799 self.parent = Credential(string=parent_xml)
\r
802 # Assign the signatures to the credentials
\r
804 Sig = Signature(string=sig.toxml("utf-8"))
\r
806 for cur_cred in self.get_credential_list():
\r
807 if cur_cred.get_refid() == Sig.get_refid():
\r
808 cur_cred.set_signature(Sig)
\r
813 # trusted_certs: A list of trusted GID filenames (not GID objects!)
\r
814 # Chaining is not supported within the GIDs by xmlsec1.
\r
816 # trusted_certs_required: Should usually be true. Set False means an
\r
817 # empty list of trusted_certs would still let this method pass.
\r
818 # It just skips xmlsec1 verification et al. Only used by some utils
\r
821 # . All of the signatures are valid and that the issuers trace back
\r
822 # to trusted roots (performed by xmlsec1)
\r
823 # . The XML matches the credential schema
\r
824 # . That the issuer of the credential is the authority in the target's urn
\r
825 # . In the case of a delegated credential, this must be true of the root
\r
826 # . That all of the gids presented in the credential are valid
\r
827 # . Including verifying GID chains, and includ the issuer
\r
828 # . The credential is not expired
\r
830 # -- For Delegates (credentials with parents)
\r
831 # . The privileges must be a subset of the parent credentials
\r
832 # . The privileges must have "can_delegate" set for each delegated privilege
\r
833 # . The target gid must be the same between child and parents
\r
834 # . The expiry time on the child must be no later than the parent
\r
835 # . The signer of the child must be the owner of the parent
\r
837 # -- Verify does *NOT*
\r
838 # . ensure that an xmlrpc client's gid matches a credential gid, that
\r
839 # must be done elsewhere
\r
841 # @param trusted_certs: The certificates of trusted CA certificates
\r
842 def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True):
\r
846 # validate against RelaxNG schema
\r
847 if HAVELXML and not self.legacy:
\r
848 if schema and os.path.exists(schema):
\r
849 tree = etree.parse(StringIO(self.xml))
\r
850 schema_doc = etree.parse(schema)
\r
851 xmlschema = etree.XMLSchema(schema_doc)
\r
852 if not xmlschema.validate(tree):
\r
853 error = xmlschema.error_log.last_error
\r
854 message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line)
\r
855 raise CredentialNotVerifiable(message)
\r
857 if trusted_certs_required and trusted_certs is None:
\r
860 # trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
\r
861 trusted_cert_objects = []
\r
862 ok_trusted_certs = []
\r
863 # If caller explicitly passed in None that means skip cert chain validation.
\r
864 # Strange and not typical
\r
865 if trusted_certs is not None:
\r
866 for f in trusted_certs:
\r
868 # Failures here include unreadable files
\r
870 trusted_cert_objects.append(GID(filename=f))
\r
871 ok_trusted_certs.append(f)
\r
872 except Exception, exc:
\r
873 logger.error("Failed to load trusted cert from %s: %r", f, exc)
\r
874 trusted_certs = ok_trusted_certs
\r
876 # Use legacy verification if this is a legacy credential
\r
878 self.legacy.verify_chain(trusted_cert_objects)
\r
879 if self.legacy.client_gid:
\r
880 self.legacy.client_gid.verify_chain(trusted_cert_objects)
\r
881 if self.legacy.object_gid:
\r
882 self.legacy.object_gid.verify_chain(trusted_cert_objects)
\r
885 # make sure it is not expired
\r
886 if self.get_expiration() < datetime.datetime.utcnow():
\r
887 raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat()))
\r
889 # Verify the signatures
\r
890 filename = self.save_to_random_tmp_file()
\r
891 if trusted_certs is not None:
\r
892 cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
\r
894 # If caller explicitly passed in None that means skip cert chain validation.
\r
895 # - Strange and not typical
\r
896 if trusted_certs is not None:
\r
897 # Verify the gids of this cred and of its parents
\r
898 for cur_cred in self.get_credential_list():
\r
899 cur_cred.get_gid_object().verify_chain(trusted_cert_objects)
\r
900 cur_cred.get_gid_caller().verify_chain(trusted_cert_objects)
\r
903 refs.append("Sig_%s" % self.get_refid())
\r
905 parentRefs = self.updateRefID()
\r
906 for ref in parentRefs:
\r
907 refs.append("Sig_%s" % ref)
\r
910 # If caller explicitly passed in None that means skip xmlsec1 validation.
\r
911 # Strange and not typical
\r
912 if trusted_certs is None:
\r
915 # print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \
\r
916 # (self.xmlsec_path, ref, cert_args, filename)
\r
917 verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \
\r
918 % (self.xmlsec_path, ref, cert_args, filename)).read()
\r
919 if not verified.strip().startswith("OK"):
\r
920 # xmlsec errors have a msg= which is the interesting bit.
\r
921 mstart = verified.find("msg=")
\r
923 if mstart > -1 and len(verified) > 4:
\r
924 mstart = mstart + 4
\r
925 mend = verified.find('\\', mstart)
\r
926 msg = verified[mstart:mend]
\r
927 raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip()))
\r
928 os.remove(filename)
\r
930 # Verify the parents (delegation)
\r
932 self.verify_parent(self.parent)
\r
934 # Make sure the issuer is the target's authority, and is
\r
935 # itself a valid GID
\r
936 self.verify_issuer(trusted_cert_objects)
\r
940 # Creates a list of the credential and its parents, with the root
\r
941 # (original delegated credential) as the last item in the list
\r
942 def get_credential_list(self):
\r
946 list.append(cur_cred)
\r
947 if cur_cred.parent:
\r
948 cur_cred = cur_cred.parent
\r
954 # Make sure the credential's target gid (a) was signed by or (b)
\r
955 # is the same as the entity that signed the original credential,
\r
956 # or (c) is an authority over the target's namespace.
\r
957 # Also ensure that the credential issuer / signer itself has a valid
\r
958 # GID signature chain (signed by an authority with namespace rights).
\r
959 def verify_issuer(self, trusted_gids):
\r
960 root_cred = self.get_credential_list()[-1]
\r
961 root_target_gid = root_cred.get_gid_object()
\r
962 if root_cred.get_signature() is None:
\r
964 raise CredentialNotVerifiable("Could not verify credential owned by %s for object %s. Cred has no signature" % (self.gidCaller.get_urn(), self.gidObject.get_urn()))
\r
966 root_cred_signer = root_cred.get_signature().get_issuer_gid()
\r
969 # Allow non authority to sign target and cred about target.
\r
971 # Why do we need to allow non authorities to sign?
\r
972 # If in the target gid validation step we correctly
\r
973 # checked that the target is only signed by an authority,
\r
974 # then this is just a special case of case 3.
\r
975 # This short-circuit is the common case currently -
\r
976 # and cause GID validation doesn't check 'authority',
\r
977 # this allows users to generate valid slice credentials.
\r
978 if root_target_gid.is_signed_by_cert(root_cred_signer):
\r
979 # cred signer matches target signer, return success
\r
983 # Allow someone to sign credential about themeselves. Used?
\r
984 # If not, remove this.
\r
985 #root_target_gid_str = root_target_gid.save_to_string()
\r
986 #root_cred_signer_str = root_cred_signer.save_to_string()
\r
987 #if root_target_gid_str == root_cred_signer_str:
\r
988 # # cred signer is target, return success
\r
993 # root_cred_signer is not the target_gid
\r
994 # So this is a different gid that we have not verified.
\r
995 # xmlsec1 verified the cert chain on this already, but
\r
996 # it hasn't verified that the gid meets the HRN namespace
\r
998 # Below we'll ensure that it is an authority.
\r
999 # But we haven't verified that it is _signed by_ an authority
\r
1000 # We also don't know if xmlsec1 requires that cert signers
\r
1001 # are marked as CAs.
\r
1003 # Note that if verify() gave us no trusted_gids then this
\r
1004 # call will fail. So skip it if we have no trusted_gids
\r
1005 if trusted_gids and len(trusted_gids) > 0:
\r
1006 root_cred_signer.verify_chain(trusted_gids)
\r
1008 logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.")
\r
1010 # See if the signer is an authority over the domain of the target.
\r
1011 # There are multiple types of authority - accept them all here
\r
1012 # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn())
\r
1013 root_cred_signer_type = root_cred_signer.get_type()
\r
1014 if (root_cred_signer_type.find('authority') == 0):
\r
1015 #logger.debug('Cred signer is an authority')
\r
1016 # signer is an authority, see if target is in authority's domain
\r
1017 signerhrn = root_cred_signer.get_hrn()
\r
1018 if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()):
\r
1021 # We've required that the credential be signed by an authority
\r
1022 # for that domain. Reasonable and probably correct.
\r
1023 # A looser model would also allow the signer to be an authority
\r
1024 # in my control framework - eg My CA or CH. Even if it is not
\r
1025 # the CH that issued these, eg, user credentials.
\r
1027 # Give up, credential does not pass issuer verification
\r
1029 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
1033 # -- For Delegates (credentials with parents) verify that:
\r
1034 # . The privileges must be a subset of the parent credentials
\r
1035 # . The privileges must have "can_delegate" set for each delegated privilege
\r
1036 # . The target gid must be the same between child and parents
\r
1037 # . The expiry time on the child must be no later than the parent
\r
1038 # . The signer of the child must be the owner of the parent
\r
1039 def verify_parent(self, parent_cred):
\r
1040 # make sure the rights given to the child are a subset of the
\r
1041 # parents rights (and check delegate bits)
\r
1042 if not parent_cred.get_privileges().is_superset(self.get_privileges()):
\r
1043 raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) +
\r
1044 self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) +
\r
1045 self.get_privileges().save_to_string())
\r
1047 # make sure my target gid is the same as the parent's
\r
1048 if not parent_cred.get_gid_object().save_to_string() == \
\r
1049 self.get_gid_object().save_to_string():
\r
1050 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
1052 # make sure my expiry time is <= my parent's
\r
1053 if not parent_cred.get_expiration() >= self.get_expiration():
\r
1054 raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
\r
1056 # make sure my signer is the parent's caller
\r
1057 if not parent_cred.get_gid_caller().save_to_string(False) == \
\r
1058 self.get_signature().get_issuer_gid().save_to_string(False):
\r
1059 raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring()))
\r
1062 if parent_cred.parent:
\r
1063 parent_cred.verify_parent(parent_cred.parent)
\r
1066 def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile):
\r
1068 Return a delegated copy of this credential, delegated to the
\r
1069 specified gid's user.
\r
1071 # get the gid of the object we are delegating
\r
1072 object_gid = self.get_gid_object()
\r
1073 object_hrn = object_gid.get_hrn()
\r
1075 # the hrn of the user who will be delegated to
\r
1076 delegee_gid = GID(filename=delegee_gidfile)
\r
1077 delegee_hrn = delegee_gid.get_hrn()
\r
1079 #user_key = Keypair(filename=keyfile)
\r
1080 #user_hrn = self.get_gid_caller().get_hrn()
\r
1081 subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn)
\r
1082 dcred = Credential(subject=subject_string)
\r
1083 dcred.set_gid_caller(delegee_gid)
\r
1084 dcred.set_gid_object(object_gid)
\r
1085 dcred.set_parent(self)
\r
1086 dcred.set_expiration(self.get_expiration())
\r
1087 dcred.set_privileges(self.get_privileges())
\r
1088 dcred.get_privileges().delegate_all_privileges(True)
\r
1089 #dcred.set_issuer_keys(keyfile, delegee_gidfile)
\r
1090 dcred.set_issuer_keys(caller_keyfile, caller_gidfile)
\r
1096 # only informative
\r
1097 def get_filename(self):
\r
1098 return getattr(self,'filename',None)
\r
1101 # Dump the contents of a credential to stdout in human-readable format
\r
1103 # @param dump_parents If true, also dump the parent certificates
\r
1104 def dump (self, *args, **kwargs):
\r
1105 print self.dump_string(*args, **kwargs)
\r
1108 def dump_string(self, dump_parents=False, show_xml=False):
\r
1110 result += "CREDENTIAL %s\n" % self.get_subject()
\r
1111 filename=self.get_filename()
\r
1112 if filename: result += "Filename %s\n"%filename
\r
1113 result += " privs: %s\n" % self.get_privileges().save_to_string()
\r
1114 gidCaller = self.get_gid_caller()
\r
1116 result += " gidCaller:\n"
\r
1117 result += gidCaller.dump_string(8, dump_parents)
\r
1119 if self.get_signature():
\r
1120 result += " gidIssuer:\n"
\r
1121 result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents)
\r
1123 if self.expiration:
\r
1124 result += " expiration: " + self.expiration.isoformat() + "\n"
\r
1126 gidObject = self.get_gid_object()
\r
1128 result += " gidObject:\n"
\r
1129 result += gidObject.dump_string(8, dump_parents)
\r
1131 if self.parent and dump_parents:
\r
1132 result += "\nPARENT"
\r
1133 result += self.parent.dump_string(True)
\r
1135 if show_xml and HAVELXML:
\r
1137 tree = etree.parse(StringIO(self.xml))
\r
1138 aside = etree.tostring(tree, pretty_print=True)
\r
1139 result += "\nXML:\n\n"
\r
1141 result += "\nEnd XML\n"
\r
1144 print "exc. Credential.dump_string / XML"
\r
1145 traceback.print_exc()
\r