Building Secure Java RMI Servers

Paulo uses the proxy pattern along with the Java Authentication and Authorization Service API to build secure Java Remote Method Invocation servers that allow only properly authenticated users access to systems.


November 01, 2002
URL:http://www.drdobbs.com/jvm/building-secure-java-rmi-servers/184405197

Nov02: Java

Paulo is a researcher in distributed systems at the University of Coimbra, Portugal. He can be reached at [email protected].


When building server applications using Java Remote Method Invocation (RMI), it is not possible to directly control which users are calling what methods on the server. Consider the following: In Java, you define an interface with several methods that your server objects implement. You then create the objects and bind them to the registry running on the local machine. At that time, any client can connect to the registry, retrieve a reference to an object, and call methods on it. There is no direct way to prevent certain users from calling certain methods. When you publish an object, that's it—all the functionality is available to whomever wants to use it. If your server is exposing certain functionality that shouldn't be available to everyone, you have a problem.

In this article, I explain how to use the proxy pattern in conjunction with the Java Authentication and Authorization Service (JAAS) API for building secure RMI servers. Such servers allow only properly authenticated users access to a system. The complete source code for the server is available electronically; see "Resource Center," page 5.

Architecture

Imagine that you have a server interface like that in Listing One. The server has two methods, doOperationA() and doOperationB(), that print out on the server the name of the operation being performed. What you would like to have is a policy file, such as Listing Two, where you can configure which operations are available to each user. For instance, in this file you can see that "root" can call all the methods on the server (denoted by "*"), "alice" can only call doOperationA(), and "guest" cannot call anything at all. To implement this functionality, you must make extensive use of proxies; see Figure 1. The server application does not publish its server object directly. Instead, it publishes a login object, which is bound to the local registry on the server. Whenever a client wants to use the server, it first locates the login object from the registry, then authenticates with it by sending its username and password. If the user is unknown to the server, the client gets a security exception. If the authentication process succeeds, the login object returns a reference to a proxy object that lives on the server. All the calls from the client go through that proxy object. Whenever there is a call, that call is forwarded to the "real" server object if the user has the necessary permissions for invoking the method; if not, an exception is thrown.

One key aspect of this approach is that the login object creates a new proxy for each existing client. The proxy contains information about the particular client using it. The server knows exactly who is making the call because the only way a client can get a reference to a server proxy is by using the login object whenever there is a method evocation.

In the Java 2 security architecture, security checks are based on who signed the code and where it came from. But when developing server applications, it is often also necessary to make security verifications based on who is running the code. The Java Authentication and Authorization Service (JAAS) API lets you do that. Here, I use JAAS for associating permissions to users when they access the login object. This way, it's straightforward to check whether users have the necessary privileges when a call is about to be made on the server object. With JAAS, you can create a fine-grained security policy according to who is making the calls.

Authentication

The LoginInterface and the LoginImpl definitions (Listings Three and Four, respectively) provide what is needed for authentication. LoginImpl is the login object that is bound to the local server registry. It implements a method called login(), which takes a username and password as parameters. If a user is correctly authenticated, it returns a reference to a proxy object that implements the interface of the server (ServerInterface); if not, a security exception is thrown.

The login() method starts by creating a Subject object, named user, which represents the user on the server. Subject is a basic class from JAAS that represents any entity that can be authenticated.

After user is created, an RMILoginPrincipal containing the user's username is added to the object. RMILoginPrincipal is a simple class derived from java.security.Principal (see Listing Five), which lets the username be stored and used in the JAAS policy file. Referring to the policy file in Listing Two, you see the name of the class being used to refer to "root", "alice", and "guest" (that is, authrmi.RMIPrincipal). The method then checks whether the username/password supplied by the client matches anyone in the password file. If so, a new proxy object that represents that user is created, and a reference is returned to the client; if not, an InvalidUserException is raised. In this example, the password file is in plain text. In a real application, this would not be acceptable, and passwords should at least be saved as a hash value.

