From 090353fb0817efbfbe350140a1e182ff16d65e13 Mon Sep 17 00:00:00 2001 From: Aaron Helsinger Date: Wed, 18 Nov 2015 14:43:52 -0500 Subject: [PATCH] Issue #854. Update to SFA 3.1-18 as of June 11, 2015 (from 3.1-9 of May 2014). This update includes: * drop legacy credential support * invoke xmlxec using subprocess and use the return code to measure success * rename methods for printing pretty certs and creds * rename the inner cert in the Certificate This merge is not a complete replace of what we had with the latest from SFA, as the latest from SFA does not have ABAC credential support, support multiple certs in the credential signature, support utf-8 credentials, or include many of our error checks. --- src/delegateSliceCred.py | 10 +- src/gcf/geni/util/cred_util.py | 2 +- src/gcf/geni/util/speaksfor_util.py | 18 +- src/gcf/omnilib/util/handler_utils.py | 2 +- src/gcf/sfa/README.txt | 8 +- src/gcf/sfa/trust/abac_credential.py | 9 +- src/gcf/sfa/trust/certificate.py | 152 ++++++++----- src/gcf/sfa/trust/credential.py | 289 ++++++++++++++----------- src/gcf/sfa/trust/credential_legacy.py | 271 ----------------------- src/gcf/sfa/trust/gid.py | 24 +- src/gcf/sfa/trust/rights.py | 18 +- src/gcf/sfa/util/faults.py | 49 ++++- src/gcf/sfa/util/genicode.py | 3 +- src/gcf/sfa/util/sfatime.py | 66 +++++- src/gcf/sfa/util/xrn.py | 39 +++- windows_install/LICENSE.TXT | 8 +- 16 files changed, 446 insertions(+), 522 deletions(-) delete mode 100644 src/gcf/sfa/trust/credential_legacy.py diff --git a/src/delegateSliceCred.py b/src/delegateSliceCred.py index 72755dea..dc821fd6 100755 --- a/src/delegateSliceCred.py +++ b/src/delegateSliceCred.py @@ -235,12 +235,12 @@ def naiveUTC(dt): delegee_cert = GID(filename=opts.delegeegid) # confirm cert hasn't expired - if owner_cert.cert.has_expired(): - sys.exit("Cred owner %s cert has expired at %s - cannot delegate" % (owner_cert.cert.get_subject(), owner_cert.cert.get_notAfter())) + if owner_cert.x509.has_expired(): + sys.exit("Cred owner %s cert has expired at %s - cannot delegate" % (owner_cert.x509.get_subject(), owner_cert.x509.get_notAfter())) # confirm cert to delegate to hasn't expired - if delegee_cert.cert.has_expired(): - sys.exit("Delegee %s cert has expired at %s - cannot delegate" % (delegee_cert.cert.get_subject(), delegee_cert.cert.get_notAfter())) + if delegee_cert.x509.has_expired(): + sys.exit("Delegee %s cert has expired at %s - cannot delegate" % (delegee_cert.x509.get_subject(), delegee_cert.x509.get_notAfter())) if len(root_objects) > 0: try: @@ -315,7 +315,7 @@ def naiveUTC(dt): if opts.debug: dcred.dump(True) else: - logger.info("Created delegated credential %s", dcred.get_summary_tostring()) + logger.info("Created delegated credential %s", dcred.pretty_cred()) # Save the result to a file bad = u'!"#%\'()*+,-./:;<=>?@[\]^_`{|}~' diff --git a/src/gcf/geni/util/cred_util.py b/src/gcf/geni/util/cred_util.py index 9e17d325..a589b85c 100644 --- a/src/gcf/geni/util/cred_util.py +++ b/src/gcf/geni/util/cred_util.py @@ -268,7 +268,7 @@ def verify(self, gid, credentials, target_urn, privileges): if cred.get_cred_type() == cred.SFA_CREDENTIAL_TYPE: cS = cred.get_gid_caller().get_urn() elif cred.get_cred_type() == ABACCredential.ABAC_CREDENTIAL_TYPE: - cS = cred.get_summary_tostring() + cS = cred.pretty_cred() else: cS = "Unknown credential type %s" % cred.get_cred_type() diff --git a/src/gcf/geni/util/speaksfor_util.py b/src/gcf/geni/util/speaksfor_util.py index 69e8bd20..fabc6335 100644 --- a/src/gcf/geni/util/speaksfor_util.py +++ b/src/gcf/geni/util/speaksfor_util.py @@ -138,26 +138,26 @@ def verify_speaks_for(cred, tool_gid, speaking_for_urn, \ # Credential has not expired if cred.expiration and cred.expiration < datetime.datetime.utcnow(): - return False, None, "ABAC Credential expired at %s (%s)" % (cred.expiration.isoformat(), cred.get_summary_tostring()) + return False, None, "ABAC Credential expired at %s (%s)" % (cred.expiration.isoformat(), cred.pretty_cred()) # Must be ABAC if cred.get_cred_type() != ABACCredential.ABAC_CREDENTIAL_TYPE: return False, None, "Credential not of type ABAC but %s" % cred.get_cred_type if cred.signature is None or cred.signature.gid is None: - return False, None, "Credential malformed: missing signature or signer cert. Cred: %s" % cred.get_summary_tostring() + return False, None, "Credential malformed: missing signature or signer cert. Cred: %s" % cred.pretty_cred() user_gid = cred.signature.gid user_urn = user_gid.get_urn() # URN of signer from cert must match URN of 'speaking-for' argument if user_urn != speaking_for_urn: return False, None, "User URN from cred doesn't match speaking_for URN: %s != %s (cred %s)" % \ - (user_urn, speaking_for_urn, cred.get_summary_tostring()) + (user_urn, speaking_for_urn, cred.pretty_cred()) tails = cred.get_tails() if len(tails) != 1: return False, None, "Invalid ABAC-SF credential: Need exactly 1 tail element, got %d (%s)" % \ - (len(tails), cred.get_summary_tostring()) + (len(tails), cred.pretty_cred()) user_keyid = get_cert_keyid(user_gid) tool_keyid = get_cert_keyid(tool_gid) @@ -195,7 +195,7 @@ def verify_speaks_for(cred, tool_gid, speaking_for_urn, \ if user_keyid != principal_keyid or \ tool_keyid != subject_keyid or \ role != ('speaks_for_%s' % user_keyid): - return False, None, "ABAC statement doesn't assert U.speaks_for(U)<-T (%s)" % cred.get_summary_tostring() + return False, None, "ABAC statement doesn't assert U.speaks_for(U)<-T (%s)" % cred.pretty_cred() # If schema provided, validate against schema if HAVELXML and schema and os.path.exists(schema): @@ -205,7 +205,7 @@ def verify_speaks_for(cred, tool_gid, speaking_for_urn, \ xmlschema = etree.XMLSchema(schema_doc) if not xmlschema.validate(tree): error = xmlschema.error_log.last_error - message = "%s: %s (line %s)" % (cred.get_summary_tostring(), error.message, error.line) + message = "%s: %s (line %s)" % (cred.pretty_cred(), error.message, error.line) return False, None, ("XML Credential schema invalid: %s" % message) if trusted_roots: @@ -222,7 +222,7 @@ def verify_speaks_for(cred, tool_gid, speaking_for_urn, \ except Exception, e: if user_gid.get_issuer() == tool_gid.get_issuer() and user_gid.get_parent() and not tool_gid.get_parent(): if logger: - logger.debug("Tool cert didn't verify (%s). Adding tool issuer (%s) as parent (taken from user_gid)", e, user_gid.get_parent().get_printable_subject()) + logger.debug("Tool cert didn't verify (%s). Adding tool issuer (%s) as parent (taken from user_gid)", e, user_gid.get_parent().pretty_cert()) tool_gid.set_parent(user_gid.get_parent()) try: tool_gid.verify_chain(trusted_roots) @@ -270,7 +270,7 @@ def determine_speaks_for(logger, credentials, caller_gid, options, \ if not isinstance(cred_value, ABACCredential): cred = CredentialFactory.createCred(cred_value) -# print "Got a cred to check speaksfor for: %s" % cred.get_summary_tostring() +# print "Got a cred to check speaksfor for: %s" % cred.pretty_cred() # #cred.dump(True, True) # print "Caller: %s" % caller_gid.dump_string(2, True) @@ -323,7 +323,7 @@ def create_sign_abaccred(tool_gid, user_gid, ma_gid, user_key_file, cred_filenam # Save it cred.save_to_file(cred_filename) print "Created ABAC credential: '%s' in file %s" % \ - (cred.get_summary_tostring(), cred_filename) + (cred.pretty_cred(), cred_filename) # FIXME: Assumes xmlsec1 is on path # FIXME: Assumes signer is itself signed by an 'ma_gid' that can be trusted diff --git a/src/gcf/omnilib/util/handler_utils.py b/src/gcf/omnilib/util/handler_utils.py index a01ee8aa..a3ef585b 100644 --- a/src/gcf/omnilib/util/handler_utils.py +++ b/src/gcf/omnilib/util/handler_utils.py @@ -940,7 +940,7 @@ def _is_user_cert_expired(handler): except Exception, e: handler.logger.debug("Failed to create GID from %s: %s", handler.framework.config['cert'], e) - if usergid and usergid.cert.has_expired(): + if usergid and usergid.x509.has_expired(): return True return False diff --git a/src/gcf/sfa/README.txt b/src/gcf/sfa/README.txt index 7a2ec297..26c70f62 100644 --- a/src/gcf/sfa/README.txt +++ b/src/gcf/sfa/README.txt @@ -7,16 +7,16 @@ directory is subject to the PlanetLab license (see "License" below). There are a small number of modifications to the code. These modifications are documented in the "Modifications" section below. -This code is based on revision 2947b0cb620ce8beb4c8c9bfe25adeadcbf1829f -(May 23rd, 2014, tag sfa-3.1-9 ) of the master branch of the +This code is based on revision 468d984409e02e84d15eb35d3eb464f6a3059dd8 +(June 11th, 2015, tag sfa-3.1-18 ) of the master branch of the PlanetLab Git repository (git.planet-lab.org/git/sfa.git). License ======= http://git.planet-lab.org/?p=sfa.git;a=blob_plain;f=LICENSE.txt;hb=HEAD -Copyright (c) 2008-2014 Board of Trustees, Princeton University -Copyright (c) 2010-2014 INRIA, Institut National d'Informatique et Automatique +Copyright (c) 2008-2015 Board of Trustees, Princeton University +Copyright (c) 2010-2015 INRIA, Institut National d'Informatique et Automatique Permission is hereby granted, free of charge, to any person obtaining a copy of this software and/or hardware specification (the "Work") to diff --git a/src/gcf/sfa/trust/abac_credential.py b/src/gcf/sfa/trust/abac_credential.py index b7f68c51..d1aba791 100644 --- a/src/gcf/sfa/trust/abac_credential.py +++ b/src/gcf/sfa/trust/abac_credential.py @@ -23,8 +23,9 @@ from __future__ import absolute_import -from .credential import Credential, append_sub +from .credential import Credential, append_sub, DEFAULT_CREDENTIAL_LIFETIME from ..util.sfalogging import logger +from ..util.sfatime import SFATIME_FORMAT from StringIO import StringIO from xml.dom.minidom import Document, parseString @@ -163,7 +164,7 @@ def dump_string(self, dump_parents=False, show_xml=False): filename=self.get_filename() if filename: result += "Filename %s\n"%filename if self.expiration: - result += "\texpiration: %s \n" % self.expiration.isoformat() + result += "\texpiration: %s \n" % self.expiration.strftime(SFATIME_FORMAT) result += "\tHead: %s\n" % self.get_head() for tail in self.get_tails(): @@ -186,7 +187,7 @@ def dump_string(self, dump_parents=False, show_xml=False): # sounds like this should be __repr__ instead ?? # Produce the ABAC assertion. Something like [ABAC cred: Me.role<-You] or similar - def get_summary_tostring(self): + def pretty_cred(self): result = "[ABAC cred: " + str(self.get_head()) for tail in self.get_tails(): result += "<-%s" % str(tail) @@ -259,7 +260,7 @@ def encode(self): if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None: # TZ aware. Make sure it is UTC self.expiration = self.expiration.astimezone(tz.tzutc()) - append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339 + append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT)) # RFC3339 abac = doc.createElement("abac") rt0 = doc.createElement("rt0") diff --git a/src/gcf/sfa/trust/certificate.py b/src/gcf/sfa/trust/certificate.py index 90528c39..4dc01348 100644 --- a/src/gcf/sfa/trust/certificate.py +++ b/src/gcf/sfa/trust/certificate.py @@ -50,6 +50,9 @@ from ..util.faults import CertExpired, CertMissingParent, CertNotSignedByParent from ..util.sfalogging import logger +# this tends to generate quite some logs for little or no value +debug_verify_chain = False + glo_passphrase_callback = None ## @@ -301,10 +304,10 @@ def dump_string (self): class Certificate: digest = "sha256" - cert = None - issuerKey = None - issuerSubject = None - parent = None +# x509 = None +# issuerKey = None +# issuerSubject = None +# parent = None isCA = None # will be a boolean once set separator="-----parent-----" @@ -321,6 +324,12 @@ class Certificate: # @param isCA If !=None, set whether this cert is for a CA def __init__(self, lifeDays=1825, create=False, subject=None, string=None, filename=None, isCA=None): + # these used to be defined in the class ! + self.x509 = None + self.issuerKey = None + self.issuerSubject = None + self.parent = None + self.data = {} if create or subject: self.create(lifeDays) @@ -338,12 +347,12 @@ def __init__(self, lifeDays=1825, create=False, subject=None, string=None, filen # Create a blank X509 certificate and store it in this object. def create(self, lifeDays=1825): - self.cert = crypto.X509() + self.x509 = crypto.X509() # FIXME: Use different serial #s - self.cert.set_serial_number(3) - self.cert.gmtime_adj_notBefore(0) # 0 means now - self.cert.gmtime_adj_notAfter(lifeDays*60*60*24) # five years is default - self.cert.set_version(2) # x509v3 so it can have extensions + self.x509.set_serial_number(3) + self.x509.gmtime_adj_notBefore(0) # 0 means now + self.x509.gmtime_adj_notAfter(lifeDays*60*60*24) # five years is default + self.x509.set_version(2) # x509v3 so it can have extensions ## @@ -351,7 +360,7 @@ def create(self, lifeDays=1825): # certificate object. def load_from_pyopenssl_x509(self, x509): - self.cert = x509 + self.x509 = x509 ## # Load the certificate from a string @@ -386,9 +395,9 @@ def load_from_string(self, string): else: parts = string.split(Certificate.separator, 1) - self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0]) + self.x509 = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0]) - if self.cert is None: + if self.x509 is None: logger.warn("Loaded from string but cert is None: %s" % string) # if there are more certs, then create a parent and let the parent load @@ -412,10 +421,10 @@ def load_from_file(self, filename): # @param save_parents If save_parents==True, then also save the parent certificates. def save_to_string(self, save_parents=True): - if self.cert is None: + if self.x509 is None: logger.warn("None cert in certificate.save_to_string") return "" - string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.x509) if save_parents and self.parent: string = string + self.parent.save_to_string(save_parents) return string @@ -467,7 +476,7 @@ def set_issuer(self, key, subject=None, cert=None): self.issuerReq = req if cert: # if a cert was supplied, then get the subject from the cert - subject = cert.cert.get_subject() + subject = cert.x509.get_subject() assert(subject) self.issuerSubject = subject @@ -475,7 +484,7 @@ def set_issuer(self, key, subject=None, cert=None): # Get the issuer name def get_issuer(self, which="CN"): - x = self.cert.get_issuer() + x = self.x509.get_issuer() return getattr(x, which) ## @@ -489,21 +498,43 @@ def set_subject(self, name): setattr(subj, key, name[key]) else: setattr(subj, "CN", name) - self.cert.set_subject(subj) + self.x509.set_subject(subj) ## # Get the subject name of the certificate def get_subject(self, which="CN"): - x = self.cert.get_subject() + x = self.x509.get_subject() return getattr(x, which) ## # Get a pretty-print subject name of the certificate - - def get_printable_subject(self): - x = self.cert.get_subject() - return "[ OU: %s, CN: %s, SubjectAltName: %s ]" % (getattr(x, "OU"), getattr(x, "CN"), self.get_data()) + # let's try to make this a little more usable as is makes logs hairy + # FIXME: Consider adding URN and UID back for GENI? + pretty_fields = ['email'] + def filter_chunk(self, chunk): + for field in self.pretty_fields: + if field in chunk: + return " "+chunk + + def pretty_cert(self): + message = "[Cert." + x = self.x509.get_subject() + ou = getattr(x, "OU") + if ou: message += " OU: {}".format(ou) + cn = getattr(x, "CN") + if cn: message += " CN: {}".format(cn) + data = self.get_data(field='subjectAltName') + if data: + message += " SubjectAltName:" + counter = 0 + filtered = [self.filter_chunk(chunk) for chunk in data.split()] + message += " ".join( [f for f in filtered if f]) + omitted = len ([f for f in filtered if not f]) + if omitted: + message += "..+{} omitted".format(omitted) + message += "]" + return message ## # Get the public key of the certificate. @@ -512,7 +543,7 @@ def get_printable_subject(self): def set_pubkey(self, key): assert(isinstance(key, Keypair)) - self.cert.set_pubkey(key.get_openssl_pkey()) + self.x509.set_pubkey(key.get_openssl_pkey()) ## # Get the public key of the certificate. @@ -521,7 +552,7 @@ def set_pubkey(self, key): def get_pubkey(self): m2x509 = X509.load_cert_string(self.save_to_string()) pkey = Keypair() - pkey.key = self.cert.get_pubkey() + pkey.key = self.x509.get_pubkey() pkey.m2key = m2x509.get_pubkey() return pkey @@ -576,7 +607,7 @@ def add_extension(self, name, critical, value): # raise "Cannot add extension %s which had val %s with new val %s" % (name, oldExtVal, value) ext = crypto.X509Extension (name, critical, value) - self.cert.add_extensions([ext]) + self.x509.add_extensions([ext]) ## # Get an X509 extension from the certificate @@ -633,11 +664,11 @@ def get_data(self, field='subjectAltName'): def sign(self): logger.debug('certificate.sign') - assert self.cert != None + assert self.x509 != None assert self.issuerSubject != None assert self.issuerKey != None - self.cert.set_issuer(self.issuerSubject) - self.cert.sign(self.issuerKey.get_openssl_pkey(), self.digest) + self.x509.set_issuer(self.issuerSubject) + self.x509.sign(self.issuerKey.get_openssl_pkey(), self.digest) ## # Verify the authenticity of a certificate. @@ -653,7 +684,7 @@ def verify(self, pkey): # XXX alternatively, if openssl has been patched, do the much simpler: # try: - # self.cert.verify(pkey.get_openssl_key()) + # self.x509.verify(pkey.get_openssl_key()) # return 1 # except: # return 0 @@ -717,37 +748,45 @@ def verify_chain(self, trusted_certs = None): # until a certificate is found that is signed by a trusted root. # verify expiration time - if self.cert.has_expired(): - logger.debug("verify_chain: NO, Certificate %s has expired" % self.get_printable_subject()) - raise CertExpired(self.get_printable_subject(), "client cert") + if self.x509.has_expired(): + if debug_verify_chain: + logger.debug("verify_chain: NO, Certificate %s has expired" % self.pretty_cert()) + raise CertExpired(self.pretty_cert(), "client cert") # if this cert is signed by a trusted_cert, then we are set for trusted_cert in trusted_certs: if self.is_signed_by_cert(trusted_cert): # verify expiration of trusted_cert ? - if not trusted_cert.cert.has_expired(): - logger.debug("verify_chain: YES. Cert %s signed by trusted cert %s"%( - self.get_printable_subject(), trusted_cert.get_printable_subject())) + if not trusted_cert.x509.has_expired(): + if debug_verify_chain: + logger.debug("verify_chain: YES. Cert %s signed by trusted cert %s"%( + self.pretty_cert(), trusted_cert.pretty_cert())) return trusted_cert else: - logger.debug("verify_chain: NO. Cert %s is signed by trusted_cert %s, but that signer is expired..."%( - self.get_printable_subject(),trusted_cert.get_printable_subject())) - raise CertExpired(self.get_printable_subject()," signer trusted_cert %s"%trusted_cert.get_printable_subject()) + if debug_verify_chain: + logger.debug("verify_chain: NO. Cert %s is signed by trusted_cert %s, but that signer is expired..."%( + self.pretty_cert(),trusted_cert.pretty_cert())) + raise CertExpired(self.pretty_cert()," signer trusted_cert %s"%trusted_cert.pretty_cert()) # if there is no parent, then no way to verify the chain if not self.parent: - logger.debug("verify_chain: NO. %s has no parent and issuer %s is not in %d trusted roots"%(self.get_printable_subject(), self.get_issuer(), len(trusted_certs))) - raise CertMissingParent(self.get_printable_subject() + ": Issuer %s is not one of the %d trusted roots, and cert has no parent." % (self.get_issuer(), len(trusted_certs))) + if debug_verify_chain: + logger.debug("verify_chain: NO. %s has no parent and issuer %s is not in %d trusted roots"%\ + (self.pretty_cert(), self.get_issuer(), len(trusted_certs))) + raise CertMissingParent(self.pretty_cert() + \ + ": Issuer %s is not one of the %d trusted roots, and cert has no parent." %\ + (self.get_issuer(), len(trusted_certs))) # if it wasn't signed by the parent... if not self.is_signed_by_cert(self.parent): - logger.debug("verify_chain: NO. %s is not signed by parent %s, but by %s"%\ - (self.get_printable_subject(), - self.parent.get_printable_subject(), + if debug_verify_chain: + logger.debug("verify_chain: NO. %s is not signed by parent %s, but by %s"%\ + (self.pretty_cert(), + self.parent.pretty_cert(), self.get_issuer())) raise CertNotSignedByParent("%s: Parent %s, issuer %s"\ - % (self.get_printable_subject(), - self.parent.get_printable_subject(), + % (self.pretty_cert(), + self.parent.pretty_cert(), self.get_issuer())) # Confirm that the parent is a CA. Only CAs can be trusted as @@ -758,13 +797,14 @@ def verify_chain(self, trusted_certs = None): # extension and hope there are no other basicConstraints if not self.parent.isCA and not (self.parent.get_extension('basicConstraints') == 'CA:TRUE'): logger.warn("verify_chain: cert %s's parent %s is not a CA" % \ - (self.get_printable_subject(), self.parent.get_printable_subject())) - raise CertNotSignedByParent("%s: Parent %s not a CA" % (self.get_printable_subject(), - self.parent.get_printable_subject())) + (self.pretty_cert(), self.parent.pretty_cert())) + raise CertNotSignedByParent("%s: Parent %s not a CA" % (self.pretty_cert(), + self.parent.pretty_cert())) # if the parent isn't verified... - logger.debug("verify_chain: .. %s, -> verifying parent %s"%\ - (self.get_printable_subject(),self.parent.get_printable_subject())) + if debug_verify_chain: + logger.debug("verify_chain: .. %s, -> verifying parent %s"%\ + (self.pretty_cert(),self.parent.pretty_cert())) self.parent.verify_chain(trusted_certs) return @@ -772,9 +812,9 @@ def verify_chain(self, trusted_certs = None): ### more introspection def get_extensions(self): # pyOpenSSL does not have a way to get extensions - triples=[] + triples = [] m2x509 = X509.load_cert_string(self.save_to_string()) - nb_extensions=m2x509.get_ext_count() + nb_extensions = m2x509.get_ext_count() logger.debug("X509 had %d extensions"%nb_extensions) for i in range(nb_extensions): ext=m2x509.get_ext_at(i) @@ -785,7 +825,7 @@ def get_data_names(self): return self.data.keys() def get_all_datas (self): - triples=self.get_extensions() + triples = self.get_extensions() for name in self.get_data_names(): triples.append( (name,self.get_data(name),'data',) ) return triples @@ -799,14 +839,14 @@ def dump (self, *args, **kwargs): def dump_string (self,show_extensions=False): result = "" - result += "CERTIFICATE for %s\n"%self.get_printable_subject() + result += "CERTIFICATE for %s\n"%self.pretty_cert() result += "Issued by %s\n"%self.get_issuer() filename=self.get_filename() if filename: result += "Filename %s\n"%filename if show_extensions: - all_datas=self.get_all_datas() + all_datas = self.get_all_datas() result += " has %d extensions/data attached"%len(all_datas) - for (n,v,c) in all_datas: + for (n, v, c) in all_datas: if c=='data': result += " data: %s=%s\n"%(n,v) else: diff --git a/src/gcf/sfa/trust/credential.py b/src/gcf/sfa/trust/credential.py index 31ebf366..05cd00a6 100644 --- a/src/gcf/sfa/trust/credential.py +++ b/src/gcf/sfa/trust/credential.py @@ -28,7 +28,8 @@ from __future__ import absolute_import -import os +import os, os.path +import subprocess from types import StringTypes import datetime from StringIO import StringIO @@ -46,13 +47,12 @@ from ..util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent from ..util.sfalogging import logger -from ..util.sfatime import utcparse +from ..util.sfatime import utcparse, SFATIME_FORMAT from ..util.xrn import urn_to_hrn, hrn_authfor_hrn -from .credential_legacy import CredentialLegacy from .rights import Right, Rights, determine_rights from .gid import GID -# 2 weeks, in seconds +# 31 days, in seconds DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31 @@ -218,14 +218,13 @@ def decode(self): def encode(self): self.xml = signature_template % (self.get_refid(), self.get_refid()) - ## # A credential provides a caller gid with privileges to an object gid. # A signed credential is signed by the object's authority. # -# Credentials are encoded in one of two ways. The legacy style places -# it in the subjectAltName of an X509 certificate. The new credentials -# are placed in signed XML. +# Credentials are encoded in one of two ways. +# The legacy style (now unsupported) places it in the subjectAltName of an X509 certificate. +# The new credentials are placed in signed XML. # # WARNING: # In general, a signed credential obtained externally should @@ -263,7 +262,7 @@ class Credential(object): # @param string If string!=None, load the credential from the string # @param filename If filename!=None, load the credential from the file # FIXME: create and subject are ignored! - def __init__(self, create=False, subject=None, string=None, filename=None): + def __init__(self, create=False, subject=None, string=None, filename=None, cred=None): self.gidCaller = None self.gidObject = None self.expiration = None @@ -275,19 +274,29 @@ def __init__(self, create=False, subject=None, string=None, filename=None): self.signature = None self.xml = None self.refid = None - self.legacy = None self.cred_type = Credential.SFA_CREDENTIAL_TYPE + self.version = None + + if cred: + if isinstance(cred, StringTypes): + string = cred + self.cred_type = Credential.SFA_CREDENTIAL_TYPE + self.version = '3' + elif isinstance(cred, dict): + string = cred['geni_value'] + self.cred_type = cred['geni_type'] + self.version = cred['geni_version'] - # Check if this is a legacy credential, translate it if so if string or filename: if string: str = string elif filename: str = file(filename).read() - if str.strip().startswith("-----"): - self.legacy = CredentialLegacy(False,string=str) - self.translate_legacy(str) + # if this is a legacy credential, write error and bail out + if isinstance (str, StringTypes) and str.strip().startswith("-----"): + logger.error("Legacy credentials not supported any more - giving up with %s..."%str[:10]) + return else: self.xml = str self.decode() @@ -310,15 +319,23 @@ def get_subject(self): self.decode() return self.gidObject.get_subject() + def pretty_subject(self): + subject = "" + if not self.gidObject: + self.decode() + if self.gidObject: + subject = self.gidObject.pretty_cert() + return subject + # sounds like this should be __repr__ instead ?? - def get_summary_tostring(self): + def pretty_cred(self): if not self.gidObject: self.decode() - obj = self.gidObject.get_printable_subject() - caller = self.gidCaller.get_printable_subject() + obj = self.gidObject.pretty_cert() + caller = self.gidCaller.pretty_cert() exp = self.get_expiration() # Summarize the rights too? The issuer? - return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp) + return "[Cred. for {caller} rights on {obj} until {exp} ]".format(**locals()) def get_signature(self): if not self.signature: @@ -329,24 +346,6 @@ def set_signature(self, sig): self.signature = sig - ## - # Translate a legacy credential into a new one - # - # @param String of the legacy credential - - def translate_legacy(self, str): - legacy = CredentialLegacy(False,string=str) - self.gidCaller = legacy.get_gid_caller() - self.gidObject = legacy.get_gid_object() - lifetime = legacy.get_lifetime() - if not lifetime: - self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) - else: - self.set_expiration(int(lifetime)) - self.lifeTime = legacy.get_lifetime() - self.set_privileges(legacy.get_privileges()) - self.get_privileges().delegate_all_privileges(legacy.get_delegate()) - ## # Need the issuer's private key and name # @param key Keypair object containing the private key of the issuer @@ -401,15 +400,11 @@ def get_gid_object(self): # Expiration: an absolute UTC time of expiration (as either an int or string or datetime) # def set_expiration(self, expiration): - if isinstance(expiration, (int, float)): - self.expiration = datetime.datetime.fromtimestamp(expiration) - elif isinstance (expiration, datetime.datetime): - self.expiration = expiration - elif isinstance (expiration, StringTypes): - self.expiration = utcparse (expiration) + expiration_datetime = utcparse (expiration) + if expiration_datetime is not None: + self.expiration = expiration_datetime else: - logger.error ("unexpected input type in Credential.set_expiration") - + logger.error ("unexpected input %s in Credential.set_expiration"%expiration) ## # get the lifetime of the credential (always in datetime format) @@ -420,11 +415,6 @@ def get_expiration(self): # at this point self.expiration is normalized as a datetime - DON'T call utcparse again return self.expiration - ## - # For legacy sake - def get_lifetime(self): - return self.get_expiration() - ## # set the privileges # @@ -472,20 +462,20 @@ def encode(self): doc = Document() signed_cred = doc.createElement("signed-credential") -# Declare namespaces -# Note that credential/policy.xsd are really the PG schemas -# in a PL namespace. -# Note that delegation of credentials between the 2 only really works -# cause those schemas are identical. -# Also note these PG schemas talk about PG tickets and CM policies. + # Declare namespaces + # Note that credential/policy.xsd are really the PG schemas + # in a PL namespace. + # Note that delegation of credentials between the 2 only really works + # cause those schemas are identical. + # Also note these PG schemas talk about PG tickets and CM policies. signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") # FIXME: See v2 schema at www.geni.net/resources/credential/2/credential.xsd signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.planet-lab.org/resources/sfa/credential.xsd") 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") -# PG says for those last 2: -# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") -# 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") + # PG says for those last 2: + # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") + # 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") doc.appendChild(signed_cred) @@ -501,12 +491,13 @@ def encode(self): append_sub(doc, cred, "target_urn", self.gidObject.get_urn()) append_sub(doc, cred, "uuid", "") if not self.expiration: + logger.debug("Creating credential valid for %s s"%DEFAULT_CREDENTIAL_LIFETIME) self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) self.expiration = self.expiration.replace(microsecond=0) if self.expiration.tzinfo is not None and self.expiration.tzinfo.utcoffset(self.expiration) is not None: # TZ aware. Make sure it is UTC self.expiration = self.expiration.astimezone(tz.tzutc()) - append_sub(doc, cred, "expires", self.expiration.strftime('%Y-%m-%dT%H:%M:%SZ')) # RFC3339 + append_sub(doc, cred, "expires", self.expiration.strftime(SFATIME_FORMAT)) privileges = doc.createElement("privileges") cred.appendChild(privileges) @@ -528,10 +519,10 @@ def encode(self): # and we need to include those again here or else their signature # no longer matches on the credential. # We expect three of these, but here we copy them all: -# signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") -# and from PG (PL is equivalent, as shown above): -# signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") -# 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") + # signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + # and from PG (PL is equivalent, as shown above): + # signed_cred.setAttribute("xsi:noNamespaceSchemaLocation", "http://www.protogeni.net/resources/credential/credential.xsd") + # 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") # HOWEVER! # PL now also declares these, with different URLs, so @@ -561,7 +552,8 @@ def encode(self): # Below throws InUse exception if we forgot to clone the attribute first oldAttr = signed_cred.setAttributeNode(attr.cloneNode(True)) if oldAttr and oldAttr.value != attr.value: - 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) + 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) logger.warn(msg) #raise CredentialNotVerifiable("Can't encode new valid delegated credential: %s" % msg) @@ -706,10 +698,6 @@ def sign(self): self.xml = signed - # This is no longer a legacy credential - if self.legacy: - self.legacy = None - # Update signatures self.decode() @@ -722,6 +710,12 @@ def sign(self): def decode(self): if not self.xml: return + + doc = None + try: + doc = parseString(self.xml) + except ExpatError,e: + raise CredentialNotVerifiable("Malformed credential") doc = parseString(self.xml) sigs = [] signed_cred = doc.getElementsByTagName("signed-credential") @@ -744,31 +738,9 @@ def decode(self): self.set_refid(cred.getAttribute("xml:id")) self.set_expiration(utcparse(getTextNode(cred, "expires"))) + self.gidCaller = GID(string=getTextNode(cred, "owner_gid")) + self.gidObject = GID(string=getTextNode(cred, "target_gid")) -# import traceback -# stack = traceback.extract_stack() - - og = getTextNode(cred, "owner_gid") - # ABAC creds will have this be None and use this method -# if og is None: -# found = False -# for frame in stack: -# if 'super(ABACCredential, self).decode()' in frame: -# found = True -# break -# if not found: -# raise CredentialNotVerifiable("Malformed XML: No owner_gid found") - self.gidCaller = GID(string=og) - tg = getTextNode(cred, "target_gid") -# if tg is None: -# found = False -# for frame in stack: -# if 'super(ABACCredential, self).decode()' in frame: -# found = True -# break -# if not found: -# raise CredentialNotVerifiable("Malformed XML: No target_gid found") - self.gidObject = GID(string=tg) # Process privileges rlist = Rights() @@ -846,14 +818,14 @@ def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): self.decode() # validate against RelaxNG schema - if HAVELXML and not self.legacy: + if HAVELXML: if schema and os.path.exists(schema): tree = etree.parse(StringIO(self.xml)) schema_doc = etree.parse(schema) xmlschema = etree.XMLSchema(schema_doc) if not xmlschema.validate(tree): error = xmlschema.error_log.last_error - message = "%s: %s (line %s)" % (self.get_summary_tostring(), error.message, error.line) + message = "%s: %s (line %s)" % (self.pretty_cred(), error.message, error.line) raise CredentialNotVerifiable(message) if trusted_certs_required and trusted_certs is None: @@ -875,23 +847,14 @@ def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): logger.error("Failed to load trusted cert from %s: %r", f, exc) trusted_certs = ok_trusted_certs - # Use legacy verification if this is a legacy credential - if self.legacy: - self.legacy.verify_chain(trusted_cert_objects) - if self.legacy.client_gid: - self.legacy.client_gid.verify_chain(trusted_cert_objects) - if self.legacy.object_gid: - self.legacy.object_gid.verify_chain(trusted_cert_objects) - return True - # make sure it is not expired if self.get_expiration() < datetime.datetime.utcnow(): - raise CredentialNotVerifiable("Credential %s expired at %s" % (self.get_summary_tostring(), self.expiration.isoformat())) + raise CredentialNotVerifiable("Credential %s expired at %s" % \ + (self.pretty_cred(), + self.expiration.strftime(SFATIME_FORMAT))) # Verify the signatures filename = self.save_to_random_tmp_file() - if trusted_certs is not None: - cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs]) # If caller explicitly passed in None that means skip cert chain validation. # - Strange and not typical @@ -914,11 +877,26 @@ def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): if trusted_certs is None: break -# print "Doing %s --verify --node-id '%s' %s %s 2>&1" % \ -# (self.xmlsec_path, ref, cert_args, filename) - verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \ - % (self.xmlsec_path, ref, cert_args, filename)).read() - if not verified.strip().startswith("OK"): + # Thierry - Jan 2015 + # Up to fedora20 we used os.popen and checked that the output begins with OK + # Turns out, with fedora21, there is extra input before this 'OK' thing + # Looks like we're better off just using the exit code - that's what it is made for + command = [ self.xmlsec_path, '--verify', '--node-id', ref ] + for trusted in trusted_certs: + command += ["--trusted-pem", trusted ] + command += [ filename ] + logger.debug("Running " + " ".join(command)) + try: + verified = subprocess.check_output(command, stderr=subprocess.STDOUT) + logger.debug("xmlsec command returned {}".format(verified)) + if "OK\n" not in verified: + logger.warning("WARNING: xmlsec1 seemed to return fine but without a OK in its output") + except subprocess.CalledProcessError as e: + verified = e.output + + # verified = os.popen('%s --verify --node-id "%s" %s %s 2>&1' \ + # % (self.xmlsec_path, ref, cert_args, filename)).read() + # if not verified.strip().startswith("OK"): # xmlsec errors have a msg= which is the interesting bit. mstart = verified.find("msg=") msg = "" @@ -926,7 +904,9 @@ def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): mstart = mstart + 4 mend = verified.find('\\', mstart) msg = verified[mstart:mend] - raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s %s" % (self.get_summary_tostring(), ref, msg, verified.strip())) + logger.warning("Credential.verify - failed - xmlsec1 returned {}".format(verified.strip())) + raise CredentialNotVerifiable("xmlsec1 error verifying cred %s using Signature ID %s: %s" % \ + (self.pretty_cred(), ref, msg)) os.remove(filename) # Verify the parents (delegation) @@ -1007,13 +987,14 @@ def verify_issuer(self, trusted_gids): if trusted_gids and len(trusted_gids) > 0: root_cred_signer.verify_chain(trusted_gids) else: - logger.debug("No trusted gids. Cannot verify that cred signer is signed by a trusted authority. Skipping that check.") + logger.debug("Cannot verify that cred signer is signed by a trusted authority. " + "No trusted gids. Skipping that check.") # See if the signer is an authority over the domain of the target. # There are multiple types of authority - accept them all here # Maybe should be (hrn, type) = urn_to_hrn(root_cred_signer.get_urn()) root_cred_signer_type = root_cred_signer.get_type() - if (root_cred_signer_type.find('authority') == 0): + if root_cred_signer_type.find('authority') == 0: #logger.debug('Cred signer is an authority') # signer is an authority, see if target is in authority's domain signerhrn = root_cred_signer.get_hrn() @@ -1028,8 +1009,11 @@ def verify_issuer(self, trusted_gids): # Give up, credential does not pass issuer verification - 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())) - + raise CredentialNotVerifiable( + "Could not verify credential owned by {} for object {}. " + "Cred signer {} not the trusted authority for Cred target {}" + .format(self.gidCaller.get_hrn(), self.gidObject.get_hrn(), + root_cred_signer.get_hrn(), root_target_gid.get_hrn())) ## # -- For Delegates (credentials with parents) verify that: @@ -1042,23 +1026,46 @@ def verify_parent(self, parent_cred): # make sure the rights given to the child are a subset of the # parents rights (and check delegate bits) if not parent_cred.get_privileges().is_superset(self.get_privileges()): - raise ChildRightsNotSubsetOfParent(("Parent cred ref %s rights " % parent_cred.get_refid()) + - self.parent.get_privileges().save_to_string() + (" not superset of delegated cred %s ref %s rights " % (self.get_summary_tostring(), self.get_refid())) + - self.get_privileges().save_to_string()) + message = ( + "Parent cred {} (ref {}) rights {} " + " not superset of delegated cred {} (ref {}) rights {}" + .format(parent_cred.pretty_cred(),parent_cred.get_refid(), + parent_cred.get_privileges().pretty_rights(), + self.pretty_cred(), self.get_refid(), + self.get_privileges().pretty_rights())) + logger.error(message) + logger.error("parent details {}".format(parent_cred.get_privileges().save_to_string())) + logger.error("self details {}".format(self.get_privileges().save_to_string())) + raise ChildRightsNotSubsetOfParent(message) # make sure my target gid is the same as the parent's if not parent_cred.get_gid_object().save_to_string() == \ self.get_gid_object().save_to_string(): - raise CredentialNotVerifiable("Delegated cred %s: Target gid not equal between parent and child. Parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + message = ( + "Delegated cred {}: Target gid not equal between parent and child. Parent {}" + .format(self.pretty_cred(), parent_cred.pretty_cred())) + logger.error(message) + logger.error("parent details {}".format(parent_cred.save_to_string())) + logger.error("self details {}".format(self.save_to_string())) + raise CredentialNotVerifiable(message) # make sure my expiry time is <= my parent's if not parent_cred.get_expiration() >= self.get_expiration(): - raise CredentialNotVerifiable("Delegated credential %s expires after parent %s" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + raise CredentialNotVerifiable( + "Delegated credential {} expires after parent {}" + .format(self.pretty_cred(), parent_cred.pretty_cred())) # make sure my signer is the parent's caller if not parent_cred.get_gid_caller().save_to_string(False) == \ self.get_signature().get_issuer_gid().save_to_string(False): - raise CredentialNotVerifiable("Delegated credential %s not signed by parent %s's caller" % (self.get_summary_tostring(), parent_cred.get_summary_tostring())) + message = "Delegated credential {} not signed by parent {}'s caller"\ + .format(self.pretty_cred(), parent_cred.pretty_cred()) + logger.error(message) + logger.error("compare1 parent {}".format(parent_cred.get_gid_caller().pretty_cred())) + logger.error("compare1 parent details {}".format(parent_cred.get_gid_caller().save_to_string())) + logger.error("compare2 self {}".format(self.get_signature().get_issuer_gid().pretty_cred())) + logger.error("compare2 self details {}".format(self.get_signature().get_issuer_gid().save_to_string())) + raise CredentialNotVerifiable(message) # Recurse if parent_cred.parent: @@ -1099,6 +1106,32 @@ def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile): def get_filename(self): return getattr(self,'filename',None) + def actual_caller_hrn (self): + """a helper method used by some API calls like e.g. Allocate + to try and find out who really is the original caller + + This admittedly is a bit of a hack, please USE IN LAST RESORT + + This code uses a heuristic to identify a delegated credential + + A first known restriction if for traffic that gets through a slice manager + in this case the hrn reported is the one from the last SM in the call graph + which is not at all what is meant here""" + + caller_hrn = self.get_gid_caller().get_hrn() + issuer_hrn = self.get_signature().get_issuer_gid().get_hrn() + subject_hrn = self.get_gid_object().get_hrn() + # if we find that the caller_hrn is an immediate descendant of the issuer, then + # this seems to be a 'regular' credential + if caller_hrn.startswith(issuer_hrn): + actual_caller_hrn=caller_hrn + # else this looks like a delegated credential, and the real caller is the issuer + else: + actual_caller_hrn=issuer_hrn + logger.info("actual_caller_hrn: caller_hrn=%s, issuer_hrn=%s, returning %s" + %(caller_hrn,issuer_hrn,actual_caller_hrn)) + return actual_caller_hrn + ## # Dump the contents of a credential to stdout in human-readable format # @@ -1106,13 +1139,17 @@ def get_filename(self): def dump (self, *args, **kwargs): print self.dump_string(*args, **kwargs) - + # SFA code ignores show_xml and disables printing the cred xml def dump_string(self, dump_parents=False, show_xml=False): result="" - result += "CREDENTIAL %s\n" % self.get_subject() + result += "CREDENTIAL %s\n" % self.pretty_subject() filename=self.get_filename() if filename: result += "Filename %s\n"%filename - result += " privs: %s\n" % self.get_privileges().save_to_string() + privileges = self.get_privileges() + if privileges: + result += " privs: %s\n" % privileges.save_to_string() + else: + result += " privs: \n" gidCaller = self.get_gid_caller() if gidCaller: result += " gidCaller:\n" @@ -1123,7 +1160,7 @@ def dump_string(self, dump_parents=False, show_xml=False): result += self.get_signature().get_issuer_gid().dump_string(8, dump_parents) if self.expiration: - result += " expiration: " + self.expiration.isoformat() + "\n" + result += " expiration: " + self.expiration.strftime(SFATIME_FORMAT) + "\n" gidObject = self.get_gid_object() if gidObject: diff --git a/src/gcf/sfa/trust/credential_legacy.py b/src/gcf/sfa/trust/credential_legacy.py deleted file mode 100644 index f3416519..00000000 --- a/src/gcf/sfa/trust/credential_legacy.py +++ /dev/null @@ -1,271 +0,0 @@ -#---------------------------------------------------------------------- -# Copyright (c) 2008 Board of Trustees, Princeton University -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and/or hardware specification (the "Work") to -# deal in the Work without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, -# and/or sell copies of the Work, and to permit persons to whom the Work -# is furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Work. -# -# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS -# IN THE WORK. -#---------------------------------------------------------------------- -## -# Implements SFA Credentials -# -# Credentials are layered on top of certificates, and are essentially a -# certificate that stores a tuple of parameters. -## - -from __future__ import absolute_import - -import xmlrpclib - -from ..util.faults import MissingDelegateBit, ChildRightsNotSubsetOfParent -from .certificate import Certificate -from .gid import GID - -## -# Credential is a tuple: -# (GIDCaller, GIDObject, LifeTime, Privileges, Delegate) -# -# These fields are encoded using xmlrpc into the subjectAltName field of the -# x509 certificate. Note: Call encode() once the fields have been filled in -# to perform this encoding. - -class CredentialLegacy(Certificate): - gidCaller = None - gidObject = None - lifeTime = None - privileges = None - delegate = False - - ## - # Create a Credential object - # - # @param create If true, create a blank x509 certificate - # @param subject If subject!=None, create an x509 cert with the subject name - # @param string If string!=None, load the credential from the string - # @param filename If filename!=None, load the credential from the file - - def __init__(self, create=False, subject=None, string=None, filename=None): - Certificate.__init__(self, create, subject, string, filename) - - ## - # set the GID of the caller - # - # @param gid GID object of the caller - - def set_gid_caller(self, gid): - self.gidCaller = gid - # gid origin caller is the caller's gid by default - self.gidOriginCaller = gid - - ## - # get the GID of the object - - def get_gid_caller(self): - if not self.gidCaller: - self.decode() - return self.gidCaller - - ## - # set the GID of the object - # - # @param gid GID object of the object - - def set_gid_object(self, gid): - self.gidObject = gid - - ## - # get the GID of the object - - def get_gid_object(self): - if not self.gidObject: - self.decode() - return self.gidObject - - ## - # set the lifetime of this credential - # - # @param lifetime lifetime of credential - - def set_lifetime(self, lifeTime): - self.lifeTime = lifeTime - - ## - # get the lifetime of the credential - - def get_lifetime(self): - if not self.lifeTime: - self.decode() - return self.lifeTime - - ## - # set the delegate bit - # - # @param delegate boolean (True or False) - - def set_delegate(self, delegate): - self.delegate = delegate - - ## - # get the delegate bit - - def get_delegate(self): - if not self.delegate: - self.decode() - return self.delegate - - ## - # set the privileges - # - # @param privs either a comma-separated list of privileges of a Rights object - - def set_privileges(self, privs): - if isinstance(privs, str): - self.privileges = Rights(string = privs) - else: - self.privileges = privs - - ## - # return the privileges as a Rights object - - def get_privileges(self): - if not self.privileges: - self.decode() - return self.privileges - - ## - # determine whether the credential allows a particular operation to be - # performed - # - # @param op_name string specifying name of operation ("lookup", "update", etc) - - def can_perform(self, op_name): - rights = self.get_privileges() - if not rights: - return False - return rights.can_perform(op_name) - - ## - # Encode the attributes of the credential into a string and store that - # string in the alt-subject-name field of the X509 object. This should be - # done immediately before signing the credential. - - def encode(self): - dict = {"gidCaller": None, - "gidObject": None, - "lifeTime": self.lifeTime, - "privileges": None, - "delegate": self.delegate} - if self.gidCaller: - dict["gidCaller"] = self.gidCaller.save_to_string(save_parents=True) - if self.gidObject: - dict["gidObject"] = self.gidObject.save_to_string(save_parents=True) - if self.privileges: - dict["privileges"] = self.privileges.save_to_string() - str = xmlrpclib.dumps((dict,), allow_none=True) - self.set_data('URI:http://' + str) - - ## - # Retrieve the attributes of the credential from the alt-subject-name field - # of the X509 certificate. This is automatically done by the various - # get_* methods of this class and should not need to be called explicitly. - - def decode(self): - data = self.get_data().lstrip('URI:http://') - - if data: - dict = xmlrpclib.loads(data)[0][0] - else: - dict = {} - - self.lifeTime = dict.get("lifeTime", None) - self.delegate = dict.get("delegate", None) - - privStr = dict.get("privileges", None) - if privStr: - self.privileges = Rights(string = privStr) - else: - self.privileges = None - - gidCallerStr = dict.get("gidCaller", None) - if gidCallerStr: - self.gidCaller = GID(string=gidCallerStr) - else: - self.gidCaller = None - - gidObjectStr = dict.get("gidObject", None) - if gidObjectStr: - self.gidObject = GID(string=gidObjectStr) - else: - self.gidObject = None - - ## - # Verify that a chain of credentials is valid (see cert.py:verify). In - # addition to the checks for ordinary certificates, verification also - # ensures that the delegate bit was set by each parent in the chain. If - # a delegate bit was not set, then an exception is thrown. - # - # Each credential must be a subset of the rights of the parent. - - def verify_chain(self, trusted_certs = None): - # do the normal certificate verification stuff - Certificate.verify_chain(self, trusted_certs) - - if self.parent: - # make sure the parent delegated rights to the child - if not self.parent.get_delegate(): - raise MissingDelegateBit(self.parent.get_subject()) - - # make sure the rights given to the child are a subset of the - # parents rights - if not self.parent.get_privileges().is_superset(self.get_privileges()): - raise ChildRightsNotSubsetOfParent(self.get_subject() - + " " + self.parent.get_privileges().save_to_string() - + " " + self.get_privileges().save_to_string()) - - return - - ## - # Dump the contents of a credential to stdout in human-readable format - # - # @param dump_parents If true, also dump the parent certificates - - def dump(self, *args, **kwargs): - print self.dump_string(*args,**kwargs) - - def dump_string(self, dump_parents=False): - result="" - result += "CREDENTIAL %s\n" % self.get_subject() - - result += " privs: %s\n" % self.get_privileges().save_to_string() - - gidCaller = self.get_gid_caller() - if gidCaller: - result += " gidCaller:\n" - gidCaller.dump(8, dump_parents) - - gidObject = self.get_gid_object() - if gidObject: - result += " gidObject:\n" - result += gidObject.dump_string(8, dump_parents) - - result += " delegate: %s" % self.get_delegate() - - if self.parent and dump_parents: - result += "PARENT\n" - result += self.parent.dump_string(dump_parents) - - return result diff --git a/src/gcf/sfa/trust/gid.py b/src/gcf/sfa/trust/gid.py index 7a740a94..da86a602 100644 --- a/src/gcf/sfa/trust/gid.py +++ b/src/gcf/sfa/trust/gid.py @@ -76,7 +76,9 @@ class GID(Certificate): # @param filename If filename!=None, load the GID from a file # @param lifeDays life of GID in days - default is 1825==5 years # @param email Email address to put in subjectAltName - default is None - def __init__(self, create=False, subject=None, string=None, filename=None, uuid=None, hrn=None, urn=None, lifeDays=1825, email=None): + + def __init__(self, create=False, subject=None, string=None, filename=None, + uuid=None, hrn=None, urn=None, lifeDays=1825, email=None): self.uuid = None self.hrn = None self.urn = None @@ -93,7 +95,9 @@ def __init__(self, create=False, subject=None, string=None, filename=None, uuid= if urn: self.urn = urn self.hrn, type = urn_to_hrn(urn) + if email: + logger.debug("Creating GID for subject using email: %s" % email) self.set_email(email) def set_uuid(self, uuid): @@ -232,12 +236,16 @@ def verify_chain(self, trusted_certs = None): if self.parent: # make sure the parent's hrn is a prefix of the child's hrn if not hrn_authfor_hrn(self.parent.get_hrn(), self.get_hrn()): - raise GidParentHrn("This cert HRN %s isn't in the namespace for parent HRN %s" % (self.get_hrn(), self.parent.get_hrn())) + raise GidParentHrn( + "This cert HRN {} isn't in the namespace for parent HRN {}" + .format(self.get_hrn(), self.parent.get_hrn())) # Parent must also be an authority (of some type) to sign a GID # There are multiple types of authority - accept them all here if not self.parent.get_type().find('authority') == 0: - raise GidInvalidParentHrn("This cert %s's parent %s is not an authority (is a %s)" % (self.get_hrn(), self.parent.get_hrn(), self.parent.get_type())) + raise GidInvalidParentHrn( + "This cert {}'s parent {} is not an authority (is a %{})" + .format(self.get_hrn(), self.parent.get_hrn(), self.parent.get_type())) # Then recurse up the chain - ensure the parent is a trusted # root or is in the namespace of a trusted root @@ -251,10 +259,12 @@ def verify_chain(self, trusted_certs = None): # trusted_hrn = trusted_hrn[:trusted_hrn.rindex('.')] cur_hrn = self.get_hrn() if not hrn_authfor_hrn(trusted_hrn, cur_hrn): - raise GidParentHrn("Trusted root with HRN %s isn't a namespace authority for this cert: %s" % (trusted_hrn, cur_hrn)) + raise GidParentHrn( + "Trusted root with HRN {} isn't a namespace authority for this cert: {}" + .format(trusted_hrn, cur_hrn)) # There are multiple types of authority - accept them all here if not trusted_type.find('authority') == 0: - raise GidInvalidParentHrn("This cert %s's trusted root signer %s is not an authority (is a %s)" % (self.get_hrn(), trusted_hrn, trusted_type)) - - return + raise GidInvalidParentHrn( + "This cert {}'s trusted root signer {} is not an authority (is a {})" + .format(self.get_hrn(), trusted_hrn, trusted_type)) diff --git a/src/gcf/sfa/trust/rights.py b/src/gcf/sfa/trust/rights.py index 7fa4fa43..da15ed20 100644 --- a/src/gcf/sfa/trust/rights.py +++ b/src/gcf/sfa/trust/rights.py @@ -11,16 +11,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Work. # -# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS # IN THE WORK. #---------------------------------------------------------------------- - ## # This Module implements rights and lists of rights for the SFA. Rights # are implemented by two classes: @@ -228,7 +227,6 @@ def save_to_string(self): # @param op_name is an operation to check, for example "listslices" def can_perform(self, op_name): - for right in self.rights: if right.can_perform(op_name): return True @@ -274,3 +272,5 @@ def get_all_delegate(self): return False return True + def pretty_rights(self): + return "".format(";".join(["{}".format(r) for r in self.rights])) diff --git a/src/gcf/sfa/util/faults.py b/src/gcf/sfa/util/faults.py index 6e7a1d47..56fcafdb 100644 --- a/src/gcf/sfa/util/faults.py +++ b/src/gcf/sfa/util/faults.py @@ -35,6 +35,22 @@ def __init__(self, faultCode, faultString, extra = None): faultString += ": " + str(extra) xmlrpclib.Fault.__init__(self, faultCode, faultString) +class Forbidden(SfaFault): + def __init__(self, extra = None): + faultString = "FORBIDDEN" + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + +class BadArgs(SfaFault): + def __init__(self, extra = None): + faultString = "BADARGS" + SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) + + +class CredentialMismatch(SfaFault): + def __init__(self, extra = None): + faultString = "Credential mismatch" + SfaFault.__init__(self, GENICODE.CREDENTIAL_MISMATCH, faultString, extra) + class SfaInvalidAPIMethod(SfaFault): def __init__(self, method, interface = None, extra = None): faultString = "Invalid method " + method @@ -103,6 +119,14 @@ def __init__(self, value, extra = None): def __str__(self): return repr(self.value) +class SearchFailed(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "%s does not exist here " % self.value + SfaFault.__init__(self, GENICODE.SEARCHFAILED, faultString, extra) + def __str__(self): + return repr(self.value) + class NonExistingRecord(SfaFault): def __init__(self, value, extra = None): self.value = value @@ -307,7 +331,7 @@ class InvalidXML(SfaFault): def __init__(self, value, extra = None): self.value = value faultString = "Invalid XML Document: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) def __str__(self): return repr(self.value) @@ -319,10 +343,13 @@ def __str__(self): return repr(self.value) class CredentialNotVerifiable(SfaFault): - def __init__(self, value, extra = None): + def __init__(self, value=None, extra = None): self.value = value - faultString = "Unable to verify credential: %(value)s, " %locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + faultString = "Unable to verify credential" %locals() + if value: + faultString += ": %s" % value + faultString += ", " + SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) def __str__(self): return repr(self.value) @@ -331,4 +358,16 @@ def __init__(self, value, extra=None): self.value = value faultString = "%s cert is expired" % value SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - + +class SfatablesRejected(SfaFault): + def __init__(self, value, extra=None): + self.value =value + faultString = "%s rejected by sfatables" + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + +class UnsupportedOperation(SfaFault): + def __init__(self, value, extra=None): + self.value = value + faultString = "Unsupported operation: %s" % value + SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) + diff --git a/src/gcf/sfa/util/genicode.py b/src/gcf/sfa/util/genicode.py index e39e7c4b..b68e9714 100644 --- a/src/gcf/sfa/util/genicode.py +++ b/src/gcf/sfa/util/genicode.py @@ -43,5 +43,6 @@ BUSY=14, EXPIRED=15, INPORGRESS=16, - ALREADYEXISTS=17 + ALREADYEXISTS=17, + CREDENTIAL_MISMATCH=22 ) diff --git a/src/gcf/sfa/util/sfatime.py b/src/gcf/sfa/util/sfatime.py index 4b727e68..5f3ef62f 100644 --- a/src/gcf/sfa/util/sfatime.py +++ b/src/gcf/sfa/util/sfatime.py @@ -24,13 +24,15 @@ from __future__ import absolute_import from types import StringTypes -import dateutil.parser -import datetime import time +import datetime +import dateutil.parser +import calendar +import re from .sfalogging import logger -DATEFORMAT = "%Y-%m-%dT%H:%M:%SZ" +SFATIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # RFC3339 def utcparse(input): """ Translate a string into a time using dateutil.parser.parse but make sure it's in UTC time and strip @@ -38,16 +40,36 @@ def utcparse(input): For safety this can also handle inputs that are either timestamps, or datetimes """ + + def handle_shorthands (input): + """recognize string like +5d or +3w or +2m as + 2 days, 3 weeks or 2 months from now""" + if input.startswith('+'): + match=re.match (r"([0-9]+)([dwm])",input[1:]) + if match: + how_many=int(match.group(1)) + what=match.group(2) + if what == 'd': d=datetime.timedelta(days=how_many) + elif what == 'w': d=datetime.timedelta(weeks=how_many) + elif what == 'm': d=datetime.timedelta(weeks=4*how_many) + return datetime.datetime.utcnow()+d + # prepare the input for the checks below by # casting strings ('1327098335') to ints if isinstance(input, StringTypes): try: input = int(input) except ValueError: - pass + try: + new_input=handle_shorthands(input) + if new_input is not None: input=new_input + except: + import traceback + traceback.print_exc() + #################### here we go if isinstance (input, datetime.datetime): - logger.warn ("argument to utcparse already a datetime - doing nothing") + #logger.info ("argument to utcparse already a datetime - doing nothing") return input elif isinstance (input, StringTypes): t = dateutil.parser.parse(input) @@ -59,18 +81,38 @@ def utcparse(input): else: logger.error("Unexpected type in utcparse [%s]"%type(input)) -def datetime_to_string(input): - return datetime.datetime.strftime(input, DATEFORMAT) +def datetime_to_string(dt): + return datetime.datetime.strftime(dt, SFATIME_FORMAT) -def datetime_to_utc(input): - return time.gmtime(datetime_to_epoch(input)) +def datetime_to_utc(dt): + return time.gmtime(datetime_to_epoch(dt)) -def datetime_to_epoch(input): - return int(time.mktime(input.timetuple())) +# see https://docs.python.org/2/library/time.html +# all timestamps are in UTC so time.mktime() would be *wrong* +def datetime_to_epoch(dt): + return int(calendar.timegm(dt.timetuple())) -def adjust_datetime(input, days=0, hours=0, minutes=0, seconds=0): +def add_datetime(input, days=0, hours=0, minutes=0, seconds=0): """ Adjust the input date by the specified delta (in seconds). """ dt = utcparse(input) return dt + datetime.timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds) + +if __name__ == '__main__': + # checking consistency + print 20*'X' + print ("Should be close to zero: %s"%(datetime_to_epoch(datetime.datetime.utcnow())-time.time())) + print 20*'X' + for input in [ + '+2d', + '+3w', + '+2m', + 1401282977.575632, + 1401282977, + '1401282977', + '2014-05-28', + '2014-05-28T15:18', + '2014-05-28T15:18:30', + ]: + print "input=%20s -> parsed %s"%(input,datetime_to_string(utcparse(input))) diff --git a/src/gcf/sfa/util/xrn.py b/src/gcf/sfa/util/xrn.py index d5712564..a3ce072f 100644 --- a/src/gcf/sfa/util/xrn.py +++ b/src/gcf/sfa/util/xrn.py @@ -111,6 +111,19 @@ def urn_meaningful (urn): def urn_split (urn): return Xrn.urn_meaningful(urn).split('+') + @staticmethod + def filter_type(urns=None, type=None): + if urns is None: urns=[] + urn_list = [] + if not type: + return urns + + for urn in urns: + xrn = Xrn(xrn=urn) + if (xrn.type == type): + # Xrn is probably a urn so we can just compare types + urn_list.append(urn) + return urn_list #################### # the local fields that are kept consistent # self.urn @@ -118,7 +131,7 @@ def urn_split (urn): # self.type # self.path # provide either urn, or (hrn + type) - def __init__ (self, xrn, type=None, id=None): + def __init__ (self, xrn="", type=None, id=None): if not xrn: xrn = "" # user has specified xrn : guess if urn or hrn self.id = id @@ -127,9 +140,9 @@ def __init__ (self, xrn, type=None, id=None): if Xrn.is_urn(xrn): self.hrn=None self.urn=xrn - self.urn_to_hrn() if id: - self.hrn_to_urn() + self.urn = "%s:%s" % (self.urn, str(id)) + self.urn_to_hrn() else: self.urn=None self.hrn=xrn @@ -178,11 +191,23 @@ def set_authority(self, authority): update the authority section of an existing urn """ authority_hrn = self.get_authority_hrn() - if not authority_hrn.startswith(authority+"."): - self.hrn = authority + "." + self.hrn - self.hrn_to_urn() + if not authority_hrn.startswith(authority): + hrn = ".".join([authority,authority_hrn, self.get_leaf()]) + else: + hrn = ".".join([authority_hrn, self.get_leaf()]) + + self.hrn = hrn + self.hrn_to_urn() self._normalize() + # sliver_id_parts is list that contains the sliver's + # slice id and node id + def get_sliver_id_parts(self): + sliver_id_parts = [] + if self.type == 'sliver' or '-' in self.leaf: + sliver_id_parts = self.leaf.split('-') + return sliver_id_parts + def urn_to_hrn(self): """ compute tuple (hrn, type) from urn @@ -254,7 +279,7 @@ def hrn_to_urn(self): urn = "+".join(['',authority_string,self.type,Xrn.unescape(name)]) if hasattr(self, 'id') and self.id: - urn = "%s-%s" % (urn, self.id) + urn = "%s:%s" % (urn, self.id) self.urn = Xrn.URN_PREFIX + urn diff --git a/windows_install/LICENSE.TXT b/windows_install/LICENSE.TXT index 2e88c62d..c04af002 100644 --- a/windows_install/LICENSE.TXT +++ b/windows_install/LICENSE.TXT @@ -1468,16 +1468,16 @@ PlanetLab license (see "License" below). There are a small number of modifications to the code. These modifications are documented in the "Modifications" section below. -This code is based on revision 2947b0cb620ce8beb4c8c9bfe25adeadcbf1829f -(May 23rd, 2014, tag sfa-3.1-9 ) of the master branch of the +This code is based on revision 468d984409e02e84d15eb35d3eb464f6a3059dd8 +(June 11th, 2015, tag sfa-3.1-18 ) of the master branch of the PlanetLab Git repository (git.planet-lab.org/git/sfa.git). License ======= http://git.planet-lab.org/?p=sfa.git;a=blob_plain;f=LICENSE.txt;hb=HEAD -Copyright (c) 2008-2014 Board of Trustees, Princeton University -Copyright (c) 2010-2014 INRIA, Institut National d'Informatique et Automatique +Copyright (c) 2008-2015 Board of Trustees, Princeton University +Copyright (c) 2010-2015 INRIA, Institut National d'Informatique et Automatique Permission is hereby granted, free of charge, to any person obtaining a copy of this software and/or hardware specification (the "Work") to