Search the Catalog
Java Cryptography

Java Cryptography

By Jonathan B. Knudsen
1st Edition May 1998
1-56592-402-9, Order Number: 4029
362 pages, $29.95

Sample Chapter 6:

Authentication

In this chapter:
Message Digests
MACs
Signatures
Certificates

The first challenge of building a secure application is authentication. Let's look at some examples of authentication from everyday life:

Authentication is tremendously important in computer applications. The program or person you communicate with may be in the next room or on another continent; you have none of the usual visual or aural clues that are helpful in everyday transactions. Public key cryptography offers some powerful tools for proving identity.

In this chapter, I'll describe three cryptographic concepts that are useful for authentication:

A common feature of applications, especially custom-developed "enterprise" applications, is a login window. Users have to authenticate themselves to the application before they use it. In this chapter, we'll examine several ways to implement this with cryptography.[1] In the next section, for instance, I'll show two ways to use a message digest to avoid transmitting a password in cleartext from a client to a server. Later on, we'll use digital signatures instead of passwords.

 
Message Digests

As you saw in Chapter 2, Concepts, a message digest takes an arbitrary amount of input data and produces a short, digested version of the data. The Java Cryptography Architecture (JCA) makes it very easy to use message digests. The java .security.MessageDigest class encapsulates a cryptographic message digest.

Getting

To obtain a MessageDigest for a particular algorithm use one of its getInstance() factory methods:

public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException
This method returns a MessageDigest for the given algorithm. The first provider supporting the given algorithm is used.
public static MessageDigest getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException
This method returns a MessageDigest for the given algorithm, using the given provider.

Feeding

To feed data into the MessageDigest, use one of the update() methods:

public void update(byte input)
This method adds the specified byte to the message digest's input data.
public void update(byte[] input)
Use this method to add the entire input array to the message digest's input data.
public void update(byte[] input, int offset, int len)
This method adds len bytes of the given array, starting at offset, to the message digest's input data.

You can call the update() methods as many times as you want before calculating the digest value.

Digesting

To find out the digest value, use one of the digest() methods:

public byte[] digest()
The value of the message digest is returned as a byte array.
public byte[] digest(byte[] input)
This method is provided for convenience. It is equivalent to calling update(input), followed by digest().

If you use a MessageDigest to calculate a digest value for one set of input data, you can reuse the MessageDigest for a second set of data by clearing its internal state first.

public void reset()
This method clears the internal state of the MessageDigest. It can then be used to calculate a new digest value for an entirely new set of input data.

One, Two, Three!

Thus, you can calculate a message digest value for any input data with just a few lines of code:

// Define byte[] inputData first.
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(inputData);
byte[] digest = md.digest();

Message digests are one of the building blocks of digital signatures. Message digests alone, however, can be useful, as you'll see in the following sections.

Digest Streams

The java.security package comes with two classes that make it easy to calculate message digests on stream data. These classes are DigestInputStream and DigestOutputStream, descendants of the FilterInputStream and FilterOutputStream classes in java.io.

Let's apply DigestInputStream to the Masher class from Chapter 1, Introduction. In that class, we read a file and calculated its message digest value as follows:

    // Obtain a message digest object.
    MessageDigest md = MessageDigest.getInstance("MD5");
 
    // Calculate the digest for the given file.
    FileInputStream in = new FileInputStream(args[0]);
    byte[] buffer = new byte[8192];
    int length;
    while ((length = in.read(buffer)) != -1)
        md.update(buffer, 0, length);
    byte[] raw = md.digest();

Now let's wrap a DigestInputStream around the FileInputStream, as follows. As we read data from the file, the MessageDigest will automatically be updated. All we need to do is read the entire file.

    // Obtain a message digest object.
    MessageDigest md = MessageDigest.getInstance("MD5");
 
    // Calculate the digest for the given file.
    DigestInputStream in = new DigestInputStream(
        new FileInputStream(args[0]), md);
    byte[] buffer = new byte[8192];
    while (in.read(buffer) != -1)
      ;
    byte[] raw = md.digest();

DigestOutputStream works the same way; all bytes written to the stream are automatically passed to the MessageDigest.

Protected Password Login

A basic problem in client/server applications is that the server wants to know who its clients are. In a password-based scheme, the client prompts the user for his or her name and password. The client relays this information to the server. The server checks the name and password and either allows the user into the system or denies access. The password is a shared secret because both the user and the server must know it. The obvious solution is to send the user's name and password directly to the server. Most computer networks, however, are highly susceptible to eavesdropping, so this is not a very secure solution.

To avoid passing a cleartext password from client to server, you can send a message digest of the password instead. The server can create a message digest of its copy of the password. If the two message digests are equal, then the client is authenticated. This simple procedure, however, is vulnerable to a replay attack. A malicious user could listen to the digested password and replay it later to gain illicit access to the server. To avoid this problem, some session-specific information is added to the message digest. In particular, the client generates a random number and a timestamp and includes them in the digest. These values must also be sent, in the clear, to the server, so that the server can use them to calculate a matching digest value. The server must be programmed to receive the extra information and include it in its message digest calculations. Figure 6-1 shows how this works on the client side.

 
Figure 6-1. Protecting a password

 

The server uses the given name to look up the password in a private database. Then it uses the given name, random number, timestamp, and the password it just retrieved to calculate a message digest. If this digest value matches the digest sent by the client, the client has been authenticated.

The following program shows the procedure from the client's point of view:

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.Date;
 
import Protection;
 
