Jini from the beginning has used the Java security model to control the actions of proxies when they are run from clients. However, this does not control security of the transport aspects of remote method calls. Jini 2.0 introduced a wide-ranging set of mechanisms largely aimed at such issues as encryption, authorisation, etc
Prior to version 2.0, Jini just used the standard Java security mechanism. This was designed to deal with code downloaded from a remote location, and was put in place to limit what foreign code could be in a local virtual machine. This is a capability-based model, in which the client grants to foreign code the capability of performing certain activities. So for example, foreign code cannot write to the local file system unless this permission has been granted. This was covered in detail on the chapter on Basic Security.
What this has ignored is a range of issues concerning the network transport. For example
Integrity: have the classes and instance data reached the client in the form they started, or has someone along the way corrupted them?
Authentication: did the data come from whom you expected, or did it come from someone else? The client may need to authenticate itself to the server, or vice-versa
Authorisation: once you know who it came from, what rights will you grant it? This was covered in the Basic Security chapter
Confidentiality: has the data been encrypted so that others cannot read it?
These are standard network security concerns. However, Jini gives them some special wrinkles. For example, instance data for a proxy may be sent from a server to a client either directly or via a lookup server. In addition to the instance data, class files often have to be loaded, and these may come from a third party HTTP server. There are even subtleties in where a service may be running: an activatable service doesn't run in the server that started it, but in a third party activation service.
The security considerations act as constraints on normal execution: something that might have been allowed will be restricted. For example, a client may put the constraint that communications be encrypted. It might not want to know many details of how this has been done (that sort of detail can be left to the middleware itself). But if it hasn't been done, then it will just not accept the communication.
Jini 2.0 defines a set of objects that specify constraints on behaviour. They don't specify how a constraint is implemented, just what the constraint is. Some of these are
In between YES and NO is DON'T CARE. There is no specific object to express this constraint (or lack of it). If you don't care whether it is checked or not, then you don't specify either a YES or NO constraint - you just don't mention the constraint at all
Similarly, in between YES and NO is DON'T CARE. There is no specific object to express this constraint (or lack of it). You just don't use either the YES or NO object to mean that you don't care if it is confidential or not - this is common to all constraints
Each
An
class InvocationConstraints {
InvocationConstraints(Collection reqs, Collection prefs);
InvocationConstraints(InvocationConstraint[] reqs,
InvocationConstraint[] prefs);
InvocationConstraints(InvocationConstraint req,
InvocationConstraint pref);
...
}
Whenever a method call is made, constraint checks should be made.
For example, a bank method
The
class BasicMethodConstraints {
BasicMethodConstraints(InvocationConstraints constraints);
BasicMethodConstraints(BasicMethodConstraints.MethodDesc[] descs);
...
}
It is difficult to get security right, and hard to debug. The
Sample code to get logging information from the client is
static final String TRUST_LOG = "net.jini.security.trust";
static final String INTEGRITY_LOG = "net.jini.security.integrity";
static final String POLICY_LOG = "net.jini.security.policy";
static final Logger trustLogger = Logger.getLogger(TRUST_LOG);
static final Logger integrityLogger = Logger.getLogger(INTEGRITY_LOG);
static final Logger policyLogger = Logger.getLogger(POLICY_LOG);
private static FileHandler trustFh;
private static FileHandler integrityFh;
private static FileHandler policyFh;
private static void installLoggers() {
try {
// this handler will save ALL log messages in the file
trustFh = new FileHandler("log.client.trust.txt");
integrityFh = new FileHandler("log.client.integrity.txt");
policyFh = new FileHandler("log.client.policy.txt");
// the format is simple rather than XML
trustFh.setFormatter(new SimpleFormatter());
integrityFh.setFormatter(new SimpleFormatter());
policyFh.setFormatter(new SimpleFormatter());
trustLogger.addHandler(trustFh);
integrityLogger.addHandler(integrityFh);
policyLogger.addHandler(policyFh);
trustLogger.setLevel(java.util.logging.Level.ALL);
integrityLogger.setLevel(java.util.logging.Level.ALL);
policyLogger.setLevel(java.util.logging.Level.ALL);
} catch(Exception e) {
e.printStackTrace();
}
}
There is a range of different protocols that can be used to shift data around the network. These include TCP and HTTP (which of course is layered above TCP), and those designed with security in mind such as HTTPS, SSL (now officially TLS) and others.
A service uses TCP by using a
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new BasicILFactory());
}
TCP does not support any of the security mechanisms of this chapter. So we use it as a "bad example" once and then no longer consider it.
The server can use
Jeri over SSL, by a configuration such as
/* Configuration source file for an SSL server */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require integrity for all methods */
new BasicMethodConstraints(
new InvocationConstraints(
(InvocationConstraint[]) null,
(InvocationConstraint[]) null)),
/* No Permission */
null
)
);
}
SSL is designed to support encryption using a secret key mechanism
following open negotiation. It can also support authentication
of both client and server using public key certificates.
When a client gets a proxy from a server, the server may already have
placed some constraints on it. But any of these constraints are those that
the server requires, not those that the client may require. So the
client has to set its own constraints on the service proxy. It does
this by creating a new proxy from the original by adding in its own
constraints. The is described by both an interface and sample
implementations. The interface is
interface ProxyPreparer {
Object prepareProxy(Object proxy)
throws RemoteException;
}
and an implementation is
class BasicProxyPreparer {
BasicProxyPreparer();
BasicProxyPreparer(boolean verify,
MethodConstraints methodConstraints,
Permission[] permissions);
BasicProxyPreparer(boolean verify,
Permission[] permissions);
}
The second constructor is the one most likely to be used by a client:
get a proxy from a service, create a
basic proxy preparer with constraints and permissions (and whether
or not to verify the proxy - see later) and use this to
prepare a new proxy with the constraints
and permissions. The new proxy is then used for all calls on the
service.
A client that finds a file classifier and prepares a new service proxy, taking the proxy preparer from a configuration file is
package client;
import common.FileClassifier;
import common.MIMEType;
import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import java.rmi.RemoteException;
import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import java.util.logging.*;
/**
* TestFileClassifierProxyPreparer.java
*/
public class TestFileClassifierProxyPreparer implements DiscoveryListener {
private Configuration config;
static final String TRUST_LOG = "net.jini.security.trust";
static final String INTEGRITY_LOG = "net.jini.security.integrity";
static final String POLICY_LOG = "net.jini.security.policy";
static final Logger trustLogger = Logger.getLogger(TRUST_LOG);
static final Logger integrityLogger = Logger.getLogger(INTEGRITY_LOG);
static final Logger policyLogger = Logger.getLogger(POLICY_LOG);
private static FileHandler trustFh;
private static FileHandler integrityFh;
private static FileHandler policyFh;
public static void main(String argv[])
throws ConfigurationException {
installLoggers();
new TestFileClassifierProxyPreparer(argv);
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(100000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public TestFileClassifierProxyPreparer(String[] argv)
throws ConfigurationException {
config = ConfigurationProvider.getInstance(argv);
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
private static void installLoggers() {
try {
// this handler will save ALL log messages in the file
trustFh = new FileHandler("log.client.trust.txt");
integrityFh = new FileHandler("log.client.integrity.txt");
policyFh = new FileHandler("log.client.policy.txt");
// the format is simple rather than XML
trustFh.setFormatter(new SimpleFormatter());
integrityFh.setFormatter(new SimpleFormatter());
policyFh.setFormatter(new SimpleFormatter());
trustLogger.addHandler(trustFh);
integrityLogger.addHandler(integrityFh);
policyLogger.addHandler(policyFh);
trustLogger.setLevel(java.util.logging.Level.ALL);
integrityLogger.setLevel(java.util.logging.Level.ALL);
policyLogger.setLevel(java.util.logging.Level.ALL);
} catch(Exception e) {
e.printStackTrace();
}
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class [] classes = new Class[] {FileClassifier.class};
FileClassifier classifier = null;
ServiceTemplate template = new ServiceTemplate(null, classes,
null);
for (int n = 0; n < registrars.length; n++) {
System.out.println("Lookup service found");
ServiceRegistrar registrar = registrars[n];
try {
classifier = (FileClassifier) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(4);
continue;
}
if (classifier == null) {
System.out.println("Classifier null");
continue;
}
System.out.println("Getting the proxy");
// Get the proxy preparer
ProxyPreparer preparer = null;
try {
preparer =
(ProxyPreparer) config.getEntry(
"client.TestFileClassifierProxyPreparer",
"preparer", ProxyPreparer.class,
new BasicProxyPreparer());
} catch(ConfigurationException e) {
e.printStackTrace();
preparer = new BasicProxyPreparer();
}
// Prepare the new proxy
System.out.println("Preparing the proxy");
try {
classifier = (FileClassifier) preparer.prepareProxy(classifier);
} catch(RemoteException e) {
e.printStackTrace();
System.exit(3);
} catch(java.lang.SecurityException e) {
e.printStackTrace();
System.exit(6);
}
// Use the service to classify a few file types
System.out.println("Calling the proxy");
MIMEType type;
try {
String fileName;
fileName = "file1.txt";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file2.rtf";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file3.abc";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
} catch(java.rmi.RemoteException e) {
System.out.println("Failed to call method");
System.err.println(e.toString());
System.exit(5);
continue;
}
// success
System.exit(0);
}
}
private void printType(String fileName, MIMEType type) {
System.out.print("Type of " + fileName + " is ");
if (type == null) {
System.out.println("null");
} else {
System.out.println(type.toString());
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // TestFileClassifier
A minimal configuration file for this client is
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Don't verify the proxy. */
false,
/* No constraints */
new BasicMethodConstraints(
new InvocationConstraints(
(InvocationConstraint[]) null,
(InvocationConstraint[]) null
)
),
new Permission[] {}
);
}
This can be run directly by
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-minimal.config
or from the Ant build files by
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-minimal.config
This client will run successfully with any service that does not impose any constraints on the client. So, for example, it will run with any service of earlier chapters which do not impose any contraints at all. However, using this configuration it will not run with some of the examples later in this chapter which do impose client-side constraints.
A file classifier server using configuration has already been given in the chapter Configuration. The version here is almost the same, with the addition of placing the service name in the configuration (since we might need to run different versions of the service for different security requirements).
package config;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.ExportException;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID ;
import net.jini.lease.LeaseListener;
import net.jini.lease.LeaseRenewalEvent;
import net.jini.lease.LeaseRenewalManager;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import net.jini.lookup.JoinManager;
import net.jini.id.UuidFactory;
import net.jini.id.Uuid;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.export.Exporter;
import rmi.RemoteFileClassifier;
import rmi.FileClassifierImpl;
import java.io.*;
/**
* FileClassifierServerConfig.java
*/
public class FileClassifierServerConfig implements LeaseListener {
private LeaseRenewalManager leaseManager = new LeaseRenewalManager();
private ServiceID serviceID = null;
private RemoteFileClassifier impl;
private File serviceIdFile;
private Configuration config;
public static void main(String args[]) {
FileClassifierServerConfig s = new FileClassifierServerConfig(args);
// keep server running forever to
// - allow time for locator discovery and
// - keep re-registering the lease
Object keepAlive = new Object();
synchronized(keepAlive) {
try {
keepAlive.wait();
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
}
public FileClassifierServerConfig(String[] args) {
System.setSecurityManager(new RMISecurityManager());
try {
config = ConfigurationProvider.getInstance(args);
} catch(ConfigurationException e) {
System.err.println("Configuration error: " + e.toString());
System.exit(1);
}
Exporter exporter = null;
try {
exporter = (Exporter)
config.getEntry( "config.FileClassifierServerConfig",
"exporter",
Exporter.class);
} catch(ConfigurationException e) {
e.printStackTrace();
System.exit(1);
}
// Create the service and its proxy
try {
impl = new rmi.FileClassifierImpl();
} catch(RemoteException e) {
e.printStackTrace();
System.exit(1);
}
Remote proxy = null;
try {
proxy = exporter.export(impl);
} catch(ExportException e) {
e.printStackTrace();
System.exit(1);
}
// register proxy with lookup services
JoinManager joinMgr = null;
try {
LookupDiscoveryManager mgr =
new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
null, // unicast locators
null); // DiscoveryListener
joinMgr = new JoinManager(proxy, // service proxy
null, // attr sets
serviceID,
mgr, // DiscoveryManager
new LeaseRenewalManager());
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
}
void getServiceID() {
// Make up our own
Uuid id = UuidFactory.generate();
serviceID = new ServiceID(id.getMostSignificantBits(),
id.getLeastSignificantBits());
}
public void serviceIDNotify(ServiceID serviceID) {
// called as a ServiceIDListener
// Should save the id to permanent storage
System.out.println("got service ID " + serviceID.toString());
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServer
This server can be run using a configuration file such as a standard
Jeri over TCP configuration
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new BasicILFactory());
}
This can be run from gthe command line by
java ... security.FileClassifierServer \
config/security/jeri-tcp.config
or from the Ant build files by
ant run -DrunFile=security.FileClassifierServer \
-Dconfig=config/security/jeri-tcp.config
The server with the
To build the server and run it with various configuration files
I use the Ant file
The Ant file is
Integrity ensures that each method call sent from the client to server gets there in original form - that is, it is not altered in any way, and similarly that replies are not altered. It does not guarantee privacy - anyone can look at messages (that is the role of confidentiality). It also does not guarantee that the entity you are sending messages to is the one you think it is (that is the role of authentication).
A client can enforce integrity by requiring that the proxy support
the constraint
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Don't verify the proxy. */
false,
/*
* Require integrity for all methods.
*/
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
Integrity.YES
},
null
)
),
new Permission[] {}
);
}
To run the client using this configuration, run
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-integrity.config
or
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-integrity.config
instead of
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-minimal.config
or
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-minimal.config
Note that only the configuration file has changed.
TCP does not support integrity checking.
Using TCP, we can expect integrity to fail.
The server can use
Jeri over TCP, by a configuration such as
import net.jini.jeri.BasicILFactory;
import net.jini.jeri.BasicJeriExporter;
import net.jini.jeri.tcp.TcpServerEndpoint;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0),
new BasicILFactory());
}
It can be run by
java ... security.FileClassifierServer config/security/jeri-tcp.config
or from the Ant file by
ant run -DrunFile=security.FileClassifierServer \
-Dconfig=config/security/jeri-tcp.config
The client can find the service, and create a new proxy for it. But when it tries to call a method through this proxy, integrity checking will fail. This shows by the client throwing an exception
java.rmi.ConnectIOException: I/O exception connecting to
BasicObjectEndpoint[e42fc746-e7c7-444b-bbc9-b124217439c4,
TcpEndpoint[127.0.0.1:43084]]; nested exception is:
net.jini.io.UnsupportedConstraintException:
cannot satisfy constraint: Integrity.YES
This exception is thrown when the client attempts to call any
method on the proxy. In the example above, it occurs in the
first method call to the proxy
type = classifier.getMIMEType(fileName)
TCP not only fails to support integrity checking, it also fails to support any of the other security mechanisms of this chapter. We will not consider it any further in this chapter.
SSL (or TLS) does support integrity checking. The server can use
Jeri over SSL, by a configuration such as
/* Configuration source file for an SSL server */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require integrity for all methods */
new BasicMethodConstraints(
new InvocationConstraints(
(InvocationConstraint[]) null,
(InvocationConstraint[]) null)),
/* No Permission */
null
)
);
}
It can be run by
java ... security.FileClassifierServer config/security/jeri-ssl-minimal.config
or from the Ant file by
ant run -DrunFile=security.FileClassifierServer \
-Dconfig=config/security/jeri-ssl-minimal.config
The service used here is still the RMI service
discussed in earlier chapters
package rmi;
import common.MIMEType;
import common.FileClassifier;
/**
* FileClassifierImpl.java
*/
public class FileClassifierImpl implements RemoteFileClassifier {
public MIMEType getMIMEType(String fileName)
throws java.rmi.RemoteException {
System.out.println("Called with " + fileName);
if (fileName.endsWith(".gif")) {
return new MIMEType("image", "gif");
} else if (fileName.endsWith(".jpeg")) {
return new MIMEType("image", "jpeg");
} else if (fileName.endsWith(".mpg")) {
return new MIMEType("video", "mpeg");
} else if (fileName.endsWith(".txt")) {
return new MIMEType("text", "plain");
} else if (fileName.endsWith(".html")) {
return new MIMEType("text", "html");
} else
// fill in lots of other types,
// but eventually give up and
return new MIMEType(null, null);
}
public FileClassifierImpl() throws java.rmi.RemoteException {
// empty constructor required by RMI
}
} // FileClassifierImpl
The service needs no changes to satisfy integrity - integrity
is supplied by the SSL protocol.
This service/server/configuration combination works with no exceptions thrown.
This section was the "teaser": Look how easy it is to implement advanced security! Just add a constraint to the client and use an appropriate protocol for the service! The following sections look at other aspects of security, and it does get a little more complex :-)
When a client finds a service, it goes through several stages: it
prepares a service description and asks lookup services if they
have any services matching that description. If they do, then the
lookup service downloads a
The service places class or jar files on the HTTP server. The client gets these files from the HTTP server. In Jini 1.2 both the service and client trust the HTTP server. For a secure system this trust should be demonstrable. The client needs to able to verify that the class files it got from the server are the class files that the service put there. This can be done in many ways, but the Jini team has come up with a neat way, called HTTPMD.
There is no integrity or other security aspects in getting files from an HTTP server. The standard way of ensuring security means using a protocol such as HTTPS, but this is quite heavyweight, and not really suited to the purpose of proxy verification: while we can get a verified document from an HTTPS server, we still don't know whether or not to trust that server!
What we want to get from an HTTP server is a jar file for the classes that are provided by the service. It is only the service that knows if they are correct or not. That is, it doesn't really matter whether or not the HTTP server can be trusted, but whether we can get information from the service to verify the jar file. A common way of checking that two files are identical is to use a hash of each file. A hash is a number (often 128 bits) that is calculated from the file contents. Hash algorithms are designed with two properties
If two files have the same hash, then it is "almost certain" that they are the same file (that is, have the same contents)
Given a hash value, it is "nearly impossible" to create a file with that hash value
There are many hash algorithms. Popular ones are
MD5
SHA
When you get a marshalled object from a lookup service, it contains the URL for the class files. This URL was inserted by the service. If the URL contained the hash for the class files, then it would be possible for a client to verify that it had obtained the correct files. (Of course, this assumes that the lookup service and HTTP server are not in collusion to deliver false hash values - see later for trusting the lookup service.)
Jini defines an HTTPMD (HTTP + Message Digest) URL that adds the hash value as component of the URL. The scheme is changed from "http" to "httpmd" and the hash is added as an extra component, along with a statement of the hash algorithm. For example, a URL of
http:jan.newmarch.name/classes/FileClassifierServer-dl.jar
would change to
httpmd:jan.newmarch.name/classes/FileClassifierServer-dl.jar;\
md5=7ef2019216d0e9069308cec29b779bc0
using the MD5 hash algorithm.
The service can specify such a URL in its
java -Djava.protocol.handler.pkgs=net.jini.url ...
which looks for the class http://java.sun.com/developer/onlineTraining/protocolhandlers/
There are often tools available in any Operating System for calculation
of message digests. For example, most Linux distributions include
the
There is an message digest class in
class HttpmdUtil {
static String computeDigest(URL url,
String algorithm);
static String computeDigestCodebase(String sourceDirectory,
String codebase);
}
The first method useful if you already have a jar file installed in an HTTP server and wish to calculate its digest. So for example you could call
HttpmdUtil.computeDigest("http://localhost/classes/ClassFiles.jar", "MD5");
The resulting digest could be appended to a new URL of type HTTPMD.
Note that for this mechanism to be valid, the program using this
must trust the HTTP server!
A simple program to calculate and print the hash value of a URL
using this is
import net.jini.url.httpmd.HttpmdUtil;
import java.net.URL;
public class PrintDigest {
public static void main(String[] args) {
if (args.length == 0) {
System.out.println("");
return;
}
String codebase = args[0];
try {
System.out.println(HttpmdUtil.computeDigest(
new URL(codebase),
"MD5"));
} catch(Exception e) {
System.out.println(codebase);
}
}
}
We use this program in our Ant files which assume
a local trusted HTTP server.
The second method is useful before deployment of the jar file to an HTTP server. It returns a new URL for a given URL with a new digest value. For example, the call
HttpmdUtil.computeDigestCodebase("dist",
"httpmd://localhost/classes/ClassFiles.jar;md5=0");
This strips the scheme, host and digest value from the URL and appends
the directories and jar filename to the given directory. In this case
it calculates the digest for the local file
A third technique is given in the Jini "hello" example
The standard setup for
When the client gets a proxy for a service, it gets a marshalled object with instance data and a URL for the class files. Assuming that the URL has not been tampered with, it can download the class files from an HTTP server. If it is an HTTPMD URL then it can verify that the class files are correct - as long as it trusts the proxy. This is a tricky problem: how to verify that the proxy is correct when you have a - possibly - false and misleading proxy? Moreover, this possibly antagonistic proxy is the only way you have of talking to the service.
The only entity that can really verify that the proxy is correct is the original service. So can we send the proxy to the service and get it to tell us? Well, no: if we ask the untrusted proxy to send itself for verification to the service, then it might just lie and claim that, yes, it has done so. What we have to do is to get an object from the service that can perform verification locally - under the client's eyes, as it were.
The mechanism adopted by Jini to solve this is to use several levels of proxy: the (untrusted) service proxy is asked to deliver a "bootstrap" proxy that can deliver a verifier. This verifier is the object that will deliver the verdict on whether the proxy can be trusted, so it must be trustworthy itself. Jini ensures this by insisting that the class files for this verifier are local to the client and so are trusted just like any other local code.
The client needs to have a list of local verifiers that it trusts
just because they are local. A standard set is given in the Jini
library
To require trust from a service, the client must do three things
Include
Install an HTTPMD handler by the runtime property
Specify trust checking by setting the first argument of
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Verify the proxy. */
true,
/* No constraints */
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
},
null
)
),
new Permission[] {}
);
}
A command line to run this client is
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-trust.config
or using Ant
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-trust.config
SSL will allow trust checking to be performed.
The service does not
need to be adapted, and can still be the
The server needs to install an HTTPMD handler
by the runtime property
The common/MIMEType.class,
common/FileClassifier.class,
rmi/RemoteFileClassifier.class,
rmi/FileClassifierImpl.class
and copied to an HTTP server
A hash needs to be performed on this
The codebase should be an
The service does not need to specify anything other than that it
uses SSL for transport (the server supplies the HTTPMD codebase).
The server can use the
/* Configuration source file for an SSL server */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require integrity for all methods */
new BasicMethodConstraints(
new InvocationConstraints(
(InvocationConstraint[]) null,
(InvocationConstraint[]) null)),
/* No Permission */
null
)
);
}
A command line to run this server is
java ... -Djava.rmi.server.codebase=httpmd://... \
security.FileClassiferServer \
config/security/jeri-ssl-minimal.config
or from Ant
ant run -DrunFile=security.FileClassiferServer \
-Dconfig=config/security/jeri-ssl-minimal.config \
-Ddo.trust=yes
This server/configuration will handle a client that requires trust verification. If the trust logger file for the client is examined it will contain lines such as
FINE: trust verifiers [net.jini.constraint.ConstraintTrustVerifier...]
Aug 16, 2004 9:59:35 PM net.jini.security.Security$Context isTrustedObject
FINE: net.jini.jeri.ssl.SslTrustVerifier@df1832 trusts
SslEndpoint[127.0.0.1:39693]
Aug 16, 2004 9:59:35 PM net.jini.security.Security$Context isTrustedObject
FINE: net.jini.jeri.BasicJeriTrustVerifier@1a116c9 trusts
BasicObjectEndpoint[...,SslEndpoint[...]]
Aug 16, 2004 9:59:35 PM net.jini.security.Security$Context isTrustedObject
FINE: net.jini.constraint.ConstraintTrustVerifier@1d1e730 trusts
InvocationConstraints[reqs: {}, prefs: {}]
Aug 16, 2004 9:59:35 PM net.jini.security.Security$Context isTrustedObject
FINE: net.jini.constraint.ConstraintTrustVerifier@1d1e730 trusts
BasicMethodConstraints{default => null}
Aug 16, 2004 9:59:35 PM net.jini.security.Security$Context isTrustedObject
FINE: net.jini.jeri.BasicJeriTrustVerifier@1a116c9 trusts
Proxy[RemoteFileClassifier,
BasicInvocationHandler[BasicObjectEndpoint[...]]
This shows that the It is important to note the limits of what has been achieved in this section: we have downloaded a proxy that we can trust - but whose proxy is it? We have been assured that the code we have got has not been tampered with by anyone else, but we could still be getting code from "Antagonistic Alice". All that we know at this point is that we have the code that Alice intended to send to us and that Mallory in the middle hasn't tampered with it!
If you forget to include
java.lang.SecurityException: object is not trusted:
Proxy[RemoteFileClassifier,BasicInvocationHandler[
BasicObjectEndpoint[1c4c3ec0-f91e-46a6-827b-626575702a07,
SslEndpoint[127.0.0.1:56641]]]]
at net.jini.security.Security.verifyObjectTrust(Security.java:268)
at net.jini.security.BasicProxyPreparer.verify(BasicProxyPreparer.java:309)
The trust logger will also show
FINE: trust verifiers []
Aug 16, 2004 5:54:27 PM net.jini.security.Security$Context isTrustedObject
FAILED: no verifier trusts Proxy[RemoteFileClassifier,BasicInvocationHandler[Bas
icObjectEndpoint[1c4c3ec0-f91e-46a6-827b-626575702a07,SslEndpoint[127.0.0.1:5664
1]]]]
with the failure caused by an empty verifiers list
If the server uses HTTP URLs instead of HTTPMD URLs, then the
client is unable to perform an integrity check. This will
result in the client throwing a
java.lang.SecurityException: URL does not provide integrity:
http://192.168.1.13/classes/security.FileClassifierServer-dl.jar
at net.jini.security.Security.verifyCodebaseIntegrity(Security.java:343)
The integrity logger will also show
FINE: integrity verifiers [net.jini.url.httpmd.HttpmdIntegrityVerifier@1b5998f,
net.jini.url.https.HttpsIntegrityVerifier@17494c8,
net.jini.url.file.FileIntegrityVerifier@d3db51]
Aug 16, 2004 6:01:27 PM net.jini.security.Security verifyCodebaseIntegrity
FAILED: no verifier verifies
http://192.168.1.13/classes/security.FileClassifierServer-dl.jar
It is necessary to install the HTTPMD handler in the server,
the client and in
INFO: exception occurred during unicast discovery
java.net.MalformedURLException: unknown protocol: httpmd
at java.net.URL.>init<(URL.java:544)
If you leave this handler out of the client it throws an
exception during service discovery
java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
java.net.MalformedURLException: unknown protocol: httpmd
at com.sun.jini.reggie.RegistrarProxy.lookup(RegistrarProxy.java:130)
If you omit to install the HTTPMD handler in
INFO: JoinManager - failure
java.rmi.ServerException: RemoteException in server thread; nested exception is:
java.rmi.UnmarshalException: unmarshalling method/arguments;
nested exception is:
java.net.MalformedURLException: unknown protocol: httpmd
at net.jini.jeri.BasicInvocationDispatcher.dispatch(...)
A conversation is confidential if no-one can overhear it, or even if they can hear it then they cannot understand it. Typically applications use encryption to ensure that messages cannot be read by others.
Either a client or a server can specify confidentiality.
A client specifies this by making adding a
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Confidentiality;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Don't verify the proxy. */
false,
/*
* Require integrity for all methods.
*/
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
Confidentiality.YES
},
null
)
),
new Permission[] {}
);
}
To run the client using this configuration, run
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-conf.config
or
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-conf.config
Note that only the configuration file has changed.
SSL supports confidentiality. Indeed, that is the major purpose
behind its design. So all that is needed is for a server to
specify that it is using SSL, which can be done using the earlier
/* Configuration source file for an SSL server */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require integrity for all methods */
new BasicMethodConstraints(
new InvocationConstraints(
(InvocationConstraint[]) null,
(InvocationConstraint[]) null)),
/* No Permission */
null
)
);
}
A command line to run this server is
java ... security.FileClassiferServer \
config/security/jeri-ssl-minimal.config
or from Ant
ant run -DrunFile=security.FileClassiferServer \
-Dconfig=config/security/jeri-ssl-minimal.config
So far we have tried the following combinations
We can try variations on these: for example, a client that requires
trust and integrity with a server that requires encryption.
This is just an additive process: add in the extra constraints
to the appropriate configuration and ensure that the client or
server has the correct runtime to handle the constraints.
The client
configuration in this case is
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Integrity;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Verify the proxy. */
true,
/* No constraints */
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
Integrity.YES
},
null
)
),
new Permission[] {}
);
}
and the server configuration is
/* Configuration source file for an SSL server with confidentiality */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.Confidentiality;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require confidentiality for all methods */
new BasicMethodConstraints(
new InvocationConstraints(Confidentiality.YES, null)),
/* No Permission */
null
)
);
}
These can be run by
java ... client.TestFileClassifierProxyPreparer \
config/security/preparer-trust-integrity.config
java ... -Djava.rmi.server.codebase=httpmd://... \
security.FileClassiferServer \
config/security/jeri-ssl-conf.config
or from Ant
ant run -DrunFile=client.TestFileClassifierProxyPreparer \
-Dconfig=config/security/preparer-trust-integrity.config
ant run -DrunFile=security.FileClassiferServer \
-Dconfig=config/security/jeri-ssl-.config \
-Ddo.trust=yes
This combination works satisfactorily. The client logs are indicative of what has happened. The trust logger shows (with much text elided)
FINE: HttpmdIntegrityVerifier verifies httpmd://...
FINE: trust verifiers [...]
FINE: SslTrustVerifier trusts SslEndpoint[...]
FINE: BasicJeriTrustVerifier trusts BasicObjectEndpoint[...]
FINE: ConstraintTrustVerifier trusts Confidentiality.YES
FINE: ConstraintTrustVerifier trusts
InvocationConstraints[reqs: {Confidentiality.YES}, prefs: {}]
FINE: ConstraintTrustVerifier trusts
BasicMethodConstraints{default =>
InvocationConstraints[reqs: {Confidentiality.YES}, prefs: {}]}
FINE: BasicJeriTrustVerifier trusts Proxy[...]
This shows that the Similarly, the integrity log shows
FINE: integrity verifiers [...]
FINE: HttpmdIntegrityVerifier verifies httpmd://...
showing that the
By way of contrast, if the client is run with one constraint and the
server is run with its opposite, then the constraints cannot be
satisfied. For example, if the client has set
java.rmi.ConnectIOException: I/O exception connecting to
BasicObjectEndpoint[...,SslEndpoint[...]]; nested exception is:
net.jini.io.UnsupportedConstraintException: Constraints not supported:
InvocationConstraints[reqs: {Confidentiality.NO,
Confidentiality.YES}, prefs: {}]
If you want to give different individuals different access rights,
then you need to be able to verify their identity. This means that
they must have some way of expressing what their identity is in a
form that you will recognise. People (and things) may have a number
of identities: the father of a particular person, the staff id number,
the driving license number, their name, and so on. Essentially, different
labels for the one entity. The terminology adopted is that the entity
is called a subject, and in Java
this is represented by the
A subject authenticates itself to a service using a principal and information to verify itself as that principal. For example, you log into a computer using your username as principal and password for verification. Other mechanisms could be used. For example, you verify yourself to the police officer who has just pulled you over by the photo on your drivers license.
A credential (such as a driver's license) is used to authenticate a subject to later services. In the computer world, these include be X.509 certificates and Kerberos tickets.
A white paper describing JAAS is at
USER AUTHENTICATION AND AUTHORIZATION IN THE JAVA(TM) PLATFORM
(http://java.sun.com/security/jaas/doc/acsac.html), with other
information such as "JavaTM Authentication and Authorization
Service (JAAS) Reference Guide" which should be in the Java
distribution directory
JAAS (Java Authentication and Authorization Service) is a framework for verifying common identities. These include
JNDI
KeyStore
Kerberos
Windows NT
Unix
JAAS augments the standard Java security model by adding support for principals. So security access is not just granted on the properties of the code itself (signed, etc) but also on the principals running the code.
In order to use JAAS, you firstly need to create a
Once you have a context, you attempt to
You then add the JAAS security checks to code by running it as the privileged subject. The code for this looks like
LoginContext loginContext =
new LoginContext("...");
if (loginContext == null) {
// do some action without JAAS security
} else {
loginContext.login();
Subject.doAsPrivileged(
loginContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
// do the same action, but now
// as a particular subject
return null;
}
},
null);
}
A keystore is a place to store certain types of credentials, such as
X.509 certificates which are used by SSL. A keystore is manipulated
by the
We can create private keys for the client by
keytool -keystore keystore.client -genkey
This will prompt for X.509 information, which assumes that you are
an individual working for an organisation
Enter keystore password: client
What is your first and last name?
[Unknown]: Client
What is the name of your organizational unit?
[Unknown]: IT
What is the name of your organization?
[Unknown]: Monash
What is the name of your City or Locality?
[Unknown]: Melbourne
What is the name of your State or Province?
[Unknown]: Vic
What is the two-letter country code for this unit?
[Unknown]: AU
Is CN=Client, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU correct?
[no]: yes
Enter key password for
(RETURN if same as keystore password):
Similarly, we can set up a keystore for the server
keytool -keystore keystore.server -genkey
Enter keystore password: server
What is your first and last name?
[Unknown]: Server
What is the name of your organizational unit?
[Unknown]: IT
What is the name of your organization?
[Unknown]: Monash
What is the name of your City or Locality?
[Unknown]: Melbourne
What is the name of your State or Province?
[Unknown]: Vic
What is the two-letter country code for this unit?
[Unknown]: AU
Is CN=Server, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU correct?
[no]: yes
Enter key password for
(RETURN if same as keystore password):
We can export the server's public key from the its keystore and import it into the client's truststore under the alias "Server" by
keytool -keystore keystore.server -export -file server.cert
Enter keystore password: server
Certificate stored in file
keytool -keystore truststore.client -import -file server.cert -alias Server
Enter keystore password: client
Owner: CN=Server, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU
Issuer: CN=Server, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU
Serial number: 4123f906
Valid from: Thu Aug 19 10:49:10 EST 2004 until: Wed Nov 17 11:49:10 EST 2004
Certificate fingerprints:
MD5: 6E:24:70:EB:E2:2C:A0:72:C5:B9:9B:95:72:39:87:B1
SHA1: 2B:AB:0D:80:4F:DF:B8:66:3B:E7:49:66:3D:53:EC:C5:B8:3A:91:5E
Trust this certificate? [no]: yes
Certificate was added to keystore
and similarly we can export the client's public key and import it
into the server's truststore under the alias "Client"
keytool -keystore keystore.client -export -file client.cert
Enter keystore password: client
Certificate stored in file
keytool -keystore truststore.server -import -file client.cert -alias Client
Enter keystore password: server
Owner: CN=Client, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU
Issuer: CN=Client, OU=IT, O=Monash, L=Melbourne, ST=Vic, C=AU
Serial number: 4123f88d
Valid from: Thu Aug 19 10:47:09 EST 2004 until: Wed Nov 17 11:47:09 EST 2004
Certificate fingerprints:
MD5: EA:4A:67:E3:A6:58:2D:F4:52:00:FE:CF:2C:AC:7A:6A
SHA1: 8C:68:E3:9C:E8:08:4A:33:F5:12:E4:9D:73:D6:EF:A4:A5:82:B2:79
Trust this certificate? [no]: yes
Certificate was added to keystore
A server that is prepared to authenticate itself must be able to offer suitable credentials when challenged. For SSL, this would be an X.509 certificate, for Kerberos it would be a Kerberos ticket, etc. If JAAS is used to provide these credentials then it must be able to "login" to gets it authentication information.
The server code needs to be modified slightly to get a login
context and then login using a principal to get a subject.
If this succeeds then it can run the rest of the code as
that subject. The changes to the file classifier server are given
as static code to execute before creating the server. The
package security;
import java.rmi.RMISecurityManager;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.ExportException;
import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedActionException;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceItem;
import net.jini.core.lookup.ServiceRegistration;
import net.jini.core.lease.Lease;
import net.jini.core.lookup.ServiceID ;
import net.jini.lease.LeaseListener;
import net.jini.lease.LeaseRenewalEvent;
import net.jini.lease.LeaseRenewalManager;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import net.jini.lookup.JoinManager;
import net.jini.id.UuidFactory;
import net.jini.id.Uuid;
import net.jini.discovery.LookupDiscoveryManager;
import net.jini.export.Exporter;
import rmi.RemoteFileClassifier;
import rmi.FileClassifierImpl;
import java.util.logging.*;
import java.io.*;
/**
* FileClassifierServerAuth
*/
public class FileClassifierServerAuth implements LeaseListener {
private LeaseRenewalManager leaseManager = new LeaseRenewalManager();
private ServiceID serviceID = null;
private RemoteFileClassifier impl;
private File serviceIdFile;
private Configuration config;
static final String TRUST_LOG = "net.jini.security.trust";
static final String INTEGRITY_LOG = "net.jini.security.integrity";
static final String POLICY_LOG = "net.jini.security.policy";
static final Logger trustLogger = Logger.getLogger(TRUST_LOG);
static final Logger integrityLogger = Logger.getLogger(INTEGRITY_LOG);
static final Logger policyLogger = Logger.getLogger(POLICY_LOG);
private static FileHandler trustFh;
private static FileHandler integrityFh;
private static FileHandler policyFh;
private static FileClassifierServerAuth server;
static final String DISCOVERY_LOG = "net.jini.security.trust";
static final Logger logger = Logger.getLogger(DISCOVERY_LOG);
private static FileHandler fh;
public static void main(String args[]) {
/*
try {
// this handler will save ALL log messages in the file
fh = new FileHandler("mylog.svr.txt");
// the format is simple rather than XML
fh.setFormatter(new SimpleFormatter());
logger.addHandler(fh);
} catch(Exception e) {
e.printStackTrace();
}
*/
installLoggers();
init(args);
Object keepAlive = new Object();
synchronized(keepAlive) {
try {
keepAlive.wait();
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
}
private static void init(final String[] args) {
try {
LoginContext loginContext =
new LoginContext("security.FileClassifierServerAuth");
if (loginContext == null) {
System.out.println("No login context");
server = new FileClassifierServerAuth(args);
} else {
loginContext.login();
System.out.println("Login succeeded as " +
loginContext.getSubject().toString());
Subject.doAsPrivileged(
loginContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
server = new FileClassifierServerAuth(args);
return null;
}
},
null);
}
} catch(LoginException e) {
e.printStackTrace();
System.exit(3);
} catch(PrivilegedActionException e) {
e.printStackTrace();
System.exit(3);
}
}
public FileClassifierServerAuth(String[] args) {
System.setSecurityManager(new RMISecurityManager());
Exporter exporter = null;
String serviceName = null;
try {
config = ConfigurationProvider.getInstance(args);
exporter = (Exporter)
config.getEntry( "security.FileClassifierServer",
"exporter",
Exporter.class);
serviceName = (String)
config.getEntry( "security.FileClassifierServer",
"serviceName",
String.class);
} catch(ConfigurationException e) {
System.err.println("Configuration error: " + e.toString());
System.exit(1);
}
// Create the service and its proxy
try {
// impl = new security.FileClassifierImpl();
impl = (RemoteFileClassifier) Class.forName(serviceName).newInstance();
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
Remote proxy = null;
try {
proxy = exporter.export(impl);
System.out.println("Proxy is " + proxy.toString());
} catch(ExportException e) {
e.printStackTrace();
System.exit(1);
}
// register proxy with lookup services
JoinManager joinMgr = null;
try {
LookupDiscoveryManager mgr =
new LookupDiscoveryManager(LookupDiscovery.ALL_GROUPS,
null, // unicast locators
null); // DiscoveryListener
joinMgr = new JoinManager(proxy, // service proxy
null, // attr sets
serviceID,
mgr, // DiscoveryManager
new LeaseRenewalManager());
} catch(Exception e) {
e.printStackTrace();
System.exit(1);
}
}
private static void installLoggers() {
try {
// this handler will save ALL log messages in the file
trustFh = new FileHandler("log.server.trust.txt");
integrityFh = new FileHandler("log.server.integrity.txt");
policyFh = new FileHandler("log.server.policy.txt");
// the format is simple rather than XML
trustFh.setFormatter(new SimpleFormatter());
integrityFh.setFormatter(new SimpleFormatter());
policyFh.setFormatter(new SimpleFormatter());
trustLogger.addHandler(trustFh);
integrityLogger.addHandler(integrityFh);
policyLogger.addHandler(policyFh);
trustLogger.setLevel(java.util.logging.Level.ALL);
integrityLogger.setLevel(java.util.logging.Level.ALL);
policyLogger.setLevel(java.util.logging.Level.ALL);
} catch(Exception e) {
e.printStackTrace();
}
}
void getServiceID() {
// Make up our own
Uuid id = UuidFactory.generate();
serviceID = new ServiceID(id.getMostSignificantBits(),
id.getLeastSignificantBits());
}
public void serviceIDNotify(ServiceID serviceID) {
// called as a ServiceIDListener
// Should save the id to permanent storage
System.out.println("got service ID " + serviceID.toString());
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServerAuth
This login context uses a hard-coded string to specify the name
"security.FileClassifierServerAuth" used by JAAS to find
information from the JAAS configuration file; this string
could better be given in a Jini configuration file in a
production environment.
A few other pieces need to be put in place in order that this server can authenticate itself
The server needs to be run with an additional runtime property.
The property
java ... -Djava.security.auth.login.config=ssl-server.login ...
The JAAS login file specifies how JAAS is to get its credentials.
For example, for SSL it will need a certificate which it can
get from a keystore. So for an SSL authenticating server the
/* JAAS login configuration file for SSL server */
security.FileClassifierServerAuth {
com.sun.security.auth.module.KeyStoreLoginModule required
keyStoreAlias="mykey"
keyStoreURL="file:resources/security/keystore.server"
keyStorePasswordURL="file:resources/security/password.server";
};
The configuration name "security.FileClassifierServerAuth"
is the same as the parameter to the
The password file
This server can be run from the command line
java ... security.FileClassiferServerAuth \
config/security/jeri-ssl-minimal.config
or from Ant
ant run -DrunFile=security.FileClassiferServerAuth \
-Dconfig=config/security/jeri-ssl-minimal.config
The server does not need to set any constraints (since it just
authenticates itself), so the minimal server configuration file
can be used.
The client can require that the server have a proof of identity, or that it identifies itself as a particular subject. This is just like asking "Do you have a card that proves you are over eighteen?" versus "Do you have a card that proves you are Joe Bloggs?".
The first case ("do you have a credential?") can be specified in the
client configuration file by just adding the
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.ServerAuthentication;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
client.TestFileClassifierProxyPreparer {
preparer =
new BasicProxyPreparer(
/* Don't verify the proxy. */
false,
/* Require authentication as anyone */
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
ServerAuthentication.YES
},
null
)
),
new Permission[] {}
);
}
The client is the previous
-Djavax.net.ssl.trustStore=truststore.client
The second case requires specifying which principal(s)
the server is required to authenticate as. The most common
case is when the client requires a single principal as identity.
The
The client is still unaltered from
/* config file to ask the server to authenticate itself */
import java.security.Permission;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.ServerAuthentication;
import net.jini.security.BasicProxyPreparer;
import net.jini.constraint.BasicMethodConstraints;
import com.sun.jini.config.KeyStores;
import net.jini.core.constraint.ServerMinPrincipal;
client.TestFileClassifierProxyPreparer {
/* Keystore for getting principals */
private static users=
KeyStores.getKeyStore("file:resources/security/truststore.client", null);
private static serverUser =
KeyStores.getX500Principal("server", users);
preparer =
new BasicProxyPreparer(
/* Don't verify the proxy. */
false,
/* Require authentication as "server" */
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
ServerAuthentication.YES,
new ServerMinPrincipal(serverUser)
},
null
)
),
new Permission[] {}
);
}
Classes such as
The same mechanism used for a server to authenticate itself is used
by the client. That is, it sets up a login context, logs in and then
runs code as a particular subject. The modified code is
package client;
import common.FileClassifier;
import common.MIMEType;
import java.security.PrivilegedExceptionAction;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import java.security.PrivilegedActionException;
import java.rmi.RMISecurityManager;
import net.jini.discovery.LookupDiscovery;
import net.jini.discovery.DiscoveryListener;
import net.jini.discovery.DiscoveryEvent;
import net.jini.core.lookup.ServiceRegistrar;
import net.jini.core.lookup.ServiceTemplate;
import java.rmi.RemoteException;
import net.jini.security.BasicProxyPreparer;
import net.jini.security.ProxyPreparer;
import net.jini.config.Configuration;
import net.jini.config.ConfigurationException;
import net.jini.config.ConfigurationProvider;
import java.util.logging.*;
/**
* TestFileClassifierAuth.java
*/
public class TestFileClassifierAuth implements DiscoveryListener {
private Configuration config;
static final String TRUST_LOG = "net.jini.security.trust";
static final String INTEGRITY_LOG = "net.jini.security.integrity";
static final String POLICY_LOG = "net.jini.security.policy";
static final Logger trustLogger = Logger.getLogger(TRUST_LOG);
static final Logger integrityLogger = Logger.getLogger(INTEGRITY_LOG);
static final Logger policyLogger = Logger.getLogger(POLICY_LOG);
private static FileHandler trustFh;
private static FileHandler integrityFh;
private static FileHandler policyFh;
public static void main(String argv[])
throws ConfigurationException {
installLoggers();
// Become a subject if possible
init(argv);
// stay around long enough to receive replies
try {
Thread.currentThread().sleep(100000L);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
private static void init(final String[] args) {
try {
LoginContext loginContext =
new LoginContext("security.TestFileClassifierAuth");
if (loginContext == null) {
System.out.println("No login context");
new TestFileClassifierAuth(args);
} else {
loginContext.login();
System.out.println("Login succeeded as " +
loginContext.getSubject().toString());
Subject.doAsPrivileged(
loginContext.getSubject(),
new PrivilegedExceptionAction() {
public Object run() throws Exception {
new TestFileClassifierAuth(args);
return null;
}
},
null);
}
} catch(LoginException e) {
e.printStackTrace();
System.exit(3);
} catch(PrivilegedActionException e) {
e.printStackTrace();
System.exit(3);
} catch(ConfigurationException e) {
e.printStackTrace();
System.exit(3);
}
}
public TestFileClassifierAuth(String[] argv)
throws ConfigurationException {
config = ConfigurationProvider.getInstance(argv);
System.setSecurityManager(new RMISecurityManager());
LookupDiscovery discover = null;
try {
discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS);
} catch(Exception e) {
System.err.println(e.toString());
System.exit(1);
}
discover.addDiscoveryListener(this);
}
private static void installLoggers() {
try {
// this handler will save ALL log messages in the file
trustFh = new FileHandler("log.client.trust.txt");
integrityFh = new FileHandler("log.client.integrity.txt");
policyFh = new FileHandler("log.client.policy.txt");
// the format is simple rather than XML
trustFh.setFormatter(new SimpleFormatter());
integrityFh.setFormatter(new SimpleFormatter());
policyFh.setFormatter(new SimpleFormatter());
trustLogger.addHandler(trustFh);
integrityLogger.addHandler(integrityFh);
policyLogger.addHandler(policyFh);
trustLogger.setLevel(java.util.logging.Level.ALL);
integrityLogger.setLevel(java.util.logging.Level.ALL);
policyLogger.setLevel(java.util.logging.Level.ALL);
} catch(Exception e) {
e.printStackTrace();
}
}
public void discovered(DiscoveryEvent evt) {
ServiceRegistrar[] registrars = evt.getRegistrars();
Class [] classes = new Class[] {FileClassifier.class};
FileClassifier classifier = null;
ServiceTemplate template = new ServiceTemplate(null, classes,
null);
for (int n = 0; n < registrars.length; n++) {
System.out.println("Lookup service found");
ServiceRegistrar registrar = registrars[n];
try {
classifier = (FileClassifier) registrar.lookup(template);
} catch(java.rmi.RemoteException e) {
e.printStackTrace();
System.exit(4);
continue;
}
if (classifier == null) {
System.out.println("Classifier null");
continue;
}
System.out.println("Getting the proxy");
// Get the proxy preparer
ProxyPreparer preparer = null;
try {
preparer =
(ProxyPreparer) config.getEntry(
"client.TestFileClassifierProxyPreparer",
"preparer", ProxyPreparer.class,
new BasicProxyPreparer());
} catch(ConfigurationException e) {
e.printStackTrace();
preparer = new BasicProxyPreparer();
}
// Prepare the new proxy
System.out.println("Preparing the proxy");
try {
classifier = (FileClassifier) preparer.prepareProxy(classifier);
} catch(RemoteException e) {
e.printStackTrace();
System.exit(3);
} catch(java.lang.SecurityException e) {
e.printStackTrace();
System.exit(6);
}
// Use the service to classify a few file types
System.out.println("Calling the proxy");
MIMEType type;
try {
String fileName;
fileName = "file1.txt";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file2.rtf";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
fileName = "file3.abc";
type = classifier.getMIMEType(fileName);
printType(fileName, type);
} catch(java.rmi.RemoteException e) {
System.out.println("Failed to call method");
System.err.println(e.toString());
System.exit(5);
continue;
}
// success
System.exit(0);
}
}
private void printType(String fileName, MIMEType type) {
System.out.print("Type of " + fileName + " is ");
if (type == null) {
System.out.println("null");
} else {
System.out.println(type.toString());
}
}
public void discarded(DiscoveryEvent evt) {
// empty
}
} // TestFileClassifierAuth
Like the server, other pieces need to be in place order that this client can authenticate itself
The client needs to be run with an additional runtime property.
The property
java ... -Djava.security.auth.login.config=ssl-client.login ...
The JAAS login file specifies how JAAS is to get its credentials.
For example, for SSL it will need a certificate which it can
get from a keystore. So for an SSL authenticating server the
/* JAAS login configuration file for SSL server */
security.TestFileClassifierAuth {
com.sun.security.auth.module.KeyStoreLoginModule required
keyStoreAlias="mykey"
keyStoreURL="file:resources/security/keystore.client"
keyStorePasswordURL="file:resources/security/password.client";
};
The configuration name "security.TestFileClassifierAuth"
is the same as the parameter to the
The password file
If the server requires the client to authenticate as a particular
user then it can be the
/* Configuration source file for an SSL server */
import java.security.Permission;
import net.jini.constraint.BasicMethodConstraints;
import net.jini.core.constraint.InvocationConstraint;
import net.jini.core.constraint.InvocationConstraints;
import net.jini.core.constraint.ClientAuthentication;
import net.jini.core.constraint.ClientMinPrincipal;
import net.jini.jeri.*;
import net.jini.jeri.ssl.*;
import com.sun.jini.config.KeyStores;
security.FileClassifierServer {
/* class name for the service */
serviceName = "rmi.FileClassifierImpl";
/* Keystore for getting principals */
private static users=
KeyStores.getKeyStore("file:resources/security/truststore.server", null);
private static clientUser =
KeyStores.getX500Principal("client", users);
/* Exporter for the server proxy */
exporter =
/* Use secure exporter */
new BasicJeriExporter(
/* Use SSL transport */
SslServerEndpoint.getInstance(0),
new BasicILFactory(
/* Require integrity for all methods */
new BasicMethodConstraints(
new InvocationConstraints(
new InvocationConstraint[] {
ClientAuthentication.YES,
new ClientMinPrincipal(clientUser)
},
(InvocationConstraint[]) null)),
/* No Permission */
null
)
);
}
In addition to this, the server needs to be run with a define
-Djavax.net.ssl.trustStore=resources/security/truststore.server
to locate the truststore file it will use to verify certificates
from the client.
Standard Java uses policy files to determine what foreign code is allowed to do. This policy is installed when the application starts, so is a static policy mechanism. In Jini 2.0, when a service is discovered, it may wish to ask for a policy to be applied at that time, ie dynamically. Extensions to the basic security model in JDK 1.4 allow this to occur, by permitting dynamic policy setting on class loaders.
In order to allow dynamic policy granting, the Java runtime must
have the appropriate classes installed and trusted. This is the
purpose of the
The runtime needs to be told about these classes. This can be done by using the runtime define
-Djava.security.properties=security.properties
where
policy.provider=net.jini.security.policy.DynamicPolicyProvider
For the client, an array of permissions specifies the permissions it will
grant to a proxy. This array is set in the
The server can set a permission in the
Ensuring security on the network is a complex task anyway, and the Jini possibilities of mobile code increases the security risks. This chapter has presented an "end-programmers" view of the new Jini 2.0 security. The architecture behind this is highly configurable, and what has been presented is one set of "plugins" to make it (relatively) easy for the programmer. However, if you want more control over any part of this process, then you can dig further into this architecture and "roll your own" for almost all parts of it.
If you found this chapter of value, the full book "Foundations of Jini 2 Programming" is available from APress or Amazon .
This work is licensed under a
Creative Commons License, the replacement for the earlier Open Content License.