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.

Reply via email to