public class ProtectedClient {
  public void sendAuthentication(String user, String password,
      OutputStream outStream) throws IOException, NoSuchAlgorithmException {
    DataOutputStream out = new DataOutputStream(outStream);
    long t1 = (new Date()).getTime();
    double q1 = Math.random();
    byte[] protected1 = Protection.makeDigest(user, password, t1, q1);
 
    out.writeUTF(user);
    out.writeLong(t1);
    out.writeDouble(q1);
    out.writeInt(protected1.length);
    out.write(protected1);
    out.flush();
  }
 

public static void main(String[] args) throws Exception {

    String host = args[0];
    int port = 7999;
    String user = "Jonathan";
    String password = "buendia";
    Socket s = new Socket(host, port);
 
    ProtectedClient client = new ProtectedClient();
    client.sendAuthentication(user, password, s.getOutputStream());
 
    s.close();
  }
}

The bulk of the algorithm is in the SendAuthentication() method, in these lines:

    out.writeUTF(user);
    out.writeLong(t1);
    out.writeDouble(q1);
    out.writeInt(protected1.length);
    out.write(protected1);

Here we write the user string, timestamp, and random number as cleartext. Instead of writing the message digest right away, we first write out its length. This makes it easier for the server to read the message digest. Although we could code the server to always read a 20-byte SHA digest, we might decide to change algorithms some time in the future. Writing the digest length into the stream means we don't have to worry about the length of the digest, whatever algorithm we use.

Also note that ProtectedClient is not Socket-specific. You could use it to write authentication information to a file or an email message.

Some of the digestion that ProtectedClient performs will be mirrored in the server class. Therefore, ProtectedClient's sendAuthentication() method uses a static utility method, makeDigest(), that is defined in the Protection class. This class is shown below:

import java.io.*;
import java.security.*;
 
public class Protection {
  public static byte[] makeDigest(String user, String password,
      long t1, double q1) throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA");
    md.update(user.getBytes());
    md.update(password.getBytes());
    md.update(makeBytes(t1, q1));
    return md.digest();
  }
 
  public static byte[] makeBytes(long t, double q) {
    try {
      ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
      DataOutputStream dataOut = new DataOutputStream(byteOut);
      dataOut.writeLong(t);
      dataOut.writeDouble(q);
      return byteOut.toByteArray();
    }
    catch (IOException e) {
      return new byte[0];
    }
  }
}

Protection defines two static methods. The makeDigest() method creates a message digest from its input data. It uses a helper method, makeBytes(), whose purpose is to convert a long and a double into an array of bytes.

On the server side, the process is similar. The ProtectedServer class has a method, lookupPassword(), that returns the password of a supplied user. In our implementation, it is hardcoded to return one password. In a real application, this method would probably connect to a database or a password file to find the user's password.

import java.io.*;
import java.net.*;
import java.security.*;
 
import Protection;
 
public class ProtectedServer {
  public boolean authenticate(InputStream inStream)
      throws IOException, NoSuchAlgorithmException {
    DataInputStream in = new DataInputStream(inStream);
 
    String user = in.readUTF();
    long t1 = in.readLong();
    double q1 = in.readDouble();
    int length = in.readInt();
    byte[] protected1 = new byte[length];
    in.readFully(protected1);
 
    String password = lookupPassword(user);
    byte[] local = Protection.makeDigest(user, password, t1, q1);
    return MessageDigest.isEqual(protected1, local);
  }
 
  protected String lookupPassword(String user) { return "buendia"; }
 
  public static void main(String[] args) throws Exception {
    int port = 7999;
    ServerSocket s = new ServerSocket(port);
    Socket client = s.accept();
 
    ProtectedServer server = new ProtectedServer();
    if (server.authenticate(client.getInputStream()))
      System.out.println("Client logged in.");
    else
      System.out.println("Client failed to log in.");
 
    s.close();
  }
}

To test the protected password login, first start up the server:

C:\ java ProtectedServer

Then run the client, pointing it to the machine where the server is running. I run both these programs on the same machine, so I type this in a different command-line window:

C:\ java ProtectedClient localhost

The server will print out a message indicating whether the client logged in. Then both programs exit.

Double-Strength Password Login

There is a stronger method for protecting password information using message digests. It involves an additional timestamp and random number, as shown in Figure 6-2.

 
Figure 6-2. A doubly protected password

 

First, a digest is computed, just as in the previous example. Then, the digest value, another random number, and another timestamp are fed into a second digest. Then the server is sent the second digest value, along with the timestamps and random numbers.

Why is this better than the simpler scheme we outlined earlier? To understand why, think about how you might try to break the protected password scheme. Recall that a message digest is a one-way function; ideally, this means that it's impossible to figure out what input produced a given digest value.[2] Thus, your best bet is to launch a dictionary attack. This means that you try passwords, one at a time, running them through the simple protection algorithm just described and attempting to log in each time. In this process, it's important to consider how much time it takes to test a single password. In the double-strength protection scheme, two digest values must be computed instead of just one, which should double the time required for a dictionary attack.

We can implement the double protection scheme with a few minimal changes to the ProtectedClient and ProtectedServer classes. First, ProtectedClient's sendAuthentication() method needs some additional logic. The new lines are shown in bold.

  public void sendAuthentication(String user, String password,
      OutputStream outStream) throws IOException, NoSuchAlgorithmException {
    DataOutputStream out = new DataOutputStream(outStream);
    long t1 = (new Date()).getTime();
    double q1 = Math.random();
    byte[] protected1 = Protection.makeDigest(user, password, t1, q1);
    long t2 = (new Date()).getTime();
    double q2 = Math.random();
    byte[] protected2 = Protection.makeDigest(protected1, t2, q2);
 
    out.writeUTF(user);
    out.writeLong(t1);
    out.writeDouble(q1);
    out.writeLong(t2);
    out.writeDouble(q2);
    out.writeInt(protected2.length);
    out.write(protected2);
    out.flush();
  }

