|
|
|
|
Java CryptographyBy Jonathan B. Knudsen1st 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:
- At an automated bank machine, you identify yourself using your bank card. You authenticate yourself using a personal identification number (PIN). The PIN is a shared secret, something that both you and the bank know. Presumably, you and the bank are the only ones who know this number.
- When you use a credit card, you identify yourself with the card. You authenticate yourself with your signature. Most store clerks never check the signature; in this situation, possession of the card is authentication enough. This is true when you order something over the telephone, as well; simply knowing the credit card number is proof of your identity.
- When you rent a movie at a video store, you prove your identity with a card or by saying your telephone number.
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:
- Message digests produce a small "fingerprint" of a larger set of data.
- Digital signatures can be used to prove the integrity of data.
- Certificates are used as cryptographically safe containers for public keys.
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 DigestsAs 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.MessageDigestclass encapsulates a cryptographic message digest.Getting
To obtain a
MessageDigestfor a particular algorithm use one of itsgetInstance()factory methods:
- public static MessageDigest getInstance(String algorithm) throws NoSuchAlgorithmException
- This method returns a
MessageDigestfor 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
MessageDigestfor the given algorithm, using the given provider.Feeding
To feed data into the
MessageDigest, use one of theupdate()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
inputarray to the message digest's input data.- public void update(byte[] input, int offset, int len)
- This method adds
lenbytes of the given array, starting atoffset, 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 bydigest().If you use a
MessageDigestto calculate a digest value for one set of input data, you can reuse theMessageDigestfor 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.securitypackage comes with two classes that make it easy to calculate message digests on stream data. These classes areDigestInputStreamandDigestOutputStream, descendants of theFilterInputStreamandFilterOutputStreamclasses injava.io.Let's apply
DigestInputStreamto theMasherclass 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
DigestInputStreamaround theFileInputStream, as follows. As we read data from the file, theMessageDigestwill 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();
DigestOutputStreamworks the same way; all bytes written to the stream are automatically passed to theMessageDigest.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
ProtectedClientis notSocket-specific. You could use it to write authentication information to a file or an email message.Some of the digestion that
ProtectedClientperforms will be mirrored in the server class. Therefore,ProtectedClient'ssendAuthentication()method uses a static utility method,makeDigest(), that is defined in theProtectionclass. 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];}}}
Protectiondefines two static methods. ThemakeDigest()method creates a message digest from its input data. It uses a helper method,makeBytes(), whose purpose is to convert alongand adoubleinto an array of bytes.On the server side, the process is similar. The
ProtectedServerclass 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.");elseSystem.out.println("Client failed to log in.");s.close();}}To test the protected password login, first start up the server:
C:\ java ProtectedServerThen 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 localhostThe 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
ProtectedClientandProtectedServerclasses. First,ProtectedClient'ssendAuthentication()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
Protectionclass. 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 theProtectionclass 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 modifiedauthenticate()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/.
MACsA 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.Macclass encapsulates a MAC.Setting Up
To create a
Mac, use one of itsgetInstance()methods:
- public static final Mac getInstance(String algorithm) throws NoSuchAlgorithmException
- This method returns a new
Macfor the given algorithm.- public static final Mac getInstance(String algorithm, String provider) throws NoSuchAlgorithmException, NoSuchProviderException
- This method returns a new
Macfor 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
Macwith the supplied key and algorithm-specific parameters.Feeding
A
Machas severalupdate()methods for adding data. These are just like theupdate()methods inMessageDigest:
- public final void update(byte input) throws IllegalStateException
- This method adds the given byte to the
Mac's input data. If theMachas not been initialized, an exception is thrown.- public final void update(byte[] input) throws IllegalStateException
- Use this method to add the entire
inputarray to theMac.- public final void update(byte[] input, int offset, int len) throws IllegalStateException
- This method adds
lenbytes of the given array, starting atoffset, to theMac.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 callingupdate()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 theMac.- public final byte[] doFinal(byte[] input) throws IllegalStateException
- This method adds the entire
inputarray to thisMac. Then the MAC value is calculated and returned. The internal state of theMacis reset.To clear the results of previous calls to
update()without calculating the MAC value, use thereset()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 theMacusinginit().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();
SignaturesA 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,
Signaturehas two factory methods:
- public static Signature getInstance(String algorithm) throws NoSuchAlgorithmException
- This method returns a
Signaturefor 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
Signaturefor 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
Signaturewith 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
Signatureobject, you can pass anAlgorithmParameterSpectosetParameter().
- public final void setParameter(AlgorithmParameterSpec params) throws InvalidAlgorithmPararmeterException
- You can pass algorithm-specific parameters to a
Signatureusing this object. If theSignaturedoes not recognize theAlgorithmParameterSpecobject, an exception is thrown.You can add data to a
Signaturethe same way as for a message digest, using theupdate()methods. ASignatureExceptionis thrown if theSignaturehas 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
lenbytes from the given array, starting atoffset, to theSignature'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(). ASignatureExceptionis thrown if theSignatureis 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:
- Obtain a
Signatureobject using thegetInstance()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.- Initialize the
Signaturewith the signer's private key usinginitSign().- Use the
update()method to add the data of the message into the signature. You can callupdate()as many times as you would like. Three different overloads allow you to update the signature with byte data.- 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'sverify()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 usingupdate(). If the signature verifies,trueis returned. If theSignatureis not properly initialized, aSignatureExceptionis 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:
- Obtain a
Signatureusing thegetInstance()factory method.- Initialize the
Signaturewith the signer's public key usinginitVerify().- Use
update()to add message data into the signature.- 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 abooleanvalue that istrueif the signatures match andfalseotherwise.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 aKeyStore. (You can manipulate keystores with thekeytoolutility, described in Chapter 5, Key Management. To run this example, you'll have to have created a keystore with at least one key pair.)Hancockis a command-line utility that accepts parameters as follows:java Hancock -s|-v keystore storepass alias messagefile signaturefileThe
-soption is used for signing. The private key of the givenaliasis 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, andstorepassis the password needed to access the keystore.The
-voption tellsHancockto verify a signature. The signature is assumed to be in signaturefile.Hancockverifies that the signature is from the givenaliasfor the data contained in messagefile. Again, keystore is a keystore file, andstorepassis 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
Signatureobject. We use DSA because it's supplied with the Sun provider:Signature signature = Signature.getInstance("DSA");Next, the
Signatureneeds 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
Signaturewith a private key. The password for the private key is assumed to be the same as the keystore password. To verify, we initialize theSignaturewith a public key.if (options.indexOf("s") != -1)signature.initSign(keystore.getPrivateKey(alias, storepass));elsesignature.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.");elseSystem.out.println("The signature is bad.");}}}You can use
Hancockto sign any file with any private key that's in a keystore. A friend who has your public key can useHancockto 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
StrongClientandStrongServer.StrongClientcreates 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.");elseSystem.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 buendiaThen 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 buendiaThe 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 anySerializableobject and a matching signature. You can construct aSignedObjectwith aSerializableobject, a private key, and aSignature:
- public SignedObject(Serializable object, PrivateKey signingKey, Signature signingEngine) throws IOException, InvalidKeyException, SignatureException
- This constructor creates a
SignedObjectthat encapsulates the givenSerializableobject. The object is serialized and stored internally. The serialized object is signed using the suppliedSignatureand private key.You can verify the signature on a
SignedObjectwith theverify()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 andSignatureto perform the verification. As before, theSignaturedoes not need to be initialized. This method returnstrueif theSignedObject's signature matches its contained object; that is, the contained object's integrity is verified.You can retrieve the
SignedObject's contained object using thegetObject()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 callverify()before calling this method.One possible application of
SignedObjectis 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 aSignedObjectthat could be passed from client to server.
CertificatesTo 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:
- Information about the subject
- The subject's public key
- Information about the issuer
- The issuer's signature of the above information
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.Certificateinterface. 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.Certificateis 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 theX509Certificateclass. I'll talk about this later.NOTE:
Working with certificates in JDK 1.2 is sometimes difficult because there are two things namedCertificate. Thejava.security.Certificateinterface was introduced in JDK 1.1, but it's now deprecated. The "official" certificate class in JDK 1.2 isjava.security.cert.Certificate. Whenever you seeCertificatein source code, make sure you understand what it refers to. And be careful if you import bothjava.security.*andjava.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 agetInstance()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 theX509Certificatemethods 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 fromCertificate)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
X509Certificateand 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.X509CertImplLet's say you call
getInstance()with an input stream. Asun.security .x509.X509CertImplwill be created, using a constructor that accepts the input stream. It's up to theX509CertImplto read data from the input stream to initialize itself.X509CertImplknows 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 anX509CertImplcan 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 likekeytool -printcert. Likekeytool, 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-----MIICMTCCAZoCAS0wDQYJKoZIhvcNAQEEBQAwXDELMAkGA1UEBhMCQ1oxETAPBgNVBAoTCFBWVCBhLnMuMRAwDgYDVQQDEwdDQS1QVlQxMSgwJgYJKoZIhvcNAQkBFhljYS1vcGVyQHA3MHgwMy5icm4ucHZ0LmN6MB4XDTk3MDgwNDA1MDQ1NloXDTk4MDIwMzA1MDQ1NlowgakxCzAJBgNVBAYTAkNaMQowCAYDVQQIEwEyMRkwFwYDVQQHExBDZXNrZSBCdWRlam92aWNlMREwDwYDVQQKEwhQVlQsYS5zLjEMMAoGA1UECxMDVkNVMRcwFQYDVQQDEw5MaWJvciBEb3N0YWxlazEfMB0GCSqGSIb3DQEJARYQZG9zdGFsZWtAcHZ0Lm5ldDEYMBYGA1UEDBMPKzQyIDM4IDc3NDcgMzYxMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAORQnnnaTGhwrWBGK+qdvIGiBGyaPNZfnqXlbtXuSUqRHXhEacIYDtMVfK4wdROe6lmdlr3DuMc747/oT7SjO2UCAwEAATANBgkqhkiG9w0BAQQFAAOBgQBxfebIQCCxnVtyY/YVfsAct1dbmxrBkeb9Z+xN7i/Fc3XYLig8rag3cfWgwDqbnt8LKzvFt+FzlrO1qIm7miYlWNq26rlY3KGpWPNoWGJTkyrqX80/WAhU5B9lQOqgL9zXHhE65Qq0Wu/3ryRgyBgebSiFem10RZVavBHjgVcejw==-----END CERTIFICATE-----Our class performs three tasks:
- 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.Base64class is used to perform the base64 conversion. This class is presented in Appendix B, Base64.- 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.- Finally, we'll calculate certificate fingerprints and print them.
Spillbegins 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,
Spillcreates aBufferedReaderfor 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 theX509Certificateclass:X509Certificate c = X509Certificate.getInstance(certificateData);Having obtained an
X509Certificate,Spillprints 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
Spillas follows:C:\ java Spill ca1.x509Subject: 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=CZIssuer : OID.1.2.840.113549.1.9.1=ca-oper@p70x03.brn.pvt.cz, CN=CA-PVT1, O=PVT a.s., C=CZSerial number: 2dValid from Mon Aug 04 01:04:56 EDT 1997 to Tue Feb 03 00:04:56 EST 1998Fingerprints:MD5: d9:6f:56:3e:e0:ec:35:70:94:bb:df:05:75:d6:32:0eSHA: db:be:df:e5:ff:ec:f9:53:98:dc:88:dd:6b:ba:cf:2e:2a:68:0c:44If you run
keytool -printcerton the same file, you'll see the same information:C:\ keytool -printcert -file ca1.x509Owner: 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=CZIssuer: OID.1.2.840.113549.1.9.1=ca-oper@p70x03.brn.pvt.cz, CN=CA-PVT1, O=PVT a.s., C=CZSerial Number: 2dValid from: Mon Aug 04 01:04:56 EDT 1997 until: Tue Feb 03 00:04:56 EST 1998Certificate Fingerprints:MD5: D9:6F:56:3E:E0:EC:35:70:94:BB:DF:05:75:D6:32:0ESHA1: DB:BE:DF:E5:FF:EC:F9:53:98:DC:88:DD:6B:BA:CF:2E:2A:68:0C:44Certificate 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.X509CRLrepresents a CRL as specified in the X.509 standard. You can create anX509CRLfrom a file usinggetInstance(), just as withX509Certificate:
- public static final X509CRL getInstance(InputStream inStream) throws CRLException, X509ExtensionException
- This method instantiates a concrete subclass of
X509CRLand 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'sgetInstance()works in much the same way asX509Certificate. The actual subclass ofX509CRLthat is returned bygetInstance()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
X509CRLis similar toX509Certificatein many ways. It includesgetEncoded()andverify()methods that accomplish the same thing as inX509Certificate. It also includes methods that return information about the CRL itself, likegetIssuerDN()andgetSigAlgName().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
trueif 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()andgetRevokedCertificates()methods. These return instances ofjava.security.cert.RevokedCertificate, which can be used to check the revocation date:
- public abstract RevokedCertificate getRevokedCertificate(BigInteger serialNumber) throws CRLException
- This method returns a
RevokedCertificatecorresponding 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.
© 2001, O'Reilly & Associates, Inc.