The Source for Java Technology Collaboration
User: Password:
Register | Login help    

Search

Online Books:
java.net on MarkMail:


 E-mail  Print

The New RMI

Thu, 2005-10-06

{cs.r.title}




Contents
RMI Security
   Security Overview
   Securing RMI
RMI as Service
Stubless RMI
Conclusion
Resources

Java Remote Method Invocation (RMI) introduced a powerful mechanism for distributing application logic across different machines. Instead of having to perform tasks in one monolithic central system, RMI made it possible to build modular and manageable applications where the computing logic could be distributed. RMI has been the primary communication mechanism for various server-side component architectures, including Enterprise JavaBeans (EJB). Introduced with Java 1.1, RMI has been steadily evolving with every major release and has seen the introduction of three new important features with the release of Java 5.0. The new features include support for dynamic stub generation, RMI over SSL, and the ability to launch a Java RMI sever as an extended Internet service (xinetd) daemon in Unix systems. In this article we will cover these additions.

RMI Security

The default RMI communication mechanism—the Java Remote Method Protocol (JRMP)—is not secure. It is possible to secure the communication by writing custom socket factories using Java Secure Socket Extension (JSSE). But this approach puts the burden of writing additional code on the developers, who must take care of securing data exchange using cryptography. Java 5.0 alleviates this issue by introducing two new classes, javax.rmi.ssl.SslRMIClientSocketFactory and javax.rmi.ssl.SslRMIServerSocketFactory, that provide the ability to secure the communication channel between the client and the server using the Secure Sockets Layer (SSL)/Transport Layer Security (TLS) protocols. These socket factory classes provide a simple and elegant way to use JSSE for secure Java RMI communication, which enables enforcement of data integrity, data confidentiality (through encryption), server authentication, and (optionally) client authentication for remote method invocations. This means developers can focus on the business logic of the distributed application instead of dealing with security-related plumbing.

In this section we will go over the details of using the new SSL socket factories for RMI communication. But before we delve into details, we will provide a brief introduction on transport level security. Secure Sockets Layer (SSL) is the most widely used protocol for implementing cryptography over a distributed communication protocol such as HTTP. The primary purpose of SSL is to provide privacy, data integrity, authenticity, and non-repudiation. This is achieved by using symmetric key cryptography for data encryption between the client and server, and asymmetric key cryptography (or public/private key cryptography) to authenticate the identities of communicating parties as well as to encrypt the shared encryption key that is used during establishing SSL session.

Security Overview

A client and server that are about to exchange information via the SSL protocol first establish an SSL session after exchanging a series of messages, a process known as an SSL handshake. This is a multi-step process, so we will take a look at a few important messages that are sent between the client and server. The messages in the following sections are from the SSL debug output from the sample application that is provided along with this article (see Resources). The client starts the handshake process by sending a "Hello" message to the server with a list of cipher suites that it supports. This message is as follows:

*** ClientHello, TLSv1
RandomCookie: GMT: 1101964544 bytes = { 99,...,232 }
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5,
SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA,
TLS_DHE_RSA_WITH_AES_128_CBC_SHA, ...]

The server responds with a "Hello" message along with a decision on the cipher suite and the compression method. In this particular case, the server chose SSL_RSA_WITH_RC4_128_MD5 for cipher method and 0 for compression method.

*** ServerHello, TLSv1
RandomCookie:  GMT: 1101964544 bytes = { 78,...,209}
Cipher Suite: SSL_RSA_WITH_RC4_128_MD5
Compression Method: 0
***
%% Created:  [Session-1, SSL_RSA_WITH_RC4_128_MD5]
** SSL_RSA_WITH_RC4_128_MD5

What this means is that the client-server communication is going to perform the following:

  • Use RSA (Rivest Shamir Adleman) public/private key cryptography for key exchange.
  • Use RC4 (Rivest Cipher Version 4) with 128-bit encryption for data exchange.
  • Use MD5 (Message Digest Version 5) for hashing to ensure message integrity during the exchange.

