home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Book Home Java Security Search this book

Chapter 12. Digital Signatures

In the previous few chapters, we've examined various aspects of Java's security package with an eye toward the topics of this chapter: the ability to generate and to verify digital signatures. We've now reached the fruits of that examination. In this chapter, we'll explore the mechanisms of the digital signature.

The use and verification of digital signatures is another standard engine that is included in the security provider architecture. Like the other engines we've examined, the classes that implement this engine have both a public interface and an SPI for implementors of the engine.

In the JDK, the most common use of digital signatures is to create signed classes; users have the option of granting additional privileges to these signed classes using the mechanics of the access controller. In addition, a security manager and a class loader can use this information to change the policy of the security manager; this technique is quite useful in 1.1. Hence, we'll also show an example that reads a signed JAR file.

12.1. The Signature Class

Operations on digital signatures are abstracted by the Signature class (java.security.Signature):

public abstract class Signature extends SignatureSpi

Provide an engine to create and verify digital signatures. In Java 1.1, there is no SignatureSpi class, and this class simply extends the Object class.

The Sun security provider includes a single implementation of this class that generates signatures based on the DSA algorithm.

12.1.1. Using the Signature Class

As with all engine classes, instances of the Signature class are obtained by calling one of these methods:

public static Signature getInstance(String algorithm)
public static Signature getInstance(String algorithm, String provider)

Generate a signature object that implements the given algorithm. If no provider is specified, all providers are searched in order for the given algorithm as discussed in Chapter 8, "Security Providers"; otherwise, the system searches for the given algorithm only in the given provider. If an implementation of the given algorithm is not found, a NoSuchAlgorithmException is thrown. If the named security provider cannot be found, a NoSuchProviderException is thrown.

Beginning in 1.2,[1] if the algorithm string is "DSA", the string "SHA/DSA" is substituted for it. Hence, implementors of this class that provide support for DSA signing must register themselves appropriately (that is, with the message digest algorithm name) in the security provider.

[1]1.2 is now Java 2.

Once a signature object is obtained, the following methods can be invoked on it:

public void final initVerify(PublicKey publicKey)

Initialize the signature object, preparing it to verify a signature. A signature object must be initialized before it can be used. If the key is not of the correct type for the algorithm or is otherwise invalid, an InvalidKeyException is thrown.

public final void initSign(PrivateKey privateKey)

Initialize the signature object, preparing it to create a signature. A signature object must be initialized before it can be used. If the key is not of the correct type for the algorithm or is otherwise invalid, an InvalidKeyException is thrown.

public final void update(byte b)
public final void update(byte[] b)
public final void update(byte b[], int offset, int length)

Add the given data to the accumulated data the object will eventually sign or verify. If the object has not been initialized, a SignatureException is thrown.

public final byte[] sign()
public final int sign(byte[] outbuf, int offset, int len) figure

Create the digital signature, assuming that the object has been initialized for signing. If the object has not been properly initialized, a SignatureException is thrown. Once the signature has been generated, the object is reset so that it may generate another signature based on some new data (however, it is still initialized for signing; a new call to the initSign() method is not required).

In the first of these methods, the signature is returned from the method. Otherwise, the signature is stored into the outbuf array at the given offset, and the length of the signature is returned. If the output buffer is too small to hold the data, an IllegalArgumentException will be thrown.

public final boolean verify(byte[] signature)

Test the validity of the given signature, assuming that the object has been initialized for verification. If the object has not been properly initialized, then a SignatureException is thrown. Once the signature has been verified (whether or not the verification succeeds), the object is reset so that it may verify another signature based on some new data (no new call to the initVerify() method is required).

public final String getAlgorithm()

Get the name of the algorithm this object implements.

public String toString()

A printable version of a signature object is composed of the string "Signature object:" followed by the name of the algorithm implemented by the object, followed by the initialized state of the object. The state is either <not initialized>, <initialized for verifying>, or <initialized for signing>. However, the Sun DSA implementation of this class overrides this method to show the parameters of the DSA algorithm instead.

public final void setParameter(String param, Object value) figure
public final void setParameter(AlgorithmParameterSpec param) figure

Set the parameter of the signature engine. In the first format, the named parameter is set to the given value; in the second format, parameters are set based on the information in the param specification.

In the Sun implementation of the DSA signing algorithm, the only valid param string is KSEED, which requires an array of bytes that will be used to seed the random number generator used to generate the k value. There is no way to set this value through the parameter specification, which in the Sun implementation always returns an UnsupportedOperationException.

public final Object getParameter(String param) figure