Authorization

At this point, the client has a reference to a server proxy that knows the client's identity. This proxy is an instance of ServerProxy (see ServerProxy.java, available electronically). The proxy object implements all the methods that the real server object does (it implements ServerInterface). Whenever a call is made to doOperationA() or doOpeationB(), the checkPermission() method is called. When this happens, two outcomes are possible: The call returns silently and the method on the real server object is invoked, or checkPermission() throws a security exception that is propagated to the client. This means that the user doesn't have the necessary permissions for making the call.

The checkPermission() method contains a single call:

try
{
	Subject.doAs(user, new ValidateMethodCall(methodName));
}
catch (java.security.PrivilegedActionException e)
{
	throw (SecurityException) e.getException();
}

The static method Subject.doAs(), in the core of the JAAS API, lets a specific piece of code be called using the permissions of a Subject, instead of the permissions of the calling code. Thus, you use the permissions specified for the user in the JAAS policy file to execute the code in ValidateMethodCall. ValidateMethodCall checks if the user actually has the permissions to make the doOperationXXX() on the server. This is the tricky part.

Whenever you make a call to Subject.doAs(), an object that either implements the java.security.PrivilegedAction or java.security.PrivilegedExceptionAction must be passed. ValidateMethodCall implements the second one. Both interfaces require a single method to be implemented—run(). It is in this method that the code to be executed in the name of the Subject lives. The difference between the two interfaces is that the second one lets you retrieve an exception that was thrown during the execution of run(). Looking at ServerProxy.java, you see that any exception thrown by the call Subject.doAs() is propagated back to the client application. So, what is being done inside the run() method of ValidateMethodCall? Inside this method, there is a single call in which a number of things happen:

AccessController.checkPermission(new ServerPermission(methodName));

For one thing, a new ServerPermission object is created containing the name of the method being called. This object is passed to the AccessController, which checks whether the current context has a permission that implies the specified permission. What this means is that if the calling code has a permission that is equal to or supersedes the specified permission, the call returns silent; if not, a security exception is thrown. AccessController is a class in the Java 2 platform that lets security checks be made according to the current context. When you install a security manager in Java, the calls are typically forwarded to the AccessController class.

Granted, this can be confusing; see Figure 2. The call to AccessController is being made inside of a call to Subject.doAs(). Thus, the current context only has the permissions that were specified for the user in the policy file. But how does the AccessController know which permissions users have? The java.security.auth.policy property specifies which policy file is used by JAAS. When the API is loaded, that file is parsed and the permissions associated to each user is read. When a call to Subject.doAs() is made, JAAS makes sure that the called context will only contain the permissions of the Subject specified in the call (the user object). ServerPermission is a class that represents the permissions that users have on the server. As you can see in ServerPermission.java (available electronically), it extends java.security.BasicPermission. This type of permission represents a named Boolean permission that a principal either has or doesn't have. It also supports wildcards. Looking at the policy file (Listing Two), you see the full name of this class being used for associating permissions with users. When JAAS is parsing the file, it automatically instantiates objects of this class for representing the permissions each user has. In fact, the AccessController traverses all the permissions a user has to see if any implies the permission passed as a parameter.

That's basically it. Through this scheme, you get a server that has a fine-grained security policy for the methods being called on the server objects. One other thing you should consider is that JAAS is only used to verify whether a user has the necessary permissions for calling methods. The methods themselves are still called with the full permissions given to the server. Nevertheless, if you wish to specify exactly what permissions each user has on the code being executed on the server, it's just a question of moving the proxy calls inside the Subject.doAs() evocation. For instance, if you have a method on the server that lets a client read a file from disk, you could use a java.io.FilePermission to say that "alice" can only access files in "/home/alice."

Technical Hurdles