After sending the "Hello" message, the server sends its certificate and the client verifies it. The certificate that is sent to the client is served up from the server's keystore, which is essentially a flat file that serves as a database for the contents, such as key pairs and their associated certificates or certificate chains. The client that receives the certificate from the server will pursue establishing the SSL connection only when it can authenticate the server. This trust can be established only when the client can verify the digital signature on the server's certificate. This is possible when the client trusts the server or it trusts at least one of the signers in the certificate chain provided by the server. This trust assertion is based on the certificates present in the truststore that the client is configured with. Essentially, truststore is a database of certificates or certificate chains that the client can trust. The default truststore is cacerts, which can be found in $JAVA_HOME\jre\lib\security.

Once the server certificate verification process is complete, the client uses the public key of the server to send a ClientKeyExchange message to the server. This message contains some random information that will be used for the generation of a symmetric key. This key will be used for encrypting the content during data exchange. The server checks to see if any other client already uses the symmetric key; if so, it will ask the client to regenerate another random key.

*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
Random Secret:  { 3, 1, 59, 151, 102, 56, 204, 6,
249, 100, 25, 171, 9, 221, 105, 97, 0, 106, 77,
10, 180, 237, 222, 165, 9, 116, 216, 10, 181, 54,
32, 244, 46, 158, 73, 18, 17, 249, 32, 254, 10,
249, 196, 196, 185, 139, 70, 17 }

Once the client and server agree on a symmetric key, the client sends a ChangeCipherSpec message indicating that it is now ready to communicate; this message is followed by a Finished message. The server responds by sending its ChangeCipherSpec message and a Finished message:

main, WRITE: TLSv1 Change Cipher Spec, length = 1
[Raw write]: length = 6
0000: 14 03 01 00 01 01
*** Finished

The setup configuration that an application developer needs to provide for the above client-server exchange is to specify a keystore for the server application and a truststore for the client application. We will go over how to create a keystore and a truststore and once we are done with infrastructure-related aspects, we will write RMI code and integrate it in order to establish secure communication. The keystore and truststore can be created using the keytool utility that is bundled with the JDK distribution. This utility can be found under $JAVA_HOME/bin. If the JDK's bin directory is in the PATH, then the keytool utility can be executed from a command window or shell.

  • First, we will generate a keystore that has a key pair (public and private key) along with a self-signed certificate. This is accomplished by executing the following command, which will create a file called Server_Keystore. The key algorithm is RSA, and the keysize is 1024 bits.
    $keytool -genkey -alias SecureServer
               -keyalg RSA -keystore Server_Keystore
    Enter keystore password:  password
    What is your first and last name?
      [Unknown]:  kv
    What is the name of your organizational unit?
      [Unknown]:  IT
    What is the name of your organization?
      [Unknown]:  ABC
    What is the name of your City or Locality?
      [Unknown]:  KC
    What is the name of your State or Province?
      [Unknown]:  MO
    What is the two-letter country code for this unit?
      [Unknown]:  US
    Is CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US correct?
      [no]:  y
    
    Enter key password for <SecureServer>
        (RETURN if same as keystore password):
            
  • Next, we will examine the contents of the generated Server Keystore, which is accomplished by the following command.
    $keytool -list -v  -keystore Server_Keystore
    
    Enter keystore password:  password
    
    Keystore type: jks
    Keystore provider: SUN
    
    Your keystore contains 1 entry
    
    Alias name: secureserver
    Creation date: Aug 12, 2005
    Entry type: keyEntry
    Certificate chain length: 1
    Certificate[1]:
    Owner: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
          until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
      MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:
            B2:43:CB
      SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:
            C0:DD:51:4F:B0:E8:97
    
    *******************************************
    *******************************************
    As it can be seen from the output of the above command that there is a "keyEntry" which is the private key for corresponding self signed certificate.

  • We are done creating a key entry for the server, so we will shift our focus to the client side. The next step is to create a self-signed certificate and this is accomplished by executing the following commands.
    $keytool -export -alias SecureServer -keystore
                 Server_Keystore -rfc -file Server.cer
    Enter keystore password:  password
    Certificate stored in file <Server.cer>

    Just to see what the certificate looks like, we'll print to the console with the following:

    $cat Server.cer 

    This will print a byte string that starts with -----BEGIN CERTIFICATE----- and ends with -----END CERTIFICATE-----


  • Now that we have created a self-signed certificate, the next logical step is to import this certificate into a truststore, which then can be used by the client. This is accomplished by the following command.
    $keytool -import -alias SecureServer -file Server.cer \
            -keystore Client_Truststore
    Enter keystore password:  passsword
    Owner: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=KC, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
          until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
       MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:
             B2:43:CB
       SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:
             C0:DD:51:4F:B0:E8:97
    Trust this certificate? [no]:  y
    Certificate was added to keystore
  • To verify the contents of the truststore that we created, we issue the following command. As can be seen from the output, the contents of the truststore contain a trustedCertEntry, which means that a private key is not available and should not be.
    keytool -list -v  -keystore Client_Truststore
    Enter keystore password:  password
    
    Keystore type: jks
    Keystore provider: SUN
    
    Your keystore contains 1 entry
    
    Alias name: secureserver
    Creation date: Aug 12, 2005
    Entry type: trustedCertEntry
    
    Owner: CN=kv, OU=IT, O=ABC, L=Kansas City, ST=MO, C=US
    Issuer: CN=kv, OU=IT, O=ABC, L=Kansas City, ST=MO, C=US
    Serial number: 42fc999e
    Valid from: Fri Aug 12 07:44:14 CDT 2005
        until: Thu Nov 10 06:44:14 CST 2005
    Certificate fingerprints:
      MD5:  08:09:5D:2C:4B:28:D9:94:48:69:6D:AE:8E:B2:43:CB
      SHA1: 14:BE:5F:88:1F:8D:2D:04:93:F6:22:02:84:C0:DD:51:4F:B0:E8:97
    
    *******************************************
    *******************************************

