donderdag 26 februari 2009

Authentication when using RMI

Today, i was working on an RMI application and i ran into the following problem: I wanted my RMI clients to authenticate themselves when accessing the RMI service, so that clients cannot access eachother's data and the service cannot be abused by un-authenticated clients.

So, how do you implement client authentication? I wanted a SIMPLE solution without setting up a public key infrastructure and without using SSL/TLS, as this is just too heavy for my simple requirements.

Then i ran into an authentication mechanism called CRAM-MD5, which is an abbreviation of Challenge Response Authentication Method. It operates as follows:



Each user (client) has a password that is also known to the server but secret otherwise.


When the client wants to authenticate itself to the server, it requests a `challenge' which is just a random sequence. The challenge is prepended with the password and the result is transformed with a `MessageDigest' transformation, e.g. MD5. This is a one-way transformation: it is (in practice) impossible to recover the input value from the transformed value... However, the transformed value is unique to the input value. The client then sends the transformed value over the network to the server.


The server performs the same computation as the client and compares the two transformed values. When they are equal, the server concludes that the client must have known the password and the client is authenticated.




To implement this in a toy example, you need the following classes/interfaces:

The RMI service definition interface:
package demo.authenticate;


import java.math.BigInteger;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Authenticate extends Remote {

String sayHello() throws RemoteException;

BigInteger challenge(String username) throws RemoteException;

boolean authenticate(String username, BigInteger digestClient)
throws RemoteException;
}



The RMI service:
package demo.authenticate;

import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

/**
* Class for testing MD5 authentication in RMI connection.
*
* Each user (client) has a password that is also known to the server but secret
* otherwise.
*
* When the client wants to authenticate itself to the server, it requests a
* `challenge' which is just a random sequence. The challenge is prepended with
* the password and the result is transformed with a `MessageDigest'
* transformation, e.g. MD5. This is a one-way transformation: it is (in
* practice) impossible to recover the input value from the transformed value...
* However, the transformed value is unique to the input value. The client then
* sends the transformed value over the network to the server.
*
* The server performs the same computation as the client and compares the two
* transformed values. When they are equal, the server concludes that the client
* must have known the password and the client is authenticated.
*/
public class Server implements Authenticate {

private static Map<String, String> passwords;

private static Map<String, BigInteger> serverChallenges;

private static SecureRandom secureRandom;

public Server() {
}

public String sayHello() throws RemoteException {
return "Hello. Please authenticate!";
}

public static void main(String args[]) {

try {
secureRandom = new SecureRandom();
passwords = new HashMap<String, String>();
serverChallenges = new HashMap<String, BigInteger>();

// put some data in passwords map
passwords.put("John", "JohnsSecret");
passwords.put("Jane", "JanesSecret");

// instantiate the rmi service
Server obj = new Server();
Authenticate stub = (Authenticate) UnicastRemoteObject
.exportObject(obj, 0);

// Bind the remote object's stub in the registry
Registry registry = LocateRegistry.getRegistry();
registry.rebind("Authenticate", stub);

System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}

public boolean authenticate(String username, BigInteger digestClient)
throws RemoteException {

if (!passwords.containsKey(username))
throw new RemoteException("Unknown user trying to authenticate.");
if (serverChallenges.get(username) == null)
throw new RemoteException("Unable to authenticate " + username
+ ". Missing authentication challenge.");

try {
// First compute the server-side digest
MessageDigest messageDigest = java.security.MessageDigest
.getInstance("SHA-256");
// feed stored challenge and stored password
messageDigest.reset();
messageDigest.update(serverChallenges.get(username).toByteArray());
messageDigest.update(passwords.get(username).getBytes("UTF-16"));
// and read the digest into a BigInteger
BigInteger digestServer = new BigInteger(1, messageDigest.digest());

System.out.println("Digest server: " + digestServer);
System.out.println("Digest client: " + digestClient);

// The see if match. If so, client must have known password.
if (digestClient.equals(digestServer)) {
System.out.println("Authentication succeeded");
return true;
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

System.out.println("Authentication failed");
return false;
}

public BigInteger challenge(String username) throws RemoteException {
byte[] bytes = new byte[100];

if (!passwords.containsKey(username))
throw new RemoteException("Unknown user trying to authenticate.");

// generate a random sequence of bytes
secureRandom.nextBytes(bytes);
// transform the bytes into a BigInteger
BigInteger serverChallenge = new BigInteger(1, bytes);
serverChallenges.put(username, serverChallenge);
// and return it...
return serverChallenge;
}
}


The RMI service sends the challenge in the form of a BigInteger. The challenge is initially created as an array of random bytes using the SecureRandom class. But this array cannot be transported over the network: When an array is given as an argument just its starting address in memory is passed. An easy way around it is to creat a BigInteger based on the array. (The byte array is then interpreted as a twos complement integer of arbitrary length.) The BigInteger can be given as argument since it is passed by value.

The RMI client:
package demo.authenticate;

import java.math.BigInteger;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.MessageDigest;

public class Client {

private static String password = "JohnsSecret";

private Client() {}

public static void main(String[] args) {
MessageDigest digest;

String host = (args.length < 1) ? null : args[0];
try {
Registry registry = LocateRegistry.getRegistry(host);
Authenticate stub = (Authenticate) registry.lookup("Authenticate");

// Then authenticate myself...
digest = java.security.MessageDigest.getInstance("SHA-256");
digest.reset();
digest.update(stub.challenge("John").toByteArray());
digest.update(password.getBytes("UTF-16"));
BigInteger digestClient = new BigInteger(1,digest.digest());

boolean success = stub.authenticate("John", digestClient);
System.out.println("Client digest: "+ digestClient);
System.out.println("Client: authentication success: "+ success);

} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}



There seems to be a security problem with the SHA algorithm and similar digest algorithms, so the method is not watertight. (See http://www.win.tue.nl/hashclash/rogue-ca/ ). But if your security requirements are not very strict, as in my case, you should keep most crooks outdoor!

(Don't take my word for it, i'm not an expert.)

Geen opmerkingen:

Een reactie posten