You probably noticed that there's a new helper method in the Protection class. It takes a message digest value (an array of bytes), a timestamp, and a random number and generates a new digest value. This new static method in the Protection class is shown next:

  public static byte[] makeDigest(byte[] mush, long t2, double q2)
      throws NoSuchAlgorithmException {
    MessageDigest md = MessageDigest.getInstance("SHA");
    md.update(mush);
    md.update(makeBytes(t2, q2));
    return md.digest();
  }

Finally, the server needs to be updated to accept the additional protection information. ProtectedServer's modified authenticate() method is shown here, with the new lines indicated in bold:

  public boolean authenticate(InputStream inStream)
      throws IOException, NoSuchAlgorithmException {
    DataInputStream in = new DataInputStream(inStream);
 
    String user = in.readUTF();
    long t1 = in.readLong();
    double q1 = in.readDouble();
    long t2 = in.readLong();
    double q2 = in.readDouble();
    int length = in.readInt();
    byte[] protected2 = new byte[length];
    in.readFully(protected2);
 
    String password = lookupPassword(user);
    byte[] local1 = Protection.makeDigest(user, password, t1, q1);
    byte[] local2 = Protection.makeDigest(local1, t2, q2);
    return MessageDigest.isEqual(protected2, local2);
  }

Neither the regular or double-strength login methods described here prevent a dictionary attack on the password. For a method that does prevent a dictionary attack, see http://srp.stanford.edu/srp/.

 
MACs

A message authentication code (MAC) is basically a keyed message digest. Like a message digest, a MAC takes an arbitrary amount of input data and creates a short digest value. Unlike a message digest, a MAC uses a key to create the digest value. This makes it useful for protecting the integrity of data that is sent over an insecure network. The javax.crypto.Mac class encapsulates a MAC.

Setting Up

To create a Mac, use one of its getInstance() methods:

public static final Mac getInstance(String algorithm) throws NoSuchAlgorithmException
This method returns a new Mac for the given algorithm.
public static final Mac getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException
This method returns a new Mac for the given algorithm using the supplied provider.

Once you have obtained the Mac, you need to initialize it with a key. You can also use algorithm-specific initialization information, if you wish.

public final void init(Key key) throws InvalidKeyException
Use this method to initialize the Macwith the supplied key. An exception is thrown if the key cannot be used.
public final void init(Key key, AlgorithmParameterSpec params) throws InvalidKeyException, InvalidAlgorithmParameterException
This method initializes the Mac with the supplied key and algorithm-specific parameters.

Feeding

A Mac has several update() methods for adding data. These are just like the update() methods in MessageDigest:

public final void update(byte input) throws IllegalStateException
This method adds the given byte to the Mac's input data. If the Mac has not been initialized, an exception is thrown.
public final void update(byte[] input) throws IllegalStateException
Use this method to add the entire input array to the Mac.
public final void update(byte[] input, int offset, int len) throws IllegalStateException
This method adds len bytes of the given array, starting at offset, to the Mac.

Calculating the Code

To actually calculate the MAC value, use one of the doFinal() methods:

public final byte[] doFinal() throws IllegalStateException
This method returns the MAC value and resets the state of the Mac. You can calculate a fresh MAC value using the same key by calling update() with new data.
public final void doFinal(byte[] output, int outOffset) throws IllegalStateException, ShortBufferException
This method places the MAC value into the given array, starting at outOffset, and resets the state of the Mac.
public final byte[] doFinal(byte[] input) throws IllegalStateException
This method adds the entire input array to this Mac. Then the MAC value is calculated and returned. The internal state of the Mac is reset.

To clear the results of previous calls to update() without calculating the MAC value, use the reset() method:

public final void reset()
This method clears the internal state of the Mac. If you wish to use a different key to calculate a MAC value, you can reinitialize the Mac using init().

For Instance

The following example shows how to create a MAC key and calculate a MAC value:

SecureRandom sr = new SecureRandom();
byte[] keyBytes = new byte[20];
sr.nextBytes(keyBytes);
SecretKey key = new SecretKeySpec(keyBytes, "HmacSHA1");
Mac m = Mac.getInstance("HmacSHA1");
m.init(key);
m.update(inputData);
byte[] mac = m.doFinal();

 
Signatures

A signature provides two security services, authentication and integrity. A signature gives you assurance that a message has not been tampered with and that it originated from a certain person. As you'll recall from Chapter 2, a signature is a message digest that is encrypted with the signer's private key. Only the signer's public key can decrypt the signature, which provides authentication. If the message digest of the message matches the decrypted message digest from the signature, then integrity is also assured.

Signatures do not provide confidentiality. A signature accompanies a plaintext message. Anyone can intercept and read the message. Signatures are useful for distributing software and documentation because they foil forgery.

The Java Security API provides a class, java.security.Signature, that represents cryptographic signatures. This class operates in two distinct modes, depending on whether you wish to generate a signature or verify a signature.

Like the other cryptography classes, Signature has two factory methods:

public static Signature getInstance(String algorithm) throws NoSuchAlgorithmException
This method returns a Signature for the given algorithm. The first provider supporting the given algorithm is used.
public static Signature getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException
This method returns a Signature for the given algorithm, using the given provider.

One of two methods initializes the Signature:

public final void initSign(PrivateKey privateKey) throws InvalidKeyException
If you want to generate a signature, use this method to initialize the Signature with the given private key.
public final void initVerify(PublicKey publicKey) throws InvalidKeyException
To verify a signature, call this method with the public key that matches the private key that was used to generate the signature.

If you want to set algorithm-specific parameters in the Signature object, you can pass an AlgorithmParameterSpec to setParameter().