In this particular case, we are working with a self-signed certificate instead of certificates signed by Certification Authority (CA). If there is a need to get the certificate signed by a CA then a Certificate Signing Request(CSR) needs to be generated. The generated CSR, then, should to be submitted along with other pertinent information to a Certification Authority such as VeriSign or USPS, who will then digitally sign the certificate. The aspects of creating a CSR and getting it signed by a CA is beyond the scope of this article.

Securing RMI

We have covered all the security-related aspects. Now we will go about writing a simple secure server and a client. The server in this case is a simplified version of an RMI service that performs credit card authorization for the client. Of course, in the real world this will involve an EDI or Web service call to a credit card authorization gateway, made available by banking providers. In our case, we will just mimic this transaction by performing a simple mod 10 check and, if the check passes, we will return a good return-code; otherwise, we will return a failure return-code. This simple exercise proves the point that we need to secure the transaction because of sensitive data exchange.

The class diagram in Figure 1 provides a high-level view of the classes involved and follows the standard RMI programming semantics.

Typical RMI programming arrangement
Figure 1. Typical RMI programming arrangement

The CreditCardAuthorizer describes the core business operation and also extends the java.rmi.Remote interface. The CreditCardAuthImpl provides an implementation for the business method authorizeCreditCard(). In addition, there is a bind() method, which takes care of the RMI-specific plumbing. Three important things happen in bind(). First, the remote object is exported, so it can receive incoming calls using a transport that is specified by the socket factory parameters. Next, a Registry object is created on the specified port. Finally, the remote object is bound to the registry. As can be seen, a standard RMI programming paradigm is followed, and the only difference is the exportObject() method, where the SSL version of the client and server socket factory are used as method parameters.

javax.rmi.ssl.SslRMIClientSocketFactory;
javax.rmi.ssl.SslRMIServerSocketFactory;
....
RMIClientSocketFactory rmiClientSocketFactory =
                    new SslRMIClientSocketFactory();
