Book Home Java Distributed Computing Search this book

5.5. Digital Signatures

Certification and authentication are used to protect access to resources in general, by ensuring that only those authorized to have them can get them. An entity (e.g., person, host, software agent) is given some kind of certification of their identity or membership in a particular group (e.g., "Fred Smith," "employee of company X," "all computers in department Y"). The entity has to offer this certificate in order to be authenticated and given access to the resources being protected.

A typical example of certification in practice is restricting FTP sites to a selected list of hosts on the network. A remote host has to provide its IP address when requesting an FTP connection to the site. The restricted FTP site looks up the IP address in its access table to see if the remote host is certified to access the files on this server. The IP address, then, is acting as an access certificate for this transaction, and the FTP server authenticates the remote host by checking the IP address against its access table. In encrypted data transfers, the encryption key is also acting as a sort of certificate for the receiving party, indicating that they have authority to read the information being sent.

5.5.1. A Motivating Example: A Credit Agent

If you look closely at our SimpleAgent example in Example 5-1, you'll notice that the agent doesn't make any attempt to check who is at the other end of the socket that it opens. In some applications, this might not be a problem. But let's suppose that we're designing a database query agent for a credit-card holding company, servicing requests for credit information about account holders. We decide to implement this query agent with a subclass of SimpleAgent called CreditAgent, shown in Example 5-2. This subclass implements the processMsg() method from SimpleAgent by checking each message from the remote agent for information about the identity of the account being checked. Once the account name has been retrieved from the client's message, the CreditAgent retrieves the information from the database with a getCreditData() method. (In this example, the getCreditData() method isn't fully implemented.) The processMsg() method puts the retrieved account information into a return message, and adds the message to the queue to be sent to the remote agent.

Example 5-2. A Networked Credit Agent

package dcj.examples.security;

import java.io.*;

public class CreditAgent extends SimpleAgent {
  public CreditAgent(String host, int port) {
    super(host, port);
  }
  
  protected void processMsg(String msg) {
    String name = null;
    String cmd = null;
    String retMsg = new String();

    // Parse the command and account name from the input stream.
    StreamTokenizer stok = new StreamTokenizer(new StringReader(msg));
    try {
      stok.nextToken();
      cmd = stok.sval;
      name = stok.sval;
    }
    catch (IOException e) {}

    if (cmd.compareTo("GET") == 0) {
      String cData = getCreditData(name);
      retMsg = name + " " + cData;
    }
    else {
      retMsg = "UNKNOWN_CMD";
    }

    // Add return message with results to the message queue.
    addMsg(retMsg);
  }
  
  protected String getCreditData(String acctName) {
    // Real method would use account name to
    // initiate a database query...
    return "No info available.";
  }
}

Can you see where this is leading? This agent will obviously need to authenticate the identity of remote agents requesting this sensitive credit information, to ensure that they're authorized to receive it. In its current form, a client only needs to know the name of an account in order to retrieve credit information about that account. This information is readily available to just about anyone who cares to find it, so it certainly doesn't qualify as the basis for authenticating remote agents.

5.5.2. Public Key Signatures for Authentication

The Java Security API provides the Signature class as a way to generate a digital signature, or to verify the identity of a remote agent that is sending you data. The Signature class uses public/private key pairs to generate and verify signatures. The party sending a signed message generates a signature by taking a piece of data and encoding it using their private key into a digital signature:

PrivateKey myKey = ... // Retrieve my private key
byte[] data = ... // Get the data to be signed
Signature mySig = Signature.getInstance("RSA");
mySig.initSign(myKey);
mySig.update(data);
byte[] signedData = mySig.sign();

A Signature is created using the static Signature.getInstance() method with the name of the algorithm for the signature. The signature is initialized for signing by passing a PrivateKey into its initSign() method. The private key used to initialize the signature presumably comes from a key pair created earlier, where the private key was stored securely on the local host and the public key was communicated to other agents as needed. The signature is given data to be signed using its update() method, and the digital signature for the data is gotten by calling the sign() method. After this call is made, the Signature data is reset, and another piece of data can be signed by calling update() and sign() in sequence again. Note that the update() method can be called more than once before calling sign(), if the data to be signed is stored in separate data items in memory.

Now the original data and the digital signature for the data can be sent to another agent to verify our identity. Assuming that the remote agent already has our public key, the data and the signature can be sent over a simple stream connection:

DataOutputStream dout = ... // Get an output stream to the agent
dout.writeInt(data.length);
dout.write(data);
dout.writeInt(signedData.length);
dout.write(signedData);

Before each set of bytes is sent, we send the size of each chunk of data, so that the remote host knows how many bytes to expect in each piece of data. On the remote host, the data and signature can be read from the stream connection:

DataInputStream din = ... // Get an input stream from the signer
int dataLen = din.readInt();
byte data[] = new byte[dataLen];
din.read(data);
int sigLen = din.readInt();
byte sig = new byte[sigLen];
din.read(sig);

To verify the signature, the agent creates a Signature object using the signer's public key, initializes it with the raw data from the signer, and then verifies the signature from the signer:

Signature theirSig = Signature.getInstance("RSA");
PublicKey theirKey = ... // Get public key for remote agent
theirSig.initVerify(theirKey);
theirSig.update(data);
if (theirSig.verify(sig)) {
    System.out.println("Signature checks out.");
}
else {
    System.out.println("Signature failed. Possible imposter found.");
}

The agent receiving the signature also uses a Signature object to verify the signature. It creates a Signature and initializes it for verification by calling its initVerify() method with the signing agent's PublicKey. The unsigned data from the remote agent is passed into the Signature through its update() method. Again, the update() method can be called multiple times if the data to be verified is stored in multiple objects. Once all of the unsigned data has been passed into the Signature, the signed data can be verified against it by passing it into the verify() method, which returns a boolean value.

5.5.3. An Authenticating Credit Agent

To make our credit agent more secure, we've created a subclass of our SimpleAgent class called AuthAgent, shown in Example 5-3. After the SimpleAgent constructor creates a socket connection to the remote agent, the AuthAgent constructor attempts to authenticate the remote agent by reading a message from the agent with a digital signature. First it reads an identifier for the agent from the input stream, so that it can look up the agent's public key. Then it reads a set of data and the corresponding signature for the data. The agent ID, original data, and data signature are passed to the authenticate() method for verification. The authenticate() method first looks up the named agent's PublicKey from some kind of local storage, using a lookupKey() method whose implementation is not shown here. If the key is found, then a Signature is created using the algorithm specified by the PublicKey. The Signature is initialized for verification using the agent's PublicKey. Then the original data is passed into the Signature's update() method, and the digital signature from the agent is verified by calling the verify() method. If the signature checks out, then we initialize an Identity data member with the remote agent's name and PublicKey.

Example 5-3. An Authenticating Agent

package dcj.examples.security;

import java.lang.*;
import java.net.*;
import java.io.*;
import java.security.*;

public class AuthAgent extends SimpleAgent {

  Identity remoteAgent = null;

  public AuthAgent(String host, int port)
      throws IllegalArgumentException {

    super(host, port);
    DataInputStream din = new DataInputStream(inStream);

    // Try to authenticate the remote agent
    try {
      String agentId = din.readUTF();
      int dataLen = din.readInt();
      byte[] data = new byte[dataLen];
      din.read(data);
      int sigLen = din.readInt();
      byte[] sig = new byte[sigLen];
      din.read(sig);

      if (!authenticate(agentId, data, sig)) {
        // Failed to authenticate: write error message, close socket and
        // return
        System.out.println("Failed to authenticate remote agent " + 
        closeConnection();
      }
      else {
        // Remote agent is authenticated, first message is a welcome
        addMsg("HELLO " + agentId);
      }
    }
    catch (Exception e) {
      closeConnection();
    }
  }

  protected boolean authenticate(String id, byte[] data, byte[] sig) {
    boolean success = false;
    PublicKey key = lookupKey(id);
    try {
      // Set up a signature with the agent's public key
      Signature agentSig = Signature.getInstance(key.getAlgorithm());
      agentSig.initVerify(key);
      // Try to verify the signature message from the agent
      agentSig.update(data);
      success = agentSig.verify(sig);

      if (success) {
        // Agent checks out, so initialize an identity for it
        remoteAgent = new Identity(id);
        remoteAgent.setPublicKey(key);
      }
    }
    catch (Exception e) {
      System.err.println("Failed to verify agent signature.");
      success = false;
    }

    return success;
  }
}

Using this subclass of SimpleAgent, we can make our CreditAgent authenticate the agents requesting credit data by checking their authenticated identities against an access list. The updated credit agent is called AuthCreditAgent, and is shown in Example 5-4. The AuthCreditAgent extends the AuthAgent subclass, and adds an Acl for the credit data it's serving. The constructor, after calling its parent classes' constructors to initialize and authenticate the connection to the remote agent, initializes an Acl object with the access rights given to various agents over the credit data by calling its initACL() method. This data might be retrieved from a relational database, or from some kind of directory service. (The details of the implementation of initACL() are not shown.)

Example 5-4. An Authenticating Credit Agent

package dcj.examples.security;

import java.io.*;
import java.security.*;
import java.security.acl.*;

public class AuthCreditAgent extends AuthAgent {

  protected Acl creditACL;

  public AuthCreditAgent(String host, int port) {
    super(host, port);
    // Initialize our access control lists
    initACL();
  }

  protected void initACL() {
    creditACL = new Acl();
    // Read resources and access permissions
    // from a database, initialize the ACL object
    // ...
  }