public final void setParameter(AlgorithmParameterSpec params) throws InvalidAlgorithmPararmeterException
You can pass algorithm-specific parameters to a Signature using this object. If the Signature does not recognize the AlgorithmParameterSpec object, an exception is thrown.

You can add data to a Signature the same way as for a message digest, using the update() methods. A SignatureException is thrown if the Signature has not been initialized.

public final void update(byte input) throws SignatureException
You can add a single byte to the Signature's input data using this method.
public final void update(byte[] input) throws SignatureException
This method adds the given array of bytes to the Signature's input data.
public final void update(byte[] input, int offset, int len) throws SignatureException
This method adds len bytes from the given array, starting at offset, to the Signature's input data.

Generating a Signature

Generating a signature is a lot like generating a message digest value. The sign() method returns the signature itself:

public final byte[] sign() throws SignatureException
This method calculates a signature, based on the input data as supplied in calls to update(). A SignatureException is thrown if the Signature is not properly initialized.

To generate a signature, you will need the signer's private key and the message that you wish to sign. The procedure is straightforward:

  1. Obtain a Signature object using the getInstance() factory method. You'll need to specify an algorithm. A signature actually uses two algorithms--one to calculate a message digest and one to encrypt the message digest. The SUN provider shipped with the JDK 1.1 supports DSA encryption of an SHA-1 message digest. This is simply referred to as DSA.
  2. Initialize the Signature with the signer's private key using initSign().
  3. Use the update() method to add the data of the message into the signature. You can call update() as many times as you would like. Three different overloads allow you to update the signature with byte data.
  4. Calculate the signature using the sign() method. This method returns an array of bytes that are the signature itself. It's up to you to store the signature somewhere.

Verifying a Signature

You can use Signature's verify() method to verify a signature:

public final boolean verify(byte[] signature) throws SignatureException
This method verifies that the supplied byte array, signature, matches the input data that has been supplied using update(). If the signature verifies, true is returned. If the Signature is not properly initialized, a SignatureException is thrown.

Verifying a signature is similar to generating a signature. In fact, Steps 1 and 3 are identical. It's assumed that you already have a signature value. The process here verifies that the message you've received produces the same signature:

  1. Obtain a Signature using the getInstance() factory method.
  2. Initialize the Signature with the signer's public key using initVerify().
  3. Use update() to add message data into the signature.
  4. Check if your signatures match using the verify() method. This method accepts an array of bytes that are the signature to be verified. It returns a boolean value that is true if the signatures match and false otherwise.

Hancock

Let's examine a complete program, called Hancock, that generates and verifies signatures. We'll use a file for the message input, and we'll pull keys out of a KeyStore. (You can manipulate keystores with the keytool utility, described in Chapter 5, Key Management. To run this example, you'll have to have created a keystore with at least one key pair.) Hancock is a command-line utility that accepts parameters as follows:

java Hancock -s|-v keystore storepass alias messagefile signaturefile

The -s option is used for signing. The private key of the given alias is used to create a signature from the data contained in messagefile. The resulting signature is stored in signaturefile. The keystore parameter is the filename of a keystore, and storepass is the password needed to access the keystore.

The -v option tells Hancock to verify a signature. The signature is assumed to be in signaturefile. Hancock verifies that the signature is from the given alias for the data contained in messagefile. Again, keystore is a keystore file, and storepass is used to access the keystore.

Let's begin by checking our command-line arguments:

import java.io.*;
import java.security.*;
 
public class Hancock {
  public static void main(String[] args) throws Exception {
    if (args.length != 6) {
      System.out.println(
          "Usage: Hancock -s|-v keystore storepass alias " +
          "messagefile signaturefile");
      return;
    }
    
    String options = args[0];
    String keystorefile = args[1];
    String storepass = args[2];
    String alias = args[3];
    String messagefile = args[4];
    String signaturefile = args[5];

Our first step, as you'll recall, is the same for signing and verifying: We need to get a Signature object. We use DSA because it's supplied with the Sun provider:

    Signature signature = Signature.getInstance("DSA");

Next, the Signature needs to be initialized with either the public key or the private key of the named alias. In either case, we need a reference to the keystore, which we obtain as follows:

    KeyStore keystore = KeyStore.getInstance();
    keystore.load(new FileInputStream(keystorefile), storepass);

To sign, we initialize the Signature with a private key. The password for the private key is assumed to be the same as the keystore password. To verify, we initialize the Signature with a public key.

    if (options.indexOf("s") != -1)
      signature.initSign(keystore.getPrivateKey(alias, storepass));
    else
      signature.initVerify(keystore.getCertificate(alias).getPublicKey());

The next step is to update the signature with the given message. This step is the same whether we are signing or verifying. We open the message file and read it in 8K chunks. The signature is updated with every byte read from the message file.

    FileInputStream in = new FileInputStream(messagefile);
    byte[] buffer = new byte[8192];
    int length;
    while ((length = in.read(buffer)) != -1)
      signature.update(buffer, 0, length);
    in.close();

Finally, we're ready to sign the message or verify a signature. If we're signing, we simply generate a signature and store it in a file.

    if (options.indexOf("s") != -1) {
      FileOutputStream out = new FileOutputStream(signaturefile);
      byte[] raw = signature.sign();
      out.write(raw);
      out.close();
    }

Otherwise, we are verifying a signature. All we need to do is read in the signature and check if it verifies. We'll print out a message to the user that tells if the signature verified.

    else {
      FileInputStream sigIn = new FileInputStream(signaturefile);
      byte[] raw = new byte[sigIn.available()];
      sigIn.read(raw);
      sigIn.close();
      if (signature.verify(raw))
        System.out.println("The signature is good.");
      else
        System.out.println("The signature is bad.");
    }
  }
}

