Michal Kleczek wrote:
On Tuesday 12 of October 2010 00:27:12 Peter Firmstone wrote:
Michal Kleczek wrote:
On Monday 11 of October 2010 15:41:46 Peter Firmstone wrote:
What about the case where a Module depends on another Module?
I'm guessing that was the intent.
Actually - no :)
There are two different things to consider:
1. Module dependencies (which I actually did not think about too much).
Actually I'm thinking there shouldn't be any dependencies, other than
the Jini Platform and ServiceAPI, which has been used to obtain the
Module.
This is true as long as we have one special ClassLoader that loads
ServiceAPIs. That is the case with current Jini and has its ramifications.
Once you think about peer class loading model where different versions of the
same class (even ServiceAPI class) can coexist you need Module dependencies.
But let's not go there at this moment.
Hi Michal,
I think we can go there now.
When you think about versioning, it requires a management framework,
since River is a platform implementation, choosing one framework would
require all downstream users do the same, it would not be widely
supported. But we can put suitable programming hooks in place, which I
think is what your trying to do with your secure marshal stream, which
may be a very good idea in concept, but probably something we're not
quite ready for, we're still trying to understand the impact of versioning.
I did raise a concern about handing a ClassLoader to downloaded code,
but the Module service is really about injecting a management framework,
it probably should be a singleton, under certain circumstances, it might
be reasonable to give the downloaded code a ClassLoader reference. To
protect the ClassLoader you could use a GuardObject.
An example version management framework we discussed earlier was OSGi,
there are two ways OSGi can be used:
1. As an embedded framework: runs inside River, in the application
Namespace, proxy's would be outside OSGi's namespace, all Service
API and Jini Platform classes would be visible to OSGi.
2. As the controlling framework: River runs inside OSGi and is under
version control.
Since we have no control over remote JVM's, we cannot dictate that
another JVM run OSGi, in case 1 or 2, we'd require the remote node to
discover our proxy. Also since we are discovering other services, we
need to honor the Service API which they have installed too.
Versioning of Service API:
If we have two versions of a Service API, they must be compatible in
both directions, neither can have additional classes or interface
methods, only their internal class implementation can change. As soon
as there is an incompatible change in the Service API, they become
different services, one cannot discover the other, this is part of the
ServiceRegistrar's contract. Since incompatible versions produce
different services, we might as well just make them different services.
However versioned Service API might be something that you utilise
between different OSGi nodes with your Module service, I'd quite happily
support this as a subproject, if you're interested in exploring it
further, it's still early days, versioning is a very difficult unsolved
topic. I'm impressed at how quickly you're able to write up your code.
You obviously have a good understanding of how to use the Jini API, we
need more people like yourself to be part of River.
I've attached some code below which requires a reflective proxy to
authenticate itself prior to allowing a URL codebase download. I know
this just takes the most appealing idea from your code, to authenticate
using a reflective proxy prior to permitting codebase downloads. The
proxy is used for nothing but authentication and the codebase string
remains, I've appended the code below.
Note this code doesn't protect against DNS cache poisoning attacks. We
could easily extend this to require the jar file be signed as per Sim's
suggestion, by a known Certificate[], currently set to null in the
CodeSource grant, easily passed into the constructor or perhaps as
static list, or perhaps even supplied by the authentication proxy.
The Certificate[] would guarantee that a DNS cache poisoning attack
couldn't work.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.river.imp.security.io;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.util.Collection;
import net.jini.io.MarshalOutputStream;
/**
* This code was inspired by Michal Kleczek's suggestions for solving the
* DOS hole during unmarshalling of untrusted code.
*
* @author Peter Firmstone
*/
public class AuthMarshalOutputStream extends MarshalOutputStream {
private final Proxy proxy;
public AuthMarshalOutputStream(OutputStream out,
Collection context,
Proxy authenticationProxy)
throws IOException
{
super(out, context);
if (authenticationProxy == null) {
throw new NullPointerException("Null Authentication Proxy");
}
proxy = authenticationProxy;
}
@Override
protected void writeAnnotation(String annotation) throws IOException {
writeObject(annotation);
writeObject(proxy);
}
}
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.river.imp.security.io;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidObjectException;
import java.io.ObjectStreamClass;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.security.Policy;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import net.jini.io.MarshalInputStream;
import net.jini.loader.ClassLoading;
import net.jini.loader.DownloadPermission;
import net.jini.security.ProxyPreparer;
import org.apache.river.api.security.PermissionGrant;
import org.apache.river.api.security.PermissionGrantBuilder;
import org.apache.river.api.security.RevokeableDynamicPolicy;
/**
* AuthMarshalInputStream requires the AuthMarshalOutputStream to send a
proxy
* in order to authenticate itself, it is best if the size of this proxy is
* kept to a minimum by only implementing ProxyPreparer and
RemoteMethodControl.
*
* Once the remote end has authenticated, the Annotation string can be read
* and a Class file returned.
*
* The AuthMarshalInputStream will dynamically grant DownloadPermission
to the
* each URL CodeSource if the Server authenticates itself.
*
* Note that the DownloadPermission is not granted to the Server Principal
* but to the CodeSource that the Server wants the client to download.
*
* Authentication is only used to determine if we trust the server to inform
* us of a suitable URL for download. Once DownloadPermission has been
granted
* to the CodeSource, the codebase (jar file) can be downloaded.
*
* Authentication is only performed once for each codebase String.
*
* @author Peter Firmstone
*/
public class AuthMarshalInputStream extends MarshalInputStream {
/**
* maps keywords for primitive types and void to corresponding
* Class objects
**/
private static final Map<String,Class> specialClasses
= new HashMap<String,Class>(9);
static {
specialClasses.put("boolean", boolean.class);
specialClasses.put("byte", byte.class);
specialClasses.put("char", char.class);
specialClasses.put("short", short.class);
specialClasses.put("int", int.class);
specialClasses.put("long", long.class);
specialClasses.put("float", float.class);
specialClasses.put("double", double.class);
specialClasses.put("void", void.class);
}
private static final DownloadPermission[] perm = {new
DownloadPermission()};
private static List<String> dynamicGrants = new ArrayList<String>();
/**
* value to pass as the "default loader" argument to loadClass and
* loadProxyClass
**/
private final ClassLoader defaultLoader;
/** true if this stream verifies codebase integrity */
private final boolean verifyCodebaseIntegrity;
/** loader to pass to Security.verifyCodebaseIntegrity */
private final ClassLoader verifierLoader;
/**
* if false, pass null codebase values to loadClass and
* loadProxyClass methods; if true, pass codebase values from
* stream class annotations
**/
private boolean usingCodebaseAnnotations;
private final ProxyPreparer preparer;
/**
* Creates a new <code>AuthMarshalInputStream</code> that reads
* marshalled data from the specified underlying
* <code>InputStream</code>.
*
* <p>This constructor passes <code>in</code> to the superclass
* constructor that has an <code>InputStream</code> parameter.
*
* <p><code>defaultLoader</code> will be passed as the
* <code>defaultLoader</code> argument to {...@link
* ClassLoading#loadClass ClassLoading.loadClass} and {...@link
* ClassLoading#loadProxyClass ClassLoading.loadProxyClass}
* whenever those methods are invoked by {...@link #resolveClass
* resolveClass} and {...@link #resolveProxyClass resolveProxyClass}.
*
* <p>If <code>verifyCodebaseIntegrity</code> is
* <code>true</code>, then the created stream will verify that all
* codebase annotation URLs that are used to load classes resolved
* by the stream provide content integrity, and whenever {...@link
* Security#verifyCodebaseIntegrity
* Security.verifyCodebaseIntegrity} is invoked to enforce that
* verification, <code>verifierLoader</code> will be passed as the
* <code>loader</code> argument. See {...@link
* ClassLoading#loadClass ClassLoading.loadClass} and {...@link
* ClassLoading#loadProxyClass ClassLoading.loadProxyClass} for
* details of how codebase integrity verification is performed.
*
* <p><code>context</code> will be used as the return value of the
* created stream's {...@link #getObjectStreamContext
* getObjectStreamContext} method.
*
* @param in the input stream to read marshalled data from
*
* @param defaultLoader the class loader value (possibly
* <code>null</code>) to pass as the <code>defaultLoader</code>
* argument to <code>ClassLoading</code> methods
*
* @param verifyCodebaseIntegrity if <code>true</code>, this
* stream will verify that codebase annotation URLs used to load
* classes resolved by this stream provide content integrity
*
* @param verifierLoader the class loader value (possibly
* <code>null</code>) to pass to
* <code>Security.verifyCodebaseIntegrity</code>, if
* <code>verifyCodebaseIntegrity</code> is <code>true</code>
*
* @param context the collection of context information objects to
* be returned by this stream's {...@link #getObjectStreamContext
* getObjectStreamContext} method
*
* @param preparer the proxy preparer used to authenticate the server
* prior to downloading any classes.
*
* @throws IOException if the superclass's constructor throws an
* <code>IOException</code>
*
* @throws SecurityException if the superclass's constructor
* throws a <code>SecurityException</code>
*
* @throws NullPointerException if <code>in</code> or
* <code>context</code> is <code>null</code>
**/
public AuthMarshalInputStream(InputStream in,
ClassLoader defaultLoader,
boolean verifyCodebaseIntegrity,
ClassLoader verifierLoader,
Collection context,
ProxyPreparer preparer )
throws IOException
{
super (in, defaultLoader, verifyCodebaseIntegrity, verifierLoader,
context);
this.defaultLoader = defaultLoader;
this.verifyCodebaseIntegrity = verifyCodebaseIntegrity;
this.verifierLoader = verifierLoader;
this.preparer = preparer;
}
// Inherit documentation from MarshalInputStream
@Override
public void useCodebaseAnnotations() {
usingCodebaseAnnotations = true;
super.useCodebaseAnnotations();
}
// Inherit documentation from MarshalInputStream
@Override
protected Class resolveClass(ObjectStreamClass classDesc)
throws IOException, ClassNotFoundException
{
if (classDesc == null) {
throw new NullPointerException();
}
// must always consume annotation written by MarshalOutputStream
String annotation = readAnnotation();
String codebase = usingCodebaseAnnotations ? annotation : null;
authenticate(codebase);
String name = classDesc.getName();
try {
return ClassLoading.loadClass(codebase,
name,
defaultLoader,
verifyCodebaseIntegrity,
verifierLoader);
} catch (ClassNotFoundException e) {
Class c = specialClasses.get(name);
if (c != null) {
return c;
} else {
throw e;
}
}
}
// Inherit documentation from MarshalInputStream
@Override
protected Class resolveProxyClass(String[] interfaceNames)
throws IOException, ClassNotFoundException
{
for (int i = 0; i < interfaceNames.length; i++) {
if (interfaceNames[i] == null) {
throw new NullPointerException();
}
}
// must always consume annotation written by MarshalOutputStream
String annotation = readAnnotation();
String codebase = usingCodebaseAnnotations ? annotation : null;
authenticate(codebase);
return ClassLoading.loadProxyClass(codebase,
interfaceNames,
defaultLoader,
verifyCodebaseIntegrity,
verifierLoader);
}
private void authenticate(String codebase)
throws IOException, ClassNotFoundException{
// Always read the proxy from the AuthMarshalInputStream
Object proxy = readObject();
if (codebase == null) return;
if (dynamicGrants.contains(codebase)) return;
try {
//Authenticate
preparer.prepareProxy(proxy);
// Dynamically Grant DownloadPermission for each URL via a
// CodeSource grant.
Policy policy = Policy.getPolicy();
if (policy instanceof RevokeableDynamicPolicy){
StringTokenizer st = new StringTokenizer(codebase); // divide by
spaces
URL[] urls = new URL[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
urls[i] = new URL(st.nextToken());
}
PermissionGrantBuilder pgb
= ((RevokeableDynamicPolicy) policy).getGrantBuilder();
pgb.permissions(perm);
int l = urls.length;
List<PermissionGrant> grants = new ArrayList<PermissionGrant>(l);
for (int i = 0; i < l; i++){
CodeSource cs = new CodeSource(urls[i], (Certificate[]) null);
PermissionGrant pg = pgb.codeSource(cs).build();
grants.add(pg);
}
((RevokeableDynamicPolicy) policy).grant(grants);
}
dynamicGrants.add(codebase);
} catch (SecurityException e) {
throw new IOException(e);
} catch (MalformedURLException e){
throw new IOException(e);
}
}
}