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.