Authorization layer API and low level access checks.

Peter Firmstone peter.firmstone at zeus.net.au
Wed Jun 30 11:38:06 UTC 2021


A draft Authorization implementation, untested.

-- 
Regards,
  
Peter Firmstone


/**
  * Authorization class, instances contain the domains and Subject of the
  * Authorization context, used for Authorization decisions by Guard
  * implementations.  Provides static utility methods to make 
privilgedCall's
  * and record the current context.
  *
  * @author peter
  */
public final class Authorization {

     private static final ProtectionDomain MY_DOMAIN = 
Authorization.class.getProtectionDomain();

     private static final Authorization PRIVILEGED =
             new Authorization(new ProtectionDomain []{ MY_DOMAIN });

     private static final Authorization UNPRIVILEGED
         = new Authorization(
             new ProtectionDomain[]{
                 new ProtectionDomain(
                         new CodeSource(null, (Certificate [])null), null
                 )
             }
         );

     private static final ThreadLocal<Authorization> INHERITED_CONTEXT
             = new ThreadLocal();

     private static final Guard GUARD_REGISTER_CHECK =
         GuardBuilder.getInstance("RUNTIME").get("registerGuard", 
(String) null);

     private static final Guard GUARD_SUBJECT =
GuardBuilder.getInstance("AUTH").get("getSubjectFromAuthorization", null);

     private static final Set<Class<? extends Guard>> GUARDS =
             RC.set(Collections.newSetFromMap(new 
ConcurrentHashMap<>()), Ref.WEAK, 0);



     /**
      * Elevates the privileges of the Callable to those granted to the 
Subject
      * and ProtectionDomain's of the Callable and it's call stack, 
including the
      * ProtectionDomain of the caller of this method.
      *
      * @param <V>
      * @param c
      * @return
      */
     public static <V> Callable<V> privilegedCall(Callable<V> c){
         Authorization auth = INHERITED_CONTEXT.get();
         try {
             INHERITED_CONTEXT.set(PRIVILEGED);
             if (auth != null){
                 return privilegedCall(auth.getSubject(), c);
             } else {
                 return new CallableWrapper<>(new 
Authorization(captureCallerDomain(null), null), c);
             }
         } finally {
             INHERITED_CONTEXT.set(auth);
         }
     }

     /**
      * Elevates the privileges of the Callable to those granted to the 
Subject
      * and ProtectionDomain's of the Callable and it's call stack, 
including the
      * ProtectionDomain of the caller of this method.
      *
      * @param <V>
      * @param subject
      * @param c
      * @return
      */
     public static <V> Callable<V> privilegedCall(Subject subject, 
Callable<V> c){
         Authorization authorization = INHERITED_CONTEXT.get();
         try {
             INHERITED_CONTEXT.set(PRIVILEGED);
             Set<Principal> p = subject != null ? 
subject.getPrincipals() : null;
             Principal [] principals = p != null ? p.toArray(new 
Principal[p.size()]) : null;
             return new CallableWrapper<>(new 
Authorization(captureCallerDomain(principals), subject), c);
         } finally {
             INHERITED_CONTEXT.set(authorization);
         }
     }