Up until now, I have assumed that if a client doesn't go through the login object, it can't access any of the proxy objects. That isn't strictly true. When you first export a remote object such as ServerProxy, the RMI run time opens a new port on the machine where calls can be made to the object. (Only one port is open per class.) So imagine you are exporting several ServerProxy objects that correspond to several clients. Clever programmers can read the Java RMI specification and browse the RMI source code to find out how to call these objects.

Fortunately, that isn't so simple. If you check the RMI wire protocol and read the source code, you discover that when an object is published, it is identified by a 176-bit object ID. The format of this identifier is something like:

<UID[112 bit], obj_number[64 bit]>

where UID is <unique_number[32 bit], timestamp[64 bit], count[16 bit]>. The identifier unique_number represents a unique number generated in the context of the running JVM, timestamp represents the time the first object of the class was exported, and count is a simple incremental counter. obj_number represents a number uniquely associated to the object being exported. If you use RMI normally, the exported objects have no UID, and obj_number keeps counting whenever a new object is exported. Thus, it would be trivial for a malicious person to use a proxy object of someone else.

An interesting thing happens when you set the java.rmi.server.randomIDs property to True. In this case, a new UID is generated for each new object. Nevertheless, unique_number and timestamp are equal for all exported objects. Only count is changed for each new object. Thus, the UID by itself wouldn't make the system much safer. But when you set this property, obj_number stops being a counter and becomes a securely generated 64-bit random number. So if someone wants to access an object by using the wire protocol, it must find the correct UID of the object and get the 64-bit number right. This constitutes a sparse space large enough for preventing most attacks. Even so, in some cases it might be advisable to set the java.rmi.server.logCalls property to True, so that all RMI calls are logged.

If you want to check which object identifiers your program is using, just add the following line to the constructor of ServerProxy:

System.out.println("Object ID:" + get Ref().remoteToString());

