Chapter 12. Digital SignaturesIn 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 ClassOperations on digital signatures are abstracted by the Signature class (java.security.Signature):
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 ClassAs with all engine classes, instances of the Signature class are obtained by calling one of these methods:
Once a signature object is obtained, the following methods can be invoked on it:
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 Definitionpublic 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:
Reading the data and verifying the signature are similar: Class Definitionpublic 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 ClassIn 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:
Signed objects are created with this constructor:
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:
We'll use this class in examples later in this chapter. 12.1.3. Signing and CertificatesIn 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:
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 Definitionpublic 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 Definitionpublic 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 Definitionpublic 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. Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|