RMIServerSocketFactory rmiServerSockeyFactory =
                    new SslRMIServerSocketFactory();

CreditCardAuthorizer ccAuth = (CreditCardAuthorizer)
      UnicastRemoteObject.exportObject(this, 0,
      rmiClientSocketFactory, rmiServerSockeyFactory);
Registry registry = LocateRegistry.createRegistry(2004);
registry.rebind(name, ccAuth);

Having covered the key programming aspects, the next logical step is to test the server and the client. This is where the keystore and truststore will be used. The server needs to use the keystore, and this can be accomplished in either of two ways: It can be specified as a command-line argument to the JVM, or the System property can be explicit in the application code. The following code snippets demonstrate both approaches.

  • Setting Keystore - Programmatically:
    System.setProperty("javax.net.ssl.keyStore", "./resources/Server_Keystore"); System.setProperty("javax.net.ssl.keyStorePassword", "password");
  • Setting Keystore - Command Line:
    java -Djavax.net.ssl.keyStore=./resources/Server_Keystore -Djavax.net.ssl.keyStorePassword=password com.article.jn.securermi.CreditCardAuthServer
  • Setting Truststore - Programmatically:
    System.setProperty("javax.net.ssl.trustStore", "./resources/Client_Truststore");
  • Setting Truststore - Command Line:
    java -Djavax.net.ssl.trustStore= ./resources/Client_Truststore

The client specifies the truststore in a similar fashion, except that with the truststore there is no need to specify the truststore password.

That's it! Now we can start the server and then start the client to watch the transaction run in a secure manner. To get a complete idea of what is happening under the hood, we could turn the SSL debug on and watch the SSL messages exchange between the server and the client, including some of the messages presented earlier in this article. This can be accomplishing by passing -Djavax.net.debug=all as a JVM parameter.

RMI as Service

In systems like Linux or FreeBSD, Internet services such as FTP and Telnet are set up to listen in on standard, well-known ports. For instance, FTP uses port 20 and 21, and remote access using the TELNET protocol is established over port 23. Any incoming TCP connection request targeted at one of these well-known port numbers is understood to be a request for a particular service. In order to establish a Telnet session to a remote host, the client requires a hostname lookup to find the remote IP address and then implicitly connect to that host's standard telnet port: 23.

In the operating systems that are hosting the service, a program called Internet Daemon—inetd—is started at boot time. This program takes a list of services it has to manage from a startup configuration file. The daemon creates sockets on behalf of the services and listens in on all of them simultaneously. When an incoming connection is requested for any of these sockets, inetd accepts the connection and spawns the service as a child process, passing the socket to it. inetd, also called the "super server," returns to listening in on the socket for new connections after performing the task. However, inetd has a few shortcomings. It is unable to: police access control on services, prevent denial of service attacks, perform detailed logging, and distinguish virtual hosts. This has resulted in the development of a better version of Internet Service deamon as a replacement for inetd: xinetd (eXtended inetd).

With the release of Java 5.0, it is possible to expose Java services via the (x)inetd deamon and start Java RMI services on demand. To make this happen, an application must use specific programming semantics to ensure that the application and its constituent services can be started from xinetd. In this section, we will build a simple Date server that is based on the sample echo server that ships with the JDK distribution and is documented in the API documentation. This server is diagrammed in Figure 2.

Class diagram for Date server
Figure 2. Class diagram for Date server

The DateServer follows typical RMI-related programming semantics: The DateService interface specifies the business methods, and the DateServer provides implementation details. However. in this case, since the server process will be started by the inetd process, setting up the registry will use the new Java 5.0 feature.

The DateServer delegates setting up the RMI Registry to the initializeWithInheritedChannel() method of the InitializeRegistry object. At a high level, this method obtains a channel that was inherited from the entity that spawned the Java VM, and then creates a registry where the remote proxy is bound for the clients to look up. The following code snippet demonstrates how to obtain the server socket that is inherited from the process that launched this virtual machine:

Channel channel = System.inheritedChannel();
ServerSocket serverSocket = null;

if (channel instanceof ServerSocketChannel) {
    serverSocket =
      ((ServerSocketChannel) channel).socket();
}

As the code snippet shows, the static inheritedChannel() method of the System class enables the code to obtain the Channel that was inherited from the process that spawned the Java service. This method returns an instance of java.nio.channels.SocketChannel or java.nio.channels.ServerSocketChannel, depending on how xinetd is configured. A SocketChannel is used to service a single incoming connection, and a ServerSocketChannel is used to service multiple incoming connections. We will discuss the xinetd configuration later.

Now that we have an instance of a server socket, the next step is to create a registry and bind the remote object to the registry. This is accomplished as follows:

RMIServerSocketFactory ssf =
    new RegistryServerSocketFactory(serverSocket);
Registry registry =
   LocateRegistry.createRegistry(port, null, ssf);
try {
    registry.bind(name, proxy);
} catch (...) {...}

The registry that is created listens for incoming requests on a given port using an instance of ServerSocket created from the custom RMIServerSocketFactory. This custom server socket factory creates and returns an instance of DelayedAcceptServerSocket, which is a subclass of ServerSocket. The core idea in doing this is to ensure that this instance blocks while accepting requests from the inherited ServerSocketChannel until the specified proxy is completely bound in the registry. This is accomplished by overriding the accept() method of the DelayedAcceptServerSocket. This method blocks until the thread that binds the proxy to the registry notifies the blocked thread that it is okay to accept the incoming socket request. Essentially, this operation prevents the clients from getting a java.rmi.NotBoundException while trying to look up a proxy that is not yet bound or may be in the process of binding to the registry.

The following code snippet demonstrates the overridden accept() method that blocks until the thread completes the bind, which is provided in the code snippet that follows:

public Socket accept() throws IOException {
  synchronized (lock) {
    try {
       while (!serviceAvailable) {
        lock.wait();
       }
    }catch (InterruptedException e) {
       throw (IOException)
        (new InterruptedIOException()).initCause(e);
    }
  }
  return serverSocket.accept();
}

Here, once the bind operation finishes successfully, all the blocked threads are notified:

Registry registry =
 LocateRegistry.createRegistry(port, null, ssf);
try {
   registry.bind(name, proxy);
} catch (...){
  ...
}

synchronized (lock) {
 serviceAvailable = true;
  lock.notifyAll();
}

Now that we have defined our RMI code, the next step in the process is to set it up as an RMI Service. This can be accomplished two ways: editing the appropriate xinetd/inetd configuration files directly, or using a GUI editor that is used for manipulating the configuration files, which is typically supplied along with the OS. The GUI tool that is bundled along is dependent on the flavor of Unix or Linux in use. In this particular case, we will be using YaST, which is bundled with Novell's SuSE Linux.

Figure 3 shows the SuSE Linux Control Center, and we are interested in Network Services.

YaST Control
Center GUI
Figure 3. YaST Control Center GUI

Opening "Network Services" will result in the configuration screen shown in Figures 4 and 5, which enables you to add, edit, or delete network services, as well as turn services on or off individually.

YaST list of services
Figure 4. YaST list of services

Editing a service in YaST
Figure 5. Editing a service in YaST

We add our RMI Service by providing details for the following fields.

  • Service name - The name of a valid service.
  • Socket type - The choices are stream, dgram, raw, rdm (reliably delivered message), or seqpacket (sequenced packet socket).
  • Protocol - A protocol listed in /etc/protocols, which is some type of network protocol such as IP, ICMP, TCP, and UDP.
  • Flags - wait/nowait - Wait applies to datagram sockets only. All other socket types should have the "nowait" option in this entry. Nowait entries are used for multithreaded servers that free their sockets after each request so they can continue receiving more requests on the same socket.
  • User - The name of the user the server will run as.
  • Group - Specify the group so that the server can run with a different group ID than the one specified in the password file for that user.
  • Server - The path and name of the program to be executed.
  • Server Arguments - Command-line arguments for the server program that is being run.
  • Comment - Any relevant comment that describes what this service does.