     /**
      * Elevates the privileges of the Callable to those granted to the 
Subject
      * and ProtectionDomain's of the Callable and it's call stack, 
including the
      * ProtectionDomain of the caller of this method and the Authorization
      * context provided.
      *
      * @param <V>
      * @param ac
      * @param c
      * @return
      */
     public static <V> Callable<V> privilegedCall(Authorization ac, 
Callable<V> c){
         if (c == null) throw new IllegalArgumentException("Callable 
cannot be null");
         if (ac != null){
             Authorization authorization = INHERITED_CONTEXT.get();
             try {
                 INHERITED_CONTEXT.set(PRIVILEGED);
                 Subject subject = ac.getSubject();
                 Set<Principal> p = subject != null ? 
subject.getPrincipals() : null;
                 Principal [] principals = p != null ? p.toArray(new 
Principal[p.size()]) : null;
                 Set<ProtectionDomain> domains = 
captureCallerDomain(principals);
                 ac.checkEach((ProtectionDomain t) -> {
                     if (MY_DOMAIN.equals(t)) return;
                     if (principals != null){
                         domains.add(
                             new ProtectionDomainKey(t, principals)
                         );
                     } else {
                         domains.add(new ProtectionDomainKey(t));
                     }
                 });
                 Authorization auth = new Authorization(domains, subject);
                 return new CallableWrapper<>(auth, c);
             } finally {
                 INHERITED_CONTEXT.set(authorization);
             }
         } else {
             return privilegedCall(c);
         }
     }

     private static Set<ProtectionDomain> captureCallerDomain(Principal 
[] principals){
         Set<Option> options = new HashSet<>();
         options.add(Option.RETAIN_CLASS_REFERENCE);
         StackWalker walker = StackWalker.getInstance(options);
         List<StackFrame> frames = walker.walk(s ->
             s.dropWhile(f -> 
f.getClassName().equals(Authorization.class.getName()))
              .limit(1L) // Grab the caller who called privilegedCall.
              .collect(Collectors.toList()));
         Set<ProtectionDomain> domains = new HashSet<>();
         Iterator<StackFrame> it = frames.iterator();
         while (it.hasNext()){
             ProtectionDomain t = 
it.next().getDeclaringClass().getProtectionDomain();
             if (MY_DOMAIN.equals(t)) continue;
             if (principals != null){
                 domains.add(new ProtectionDomainKey(t, principals));
             } else {
                 domains.add(new ProtectionDomainKey(t));
             }
         }
         return domains;
     }

     /**
      * Avoids stack walk, returns an Authorization containing a 
ProtectionDomain
      * with the Principal [] of the current Subject, if any.  The 
CodeSource
      * of this domain contains a <code>null</code> URL.  If there is no 
current
      * Subject, this domain will be unprivileged.
      *
      * @return
      */
     public static Authorization getSubjectAuthorization(){
         Authorization inherited = INHERITED_CONTEXT.get();
         if (inherited == null) return UNPRIVILEGED;
         try {
             INHERITED_CONTEXT.set(PRIVILEGED);
             Subject subject = inherited.getSubject();
             Set<Principal> p = subject != null ? 
subject.getPrincipals() : null;
             Principal [] principals = p != null ? p.toArray(new 
Principal[p.size()]) : null;
             Set<ProtectionDomain> domains = new HashSet<>(1);
             domains.add(
                 new ProtectionDomainKey(
                     new CodeSource(null, (Certificate[]) null),
                     null,
                     null,
                     principals
                 )
             );
             return new Authorization(domains, subject);
         } finally {
             INHERITED_CONTEXT.set(inherited);
         }
     }

     /**
      * Performs a stack walk to obtain all domains since the {@link 
Callable#call() }
      * method was made, includes the domain of the caller of any of the 
three
      * {@link #privilegedCall(javax.security.auth.Subject, 
java.util.concurrent.Callable)
      * methods as well as the {@link Subject}.  All domains on the 
stack contain the
      * {@link Principal} of the Subject.
      *
      * If a privilegedCall wasn't made, then an unprivileged Authorization
      * instance is returned.
      *
      * @return
      */
     public static Authorization getAuthorization(){
         // Optimise, avoid stack walk if UNPRIVILEGED.
         Authorization inherited = INHERITED_CONTEXT.get();
         if (inherited == null) return UNPRIVILEGED;
         try {
             INHERITED_CONTEXT.set(PRIVILEGED);
             Subject subject = inherited.getSubject();
             Set<Principal> p = subject != null ? 
subject.getPrincipals() : null;
             Principal [] principals = p != null ? p.toArray(new 
Principal[p.size()]) : null;
             Set<Option> options = new HashSet<>();
             options.add(Option.RETAIN_CLASS_REFERENCE);
             StackWalker walker = StackWalker.getInstance(options);
             List<StackFrame> frames = walker.walk(s ->
                 s.skip(1) //Skips getAuthorization()
                  .takeWhile(f -> 
!f.getClassName().equals(CallableWrapper.class.getName())))
                  .collect(Collectors.toList());
             Set<ProtectionDomain> domains = new HashSet<>(frames.size());
             inherited.checkEach((ProtectionDomain t) -> {
                 if (MY_DOMAIN.equals(t)) return;
                 if (principals != null){
                     domains.add(new ProtectionDomainKey(t, principals));
                 } else {
                     domains.add(new ProtectionDomainKey(t));
                 }
             });
             Iterator<StackFrame> it = frames.iterator();
             while (it.hasNext()){
                 Class declaringClass = it.next().getDeclaringClass();
                 ProtectionDomain t = declaringClass.getProtectionDomain();
                 if (MY_DOMAIN.equals(t)) continue;
                 CodeSource cs = t.getCodeSource();
                 if (cs == null){ // Bootstrap ClassLoader?
                     Module module = declaringClass.getModule();
                     if (module.isNamed()){
                         try {
                             cs = new CodeSource( new URL("jrt:/" + 
module.getName()), (Certificate[]) null);
                         } catch (MalformedURLException ex) {
Logger.getLogger(Authorization.class.getName()).log(Level.SEVERE, null, ex);
                         }
                     }
                 }
                 if (principals != null){
                     domains.add(new ProtectionDomainKey(cs, 
t.getPermissions(), t.getClassLoader(), principals));
                 } else {
                     domains.add(new ProtectionDomainKey(cs, 
t.getPermissions(), t.getClassLoader(), t.getPrincipals()));
                 }
             }
             return new Authorization(domains, subject);
         } finally {
             INHERITED_CONTEXT.set(inherited);
         }
     }

     /**
      * Register the calling Class type for a Guard implementation.
      *
      * Prior to calling {@link 
Authorization#checkEach(java.util.function.Consumer)
      * a guard must register, this should be during initialization the 
ProtectionDomain
      * of the guard will be checked.  This should occur prior to
      *
      *
      * @param guardClass
      */
     public static void registerGuard(Class<? extends Guard> guardClass){
         GUARD_REGISTER_CHECK.checkGuard(guardClass);
         GUARDS.add(guardClass);
     }

     private final Set<ProtectionDomain> context;
     private final Subject subject;
     private final int hashCode;

     private Authorization(Set<ProtectionDomain> context, Subject s) {
         this.context = context;
         this.subject = s;
         int hash = 7;
         hash = 11 * hash + Objects.hashCode(context);
         hash = 11 * hash + Objects.hashCode(s);
         this.hashCode = hash;
     }

     private Authorization(ProtectionDomain [] context){
         this(new HashSet<ProtectionDomain>(Arrays.asList(context)), null);
     }


     public Subject getSubject(){
         if (!PRIVILEGED.equals(INHERITED_CONTEXT.get()))
             GUARD_SUBJECT.checkGuard(null);
         return subject;
     }

     /**
      *
      * @param consumer
      * @throws AuthorizationException
      */
     public void checkEach(Consumer<ProtectionDomain> consumer) throws 
AuthorizationException {
         Authorization authorization = INHERITED_CONTEXT.get();
         if (UNPRIVILEGED.equals(authorization)) throw new 
AuthorizationException("A privilegedCall is required to enable 
privileges.");
         if (PRIVILEGED.equals(authorization)) return; // Avoids 
circular checks.
         try {
             INHERITED_CONTEXT.set(PRIVILEGED);
             Set<Option> options = new HashSet<>();
             options.add(Option.RETAIN_CLASS_REFERENCE);
             StackWalker walker = StackWalker.getInstance(options);
             List<StackFrame> frames = walker.walk(s ->
                 s.dropWhile(f -> 
f.getClassName().equals(Authorization.class.getName()))
                  .limit(1L) // Grab the caller who called privilegedCall.
                  .collect(Collectors.toList()));
             frames.stream().forEach((StackFrame t) -> {
                 Class cl = t.getDeclaringClass();
                 if (!Guard.class.isAssignableFrom(cl) || 
!GUARDS.contains(cl)){
                     throw new AuthorizationException("Guard not 
registered: " + cl.getCanonicalName());
                 }
             });
         } finally {
             INHERITED_CONTEXT.set(authorization);
         }
         // The actual check for privileged code.
         context.stream().forEach(consumer);
     }

     @Override
     public boolean equals(Object o){
         if (this == o) return true;
         if (!(o instanceof Authorization)) return false;
         Authorization that = (Authorization) o;
         if (!this.subject.equals(that.subject)) return false;
         return this.context.equals(that.context);
     }

     @Override
     public int hashCode() {
         return hashCode;
     }

     private static class CallableWrapper<V> implements Callable<V> {


         private final Authorization authorization;
         private final Callable<V> callable;

         CallableWrapper(Authorization a, Callable<V> c){
             this.authorization = a;
             this.callable = c;
         }

         @Override
         public V call() throws Exception {
             Authorization existingContext = INHERITED_CONTEXT.get();
             INHERITED_CONTEXT.set(authorization);
             try {
                 return callable.call();
             } finally {
                 INHERITED_CONTEXT.set(existingContext);
             }
         }

     }

     /**
      * ProtectionDomainKey identity .
      */
     private static class ProtectionDomainKey extends ProtectionDomain{

         private static UriCodeSource getCodeSource(CodeSource cs){
             if (cs != null) return new UriCodeSource(cs);
             return null;
         }

         private final CodeSource codeSource;
         private final Principal[] princiPals;
         private final int hashCode;

         ProtectionDomainKey(ProtectionDomain pd){
             this(getCodeSource(pd.getCodeSource()), 
pd.getPermissions(), pd.getClassLoader(), pd.getPrincipals());
         }

         ProtectionDomainKey(ProtectionDomain pd, Principal [] p) {
             this(getCodeSource(pd.getCodeSource()), 
pd.getPermissions(), pd.getClassLoader(), p);
         }

         ProtectionDomainKey(CodeSource cs, PermissionCollection perms, 
ClassLoader cl, Principal [] p){
             this(getCodeSource(cs), perms, cl, p);
         }

         private ProtectionDomainKey(UriCodeSource urics, 
PermissionCollection perms, ClassLoader cl, Principal [] p){
             super(urics, perms, cl, p);
             this.codeSource = urics;
             this.princiPals = p;
             int hash = 7;
             hash = 29 * hash + Objects.hashCode(this.codeSource);
             hash = 29 * hash + Objects.hashCode(cl);
             hash = 29 * hash + Arrays.deepHashCode(this.princiPals);
             this.hashCode = hash;
         }

         @Override
         public boolean equals(Object obj) {
             if (this == obj) return true;
             if (obj == null) return false;
             if (getClass() != obj.getClass()) return false;
             final ProtectionDomainKey other = (ProtectionDomainKey) obj;
             if (!Objects.equals(getClassLoader(), 
other.getClassLoader())) return false;
             if (!Objects.equals(this.codeSource, other.codeSource)) 
return false;
             return Arrays.deepEquals(this.princiPals, other.princiPals);
         }

         @Override
         public int hashCode() {
             return hashCode;
         }

     }

     /**
      * To avoid CodeSource equals and hashCode methods.
      *
      * Shamelessly stolen from RFC3986URLClassLoader
      *
      * CodeSource uses DNS lookup calls to check location IP addresses are
      * equal.
      *
      * This class must not be serialized.
      */
     private static class UriCodeSource extends CodeSource {
         private static final long serialVersionUID = 1L;
         private final Uri uri;
         private final int hashCode;

         UriCodeSource(CodeSource cs){
             this(cs.getLocation(), cs.getCertificates());
         }

         UriCodeSource(URL url, Certificate [] certs){
             super(url, certs);
             Uri uRi = null;
             if (url != null){
                 try {
                     uRi = Uri.urlToUri(url);
                 } catch (URISyntaxException ex) { }//Ignore
             }
             this.uri = uRi;
             int hash = 7;
             hash = 23 * hash + (this.uri != null ? this.uri.hashCode() 
: 0);
             hash = 23 * hash + (certs != null ? Arrays.hashCode(certs) 
: 0);
             hashCode = hash;
         }

         @Override
         public int hashCode() {
             return hashCode;
         }

         @Override
         public boolean equals(Object o){
             if (!(o instanceof UriCodeSource)) return false;
             if (uri == null) return super.equals(o); // In case of 
URISyntaxException
             UriCodeSource that = (UriCodeSource) o;
             if ( !uri.equals(that.uri)) return false;
             Certificate [] mine = getCertificates();
             Certificate [] theirs = that.getCertificates();
             return Arrays.equals(mine, theirs);
         }

         public Object writeReplace() throws ObjectStreamException {
             return new CodeSource(getLocation(), getCertificates());
         }

     }
}

On 30/06/2021 10:45 am, Peter Firmstone wrote:
> Hi Daniel,
>
> That is the current intent, however identifying all methods which 
> require protecting isn't a simple process and will change with each 
> release.
>
> The simplest part is determining whether the combination of User and 
> domains calling have the necessary privileges (my current focus), the 
> difficulty is in determining the methods that require protection for 
> which Agents must be written.  It would be nice if OpenJDK could 
> create check points through which security sensitive objects are 
> passed and ensure these checkpoints are always used for passing 
> references for those types of objects, so that any new JVM code also 
> uses these methods, so that it would be easier to control and limit 
> the number of locations for which we need to write agents, or other 
> mechanisms, such as provider interfaces. Otherwise there is a lot of 
> work involved in auditing every JVM release for new code, which has 
> the potential to pass security sensitive object references.
>
> It is not advisable to run untrusted code, the JDK doesn't adequately 
> defend against untrusted code execution, eg memory consumption, 
> throwing Errors, spawning Threads, the intent is to capture the 
> privileged execution paths of software's intended functionality, using 
> integration tests, recording and constraining the software by 
> preventing other unintended privileged execution paths from being 
> executed.
>
> A use case for this is restricting (narrowing the audit scope) parsing 
> of data to the combination of audited code in a server with 
> authenticated client subjects.  The authenticated subject, represents 
> the source of the data, so if there's no authenticated client subject, 
> then we don't grant the privilege to avoid parsing of untrusted data, 
> but in addition to that, we want to also constrain parsing to a 
> particular domain / scope, which has been audited and approved for 
> parsing authenticated user data.    While we trust other libraries 
> utilised to generally do the right thing, it isn't practical to audit 
> all code that might also be capable of parsing data that we don't 
> intend to utilise, so we don't grant those libraries privileges they 
> are not required to have, in accordance with principles of least 
> privilege.
>
> The intent is simply to limit scope to make security auditing 
> practical and affordable.   Java has a very large ecosystem, so it 
> isn't practical to audit everything.  We also want authorization to be 
> simpler to deploy and less complex to utilize.
>
> Regards,
>
> Peter.
>
> On 30/06/2021 4:42 am, Daniel Latrémolière wrote:
>> Hello,
>>
>> Just for my knowledge, and if I understand your need to enforce a 
>> security policy on code potentially untrusted.
>>
>> Isn't it possible to simply create a Java agent instrumenting 
>> bytecode [1], which will replace [2] each Java method invocation, in 
>> untrusted bytecode, which is returning a potentially sensitive object 
>> [3], by a call to a generated adapter.
>>
>> In the generated adapter, you can add all useful code to validate if 
>> corresponding code is allowed to see this object, potentially sensitive.
>>
>> A Java agent, would be compatible with all Java versions and I think 
>> it would be possible to add exactly the permissions needed.
>>
>> Thanks,
>>
>> Daniel.
>>
>> [1]: 
>> https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/ClassFileTransformer.html
>>
>> [2]: Bytecode transformation, by example with ASM: 
>> https://stackoverflow.com/a/35635682
>>
>> [3]: By example, proxying each constructor or method returning an 
>> instance of class like java.io.File and java.nio.file.Path (if you 
>> want to do something like FilePermission).
>>
>>
>>
>> Le 29/06/2021 à 00:44, Peter Firmstone a écrit :
>>> I'm currently playing around with a simpler security model, where 
>>> one must escalate privileges with a privilegedCall, designed to be 
>>> submitted to an Executor, which is task / thread confined.
>>>


More information about the discuss mailing list