You can use Hancock to sign any file with any private key that's in a keystore. A friend who has your public key can use Hancock to verify a file he or she has downloaded from you.

Login, Again

Passwords are a simple solution to authentication, but they are not considered very secure. People choose easy-to-guess passwords, or they write down passwords in obvious places. A sly malcontent, pretending to be a system administrator, can usually convince a user to tell his or her password.

If you want a stronger form of authentication, and you are willing to pay the price in complexity, then you should use a signature-based authentication scheme.

The basic procedure is very similar to the password-based schemes examined earlier, in the section on message digests. The client generates a timestamp and a random number. This time, the client creates a signature of this data and sends it to the server. The server can verify the signature with the client's public key.

How does the client access your private key, to generate a signature? In a real application, you would probably point the client to a disk file that contained your key (preferably on removable media, like a floppy disk or a smart card). In this example, we'll just pull a private key out of a keystore.

The hard part is in creating and maintaining the public key database. The server needs to have a public key for every possible person who will log in. Furthermore, the server needs to obtain these public keys in a secure way. Certificates solve this problem; I'll discuss them a bit later.

We'll look at the simple case, with a pair of programs called StrongClient and StrongServer. StrongClient creates a timestamp and a random number and sends them along with the user's name and a signature to the server. The length of the signature is sent before the signature itself, just as it was with the message digest login examples.

The main() method attempts to use a private key extracted from a keystore. The keystore location, password, alias, and private key password are all command-line parameters. For this to work, you'll need to have created a pair of DSA keys in a keystore somewhere. See Chapter 5 if you're not sure how to do this.

import java.io.*;
import java.net.*;
import java.security.*;
import java.util.Date;
 
import Protection;
 
public class StrongClient {
  public void sendAuthentication(String user, PrivateKey key,
      OutputStream outStream) throws IOException, NoSuchAlgorithmException,
      InvalidKeyException, SignatureException {
    DataOutputStream out = new DataOutputStream(outStream);
    long t = (new Date()).getTime();
    double q = Math.random();
 
    Signature s = Signature.getInstance("DSA");
    s.initSign(key);
    s.update(Protection.makeBytes(t, q));
    byte[] signature = s.sign();
 
    out.writeUTF(user);
    out.writeLong(t);
    out.writeDouble(q);
    out.writeInt(signature.length);
    out.write(signature);
    out.flush();
  }
 
  public static void main(String[] args) throws Exception {
    if (args.length != 5) {
      System.out.println(
          "Usage: StrongClient host keystore storepass alias keypass");
      return;
    }
    
    String host = args[0];
    String keystorefile = args[1];
    String storepass = args[2];
    String alias = args[3];
    String keypass = args[4];
    
    int port = 7999;
    Socket s = new Socket(host, port);
 
    StrongClient client = new StrongClient();
    KeyStore keystore = KeyStore.getInstance();
    keystore.load(new FileInputStream(keystorefile), storepass);
    PrivateKey key = keystore.getPrivateKey(alias, keypass);

client.sendAuthentication(alias, key, s.getOutputStream());

 
    s.close();
  }
}

The server version of this program simply reads the information from the stream and verifies the given signature, using a public key from the keystore named in the command line. Note that the client sends the alias name to the server. This implies that the correct keys must be referenced by the same alias in both the keystore that the client uses and the keystore that the server uses.

import java.io.*;
import java.net.*;
import java.security.*;
 
import Protection;
 
public class StrongServer {
  protected KeyStore mKeyStore;
  
  public StrongServer(KeyStore keystore) { mKeyStore = keystore; }
  
  public boolean authenticate(InputStream inStream)
      throws IOException, NoSuchAlgorithmException,
      InvalidKeyException, SignatureException {
    DataInputStream in = new DataInputStream(inStream);
 
    String user = in.readUTF();
    long t = in.readLong();
    double q = in.readDouble();
    int length = in.readInt();
    byte[] signature = new byte[length];
    in.readFully(signature);
 
    Signature s = Signature.getInstance("DSA");
    s.initVerify(mKeyStore.getCertificate(user).getPublicKey());
    s.update(Protection.makeBytes(t, q));
    return s.verify(signature);
  }
 
  public static void main(String[] args) throws Exception {
    if (args.length != 2) {
      System.out.println("Usage: StrongServer keystore storepass");
      return;
    }
    
    String keystorefile = args[0];
    String storepass = args[1];
    
    int port = 7999;
    ServerSocket s = new ServerSocket(port);
    Socket client = s.accept();
 
    KeyStore keystore = KeyStore.getInstance();
    keystore.load(new FileInputStream(keystorefile), storepass);
    StrongServer server = new StrongServer(keystore);
    if (server.authenticate(client.getInputStream()))
      System.out.println("Client logged in.");
    else
      System.out.println("Client failed to log in.");
 
    s.close();
  }
}

Run the server by pointing it to the keystore you wish to use, as follows:

C:\ java StrongServer c:\windows\.keystore buendia

Then run the client, telling it the server's IP address, the keystore location, the alias, and the private key password. Because I'm running the server and client on the same machine, I use localhost for the server's address:

C:\ java StrongClient localhost c:\windows\.keystore buendia Jonathan 
	buendia

The server prints a message indicating if the client logged in. Then the server and client exit.

SignedObject

JDK 1.2 offers a utility class, java.security.SignedObject, that contains any Serializable object and a matching signature. You can construct a SignedObject with a Serializable object, a private key, and a Signature:

public SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException
This constructor creates a SignedObject that encapsulates the given Serializable object. The object is serialized and stored internally. The serialized object is signed using the supplied Signature and private key.

You can verify the signature on a SignedObject with the verify() method:

public final boolean verify(PublicKey verificationKey, Signature verificationEngine) throws InvalidKeyException, SignatureException
This method verifies that the SignedObject's internal signature matches its contained object. It uses the supplied public key and Signature to perform the verification. As before, the Signature does not need to be initialized. This method returns true if the SignedObject's signature matches its contained object; that is, the contained object's integrity is verified.

You can retrieve the SignedObject's contained object using the getObject() method:

public Object getObject() throws IOException, ClassNotFoundException
This method returns the object contained in this SignedObject. The object is stored internally as a byte array; this method deserializes the object and returns it. To be assured of the object's integrity, you should call verify() before calling this method.

One possible application of SignedObject is in the last example. We might write a simple class, AuthorizationToken, that contained the user's name, the timestamp, and the random value. This object, in turn, could be placed inside a SignedObject that could be passed from client to server.

 
Certificates

To verify a signature, you need the signer's public key. So how are public keys distributed securely? You could simply download the key from a server somewhere, but how would you know you got the right file and not a forgery? Even if you get a valid key, how do you know that it belongs to a particular person?

Certificates answer these questions. A certificate is a statement, signed by one person, that the public key of another person has a particular value. In some ways, it's like a driver's license. The license is a document issued by your state government that matches your face to your name, address, and date of birth. When you buy alcohol, tobacco, or dirty magazines, you can use your license to prove your identity (and your age).

Note that the license only has value because you and your local shopkeepers trust the authority of the state government. Digital certificates have the same property: You need to trust the person who issued the certificate (who is known as a Certificate Authority, or CA).

In cryptographic terminology, a certificate associates an identity with a public key. The identity is called the subject. The identity that signs the certificate is the signer. The certificate contains information about the subject and the subject's public key, plus information about the signer. The whole thing is cryptographically signed, and the signature becomes part of the certificate, too. Because the certificate is signed, it can be freely distributed over insecure channels.

At a basic level, a certificate contains these elements:

Sun recognized that certificate support was anemic in JDK 1.1. Things are improved in JDK 1.2. You can now import X.509v3 certificates and verify them. You still can't generate a certificate using the public API.

In this section, I'll talk about the JDK 1.2 classes that represent certificates and Certificate Revocation Lists (CRLs).

java.security.cert.Certificate

JDK 1.1 introduced support for certificates, based around the java.security.Certificate interface. In JDK 1.2, this interface is deprecated; we won't be covering it. It is replaced by an abstract class, java.security.cert.Certificate. This class is a little simpler than its predecessor, and it includes the ability to verify a certificate. Support for X.509 certificates is provided in a separate class, which I'll explain in a moment.

First, of course, java.security.cert.Certificate is a container for a public key:

public abstract PublicKey getPublicKey()
This method returns the public key that is contained by this certificate.

You can get an encoded version of the certificate using getEncoded(). The data returned by this method could be written to a file:

public abstract byte[] getEncoded() throws CertificateEncodingException
This method returns an encoded representation of the certificate.

Generating a Certificate

Oddly enough, there is still no programmatic way to generate a certificate from scratch, even with the new classes in JDK 1.2. You can, however, load an X.509 certificate from a file using the getInstance() method in the X509Certificate class. I'll talk about this later.

NOTE:

Working with certificates in JDK 1.2 is sometimes difficult because there are two things named Certificate. The java.security.Certificate interface was introduced in JDK 1.1, but it's now deprecated. The "official" certificate class in JDK 1.2 is java.security.cert.Certificate. Whenever you see Certificate in source code, make sure you understand what it refers to. And be careful if you import both java.security.* and java.security.cert.*.

Verifying a Certificate

To verify the contents of the certificate, use one of the verify() methods:

public abstract void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
This method uses the supplied public key to verify the certificate's contents. The public key should belong to the certificate's issuer (and has nothing to do with the public key contained in this certificate). The supplied issuer's public key is used to verify the internal signature that protects the integrity of the certificate's data.
public abstract void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException
This is the same as the previous method, but specifically uses the given provider to supply the signing algorithm implementation.

X.509

Several standards specify the contents of a certificate. One of the most popular is X.509, published by the International Telecommunications Union (ITU). Three versions of this standard have been published. Table 6-1 shows the contents of an X.509 certificate.

Support for X.509 certificates is provided by a subclass of Certificate, java.security.cert.X509Certificate. This class is also abstract although it defines a getInstance() method that returns a concrete subclass. Most of the methods in this class return the fields of an X.509 certificate: getVersion(), getSerialNumber(), getIssuerDN(), and so on. Table 6-1 shows the X509Certificate methods corresponding to the certificate fields.

Table 6-1: X.509 Certificate Contents

Field

Description

Method

Version

X.509 v1, v2, or v3

int getVersion()

Serial number

A number unique to the issuer

BigInteger getSerialNumber()

Signature algorithm

Describes the cryptographic algorithm used for the signature

String getSigAlgName()

Issuer

The issuer's name

Principal getIssuerDN()

Validity period

A range of time when the certificate is valid

Date getNotBefore(), Date getNotAfter()

Subject

The subject's name

Principal getSubjectDN()

Subject's public key

The subject's public key

PublicKey getPublicKey() (inherited from Certificate)

Issuer's unique identifier

A unique identifier representing the issuer (versions 2 and 3)

boolean[] getIssuerUniqueID()

Subject's unique identifier

A unique identifier representing the subject (versions 2 and 3)

boolean[] getSubjectUniqueID()

Extensions

Additional data (version 3)

boolean[] getKeyUsage(), int getBasicConstraints()

Signature

A signature of all of the previous fields

byte[] getSignature()

To load an X.509 certificate from a file, you can use getInstance():