Return the named parameter from the object. The only valid string for the Sun implementation is KSEED.

public final Provider getProvider() figure

Return the provider that supplied the implementation of this signature object.

It is no accident that this class has many similarities to the MessageDigest class; a digital signature algorithm is typically implemented by performing a cryptographic operation on a private key and the message digest that represents the data to be signed. For the developer, this means that generating a digital signature is virtually the same as generating a message digest; the only difference is that a key must be presented in order to operate on a signature object. This difference is important, however, since it fills in the hole we noticed previously: a message digest can be altered along with the data it represents so that the tampering is unnoticeable. A signed message digest, on the other hand, can't be altered without knowledge of the key that was used to create it. The use of a public key in the digital signature algorithm makes the digital signature more attractive than a message authentication code, in which there must be a shared key between the parties involved in the message exchange.

Let's take our example from Chapter 9, "Message Digests" where we saved a message and its digest to a file; we'll modify it now to save the message and the digital signature. We can create the digital signature like this:

Class Definition

public class Send {
	public static void main(String args[]) {
		String data;
		data = "This have I thought good to deliver thee, " +
				"that thou mightst not lose the dues of rejoicing " +
				"by being ignorant of what greatness is promised thee.";

		try {
			FileOutputStream fos = new FileOutputStream("test");
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			KeyStore ks = 
                      KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(new FileInputStream(
								System.getProperty("user.home") +
								File.separator + ".keystore"), null);
			char c[] = new char[args[1].length()];
			args[1].getChars(0, c.length, c, 0);
			PrivateKey pk = (PrivateKey) ks.getKey(args[0], c);

			Signature s = Signature.getInstance("DSA");
			s.initSign(pk);

			byte buf[] = data.getBytes();
			s.update(buf);
			oos.writeObject(data);
			oos.writeObject(s.sign());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

This example puts together many of the examples from the past few chapters. In order to create the digital signature we must accomplish the following:

  1. Obtain the private key that is used to sign the data. Here we're using the conventional keystore database ($HOME/.keystore) and the command-line arguments to obtain the alias and password of the private key we want to use.

  2. Obtain a signing object via the getInstance() method and initialize it. Since we're creating a signature in this example, we use the initSign() method for initialization.

  3. Pass the data to be signed as a series of bytes to the update() method of the signing object. Multiple calls could be made to the update() method even though in this example we only need one.

  4. Obtain the signature by calling the sign() method. We save the signature bytes and write them to a file with the data so that the data and the signature can be retrieved at a later date.

Reading the data and verifying the signature are similar:

Class Definition

public class Receive {
	public static void main(String args[]) {
		try {
			String data = null;
			byte signature[] = null;
			FileInputStream fis = new FileInputStream("test");
			ObjectInputStream ois = new ObjectInputStream(fis);
			Object o = ois.readObject();
			try {
			    data = (String) o;
			} catch (ClassCastException cce) {
			    System.out.println("Unexpected data in file");
			    System.exit(-1);
			}
			o = ois.readObject();
			try {
			    signature = (byte []) o;
			} catch (ClassCastException cce) {
			    System.out.println("Unexpected data in file");
			    System.exit(-1);
			}
			System.out.println("Received message");
			System.out.println(data);

			KeyStore ks = 
                      KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(new FileInputStream(
						System.getProperty("user.home") +
						File.separator + ".keystore"), args[1]);

			Certificate c = ks.getCertificate(args[0]);
			PublicKey pk = c.getPublicKey();
			Signature s = Signature.getInstance("DSA");
			s.initVerify(pk);
			s.update(data.getBytes());
			if (s.verify(signature)) {
				System.out.println("Message is valid");
			}
			else System.out.println("Message was corrupted");
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}

The process of verifying the signature still requires four steps. The major differences are that in step two, we initialize the signing object for verification by using the initVerify() method, and in step four, we verify (rather than create) the existing signature by using the verify() method. Note that we still have to know who signed the message in order to look up the correct key--but more about that a little later.

12.1.2. The SignedObject Class

In our last example, we had to create an object that held both the data in which we are interested and the signature for that data. This is a common enough requirement that Java provides the SignedObject class (java.security.SignedObject) to encapsulate an object and its signature:

public final class SignedObject implements Serializable figure

Encapsulate an object and its digital signature. The encapsulated object must be serializable so that a serialization of a signed object can do a deep copy of the embedded object.

Signed objects are created with this constructor:

public SignedObject(Serializable o, PrivateKey pk, Signature engine) figure

Create a signed object based on the given object, signing the serialized data in that object with the given private key and signature object. The signed object contains a copy of the given object; this copy is obtained by serializing the object parameter. If this serialization fails, an IOException is thrown.

It's very important to realize that this constructor makes, in effect, a copy of its parameter; if you create a signed object based on a string buffer and later change the contents of the string buffer, the data in the signed object remains unchanged. This preserves the integrity of the object encapsulated with its signature.

Here are the methods we can use to operate on a signed object:

public Object getContent() figure

Return the object embedded in the signed object. The object is reconstituted using object serialization; an error in serialization may cause either an IOException or a ClassNotFoundException to be thrown.

public byte[] getSignature() figure

Return the signature embedded in the signed object.

public String getAlgorithm() figure

Return the name of the algorithm that was used to sign the object.

public boolean verify(PublicKey pk, Signature s) figure

Verify the signature within the embedded object with the given key and signature engine. The signature engine parameter may be obtained by calling the getInstance() method of the Signature class. The underlying signature engine may throw an InvalidKeyException or SignatureException.

We'll use this class in examples later in this chapter.

12.1.3. Signing and Certificates

In the previous examples, we specified on the command line the name of the entity that we assumed generated the signature in the file. This was necessary because the file contained only the actual signature of the entity and the data that was signed; it did not contain any information about who the signer actually is. That's fine for an example, but it is not always appropriate in a real application. We could have asked the user for the name of the entity that was supposed to have signed the data, but that course is fraught with potential errors:

  • The user could have no idea what names are in the keystore of the application. Especially in a corporate environment, users may not know what data the keystore database might contain.

  • The user could get the name of the keystore alias wrong. Say that the application asks the user to enter the name of the signer; the user, knowing that the data came from me, may enter "sdo" as the alias of the identity.

    What the user may not remember is that when the keystore was first created, she received a public key from the San Diego Oil company; that public key was entered into the keystore with the alias "sdo." When my identity was added to the keystore, a different alias had to be chosen, so my public key was added with the alias "ScottOaks." But that was a long time ago, now forgotten, and because I use the sdo moniker all over my writings, the user assumes that I am the sdo in the keystore. And so the wrong alias will be chosen, and the signature verification will fail when it should have succeeded.

For these reasons, it makes more sense to include the public key with the signature and the signed data. This allows the application to find the identity based on the unique public key in order to determine who the signer of the data is.

We could do that by simply sending the encoded public key with the signature and data. A better solution, however, would be to send the certificate that verifies the public key. That way, if the public key is not found in the database, the credentials of the certificate can be presented to the user, and the user can have the opportunity to decide on the fly if the particular entity should be trusted.

Although an embedding of signature, data, and certificate is very common, the SignedObject class does not include the capability to contain a certificate. So we'll use the SignedObject class in this example, but we'll still need an object that contains the signed object and the certificate. We'd like to do this by extending the SignedObject class, but since that class is final we're forced to adopt this approach:

Class Definition

public class Message implements Serializable {
	SignedObject object;
	transient Certificate certificate;

	private void writeObject(ObjectOutputStream out)
									throws IOException {
		out.defaultWriteObject();
		try {
			out.writeObject(certificate.getEncoded());
		} catch (CertificateEncodingException cee) {
			throw new IOException("Can't serialize object " + cee);
		}
	}

	private void readObject(ObjectInputStream in)
							throws IOException, ClassNotFoundException {
		in.defaultReadObject();
		try {
			byte b[] = (byte []) in.readObject();
			CertificateFactory cf = 
                        CertificateFactory.getInstance("X509");
		certificate = cf.generateCertificate(new 
                             ByteArrayInputStream(b));
		} catch (CertificateException ce) {
			throw new IOException("Can't de-serialize object " + ce);
		}
	}
}

We've made the certificate variable in this class transient and have explicitly serialized and deserialized it using its external encoding. As we discussed in Chapter 10, "Keys and Certificates", whenever we have an embedded certificate or key, we must follow a procedure like this to ensure that the receiving party is able to deserialize the class.

As it turns out, the X509 certificate implementation that comes with the JDK (that is, the sun.security.x509.X509CertImpl class) also overrides the writeObject() and readObject() methods, so if we serialize a certificate explicitly, the encoded data is written to or read from the file. It is not sufficient to rely upon that, however--if we use the default serialization methods for the Message class, a reference to the sun.security.x509.X509CertImpl class is embedded into the serialized stream. A user with another security provider (and hence a different implementation of the X509Certificate class) would not be able to deserialize the stream because there is no access to the Sun implementation of the X509Certificate class. Explicitly serializing and deserializing the certificate as we've done here avoids embedding any reference to the provider class and makes the data file more portable.

When we save the message to the file, we now have to make sure that we save a certificate with it. Other than that, changes to the class are minor:

Class Definition

public class SendObject {
	public static void main(String args[]) {
		try {
			FileOutputStream fos = new FileOutputStream("test.obj");
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			KeyStore ks = 
                     KeyStore.getInstance(KeyStore.getDefaultType());
			char c[] = new char[args[1].length()];
			args[1].getChars(0, c.length, c, 0);
			ks.load(new FileInputStream(
					System.getProperty("user.home") +
					File.separator + ".keystore"), c);

			Certificate certs[] = ks.getCertificateChain(args[0]);
			PrivateKey pk = (PrivateKey) ks.getKey(args[0], c);
			Message m = new Message();
			m.object = new SignedObject(
			  "This have I thought good to deliver thee, " +
			  "that thou mightst not lose the dues of rejoicing " +
			  "by being ignorant of what greatness is promised thee.",
							pk, Signature.getInstance("DSA"));
			m.certificate = certs[0];
			oos.writeObject(m);
		} catch (Exception e) {
			System.out.println(e);
		}
	}
}

Retrieving the data is now more complicated, since we must verify both the signature in the signed object and the identity of the authority that signed the embedded certificate:

Class Definition

public class ReceiveObject {
	private static void verifySigner(Certificate c, String name)
									throws CertificateException {
		Certificate issuerCert = null;
		X509Certificate sCert = null;
		KeyStore ks = null;

		try {
			ks = KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(new FileInputStream(
						System.getProperty("user.home") +
						File.separator + ".keystore"), null);
		} catch (Exception e) {
			throw new CertificateException("Invalid keystore");
		}

		try {
			String signer = ks.getCertificateAlias(c);
			if (signer !=null){
				System.out.println("We know the signer as " + signer);
				return;
			}
			for (Enumeration alias = ks.aliases(); 
                              alias.hasMoreElements();){
				String s = (String) alias.nextElement();
				try {
					sCert = (X509Certificate) ks.getCertificate(s);
				} catch (Exception e) {
					continue;
				}
				if (name.equals(sCert.getSubjectDN().getName())){
					issuerCert = sCert;
					break;
				}
			}
		} catch(KeyStoreException kse) {
			throw new CertificateException("Invalid keystore");
		}
		if (issuerCert == null) {
			throw new CertificateException("No such certificate");
		}
		try {
			c.verify(issuerCert.getPublicKey());
		} catch (Exception e) {
			throw new CertificateException(e.toString());
		}
	}

	private static void processCertificate(X509Certificate x509)
									throws CertificateParsingException {
		Principal p;
		p = x509.getSubjectDN();
		System.out.println("This message was signed by " +
							p.getName());
		p = x509.getIssuerDN();
		System.out.println("This certificate was provided by " +
							p.getName());
		try {
			verifySigner(x509, p.getName());
		} catch (CertificateException ce) {
			System.out.println("We don't know the certificate signer");
		}
		try {
			x509.checkValidity();
		} catch (CertificateExpiredException cee) {
			System.out.println("That certificate is no longer valid");
		} catch (CertificateNotYetValidException cnyve) {
			System.out.println("That certificate is not yet valid");
		}
	}

	public static void main(String args[]) {
		try {
			FileInputStream fis = new FileInputStream("test.obj");
			ObjectInputStream ois = new ObjectInputStream(fis);
			Object o = ois.readObject();
			if (o instanceof Message) {
				Message m = (Message) o;
				System.out.println("Received message");
				processCertificate((X509Certificate) m.certificate);
				PublicKey pk = m.certificate.getPublicKey();
				if (m.object.verify(pk, Signature.getInstance("DSA"))) {
					System.out.println("Message is valid");
					System.out.println(m.object.getObject());
				}
				else System.out.println("Message signature is invalid");
			}
			else System.out.println("Message is corrupted");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

We've seen most of this code in previous chapters; in particular, the processCertificate() method uses the standard certificate methods to extract and print information about the certificate. The new code for us is primarily in the verifySigner() method, where we search the entire keystore for a name that matches the issuer of the certificate that was sent to us. If we find a match, we use the corresponding public key to verify the certificate we received.

This method shows yet another need for an alternate implementation of the KeyStore class--if you have to search the entire list of keys for a matching certificate like this, you clearly don't want to perform a linear search each time. An alternate keystore could provide a more efficient means of searching for certificates.



Library Navigation Links

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