In Chapter 5,"An Introduction to Cryptography," and Chapter 6,"Key Management and Digital Certificates," you were introduced to message digests and digital signatures. These are important elements of Java security because they provide the basic mechanism for trusted code distribution. In this chapter, you'll take a closer look at message digests and digital signatures, study their classes and interfaces, and gain practical experience in creating digests and signatures. You'll also cover message authentication codes (MACs) and learn how they can be used to protect the integrity of messages that are sent over insecure communication channels.
Support for message digests is provided by the following five classes of the java.security
package:
MessageDigestSpi
—The MessageDigestSpi
class is an abstract
class that offers a service provider interface for the development of MessageDigest
classes. It defines abstract
methods that must be implemented by a cryptographic service provider to supply the implementation of a particular message digest algorithm.
MessageDigest
—The MessageDigest
class is a subclass of MessageDigestSpi
that provides a basic implementation of a message digest. It supports the MD5 and SHA message digest algorithms.
DigestInputStream
—The DigestInputStream
class, a subclass of java.io.FilterInputStream
, is used to compute a message digest from the data that is read from a stream.
DigestOutputStream
—The DigestOutputStream
class, a subclass of java.io.FilterOutputStream
, is used to compute a message digest from the data that is written to a stream.
DigestException
—The DigestException
class, a subclass of GeneralSecurityException
, is thrown as the result of errors in message digest calculation.
These classes are described in the following sections.
MessageDigestSpi
is a service provider interface (SPI) class that defines abstract
methods that are to be supplied by MessageDigest
implementations. These methods include engineDigest()
, engineGetDigestLength()
, engineReset()
, and engineUpdate().
You don't need to learn these methods unless you're going to build your own custom MessageDigest
implementation. However, I'll summarize them here:
protected abstract void engineUpdate(byte input)
—Updates the digest with the value of a byte.
protected abstract void engineUpdate(byte[] input, int
offset
, int
len
)
—Updates the digest with the array of bytes, beginning at offset
and continuing for len
bytes.
protected abstract byte[] engineDigest()
—Completes the message digest computation and returns the message digest value as a byte
array.
protected int engineDigest(byte[]
buf
, int
offset
, int
len
)
—Completes the message digest computation and returns the number of bytes in the digest. The digest is stored in buf
as specified by offset
and len
.
protected int engineGetDigestLength()
—Returns the message digest length in bytes.
protected abstract void engineReset()
—Resets the digest to its initial starting point.
Object clone()
—Provides support for implementing the Cloneable
interface. Returns a clone if the MessageDigest
implementation implements Cloneable.
If you are interested in creating your own MessageDigest
implementation, check the Java 2 API documentation of these functions.
The MessageDigest
class provides a concrete (nonabstract) subclass of MessageDigestSpi
that serves as the foundation for message digest computation. It supports the MD5 and SHA message digest algorithms.
The MessageDigest
constructor is protected. MessageDigest
objects are created via the static
getInstance()
method. A String
identifying the type of message digest algorithm to be used is supplied as an argument:
MessageDigest md = MessageDigest.getInstance("MD5");
The methods of
MessageDigest
are as follows:
static MessageDigest getInstance(String
algorithm
)
—Creates a MessageDigest
object that implements the specified digest algorithm.
static MessageDigest getInstance(String
algorithm
, String
provider
)—
Creates a MessageDigest
object that implements the specified algorithm from the specified provider (if available).
String getAlgorithm()
—Returns a String
that identifies the algorithm used by the MessageDigest
object.
void update(byte
input
)
—Updates the digest computation using the specified byte.
void update(byte[]
input
)
—Updates the digest computation using the specified array of bytes.
void update(byte[]
input
, int
offset
, int
len
)
—Updates the digest computation using the specified portion of an array of bytes.
byte[] digest(
—Finishes the hash computation and returns the message digest as a byte
array.
byte[] digest(byte[]
input
)
—Updates the digest computation using the specified array of bytes, finishes the digest computation, and returns the digest as a byte
array.
int digest(byte[]
buf
, int
offset
, int
len
)
—Finishes the digest computation and returns the number of bytes in the message digest. The message digest is written to the specified location in buf
.
int getDigestLength()
—Returns the length of the digest in bytes.
Provider getProvider()
—Returns the provider of the MessageDigest
object.
String toString()
—Returns a String
representation of the MessageDigest
object.
Object clone()
—Returns a clone of the MessageDigest
if the implementation starts the Cloneable
interface.
static boolean isEqual(byte[]
digesta
, byte[]
digestb
)
—Compares two MessageDigest
objects for equality and returns a boolean value indicating the result of the comparison.
The basic approach to message digest computation uses getInstance()
to create a MessageDigest
object, update()
to update the digest value with a sequence of bytes, and digest()
to complete the calculation and retrieve the final digest value. The following section illustrates how this is done.
The MessageDigestTestApp
program (Listing 7.1) shows how to use the MessageDigest
class to calculate message digests. It performs each of the seven message digest test cases that are documented at the end of RFC 1321. When you run the program, it produces the following output:
Test case: Expected result: d41d8cd98f00b204e9800998ecf8427e Calculated result: d41d8cd98f00b204e9800998ecf8427e Test case: a Expected result: 0cc175b9c0f1b6a831c399e269772661 Calculated result: 0cc175b9c0f1b6a831c399e269772661 Test case: abc Expected result: 900150983cd24fb0d6963f7d28e17f72 Calculated result: 900150983cd24fb0d6963f7d28e17f72 Test case: message digest Expected result: f96b697d7cb7938d525a2f31aaf161d0 Calculated result: f96b697d7cb7938d525a2f31aaf161d0 Test case: abcdefghijklmnopqrstuvwxyz Expected result: c3fcd3d76192e4007dfb496cca67e13b Calculated result: c3fcd3d76192e4007dfb496cca67e13b Test case: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789 Expected result: d174ab98d277d9f5a5611c2c9f419d9f Calculated result: d174ab98d277d9f5a5611c2c9f419d9f Test case: 1234567890123456789012345678901234567890123456789 012345678901 2345678901234567890 Expected result: 57edf4a22be3c955ac49da2e2107b67a Calculated result: 57edf4a22be3c955ac49da2e2107b67a
As you can see, the program passes all the test cases. The testCase
array contains the seven values to be tested, and the expectedResult
array contains the expected MD5 digests for these values.
The performMD5Test()
method performs the MD5 test for each value. It creates a MessageDigest
object using getInstance()
and invokes the update()
method to process the bytes corresponding to the test case. The digest()
method is then invoked to retrieve the digest value. The Conversion
class (covered in Chapter 5) is used to convert the digest from a byte
array to a String
object. The results of the test are then displayed.
Example 7.1. The MessageDigestTestApp
Program
package com.jaworski.security.handbook; import java.security.*; public class MessageDigestTestApp { // MD5 test suite from RFC 1321 static String[] testCase = { "", "a", "abc", "message digest", "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", "123456789012345678901234567890123456789012345678901234567890123456789012345678 90"} ; static String[] expectedResult = { "d41d8cd98f00b204e9800998ecf8427e", "0cc175b9c0f1b6a831c399e269772661", "900150983cd24fb0d6963f7d28e17f72", "f96b697d7cb7938d525a2f31aaf161d0", "c3fcd3d76192e4007dfb496cca67e13b", "d174ab98d277d9f5a5611c2c9f419d9f", "57edf4a22be3c955ac49da2e2107b67a"} ; public static void main(String[] args) { for(int i=0; i<testCase.length; ++i) performMD5Test(i); } static void performMD5Test(int i) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(testCase[i].getBytes()); String s = Conversion.byteArrayToHexString(md.digest()); System.out.println(" Test case: "+testCase[i]); System.out.println("Expected result: "+expectedResult[i]); System.out.println("Calculated result: "+s); } catch(NoSuchAlgorithmException e) { System.out.println("MD5 is not supported."); System.exit(0); } } }
The DigestInputStream
and DigestOutputStream
classes simplify the computation of message digests for sequences of bytes that are read from or written to streams. DigestInputStream
is used to compute a message digest on a sequence of bytes that is read from a stream. DigestOutputStream
is used to calculate a message digest on bytes that are written to a stream. These classes extend java.io.FilterInputStream
and java.io.FilterOutputStream
, making them digest-calculating input and output filters.
The DigestInputStream
constructor takes the InputStream
to be filtered and a MessageDigest
object as arguments:
DigestInputStream(InputStream stream, MessageDigest digest)
The InputStream
is the stream for which the message digest is to be calculated. The MessageDigest
object is created using the static
getInstance()
method of MessageDigest.
The methods of DigestInputStream
are as follows:
void setMessageDigest(MessageDigest
digest
)
—Sets the MessageDigest
object to be used with the stream. Because the MessageDigest
object is supplied with the DigestInputStream
constructor, this method is needed only when you want to substitute a new MessageDigest
object.
void on(boolean on)
—Turns the digest calculation on or off. By default, the digest calculation is on, and the digest is updated for all bytes read from the stream. You can turn it off for selected input and then turn it on again.
int read()
—Reads a byte
from the input stream and returns it as an int
value. The message digest is updated if the digest function is on.
int read(byte[]
b
, int
off
, int
len
)
—Reads bytes from the input stream into a byte
array and returns the number of bytes read. The message digest is updated if the digest function is on.
MessageDigest getMessageDigest()
—Returns the message digest associated with the stream.
String
toString()
—Returns a String
representation of the digest input stream and its associated MessageDigest
object.
DigestOutputStream
is the output analog of DigestInputStream.
The DigestOutputStream
constructor takes the OutputStream
to be filtered and a MessageDigest
object as its arguments:
DigestOutputStream(OutputStream stream, MessageDigest digest)
The OutputStream
is the stream for which the message digest is to be calculated. The MessageDigest
object is created using the static
getInstance()
method of MessageDigest.
The methods of DigestOutputStream
are as follows:
void setMessageDigest(MessageDigest
digest
)
—Sets the MessageDigest
object to be used with the stream. Because the MessageDigest
object is supplied with the DigestInputStream
constructor, this method is needed only when you want to substitute a new MessageDigest
object.
void on(boolean on)
—Turns the digest calculation on or off. By default, the digest calculation is on and the digest is updated for all bytes read from the stream. You can turn it off for selected input and then turn it on again.
void write(byte[]
b
, int
off
, int
len
)
—Writes the specified portion of the byte
array to the output stream. The message digest is updated using the bytes that are written if the digest calculation is on.
void write(int
b
)
—Writes the byte to the output stream and updates the message digest (if the digest function is on).
MessageDigest getMessageDigest()
—Returns the message digest associated with the stream.
String toString()
—Returns a String
representation of the digest input stream and its associated
MessageDigest
object.
The basic approach to working with DigestInputStream
and DigestOutputStream
is to create a MessageDigest
object and then provide it to the DigestInputStream
or DigestOutputStream
constructors to create an appropriate digest stream object. Use the stream to perform input or output and then retrieve the updated MessageDigest
object. Finally, invoke the digest()
method of the MessageDigest
object to retrieve the final digest value.
The DigestStreamTestApp
program (see Listing 7.2) illustrates the use of the DigestOutputStream
and DigestInputStream
classes. It verifies one of the SHA-1 test cases described in FIPS 180-1. It performs the verification using DigestOutputStream
and then using DigestInputStream.
The program produces the following results:
Output test case: abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq Expected result: 84983e441c3bd26ebaae4aa1f95129e5e54670f1 Calculated result: 84983e441c3bd26ebaae4aa1f95129e5e54670f1 Input test case: abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq Expected result: 84983e441c3bd26ebaae4aa1f95129e5e54670f1 Calculated result: 84983e441c3bd26ebaae4aa1f95129e5e54670f1
The performOutputTest()
method creates a MessageDigest
object that implements the SHA algorithm and a FileOutputStream
object that writes to the file sha-results.txt.
A DigestOutputStream
object is then created using the MessageDigest
and FileOutputStream
objects.
The write()
method of DigestOutputStream
is used to write the test string to the output stream. The updated MessageDigest
object is then retrieved from the DigestOutputStream
, and its digest is retrieved via the digest()
method. The results of the test are then displayed.
The performInputTest()
method is very similar to the performOutputTest()
method except that performInputTest()
reads the data that was written to sha-results.txt
and performs the digest calculation on the values that are read.
Example 7.2. The DigestStreamTestApp
Program
package com.jaworski.security.handbook; import java.security.*; import java.io.*; public class DigestStreamTestApp { // SHA test suite from FIPS 180-1 static String testCase = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"; static String expectedResult = "84983e441c3bd26ebaae4aa1f95129e5e54670f1"; public static void main(String[] args) { performOutputTest(); performInputTest(); } static void performOutputTest() { try { MessageDigest md = MessageDigest.getInstance("SHA"); FileOutputStream fout = new FileOutputStream("sha-results.txt"); DigestOutputStream out = new DigestOutputStream(fout,md); byte[] b = testCase.getBytes(); out.write(b,0,b.length); md = out.getMessageDigest(); String s = Conversion.byteArrayToHexString(md.digest()); System.out.println(" Output test case: "+testCase); System.out.println("Expected result: "+expectedResult); System.out.println("Calculated result: "+s); } catch(Exception e) { System.out.println(e); System.exit(0); } } static void performInputTest() { try { MessageDigest md = MessageDigest.getInstance("SHA"); FileInputStream fin = new FileInputStream("sha-results.txt"); DigestInputStream in = new DigestInputStream(fin,md); byte[] b = new byte[testCase.length()]; in.read(b,0,testCase.length()); md = in.getMessageDigest(); String s = Conversion.byteArrayToHexString(md.digest()); System.out.println(" Input test case: "+(new String(b))); System.out.println("Expected result: "+expectedResult); System.out.println("Calculated result: "+s); } catch(Exception e) { System.out.println(e); System. exit(0); } } }
Message authentication codes (MACs) are message digests that are computed using secret key cryptography. The key is used to detect modifications to a message that is sent over an insecure communication channel. For example, consider Figure 7.1. A wants to send message m to B. A calculates md, the message digest of m, and sends both m and md over an unprotected network to B. However, C intercepts m and md, modifies m (creating m'), and creates md'(the message digest of m'). C then sends m'and md'to B. When B verifies md', he finds that it is the correct message digest for m'.
Because message authentication codes use a key in the calculation of the message digest, messages that are sent out with a MAC cannot be intercepted, modified, and retransmitted without detection. For example, in Figure 7.2, if C modifies A's message, C must have A's key in order to calculate the MAC of the modified message.
In the preceding example, a secure key distribution/agreement protocol is needed to distribute the key that is shared between A and B. Chapter 6 covers key distribution/agreement.
A MAC that uses a message digest or hash function is referred to as an HMAC. For example, HMACs can be created for use with MD5 or SHA-1. An HMAC uses a secret key with the message digest algorithm. RFC 2104 describes the use of HMACs.
Support for message authentication codes is provided with the MacSpi
and Mac
classes of javax.crypto
(part of the Java Cryptography Extension). These classes are covered in the following sections.
The MacSpi
class is an abstract class that offers a service provider interface for developing a provider-specific implementation of the Mac
class. Its methods are as follows:
protected abstract void engineInit(java.security.Key
key
, java.security.spec.AlgorithmParameterSpec
params
)
—Initializes the MAC with the specified secret key and algorithm parameters.
protected abstract void engineUpdate(byte
input
)
—Updates the MAC with the given byte.
protected abstract void engineUpdate(byte[]
input
, int
offset
, int
len
)
—Updates the MAC with the specified portion of the input
array.
protected abstract int engineGetMacLength()
—Returns the length of the MAC in bytes.
protected abstract void engineReset()
—Resets the MAC, but retains the secret key that initialized the MAC.
protected abstract byte[] engineDoFinal()
—Finishes the MAC computation and returns the MAC as a byte
array. The MAC is reset but still retains the secret key with which it was initialized.
java.lang.Object clone()
—Provides the basis for implementing the Cloneable
interface.
You don't need the preceding methods unless you want to develop your own MAC implementation. Most of us will use the default Mac
class that is provided with javax.crypto.
The Mac
class is the main class that is used in MAC calculation. The Mac
class provides support for two MAC algorithms: HMAC-MD5 and HMAC-SHA1, both of which are described in RFC 2104, "HMAC: Keyed-Hashing for Message Authentication." The strings "HmacMD5
" and "HmacSHA1
" are used to identify these algorithms.
The Mac
class, like MessageDigest
, is a factory class. Mac
objects are created using the static getInstance()
method:
Mac mac = Mac.getInstance("HmacSHA1");
Another form of getInstance()
enables a specific service provider to be identified.
The methods of Mac
are as follows:
static Mac getInstance(java.lang.String
algorithm
)
—Creates a Mac
object that implements the specified algorithm.
static Mac getInstance(java.lang.String
algorithm
, java.lang.String
provider
)
—Creates a Mac
object that implements the specified algorithm and is from the specified provider.
void init(java.security.Key
key
)
—Initializes the Mac
object with the given key.
void init(java.security.Key
key
, java.security.spec.AlgorithmParameterSpec
params
)
—Initializes the Mac
object with the specified key and algorithm parameters.
void update(byte
input
)
—Updates the MAC with the specified byte.
void update(byte[]
input
)
—Updates the MAC with the specified byte
array.
void update(byte[]
input
, int
offset
, int
len
)
—Updates the MAC with the identified portion of the byte
array.
byte[] doFinal()
—Finishes the MAC operation and returns the MAC as a byte
array.
byte[] doFinal(byte[]
input
)
—Processes the byte
array, finishes the MAC operation, and returns the MAC as a byte
array.
void doFinal(byte[]
output
, int
outOffset
)
—Processes the specified portion of the byte
array, finishes the MAC operation, and returns the MAC as a byte
array.
java.lang.String getAlgorithm()
—Returns the name of the algorithm used with the Mac
object.
java.security.Provider getProvider()
—Returns the provider of the Mac
object.
java.lang.Object clone()
—Provides support for implementing the Cloneable
interface.
You'll learn how to use these methods to calculate MACs in the next section, "MACs in Action."
The MACApp
program (shown in Listing 7.3) illustrates how MACs are created. It takes a text string (enclosed in quotes) as an argument
java com.jaworski.security.handbook.MACApp "This is a test."
and produces the following output:
Message: This is a test. MAC: ac2d81f3e5019f707fdc45ea05e842a9
The performMACTest()
method uses the getInstance()
method of Mac
to create a Mac
object that implements the HmacMD5
algorithm. It creates a KeyGenerator
object that works with HmacMD5
and then generates a SecretKey
object for use in initializing the Mac
object. The initialization is performed using the init()
method. The update()
method is then used to update the Mac
object with the text string that was typed at the command line. The doFinal()
method completes the MAC calculation and returns the MAC as a byte
array. The results of the calculation are then displayed.
Example 7.3. The MACApp
Program
package com.jaworski.security.handbook; import java.security.*; import javax.crypto.*; public class MACApp { static String errorMessage = "Usage: com.jaworski.security.handbook.MACApp testString"; public static void main(String[] args) { if(args.length < 1) System.out.println(errorMessage); else performMACTest(args[0]); } static void performMACTest(String s) { try { String alg = "HmacMD5"; Mac mac = Mac.getInstance(alg); KeyGenerator kg = KeyGenerator.getInstance(alg); SecretKey key = kg.generateKey(); mac.init(key); mac.update(s.getBytes()); byte[] b = mac.doFinal(); System.out.println(" Message: "+s); System.out.println("MAC: "+ Conversion.byteArrayToHexString((b))); } catch(Exception e) { System.out.println(e); System.exit (0); } } }
Digital signatures and the NIST digital signature algorithm were introduced in Chapter 5. In this section, you'll cover the signature classes of java.security
in detail and learn how to use them to create signed objects.
The SignatureSpi
class provides a service provider interface for the Signature
class. SignatureSpi
is implemented by developers who want to provide their own custom implementations of the Signature
class. It defines the following methods:
protected abstract void engineInitSign(PrivateKey
privateKey
)
—Initializes the Signature
object with the specified private key for use in signing operations.
protected void engineInitSign(PrivateKey
privateKey
, SecureRandom
random
)
— Initializes the Signature
object with the specified private key and source of randomness for use in signing operations.
protected abstract void engineInitVerify(PublicKey
publicKey
)
—Initializes the Signature
object with the specified public key for use in signature verification operations.
protected void engineSetParameter(AlgorithmParameterSpec
params
)
—Initializes the signature engine with the specified parameter set.
protected abstract void engineSetParameter(String
param
, Object
value
)
—This method has been deprecated in JDK 1.2 and is replaced by the previous engineSetParameter()
method.
protected abstract Object engineGetParameter(String
param
)
—This method has been deprecated in JDK 1.2.
protected abstract void engineUpdate(byte
b
)
—Updates the data to be signed or verified using the specified byte.
protected abstract void engineUpdate(byte[]
b
, int
off
, int
len
)
—Updates the data to be signed or verified using the specified portion of the byte
array.
protected abstract byte[] engineSign()
—Finishes the signature operation and returns the signature as a byte
array.
protected int engineSign(byte[]
outbuf
, int
offset
, int
len
)
—Finishes the signature operation and returns the signature via outbuf.
protected abstract boolean engineVerify(byte[]
sigBytes
)
— Verifies the signature specified by sigBytes.
Object clone()
—Provides support for implementing the Cloneable
interface.
The preceding abstract methods must be implemented by a service provider that implements a signature algorithm.
The Signature
class provides a default implementation of the SignatureSpi
abstract methods and supports both signature generation and verification. It supports an algorithm referred to as SHA1withDSA.
This algorithm calculates an SHA-1 digest of a sequence of bytes and uses DSA to create and verify DSA digital signatures. DSA is described in FIPS PUB 186. The Signature
class is a factory class, and Signature
objects are created with the static getInstance()
method:
Signature sign = Signature.getInstance("DSA");
A second form of the method allows a particular service provider to be specified.
The methods of Signature
are as follows:
static Signature getInstance(String
algorithm
)
—Creates a Signature
object that implements the specified signature algorithm.
static Signature getInstance(String
algorithm
, String
provider
)
—Creates a Signature
object that implements the specified signature algorithm and is supplied from the specified provider (if available).
void setParameter(AlgorithmParameterSpec
params
)
—Initializes the signature engine with the specified parameter set.
void setParameter(String
param
, Object
value
)
—This method has been deprecated in JDK 1.2. Use the previous form of setParameter().
Object getParameter(String
param
)
—This method has been deprecated in JDK 1.2.
Provider getProvider()
—Returns the provider of the Signature
object.
String getAlgorithm()
—Returns the name of the algorithm used with the Signature
object.
void initSign(PrivateKey
privateKey
)
—Initializes the Signature
object for signing with the specified private key.
void initSign(PrivateKey
privateKey
, SecureRandom
random
)
—Initialize the Signature
object for signing with the specified private key and source of randomness.
void initVerify(PublicKey
publicKey
)
—Initializes the Signature
object for verification with the specified public key.
void update(byte
b
)
—Updates the data to be signed or verified by a byte.
void update(byte[]
data
)
—Updates the data to be signed or verified using the specified byte
array.
void update(byte[]
data
, int
off
, int
len
)
—Updates the data to be signed or verified using the specified portion of the byte
array.
byte[] sign()
—Returns the signature that has been calculated as a byte
array.
int sign(byte[]
outbuf
, int
offset
, int
len
)
—Stores the calculated signature in outbuf
and returns the length of the signature in bytes.
boolean verify(byte[]
signature
)
—Verifies the specified signature and returns a boolean value indicating the success or failure of the verification.
String toString()
—Returns a representation of the Signature
object.
Object clone()
—Provides support for implementing the
Cloneable
interface.
The Signature
class also defines the protected
state
variable, which can be in one of the three states, UNINITIALIZED
, SIGN
, and VERIFY.
The SignApp
program of Listing 7.4 shows how signatures are created and verified. It takes a text string as an argument
java com.jaworski.security.handbook.SignApp "This is a test."
and produces the following output:
Generating key pair ... Generating signature ... Message: This is a test. Private key: MIIBTAIBADCCASwGByqGSM44BAEwggEfAoGBAOcC6xh0xr7EX6GtF7KwkrwutkQ1 uz8Y7yn2i+v1/X1gmsSKLqBBpIuJCKvPiZkepWUNmQlqCfX3YKUu9a4ULOEXzDDj Gn7tAeZF8fJqybtjljQy2G77ymXievqQ1SXTC8ksYl1Og0iAK29hQ2zvCCMx+eGu 8CEUjuYwsDV//ybXAhUA6BMUoT4DcaUi40c9nYZ6mqn0HncCgYEAg4IN8kl0qLaE LZ20GZnvngOY4MxozByPMrzsmXIFRIPUZsZbosCXfHp8xso3pz5DJfMtX10sUnxK uUYCiQ4j6GxqHvr3qt9QT03eP/bXx5DG0LmZ6s1wdIzJI6bibMYZtvTyP4VPsgVj yGiFX/uAZDELX3PYcmjZ94rBhjV9j+0EFwIVAI5j5RYwxYQEBbBtFFGlyOd4XE2V Public key: MIIBtzCCASwGByqGSM44BAEwggEfAoGBAOcC6xh0xr7EX6GtF7KwkrwutkQ1uz8Y 7yn2i+v1/X1gmsSKLqBBpIuJCKvPiZkepWUNmQlqCfX3YKUu9a4ULOEXzDDjGn7t AeZF8fJqybtjljQy2G77ymXievqQ1SXTC8ksYl1Og0iAK29hQ2zvCCMx+eGu8CEU juYwsDV//ybXAhUA6BMUoT4DcaUi40c9nYZ6mqn0HncCgYEAg4IN8kl0qLaELZ20 GZnvngOY4MxozByPMrzsmXIFRIPUZsZbosCXfHp8xso3pz5DJfMtX10sUnxKuUYC iQ4j6GxqHvr3qt9QT03eP/bXx5DG0LmZ6s1wdIzJI6bibMYZtvTyP4VPsgVjyGiF X/uAZDELX3PYcmjZ94rBhjV9j+0DgYQAAoGAX1ZnfHw1ixOqxnF4uZ9qSo4mfAbD saPS1Ts+AOXq77tOL3NWDjSZJOGQVkfua5+K0F76FI6tm/zpOlo2u1HurLELL+t4 g6OwMzkVztbWfW5JagHZwdnbs87jtyu2TwjuOLCGJrp82uCXXW+zU+Jdy0/6487l ZSfTdGK+EQ7eocM= Signature: MC4CFQDa3LzcBGuxW+HnSYcQ2nGY4Z+vPwIVAMLI+KF/DknI7tJ55D8K7DnN45tf Verifying signature ... Signature verified!
The program uses the NIST Digital Signature Algorithm for signing and is organized according to the generateKeyPair()
, performSigning()
, and performVerification()
methods. The generateKeyPair()
method returns a KeyPair
object that is used in both performSigning()
and performVerification().
The performSigning()
method returns a byte
array representation of a Signature
object. The performVerification()
method verifies this signature.
The generateKeyPair()
method creates a KeyPairGenerator
object for DSA and generates a KeyPair.
This process can take a few minutes on a slow computer.
The performSigning()
method creates a Signature
object and then initializes it, via initSign()
, for signing using the private key of the KeyPair
object produced by generateKeyPair().
It then updates the signature via the update()
method and creates the final signature via the sign()
method. This signature is returned as the result of performSigning().
The performVertification()
method verifies the signature that was returned by performSigning().
It uses the public key of the KeyPair
object created by generateKeyPair().
This method creates a Signature
object that uses DSA and initializes it for verification using the public key. It invokes the update()
method of Signature
to create a new signature and the verify()
method to perform the signature verification.
Example 7.4. The SignApp
Program
package com.jaworski.security.handbook; import java.security.*; public class SignApp { static String errorMessage = "Usage: com.jaworski.security.handbook.SignApp testString"; public static void main(String[] args) { if(args.length < 1) System.out.println(errorMessage); else{ String alg = "DSA"; KeyPair keyPair = generateKeyPair(alg); byte[] signature = performSigning(args[0],alg,keyPair); performVerification(args[0],alg,signature,keyPair.getPublic()); } } static KeyPair generateKeyPair(String alg) { try { KeyPairGenerator kg = KeyPairGenerator.getInstance(alg); System.out.println("Generating key pair ..."); KeyPair keyPair = kg.genKeyPair(); return keyPair; } catch(Exception e) { System.out.println(e); System.exit(0); } return null; } static byte[] performSigning(String s,String alg,KeyPair keyPair) { try { Signature sign = Signature.getInstance(alg); PrivateKey privateKey = keyPair.getPrivate(); PublicKey publicKey = keyPair.getPublic(); sign.initSign(privateKey); System.out.println("Generating signature ..."); sign.update(s.getBytes()); byte[] b = sign.sign(); System.out.println(" Message: "+s); System.out.println("Private key: "+ Conversion.byteArrayToBase64String(privateKey.getEncoded())); System.out.println("Public key: "+ Conversion.byteArrayToBase64String(publicKey.getEncoded())); System.out.println("Signature: "+ Conversion.byteArrayToBase64String(b)); return b; } catch(Exception e) { System.out.println(e); System.exit(0); } return null; } static void performVerification(String s, String alg, byte[] signature, PublicKey publicKey) { try { Signature sign = Signature.getInstance(alg); System.out.println(" Verifying signature ..."); sign.initVerify(publicKey); sign.update(s.getBytes()); if(sign.verify(signature)) System.out.println("Signature verified!"); else System.out.println("Signature NOT verified!"); } catch(Exception e){ System.out.println(e); System.exit(0); } } }
The SignedObject
class provides the capability to create signed objects whose integrity can be verified. It provides a copy of a Serializable
object and a signature of the serialized form of the object. The signature is created from a Signature
object and signing (private) key that is provided to the SignedObject
constructor:
Signature sign = Signature.getInstance("SHA1withDSA "); SignedObject so = new SignedObject(objectToBeSigned, privatekey, sign);
The object's signature is verified using the verify()
method. This method takes a PublicKey
and Signature
object as arguments. The getObject()
method returns the original object that was signed:
Signature sign = Signature.getInstance("SHA1withDSA "); if(so.verify(publickey, sign)) myObject = so.getObject();
The methods of SignedObject
are summarized as follows:
boolean verify(PublicKey
verificationKey
, Signature
verificationSignature
)
—Verifies that the signature in the SignedObject
is valid for the object stored inside. It bases verification on the verification key and verification signature object.
String getAlgorithm()
—Returns the name of the signature algorithm.
Object getObject()
—Returns the object that was signed.
byte[] getSignature()
—Returns the signature on the signed object as a
byte
array.
An example of using SignedObject
is provided in the following section.
The SignedObjectApp
program of Listing 7.5 shows how SignedObject
s are created and verified. It produces the following output:
Generating key pair ... Creating signed object ... Verifying signature ... Signature verified!
The key pair generation can take some time on a slow computer.
The SignedObjectApp
program creates a DSA KeyPair
object and a Vector
that serves as the object that is signed. These objects are passed to createSignedObject()
, which creates and returns a SignedObject
based on the Vector
that is signed using the private key of the KeyPair.
The performVerification()
method then verifies the signature of the SignedObject.
The createSignedObject()
method creates a Signature
object and passes the Vector
, private key, and Signature
object to the SignedObject
constructor. The SignedObject
instance that is created is then returned.
The performVerification()
method creates a Signature
object for verification purposes and passes this object and the public key of the key pair to the verify()
method of SignedObject.
The verify()
method verifies the Signature
using the public key.
Example 7.5. The SignedObjectApp
Program
package com.jaworski.security.handbook; import java.security.*; import java.util.*; public class SignedObjectApp { public static void main(String[] args) { String alg = "DSA"; KeyPair keyPair = generateKeyPair(alg); Vector v = new Vector(); v.add("This is a test!"); SignedObject so = createSignedObject(v,alg,keyPair); performVerification(so,alg,keyPair.getPublic()); } static KeyPair generateKeyPair(String alg) { try { KeyPairGenerator kg = KeyPairGenerator.getInstance(alg); System.out.println("Generating key pair ..."); KeyPair keyPair = kg.genKeyPair(); return keyPair; } catch(Exception e) { System.out.println(e); System.exit(0); } return null; } static SignedObject createSignedObject(Vector v,String alg,KeyPair keyPair) { try { Signature sign = Signature.getInstance(alg); System.out.println("Creating signed object ..."); SignedObject so = new SignedObject(v, keyPair.getPrivate(), sign); return so; } catch(Exception e) { System.out.println(e); System.exit(0); } return null; } static void performVerification(SignedObject so, String alg, PublicKey publicKey) { try { System.out.println(" Verifying signature ..."); Signature sign = Signature.getInstance(alg); if(so.verify(publicKey, sign)) System.out.println("Signature verified!"); else System.out.println("Signature NOT verified!"); } catch(Exception e){ System. out.println(e); System.exit(0); } } }
The Signer
class, a deprecated subclass of Identity,
was used to support JDK 1.1 signatures. This class has been replaced by the Keystore
class in JDK 1.2. Both Signer
and KeyStore
are covered in Chapter 6.
This chapter covered the classes used to create message digests, MACs, and digital signatures. It also provided practical examples of using the classes to create and verify these objects. In the next chapter, "The Java Cryptography Extension," you'll cover the details of the Java Cryptography Extension (JCE) and learn how to develop a service provider.
3.145.191.22