public static final X509Certificate getInstance(InputStream inStream) throws CertificateException
This method instantiates a concrete subclass of X509Certificate and initializes it with the given input stream.
public static final X509Certificate getInstance(byte[] certData) throws CertificateException
This method works as above, except the new certificate is initialized using the supplied byte array.

The way that getInstance() works is a little convoluted. The actual object that is created is determined by an entry in the java.security properties file. This file is found in the lib/security directory underneath the JDK installation directory. By default, the relevant line looks like this:

cert.provider.x509=sun.security.x509.X509CertImpl

Let's say you call getInstance() with an input stream. A sun.security .x509.X509CertImpl will be created, using a constructor that accepts the input stream. It's up to the X509CertImpl to read data from the input stream to initialize itself. X509CertImpl knows how to construct itself from a DER-encoded certificate. What is DER? In the X.509 standard, a certificate is specified as a data structure using the ASN.1 (Abstract Syntax Notation) language. There are a few different ways that ASN.1 data structures can be reduced to a byte stream, and DER (Distinguished Encoding Rules) is one of these methods. The net result is that an X509CertImpl can recognize an X.509 certificate if it is DER-encoded.

Spill

Let's look at an example that uses X509Certificate. We'll write a tool that displays information about a certificate contained in a file, just like keytool -printcert. Like keytool, we'll recognize certificate files in the format described by RFC 1421. An RFC 1421 certificate representation is simply a DER representation, converted to base64, with a header and a footer line. Here is such a file:

-----BEGIN CERTIFICATE-----
MIICMTCCAZoCAS0wDQYJKoZIhvcNAQEEBQAwXDELMAkGA1UEBhMCQ1oxETAPBgNV
BAoTCFBWVCBhLnMuMRAwDgYDVQQDEwdDQS1QVlQxMSgwJgYJKoZIhvcNAQkBFhlj
YS1vcGVyQHA3MHgwMy5icm4ucHZ0LmN6MB4XDTk3MDgwNDA1MDQ1NloXDTk4MDIw
MzA1MDQ1NlowgakxCzAJBgNVBAYTAkNaMQowCAYDVQQIEwEyMRkwFwYDVQQHExBD
ZXNrZSBCdWRlam92aWNlMREwDwYDVQQKEwhQVlQsYS5zLjEMMAoGA1UECxMDVkNV
MRcwFQYDVQQDEw5MaWJvciBEb3N0YWxlazEfMB0GCSqGSIb3DQEJARYQZG9zdGFs
ZWtAcHZ0Lm5ldDEYMBYGA1UEDBMPKzQyIDM4IDc3NDcgMzYxMFwwDQYJKoZIhvcN
AQEBBQADSwAwSAJBAORQnnnaTGhwrWBGK+qdvIGiBGyaPNZfnqXlbtXuSUqRHXhE
acIYDtMVfK4wdROe6lmdlr3DuMc747/oT7SjO2UCAwEAATANBgkqhkiG9w0BAQQF
AAOBgQBxfebIQCCxnVtyY/YVfsAct1dbmxrBkeb9Z+xN7i/Fc3XYLig8rag3cfWg
wDqbnt8LKzvFt+FzlrO1qIm7miYlWNq26rlY3KGpWPNoWGJTkyrqX80/WAhU5B9l
QOqgL9zXHhE65Qq0Wu/3ryRgyBgebSiFem10RZVavBHjgVcejw==
-----END CERTIFICATE-----

Our class performs three tasks:

  1. We need to read the file, strip off the header and footer, and convert the body from a base64 string to a byte array. The oreilly.jonathan.util.Base64 class is used to perform the base64 conversion. This class is presented in Appendix B, Base64.
  2. We'll use this byte array (a DER-encoded certificate) to create a new X509Certificate. We can then print out some basic information about the certificate.
  3. Finally, we'll calculate certificate fingerprints and print them.

Spill begins by checking its command-line arguments:

import java.io.*;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.cert.X509Certificate;
 
import oreilly.jonathan.util.Base64;
 
public class Spill {
  public static void main(String[] args) throws Exception {
    if (args.length != 1) {
      System.out.println("Usage: Spill file");

return;

    }

Next, Spill creates a BufferedReader for reading lines of text from the file. If the first line doesn't contain the certificate header, an exception is thrown. Otherwise, subsequent lines are read and accumulated as one large base64 string. We stop reading lines when we encounter the footer line. This done, we convert the base64 string to a byte array:

    BufferedReader in = new BufferedReader(new FileReader(args[0]));
    String begin = in.readLine();
    if (begin.equals("-----BEGIN CERTIFICATE-----") == false)
      throw new IOException("Couldn't find certificate beginning");
    String base64 = new String();
    boolean trucking = true;
    while (trucking) {
      String line = in.readLine();
      if (line.startsWith("-----")) trucking = false;
      else base64 += line;
    }
    in.close();
    byte[] certificateData = Base64.decode(base64);

We now have the raw certificate data and can create a new certificate using getInstance() in the X509Certificate class:

    X509Certificate c = X509Certificate.getInstance(certificateData);

Having obtained an X509Certificate, Spill prints out various bits of information about it.

    System.out.println("Subject: " + c.getSubjectDN().getName());
    System.out.println("Issuer : " + c.getIssuerDN().getName());
    System.out.println("Serial number: " +
        c.getSerialNumber().toString(16));
    System.out.println("Valid from " + c.getNotBefore() +
        " to " + c.getNotAfter());

We also want to print out the certificate's fingerprints. It's a little tricky to format the fingerprints correctly, so a helper method, doFingerprint(), is used:

    System.out.println("Fingerprints:");
    doFingerprint(certificateData, "MD5");
    doFingerprint(certificateData, "SHA");
  }

The doFingerprint() method calculates a fingerprint (message digest value) and prints it out. First, it obtains a message digest for the requested algorithm and calculates the digest value:

protected static void doFingerprint(byte[] certificateBytes,

      String algorithm) throws Exception {
    System.out.print("  " + algorithm + ": ");
    MessageDigest md = MessageDigest.getInstance(algorithm);
    md.update(certificateBytes);
    byte[] digest = md.digest();

Now doFingerprint() will print out the digest value as a series of two-digit hexadecimal numbers. We loop through the digest value. Each byte is converted to a two-digit hex string. Colons separate the hex values.

    for (int i = 0; i < digest.length; i++) {
      if (i != 0) System.out.print(":");
      int b = digest[i] & 0xff;
      String hex = Integer.toHexString(b);
      if (hex.length() == 1) System.out.print("0");
      System.out.print(hex);
    }
    System.out.println();
  }
}