Also notice that the real server object—ServerImpl from Listing One—does not extend java.rmi.server.UnicastRemoteObject. That would be a serious mistake since it would make the server object available on the network, and that object should never be exported. One final point is that it is quite easy for anyone to eavesdrop on the connections and find out the usernames and passwords of the users. If you are deploying this kind of security, you should consider using RMI over SSL. For that you will need the Java Secure Socket Extension (JSSE; http://java.sun.com/ products/jsse/index.html). It's straightforward to setup RMI to use SSL. (If you are using JDK 1.4, JSSE is already integrated into the JDK.)

Final Details

The file MyServer.java (available electronically) presents the main class of the server and of the client. On the server, a security manager is installed so that JAAS will work. Before that, two properties are configured—one that represents the policy file to use for the whole program (java.security.policy), and another that represents the policy file of permissions associated to each user (java.security.auth.policy). Also notice that the server creates a local registry, rather than relying on the rmiregistry program. This is more practical than running rmiregistry all the time. The client simply connects to a server and tries to call both methods. Since the client doesn't install any security manager, no dynamic class downloading takes place. For running applications that use JAAS, be careful with the deployment of the classes. Any classes that will be used on Subject.doAs() calls must be placed in a separate classpath from the main classes. This happens because if you give certain permissions to classes in the global policy file, they cannot be revoked on the JAAS' policy file. If you look at the policy file of Listing Six, you notice that full permissions (java.security.AllPermission) are given to the main classes of the server. If you included the ValidateMethodCall class on the same code source, it would also have those permissions, independent of which permissions were granted to the users, and no security exception would be thrown. So for deploying this server, you just create two jar files—one with all the classes of the server, and one with the ValidateMethodCall. The first one has complete permissions, the second one has none. It will have the permissions associated to each user when the Subject.doAs() call is made.

To run the server I present here, you must also download JAAS (http://java.sun .com/products/jaas/) and install it locally on your machine. You can either put it on the classpath of the server or install it as a Java standard extension. If you are using JDK 1.4, you don't have to do anything because JAAS is already integrated in the new JDK.

DDJ

Listing One

package authrmi;
/** The interface of the server. */
public interface ServerInterface
   extends java.rmi.Remote
{
   /** The first operation. @throws SecurityException If the client doesn't 
   * have permissions for executing this method. */
public void doOperationA()
      throws java.rmi.RemoteException, SecurityException;
   /** The second operation. @throws SecurityException If the client doesn't 
   * have permissions for executing this method. */
   public void doOperationB()
      throws java.rmi.RemoteException, SecurityException;
}

package authrmi;
/** The actual implementation of the server. */
public class ServerImpl
   implements ServerInterface
{
   /** The first operation. */
   public void doOperationA()
   {
      System.out.println("Operation A!");
   }
   /** The second operation. */
   public void doOperationB()
   {
      System.out.println("Operation B!");
   }
}

Back to Article

Listing Two

grant Principal authrmi.RMILoginPrincipal "root"
{
   permission authrmi.permissions.ServerPermission "*";
};
grant Principal authrmi.RMILoginPrincipal "alice"
{
   permission authrmi.permissions.ServerPermission "doOperationA";
};
grant Principal authrmi.RMILoginPrincipal "guest"
{
};

Back to Article

Listing Three

package authrmi;
/** Interface for client users to login. */
public interface LoginInterface extends java.rmi.Remote
{
   /** Method that lets clients login, returning an interface to the server.
   * @param username The name of the user.
   * @param password The password of the user.
   * @return A reference to a proxy of the server object.
   * @throws SecurityException If the client is not allowed to login. */
   public ServerInterface login(String username, String password)
      throws java.rmi.RemoteException, SecurityException;
}

Back to Article

Listing Four

package authrmi;

import authrmi.exceptions.*;
import javax.security.auth.*;
import java.util.*;

/** Implements the server object that allows clients to login. */
public class LoginImpl
   extends java.rmi.server.UnicastRemoteObject
   implements LoginInterface
{
   /** The real server object */
   private ServerInterface myServer;
   ////////////////////////
   /** Class constructor. @param theServer The real server object. */
   public LoginImpl(ServerInterface theServer)
      throws java.rmi.RemoteException
   {
      myServer = theServer;
   }
   /** Allows a client to login and get an interface to the server. */
   public ServerInterface login(String username, String password)
      throws java.rmi.RemoteException, SecurityException
   {
      // Creates a subject that represents the user
      Subject user = new Subject();
      user.getPrincipals().add(new RMILoginPrincipal(username));
      // Check if this user can login. If not, an exception is thrown
      // Checks if the user is known and the password matches
      String realPassword = null;
      try
      {
         Properties passwords = new Properties();
         passwords.load(new java.io.FileInputStream(Constants.PASS_FILENAME));
         realPassword = passwords.getProperty(username);
      }
      catch (java.io.IOException e)
      {
         throw new InvalidUserException(username);
      }
      if ((realPassword==null) || !realPassword.equals(password))
      {
         throw new InvalidUserException(username);
      }
      // Return a reference to a proxy object that encapsulates the access
      // to the server, for this client
      return new ServerProxy(user, myServer);
   }
}

Back to Article

Listing Five

package authrmi;
/** Class used for representing an authenticated user in the system. */
public class RMILoginPrincipal
   implements java.security.Principal
{
   /** The username */
   private String username;
   ////////////////////////
   /** Class constructor.  @param username The username of the user. */
   public RMILoginPrincipal(String username)
   {
      this.username = username;
   }
   /** Returns the username of the user. @return The username. */
   public String getName()
   {
      return username;
   }
}

Back to Article

Listing Six

grant codebase "file:authrmi_main.jar"
{
   permission java.security.AllPermission;
};
grant codebase "file:authrmi_actions.jar"
{
};



Back to Article

Nov02: Java

Figure 1: Global architecture.

Nov02: Java

Figure 2: Authorization by using ServerProxy.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.