After entering appropriate data and clicking the Accept button, a file called rmi-date-server will be created in the /etc/xinetd.d directory. The content of this file looks like:

#RMI Date Server service rmi-date-server
{
   socket_type  = stream
   protocol     = tcp
   wait         = no
   user         = root
   group        = users
   server       = /opt/jdk1.5.0_01/bin/java
   server_args  = -Djava.rmi.server.hostname=192.168.0.2
    -classpath /home/Article/RMI/bin
                        com.article.jn.service.DateServer
}

Next, the rmi-date-server needs to be listed as a service in the /etc/services configuration file. This is accomplished by editing (using any text editor) the file to add the following entry. Note that editing this file will require root access. The format is:

rmi-date-server <port>/<protocol>,

In this entry, port is the port number for the service's local registry.

Now that the configuration of xinetd has been modified, the service needs to be restarted to read the new configuration changes. To do this, send the HUP (hangup) signal to the xinetd/inetd process. This is accomplished by determining the process ID with following command:

$ ps -ef | grep xinetd
root 2332 1 0 Jun 30 ? 0:02 /usr/sbin/xinetd -s

In this case, the process ID for xinetd is 2332. Now, to send the hangup signal for the xinetd process, issue the following command:

$su root
$ kill -HUP 2332

Now xinetd is all set to launch the Java 5 RMI service when a client attempts to connect to the port configured in the /etc/services file. To test this out, we can run a simple client that does the following:

Registry registry =
                LocateRegistry.getRegistry(host, port);
DateService proxy =  (DateService)
                registry.lookup("ServiceInterface");
System.out.println("received message from proxy: "
                                     + proxy.getDate());

This will result in an output to the client's standard out. As Figure 6 confirms, a java process is spawned by xinetd, which listens in on a specified port (9900 in this case) for client connections.

Active listening ports
Figure 6. Active listening ports

Stubless RMI

One of the best features of the new Java 5.0 RMI is that static generation of stubs using rmic is not required anymore. This feat is accomplished by the dynamic generation of stub classes at runtime. However, if the client is not running Java 5.0, then rmic must still be used to pre-generate stub classes for remote objects. Dynamic stub generation is made possible by two different changes that were part of previous JDK releases. First, the revision of the JRMP protocol in Java 2 enabled RMI to work without skeletons. Second, the introduction of Dynamic Proxies in Java 1.3 obviated the need for client-side stubs.

The JDK release notes provide the following explanation for stubless RMI: When an application exports a remote object (using the constructors or the static exportObject methods of the classes java.rmi.server.UnicastRemoteObject or java.rmi.activation.Activatable) and a pre-generated stub class for the remote object's class cannot be loaded, the remote object's stub will be a java.lang.reflect.Proxy instance (whose class is dynamically generated) with a java.rmi.server.RemoteObjectInvocationHandler as its invocation handler. An existing application can be deployed to use dynamically generated stub classes whether or not pre-generated stub classes exist by setting the system property java.rmi.server.ignoreStubClasses to "true". If this property is set to "true," then pre-generated stub classes are never used.

Conclusion

We zipped through the three most important and powerful features of RMI in Java 5.0. SSL security makes RMI client-server communication truly secure; the stubless RMI makes deployment a breeze; and the ability to expose RMI applications as a Unix service allows RMI services to be launched on demand.

Over a period of ten years, RMI has evolved incrementally to become a robust distributed computing technology. These additions will push forth RMI as the de facto solution when there is a need for developing distributed, loosely-coupled, object-oriented applications.

Resources

Krishnan Viswanath is currently working for JP Morgan Chase & Co. in Kansas City, MO.
Comments
Comments are listed in date ascending order (oldest first)