Let's take it for a test drive. Let's say you have a certificate in a file named ca1.x509. You would run Spill as follows:

C:\ java Spill ca1.x509
Subject: T="+42 38 7747 361", OID.1.2.840.113549.1.9.1=dostalek@pvt.net, CN=Libor Dostalek, OU=VCU, O="PVT,a.s.", L=Ceske Budejovice, S=2, C=CZ
Issuer : OID.1.2.840.113549.1.9.1=ca-oper@p70x03.brn.pvt.cz, CN=CA-PVT1, O=PVT a.s., C=CZ
Serial number: 2d
Valid from Mon Aug 04 01:04:56 EDT 1997 to Tue Feb 03 00:04:56 EST 1998
Fingerprints:
  MD5: d9:6f:56:3e:e0:ec:35:70:94:bb:df:05:75:d6:32:0e
  SHA: db:be:df:e5:ff:ec:f9:53:98:dc:88:dd:6b:ba:cf:2e:2a:68:0c:44

If you run keytool -printcert on the same file, you'll see the same information:

C:\ keytool -printcert -file ca1.x509
Owner: T="+42 38 7747 361", OID.1.2.840.113549.1.9.1=dostalek@pvt.net, CN=Libor Dostalek, OU=VCU, O="PVT,a.s.", L=Ceske Budejovice, S=2, C=CZ
Issuer: OID.1.2.840.113549.1.9.1=ca-oper@p70x03.brn.pvt.cz, CN=CA-PVT1, O=PVT a.s., C=CZ
Serial Number: 2d
Valid from: Mon Aug 04 01:04:56 EDT 1997 until: Tue Feb 03 00:04:56 EST 1998
Certificate Fingerprints:
         MD5:  D9:6F:56:3E:E0:EC:35:70:94:BB:DF:05:75:D6:32:0E
         SHA1: DB:BE:DF:E5:FF:EC:F9:53:98:DC:88:DD:6B:BA:CF:2E:2A:68:0C:44

Certificate Revocation Lists

JDK 1.2 addresses another shortcoming of the JDK 1.1 certificate support: Certificate Revocation Lists (CRLs). CRLs answer the question of what happens to certificates when they're lost or stolen. A CRL is simply a list of certificates that are no longer valid. (Unfortunately, there aren't yet any standarts for how CRLs are issued; presumably they're published in some way by the CAs.) JDK 1.2 provides two classes that support CRLs. First, java.security.cert.X509CRL represents a CRL as specified in the X.509 standard. You can create an X509CRL from a file using getInstance(), just as with X509Certificate:

public static final X509CRL getInstance(InputStream inStream) throws CRLException, X509ExtensionException
This method instantiates a concrete subclass of X509CRL and initializes it with the given input stream.
public static final X509CRL getInstance(byte[] crlData) throws CRLException, X509ExtensionException
This method works like the preceding method except it uses the supplied byte array to initialize the X509CRL.

X509CRL's getInstance() works in much the same way as X509Certificate. The actual subclass of X509CRL that is returned by getInstance() is determined, again, by an entry in the java.security file. The relevant entry for CRLs is this:

crl.provider.x509=sun.security.x509.X509CRLImpl

X509CRL is similar to X509Certificate in many ways. It includes getEncoded() and verify() methods that accomplish the same thing as in X509Certificate. It also includes methods that return information about the CRL itself, like getIssuerDN() and getSigAlgName().

To find out if a particular certificate has been revoked, you can use the isRevoked() method:

public abstract boolean isRevoked(BigInteger serialNumber)
This method returns true if the certificate matching the given serial number has been revoked. Serial numbers are unique to a Certificate Authority (CA). Each CA issues its own CRLs. Thus, this method is used to correlate certificate serial numbers from the same CA.

If you want more information about a revoked certificate, you can use the getRevokedCertificate() and getRevokedCertificates() methods. These return instances of java.security.cert.RevokedCertificate, which can be used to check the revocation date:

public abstract RevokedCertificate getRevokedCertificate(BigInteger serialNumber) throws CRLException
This method returns a RevokedCertificate corresponding to the given serial number.
public abstract Set getRevokedCertificates() throws CRLException
This method returns a collection of all the revoked certificates contained in this X509CRL.

1. These methods are based on the authentication procedures outlined in the X.509 standard, published by the International Telecommunications Union (ITU). Although X.509 is best known for its certificate definition, the document concerns the general problem of authentication. For more information, you can download the document from the ITU at http://www.itu.ch/.

2. In practice, it just takes a very, very long time to figure out what input produced a given digest value.


oreilly.com Home | O'Reilly Bookstores | How to Order | O'Reilly Contacts
International | About O'Reilly | Affiliated Companies | Privacy Policy

© 2001, O'Reilly & Associates, Inc.