This document covers the steps required to configure a client and server to make use of SSL under JINI 2.0. It uses a custom client (code included below) together with Blitz JavaSpaces but the steps are the same for any combination of client and service. In this particular example we will work towards a client obtaining a proxy for the service, verifying it and authenticating it. There are notes following on from the example at the end of the document which provide guidance on various other requirements such as getting the client authenticating to the server.
Comments and questions can be sent to me dan;at_dancres;dot_org.
This example uses a relatively simple directory setup as follows:
blitz_ssl
|
|----->client_classes (contains the example client source and classes)
|
|----->config (contains policy files, blitz javaspaces configuration files, starter configuration files etc)
| |
| |----->client (contains client keystore, truststore etc)
| |
| |----->server (contains server keystore, truststore etc)
|
|----->lib (contains the blitz .jars)
For the purposes of the examples, we assume that a JINI LUS and httpd are already up and running. We're making use of a sample from the Blitz JavaSpaces distribution which starts up an instance of the service and a suitable codebase all in one configuration file to save some effort. For other services, I assume you have the codebase up and running before trying to start your service. If you need some scripts for JINI, I recommend Brian Murphy's available from here (note that Brian provides some examples of SSL/Secure setups - DON'T use those as this worked example has a different setup which isn't compatible - i.e. it won't work! :)
See the appendices for copies of some of the configuration files (I've left out some of them, such as blitz.config which contains a lot of material we aren't interested in for this example).
Compiling the example clients (source code in the appendices) is relatively easy (my JINI 2.0 distribution is located in /Users/dan/jini/jini2_0/):
cd /Users/dan/blitz_ssl/client_classes/org/dancres/ssl/test
javac -classpath /Users/dan/jini/jini2_0/lib/jini-core.jar: \
/Users/dan/jini/jini2_0/lib/jini-ext.jar: \
/Users/dan/jini/jini2_0/lib/jsk-platform.jar: \
/Users/dan/blitz_ssl/client_classes *.java
Before an SSL connection can be used to send and receive data, a handshake step is performed for the purposes of establishing keys to use, authenticating etc. This step requires a collection of data in the form of certificates and keys. For this example, we'll be using public key cryptography. This means that client and server will exchange public keys whilst keeping their private keys to themselves. The client uses the server's public key to encrypt messages intended for the server and the server uses the client's public key to encrypt messages intended for the client.
It is important that client and server keep their private keys secure and accessible only to them. This material is kept in a keystore along with the public material. Then we copy the public material from the keystore into a truststore to be used by the SSL layer for determining whether a connecting entity is acceptable and communicating with that entity once the connection is established.
Okay, let's generate the keystores for client and server. We're going to use the JDK's keytool for this task:
keytool -keypass clientpw -storepass clientpw -keystore client/keystore -genkey -validity 1800 -alias client -dname CN=Client keytool -keypass serverpw -storepass serverpw -keystore server/keystore -genkey -validity 1800 -alias server -dname CN=Server
Now we need to extract the necessary public components from the keystores and place them in the appropriate truststores. Here's what we need to end up with:
Client_Truststore
|
|----->Client public key/certificate
|
|----->Server public key/certificate
Server_Truststore
|
|----->Server public key/certificate
|
|----->Client public key/certificate
In this particular case, we can see the files will be identical but it's worth keeping them separate. This is because the service may talk to multiple clients but no service other than Reggie whilst the client may talk to many other services that our example service is unaware of and thus the server truststore needn't contain any of the keys/certificates for these other services.
Certificates are of little use unless they are signed. The keytool will have generated self-signed certificates which are sufficient for this example but are no use for more public use. In these situations, one would need to arrange for the certificates to be signed by a certification authority (which could be one of the big commercial companies or you could setup your own authority. For more information on this topic, see here.
Now we'll actually create the truststores. The process is a little convoluted as we must first extract the information from the keystores and then generate truststores from that information:
// Extract the client certificate // keytool -keypass clientpw -storepass clientpw -keystore client/keystore -export -alias client -file client/cert // Put a copy in the client truststore // keytool -keypass trustpw -storepass trustpw -keystore client/truststore -import -noprompt -alias client -file client/cert // We also need to insert the client public certificate into the server // truststore. keytool -keypass trustpw -storepass trustpw -keystore server/truststore -import -noprompt -alias client -file client/cert // Repeat for the server certificate // keytool -keypass serverpw -storepass serverpw -keystore server/keystore -export -alias server -file server/cert keytool -keypass trustpw -storepass trustpw -keystore server/truststore -import -noprompt -alias server -file server/cert // We also need to insert the server public certificate into the client // truststore. keytool -keypass trustpw -storepass trustpw -keystore client/truststore -import -noprompt -alias server -file server/cert
Whilst the client can place constraints on a service's proxy (in this case, the client would specify Integrity.YES to get some kind of encrypted connection), it is actually up to the service to provide a proxy which can meet those needs. So we must configure the service to use the appropriate transport which, in this case, is SSL. Many services including Blitz JavaSpaces have adopted an unofficial convention of allowing configuration of their endpoint via the serverExporter configuration variable. So lets modify the configuration file to use an SSL transport:
// Some of these imports aren't needed 'til later
//
import net.jini.jeri.ssl.*;
import net.jini.jeri.*;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.security.*;
.........
private static ILFactory =
new ProxyTrustILFactory(null, null);
serverExporter = new BasicJeriExporter(SslServerEndpoint.getInstance(0),
ILFactory, false, true);
.........
Now we can run a simple test (note I've not provided the source for BasicTest - all it does is a basic lookup and a few JavaSpaces operations, it's available in the Blitz JavaSpaces distribution but I'm assuming you have your own little lookup client for your service - if you need help writing that code, see here):
// Start Server - we've enabled some debugging for interest
// Note the argument to tell the JDK where the SSL truststore is located.
java -Djavax.net.debug=ssl -Djava.security.policy=config/policy.all
-Djavax.net.ssl.trustStore=config/server/truststore -jar /Users/dan/jini/jini2_0/lib/start.jar
config/start-trans-blitz_with_httpd.config
// Start Client
//
java -Djava.security.policy=config/policy.all -Djavax.net.ssl.trustStore=config/client/truststore
-classpath /Users/dan/blitz_ssl/lib/blitz.jar:/Users/dan/jini/jini2_0/lib/jini-ext.jar: \
/Users/dan/jini/jini2_0/lib/sun-util.jar org.dancres.blitz.remote.test.BasicTest
Now we've established that our basic configuration works, it's time to make it more real. We want the client to assert constraints which ensure that:
In order to achieve this we need to do the following:
The source code for the new client (SSLClient.java) can be found in the appendices and handles our requirements for client changes. Now we need to change the Service's exporter:
.........
private static ILFactory =
new ProxyTrustILFactory(new BasicMethodConstraints(
new InvocationConstraints(Integrity.YES, null)), null);
serverExporter = new BasicJeriExporter(SslServerEndpoint.getInstance(0),
ILFactory, false, true);
.........
Now we need to ensure we have codebase integrity. This requires that we use an httpmd URL to specify our codebase location and that client and service use an appropriate protocol handler to download the codebase. The httpmd protocol adds an additional component to the URL which consists of a secure hash which can be used to verify the contents of the codebase have not been tampered with. We could do all of this statically, but we'll do it dynamically to make our lives easier. For this to work, we must modify our service start .config (see the appendices for the original) by replacing our current codebase value with:
import net.jini.url.httpmd.HttpmdUtil;
.......
private static codebase = HttpmdUtil.computeDigestCodebase(
"/Users/dan/blitz_ssl/lib",
(String) ConfigUtil.concat(new Object[] {
"httpmd://", ConfigUtil.getHostName(),
":", codebasePort, "/",
"blitz-dl.jar;sha=0"}));
.......
This uses HttpmdUtil to automatically generate the codebase annotation and substitute it at the appropriate point in our codebase URL. HttpmdUtil has a couple of methods but the one we use requires us to pass in the path to our codebase .jars and the template URL to use. Note the ;sha=0 which specifies the digest algorithm to use and also indicates the point in the URL at which substitution of the generated digest should be performed.
Now we are ready to test our new setup. First, we must restart our JINI infrastructure (I'm assuming you haven't cleared out the previous step in this example) and then we start the server as before but using our modified start .config and adding an additional command-line argument to configure the additional URL handler for httpmd:
java -Djava.protocol.handler.pkgs=net.jini.url -Djavax.t.debug=ssl
-Djava.security.policy=config/policy.all
-Djavax.net.ssl.trustStore=config/server/truststore -jar /Users/dan/jini/jini2_0/lib/start.jar
config/start-trans-blitz_with_httpd.config
Now we'll run our SSLClient which uses a BasicProxyPreparer to verify the service's proxy and assert the Integrity.YES constraint. It also needs the additional command-line argument to configure the URL handler:
java -Djava.protocol.handler.pkgs=net.jini.url -Djava.util.logging.config.file=logging.properties
-Djava.security.policy=config/policy.all -Djavax.net.ssl.trustStore=config/client/truststore
-classpath /Users/dan/blitz_ssl/client_classes:/Users/dan/jini/jini2_0/lib/jsk-platform.jar: \
/Users/dan/jini/jini2_0/lib/jini-ext.jar:/Users/dan/jini/jini2_0/lib/sun-util.jar
org.dancres.ssl.test.SSLClient
Just for fun, this command-line includes a logging.properties which configures some parts of the JINI Starter Kit to output interesting debug information (see the appendices for the contents of logging.properties).
The final step in this example, is to have our client authenticate the server. To do this requires us to:
Subject so as to be able to authenticate to the client.The necessary changes for the client can be seen in SSLClient2.java in the appendices. Basically, the client must load the required Principal (or Principals if it'll allow the server to authenticate as one of a number of Principals) and then specify appropriate constraints (ServerMinPrincipal or ServerMaxPrincipal).
The service must be modified to perform a JAAS login as the appropriate Subject and then initialize (advertise it's proxy etc.). A typical example of the kind of code required to perform a JAAS login is:
LoginContext myContext = new LoginContext("org.dancres.blitz.Client");
myContext.login();
Subject.doAsPrivileged(
myContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
exec();
return null;
}
},
null);
Fortunately, most services (including Blitz JavaSpaces) provide a configuration variable to set the LoginContext and will login accordingly using code similar to the above. The configuration variable is loginContext so, let's modify the blitz.config now:
.........
loginContext = new LoginContext("org.dancres.blitz.Server");
.........
This will ensure our service performs the necessary login but we still have some JAAS configuration to do. When the server attempts to authenticate, JAAS will attempt to locate a configuration block for the identified context (org.dancres.blitz.Server in this case). We must create a suitable login configuration and make it available to JAAS. For this example, we'll use the KeyStoreLoginModule which will allow us to make use of our existing server keystore for the purposes of authentication:
org.dancres.blitz.Server {
com.sun.security.auth.module.KeyStoreLoginModule required
keyStoreAlias="server"
keyStoreURL="file:config/server/keystore"
keyStorePasswordURL="file:config/server/password";
};
We'll put it in the file config/server/ssl.login. This block defines the authentication module we'll use, the identity we wish to authenticate as, the location of the keystore and a location from which the password to access the keystore can be found. Likely as not, you wouldn't configure password access this way as putting plaintest passwords in a file is not recommended! Now we need to put the password in config/server/password. We actually created this password when we generated the keystores at the start of the example:
cat serverpw > config/server/password
We are now ready to run our new setup. As before, shutdown the previous example (if it's running) and restart the JINI infrastructure. Now we run the server as follows:
java -Djava.security.auth.login.config=config/server/ssl.login -Djava.protocol.handler.pkgs=net.jini.url -Djavax.net.debug=ssl -Djava.security.policy=config/policy.all -Djavax.net.ssl.trustStore=config/server/truststore -jar /Users/dan/jini/jini2_0/lib/start.jar config/start-trans-blitz_with_httpd.config
Note the addition of the java.security.auth.login.config argument which tells JAAS where to look for configuration blocks in response to creation and login via LoginContext instances.
Now we can run our new client:
java -Djava.protocol.handler.pkgs=net.jini.url -Djava.util.logging.config.file=logging.properties -Djava.security.policy=config/policy.all -Djavax.net.ssl.trustStore=config/client/truststore -classpath /Users/dan/blitz_ssl/client_classes: \ /Users/dan/jini/jini2_0/lib/jsk-platform.jar:/Users/dan/jini/jini2_0/lib/jini-ext.jar: \ /Users/dan/jini/jini2_0/lib/sun-util.jar org.dancres.ssl.test.SSLClient2
That's it, we're done. What follows are some additional notes on other relevant topics.
Whilst the client has authenticated the server and verified the code, the supplier of the proxy (Reggie) has not been authenticated or validated and that provides possibilities for a third-party style of attack. To make this example bulletproof would require taking similar steps to secure and authenticate Reggie.
The client should also include additional constraints when instantiating the ProxyPreparer. An obvious addition would be to include a constraint for server authentication during the verification stage such that if any part of the trust verification process must communicate remotely (e.g. to download a trust verifier), the remote process would be required to authenticate itself appropriately (see Security.verifyObjectTrust and BasicProxyPreparer).
The process for setting up authentication of the client by the server follows a similar process to the example:
ProxyPreparer (this is service specific) in the service configuration file.If we want to secure Reggie we will need both service and client to assert appropriate constraints. ServiceDiscoveryManager can be setup appropriately via a configuration file which the client needs to load and make available as a constructor parameter. See the JavaDoc for full details but the basic process is to configure appropriate ProxyPreparers.
If we want to secure Reggie we will need both service and client to assert appropriate constraints. Like ServiceDiscoveryManager, JoinManager can be setup appropriately via a configuration file. Typically, one adds a JoinManager configuration block to the service's configuration file (Blitz JavaSpaces supports this as do the services in the JINI starter kit) but this isn't always the case. See your service's documentation for information. See the JavaDoc for full details but the basic process is to configure appropriate ProxyPreparers.
import com.sun.jini.start.ServiceDescriptor;
import com.sun.jini.start.NonActivatableServiceDescriptor;
import com.sun.jini.config.ConfigUtil;
// Starts up Blitz in non-activatable mode with an embedded httpd to save
// starting it up separately from the command-line. Several people suggested
// this change: Ussama Baggili, Olaf Bergner.
//
com.sun.jini.start {
private static codebasePort = "8081";
private static codebase = ConfigUtil.concat(new Object[] {
"http://", ConfigUtil.getHostName(), ":", codebasePort, "/",
"blitz-dl.jar"});
// Should be updated by installer
// JINI 2.0 libs should be in this directory
private static jiniRoot = "/home/dan/jini/jini2_0/lib/";
// Should be edited to point at the directory containing the je.jar
// a version of which can be found in the dbjava directory of the
// distribution
//
private static dbLib = "dbjava/je.jar";
// The directory where you installed Blitz
//
private static blitzRoot = "/home/dan/src/jini/space/";
private static blitzLib = ConfigUtil.concat(new Object[] {
blitzRoot, "lib/"
});
private static separator = System.getProperty("path.separator");
static classpath = ConfigUtil.concat(new Object[] {
jiniRoot, "jsk-platform.jar", separator, jiniRoot, "jini-ext.jar",
separator, jiniRoot, "sun-util.jar", separator, dbLib,
separator, blitzLib, "blitz.jar"});
private static config = ConfigUtil.concat(new Object[] {
blitzRoot, "config/blitz.config"});
private static policy = ConfigUtil.concat(new Object[] {
blitzRoot, "config/policy.all"});
static serviceDescriptors = new ServiceDescriptor[] {
// httpd
new NonActivatableServiceDescriptor(
"",
policy,
ConfigUtil.concat(new Object[] {jiniRoot, "tools.jar"}),
"com.sun.jini.tool.ClassServer",
new String[]{"-port",
codebasePort,
"-dir",
blitzLib,
"-verbose"}),
// Blitz
new NonActivatableServiceDescriptor(
codebase, policy, classpath,
"org.dancres.blitz.remote.BlitzServiceImpl",
new String[] { config }
)};
}
grant {
permission java.security.AllPermission "", "";
};
handlers= java.util.logging.ConsoleHandler .level=INFO # Limit the message that are printed on the console to INFO and above. java.util.logging.ConsoleHandler.level = FINEST java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter ############################################################ # Facility specific properties. # Provides extra control for each logger. ############################################################ # For example, set the com.xyz.foo logger to only log SEVERE # messages: net.jini.security.trust.level = FINEST #net.jini.loader.pref.PreferredClassProvider.level = ALL #net.jini.jeri.tcp.client.level = FINEST #net.jini.jeri.BasicInvocationHandler.level = FINEST #net.jini.jeri.connection.ConnectionManager.level = FINEST
package org.dancres.ssl.test;
import java.rmi.RMISecurityManager;
import net.jini.space.JavaSpace;
import net.jini.core.entry.Entry;
import net.jini.core.lease.Lease;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
public class SSLClient {
private void exec() throws Exception {
Lookup myFinder = new Lookup(JavaSpace.class);
JavaSpace mySpace = (JavaSpace)
new BasicProxyPreparer(true,
new BasicMethodConstraints(
new InvocationConstraints(Integrity.YES, null)),
null).prepareProxy(myFinder.getService());
System.out.println("Find JavaSpace: " + mySpace);
}
public static void main(String []args){
try{
if (System.getSecurityManager() == null)
System.setSecurityManager(new RMISecurityManager());
/*
LoginContext myContext = new LoginContext("org.dancres.blitz.Client");
myContext.login();
Subject.doAsPrivileged(
myContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
exec();
return null;
}
},
null);
System.exit(0);
*/
new SSLClient().exec();
}catch(Exception ex){
ex.printStackTrace();
}
}
}
package org.dancres.ssl.test;
import java.security.KeyStore;
import java.rmi.RMISecurityManager;
import net.jini.space.JavaSpace;
import net.jini.core.entry.Entry;
import net.jini.core.lease.Lease;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.Integrity;
import net.jini.core.constraint.ServerAuthentication;
import net.jini.core.constraint.ServerMinPrincipal;
import com.sun.jini.config.KeyStores;
import javax.security.auth.x500.X500Principal;
public class SSLClient2 {
private void exec() throws Exception {
KeyStore myStore =
KeyStores.getKeyStore("file:config/client/truststore",
null);
X500Principal myServerUser =
KeyStores.getX500Principal("server", myStore);
System.out.println("Server principle: " + myServerUser);
Lookup myFinder = new Lookup(JavaSpace.class);
InvocationConstraint[] myConstraints =
new InvocationConstraint[] {Integrity.YES, ServerAuthentication.YES,
new ServerMinPrincipal(myServerUser)};
InvocationConstraints myAllConstraints =
new InvocationConstraints(myConstraints, null);
JavaSpace mySpace = (JavaSpace)
new BasicProxyPreparer(true,
new BasicMethodConstraints(myAllConstraints),
null).prepareProxy(myFinder.getService());
System.out.println("Find JavaSpace: " + mySpace);
}
public static void main(String []args){
try{
if (System.getSecurityManager() == null)
System.setSecurityManager(new RMISecurityManager());
/*
LoginContext myContext = new LoginContext("org.dancres.blitz.Client");
myContext.login();
Subject.doAsPrivileged(
myContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
exec();
return null;
}
},
null);
System.exit(0);
*/
new SSLClient2().exec();
}catch(Exception ex){
ex.printStackTrace();
}
}
}
package org.dancres.ssl.test;
import java.io.IOException;
import java.rmi.RemoteException;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import net.jini.core.entry.Entry;
import net.jini.lookup.entry.Name;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
/**
A class which supports a simple JINI multicast lookup. It doesn't register
with any ServiceRegistrars it simply interrogates each one that's
discovered for a ServiceItem associated with the passed interface class.
i.e. The service needs to already have registered because we won't notice
new arrivals. [ServiceRegistrar is the interface implemented by JINI
lookup services].
@todo Be more dynamic in our lookups - see above
@author Dan Creswell (dan@dancres.org)
@version 1.00, 7/9/2003
*/
class Lookup implements DiscoveryListener {
private ServiceTemplate theTemplate;
private LookupDiscovery theDiscoverer;
private Object theProxy;
/**
@param aServiceInterface the class of the type of service you are
looking for. Class is usually an interface class.
*/
Lookup(Class aServiceInterface) {
this(aServiceInterface, null);
}
Lookup(Class aServiceInterface, String aName) {
Class[] myServiceTypes = new Class[] {aServiceInterface};
Entry[] myAttrs = null;
if (aName != null) {
myAttrs = new Entry[] {new Name(aName)};
}
theTemplate = new ServiceTemplate(null, myServiceTypes, myAttrs);
}
/**
Having created a Lookup (which means it now knows what type of service
you require), invoke this method to attempt to locate a service
of that type. The result should be cast to the interface of the
service you originally specified to the constructor.
@return proxy for the service type you requested - could be an rmi
stub or an intelligent proxy.
*/
Object getService() {
synchronized(this) {
if (theDiscoverer == null) {
try {
theDiscoverer =
new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
theDiscoverer.addDiscoveryListener(this);
} catch (IOException anIOE) {
System.err.println("Failed to init lookup");
anIOE.printStackTrace(System.err);
}
}
}
return waitForProxy();
}
/**
Location of a service causes the creation of some threads. Call this
method to shut those threads down either before exiting or after a
proxy has been returned from getService().
*/
void terminate() {
synchronized(this) {
if (theDiscoverer != null)
theDiscoverer.terminate();
}
}
/**
Caller of getService ends up here, blocked until we find a proxy.
@return the newly downloaded proxy
*/
private Object waitForProxy() {
synchronized(this) {
while (theProxy == null) {
try {
wait();
} catch (InterruptedException anIE) {
}
}
return theProxy;
}
}
/**
Invoked to inform a blocked client waiting in waitForProxy that
one is now available.
@param aProxy the newly downloaded proxy
*/
private void signalGotProxy(Object aProxy) {
synchronized(this) {
if (theProxy == null) {
theProxy = aProxy;
notify();
}
}
}
/**
Everytime a new ServiceRegistrar is found, we will be called back on
this interface with a reference to it. We then ask it for a service
instance of the type specified in our constructor.
*/
public void discovered(DiscoveryEvent anEvent) {
synchronized(this) {
if (theProxy != null)
return;
}
ServiceRegistrar[] myRegs = anEvent.getRegistrars();
for (int i = 0; i < myregs.length; i++) {
serviceregistrar myreg = myRegs[i];
Object myProxy = null;
try {
myProxy = myReg.lookup(theTemplate);
if (myProxy != null) {
signalGotProxy(myProxy);
break;
}
} catch (RemoteException anRE) {
System.err.println("ServiceRegistrar barfed");
anRE.printStackTrace(System.err);
}
}
}
/**
When a ServiceRegistrar "disappears" due to network partition etc.
we will be advised via a call to this method - as we only care about
new ServiceRegistrars, we do nothing here.
*/
public void discarded(DiscoveryEvent anEvent) {
}
}
© Copyright 2004 Dan Creswell