From 7a1d34a07426961f0d08aeda0e8fe8c8b5caa4f6 Mon Sep 17 00:00:00 2001 From: Thierry Parmentelat Date: Mon, 3 Sep 2012 14:41:04 +0200 Subject: [PATCH] dos2unix --- sfa/trust/certificate.py | 1582 ++++++++++++------------ sfa/trust/credential.py | 2124 ++++++++++++++++---------------- sfa/trust/credential_legacy.py | 540 ++++---- sfa/trust/gid.py | 516 ++++---- sfa/util/enumeration.py | 70 +- sfa/util/faults.py | 704 +++++------ sfa/util/genicode.py | 90 +- sfa/util/sfalogging.py | 402 +++--- sfa/util/sfatime.py | 132 +- sfa/util/xrn.py | 536 ++++---- 10 files changed, 3348 insertions(+), 3348 deletions(-) diff --git a/sfa/trust/certificate.py b/sfa/trust/certificate.py index 35435cd7..59ec4427 100644 --- a/sfa/trust/certificate.py +++ b/sfa/trust/certificate.py @@ -1,791 +1,791 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- - -## -# SFA uses two crypto libraries: pyOpenSSL and M2Crypto to implement -# the necessary crypto functionality. Ideally just one of these libraries -# would be used, but unfortunately each of these libraries is independently -# lacking. The pyOpenSSL library is missing many necessary functions, and -# the M2Crypto library has crashed inside of some of the functions. The -# design decision is to use pyOpenSSL whenever possible as it seems more -# stable, and only use M2Crypto for those functions that are not possible -# in pyOpenSSL. -# -# This module exports two classes: Keypair and Certificate. -## -# - -import functools -import os -import tempfile -import base64 -from tempfile import mkstemp - -from OpenSSL import crypto -import M2Crypto -from M2Crypto import X509 - -from sfa.util.faults import CertExpired, CertMissingParent, CertNotSignedByParent -from sfa.util.sfalogging import logger - -glo_passphrase_callback = None - -## -# A global callback may be implemented for requesting passphrases from the -# user. The function will be called with three arguments: -# -# keypair_obj: the keypair object that is calling the passphrase -# string: the string containing the private key that's being loaded -# x: unknown, appears to be 0, comes from pyOpenSSL and/or m2crypto -# -# The callback should return a string containing the passphrase. - -def set_passphrase_callback(callback_func): - global glo_passphrase_callback - - glo_passphrase_callback = callback_func - -## -# Sets a fixed passphrase. - -def set_passphrase(passphrase): - set_passphrase_callback( lambda k,s,x: passphrase ) - -## -# Check to see if a passphrase works for a particular private key string. -# Intended to be used by passphrase callbacks for input validation. - -def test_passphrase(string, passphrase): - try: - crypto.load_privatekey(crypto.FILETYPE_PEM, string, (lambda x: passphrase)) - return True - except: - return False - -def convert_public_key(key): - keyconvert_path = "/usr/bin/keyconvert.py" - if not os.path.isfile(keyconvert_path): - raise IOError, "Could not find keyconvert in %s" % keyconvert_path - - # we can only convert rsa keys - if "ssh-dss" in key: - raise Exception, "keyconvert: dss keys are not supported" - - (ssh_f, ssh_fn) = tempfile.mkstemp() - ssl_fn = tempfile.mktemp() - os.write(ssh_f, key) - os.close(ssh_f) - - cmd = keyconvert_path + " " + ssh_fn + " " + ssl_fn - os.system(cmd) - - # this check leaves the temporary file containing the public key so - # that it can be expected to see why it failed. - # TODO: for production, cleanup the temporary files - if not os.path.exists(ssl_fn): - raise Exception, "keyconvert: generated certificate not found. keyconvert may have failed." - - k = Keypair() - try: - k.load_pubkey_from_file(ssl_fn) - return k - except: - logger.log_exc("convert_public_key caught exception") - raise - finally: - # remove the temporary files - if os.path.exists(ssh_fn): - os.remove(ssh_fn) - if os.path.exists(ssl_fn): - os.remove(ssl_fn) - -## -# Public-private key pairs are implemented by the Keypair class. -# A Keypair object may represent both a public and private key pair, or it -# may represent only a public key (this usage is consistent with OpenSSL). - -class Keypair: - key = None # public/private keypair - m2key = None # public key (m2crypto format) - - ## - # Creates a Keypair object - # @param create If create==True, creates a new public/private key and - # stores it in the object - # @param string If string!=None, load the keypair from the string (PEM) - # @param filename If filename!=None, load the keypair from the file - - def __init__(self, create=False, string=None, filename=None): - if create: - self.create() - if string: - self.load_from_string(string) - if filename: - self.load_from_file(filename) - - ## - # Create a RSA public/private key pair and store it inside the keypair object - - def create(self): - self.key = crypto.PKey() - self.key.generate_key(crypto.TYPE_RSA, 1024) - - ## - # Save the private key to a file - # @param filename name of file to store the keypair in - - def save_to_file(self, filename): - open(filename, 'w').write(self.as_pem()) - self.filename=filename - - ## - # Load the private key from a file. Implicity the private key includes the public key. - - def load_from_file(self, filename): - self.filename=filename - buffer = open(filename, 'r').read() - self.load_from_string(buffer) - - ## - # Load the private key from a string. Implicitly the private key includes the public key. - - def load_from_string(self, string): - if glo_passphrase_callback: - self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string, functools.partial(glo_passphrase_callback, self, string) ) - self.m2key = M2Crypto.EVP.load_key_string(string, functools.partial(glo_passphrase_callback, self, string) ) - else: - self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string) - self.m2key = M2Crypto.EVP.load_key_string(string) - - ## - # Load the public key from a string. No private key is loaded. - - def load_pubkey_from_file(self, filename): - # load the m2 public key - m2rsakey = M2Crypto.RSA.load_pub_key(filename) - self.m2key = M2Crypto.EVP.PKey() - self.m2key.assign_rsa(m2rsakey) - - # create an m2 x509 cert - m2name = M2Crypto.X509.X509_Name() - m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0) - m2x509 = M2Crypto.X509.X509() - m2x509.set_pubkey(self.m2key) - m2x509.set_serial_number(0) - m2x509.set_issuer_name(m2name) - m2x509.set_subject_name(m2name) - ASN1 = M2Crypto.ASN1.ASN1_UTCTIME() - ASN1.set_time(500) - m2x509.set_not_before(ASN1) - m2x509.set_not_after(ASN1) - # x509v3 so it can have extensions - # prob not necc since this cert itself is junk but still... - m2x509.set_version(2) - junk_key = Keypair(create=True) - m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1") - - # convert the m2 x509 cert to a pyopenssl x509 - m2pem = m2x509.as_pem() - pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem) - - # get the pyopenssl pkey from the pyopenssl x509 - self.key = pyx509.get_pubkey() - self.filename=filename - - ## - # Load the public key from a string. No private key is loaded. - - def load_pubkey_from_string(self, string): - (f, fn) = tempfile.mkstemp() - os.write(f, string) - os.close(f) - self.load_pubkey_from_file(fn) - os.remove(fn) - - ## - # Return the private key in PEM format. - - def as_pem(self): - return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key) - - ## - # Return an M2Crypto key object - - def get_m2_pkey(self): - if not self.m2key: - self.m2key = M2Crypto.EVP.load_key_string(self.as_pem()) - return self.m2key - - ## - # Returns a string containing the public key represented by this object. - - def get_pubkey_string(self): - m2pkey = self.get_m2_pkey() - return base64.b64encode(m2pkey.as_der()) - - ## - # Return an OpenSSL pkey object - - def get_openssl_pkey(self): - return self.key - - ## - # Given another Keypair object, return TRUE if the two keys are the same. - - def is_same(self, pkey): - return self.as_pem() == pkey.as_pem() - - def sign_string(self, data): - k = self.get_m2_pkey() - k.sign_init() - k.sign_update(data) - return base64.b64encode(k.sign_final()) - - def verify_string(self, data, sig): - k = self.get_m2_pkey() - k.verify_init() - k.verify_update(data) - return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey) - - def compute_hash(self, value): - return self.sign_string(str(value)) - - # only informative - def get_filename(self): - return getattr(self,'filename',None) - - def dump (self, *args, **kwargs): - print self.dump_string(*args, **kwargs) - - def dump_string (self): - result="" - result += "KEYPAIR: pubkey=%40s..."%self.get_pubkey_string() - filename=self.get_filename() - if filename: result += "Filename %s\n"%filename - return result - -## -# The certificate class implements a general purpose X509 certificate, making -# use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds -# several addition features, such as the ability to maintain a chain of -# parent certificates, and storage of application-specific data. -# -# Certificates include the ability to maintain a chain of parents. Each -# certificate includes a pointer to it's parent certificate. When loaded -# from a file or a string, the parent chain will be automatically loaded. -# When saving a certificate to a file or a string, the caller can choose -# whether to save the parent certificates as well. - -class Certificate: - digest = "md5" - - cert = None - issuerKey = None - issuerSubject = None - parent = None - isCA = None # will be a boolean once set - - separator="-----parent-----" - - ## - # Create a certificate object. - # - # @param lifeDays life of cert in days - default is 1825==5 years - # @param create If create==True, then also create a blank X509 certificate. - # @param subject If subject!=None, then create a blank certificate and set - # it's subject name. - # @param string If string!=None, load the certficate from the string. - # @param filename If filename!=None, load the certficiate from the file. - # @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): - self.data = {} - if create or subject: - self.create(lifeDays) - if subject: - self.set_subject(subject) - if string: - self.load_from_string(string) - if filename: - self.load_from_file(filename) - - # Set the CA bit if a value was supplied - if isCA != None: - self.set_is_ca(isCA) - - # Create a blank X509 certificate and store it in this object. - - def create(self, lifeDays=1825): - self.cert = 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 - - - ## - # Given a pyOpenSSL X509 object, store that object inside of this - # certificate object. - - def load_from_pyopenssl_x509(self, x509): - self.cert = x509 - - ## - # Load the certificate from a string - - def load_from_string(self, string): - # if it is a chain of multiple certs, then split off the first one and - # load it (support for the ---parent--- tag as well as normal chained certs) - - string = string.strip() - - # If it's not in proper PEM format, wrap it - if string.count('-----BEGIN CERTIFICATE') == 0: - string = '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----' % string - - # If there is a PEM cert in there, but there is some other text first - # such as the text of the certificate, skip the text - beg = string.find('-----BEGIN CERTIFICATE') - if beg > 0: - # skipping over non cert beginning - string = string[beg:] - - parts = [] - - if string.count('-----BEGIN CERTIFICATE-----') > 1 and \ - string.count(Certificate.separator) == 0: - parts = string.split('-----END CERTIFICATE-----',1) - parts[0] += '-----END CERTIFICATE-----' - else: - parts = string.split(Certificate.separator, 1) - - self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0]) - - # if there are more certs, then create a parent and let the parent load - # itself from the remainder of the string - if len(parts) > 1 and parts[1] != '': - self.parent = self.__class__() - self.parent.load_from_string(parts[1]) - - ## - # Load the certificate from a file - - def load_from_file(self, filename): - file = open(filename) - string = file.read() - self.load_from_string(string) - self.filename=filename - - ## - # Save the certificate to a string. - # - # @param save_parents If save_parents==True, then also save the parent certificates. - - def save_to_string(self, save_parents=True): - string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) - if save_parents and self.parent: - string = string + self.parent.save_to_string(save_parents) - return string - - ## - # Save the certificate to a file. - # @param save_parents If save_parents==True, then also save the parent certificates. - - def save_to_file(self, filename, save_parents=True, filep=None): - string = self.save_to_string(save_parents=save_parents) - if filep: - f = filep - else: - f = open(filename, 'w') - f.write(string) - f.close() - self.filename=filename - - ## - # Save the certificate to a random file in /tmp/ - # @param save_parents If save_parents==True, then also save the parent certificates. - def save_to_random_tmp_file(self, save_parents=True): - fp, filename = mkstemp(suffix='cert', text=True) - fp = os.fdopen(fp, "w") - self.save_to_file(filename, save_parents=True, filep=fp) - return filename - - ## - # Sets the issuer private key and name - # @param key Keypair object containing the private key of the issuer - # @param subject String containing the name of the issuer - # @param cert (optional) Certificate object containing the name of the issuer - - def set_issuer(self, key, subject=None, cert=None): - self.issuerKey = key - if subject: - # it's a mistake to use subject and cert params at the same time - assert(not cert) - if isinstance(subject, dict) or isinstance(subject, str): - req = crypto.X509Req() - reqSubject = req.get_subject() - if (isinstance(subject, dict)): - for key in reqSubject.keys(): - setattr(reqSubject, key, subject[key]) - else: - setattr(reqSubject, "CN", subject) - subject = reqSubject - # subject is not valid once req is out of scope, so save req - self.issuerReq = req - if cert: - # if a cert was supplied, then get the subject from the cert - subject = cert.cert.get_subject() - assert(subject) - self.issuerSubject = subject - - ## - # Get the issuer name - - def get_issuer(self, which="CN"): - x = self.cert.get_issuer() - return getattr(x, which) - - ## - # Set the subject name of the certificate - - def set_subject(self, name): - req = crypto.X509Req() - subj = req.get_subject() - if (isinstance(name, dict)): - for key in name.keys(): - setattr(subj, key, name[key]) - else: - setattr(subj, "CN", name) - self.cert.set_subject(subj) - - ## - # Get the subject name of the certificate - - def get_subject(self, which="CN"): - x = self.cert.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()) - - ## - # Get the public key of the certificate. - # - # @param key Keypair object containing the public key - - def set_pubkey(self, key): - assert(isinstance(key, Keypair)) - self.cert.set_pubkey(key.get_openssl_pkey()) - - ## - # Get the public key of the certificate. - # It is returned in the form of a Keypair object. - - def get_pubkey(self): - m2x509 = X509.load_cert_string(self.save_to_string()) - pkey = Keypair() - pkey.key = self.cert.get_pubkey() - pkey.m2key = m2x509.get_pubkey() - return pkey - - def set_intermediate_ca(self, val): - return self.set_is_ca(val) - - # Set whether this cert is for a CA. All signers and only signers should be CAs. - # The local member starts unset, letting us check that you only set it once - # @param val Boolean indicating whether this cert is for a CA - def set_is_ca(self, val): - if val is None: - return - - if self.isCA != None: - # Can't double set properties - raise Exception, "Cannot set basicConstraints CA:?? more than once. Was %s, trying to set as %s" % (self.isCA, val) - - self.isCA = val - if val: - self.add_extension('basicConstraints', 1, 'CA:TRUE') - else: - self.add_extension('basicConstraints', 1, 'CA:FALSE') - - - - ## - # Add an X509 extension to the certificate. Add_extension can only be called - # once for a particular extension name, due to limitations in the underlying - # library. - # - # @param name string containing name of extension - # @param value string containing value of the extension - - def add_extension(self, name, critical, value): - oldExtVal = None - try: - oldExtVal = self.get_extension(name) - except: - # M2Crypto LookupError when the extension isn't there (yet) - pass - - # This code limits you from adding the extension with the same value - # The method comment says you shouldn't do this with the same name - # But actually it (m2crypto) appears to allow you to do this. - if oldExtVal and oldExtVal == value: - # don't add this extension again - # just do nothing as here - return - # FIXME: What if they are trying to set with a different value? - # Is this ever OK? Or should we raise an exception? -# elif oldExtVal: -# 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]) - - ## - # Get an X509 extension from the certificate - - def get_extension(self, name): - - # pyOpenSSL does not have a way to get extensions - m2x509 = X509.load_cert_string(self.save_to_string()) - value = m2x509.get_ext(name).get_value() - - return value - - ## - # Set_data is a wrapper around add_extension. It stores the parameter str in - # the X509 subject_alt_name extension. Set_data can only be called once, due - # to limitations in the underlying library. - - def set_data(self, str, field='subjectAltName'): - # pyOpenSSL only allows us to add extensions, so if we try to set the - # same extension more than once, it will not work - if self.data.has_key(field): - raise "Cannot set ", field, " more than once" - self.data[field] = str - self.add_extension(field, 0, str) - - ## - # Return the data string that was previously set with set_data - - def get_data(self, field='subjectAltName'): - if self.data.has_key(field): - return self.data[field] - - try: - uri = self.get_extension(field) - self.data[field] = uri - except LookupError: - return None - - return self.data[field] - - ## - # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer(). - - def sign(self): - logger.debug('certificate.sign') - assert self.cert != 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) - - ## - # Verify the authenticity of a certificate. - # @param pkey is a Keypair object representing a public key. If Pkey - # did not sign the certificate, then an exception will be thrown. - - def verify(self, pkey): - # pyOpenSSL does not have a way to verify signatures - m2x509 = X509.load_cert_string(self.save_to_string()) - m2pkey = pkey.get_m2_pkey() - # verify it - return m2x509.verify(m2pkey) - - # XXX alternatively, if openssl has been patched, do the much simpler: - # try: - # self.cert.verify(pkey.get_openssl_key()) - # return 1 - # except: - # return 0 - - ## - # Return True if pkey is identical to the public key that is contained in the certificate. - # @param pkey Keypair object - - def is_pubkey(self, pkey): - return self.get_pubkey().is_same(pkey) - - ## - # Given a certificate cert, verify that this certificate was signed by the - # public key contained in cert. Throw an exception otherwise. - # - # @param cert certificate object - - def is_signed_by_cert(self, cert): - k = cert.get_pubkey() - result = self.verify(k) - return result - - ## - # Set the parent certficiate. - # - # @param p certificate object. - - def set_parent(self, p): - self.parent = p - - ## - # Return the certificate object of the parent of this certificate. - - def get_parent(self): - return self.parent - - ## - # Verification examines a chain of certificates to ensure that each parent - # signs the child, and that some certificate in the chain is signed by a - # trusted certificate. - # - # Verification is a basic recursion:
-    #     if this_certificate was signed by trusted_certs:
-    #         return
-    #     else
-    #         return verify_chain(parent, trusted_certs)
-    # 
- # - # At each recursion, the parent is tested to ensure that it did sign the - # child. If a parent did not sign a child, then an exception is thrown. If - # the bottom of the recursion is reached and the certificate does not match - # a trusted root, then an exception is thrown. - # Also require that parents are CAs. - # - # @param Trusted_certs is a list of certificates that are trusted. - # - - def verify_chain(self, trusted_certs = None): - # Verify a chain of certificates. Each certificate must be signed by - # the public key contained in it's parent. The chain is recursed - # 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 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())) - 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 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 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(), - self.get_issuer())) - raise CertNotSignedByParent("%s: Parent %s, issuer %s"\ - % (self.get_printable_subject(), - self.parent.get_printable_subject(), - self.get_issuer())) - - # Confirm that the parent is a CA. Only CAs can be trusted as - # signers. - # Note that trusted roots are not parents, so don't need to be - # CAs. - # Ugly - cert objects aren't parsed so we need to read the - # 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())) - - # if the parent isn't verified... - logger.debug("verify_chain: .. %s, -> verifying parent %s"%\ - (self.get_printable_subject(),self.parent.get_printable_subject())) - self.parent.verify_chain(trusted_certs) - - return - - ### more introspection - def get_extensions(self): - # pyOpenSSL does not have a way to get extensions - triples=[] - m2x509 = X509.load_cert_string(self.save_to_string()) - 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) - triples.append( (ext.get_name(), ext.get_value(), ext.get_critical(),) ) - return triples - - def get_data_names(self): - return self.data.keys() - - def get_all_datas (self): - triples=self.get_extensions() - for name in self.get_data_names(): - triples.append( (name,self.get_data(name),'data',) ) - return triples - - # only informative - def get_filename(self): - return getattr(self,'filename',None) - - def dump (self, *args, **kwargs): - print self.dump_string(*args, **kwargs) - - def dump_string (self,show_extensions=False): - result = "" - result += "CERTIFICATE for %s\n"%self.get_printable_subject() - 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() - result += " has %d extensions/data attached"%len(all_datas) - for (n,v,c) in all_datas: - if c=='data': - result += " data: %s=%s\n"%(n,v) - else: - result += " ext: %s (crit=%s)=<<<%s>>>\n"%(n,c,v) - return result +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- + +## +# SFA uses two crypto libraries: pyOpenSSL and M2Crypto to implement +# the necessary crypto functionality. Ideally just one of these libraries +# would be used, but unfortunately each of these libraries is independently +# lacking. The pyOpenSSL library is missing many necessary functions, and +# the M2Crypto library has crashed inside of some of the functions. The +# design decision is to use pyOpenSSL whenever possible as it seems more +# stable, and only use M2Crypto for those functions that are not possible +# in pyOpenSSL. +# +# This module exports two classes: Keypair and Certificate. +## +# + +import functools +import os +import tempfile +import base64 +from tempfile import mkstemp + +from OpenSSL import crypto +import M2Crypto +from M2Crypto import X509 + +from sfa.util.faults import CertExpired, CertMissingParent, CertNotSignedByParent +from sfa.util.sfalogging import logger + +glo_passphrase_callback = None + +## +# A global callback may be implemented for requesting passphrases from the +# user. The function will be called with three arguments: +# +# keypair_obj: the keypair object that is calling the passphrase +# string: the string containing the private key that's being loaded +# x: unknown, appears to be 0, comes from pyOpenSSL and/or m2crypto +# +# The callback should return a string containing the passphrase. + +def set_passphrase_callback(callback_func): + global glo_passphrase_callback + + glo_passphrase_callback = callback_func + +## +# Sets a fixed passphrase. + +def set_passphrase(passphrase): + set_passphrase_callback( lambda k,s,x: passphrase ) + +## +# Check to see if a passphrase works for a particular private key string. +# Intended to be used by passphrase callbacks for input validation. + +def test_passphrase(string, passphrase): + try: + crypto.load_privatekey(crypto.FILETYPE_PEM, string, (lambda x: passphrase)) + return True + except: + return False + +def convert_public_key(key): + keyconvert_path = "/usr/bin/keyconvert.py" + if not os.path.isfile(keyconvert_path): + raise IOError, "Could not find keyconvert in %s" % keyconvert_path + + # we can only convert rsa keys + if "ssh-dss" in key: + raise Exception, "keyconvert: dss keys are not supported" + + (ssh_f, ssh_fn) = tempfile.mkstemp() + ssl_fn = tempfile.mktemp() + os.write(ssh_f, key) + os.close(ssh_f) + + cmd = keyconvert_path + " " + ssh_fn + " " + ssl_fn + os.system(cmd) + + # this check leaves the temporary file containing the public key so + # that it can be expected to see why it failed. + # TODO: for production, cleanup the temporary files + if not os.path.exists(ssl_fn): + raise Exception, "keyconvert: generated certificate not found. keyconvert may have failed." + + k = Keypair() + try: + k.load_pubkey_from_file(ssl_fn) + return k + except: + logger.log_exc("convert_public_key caught exception") + raise + finally: + # remove the temporary files + if os.path.exists(ssh_fn): + os.remove(ssh_fn) + if os.path.exists(ssl_fn): + os.remove(ssl_fn) + +## +# Public-private key pairs are implemented by the Keypair class. +# A Keypair object may represent both a public and private key pair, or it +# may represent only a public key (this usage is consistent with OpenSSL). + +class Keypair: + key = None # public/private keypair + m2key = None # public key (m2crypto format) + + ## + # Creates a Keypair object + # @param create If create==True, creates a new public/private key and + # stores it in the object + # @param string If string!=None, load the keypair from the string (PEM) + # @param filename If filename!=None, load the keypair from the file + + def __init__(self, create=False, string=None, filename=None): + if create: + self.create() + if string: + self.load_from_string(string) + if filename: + self.load_from_file(filename) + + ## + # Create a RSA public/private key pair and store it inside the keypair object + + def create(self): + self.key = crypto.PKey() + self.key.generate_key(crypto.TYPE_RSA, 1024) + + ## + # Save the private key to a file + # @param filename name of file to store the keypair in + + def save_to_file(self, filename): + open(filename, 'w').write(self.as_pem()) + self.filename=filename + + ## + # Load the private key from a file. Implicity the private key includes the public key. + + def load_from_file(self, filename): + self.filename=filename + buffer = open(filename, 'r').read() + self.load_from_string(buffer) + + ## + # Load the private key from a string. Implicitly the private key includes the public key. + + def load_from_string(self, string): + if glo_passphrase_callback: + self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string, functools.partial(glo_passphrase_callback, self, string) ) + self.m2key = M2Crypto.EVP.load_key_string(string, functools.partial(glo_passphrase_callback, self, string) ) + else: + self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, string) + self.m2key = M2Crypto.EVP.load_key_string(string) + + ## + # Load the public key from a string. No private key is loaded. + + def load_pubkey_from_file(self, filename): + # load the m2 public key + m2rsakey = M2Crypto.RSA.load_pub_key(filename) + self.m2key = M2Crypto.EVP.PKey() + self.m2key.assign_rsa(m2rsakey) + + # create an m2 x509 cert + m2name = M2Crypto.X509.X509_Name() + m2name.add_entry_by_txt(field="CN", type=0x1001, entry="junk", len=-1, loc=-1, set=0) + m2x509 = M2Crypto.X509.X509() + m2x509.set_pubkey(self.m2key) + m2x509.set_serial_number(0) + m2x509.set_issuer_name(m2name) + m2x509.set_subject_name(m2name) + ASN1 = M2Crypto.ASN1.ASN1_UTCTIME() + ASN1.set_time(500) + m2x509.set_not_before(ASN1) + m2x509.set_not_after(ASN1) + # x509v3 so it can have extensions + # prob not necc since this cert itself is junk but still... + m2x509.set_version(2) + junk_key = Keypair(create=True) + m2x509.sign(pkey=junk_key.get_m2_pkey(), md="sha1") + + # convert the m2 x509 cert to a pyopenssl x509 + m2pem = m2x509.as_pem() + pyx509 = crypto.load_certificate(crypto.FILETYPE_PEM, m2pem) + + # get the pyopenssl pkey from the pyopenssl x509 + self.key = pyx509.get_pubkey() + self.filename=filename + + ## + # Load the public key from a string. No private key is loaded. + + def load_pubkey_from_string(self, string): + (f, fn) = tempfile.mkstemp() + os.write(f, string) + os.close(f) + self.load_pubkey_from_file(fn) + os.remove(fn) + + ## + # Return the private key in PEM format. + + def as_pem(self): + return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key) + + ## + # Return an M2Crypto key object + + def get_m2_pkey(self): + if not self.m2key: + self.m2key = M2Crypto.EVP.load_key_string(self.as_pem()) + return self.m2key + + ## + # Returns a string containing the public key represented by this object. + + def get_pubkey_string(self): + m2pkey = self.get_m2_pkey() + return base64.b64encode(m2pkey.as_der()) + + ## + # Return an OpenSSL pkey object + + def get_openssl_pkey(self): + return self.key + + ## + # Given another Keypair object, return TRUE if the two keys are the same. + + def is_same(self, pkey): + return self.as_pem() == pkey.as_pem() + + def sign_string(self, data): + k = self.get_m2_pkey() + k.sign_init() + k.sign_update(data) + return base64.b64encode(k.sign_final()) + + def verify_string(self, data, sig): + k = self.get_m2_pkey() + k.verify_init() + k.verify_update(data) + return M2Crypto.m2.verify_final(k.ctx, base64.b64decode(sig), k.pkey) + + def compute_hash(self, value): + return self.sign_string(str(value)) + + # only informative + def get_filename(self): + return getattr(self,'filename',None) + + def dump (self, *args, **kwargs): + print self.dump_string(*args, **kwargs) + + def dump_string (self): + result="" + result += "KEYPAIR: pubkey=%40s..."%self.get_pubkey_string() + filename=self.get_filename() + if filename: result += "Filename %s\n"%filename + return result + +## +# The certificate class implements a general purpose X509 certificate, making +# use of the appropriate pyOpenSSL or M2Crypto abstractions. It also adds +# several addition features, such as the ability to maintain a chain of +# parent certificates, and storage of application-specific data. +# +# Certificates include the ability to maintain a chain of parents. Each +# certificate includes a pointer to it's parent certificate. When loaded +# from a file or a string, the parent chain will be automatically loaded. +# When saving a certificate to a file or a string, the caller can choose +# whether to save the parent certificates as well. + +class Certificate: + digest = "md5" + + cert = None + issuerKey = None + issuerSubject = None + parent = None + isCA = None # will be a boolean once set + + separator="-----parent-----" + + ## + # Create a certificate object. + # + # @param lifeDays life of cert in days - default is 1825==5 years + # @param create If create==True, then also create a blank X509 certificate. + # @param subject If subject!=None, then create a blank certificate and set + # it's subject name. + # @param string If string!=None, load the certficate from the string. + # @param filename If filename!=None, load the certficiate from the file. + # @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): + self.data = {} + if create or subject: + self.create(lifeDays) + if subject: + self.set_subject(subject) + if string: + self.load_from_string(string) + if filename: + self.load_from_file(filename) + + # Set the CA bit if a value was supplied + if isCA != None: + self.set_is_ca(isCA) + + # Create a blank X509 certificate and store it in this object. + + def create(self, lifeDays=1825): + self.cert = 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 + + + ## + # Given a pyOpenSSL X509 object, store that object inside of this + # certificate object. + + def load_from_pyopenssl_x509(self, x509): + self.cert = x509 + + ## + # Load the certificate from a string + + def load_from_string(self, string): + # if it is a chain of multiple certs, then split off the first one and + # load it (support for the ---parent--- tag as well as normal chained certs) + + string = string.strip() + + # If it's not in proper PEM format, wrap it + if string.count('-----BEGIN CERTIFICATE') == 0: + string = '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----' % string + + # If there is a PEM cert in there, but there is some other text first + # such as the text of the certificate, skip the text + beg = string.find('-----BEGIN CERTIFICATE') + if beg > 0: + # skipping over non cert beginning + string = string[beg:] + + parts = [] + + if string.count('-----BEGIN CERTIFICATE-----') > 1 and \ + string.count(Certificate.separator) == 0: + parts = string.split('-----END CERTIFICATE-----',1) + parts[0] += '-----END CERTIFICATE-----' + else: + parts = string.split(Certificate.separator, 1) + + self.cert = crypto.load_certificate(crypto.FILETYPE_PEM, parts[0]) + + # if there are more certs, then create a parent and let the parent load + # itself from the remainder of the string + if len(parts) > 1 and parts[1] != '': + self.parent = self.__class__() + self.parent.load_from_string(parts[1]) + + ## + # Load the certificate from a file + + def load_from_file(self, filename): + file = open(filename) + string = file.read() + self.load_from_string(string) + self.filename=filename + + ## + # Save the certificate to a string. + # + # @param save_parents If save_parents==True, then also save the parent certificates. + + def save_to_string(self, save_parents=True): + string = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + if save_parents and self.parent: + string = string + self.parent.save_to_string(save_parents) + return string + + ## + # Save the certificate to a file. + # @param save_parents If save_parents==True, then also save the parent certificates. + + def save_to_file(self, filename, save_parents=True, filep=None): + string = self.save_to_string(save_parents=save_parents) + if filep: + f = filep + else: + f = open(filename, 'w') + f.write(string) + f.close() + self.filename=filename + + ## + # Save the certificate to a random file in /tmp/ + # @param save_parents If save_parents==True, then also save the parent certificates. + def save_to_random_tmp_file(self, save_parents=True): + fp, filename = mkstemp(suffix='cert', text=True) + fp = os.fdopen(fp, "w") + self.save_to_file(filename, save_parents=True, filep=fp) + return filename + + ## + # Sets the issuer private key and name + # @param key Keypair object containing the private key of the issuer + # @param subject String containing the name of the issuer + # @param cert (optional) Certificate object containing the name of the issuer + + def set_issuer(self, key, subject=None, cert=None): + self.issuerKey = key + if subject: + # it's a mistake to use subject and cert params at the same time + assert(not cert) + if isinstance(subject, dict) or isinstance(subject, str): + req = crypto.X509Req() + reqSubject = req.get_subject() + if (isinstance(subject, dict)): + for key in reqSubject.keys(): + setattr(reqSubject, key, subject[key]) + else: + setattr(reqSubject, "CN", subject) + subject = reqSubject + # subject is not valid once req is out of scope, so save req + self.issuerReq = req + if cert: + # if a cert was supplied, then get the subject from the cert + subject = cert.cert.get_subject() + assert(subject) + self.issuerSubject = subject + + ## + # Get the issuer name + + def get_issuer(self, which="CN"): + x = self.cert.get_issuer() + return getattr(x, which) + + ## + # Set the subject name of the certificate + + def set_subject(self, name): + req = crypto.X509Req() + subj = req.get_subject() + if (isinstance(name, dict)): + for key in name.keys(): + setattr(subj, key, name[key]) + else: + setattr(subj, "CN", name) + self.cert.set_subject(subj) + + ## + # Get the subject name of the certificate + + def get_subject(self, which="CN"): + x = self.cert.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()) + + ## + # Get the public key of the certificate. + # + # @param key Keypair object containing the public key + + def set_pubkey(self, key): + assert(isinstance(key, Keypair)) + self.cert.set_pubkey(key.get_openssl_pkey()) + + ## + # Get the public key of the certificate. + # It is returned in the form of a Keypair object. + + def get_pubkey(self): + m2x509 = X509.load_cert_string(self.save_to_string()) + pkey = Keypair() + pkey.key = self.cert.get_pubkey() + pkey.m2key = m2x509.get_pubkey() + return pkey + + def set_intermediate_ca(self, val): + return self.set_is_ca(val) + + # Set whether this cert is for a CA. All signers and only signers should be CAs. + # The local member starts unset, letting us check that you only set it once + # @param val Boolean indicating whether this cert is for a CA + def set_is_ca(self, val): + if val is None: + return + + if self.isCA != None: + # Can't double set properties + raise Exception, "Cannot set basicConstraints CA:?? more than once. Was %s, trying to set as %s" % (self.isCA, val) + + self.isCA = val + if val: + self.add_extension('basicConstraints', 1, 'CA:TRUE') + else: + self.add_extension('basicConstraints', 1, 'CA:FALSE') + + + + ## + # Add an X509 extension to the certificate. Add_extension can only be called + # once for a particular extension name, due to limitations in the underlying + # library. + # + # @param name string containing name of extension + # @param value string containing value of the extension + + def add_extension(self, name, critical, value): + oldExtVal = None + try: + oldExtVal = self.get_extension(name) + except: + # M2Crypto LookupError when the extension isn't there (yet) + pass + + # This code limits you from adding the extension with the same value + # The method comment says you shouldn't do this with the same name + # But actually it (m2crypto) appears to allow you to do this. + if oldExtVal and oldExtVal == value: + # don't add this extension again + # just do nothing as here + return + # FIXME: What if they are trying to set with a different value? + # Is this ever OK? Or should we raise an exception? +# elif oldExtVal: +# 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]) + + ## + # Get an X509 extension from the certificate + + def get_extension(self, name): + + # pyOpenSSL does not have a way to get extensions + m2x509 = X509.load_cert_string(self.save_to_string()) + value = m2x509.get_ext(name).get_value() + + return value + + ## + # Set_data is a wrapper around add_extension. It stores the parameter str in + # the X509 subject_alt_name extension. Set_data can only be called once, due + # to limitations in the underlying library. + + def set_data(self, str, field='subjectAltName'): + # pyOpenSSL only allows us to add extensions, so if we try to set the + # same extension more than once, it will not work + if self.data.has_key(field): + raise "Cannot set ", field, " more than once" + self.data[field] = str + self.add_extension(field, 0, str) + + ## + # Return the data string that was previously set with set_data + + def get_data(self, field='subjectAltName'): + if self.data.has_key(field): + return self.data[field] + + try: + uri = self.get_extension(field) + self.data[field] = uri + except LookupError: + return None + + return self.data[field] + + ## + # Sign the certificate using the issuer private key and issuer subject previous set with set_issuer(). + + def sign(self): + logger.debug('certificate.sign') + assert self.cert != 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) + + ## + # Verify the authenticity of a certificate. + # @param pkey is a Keypair object representing a public key. If Pkey + # did not sign the certificate, then an exception will be thrown. + + def verify(self, pkey): + # pyOpenSSL does not have a way to verify signatures + m2x509 = X509.load_cert_string(self.save_to_string()) + m2pkey = pkey.get_m2_pkey() + # verify it + return m2x509.verify(m2pkey) + + # XXX alternatively, if openssl has been patched, do the much simpler: + # try: + # self.cert.verify(pkey.get_openssl_key()) + # return 1 + # except: + # return 0 + + ## + # Return True if pkey is identical to the public key that is contained in the certificate. + # @param pkey Keypair object + + def is_pubkey(self, pkey): + return self.get_pubkey().is_same(pkey) + + ## + # Given a certificate cert, verify that this certificate was signed by the + # public key contained in cert. Throw an exception otherwise. + # + # @param cert certificate object + + def is_signed_by_cert(self, cert): + k = cert.get_pubkey() + result = self.verify(k) + return result + + ## + # Set the parent certficiate. + # + # @param p certificate object. + + def set_parent(self, p): + self.parent = p + + ## + # Return the certificate object of the parent of this certificate. + + def get_parent(self): + return self.parent + + ## + # Verification examines a chain of certificates to ensure that each parent + # signs the child, and that some certificate in the chain is signed by a + # trusted certificate. + # + # Verification is a basic recursion:
+    #     if this_certificate was signed by trusted_certs:
+    #         return
+    #     else
+    #         return verify_chain(parent, trusted_certs)
+    # 
+ # + # At each recursion, the parent is tested to ensure that it did sign the + # child. If a parent did not sign a child, then an exception is thrown. If + # the bottom of the recursion is reached and the certificate does not match + # a trusted root, then an exception is thrown. + # Also require that parents are CAs. + # + # @param Trusted_certs is a list of certificates that are trusted. + # + + def verify_chain(self, trusted_certs = None): + # Verify a chain of certificates. Each certificate must be signed by + # the public key contained in it's parent. The chain is recursed + # 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 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())) + 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 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 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(), + self.get_issuer())) + raise CertNotSignedByParent("%s: Parent %s, issuer %s"\ + % (self.get_printable_subject(), + self.parent.get_printable_subject(), + self.get_issuer())) + + # Confirm that the parent is a CA. Only CAs can be trusted as + # signers. + # Note that trusted roots are not parents, so don't need to be + # CAs. + # Ugly - cert objects aren't parsed so we need to read the + # 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())) + + # if the parent isn't verified... + logger.debug("verify_chain: .. %s, -> verifying parent %s"%\ + (self.get_printable_subject(),self.parent.get_printable_subject())) + self.parent.verify_chain(trusted_certs) + + return + + ### more introspection + def get_extensions(self): + # pyOpenSSL does not have a way to get extensions + triples=[] + m2x509 = X509.load_cert_string(self.save_to_string()) + 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) + triples.append( (ext.get_name(), ext.get_value(), ext.get_critical(),) ) + return triples + + def get_data_names(self): + return self.data.keys() + + def get_all_datas (self): + triples=self.get_extensions() + for name in self.get_data_names(): + triples.append( (name,self.get_data(name),'data',) ) + return triples + + # only informative + def get_filename(self): + return getattr(self,'filename',None) + + def dump (self, *args, **kwargs): + print self.dump_string(*args, **kwargs) + + def dump_string (self,show_extensions=False): + result = "" + result += "CERTIFICATE for %s\n"%self.get_printable_subject() + 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() + result += " has %d extensions/data attached"%len(all_datas) + for (n,v,c) in all_datas: + if c=='data': + result += " data: %s=%s\n"%(n,v) + else: + result += " ext: %s (crit=%s)=<<<%s>>>\n"%(n,c,v) + return result diff --git a/sfa/trust/credential.py b/sfa/trust/credential.py index 07f86806..9ccf18fa 100644 --- a/sfa/trust/credential.py +++ b/sfa/trust/credential.py @@ -1,1062 +1,1062 @@ -#---------------------------------------------------------------------- -# 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 signed XML files that assign a subject gid privileges to an object gid -## - -import os -from types import StringTypes -import datetime -from StringIO import StringIO -from tempfile import mkstemp -from xml.dom.minidom import Document, parseString - -HAVELXML = False -try: - from lxml import etree - HAVELXML = True -except: - pass - -from xml.parsers.expat import ExpatError - -from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent -from sfa.util.sfalogging import logger -from sfa.util.sfatime import utcparse -from sfa.trust.credential_legacy import CredentialLegacy -from sfa.trust.rights import Right, Rights, determine_rights -from sfa.trust.gid import GID -from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn - -# 2 weeks, in seconds -DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31 - - -# TODO: -# . make privs match between PG and PL -# . Need to add support for other types of credentials, e.g. tickets -# . add namespaces to signed-credential element? - -signature_template = \ -''' - - - - - - - - - - - - - - - - - - - - - - -''' - -# PG formats the template (whitespace) slightly differently. -# Note that they don't include the xmlns in the template, but add it later. -# Otherwise the two are equivalent. -#signature_template_as_in_pg = \ -#''' -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -# -#''' - -## -# Convert a string into a bool -# used to convert an xsd:boolean to a Python boolean -def str2bool(str): - if str.lower() in ['true','1']: - return True - return False - - -## -# Utility function to get the text of an XML element - -def getTextNode(element, subele): - sub = element.getElementsByTagName(subele)[0] - if len(sub.childNodes) > 0: - return sub.childNodes[0].nodeValue - else: - return None - -## -# Utility function to set the text of an XML element -# It creates the element, adds the text to it, -# and then appends it to the parent. - -def append_sub(doc, parent, element, text): - ele = doc.createElement(element) - ele.appendChild(doc.createTextNode(text)) - parent.appendChild(ele) - -## -# Signature contains information about an xmlsec1 signature -# for a signed-credential -# - -class Signature(object): - - def __init__(self, string=None): - self.refid = None - self.issuer_gid = None - self.xml = None - if string: - self.xml = string - self.decode() - - - def get_refid(self): - if not self.refid: - self.decode() - return self.refid - - def get_xml(self): - if not self.xml: - self.encode() - return self.xml - - def set_refid(self, id): - self.refid = id - - def get_issuer_gid(self): - if not self.gid: - self.decode() - return self.gid - - def set_issuer_gid(self, gid): - self.gid = gid - - def decode(self): - try: - doc = parseString(self.xml) - except ExpatError,e: - logger.log_exc ("Failed to parse credential, %s"%self.xml) - raise - sig = doc.getElementsByTagName("Signature")[0] - self.set_refid(sig.getAttribute("xml:id").strip("Sig_")) - keyinfo = sig.getElementsByTagName("X509Data")[0] - szgid = getTextNode(keyinfo, "X509Certificate") - szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid - self.set_issuer_gid(GID(string=szgid)) - - 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. -# -# WARNING: -# In general, a signed credential obtained externally should -# not be changed else the signature is no longer valid. So, once -# you have loaded an existing signed credential, do not call encode() or sign() on it. - -def filter_creds_by_caller(creds, caller_hrn_list): - """ - Returns a list of creds who's gid caller matches the - specified caller hrn - """ - if not isinstance(creds, list): creds = [creds] - if not isinstance(caller_hrn_list, list): - caller_hrn_list = [caller_hrn_list] - caller_creds = [] - for cred in creds: - try: - tmp_cred = Credential(string=cred) - if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list: - caller_creds.append(cred) - except: pass - return caller_creds - -class Credential(object): - - ## - # 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 - # FIXME: create and subject are ignored! - def __init__(self, create=False, subject=None, string=None, filename=None): - self.gidCaller = None - self.gidObject = None - self.expiration = None - self.privileges = None - self.issuer_privkey = None - self.issuer_gid = None - self.issuer_pubkey = None - self.parent = None - self.signature = None - self.xml = None - self.refid = None - self.legacy = None - - # 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) - else: - self.xml = str - self.decode() - - # Find an xmlsec1 path - self.xmlsec_path = '' - paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin'] - for path in paths: - if os.path.isfile(path + '/' + 'xmlsec1'): - self.xmlsec_path = path + '/' + 'xmlsec1' - break - - def get_subject(self): - if not self.gidObject: - self.decode() - return self.gidObject.get_printable_subject() - - # sounds like this should be __repr__ instead ?? - def get_summary_tostring(self): - if not self.gidObject: - self.decode() - obj = self.gidObject.get_printable_subject() - caller = self.gidCaller.get_printable_subject() - exp = self.get_expiration() - # Summarize the rights too? The issuer? - return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp) - - def get_signature(self): - if not self.signature: - self.decode() - return self.signature - - 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 - # @param gid GID of the issuing authority - - def set_issuer_keys(self, privkey, gid): - self.issuer_privkey = privkey - self.issuer_gid = gid - - - ## - # Set this credential's parent - def set_parent(self, cred): - self.parent = cred - self.updateRefID() - - ## - # 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 - - ## - # 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) - else: - logger.error ("unexpected input type in Credential.set_expiration") - - - ## - # get the lifetime of the credential (always in datetime format) - - def get_expiration(self): - if not self.expiration: - self.decode() - # 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 - # - # @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 an XML string - # This should be done immediately before signing the credential. - # WARNING: - # In general, a signed credential obtained externally should - # not be changed else the signature is no longer valid. So, once - # you have loaded an existing signed credential, do not call encode() or sign() on it. - - def encode(self): - # Create the XML document - 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. - signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") - 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") - - doc.appendChild(signed_cred) - - # Fill in the bit - cred = doc.createElement("credential") - cred.setAttribute("xml:id", self.get_refid()) - signed_cred.appendChild(cred) - append_sub(doc, cred, "type", "privilege") - append_sub(doc, cred, "serial", "8") - append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string()) - append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn()) - append_sub(doc, cred, "target_gid", self.gidObject.save_to_string()) - append_sub(doc, cred, "target_urn", self.gidObject.get_urn()) - append_sub(doc, cred, "uuid", "") - if not self.expiration: - self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) - self.expiration = self.expiration.replace(microsecond=0) - append_sub(doc, cred, "expires", self.expiration.isoformat()) - privileges = doc.createElement("privileges") - cred.appendChild(privileges) - - if self.privileges: - rights = self.get_privileges() - for right in rights.rights: - priv = doc.createElement("privilege") - append_sub(doc, priv, "name", right.kind) - append_sub(doc, priv, "can_delegate", str(right.delegate).lower()) - privileges.appendChild(priv) - - # Add the parent credential if it exists - if self.parent: - sdoc = parseString(self.parent.get_xml()) - # If the root node is a signed-credential (it should be), then - # get all its attributes and attach those to our signed_cred - # node. - # Specifically, PG and PLadd attributes for namespaces (which is reasonable), - # 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") - - # HOWEVER! - # PL now also declares these, with different URLs, so - # the code notices those attributes already existed with - # different values, and complains. - # This happens regularly on delegation now that PG and - # PL both declare the namespace with different URLs. - # If the content ever differs this is a problem, - # but for now it works - different URLs (values in the attributes) - # but the same actual schema, so using the PG schema - # on delegated-to-PL credentials works fine. - - # Note: you could also not copy attributes - # which already exist. It appears that both PG and PL - # will actually validate a slicecred with a parent - # signed using PG namespaces and a child signed with PL - # namespaces over the whole thing. But I don't know - # if that is a bug in xmlsec1, an accident since - # the contents of the schemas are the same, - # or something else, but it seems odd. And this works. - parentRoot = sdoc.documentElement - if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes(): - for attrIx in range(0, parentRoot.attributes.length): - attr = parentRoot.attributes.item(attrIx) - # returns the old attribute of same name that was - # on the credential - # 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 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) - - p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True) - p = doc.createElement("parent") - p.appendChild(p_cred) - cred.appendChild(p) - # done handling parent credential - - # Create the tag - signatures = doc.createElement("signatures") - signed_cred.appendChild(signatures) - - # Add any parent signatures - if self.parent: - for cur_cred in self.get_credential_list()[1:]: - sdoc = parseString(cur_cred.get_signature().get_xml()) - ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) - signatures.appendChild(ele) - - # Get the finished product - self.xml = doc.toxml() - - - def save_to_random_tmp_file(self): - fp, filename = mkstemp(suffix='cred', text=True) - fp = os.fdopen(fp, "w") - self.save_to_file(filename, save_parents=True, filep=fp) - return filename - - def save_to_file(self, filename, save_parents=True, filep=None): - if not self.xml: - self.encode() - if filep: - f = filep - else: - f = open(filename, "w") - f.write(self.xml) - f.close() - - def save_to_string(self, save_parents=True): - if not self.xml: - self.encode() - return self.xml - - def get_refid(self): - if not self.refid: - self.refid = 'ref0' - return self.refid - - def set_refid(self, rid): - self.refid = rid - - ## - # Figure out what refids exist, and update this credential's id - # so that it doesn't clobber the others. Returns the refids of - # the parents. - - def updateRefID(self): - if not self.parent: - self.set_refid('ref0') - return [] - - refs = [] - - next_cred = self.parent - while next_cred: - refs.append(next_cred.get_refid()) - if next_cred.parent: - next_cred = next_cred.parent - else: - next_cred = None - - - # Find a unique refid for this credential - rid = self.get_refid() - while rid in refs: - val = int(rid[3:]) - rid = "ref%d" % (val + 1) - - # Set the new refid - self.set_refid(rid) - - # Return the set of parent credential ref ids - return refs - - def get_xml(self): - if not self.xml: - self.encode() - return self.xml - - ## - # Sign the XML file created by encode() - # - # WARNING: - # In general, a signed credential obtained externally should - # not be changed else the signature is no longer valid. So, once - # you have loaded an existing signed credential, do not call encode() or sign() on it. - - def sign(self): - if not self.issuer_privkey or not self.issuer_gid: - return - doc = parseString(self.get_xml()) - sigs = doc.getElementsByTagName("signatures")[0] - - # Create the signature template to be signed - signature = Signature() - signature.set_refid(self.get_refid()) - sdoc = parseString(signature.get_xml()) - sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) - sigs.appendChild(sig_ele) - - self.xml = doc.toxml() - - - # Split the issuer GID into multiple certificates if it's a chain - chain = GID(filename=self.issuer_gid) - gid_files = [] - while chain: - gid_files.append(chain.save_to_random_tmp_file(False)) - if chain.get_parent(): - chain = chain.get_parent() - else: - chain = None - - - # Call out to xmlsec1 to sign it - ref = 'Sig_%s' % self.get_refid() - filename = self.save_to_random_tmp_file() - signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \ - % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read() - os.remove(filename) - - for gid_file in gid_files: - os.remove(gid_file) - - self.xml = signed - - # This is no longer a legacy credential - if self.legacy: - self.legacy = None - - # Update signatures - self.decode() - - - ## - # Retrieve the attributes of the credential from the XML. - # This is automatically called by the various get_* methods of - # this class and should not need to be called explicitly. - - def decode(self): - if not self.xml: - return - doc = parseString(self.xml) - sigs = [] - signed_cred = doc.getElementsByTagName("signed-credential") - - # Is this a signed-cred or just a cred? - if len(signed_cred) > 0: - creds = signed_cred[0].getElementsByTagName("credential") - signatures = signed_cred[0].getElementsByTagName("signatures") - if len(signatures) > 0: - sigs = signatures[0].getElementsByTagName("Signature") - else: - creds = doc.getElementsByTagName("credential") - - if creds is None or len(creds) == 0: - # malformed cred file - raise CredentialNotVerifiable("Malformed XML: No credential tag found") - - # Just take the first cred if there are more than one - cred = creds[0] - - 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")) - - - # Process privileges - privs = cred.getElementsByTagName("privileges")[0] - rlist = Rights() - for priv in privs.getElementsByTagName("privilege"): - kind = getTextNode(priv, "name") - deleg = str2bool(getTextNode(priv, "can_delegate")) - if kind == '*': - # Convert * into the default privileges for the credential's type - # Each inherits the delegatability from the * above - _ , type = urn_to_hrn(self.gidObject.get_urn()) - rl = determine_rights(type, self.gidObject.get_urn()) - for r in rl.rights: - r.delegate = deleg - rlist.add(r) - else: - rlist.add(Right(kind.strip(), deleg)) - self.set_privileges(rlist) - - - # Is there a parent? - parent = cred.getElementsByTagName("parent") - if len(parent) > 0: - parent_doc = parent[0].getElementsByTagName("credential")[0] - parent_xml = parent_doc.toxml() - self.parent = Credential(string=parent_xml) - self.updateRefID() - - # Assign the signatures to the credentials - for sig in sigs: - Sig = Signature(string=sig.toxml()) - - for cur_cred in self.get_credential_list(): - if cur_cred.get_refid() == Sig.get_refid(): - cur_cred.set_signature(Sig) - - - ## - # Verify - # trusted_certs: A list of trusted GID filenames (not GID objects!) - # Chaining is not supported within the GIDs by xmlsec1. - # - # trusted_certs_required: Should usually be true. Set False means an - # empty list of trusted_certs would still let this method pass. - # It just skips xmlsec1 verification et al. Only used by some utils - # - # Verify that: - # . All of the signatures are valid and that the issuers trace back - # to trusted roots (performed by xmlsec1) - # . The XML matches the credential schema - # . That the issuer of the credential is the authority in the target's urn - # . In the case of a delegated credential, this must be true of the root - # . That all of the gids presented in the credential are valid - # . Including verifying GID chains, and includ the issuer - # . The credential is not expired - # - # -- For Delegates (credentials with parents) - # . The privileges must be a subset of the parent credentials - # . The privileges must have "can_delegate" set for each delegated privilege - # . The target gid must be the same between child and parents - # . The expiry time on the child must be no later than the parent - # . The signer of the child must be the owner of the parent - # - # -- Verify does *NOT* - # . ensure that an xmlrpc client's gid matches a credential gid, that - # must be done elsewhere - # - # @param trusted_certs: The certificates of trusted CA certificates - def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): - if not self.xml: - self.decode() - - # validate against RelaxNG schema - if HAVELXML and not self.legacy: - 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) - raise CredentialNotVerifiable(message) - - if trusted_certs_required and trusted_certs is None: - trusted_certs = [] - -# trusted_cert_objects = [GID(filename=f) for f in trusted_certs] - trusted_cert_objects = [] - ok_trusted_certs = [] - # If caller explicitly passed in None that means skip cert chain validation. - # Strange and not typical - if trusted_certs is not None: - for f in trusted_certs: - try: - # Failures here include unreadable files - # or non PEM files - trusted_cert_objects.append(GID(filename=f)) - ok_trusted_certs.append(f) - except Exception, exc: - 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())) - - # 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 - if trusted_certs is not None: - # Verify the gids of this cred and of its parents - for cur_cred in self.get_credential_list(): - cur_cred.get_gid_object().verify_chain(trusted_cert_objects) - cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) - - refs = [] - refs.append("Sig_%s" % self.get_refid()) - - parentRefs = self.updateRefID() - for ref in parentRefs: - refs.append("Sig_%s" % ref) - - for ref in refs: - # If caller explicitly passed in None that means skip xmlsec1 validation. - # Strange and not typical - 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"): - # xmlsec errors have a msg= which is the interesting bit. - mstart = verified.find("msg=") - msg = "" - if mstart > -1 and len(verified) > 4: - 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())) - os.remove(filename) - - # Verify the parents (delegation) - if self.parent: - self.verify_parent(self.parent) - - # Make sure the issuer is the target's authority, and is - # itself a valid GID - self.verify_issuer(trusted_cert_objects) - return True - - ## - # Creates a list of the credential and its parents, with the root - # (original delegated credential) as the last item in the list - def get_credential_list(self): - cur_cred = self - list = [] - while cur_cred: - list.append(cur_cred) - if cur_cred.parent: - cur_cred = cur_cred.parent - else: - cur_cred = None - return list - - ## - # Make sure the credential's target gid (a) was signed by or (b) - # is the same as the entity that signed the original credential, - # or (c) is an authority over the target's namespace. - # Also ensure that the credential issuer / signer itself has a valid - # GID signature chain (signed by an authority with namespace rights). - def verify_issuer(self, trusted_gids): - root_cred = self.get_credential_list()[-1] - root_target_gid = root_cred.get_gid_object() - root_cred_signer = root_cred.get_signature().get_issuer_gid() - - # Case 1: - # Allow non authority to sign target and cred about target. - # - # Why do we need to allow non authorities to sign? - # If in the target gid validation step we correctly - # checked that the target is only signed by an authority, - # then this is just a special case of case 3. - # This short-circuit is the common case currently - - # and cause GID validation doesn't check 'authority', - # this allows users to generate valid slice credentials. - if root_target_gid.is_signed_by_cert(root_cred_signer): - # cred signer matches target signer, return success - return - - # Case 2: - # Allow someone to sign credential about themeselves. Used? - # If not, remove this. - #root_target_gid_str = root_target_gid.save_to_string() - #root_cred_signer_str = root_cred_signer.save_to_string() - #if root_target_gid_str == root_cred_signer_str: - # # cred signer is target, return success - # return - - # Case 3: - - # root_cred_signer is not the target_gid - # So this is a different gid that we have not verified. - # xmlsec1 verified the cert chain on this already, but - # it hasn't verified that the gid meets the HRN namespace - # requirements. - # Below we'll ensure that it is an authority. - # But we haven't verified that it is _signed by_ an authority - # We also don't know if xmlsec1 requires that cert signers - # are marked as CAs. - - # Note that if verify() gave us no trusted_gids then this - # call will fail. So skip it if we have no 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.") - - # 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): - #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() - if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()): - return - - # We've required that the credential be signed by an authority - # for that domain. Reasonable and probably correct. - # A looser model would also allow the signer to be an authority - # in my control framework - eg My CA or CH. Even if it is not - # the CH that issued these, eg, user credentials. - - # 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())) - - - ## - # -- For Delegates (credentials with parents) verify that: - # . The privileges must be a subset of the parent credentials - # . The privileges must have "can_delegate" set for each delegated privilege - # . The target gid must be the same between child and parents - # . The expiry time on the child must be no later than the parent - # . The signer of the child must be the owner of the parent - 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()) - - # 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())) - - # 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())) - - # 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())) - - # Recurse - if parent_cred.parent: - parent_cred.verify_parent(parent_cred.parent) - - - def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile): - """ - Return a delegated copy of this credential, delegated to the - specified gid's user. - """ - # get the gid of the object we are delegating - object_gid = self.get_gid_object() - object_hrn = object_gid.get_hrn() - - # the hrn of the user who will be delegated to - delegee_gid = GID(filename=delegee_gidfile) - delegee_hrn = delegee_gid.get_hrn() - - #user_key = Keypair(filename=keyfile) - #user_hrn = self.get_gid_caller().get_hrn() - subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn) - dcred = Credential(subject=subject_string) - dcred.set_gid_caller(delegee_gid) - dcred.set_gid_object(object_gid) - dcred.set_parent(self) - dcred.set_expiration(self.get_expiration()) - dcred.set_privileges(self.get_privileges()) - dcred.get_privileges().delegate_all_privileges(True) - #dcred.set_issuer_keys(keyfile, delegee_gidfile) - dcred.set_issuer_keys(caller_keyfile, caller_gidfile) - dcred.encode() - dcred.sign() - - return dcred - - # only informative - def get_filename(self): - return getattr(self,'filename',None) - - ## - # 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() - filename=self.get_filename() - if filename: result += "Filename %s\n"%filename - result += " privs: %s\n" % self.get_privileges().save_to_string() - gidCaller = self.get_gid_caller() - if gidCaller: - result += " gidCaller:\n" - result += gidCaller.dump_string(8, dump_parents) - - if self.get_signature(): - print " gidIssuer:" - self.get_signature().get_issuer_gid().dump(8, dump_parents) - - gidObject = self.get_gid_object() - if gidObject: - result += " gidObject:\n" - result += gidObject.dump_string(8, dump_parents) - - if self.parent and dump_parents: - result += "\nPARENT" - result += self.parent.dump_string(True) - - return result +#---------------------------------------------------------------------- +# 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 signed XML files that assign a subject gid privileges to an object gid +## + +import os +from types import StringTypes +import datetime +from StringIO import StringIO +from tempfile import mkstemp +from xml.dom.minidom import Document, parseString + +HAVELXML = False +try: + from lxml import etree + HAVELXML = True +except: + pass + +from xml.parsers.expat import ExpatError + +from sfa.util.faults import CredentialNotVerifiable, ChildRightsNotSubsetOfParent +from sfa.util.sfalogging import logger +from sfa.util.sfatime import utcparse +from sfa.trust.credential_legacy import CredentialLegacy +from sfa.trust.rights import Right, Rights, determine_rights +from sfa.trust.gid import GID +from sfa.util.xrn import urn_to_hrn, hrn_authfor_hrn + +# 2 weeks, in seconds +DEFAULT_CREDENTIAL_LIFETIME = 86400 * 31 + + +# TODO: +# . make privs match between PG and PL +# . Need to add support for other types of credentials, e.g. tickets +# . add namespaces to signed-credential element? + +signature_template = \ +''' + + + + + + + + + + + + + + + + + + + + + + +''' + +# PG formats the template (whitespace) slightly differently. +# Note that they don't include the xmlns in the template, but add it later. +# Otherwise the two are equivalent. +#signature_template_as_in_pg = \ +#''' +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +#''' + +## +# Convert a string into a bool +# used to convert an xsd:boolean to a Python boolean +def str2bool(str): + if str.lower() in ['true','1']: + return True + return False + + +## +# Utility function to get the text of an XML element + +def getTextNode(element, subele): + sub = element.getElementsByTagName(subele)[0] + if len(sub.childNodes) > 0: + return sub.childNodes[0].nodeValue + else: + return None + +## +# Utility function to set the text of an XML element +# It creates the element, adds the text to it, +# and then appends it to the parent. + +def append_sub(doc, parent, element, text): + ele = doc.createElement(element) + ele.appendChild(doc.createTextNode(text)) + parent.appendChild(ele) + +## +# Signature contains information about an xmlsec1 signature +# for a signed-credential +# + +class Signature(object): + + def __init__(self, string=None): + self.refid = None + self.issuer_gid = None + self.xml = None + if string: + self.xml = string + self.decode() + + + def get_refid(self): + if not self.refid: + self.decode() + return self.refid + + def get_xml(self): + if not self.xml: + self.encode() + return self.xml + + def set_refid(self, id): + self.refid = id + + def get_issuer_gid(self): + if not self.gid: + self.decode() + return self.gid + + def set_issuer_gid(self, gid): + self.gid = gid + + def decode(self): + try: + doc = parseString(self.xml) + except ExpatError,e: + logger.log_exc ("Failed to parse credential, %s"%self.xml) + raise + sig = doc.getElementsByTagName("Signature")[0] + self.set_refid(sig.getAttribute("xml:id").strip("Sig_")) + keyinfo = sig.getElementsByTagName("X509Data")[0] + szgid = getTextNode(keyinfo, "X509Certificate") + szgid = "-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----" % szgid + self.set_issuer_gid(GID(string=szgid)) + + 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. +# +# WARNING: +# In general, a signed credential obtained externally should +# not be changed else the signature is no longer valid. So, once +# you have loaded an existing signed credential, do not call encode() or sign() on it. + +def filter_creds_by_caller(creds, caller_hrn_list): + """ + Returns a list of creds who's gid caller matches the + specified caller hrn + """ + if not isinstance(creds, list): creds = [creds] + if not isinstance(caller_hrn_list, list): + caller_hrn_list = [caller_hrn_list] + caller_creds = [] + for cred in creds: + try: + tmp_cred = Credential(string=cred) + if tmp_cred.get_gid_caller().get_hrn() in caller_hrn_list: + caller_creds.append(cred) + except: pass + return caller_creds + +class Credential(object): + + ## + # 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 + # FIXME: create and subject are ignored! + def __init__(self, create=False, subject=None, string=None, filename=None): + self.gidCaller = None + self.gidObject = None + self.expiration = None + self.privileges = None + self.issuer_privkey = None + self.issuer_gid = None + self.issuer_pubkey = None + self.parent = None + self.signature = None + self.xml = None + self.refid = None + self.legacy = None + + # 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) + else: + self.xml = str + self.decode() + + # Find an xmlsec1 path + self.xmlsec_path = '' + paths = ['/usr/bin','/usr/local/bin','/bin','/opt/bin','/opt/local/bin'] + for path in paths: + if os.path.isfile(path + '/' + 'xmlsec1'): + self.xmlsec_path = path + '/' + 'xmlsec1' + break + + def get_subject(self): + if not self.gidObject: + self.decode() + return self.gidObject.get_printable_subject() + + # sounds like this should be __repr__ instead ?? + def get_summary_tostring(self): + if not self.gidObject: + self.decode() + obj = self.gidObject.get_printable_subject() + caller = self.gidCaller.get_printable_subject() + exp = self.get_expiration() + # Summarize the rights too? The issuer? + return "[ Grant %s rights on %s until %s ]" % (caller, obj, exp) + + def get_signature(self): + if not self.signature: + self.decode() + return self.signature + + 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 + # @param gid GID of the issuing authority + + def set_issuer_keys(self, privkey, gid): + self.issuer_privkey = privkey + self.issuer_gid = gid + + + ## + # Set this credential's parent + def set_parent(self, cred): + self.parent = cred + self.updateRefID() + + ## + # 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 + + ## + # 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) + else: + logger.error ("unexpected input type in Credential.set_expiration") + + + ## + # get the lifetime of the credential (always in datetime format) + + def get_expiration(self): + if not self.expiration: + self.decode() + # 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 + # + # @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 an XML string + # This should be done immediately before signing the credential. + # WARNING: + # In general, a signed credential obtained externally should + # not be changed else the signature is no longer valid. So, once + # you have loaded an existing signed credential, do not call encode() or sign() on it. + + def encode(self): + # Create the XML document + 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. + signed_cred.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") + 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") + + doc.appendChild(signed_cred) + + # Fill in the bit + cred = doc.createElement("credential") + cred.setAttribute("xml:id", self.get_refid()) + signed_cred.appendChild(cred) + append_sub(doc, cred, "type", "privilege") + append_sub(doc, cred, "serial", "8") + append_sub(doc, cred, "owner_gid", self.gidCaller.save_to_string()) + append_sub(doc, cred, "owner_urn", self.gidCaller.get_urn()) + append_sub(doc, cred, "target_gid", self.gidObject.save_to_string()) + append_sub(doc, cred, "target_urn", self.gidObject.get_urn()) + append_sub(doc, cred, "uuid", "") + if not self.expiration: + self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME)) + self.expiration = self.expiration.replace(microsecond=0) + append_sub(doc, cred, "expires", self.expiration.isoformat()) + privileges = doc.createElement("privileges") + cred.appendChild(privileges) + + if self.privileges: + rights = self.get_privileges() + for right in rights.rights: + priv = doc.createElement("privilege") + append_sub(doc, priv, "name", right.kind) + append_sub(doc, priv, "can_delegate", str(right.delegate).lower()) + privileges.appendChild(priv) + + # Add the parent credential if it exists + if self.parent: + sdoc = parseString(self.parent.get_xml()) + # If the root node is a signed-credential (it should be), then + # get all its attributes and attach those to our signed_cred + # node. + # Specifically, PG and PLadd attributes for namespaces (which is reasonable), + # 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") + + # HOWEVER! + # PL now also declares these, with different URLs, so + # the code notices those attributes already existed with + # different values, and complains. + # This happens regularly on delegation now that PG and + # PL both declare the namespace with different URLs. + # If the content ever differs this is a problem, + # but for now it works - different URLs (values in the attributes) + # but the same actual schema, so using the PG schema + # on delegated-to-PL credentials works fine. + + # Note: you could also not copy attributes + # which already exist. It appears that both PG and PL + # will actually validate a slicecred with a parent + # signed using PG namespaces and a child signed with PL + # namespaces over the whole thing. But I don't know + # if that is a bug in xmlsec1, an accident since + # the contents of the schemas are the same, + # or something else, but it seems odd. And this works. + parentRoot = sdoc.documentElement + if parentRoot.tagName == "signed-credential" and parentRoot.hasAttributes(): + for attrIx in range(0, parentRoot.attributes.length): + attr = parentRoot.attributes.item(attrIx) + # returns the old attribute of same name that was + # on the credential + # 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 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) + + p_cred = doc.importNode(sdoc.getElementsByTagName("credential")[0], True) + p = doc.createElement("parent") + p.appendChild(p_cred) + cred.appendChild(p) + # done handling parent credential + + # Create the tag + signatures = doc.createElement("signatures") + signed_cred.appendChild(signatures) + + # Add any parent signatures + if self.parent: + for cur_cred in self.get_credential_list()[1:]: + sdoc = parseString(cur_cred.get_signature().get_xml()) + ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) + signatures.appendChild(ele) + + # Get the finished product + self.xml = doc.toxml() + + + def save_to_random_tmp_file(self): + fp, filename = mkstemp(suffix='cred', text=True) + fp = os.fdopen(fp, "w") + self.save_to_file(filename, save_parents=True, filep=fp) + return filename + + def save_to_file(self, filename, save_parents=True, filep=None): + if not self.xml: + self.encode() + if filep: + f = filep + else: + f = open(filename, "w") + f.write(self.xml) + f.close() + + def save_to_string(self, save_parents=True): + if not self.xml: + self.encode() + return self.xml + + def get_refid(self): + if not self.refid: + self.refid = 'ref0' + return self.refid + + def set_refid(self, rid): + self.refid = rid + + ## + # Figure out what refids exist, and update this credential's id + # so that it doesn't clobber the others. Returns the refids of + # the parents. + + def updateRefID(self): + if not self.parent: + self.set_refid('ref0') + return [] + + refs = [] + + next_cred = self.parent + while next_cred: + refs.append(next_cred.get_refid()) + if next_cred.parent: + next_cred = next_cred.parent + else: + next_cred = None + + + # Find a unique refid for this credential + rid = self.get_refid() + while rid in refs: + val = int(rid[3:]) + rid = "ref%d" % (val + 1) + + # Set the new refid + self.set_refid(rid) + + # Return the set of parent credential ref ids + return refs + + def get_xml(self): + if not self.xml: + self.encode() + return self.xml + + ## + # Sign the XML file created by encode() + # + # WARNING: + # In general, a signed credential obtained externally should + # not be changed else the signature is no longer valid. So, once + # you have loaded an existing signed credential, do not call encode() or sign() on it. + + def sign(self): + if not self.issuer_privkey or not self.issuer_gid: + return + doc = parseString(self.get_xml()) + sigs = doc.getElementsByTagName("signatures")[0] + + # Create the signature template to be signed + signature = Signature() + signature.set_refid(self.get_refid()) + sdoc = parseString(signature.get_xml()) + sig_ele = doc.importNode(sdoc.getElementsByTagName("Signature")[0], True) + sigs.appendChild(sig_ele) + + self.xml = doc.toxml() + + + # Split the issuer GID into multiple certificates if it's a chain + chain = GID(filename=self.issuer_gid) + gid_files = [] + while chain: + gid_files.append(chain.save_to_random_tmp_file(False)) + if chain.get_parent(): + chain = chain.get_parent() + else: + chain = None + + + # Call out to xmlsec1 to sign it + ref = 'Sig_%s' % self.get_refid() + filename = self.save_to_random_tmp_file() + signed = os.popen('%s --sign --node-id "%s" --privkey-pem %s,%s %s' \ + % (self.xmlsec_path, ref, self.issuer_privkey, ",".join(gid_files), filename)).read() + os.remove(filename) + + for gid_file in gid_files: + os.remove(gid_file) + + self.xml = signed + + # This is no longer a legacy credential + if self.legacy: + self.legacy = None + + # Update signatures + self.decode() + + + ## + # Retrieve the attributes of the credential from the XML. + # This is automatically called by the various get_* methods of + # this class and should not need to be called explicitly. + + def decode(self): + if not self.xml: + return + doc = parseString(self.xml) + sigs = [] + signed_cred = doc.getElementsByTagName("signed-credential") + + # Is this a signed-cred or just a cred? + if len(signed_cred) > 0: + creds = signed_cred[0].getElementsByTagName("credential") + signatures = signed_cred[0].getElementsByTagName("signatures") + if len(signatures) > 0: + sigs = signatures[0].getElementsByTagName("Signature") + else: + creds = doc.getElementsByTagName("credential") + + if creds is None or len(creds) == 0: + # malformed cred file + raise CredentialNotVerifiable("Malformed XML: No credential tag found") + + # Just take the first cred if there are more than one + cred = creds[0] + + 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")) + + + # Process privileges + privs = cred.getElementsByTagName("privileges")[0] + rlist = Rights() + for priv in privs.getElementsByTagName("privilege"): + kind = getTextNode(priv, "name") + deleg = str2bool(getTextNode(priv, "can_delegate")) + if kind == '*': + # Convert * into the default privileges for the credential's type + # Each inherits the delegatability from the * above + _ , type = urn_to_hrn(self.gidObject.get_urn()) + rl = determine_rights(type, self.gidObject.get_urn()) + for r in rl.rights: + r.delegate = deleg + rlist.add(r) + else: + rlist.add(Right(kind.strip(), deleg)) + self.set_privileges(rlist) + + + # Is there a parent? + parent = cred.getElementsByTagName("parent") + if len(parent) > 0: + parent_doc = parent[0].getElementsByTagName("credential")[0] + parent_xml = parent_doc.toxml() + self.parent = Credential(string=parent_xml) + self.updateRefID() + + # Assign the signatures to the credentials + for sig in sigs: + Sig = Signature(string=sig.toxml()) + + for cur_cred in self.get_credential_list(): + if cur_cred.get_refid() == Sig.get_refid(): + cur_cred.set_signature(Sig) + + + ## + # Verify + # trusted_certs: A list of trusted GID filenames (not GID objects!) + # Chaining is not supported within the GIDs by xmlsec1. + # + # trusted_certs_required: Should usually be true. Set False means an + # empty list of trusted_certs would still let this method pass. + # It just skips xmlsec1 verification et al. Only used by some utils + # + # Verify that: + # . All of the signatures are valid and that the issuers trace back + # to trusted roots (performed by xmlsec1) + # . The XML matches the credential schema + # . That the issuer of the credential is the authority in the target's urn + # . In the case of a delegated credential, this must be true of the root + # . That all of the gids presented in the credential are valid + # . Including verifying GID chains, and includ the issuer + # . The credential is not expired + # + # -- For Delegates (credentials with parents) + # . The privileges must be a subset of the parent credentials + # . The privileges must have "can_delegate" set for each delegated privilege + # . The target gid must be the same between child and parents + # . The expiry time on the child must be no later than the parent + # . The signer of the child must be the owner of the parent + # + # -- Verify does *NOT* + # . ensure that an xmlrpc client's gid matches a credential gid, that + # must be done elsewhere + # + # @param trusted_certs: The certificates of trusted CA certificates + def verify(self, trusted_certs=None, schema=None, trusted_certs_required=True): + if not self.xml: + self.decode() + + # validate against RelaxNG schema + if HAVELXML and not self.legacy: + 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) + raise CredentialNotVerifiable(message) + + if trusted_certs_required and trusted_certs is None: + trusted_certs = [] + +# trusted_cert_objects = [GID(filename=f) for f in trusted_certs] + trusted_cert_objects = [] + ok_trusted_certs = [] + # If caller explicitly passed in None that means skip cert chain validation. + # Strange and not typical + if trusted_certs is not None: + for f in trusted_certs: + try: + # Failures here include unreadable files + # or non PEM files + trusted_cert_objects.append(GID(filename=f)) + ok_trusted_certs.append(f) + except Exception, exc: + 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())) + + # 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 + if trusted_certs is not None: + # Verify the gids of this cred and of its parents + for cur_cred in self.get_credential_list(): + cur_cred.get_gid_object().verify_chain(trusted_cert_objects) + cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) + + refs = [] + refs.append("Sig_%s" % self.get_refid()) + + parentRefs = self.updateRefID() + for ref in parentRefs: + refs.append("Sig_%s" % ref) + + for ref in refs: + # If caller explicitly passed in None that means skip xmlsec1 validation. + # Strange and not typical + 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"): + # xmlsec errors have a msg= which is the interesting bit. + mstart = verified.find("msg=") + msg = "" + if mstart > -1 and len(verified) > 4: + 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())) + os.remove(filename) + + # Verify the parents (delegation) + if self.parent: + self.verify_parent(self.parent) + + # Make sure the issuer is the target's authority, and is + # itself a valid GID + self.verify_issuer(trusted_cert_objects) + return True + + ## + # Creates a list of the credential and its parents, with the root + # (original delegated credential) as the last item in the list + def get_credential_list(self): + cur_cred = self + list = [] + while cur_cred: + list.append(cur_cred) + if cur_cred.parent: + cur_cred = cur_cred.parent + else: + cur_cred = None + return list + + ## + # Make sure the credential's target gid (a) was signed by or (b) + # is the same as the entity that signed the original credential, + # or (c) is an authority over the target's namespace. + # Also ensure that the credential issuer / signer itself has a valid + # GID signature chain (signed by an authority with namespace rights). + def verify_issuer(self, trusted_gids): + root_cred = self.get_credential_list()[-1] + root_target_gid = root_cred.get_gid_object() + root_cred_signer = root_cred.get_signature().get_issuer_gid() + + # Case 1: + # Allow non authority to sign target and cred about target. + # + # Why do we need to allow non authorities to sign? + # If in the target gid validation step we correctly + # checked that the target is only signed by an authority, + # then this is just a special case of case 3. + # This short-circuit is the common case currently - + # and cause GID validation doesn't check 'authority', + # this allows users to generate valid slice credentials. + if root_target_gid.is_signed_by_cert(root_cred_signer): + # cred signer matches target signer, return success + return + + # Case 2: + # Allow someone to sign credential about themeselves. Used? + # If not, remove this. + #root_target_gid_str = root_target_gid.save_to_string() + #root_cred_signer_str = root_cred_signer.save_to_string() + #if root_target_gid_str == root_cred_signer_str: + # # cred signer is target, return success + # return + + # Case 3: + + # root_cred_signer is not the target_gid + # So this is a different gid that we have not verified. + # xmlsec1 verified the cert chain on this already, but + # it hasn't verified that the gid meets the HRN namespace + # requirements. + # Below we'll ensure that it is an authority. + # But we haven't verified that it is _signed by_ an authority + # We also don't know if xmlsec1 requires that cert signers + # are marked as CAs. + + # Note that if verify() gave us no trusted_gids then this + # call will fail. So skip it if we have no 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.") + + # 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): + #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() + if hrn_authfor_hrn(signerhrn, root_target_gid.get_hrn()): + return + + # We've required that the credential be signed by an authority + # for that domain. Reasonable and probably correct. + # A looser model would also allow the signer to be an authority + # in my control framework - eg My CA or CH. Even if it is not + # the CH that issued these, eg, user credentials. + + # 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())) + + + ## + # -- For Delegates (credentials with parents) verify that: + # . The privileges must be a subset of the parent credentials + # . The privileges must have "can_delegate" set for each delegated privilege + # . The target gid must be the same between child and parents + # . The expiry time on the child must be no later than the parent + # . The signer of the child must be the owner of the parent + 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()) + + # 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())) + + # 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())) + + # 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())) + + # Recurse + if parent_cred.parent: + parent_cred.verify_parent(parent_cred.parent) + + + def delegate(self, delegee_gidfile, caller_keyfile, caller_gidfile): + """ + Return a delegated copy of this credential, delegated to the + specified gid's user. + """ + # get the gid of the object we are delegating + object_gid = self.get_gid_object() + object_hrn = object_gid.get_hrn() + + # the hrn of the user who will be delegated to + delegee_gid = GID(filename=delegee_gidfile) + delegee_hrn = delegee_gid.get_hrn() + + #user_key = Keypair(filename=keyfile) + #user_hrn = self.get_gid_caller().get_hrn() + subject_string = "%s delegated to %s" % (object_hrn, delegee_hrn) + dcred = Credential(subject=subject_string) + dcred.set_gid_caller(delegee_gid) + dcred.set_gid_object(object_gid) + dcred.set_parent(self) + dcred.set_expiration(self.get_expiration()) + dcred.set_privileges(self.get_privileges()) + dcred.get_privileges().delegate_all_privileges(True) + #dcred.set_issuer_keys(keyfile, delegee_gidfile) + dcred.set_issuer_keys(caller_keyfile, caller_gidfile) + dcred.encode() + dcred.sign() + + return dcred + + # only informative + def get_filename(self): + return getattr(self,'filename',None) + + ## + # 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() + filename=self.get_filename() + if filename: result += "Filename %s\n"%filename + result += " privs: %s\n" % self.get_privileges().save_to_string() + gidCaller = self.get_gid_caller() + if gidCaller: + result += " gidCaller:\n" + result += gidCaller.dump_string(8, dump_parents) + + if self.get_signature(): + print " gidIssuer:" + self.get_signature().get_issuer_gid().dump(8, dump_parents) + + gidObject = self.get_gid_object() + if gidObject: + result += " gidObject:\n" + result += gidObject.dump_string(8, dump_parents) + + if self.parent and dump_parents: + result += "\nPARENT" + result += self.parent.dump_string(True) + + return result diff --git a/sfa/trust/credential_legacy.py b/sfa/trust/credential_legacy.py index 85ddc68d..b5fc449a 100644 --- a/sfa/trust/credential_legacy.py +++ b/sfa/trust/credential_legacy.py @@ -1,270 +1,270 @@ -#---------------------------------------------------------------------- -# 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. -## - - -import xmlrpclib - -from sfa.util.faults import MissingDelegateBit, ChildRightsNotSubsetOfParent -from sfa.trust.certificate import Certificate -from sfa.trust.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 +#---------------------------------------------------------------------- +# 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. +## + + +import xmlrpclib + +from sfa.util.faults import MissingDelegateBit, ChildRightsNotSubsetOfParent +from sfa.trust.certificate import Certificate +from sfa.trust.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/sfa/trust/gid.py b/sfa/trust/gid.py index 2653cf02..4f482707 100644 --- a/sfa/trust/gid.py +++ b/sfa/trust/gid.py @@ -1,258 +1,258 @@ -#---------------------------------------------------------------------- -# 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 GID. GIDs are based on certificates, and the GID class is a -# descendant of the certificate class. -## - -import xmlrpclib -import uuid - -from sfa.trust.certificate import Certificate - -from sfa.util.faults import GidInvalidParentHrn, GidParentHrn -from sfa.util.sfalogging import logger -from sfa.util.xrn import hrn_to_urn, urn_to_hrn, hrn_authfor_hrn - -## -# Create a new uuid. Returns the UUID as a string. - -def create_uuid(): - return str(uuid.uuid4().int) - -## -# GID is a tuple: -# (uuid, urn, public_key) -# -# UUID is a unique identifier and is created by the python uuid module -# (or the utility function create_uuid() in gid.py). -# -# HRN is a human readable name. It is a dotted form similar to a backward domain -# name. For example, planetlab.us.arizona.bakers. -# -# URN is a human readable identifier of form: -# "urn:publicid:IDN+toplevelauthority[:sub-auth.]*[\res. type]\ +object name" -# For example, urn:publicid:IDN+planetlab:us:arizona+user+bakers -# -# PUBLIC_KEY is the public key of the principal identified by the UUID/HRN. -# It is a Keypair object as defined in the cert.py module. -# -# It is expected that there is a one-to-one pairing between UUIDs and HRN, -# but it is uncertain how this would be inforced or if it needs to be enforced. -# -# 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 GID(Certificate): - uuid = None - hrn = None - urn = None - email = None # for adding to the SubjectAltName - - ## - # Create a new GID object - # - # @param create If true, create the X509 certificate - # @param subject If subject!=None, create the X509 cert and set the subject name - # @param string If string!=None, load the GID from a string - # @param filename If filename!=None, load the GID from a file - # @param lifeDays life of GID in days - default is 1825==5 years - - def __init__(self, create=False, subject=None, string=None, filename=None, uuid=None, hrn=None, urn=None, lifeDays=1825): - - Certificate.__init__(self, lifeDays, create, subject, string, filename) - if subject: - logger.debug("Creating GID for subject: %s" % subject) - if uuid: - self.uuid = int(uuid) - if hrn: - self.hrn = hrn - self.urn = hrn_to_urn(hrn, 'unknown') - if urn: - self.urn = urn - self.hrn, type = urn_to_hrn(urn) - - def set_uuid(self, uuid): - if isinstance(uuid, str): - self.uuid = int(uuid) - else: - self.uuid = uuid - - def get_uuid(self): - if not self.uuid: - self.decode() - return self.uuid - - def set_hrn(self, hrn): - self.hrn = hrn - - def get_hrn(self): - if not self.hrn: - self.decode() - return self.hrn - - def set_urn(self, urn): - self.urn = urn - self.hrn, type = urn_to_hrn(urn) - - def get_urn(self): - if not self.urn: - self.decode() - return self.urn - - # Will be stuffed into subjectAltName - def set_email(self, email): - self.email = email - - def get_email(self): - if not self.email: - self.decode() - return self.email - - def get_type(self): - if not self.urn: - self.decode() - _, t = urn_to_hrn(self.urn) - return t - - ## - # Encode the GID fields and package them into the subject-alt-name field - # of the X509 certificate. This must be called prior to signing the - # certificate. It may only be called once per certificate. - - def encode(self): - if self.urn: - urn = self.urn - else: - urn = hrn_to_urn(self.hrn, None) - - str = "URI:" + urn - - if self.uuid: - str += ", " + "URI:" + uuid.UUID(int=self.uuid).urn - - if self.email: - str += ", " + "email:" + self.email - - self.set_data(str, 'subjectAltName') - - - ## - # Decode the subject-alt-name field of the X509 certificate into the - # fields of the GID. This is automatically called by the various get_*() - # functions in this class. - - def decode(self): - data = self.get_data('subjectAltName') - dict = {} - if data: - if data.lower().startswith('uri:http://'): - dict = xmlrpclib.loads(data[11:])[0][0] - else: - spl = data.split(', ') - for val in spl: - if val.lower().startswith('uri:urn:uuid:'): - dict['uuid'] = uuid.UUID(val[4:]).int - elif val.lower().startswith('uri:urn:publicid:idn+'): - dict['urn'] = val[4:] - elif val.lower().startswith('email:'): - # FIXME: Ensure there isn't cruft in that address... - # EG look for email:copy,.... - dict['email'] = val[6:] - - self.uuid = dict.get("uuid", None) - self.urn = dict.get("urn", None) - self.hrn = dict.get("hrn", None) - self.email = dict.get("email", None) - if self.urn: - self.hrn = urn_to_hrn(self.urn)[0] - - ## - # Dump the credential to stdout. - # - # @param indent specifies a number of spaces to indent the output - # @param dump_parents If true, also dump the parents of the GID - - def dump(self, *args, **kwargs): - print self.dump_string(*args,**kwargs) - - def dump_string(self, indent=0, dump_parents=False): - result=" "*(indent-2) + "GID\n" - result += " "*indent + "hrn:" + str(self.get_hrn()) +"\n" - result += " "*indent + "urn:" + str(self.get_urn()) +"\n" - result += " "*indent + "uuid:" + str(self.get_uuid()) + "\n" - if self.get_email() is not None: - result += " "*indent + "email:" + str(self.get_email()) + "\n" - filename=self.get_filename() - if filename: result += "Filename %s\n"%filename - - if self.parent and dump_parents: - result += " "*indent + "parent:\n" - result += self.parent.dump_string(indent+4, dump_parents) - return result - - ## - # Verify the chain of authenticity of the GID. First perform the checks - # of the certificate class (verifying that each parent signs the child, - # etc). In addition, GIDs also confirm that the parent's HRN is a prefix - # of the child's HRN, and the parent is of type 'authority'. - # - # Verifying these prefixes prevents a rogue authority from signing a GID - # for a principal that is not a member of that authority. For example, - # planetlab.us.arizona cannot sign a GID for planetlab.us.princeton.foo. - - def verify_chain(self, trusted_certs = None): - # do the normal certificate verification stuff - trusted_root = Certificate.verify_chain(self, trusted_certs) - - 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())) - - # 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())) - - # Then recurse up the chain - ensure the parent is a trusted - # root or is in the namespace of a trusted root - self.parent.verify_chain(trusted_certs) - else: - # make sure that the trusted root's hrn is a prefix of the child's - trusted_gid = GID(string=trusted_root.save_to_string()) - trusted_type = trusted_gid.get_type() - trusted_hrn = trusted_gid.get_hrn() - #if trusted_type == 'authority': - # 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)) - - # 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 +#---------------------------------------------------------------------- +# 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 GID. GIDs are based on certificates, and the GID class is a +# descendant of the certificate class. +## + +import xmlrpclib +import uuid + +from sfa.trust.certificate import Certificate + +from sfa.util.faults import GidInvalidParentHrn, GidParentHrn +from sfa.util.sfalogging import logger +from sfa.util.xrn import hrn_to_urn, urn_to_hrn, hrn_authfor_hrn + +## +# Create a new uuid. Returns the UUID as a string. + +def create_uuid(): + return str(uuid.uuid4().int) + +## +# GID is a tuple: +# (uuid, urn, public_key) +# +# UUID is a unique identifier and is created by the python uuid module +# (or the utility function create_uuid() in gid.py). +# +# HRN is a human readable name. It is a dotted form similar to a backward domain +# name. For example, planetlab.us.arizona.bakers. +# +# URN is a human readable identifier of form: +# "urn:publicid:IDN+toplevelauthority[:sub-auth.]*[\res. type]\ +object name" +# For example, urn:publicid:IDN+planetlab:us:arizona+user+bakers +# +# PUBLIC_KEY is the public key of the principal identified by the UUID/HRN. +# It is a Keypair object as defined in the cert.py module. +# +# It is expected that there is a one-to-one pairing between UUIDs and HRN, +# but it is uncertain how this would be inforced or if it needs to be enforced. +# +# 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 GID(Certificate): + uuid = None + hrn = None + urn = None + email = None # for adding to the SubjectAltName + + ## + # Create a new GID object + # + # @param create If true, create the X509 certificate + # @param subject If subject!=None, create the X509 cert and set the subject name + # @param string If string!=None, load the GID from a string + # @param filename If filename!=None, load the GID from a file + # @param lifeDays life of GID in days - default is 1825==5 years + + def __init__(self, create=False, subject=None, string=None, filename=None, uuid=None, hrn=None, urn=None, lifeDays=1825): + + Certificate.__init__(self, lifeDays, create, subject, string, filename) + if subject: + logger.debug("Creating GID for subject: %s" % subject) + if uuid: + self.uuid = int(uuid) + if hrn: + self.hrn = hrn + self.urn = hrn_to_urn(hrn, 'unknown') + if urn: + self.urn = urn + self.hrn, type = urn_to_hrn(urn) + + def set_uuid(self, uuid): + if isinstance(uuid, str): + self.uuid = int(uuid) + else: + self.uuid = uuid + + def get_uuid(self): + if not self.uuid: + self.decode() + return self.uuid + + def set_hrn(self, hrn): + self.hrn = hrn + + def get_hrn(self): + if not self.hrn: + self.decode() + return self.hrn + + def set_urn(self, urn): + self.urn = urn + self.hrn, type = urn_to_hrn(urn) + + def get_urn(self): + if not self.urn: + self.decode() + return self.urn + + # Will be stuffed into subjectAltName + def set_email(self, email): + self.email = email + + def get_email(self): + if not self.email: + self.decode() + return self.email + + def get_type(self): + if not self.urn: + self.decode() + _, t = urn_to_hrn(self.urn) + return t + + ## + # Encode the GID fields and package them into the subject-alt-name field + # of the X509 certificate. This must be called prior to signing the + # certificate. It may only be called once per certificate. + + def encode(self): + if self.urn: + urn = self.urn + else: + urn = hrn_to_urn(self.hrn, None) + + str = "URI:" + urn + + if self.uuid: + str += ", " + "URI:" + uuid.UUID(int=self.uuid).urn + + if self.email: + str += ", " + "email:" + self.email + + self.set_data(str, 'subjectAltName') + + + ## + # Decode the subject-alt-name field of the X509 certificate into the + # fields of the GID. This is automatically called by the various get_*() + # functions in this class. + + def decode(self): + data = self.get_data('subjectAltName') + dict = {} + if data: + if data.lower().startswith('uri:http://'): + dict = xmlrpclib.loads(data[11:])[0][0] + else: + spl = data.split(', ') + for val in spl: + if val.lower().startswith('uri:urn:uuid:'): + dict['uuid'] = uuid.UUID(val[4:]).int + elif val.lower().startswith('uri:urn:publicid:idn+'): + dict['urn'] = val[4:] + elif val.lower().startswith('email:'): + # FIXME: Ensure there isn't cruft in that address... + # EG look for email:copy,.... + dict['email'] = val[6:] + + self.uuid = dict.get("uuid", None) + self.urn = dict.get("urn", None) + self.hrn = dict.get("hrn", None) + self.email = dict.get("email", None) + if self.urn: + self.hrn = urn_to_hrn(self.urn)[0] + + ## + # Dump the credential to stdout. + # + # @param indent specifies a number of spaces to indent the output + # @param dump_parents If true, also dump the parents of the GID + + def dump(self, *args, **kwargs): + print self.dump_string(*args,**kwargs) + + def dump_string(self, indent=0, dump_parents=False): + result=" "*(indent-2) + "GID\n" + result += " "*indent + "hrn:" + str(self.get_hrn()) +"\n" + result += " "*indent + "urn:" + str(self.get_urn()) +"\n" + result += " "*indent + "uuid:" + str(self.get_uuid()) + "\n" + if self.get_email() is not None: + result += " "*indent + "email:" + str(self.get_email()) + "\n" + filename=self.get_filename() + if filename: result += "Filename %s\n"%filename + + if self.parent and dump_parents: + result += " "*indent + "parent:\n" + result += self.parent.dump_string(indent+4, dump_parents) + return result + + ## + # Verify the chain of authenticity of the GID. First perform the checks + # of the certificate class (verifying that each parent signs the child, + # etc). In addition, GIDs also confirm that the parent's HRN is a prefix + # of the child's HRN, and the parent is of type 'authority'. + # + # Verifying these prefixes prevents a rogue authority from signing a GID + # for a principal that is not a member of that authority. For example, + # planetlab.us.arizona cannot sign a GID for planetlab.us.princeton.foo. + + def verify_chain(self, trusted_certs = None): + # do the normal certificate verification stuff + trusted_root = Certificate.verify_chain(self, trusted_certs) + + 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())) + + # 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())) + + # Then recurse up the chain - ensure the parent is a trusted + # root or is in the namespace of a trusted root + self.parent.verify_chain(trusted_certs) + else: + # make sure that the trusted root's hrn is a prefix of the child's + trusted_gid = GID(string=trusted_root.save_to_string()) + trusted_type = trusted_gid.get_type() + trusted_hrn = trusted_gid.get_hrn() + #if trusted_type == 'authority': + # 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)) + + # 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 diff --git a/sfa/util/enumeration.py b/sfa/util/enumeration.py index bea2ce98..b65508f8 100644 --- a/sfa/util/enumeration.py +++ b/sfa/util/enumeration.py @@ -1,35 +1,35 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- - -class Enum(set): - def __init__(self, *args, **kwds): - set.__init__(self) - enums = dict(zip(args, [object() for i in range(len(args))]), **kwds) - for (key, value) in enums.items(): - setattr(self, key, value) - self.add(eval('self.%s' % key)) - - -#def Enum2(*args, **kwds): -# enums = dict(zip(sequential, range(len(sequential))), **named) -# return type('Enum', (), enums) +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- + +class Enum(set): + def __init__(self, *args, **kwds): + set.__init__(self) + enums = dict(zip(args, [object() for i in range(len(args))]), **kwds) + for (key, value) in enums.items(): + setattr(self, key, value) + self.add(eval('self.%s' % key)) + + +#def Enum2(*args, **kwds): +# enums = dict(zip(sequential, range(len(sequential))), **named) +# return type('Enum', (), enums) diff --git a/sfa/util/faults.py b/sfa/util/faults.py index 3512e962..ab3abad0 100644 --- a/sfa/util/faults.py +++ b/sfa/util/faults.py @@ -1,352 +1,352 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- -# -# SFA API faults -# - -import xmlrpclib -from sfa.util.genicode import GENICODE - -class SfaFault(xmlrpclib.Fault): - def __init__(self, faultCode, faultString, extra = None): - if extra: - faultString += ": " + str(extra) - xmlrpclib.Fault.__init__(self, faultCode, faultString) - -class SfaInvalidAPIMethod(SfaFault): - def __init__(self, method, interface = None, extra = None): - faultString = "Invalid method " + method - if interface: - faultString += " for interface " + interface - SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) - -class SfaInvalidArgumentCount(SfaFault): - def __init__(self, got, min, max = min, extra = None): - if min != max: - expected = "%d-%d" % (min, max) - else: - expected = "%d" % min - faultString = "Expected %s arguments, got %d" % \ - (expected, got) - SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) - -class SfaInvalidArgument(SfaFault): - def __init__(self, extra = None, name = None): - if name is not None: - faultString = "Invalid %s value" % name - else: - faultString = "Invalid argument" - SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) - -class SfaAuthenticationFailure(SfaFault): - def __init__(self, extra = None): - faultString = "Failed to authenticate call" - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - -class SfaDBError(SfaFault): - def __init__(self, extra = None): - faultString = "Database error" - SfaFault.__init__(self, GENICODE.DBERROR, faultString, extra) - -class SfaPermissionDenied(SfaFault): - def __init__(self, extra = None): - faultString = "Permission denied" - SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) - -class SfaNotImplemented(SfaFault): - def __init__(self, interface=None, extra = None): - faultString = "Not implemented" - if interface: - faultString += " at interface " + interface - SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) - -class SfaAPIError(SfaFault): - def __init__(self, extra = None): - faultString = "Internal API error" - SfaFault.__init__(self, GENICODE.SERVERERROR, faultString, extra) - -class MalformedHrnException(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Malformed HRN: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, extra) - def __str__(self): - return repr(self.value) - -class TreeException(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Tree Exception: %(value)s, " % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - 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 - faultString = "Non exsiting record %(value)s, " % locals() - SfaFault.__init__(self, GENICODE.SEARCHFAILED, faultString, extra) - def __str__(self): - return repr(self.value) - -class ExistingRecord(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Existing record: %(value)s, " % locals() - SfaFault.__init__(self, GENICODE.REFUSED, faultString, extra) - def __str__(self): - return repr(self.value) - - -class InvalidRPCParams(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Invalid RPC Params: %(value)s, " % locals() - SfaFault.__init__(self, GENICODE.RPCERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -# SMBAKER exceptions follow - -class ConnectionKeyGIDMismatch(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Connection Key GID mismatch: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class MissingCallerGID(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Missing Caller GID: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class RecordNotFound(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Record not found: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class UnknownSfaType(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Unknown SFA Type: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class MissingAuthority(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Missing authority: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class PlanetLabRecordDoesNotExist(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "PlanetLab record does not exist : %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class PermissionError(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Permission error: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) - def __str__(self): - return repr(self.value) - -class InsufficientRights(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Insufficient rights: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) - def __str__(self): - return repr(self.value) - -class MissingDelegateBit(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Missing delegate bit: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) - def __str__(self): - return repr(self.value) - -class ChildRightsNotSubsetOfParent(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Child rights not subset of parent: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) - def __str__(self): - return repr(self.value) - -class CertMissingParent(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Cert missing parent: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class CertNotSignedByParent(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Cert not signed by parent: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class GidParentHrn(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Cert URN is not an extension of its parent: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class GidInvalidParentHrn(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "GID invalid parent hrn: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class SliverDoesNotExist(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Sliver does not exist : %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class BadRequestHash(xmlrpclib.Fault): - def __init__(self, hash = None, extra = None): - faultString = "bad request hash: " + str(hash) - xmlrpclib.Fault.__init__(self, GENICODE.ERROR, faultString) - -class MissingTrustedRoots(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Trusted root directory does not exist: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.SERVERERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class MissingSfaInfo(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Missing information: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class InvalidRSpec(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Invalid RSpec: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class InvalidRSpecVersion(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Invalid RSpec version: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.BADVERSION, faultString, extra) - def __str__(self): - return repr(self.value) - -class UnsupportedRSpecVersion(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Unsupported RSpec version: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) - def __str__(self): - return repr(self.value) - -class InvalidRSpecElement(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Invalid RSpec Element: %(value)s" % locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -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) - def __str__(self): - return repr(self.value) - -class AccountNotEnabled(SfaFault): - def __init__(self, extra = None): - faultString = "Account Disabled" - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class CredentialNotVerifiable(SfaFault): - def __init__(self, value, extra = None): - self.value = value - faultString = "Unable to verify credential: %(value)s, " %locals() - SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) - def __str__(self): - return repr(self.value) - -class CertExpired(SfaFault): - 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) - +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- +# +# SFA API faults +# + +import xmlrpclib +from sfa.util.genicode import GENICODE + +class SfaFault(xmlrpclib.Fault): + def __init__(self, faultCode, faultString, extra = None): + if extra: + faultString += ": " + str(extra) + xmlrpclib.Fault.__init__(self, faultCode, faultString) + +class SfaInvalidAPIMethod(SfaFault): + def __init__(self, method, interface = None, extra = None): + faultString = "Invalid method " + method + if interface: + faultString += " for interface " + interface + SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) + +class SfaInvalidArgumentCount(SfaFault): + def __init__(self, got, min, max = min, extra = None): + if min != max: + expected = "%d-%d" % (min, max) + else: + expected = "%d" % min + faultString = "Expected %s arguments, got %d" % \ + (expected, got) + SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) + +class SfaInvalidArgument(SfaFault): + def __init__(self, extra = None, name = None): + if name is not None: + faultString = "Invalid %s value" % name + else: + faultString = "Invalid argument" + SfaFault.__init__(self, GENICODE.BADARGS, faultString, extra) + +class SfaAuthenticationFailure(SfaFault): + def __init__(self, extra = None): + faultString = "Failed to authenticate call" + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + +class SfaDBError(SfaFault): + def __init__(self, extra = None): + faultString = "Database error" + SfaFault.__init__(self, GENICODE.DBERROR, faultString, extra) + +class SfaPermissionDenied(SfaFault): + def __init__(self, extra = None): + faultString = "Permission denied" + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + +class SfaNotImplemented(SfaFault): + def __init__(self, interface=None, extra = None): + faultString = "Not implemented" + if interface: + faultString += " at interface " + interface + SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) + +class SfaAPIError(SfaFault): + def __init__(self, extra = None): + faultString = "Internal API error" + SfaFault.__init__(self, GENICODE.SERVERERROR, faultString, extra) + +class MalformedHrnException(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Malformed HRN: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, extra) + def __str__(self): + return repr(self.value) + +class TreeException(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Tree Exception: %(value)s, " % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + 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 + faultString = "Non exsiting record %(value)s, " % locals() + SfaFault.__init__(self, GENICODE.SEARCHFAILED, faultString, extra) + def __str__(self): + return repr(self.value) + +class ExistingRecord(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Existing record: %(value)s, " % locals() + SfaFault.__init__(self, GENICODE.REFUSED, faultString, extra) + def __str__(self): + return repr(self.value) + + +class InvalidRPCParams(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Invalid RPC Params: %(value)s, " % locals() + SfaFault.__init__(self, GENICODE.RPCERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +# SMBAKER exceptions follow + +class ConnectionKeyGIDMismatch(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Connection Key GID mismatch: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class MissingCallerGID(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Missing Caller GID: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class RecordNotFound(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Record not found: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class UnknownSfaType(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Unknown SFA Type: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class MissingAuthority(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Missing authority: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class PlanetLabRecordDoesNotExist(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "PlanetLab record does not exist : %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class PermissionError(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Permission error: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + def __str__(self): + return repr(self.value) + +class InsufficientRights(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Insufficient rights: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + def __str__(self): + return repr(self.value) + +class MissingDelegateBit(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Missing delegate bit: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + def __str__(self): + return repr(self.value) + +class ChildRightsNotSubsetOfParent(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Child rights not subset of parent: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.FORBIDDEN, faultString, extra) + def __str__(self): + return repr(self.value) + +class CertMissingParent(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Cert missing parent: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class CertNotSignedByParent(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Cert not signed by parent: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class GidParentHrn(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Cert URN is not an extension of its parent: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class GidInvalidParentHrn(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "GID invalid parent hrn: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class SliverDoesNotExist(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Sliver does not exist : %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class BadRequestHash(xmlrpclib.Fault): + def __init__(self, hash = None, extra = None): + faultString = "bad request hash: " + str(hash) + xmlrpclib.Fault.__init__(self, GENICODE.ERROR, faultString) + +class MissingTrustedRoots(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Trusted root directory does not exist: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.SERVERERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class MissingSfaInfo(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Missing information: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class InvalidRSpec(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Invalid RSpec: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class InvalidRSpecVersion(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Invalid RSpec version: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.BADVERSION, faultString, extra) + def __str__(self): + return repr(self.value) + +class UnsupportedRSpecVersion(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Unsupported RSpec version: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.UNSUPPORTED, faultString, extra) + def __str__(self): + return repr(self.value) + +class InvalidRSpecElement(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Invalid RSpec Element: %(value)s" % locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +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) + def __str__(self): + return repr(self.value) + +class AccountNotEnabled(SfaFault): + def __init__(self, extra = None): + faultString = "Account Disabled" + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class CredentialNotVerifiable(SfaFault): + def __init__(self, value, extra = None): + self.value = value + faultString = "Unable to verify credential: %(value)s, " %locals() + SfaFault.__init__(self, GENICODE.ERROR, faultString, extra) + def __str__(self): + return repr(self.value) + +class CertExpired(SfaFault): + 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/sfa/util/genicode.py b/sfa/util/genicode.py index 61ae4895..ca201a0a 100644 --- a/sfa/util/genicode.py +++ b/sfa/util/genicode.py @@ -1,45 +1,45 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- - -from sfa.util.enumeration import Enum - -GENICODE = Enum( - SUCCESS=0, - BADARGS=1, - ERROR=2, - FORBIDDEN=3, - BADVERSION=4, - SERVERERROR=5, - TOOBIG=6, - REFUSED=7, - TIMEDOUT=8, - DBERROR=9, - RPCERROR=10, - UNAVAILABLE=11, - SEARCHFAILED=12, - UNSUPPORTED=13, - BUSY=14, - EXPIRED=15, - INPORGRESS=16, - ALREADYEXISTS=17 -) +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- + +from sfa.util.enumeration import Enum + +GENICODE = Enum( + SUCCESS=0, + BADARGS=1, + ERROR=2, + FORBIDDEN=3, + BADVERSION=4, + SERVERERROR=5, + TOOBIG=6, + REFUSED=7, + TIMEDOUT=8, + DBERROR=9, + RPCERROR=10, + UNAVAILABLE=11, + SEARCHFAILED=12, + UNSUPPORTED=13, + BUSY=14, + EXPIRED=15, + INPORGRESS=16, + ALREADYEXISTS=17 +) diff --git a/sfa/util/sfalogging.py b/sfa/util/sfalogging.py index 5529fe73..495a2747 100644 --- a/sfa/util/sfalogging.py +++ b/sfa/util/sfalogging.py @@ -1,201 +1,201 @@ -#!/usr/bin/python - -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- - -import os, sys -import traceback -import logging, logging.handlers - -CRITICAL=logging.CRITICAL -ERROR=logging.ERROR -WARNING=logging.WARNING -INFO=logging.INFO -DEBUG=logging.DEBUG - -# a logger that can handle tracebacks -class _SfaLogger: - def __init__ (self,logfile=None,loggername=None,level=logging.INFO): - # default is to locate loggername from the logfile if avail. - if not logfile: - #loggername='console' - #handler=logging.StreamHandler() - #handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) - logfile = "/var/log/sfa.log" - - if not loggername: - loggername=os.path.basename(logfile) - try: - handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=5) - except IOError: - # This is usually a permissions error becaue the file is - # owned by root, but httpd is trying to access it. - tmplogfile=os.getenv("TMPDIR", "/tmp") + os.path.sep + os.path.basename(logfile) - # In strange uses, 2 users on same machine might use same code, - # meaning they would clobber each others files - # We could (a) rename the tmplogfile, or (b) - # just log to the console in that case. - # Here we default to the console. - if os.path.exists(tmplogfile) and not os.access(tmplogfile,os.W_OK): - loggername = loggername + "-console" - handler = logging.StreamHandler() - else: - handler=logging.handlers.RotatingFileHandler(tmplogfile,maxBytes=1000000, backupCount=5) - handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) - self.logger=logging.getLogger(loggername) - self.logger.setLevel(level) - # check if logger already has the handler we're about to add - handler_exists = False - for l_handler in self.logger.handlers: - if l_handler.baseFilename == handler.baseFilename and \ - l_handler.level == handler.level: - handler_exists = True - - if not handler_exists: - self.logger.addHandler(handler) - - self.loggername=loggername - - def setLevel(self,level): - self.logger.setLevel(level) - - # shorthand to avoid having to import logging all over the place - def setLevelDebug(self): - self.logger.setLevel(logging.DEBUG) - - # define a verbose option with s/t like - # parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0) - # and pass the coresponding options.verbose to this method to adjust level - def setLevelFromOptVerbose(self,verbose): - if verbose==0: - self.logger.setLevel(logging.WARNING) - elif verbose==1: - self.logger.setLevel(logging.INFO) - elif verbose>=2: - self.logger.setLevel(logging.DEBUG) - # in case some other code needs a boolean - def getBoolVerboseFromOpt(self,verbose): - return verbose>=1 - - #################### - def info(self, msg): - self.logger.info(msg) - - def debug(self, msg): - self.logger.debug(msg) - - def warn(self, msg): - self.logger.warn(msg) - - # some code is using logger.warn(), some is using logger.warning() - def warning(self, msg): - self.logger.warning(msg) - - def error(self, msg): - self.logger.error(msg) - - def critical(self, msg): - self.logger.critical(msg) - - # logs an exception - use in an except statement - def log_exc(self,message): - self.error("%s BEG TRACEBACK"%message+"\n"+traceback.format_exc().strip("\n")) - self.error("%s END TRACEBACK"%message) - - def log_exc_critical(self,message): - self.critical("%s BEG TRACEBACK"%message+"\n"+traceback.format_exc().strip("\n")) - self.critical("%s END TRACEBACK"%message) - - # for investigation purposes, can be placed anywhere - def log_stack(self,message): - to_log="".join(traceback.format_stack()) - self.info("%s BEG STACK"%message+"\n"+to_log) - self.info("%s END STACK"%message) - - def enable_console(self, stream=sys.stdout): - formatter = logging.Formatter("%(message)s") - handler = logging.StreamHandler(stream) - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - -info_logger = _SfaLogger(loggername='info', level=logging.INFO) -debug_logger = _SfaLogger(loggername='debug', level=logging.DEBUG) -warn_logger = _SfaLogger(loggername='warning', level=logging.WARNING) -error_logger = _SfaLogger(loggername='error', level=logging.ERROR) -critical_logger = _SfaLogger(loggername='critical', level=logging.CRITICAL) -logger = info_logger -sfi_logger = _SfaLogger(logfile=os.path.expanduser("~/.sfi/")+'sfi.log',loggername='sfilog', level=logging.DEBUG) -######################################## -import time - -def profile(logger): - """ - Prints the runtime of the specified callable. Use as a decorator, e.g., - - @profile(logger) - def foo(...): - ... - """ - def logger_profile(callable): - def wrapper(*args, **kwds): - start = time.time() - result = callable(*args, **kwds) - end = time.time() - args = map(str, args) - args += ["%s = %s" % (name, str(value)) for (name, value) in kwds.iteritems()] - # should probably use debug, but then debug is not always enabled - logger.info("PROFILED %s (%s): %.02f s" % (callable.__name__, ", ".join(args), end - start)) - return result - return wrapper - return logger_profile - - -if __name__ == '__main__': - print 'testing sfalogging into logger.log' - logger1=_SfaLogger('logger.log', loggername='std(info)') - logger2=_SfaLogger('logger.log', loggername='error', level=logging.ERROR) - logger3=_SfaLogger('logger.log', loggername='debug', level=logging.DEBUG) - - for (logger,msg) in [ (logger1,"std(info)"),(logger2,"error"),(logger3,"debug")]: - - print "====================",msg, logger.logger.handlers - - logger.enable_console() - logger.critical("logger.critical") - logger.error("logger.error") - logger.warn("logger.warning") - logger.info("logger.info") - logger.debug("logger.debug") - logger.setLevel(logging.DEBUG) - logger.debug("logger.debug again") - - @profile(logger) - def sleep(seconds = 1): - time.sleep(seconds) - - logger.info('console.info') - sleep(0.5) - logger.setLevel(logging.DEBUG) - sleep(0.25) - +#!/usr/bin/python + +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- + +import os, sys +import traceback +import logging, logging.handlers + +CRITICAL=logging.CRITICAL +ERROR=logging.ERROR +WARNING=logging.WARNING +INFO=logging.INFO +DEBUG=logging.DEBUG + +# a logger that can handle tracebacks +class _SfaLogger: + def __init__ (self,logfile=None,loggername=None,level=logging.INFO): + # default is to locate loggername from the logfile if avail. + if not logfile: + #loggername='console' + #handler=logging.StreamHandler() + #handler.setFormatter(logging.Formatter("%(levelname)s %(message)s")) + logfile = "/var/log/sfa.log" + + if not loggername: + loggername=os.path.basename(logfile) + try: + handler=logging.handlers.RotatingFileHandler(logfile,maxBytes=1000000, backupCount=5) + except IOError: + # This is usually a permissions error becaue the file is + # owned by root, but httpd is trying to access it. + tmplogfile=os.getenv("TMPDIR", "/tmp") + os.path.sep + os.path.basename(logfile) + # In strange uses, 2 users on same machine might use same code, + # meaning they would clobber each others files + # We could (a) rename the tmplogfile, or (b) + # just log to the console in that case. + # Here we default to the console. + if os.path.exists(tmplogfile) and not os.access(tmplogfile,os.W_OK): + loggername = loggername + "-console" + handler = logging.StreamHandler() + else: + handler=logging.handlers.RotatingFileHandler(tmplogfile,maxBytes=1000000, backupCount=5) + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + self.logger=logging.getLogger(loggername) + self.logger.setLevel(level) + # check if logger already has the handler we're about to add + handler_exists = False + for l_handler in self.logger.handlers: + if l_handler.baseFilename == handler.baseFilename and \ + l_handler.level == handler.level: + handler_exists = True + + if not handler_exists: + self.logger.addHandler(handler) + + self.loggername=loggername + + def setLevel(self,level): + self.logger.setLevel(level) + + # shorthand to avoid having to import logging all over the place + def setLevelDebug(self): + self.logger.setLevel(logging.DEBUG) + + # define a verbose option with s/t like + # parser.add_option("-v", "--verbose", action="count", dest="verbose", default=0) + # and pass the coresponding options.verbose to this method to adjust level + def setLevelFromOptVerbose(self,verbose): + if verbose==0: + self.logger.setLevel(logging.WARNING) + elif verbose==1: + self.logger.setLevel(logging.INFO) + elif verbose>=2: + self.logger.setLevel(logging.DEBUG) + # in case some other code needs a boolean + def getBoolVerboseFromOpt(self,verbose): + return verbose>=1 + + #################### + def info(self, msg): + self.logger.info(msg) + + def debug(self, msg): + self.logger.debug(msg) + + def warn(self, msg): + self.logger.warn(msg) + + # some code is using logger.warn(), some is using logger.warning() + def warning(self, msg): + self.logger.warning(msg) + + def error(self, msg): + self.logger.error(msg) + + def critical(self, msg): + self.logger.critical(msg) + + # logs an exception - use in an except statement + def log_exc(self,message): + self.error("%s BEG TRACEBACK"%message+"\n"+traceback.format_exc().strip("\n")) + self.error("%s END TRACEBACK"%message) + + def log_exc_critical(self,message): + self.critical("%s BEG TRACEBACK"%message+"\n"+traceback.format_exc().strip("\n")) + self.critical("%s END TRACEBACK"%message) + + # for investigation purposes, can be placed anywhere + def log_stack(self,message): + to_log="".join(traceback.format_stack()) + self.info("%s BEG STACK"%message+"\n"+to_log) + self.info("%s END STACK"%message) + + def enable_console(self, stream=sys.stdout): + formatter = logging.Formatter("%(message)s") + handler = logging.StreamHandler(stream) + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + +info_logger = _SfaLogger(loggername='info', level=logging.INFO) +debug_logger = _SfaLogger(loggername='debug', level=logging.DEBUG) +warn_logger = _SfaLogger(loggername='warning', level=logging.WARNING) +error_logger = _SfaLogger(loggername='error', level=logging.ERROR) +critical_logger = _SfaLogger(loggername='critical', level=logging.CRITICAL) +logger = info_logger +sfi_logger = _SfaLogger(logfile=os.path.expanduser("~/.sfi/")+'sfi.log',loggername='sfilog', level=logging.DEBUG) +######################################## +import time + +def profile(logger): + """ + Prints the runtime of the specified callable. Use as a decorator, e.g., + + @profile(logger) + def foo(...): + ... + """ + def logger_profile(callable): + def wrapper(*args, **kwds): + start = time.time() + result = callable(*args, **kwds) + end = time.time() + args = map(str, args) + args += ["%s = %s" % (name, str(value)) for (name, value) in kwds.iteritems()] + # should probably use debug, but then debug is not always enabled + logger.info("PROFILED %s (%s): %.02f s" % (callable.__name__, ", ".join(args), end - start)) + return result + return wrapper + return logger_profile + + +if __name__ == '__main__': + print 'testing sfalogging into logger.log' + logger1=_SfaLogger('logger.log', loggername='std(info)') + logger2=_SfaLogger('logger.log', loggername='error', level=logging.ERROR) + logger3=_SfaLogger('logger.log', loggername='debug', level=logging.DEBUG) + + for (logger,msg) in [ (logger1,"std(info)"),(logger2,"error"),(logger3,"debug")]: + + print "====================",msg, logger.logger.handlers + + logger.enable_console() + logger.critical("logger.critical") + logger.error("logger.error") + logger.warn("logger.warning") + logger.info("logger.info") + logger.debug("logger.debug") + logger.setLevel(logging.DEBUG) + logger.debug("logger.debug again") + + @profile(logger) + def sleep(seconds = 1): + time.sleep(seconds) + + logger.info('console.info') + sleep(0.5) + logger.setLevel(logging.DEBUG) + sleep(0.25) + diff --git a/sfa/util/sfatime.py b/sfa/util/sfatime.py index 70f88302..48796892 100644 --- a/sfa/util/sfatime.py +++ b/sfa/util/sfatime.py @@ -1,66 +1,66 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- -from types import StringTypes -import dateutil.parser -import datetime -import time - -from sfa.util.sfalogging import logger - -DATEFORMAT = "%Y-%m-%dT%H:%M:%SZ" - -def utcparse(input): - """ Translate a string into a time using dateutil.parser.parse but make sure it's in UTC time and strip -the timezone, so that it's compatible with normal datetime.datetime objects. - -For safety this can also handle inputs that are either timestamps, or datetimes -""" - # prepare the input for the checks below by - # casting strings ('1327098335') to ints - if isinstance(input, StringTypes): - try: - input = int(input) - except ValueError: - pass - - if isinstance (input, datetime.datetime): - logger.warn ("argument to utcparse already a datetime - doing nothing") - return input - elif isinstance (input, StringTypes): - t = dateutil.parser.parse(input) - if t.utcoffset() is not None: - t = t.utcoffset() + t.replace(tzinfo=None) - return t - elif isinstance (input, (int,float,long)): - return datetime.datetime.fromtimestamp(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_utc(input): - return time.gmtime(datetime_to_epoch(input)) - -def datetime_to_epoch(input): - return int(time.mktime(input.timetuple())) +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- +from types import StringTypes +import dateutil.parser +import datetime +import time + +from sfa.util.sfalogging import logger + +DATEFORMAT = "%Y-%m-%dT%H:%M:%SZ" + +def utcparse(input): + """ Translate a string into a time using dateutil.parser.parse but make sure it's in UTC time and strip +the timezone, so that it's compatible with normal datetime.datetime objects. + +For safety this can also handle inputs that are either timestamps, or datetimes +""" + # prepare the input for the checks below by + # casting strings ('1327098335') to ints + if isinstance(input, StringTypes): + try: + input = int(input) + except ValueError: + pass + + if isinstance (input, datetime.datetime): + logger.warn ("argument to utcparse already a datetime - doing nothing") + return input + elif isinstance (input, StringTypes): + t = dateutil.parser.parse(input) + if t.utcoffset() is not None: + t = t.utcoffset() + t.replace(tzinfo=None) + return t + elif isinstance (input, (int,float,long)): + return datetime.datetime.fromtimestamp(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_utc(input): + return time.gmtime(datetime_to_epoch(input)) + +def datetime_to_epoch(input): + return int(time.mktime(input.timetuple())) diff --git a/sfa/util/xrn.py b/sfa/util/xrn.py index 0721f818..ee587a7a 100644 --- a/sfa/util/xrn.py +++ b/sfa/util/xrn.py @@ -1,268 +1,268 @@ -#---------------------------------------------------------------------- -# 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. -#---------------------------------------------------------------------- - -import re - -from sfa.util.faults import SfaAPIError - -# for convenience and smoother translation - we should get rid of these functions eventually -def get_leaf(hrn): return Xrn(hrn).get_leaf() -def get_authority(hrn): return Xrn(hrn).get_authority_hrn() -def urn_to_hrn(urn): xrn=Xrn(urn); return (xrn.hrn, xrn.type) -def hrn_to_urn(hrn,type): return Xrn(hrn, type=type).urn -def hrn_authfor_hrn(parenthrn, hrn): return Xrn.hrn_is_auth_for_hrn(parenthrn, hrn) - -class Xrn: - - ########## basic tools on HRNs - # split a HRN-like string into pieces - # this is like split('.') except for escaped (backslashed) dots - # e.g. hrn_split ('a\.b.c.d') -> [ 'a\.b','c','d'] - @staticmethod - def hrn_split(hrn): - return [ x.replace('--sep--','\\.') for x in hrn.replace('\\.','--sep--').split('.') ] - - # e.g. hrn_leaf ('a\.b.c.d') -> 'd' - @staticmethod - def hrn_leaf(hrn): return Xrn.hrn_split(hrn)[-1] - - # e.g. hrn_auth_list ('a\.b.c.d') -> ['a\.b', 'c'] - @staticmethod - def hrn_auth_list(hrn): return Xrn.hrn_split(hrn)[0:-1] - - # e.g. hrn_auth ('a\.b.c.d') -> 'a\.b.c' - @staticmethod - def hrn_auth(hrn): return '.'.join(Xrn.hrn_auth_list(hrn)) - - # e.g. escape ('a.b') -> 'a\.b' - @staticmethod - def escape(token): return re.sub(r'([^\\])\.', r'\1\.', token) - - # e.g. unescape ('a\.b') -> 'a.b' - @staticmethod - def unescape(token): return token.replace('\\.','.') - - # Return the HRN authority chain from top to bottom. - # e.g. hrn_auth_chain('a\.b.c.d') -> ['a\.b', 'a\.b.c'] - @staticmethod - def hrn_auth_chain(hrn): - parts = Xrn.hrn_auth_list(hrn) - chain = [] - for i in range(len(parts)): - chain.append('.'.join(parts[:i+1])) - # Include the HRN itself? - #chain.append(hrn) - return chain - - # Is the given HRN a true authority over the namespace of the other - # child HRN? - # A better alternative than childHRN.startswith(parentHRN) - # e.g. hrn_is_auth_for_hrn('a\.b', 'a\.b.c.d') -> True, - # but hrn_is_auth_for_hrn('a', 'a\.b.c.d') -> False - # Also hrn_is_auth_for_hrn('a\.b.c.d', 'a\.b.c.d') -> True - @staticmethod - def hrn_is_auth_for_hrn(parenthrn, hrn): - if parenthrn == hrn: - return True - for auth in Xrn.hrn_auth_chain(hrn): - if parenthrn == auth: - return True - return False - - ########## basic tools on URNs - URN_PREFIX = "urn:publicid:IDN" - URN_PREFIX_lower = "urn:publicid:idn" - - @staticmethod - def is_urn (text): - return text.lower().startswith(Xrn.URN_PREFIX_lower) - - @staticmethod - def urn_full (urn): - if Xrn.is_urn(urn): return urn - else: return Xrn.URN_PREFIX+urn - @staticmethod - def urn_meaningful (urn): - if Xrn.is_urn(urn): return urn[len(Xrn.URN_PREFIX):] - else: return urn - @staticmethod - def urn_split (urn): - return Xrn.urn_meaningful(urn).split('+') - - #################### - # the local fields that are kept consistent - # self.urn - # self.hrn - # self.type - # self.path - # provide either urn, or (hrn + type) - def __init__ (self, xrn="", type=None, id=None): - if not xrn: xrn = "" - # user has specified xrn : guess if urn or hrn - self.id = id - if Xrn.is_urn(xrn): - self.hrn=None - self.urn=xrn - if id: - self.urn = "%s:%s" % (self.urn, str(id)) - self.urn_to_hrn() - else: - self.urn=None - self.hrn=xrn - self.type=type - self.hrn_to_urn() - self._normalize() -# happens all the time .. -# if not type: -# debug_logger.debug("type-less Xrn's are not safe") - - def __repr__ (self): - result=" 1: - self.id = ":".join(parts[1:]) - name = parts[0] - hrn += '.%s' % Xrn.escape(name) - - self.hrn=str(hrn) - self.type=str(type) - - def hrn_to_urn(self): - """ - compute urn from (hrn, type) - """ - -# if not self.hrn or self.hrn.startswith(Xrn.URN_PREFIX): - if Xrn.is_urn(self.hrn): - raise SfaAPIError, "Xrn.hrn_to_urn, hrn=%s"%self.hrn - - if self.type and self.type.startswith('authority'): - self.authority = Xrn.hrn_auth_list(self.hrn) - leaf = self.get_leaf() - #if not self.authority: - # self.authority = [self.hrn] - type_parts = self.type.split("+") - self.type = type_parts[0] - name = 'sa' - if len(type_parts) > 1: - name = type_parts[1] - auth_parts = [part for part in [self.get_authority_urn(), leaf] if part] - authority_string = ":".join(auth_parts) - else: - self.authority = Xrn.hrn_auth_list(self.hrn) - name = Xrn.hrn_leaf(self.hrn) - authority_string = self.get_authority_urn() - - if self.type == None: - urn = "+".join(['',authority_string,Xrn.unescape(name)]) - else: - urn = "+".join(['',authority_string,self.type,Xrn.unescape(name)]) - - if hasattr(self, 'id') and self.id: - urn = "%s:%s" % (urn, self.id) - - self.urn = Xrn.URN_PREFIX + urn - - def dump_string(self): - result="-------------------- XRN\n" - result += "URN=%s\n"%self.urn - result += "HRN=%s\n"%self.hrn - result += "TYPE=%s\n"%self.type - result += "LEAF=%s\n"%self.get_leaf() - result += "AUTH(hrn format)=%s\n"%self.get_authority_hrn() - result += "AUTH(urn format)=%s\n"%self.get_authority_urn() - return result - +#---------------------------------------------------------------------- +# 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. +#---------------------------------------------------------------------- + +import re + +from sfa.util.faults import SfaAPIError + +# for convenience and smoother translation - we should get rid of these functions eventually +def get_leaf(hrn): return Xrn(hrn).get_leaf() +def get_authority(hrn): return Xrn(hrn).get_authority_hrn() +def urn_to_hrn(urn): xrn=Xrn(urn); return (xrn.hrn, xrn.type) +def hrn_to_urn(hrn,type): return Xrn(hrn, type=type).urn +def hrn_authfor_hrn(parenthrn, hrn): return Xrn.hrn_is_auth_for_hrn(parenthrn, hrn) + +class Xrn: + + ########## basic tools on HRNs + # split a HRN-like string into pieces + # this is like split('.') except for escaped (backslashed) dots + # e.g. hrn_split ('a\.b.c.d') -> [ 'a\.b','c','d'] + @staticmethod + def hrn_split(hrn): + return [ x.replace('--sep--','\\.') for x in hrn.replace('\\.','--sep--').split('.') ] + + # e.g. hrn_leaf ('a\.b.c.d') -> 'd' + @staticmethod + def hrn_leaf(hrn): return Xrn.hrn_split(hrn)[-1] + + # e.g. hrn_auth_list ('a\.b.c.d') -> ['a\.b', 'c'] + @staticmethod + def hrn_auth_list(hrn): return Xrn.hrn_split(hrn)[0:-1] + + # e.g. hrn_auth ('a\.b.c.d') -> 'a\.b.c' + @staticmethod + def hrn_auth(hrn): return '.'.join(Xrn.hrn_auth_list(hrn)) + + # e.g. escape ('a.b') -> 'a\.b' + @staticmethod + def escape(token): return re.sub(r'([^\\])\.', r'\1\.', token) + + # e.g. unescape ('a\.b') -> 'a.b' + @staticmethod + def unescape(token): return token.replace('\\.','.') + + # Return the HRN authority chain from top to bottom. + # e.g. hrn_auth_chain('a\.b.c.d') -> ['a\.b', 'a\.b.c'] + @staticmethod + def hrn_auth_chain(hrn): + parts = Xrn.hrn_auth_list(hrn) + chain = [] + for i in range(len(parts)): + chain.append('.'.join(parts[:i+1])) + # Include the HRN itself? + #chain.append(hrn) + return chain + + # Is the given HRN a true authority over the namespace of the other + # child HRN? + # A better alternative than childHRN.startswith(parentHRN) + # e.g. hrn_is_auth_for_hrn('a\.b', 'a\.b.c.d') -> True, + # but hrn_is_auth_for_hrn('a', 'a\.b.c.d') -> False + # Also hrn_is_auth_for_hrn('a\.b.c.d', 'a\.b.c.d') -> True + @staticmethod + def hrn_is_auth_for_hrn(parenthrn, hrn): + if parenthrn == hrn: + return True + for auth in Xrn.hrn_auth_chain(hrn): + if parenthrn == auth: + return True + return False + + ########## basic tools on URNs + URN_PREFIX = "urn:publicid:IDN" + URN_PREFIX_lower = "urn:publicid:idn" + + @staticmethod + def is_urn (text): + return text.lower().startswith(Xrn.URN_PREFIX_lower) + + @staticmethod + def urn_full (urn): + if Xrn.is_urn(urn): return urn + else: return Xrn.URN_PREFIX+urn + @staticmethod + def urn_meaningful (urn): + if Xrn.is_urn(urn): return urn[len(Xrn.URN_PREFIX):] + else: return urn + @staticmethod + def urn_split (urn): + return Xrn.urn_meaningful(urn).split('+') + + #################### + # the local fields that are kept consistent + # self.urn + # self.hrn + # self.type + # self.path + # provide either urn, or (hrn + type) + def __init__ (self, xrn="", type=None, id=None): + if not xrn: xrn = "" + # user has specified xrn : guess if urn or hrn + self.id = id + if Xrn.is_urn(xrn): + self.hrn=None + self.urn=xrn + if id: + self.urn = "%s:%s" % (self.urn, str(id)) + self.urn_to_hrn() + else: + self.urn=None + self.hrn=xrn + self.type=type + self.hrn_to_urn() + self._normalize() +# happens all the time .. +# if not type: +# debug_logger.debug("type-less Xrn's are not safe") + + def __repr__ (self): + result=" 1: + self.id = ":".join(parts[1:]) + name = parts[0] + hrn += '.%s' % Xrn.escape(name) + + self.hrn=str(hrn) + self.type=str(type) + + def hrn_to_urn(self): + """ + compute urn from (hrn, type) + """ + +# if not self.hrn or self.hrn.startswith(Xrn.URN_PREFIX): + if Xrn.is_urn(self.hrn): + raise SfaAPIError, "Xrn.hrn_to_urn, hrn=%s"%self.hrn + + if self.type and self.type.startswith('authority'): + self.authority = Xrn.hrn_auth_list(self.hrn) + leaf = self.get_leaf() + #if not self.authority: + # self.authority = [self.hrn] + type_parts = self.type.split("+") + self.type = type_parts[0] + name = 'sa' + if len(type_parts) > 1: + name = type_parts[1] + auth_parts = [part for part in [self.get_authority_urn(), leaf] if part] + authority_string = ":".join(auth_parts) + else: + self.authority = Xrn.hrn_auth_list(self.hrn) + name = Xrn.hrn_leaf(self.hrn) + authority_string = self.get_authority_urn() + + if self.type == None: + urn = "+".join(['',authority_string,Xrn.unescape(name)]) + else: + urn = "+".join(['',authority_string,self.type,Xrn.unescape(name)]) + + if hasattr(self, 'id') and self.id: + urn = "%s:%s" % (urn, self.id) + + self.urn = Xrn.URN_PREFIX + urn + + def dump_string(self): + result="-------------------- XRN\n" + result += "URN=%s\n"%self.urn + result += "HRN=%s\n"%self.hrn + result += "TYPE=%s\n"%self.type + result += "LEAF=%s\n"%self.get_leaf() + result += "AUTH(hrn format)=%s\n"%self.get_authority_hrn() + result += "AUTH(urn format)=%s\n"%self.get_authority_urn() + return result + -- 2.43.0