  protected void processMsg(String msg) {
    String name = null;
    String cmd = null;
    String retMsg = new String();

    // Parse the command and account name from the input stream.
    StreamTokenizer stok = new StreamTokenizer(new StringReader(msg));
    try {
      stok.nextToken();
      cmd = stok.sval;
      name = stok.sval;
    }
    catch (IOException e) {}

    if (cmd.compareTo("GET") == 0) {
      if (isAuthorized(remoteAgent, name, "READ")) {
        String cData = getCreditData(name);
        retMsg = name + " " + cData;
      }
      else {
        retMsg = "UNAUTHORIZED";
      }
    }
    else {
      retMsg = "UNKNOWN_CMD";
    }

    // Add return message with results to the message queue.
    addMsg(retMsg);
  }

  protected String getCreditData(String acctName) {
    // Real method would use account name to
    // initiate a database query...
    return "No info available.";
  }

  protected boolean isAuthorized(Identity agent,
                                 String acctName, String access) {
    boolean auth;
    Permission p = new PermissionImpl(access);
    auth = creditACL.checkPermission(agent, p);
    return auth;
  }
}

With the remote agent authenticated and an access control list initialized, the AuthCreditAgent processes messages in its processMsg() method much as it did before. The difference is that, before looking up the credit data for an account and sending it back to the agent, the agent's access is checked by calling the isAuthorized() method and passing the agent's Identity, the account name, and the type of access required ("READ" access, in this case). The isAuthorized() method simply calls the checkPermission() method on the ACL object to verify that the authenticated agent has the required permissions on the account being accessed. In this example implementation, we're simply checking the global access rights of the user, rather than the user's access to the given account. In processMsg(), if the agent is authorized, then the data is retrieved and returned to the agent in a message. If not, then a curt "UNAUTHORIZED" message is returned to the agent.

5.5.4. Certification: The Last Identity Link

Our credit agent has come a long way from its initial, rather insecure incarnation. A remote agent now has to provide both an account name and a valid digital signature in order to even make a connection with the AuthCreditAgent. And once it has the connection, it has to be on the access control list for the account before the credit agent will release any information about it.

What we haven't answered yet is the question of how the client gets added to the access list. Somehow the credit agent has to get the remote agent's public key, verify their identity, and authorize them with the right access privileges based on their identity.

5.5.5. Distributing Certified Public Keys

A PublicKey can be distributed to other agents on the network pretty easily. Since the Key interface extends Serializable, you can send a key over an ObjectOutputStream to a remote agent:

OutputStream out = socket.getOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
PublicKey myKey = ... // Get my public key
// Send my name
objOut.writeObject(new String("Jim"));
// Send my key
objOut.writeObject(myKey);

The remote agent can read my key off of its input stream and store the key on a local "key-ring":

InputStream in = socket.getInputStream();
ObjectInputStream objIn = new ObjectInputStream(in);
String name = (String)objIn.readObject();
PublicKey key = (PublicKey)objIn.readObject();
storeKey(key, name);

But who's to say it was really me that sent the key along with my name? As we mentioned earlier, a sneaky attacker could pretend to be me, send an agent my name with her own public key, and proceed to access resources using my identity. As we mentioned earlier, this problem is solved by certificates. A certificate acts as corroborating evidence of an agent's identity. In most certification scenarios, a well-known certification authority (CA) verifies the identity of an agent with whatever means it deems necessary, physical or otherwise. When the authority is certain of the agent's identity, a certificate is issued to the agent verifying its identity. The certificate is typically signed with the CA's own digital signature, which has been communicated to parties that will need to verify agents using certificates issued by the CA. This communication may use secure, offline channels, or even nonelectronic delivery by known individuals. In any event, a CA will typically be known by many other agents on the network, and its identity can be reverified if it's believed that their signature has been forged.

Once an agent has a certificate attesting to its identity, it can broadcast its public key to other parties by sending the key over open channels, then sending a digital signature that includes its certificate. The certificate itself can be used as the data to be signed. The party receiving the signature can verify the agent's public key by verifying the signature against the data message, and then verifying the identity behind the public key by checking the certificate with the certification authority that it came from. If the certificate checks out, then we can be assured that the agent is the agent named in the certificate, and that the agent has a private key corresponding to the public key it sent us. In order to break through these security barriers, a hostile attacker would have to fool a certificate authority into thinking she is someone else, which any respectable CA will not allow to happen, or she would have to create a false certificate for her public key from a CA, which is not possible assuming that the certificate can be verified directly with the CA, and that the cryptographic algorithm used by the CA to generate their signature is not breakable.

Various standard certificate formats, such as X.509 and PKCS, have been defined and are in use in digital commerce, secure email, etc. At the time of this writing, the Java Security API does not include an API for generating or verifying these certificates. Support for standard certificate formats is promised in Java 1.2.



Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.