namespace module is gone, plxrn provides PL-specific translations
[sfa.git] / sfa / trust / credential.py
index f3fb180..5aa7631 100644 (file)
@@ -1,3 +1,25 @@
+#----------------------------------------------------------------------
+# 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
 #
 
 import os
 import datetime
-from xml.dom.minidom import Document, parseString
 from tempfile import mkstemp
-
-from sfa.trust.credential_legacy import CredentialLegacy
-from sfa.trust.rights import *
-from sfa.trust.gid import *
-from sfa.util.faults import *
-
-from sfa.util.sfalogging import logger
+from xml.dom.minidom import Document, parseString
 from dateutil.parser import parse
 
+from sfa.util.faults import *
+from sfa.util.sfalogging import sfa_logger
+from sfa.trust.certificate import Keypair
+from sfa.trust.credential_legacy import CredentialLegacy
+from sfa.trust.rights import Right, Rights
+from sfa.trust.gid import GID
+from sfa.util.xrn import urn_to_hrn
 
-
-# Two years, in seconds 
-DEFAULT_CREDENTIAL_LIFETIME = 60 * 60 * 24 * 365 * 2
+# 2 weeks, in seconds 
+DEFAULT_CREDENTIAL_LIFETIME = 86400 * 14
 
 
 # TODO:
@@ -31,7 +52,6 @@ DEFAULT_CREDENTIAL_LIFETIME = 60 * 60 * 24 * 365 * 2
 # . Need to add support for other types of credentials, e.g. tickets
 
 
