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.
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)
-
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)
-
public final void setParameter(AlgorithmParameterSpec param)
-
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)
-
Return the named parameter from the object. The only valid string for
the Sun implementation is KSEED.
-
public final Provider getProvider()
-
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:
-
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.
-
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.
-
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.
-
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
-
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)
-
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()
-
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()
-
Return the signature embedded in the signed object.
-
public String getAlgorithm()
-
Return the name of the algorithm that was used to sign the object.
-
public boolean verify(PublicKey pk, Signature s)
-
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.
 |  |  |
| 11.4. Summary |  | 12.2. Signed Classes |

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