-
 signature_template = \
 '''
 <Signature xml:id="Sig_%s" xmlns="http://www.w3.org/2000/09/xmldsig#">
@@ -67,7 +87,6 @@ def str2bool(str):
     return False
 
 
-
 ##
 # Utility function to get the text of an XML element
 
@@ -94,8 +113,7 @@ def append_sub(doc, parent, element, text):
 #
 
 class Signature(object):
-
-    
+   
     def __init__(self, string=None):
         self.refid = None
         self.issuer_gid = None
@@ -105,7 +123,6 @@ class Signature(object):
             self.decode()
 
 
-
     def get_refid(self):
         if not self.refid:
             self.decode()
@@ -153,10 +170,23 @@ class Signature(object):
 # 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):
+        """
+        Returns a list of creds who's gid caller matches the
+        specified caller hrn
+        """
+        if not isinstance(creds, list): creds = [creds]
+        caller_creds = []
+        for cred in creds:
+            try:
+                tmp_cred = Credential(string=cred)
+                if tmp_cred.get_gid_caller().get_hrn() == caller_hrn:
+                    caller_creds.append(cred)
+            except: pass
+        return caller_creds
 
 class Credential(object):
 
-
     ##
     # Create a Credential object
     #
@@ -164,7 +194,7 @@ class Credential(object):
     # @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
@@ -179,15 +209,13 @@ class Credential(object):
         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()
+                self.filename=filename
                 
             if str.strip().startswith("-----"):
                 self.legacy = CredentialLegacy(False,string=str)
@@ -204,6 +232,10 @@ class Credential(object):
                 self.xmlsec_path = path + '/' + 'xmlsec1'
                 break
 
+    def get_subject(self):
+        if not self.gidObject:
+            self.decode()
+        return self.gidObject.get_subject()   
 
     def get_signature(self):
         if not self.signature:
@@ -225,10 +257,9 @@ class Credential(object):
         self.gidObject = legacy.get_gid_object()
         lifetime = legacy.get_lifetime()
         if not lifetime:
-            # Default to two years
-            self.set_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
+            self.set_expiration(datetime.datetime.utcnow() + datetime.timedelta(seconds=DEFAULT_CREDENTIAL_LIFETIME))
         else:
-            self.set_lifetime(int(lifetime))
+            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())
@@ -283,43 +314,45 @@ class Credential(object):
             self.decode()
         return self.gidObject
 
+
+            
     ##
-    # set the lifetime of this credential
-    #
-    # @param lifetime lifetime of credential
-    # . if lifeTime is a datetime object, it is used for the expiration time
-    # . if lifeTime is an integer value, it is considered the number of seconds
-    #   remaining before expiration
-
-    def set_lifetime(self, lifeTime):
-        if isinstance(lifeTime, int):
-            self.expiration = datetime.timedelta(seconds=lifeTime) + datetime.datetime.utcnow()
+    # Expiration: an absolute UTC time of expiration (as either an int or datetime)
+    # 
+    def set_expiration(self, expiration):
+        if isinstance(expiration, int):
+            self.expiration = datetime.datetime.fromtimestamp(expiration)
         else:
-            self.expiration = lifeTime
+            self.expiration = expiration
+            
 
     ##
     # get the lifetime of the credential (in datetime format)
 
-    def get_lifetime(self):
+    def get_expiration(self):
         if not self.expiration:
             self.decode()
         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 RightList object
+    # @param privs either a comma-separated list of privileges of a Rights object
 
     def set_privileges(self, privs):
         if isinstance(privs, str):
-            self.privileges = RightList(string = privs)
+            self.privileges = Rights(string = privs)
         else:
             self.privileges = privs
         
 
     ##
-    # return the privileges as a RightList object
+    # return the privileges as a Rights object
 
     def get_privileges(self):
         if not self.privileges:
@@ -366,8 +399,8 @@ class Credential(object):
         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_lifetime(DEFAULT_CREDENTIAL_LIFETIME)
+        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")
@@ -420,6 +453,7 @@ class Credential(object):
             f = open(filename, "w")
         f.write(self.xml)
         f.close()
+        self.filename=filename
 
     def save_to_string(self, save_parents=True):
         if not self.xml:
@@ -524,9 +558,7 @@ class Credential(object):
             self.legacy = None
 
         # Update signatures
-        self.decode()
-
-        
+        self.decode()       
 
         
     ##
@@ -551,16 +583,15 @@ class Credential(object):
             cred = doc.getElementsByTagName("credential")[0]
         
 
-
         self.set_refid(cred.getAttribute("xml:id"))
-        self.set_lifetime(parse(getTextNode(cred, "expires")))
+        self.set_expiration(parse(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 = RightList()
+        rlist = Rights()
         for priv in privs.getElementsByTagName("privilege"):
             kind = getTextNode(priv, "name")
             deleg = str2bool(getTextNode(priv, "can_delegate"))
@@ -618,11 +649,22 @@ class Credential(object):
     #   must be done elsewhere
     #
     # @param trusted_certs: The certificates of trusted CA certificates
-    
     def verify(self, trusted_certs):
         if not self.xml:
             self.decode()        
-        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
+
+#        trusted_cert_objects = [GID(filename=f) for f in trusted_certs]
+        trusted_cert_objects = []
+        ok_trusted_certs = []
+        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:
+                sfa_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:
@@ -634,21 +676,17 @@ class Credential(object):
             return True
         
         # make sure it is not expired
-        if self.get_lifetime() < datetime.datetime.utcnow():
-            raise CredentialNotVerifiable("credential is expired")
+        if self.get_expiration() < datetime.datetime.utcnow():
+            raise CredentialNotVerifiable("Credential expired at %s" % self.expiration.isoformat())
 
         # Verify the signatures
         filename = self.save_to_random_tmp_file()
         cert_args = " ".join(['--trusted-pem %s' % x for x in trusted_certs])
 
         # 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)            
-
+            cur_cred.get_gid_caller().verify_chain(trusted_cert_objects) 
 
         refs = []
         refs.append("Sig_%s" % self.get_refid())
@@ -661,7 +699,7 @@ class Credential(object):
             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"):
-                raise CredentialNotVerifiable("xmlsec1 error: " + verified)
+                raise CredentialNotVerifiable("xmlsec1 error verifying cert: " + verified)
         os.remove(filename)
 
         # Verify the parents (delegation)
@@ -687,19 +725,43 @@ class Credential(object):
         return list
     
     ##
-    # Make sure the credential's target gid was signed by (or is the same) as the entity that signed
-    # the original credential.  
+    # Make sure the credential's target gid was signed by (or is the same) the entity that signed
+    # the original credential or an authority over that namespace.
     def verify_issuer(self):                
         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()
-        
-        if root_target_gid.is_signed_by_cert(root_cred_signer) or \
-            root_target_gid.save_to_string() == root_cred_signer.save_to_string():
-            pass
-        else:            
-            raise CredentialNotVerifiable("Could not verify credential signer")
-        
+
+        if root_target_gid.is_signed_by_cert(root_cred_signer):
+            # cred signer matches target signer, return success
+            return
+
+        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
+
+        # See if it the signer is an authority over the domain of the target
+        # 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 == 'authority'):
+            #sfa_logger().debug('Cred signer is an authority')
+            # signer is an authority, see if target is in authority's domain
+            hrn = root_cred_signer.get_hrn()
+            if root_target_gid.get_hrn().startswith(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:
@@ -707,8 +769,7 @@ class Credential(object):
     # . 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
-        
+    # . 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)
@@ -720,42 +781,78 @@ class Credential(object):
         # 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("target gid not equal between parent and child")
+            raise CredentialNotVerifiable("Target gid not equal between parent and child")
 
         # make sure my expiry time is <= my parent's
-        if not parent_cred.get_lifetime() >= self.get_lifetime():
-            raise CredentialNotVerifiable("delegated credential expires after parent")
+        if not parent_cred.get_expiration() >= self.get_expiration():
+            raise CredentialNotVerifiable("Delegated credential expires after parent")
 
         # 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 not signed by parent caller")
+            raise CredentialNotVerifiable("Delegated credential not signed by parent caller")
                 
+        # Recurse
         if parent_cred.parent:
             parent_cred.verify_parent(parent_cred.parent)
 
-    ##
-    # 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, dump_parents=False):
-        print "CREDENTIAL", self.get_subject()
 
-        print "      privs:", self.get_privileges().save_to_string()
+    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)
 
-        print "  gidCaller:"
+    # @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:
-            gidCaller.dump(8, dump_parents)
+            result += "  gidCaller:\n"
+            result += gidCaller.dump_string(8, dump_parents)
 
-        print "  gidObject:"
         gidObject = self.get_gid_object()
         if gidObject:
-            gidObject.dump(8, dump_parents)
-
+            result += "  gidObject:\n"
+            result += gidObject.dump_string(8, dump_parents)
 
         if self.parent and dump_parents:
-            print "PARENT",
-            self.parent.dump_parents()
+            result += "PARENT"
+            result += self.parent.dump_string(dump_parents)
+        return result