|
As we said earlier, applets were what started the craze over the Java platform. In practice, people discovered that although they could write animated applets like the famous "nervous text" applet, applets could not do a whole lot of useful stuff in the JDK 1.0 security model. For example, because applets under JDK 1.0 were so closely supervised, they couldn't do much good on a corporate intranet, even though relatively little risk attaches to executing an applet from your company's secure intranet. It quickly became clear to Sun that for applets to become truly useful, it was important for users to be able to assign different levels of security, depending on where the applet originated. If an applet comes from a trusted supplier and it has not been tampered with, the user of that applet can then decide whether to give the applet more privileges. This added control is now possible because of the applet-signing mechanism in Java 1.1. To give more trust to an applet, we need to know two things:
In the past 50 years, mathematicians and computer scientists have developed sophisticated algorithms for ensuring the integrity of data and for electronic signatures. The java.security package contains implementations of many of these algorithms. Fortunately, you don't need to understand the underlying mathematics to use the algorithms in the java.security package. In the next sections, we show you how message digests can detect changes in data files and how digital signatures can prove the identity of the signer. Message DigestsA message digest is a digital fingerprint of a block of data. For example, the so-called SHA1 (secure hash algorithm #1) condenses any data block, no matter how long, into a sequence of 160 bits (20 bytes). As with real fingerprints, one hopes that no two messages have the same SHA1 fingerprint. Of course, that cannot be truethere are only 2160 SHA1 fingerprints, so there must be some messages with the same fingerprint. But 2160 is so large that the probability of duplication occurring is negligible. How negligible? According to James Walsh in True Odds: How Risks Affect Your Everyday Life [Merritt Publishing 1996], the chance that you will die from being struck by lightning is about one in 30,000. Now, think of nine other people, for example, your nine least favorite managers or professors. The chance that you and all of them will die from lightning strikes is higher than that of a forged message having the same SHA1 fingerprint as the original. (Of course, more than 10 people, none of whom you are likely to know, will die from lightning. However, we are talking about the far slimmer chance that your particular choice of people will be wiped out.) A message digest has two essential properties:
The second property is again a matter of probabilities, of course. Consider the following message by the billionaire father:
That message has an SHA1 fingerprint of 2D 8B 35 F3 BF 49 CD B1 94 04 E0 66 21 2B 5E 57 70 49 E1 7E The distrustful father has deposited the message with one attorney and the fingerprint with another. Now, suppose George can bribe the lawyer holding the message. He wants to change the message so that Bill gets nothing. Of course, that changes the fingerprint to a completely different bit pattern: 2A 33 0B 4B B3 FE CC 1C 9D 5C 01 A7 09 51 0B 49 AC 8F 98 92 Can George find some other wording that matches the fingerprint? If he had been the proud owner of a billion computers from the time the Earth was formed, each computing a million messages a second, he would not yet have found a message he could substitute. A number of algorithms have been designed to compute these message digests. The two best-known are SHA1, the secure hash algorithm developed by the National Institute of Standards and Technology, and MD5, an algorithm invented by Ronald Rivest of MIT. Both algorithms scramble the bits of a message in ingenious ways. For details about these algorithms, see, for example, Cryptography and Network Security by William Stallings [Prentice Hall 1998]. Note that recently, subtle regularities have been discovered in MD5. Most cryptographers recommend avoiding it and using SHA1 for that reason. (Both algorithms are easy to compute.) The Java programming language implements both SHA1 and MD5. The MessageDigest class is a factory for creating objects that encapsulate the fingerprinting algorithms. It has a static method, called getInstance, that returns an object of a class that extends the MessageDigest class. This means the MessageDigest class serves double duty:
For example, here is how you obtain an object that can compute SHA fingerprints. MessageDigest alg = MessageDigest.getInstance("SHA-1"); (To get an object that can compute MD5, use the string "MD5" as the argument to getInstance.) After you have obtained a MessageDigest object, you feed it all the bytes in the message by repeatedly calling the update method. For example, the following code passes all bytes in a file to the alg object created above to do the fingerprinting: InputStream in = . . . int ch; while ((ch = in.read()) != -1) alg.update((byte) ch); Alternatively, if you have the bytes in an array, you can update the entire array at once: byte[] bytes = . . .; alg.update(bytes); When you are done, call the digest method. This method pads the inputas required by the fingerprinting algorithmdoes the computation, and returns the digest as an array of bytes. byte[] hash = alg.digest(); The program in Example 9-17 computes a message digest, using either SHA or MD5. You can load the data to be digested from a file, or you can type a message in the text area. Figure 9-10 shows the application. Example 9-17. MessageDigestTest.java1. import java.io.*; 2. import java.security.*; 3. import java.awt.*; 4. import java.awt.event.*; 5. import javax.swing.*; 6. 7. /** 8. This program computes the message digest of a file 9. or the contents of a text area. 10. */ 11. public class MessageDigestTest 12. { 13. public static void main(String[] args) 14. { 15. JFrame frame = new MessageDigestFrame(); 16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 17. frame.setVisible(true); 18. } 19. } 20. 21. /** 22. This frame contains a menu for computing the message 23. digest of a file or text area, radio buttons to toggle between 24. SHA-1 and MD5, a text area, and a text field to show the 25. message digest. 26. */ 27. class MessageDigestFrame extends JFrame 28. { 29. public MessageDigestFrame() 30. { 31. setTitle("MessageDigestTest"); 32. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); 33. 34. JPanel panel = new JPanel(); 35. ButtonGroup group = new ButtonGroup(); 36. addRadioButton(panel, "SHA-1", group); 37. addRadioButton(panel, "MD5", group); 38. 39. add(panel, BorderLayout.NORTH); 40. add(new JScrollPane(message), BorderLayout.CENTER); 41. add(digest, BorderLayout.SOUTH); 42. digest.setFont(new Font("Monospaced", Font.PLAIN, 12)); 43. 44. setAlgorithm("SHA-1"); 45. 46. JMenuBar menuBar = new JMenuBar(); 47. JMenu menu = new JMenu("File"); 48. JMenuItem fileDigestItem = new JMenuItem("File digest"); 49. fileDigestItem.addActionListener(new 50. ActionListener() 51. { 52. public void actionPerformed(ActionEvent event) { loadFile(); } 53. }); 54. menu.add(fileDigestItem); 55. JMenuItem textDigestItem = new JMenuItem("Text area digest"); 56. textDigestItem.addActionListener(new 57. ActionListener() 58. { 59. public void actionPerformed(ActionEvent event) 60. { 61. String m = message.getText(); 62. computeDigest(m.getBytes()); 63. } 64. }); 65. menu.add(textDigestItem); 66. menuBar.add(menu); 67. setJMenuBar(menuBar); 68. } 69. 70. /** 71. Adds a radio button to select an algorithm. 72. @param c the container into which to place the button 73. @param name the algorithm name 74. @param g the button group 75. */ 76. public void addRadioButton(Container c, final String name, ButtonGroup g) 77. { 78. ActionListener listener = new 79. ActionListener() 80. { public void actionPerformed(ActionEvent event) { setAlgorithm(name); } 81. }; 82. JRadioButton b = new JRadioButton(name, g.getButtonCount() == 0); 83. c.add(b); 84. g.add(b); 85. b.addActionListener(listener); 86. } 87. 88. /** 89. Sets the algorithm used for computing the digest. 90. @param alg the algorithm name 91. */ 92. public void setAlgorithm(String alg) 93. { 94. try 95. { 96. currentAlgorithm = MessageDigest.getInstance(alg); 97. digest.setText(""); 98. } 99. catch (NoSuchAlgorithmException e) 100. { 101. digest.setText("" + e); 102. } 103. } 104. 105. /** 106. Loads a file and computes its message digest. 107. */ 108. public void loadFile() 109. { 110. JFileChooser chooser = new JFileChooser(); 111. chooser.setCurrentDirectory(new File(".")); 112. 113. int r = chooser.showOpenDialog(this); 114. if (r == JFileChooser.APPROVE_OPTION) 115. { 116. try 117. { 118. String name = chooser.getSelectedFile().getAbsolutePath(); 119. computeDigest(loadBytes(name)); 120. } 121. catch (IOException e) 122. { 123. JOptionPane.showMessageDialog(null, e); 124. } 125. } 126. } 127. 128. /** 129. Loads the bytes in a file. 130. @param name the file name 131. @return an array with the bytes in the file 132. */ 133. public byte[] loadBytes(String name) throws IOException 134. { 135. FileInputStream in = null; 136. 137. in = new FileInputStream(name); 138. try 139. { 140. ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 141. int ch; 142. while ((ch = in.read()) != -1) 143. buffer.write(ch); 144. return buffer.toByteArray(); 145. } 146. finally 147. { 148. in.close(); 149. } 150. } 151. 152. /** 153. Computes the message digest of an array of bytes 154. and displays it in the text field. 155. @param b the bytes for which the message digest should 156. be computed. 157. */ 158. public void computeDigest(byte[] b) 159. { 160. currentAlgorithm.reset(); 161. currentAlgorithm.update(b); 162. byte[] hash = currentAlgorithm.digest(); 163. String d = ""; 164. for (int i = 0; i < hash.length; i++) 165. { 166. int v = hash[i] & 0xFF; 167. if (v < 16) d += "0"; 168. d += Integer.toString(v, 16).toUpperCase() + " "; 169. } 170. digest.setText(d); 171. } 172. 173. private JTextArea message = new JTextArea(); 174. private JTextField digest = new JTextField(); 175. private MessageDigest currentAlgorithm; 176. private static final int DEFAULT_WIDTH = 400; 177. private static final int DEFAULT_HEIGHT = 300; 178. } Figure 9-10. Computing a message digest java.security.MessageDigest 1.1
Message SigningIn the last section, you saw how to compute a message digest, a fingerprint for the original message. If the message is altered, then the fingerprint of the altered message will not match the fingerprint of the original. If the message and its fingerprint are delivered separately, then the recipient can check whether the message has been tampered with. However, if both the message and the fingerprint were intercepted, it is an easy matter to modify the message and then recompute the fingerprint. After all, the message digest algorithms are publicly known, and they don't require secret keys. In that case, the recipient of the forged message and the recomputed fingerprint would never know that the message has been altered. In this section, you will see how digital signatures can authenticate a message. When a message is authenticated, you know
To help you understand how digital signatures work, we explain a few concepts from the field called public key cryptography. Public key cryptography is based on the notion of a public key and private key. The idea is that you tell everyone in the world your public key. However, only you hold the private key, and it is important that you safeguard it and don't release it to anyone else. The keys are matched by mathematical relationships, but it is believed to be practically impossible to compute one from the other. That is, even though everyone knows your public key, they can't compute your private key in your lifetime, no matter how many computing resources they have available. It may seem difficult to believe that nobody can compute the private key from the public keys, but nobody has ever found an algorithm to do this for the encryption algorithms that are in common use today. If the keys are sufficiently long, brute forcesimply trying all possible keyswould require more computers than can be built from all the atoms in the solar system, crunching away for thousands of years. Of course, it is possible that someone could come up with algorithms for computing keys that are much more clever than brute force. For example, the RSA algorithm (the encryption algorithm invented by Rivest, Shamir, and Adleman) depends on the difficulty of factoring large numbers. For the last 20 years, many of the best mathematicians have tried to come up with good factoring algorithms, but so far with no success. For that reason, most cryptographers believe that keys with a "modulus" of 2,000 bits or more are currently completely safe from any attack. There are two kinds of public/private key pairs: for encryption and for authentication. If anyone sends you a message that was encrypted with your public encryption key, then you can decrypt it with your private decryption key, but nobody else can. Conversely, if you sign a message with your private authentication key, then anyone else can verify the signature by checking with your public key. The verification passes only for messages that you signed, and it fails if anyone else used his or her key to sign the message. (Kahn remarks in the new edition of his book The Codebreakers that this was the first new idea in cryptography in hundreds of years.) Many cryptographic algorithms, such as RSA and DSA (the Digital Signature Algorithm), use this idea. The exact structure of the keys and what it means for them to match depend on the algorithm. For example, here is a matching pair of public and private DSA keys. Public key: p: fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae16 17ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee7375 92e17 q: 962eddcc369cba8ebb260ee6b6a126d9346e38c5 g: 678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d1427 1b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4 y: c0b6e67b4ac098eb1a32c5f8c4c1f0e7e6fb9d832532e27d0bdab9ca2d2a 8123ce5a8018b8161a760480fadd040b927281ddb22cb9bc4df596d7de4d1b977d50 Private key: p: fca682ce8e12caba26efccf7110e526db078b05edecbcd1eb4a208f3ae16 17ae01f35b91a47e6df63413c5e12ed0899bcd132acd50d99151bdc43ee737592e17 q: 962eddcc369cba8ebb260ee6b6a126d9346e38c5 g: 678471b27a9cf44ee91a49c5147db1a9aaf244f05a434d6486931d2d1427 1b9e35030b71fd73da179069b32e2935630e1c2062354d0da20a6c416e50be794ca4 x: 146c09f881656cc6c51f27ea6c3a91b85ed1d70a These keys have a mathematical relationship, but the exact nature of the relationship is not interesting for practical programming. (If you are interested, you can look it up in The Handbook of Applied Cryptography at http://www.cacr.math.uwaterloo.ca/hac/.) The obvious question is how to generate the pair of keys. Usually, you do this by feeding the result of some random process to a deterministic procedure that returns the key pair to you. Luckily, how to get a random key pair for public key cryptography is not a question anyone but cryptographers and mathematicians need to worry about. Here is how it works in practice. Suppose Alice wants to send Bob a message, and Bob wants to know this message came from Alice and not an impostor. Alice writes the message and then signs the message digest with her private key. Bob gets a copy of her public key. Bob then applies the public key to verify the signature. If the verification passes, then Bob can be assured of two facts:
See Figure 9-11. Figure 9-11. Public key signature exchange with DSAYou can see why security for private keys is all-important. If someone steals Alice's private key or if a government can require her to turn it over, then she is in trouble. The thief or a government agent can impersonate her by sending messages, money transfer instructions, and so on, that others will believe came from Alice. Let us put the DSA algorithm to work. Actually, there are three algorithms:
Of course, you generate a key pair only once and then use it for signing and verifying many messages. To generate a new random key pair, make sure you use truly random numbers. For example, the regular random number generator in the Random class, seeded by the current date and time, is not random enough. (The jargon says the basic random number generator in java.util is not "cryptographically secure.") For example, supposing the computer clock is accurate to 1/10 of a second; then, there are at most 864,000 seeds per day. If an attacker knows the day a key was issued (as can often be deduced from the expiration date), then it is an easy matter to generate all possible seeds for that day. The SecureRandom class generates random numbers that are far more secure than those produced by the Random class. You still need to provide a seed to start the number sequence at a random spot. The best method for doing this is to obtain random input from a hardware device such as a white-noise generator. Another reasonable source for random input is to ask the user to type away aimlessly on the keyboard, but each keystroke should contribute only one or two bits to the random seed. Once you gather such random bits in an array of bytes, you pass it to the setSeed method. SecureRandom secrand = new SecureRandom(); byte[] b = new byte[20]; // fill with truly random bits secrand.setSeed(b); If you don't seed the random number generator, then it will compute its own 20-byte seed by launching threads, putting them to sleep, and measuring the exact time when they are awakened. NOTE
Once you seed the generator, you can then draw random bytes with the nextBytes method. byte[] randomBytes = new byte[64]; secrand.nextBytes(randomBytes); Actually, to compute a new DSA key, you don't compute the random numbers yourself. You just pass the random number generator object to the DSA key generation algorithm. To make a new key pair, you need a KeyPairGenerator object. Just as with the MessageDigest class of the preceding section, the KeyPairGenerator class is both a factory class and the superclass for actual key-pair-generation algorithms. To get a DSA key pair generator, you call the getInstance method with the string "DSA". KeyPairGenerator keygen = KeyPairGenerator.getInstance("DSA"); The returned object is actually an object of the class sun.security.provider.DSAKeyPairGenerator, which is a subclass of KeyPairGenerator. To generate keys, you must initialize the key generation algorithm object with the key strength and a secure random number generator. Note that the key strength is not the length of the generated keys but the size of one of the building blocks of the key. In the case of DSA, it is the number of bits in the modulus, one of the mathematical quantities that makes up the public and private keys. Suppose you want to generate a key with a modulus of 512 bits: SecureRandom secrand = new SecureRandom(); secrand.setSeed(...); keygen.initialize(512, secrand); Now you are ready to generate key pairs. KeyPair keys = keygen.generateKeyPair(); KeyPair morekeys = keygen.generateKeyPair(); Each key pair has a public and a private key. PublicKey pubkey = keys.getPublic(); PrivateKey privkey = keys.getPrivate(); To sign a message, you need a signature algorithm object. You use the Signature factory class: Signature signalg = Signature.getInstance("DSA"); Signature algorithm objects can be used both to sign and to verify a message. To prepare the object for message signing, use the initSign method and pass the private key to the signature algorithm. signalg.initSign(privkey); Now, use the update method to add bytes to the algorithm objects, in the same way as with the message digest algorithm. byte[] bytes = . . .; while (. . .) signalg.update(bytes); Finally, compute the signature with the sign method. The signature is returned as an array of bytes. byte[] signature = signalg.sign(); The recipient of the message must obtain a DSA signature algorithm object and prepare it for signature verification by calling the initVerify method with the public key as parameter. Signature verifyalg = Signature.getInstance("DSA"); verifyalg.initVerify(pubkey); Then, send the message to the algorithm object. byte[] bytes = . . .; while (. . .) verifyalg.update(bytes); Finally, verify the signature. boolean check = verifyalg.verify(signature); If the verify method returns TRue, then the signature was a valid signature of the message that was signed with the matching private key. That is, both the sender and the contents of the message have been authenticated. Example 9-18 demonstrates the key generation, signing, and verification processes. Run the program like this: java SignatureTest -genkey public.key private.key java SignatureTest -sign sample.txt sample.txt.signed private.key java SignatureTest -verify sample.txt.signed public.key The program should print the message "verified". Then use a hex editor to make a small change to sample.txt.signed, and run the last command again. You should now get a message "not verified". Example 9-18. SignatureTest.java[View full width] 1. import java.io.*; 2. import java.security.*; 3. 4. /** 5. This program demonstrates how to sign a message with a private DSA key 6. and verify it with the matching public key. Usage: 7. java SignatureTest -genkey public private 8. java SignatureTest -sign message signed private 9. java SignatureTest -verify signed public 10. */ 11. public class SignatureTest 12. { 13. public static void main(String[] args) 14. { 15. try 16. { 17. if (args[0].equals("-genkey")) 18. { 19. KeyPairGenerator pairgen = KeyPairGenerator.getInstance("DSA"); 20. SecureRandom random = new SecureRandom(); 21. pairgen.initialize(KEYSIZE, random); 22. KeyPair keyPair = pairgen.generateKeyPair(); 23. ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream (args[1])); 24. out.writeObject(keyPair.getPublic()); 25. out.close(); 26. out = new ObjectOutputStream(new FileOutputStream(args[2])); 27. out.writeObject(keyPair.getPrivate()); 28. out.close(); 29. } 30. else if (args[0].equals("-sign")) 31. { 32. ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[3])); 33. PrivateKey privkey = (PrivateKey) keyIn.readObject(); 34. keyIn.close(); 35. 36. Signature signalg = Signature.getInstance("DSA"); 37. signalg.initSign(privkey); 38. 39. File infile = new File(args[1]); 40. InputStream in = new FileInputStream(infile); 41. int length = (int) infile.length(); 42. byte[] message = new byte[length]; 43. in.read(message, 0, length); 44. in.close(); 45. 46. signalg.update(message); 47. byte[] signature = signalg.sign(); 48. 49. DataOutputStream out = new DataOutputStream(new FileOutputStream(args[2])); 50. int signlength = signature.length; 51. out.writeInt(signlength); 52. out.write(signature, 0, signlength); 53. out.write(message, 0, length); 54. out.close(); 55. } 56. else if (args[0].equals("-verify")) 57. { 58. ObjectInputStream keyIn = new ObjectInputStream(new FileInputStream(args[2])); 59. PublicKey pubkey = (PublicKey) keyIn.readObject(); 60. keyIn.close(); 61. 62. Signature verifyalg = Signature.getInstance("DSA"); 63. verifyalg.initVerify(pubkey); 64. 65. File infile = new File(args[1]); 66. DataInputStream in = new DataInputStream(new FileInputStream(infile)); 67. int signlength = in.readInt(); 68. byte[] signature = new byte[signlength]; 69. in.read(signature, 0, signlength); 70. 71. int length = (int) infile.length() - signlength - 4; 72. byte[] message = new byte[length]; 73. in.read(message, 0, length); 74. in.close(); 75. 76. verifyalg.update(message); 77. if (!verifyalg.verify(signature)) 78. System.out.print("not "); 79. System.out.println("verified"); 80. } 81. } 82. catch (Exception e) 83. { 84. e.printStackTrace(); 85. } 86. } 87. 88. private static final int KEYSIZE = 512; 89. } java.security.KeyPairGenerator 1.1
java.security.KeyPair 1.1
java.security.Signature 1.1
Message AuthenticationSuppose you get a message from your friend, signed by your friend with his private key, using the method we just showed you. You may already have his public key, or you can easily get it by asking him for a copy or by getting it from your friend's web page. Then, you can verify that the message was in fact authored by your friend and has not been tampered with. Now, suppose you get a message from a stranger who claims to represent a famous software company, urging you to run the program that is attached to the message. The stranger even sends you a copy of his public key so you can verify that he authored the message. You check that the signature is valid. This proves that the message was signed with the matching private key and that it has not been corrupted. Be careful: You still have no idea who wrote the message. Anyone could have generated a pair of public and private keys, signed the message with the private key, and sent the signed message and the public key to you. The problem of determining the identity of the sender is called the authentication problem. The usual way to solve the authentication problem is simple. Suppose the stranger and you have a common acquaintance you both trust. Suppose the stranger meets your acquaintance in person and hands over a disk with the public key. Your acquaintance later meets you, assures you that he met the stranger and that the stranger indeed works for the famous software company, and then gives you the disk (see Figure 9-12). That way, your acquaintance vouches for the authenticity of the stranger. Figure 9-12. Authentication through a trusted intermediaryIn fact, your acquaintance does not actually need to meet you. Instead, he can apply his private signature to the stranger's public key file (see Figure 9-13). Figure 9-13. Authentication through a trusted intermediary's signatureWhen you get the public key file, you verify the signature of your acquaintance, and because you trust him, you are confident that he did check the stranger's credentials before applying his signature. However, you may not have a common acquaintance. Some trust models assume that there is always a "chain of trust"a chain of mutual acquaintances so that you trust every member of that chain. In practice, of course, that isn't always true. You may trust your acquaintance, Alice, and you know that Alice trusts Bob, but you don't know Bob and aren't sure that you trust him. Other trust models assume that there is a benevolent big brother in whom we all trust. The best known of these companies is VeriSign, Inc. (http://www.verisign.com). You will often encounter digital signatures that are signed by one or more entities who will vouch for the authenticity, and you will need to evaluate to what degree you trust the authenticators. You might place a great deal of trust in VeriSign, perhaps because you saw their logo on many web pages or because you heard that they require multiple people with black attaché cases to come together into a secure chamber whenever new master keys are to be minted. However, you should have realistic expectations about what is actually being authenticated. The CEO of VeriSign does not personally meet every individual or company representative when authenticating a public key. You can get a "class 1" ID simply by filling out a web form and paying a small fee. The key is mailed to the e-mail address included in the certificate. Thus, you can be reasonably assured that the e-mail address is genuine, but the requestor could have filled in any name and organization. There are more stringent classes of IDs. For example, with a "class 3" ID, VeriSign will require an individual requestor to appear before a notary public, and it will check the financial rating of a corporate requestor. Other authenticators will have different procedures. Thus, when you receive an authenticated message, it is important that you understand what, in fact, is being authenticated. The X.509 Certificate FormatOne of the most common formats for signed certificates is the X.509 format. X.509 certificates are widely used by VeriSign, Microsoft, Netscape, and many other companies, for signing e-mail messages, authenticating program code, and certifying many other kinds of data. The X.509 standard is part of the X.500 series of recommendations for a directory service by the international telephone standards body, the CCITT. In its simplest form, an X.509 certificate contains the following data:
Thus, the signer guarantees that a certain identity has a particular public key. Extensions to the basic X.509 format make it possible for the certificates to contain additional information. The precise structure of X.509 certificates is described in a formal notation, called "abstract syntax notation #1" or ASN.1. Figure 9-14 shows the ASN.1 definition of version 3 of the X.509 format. The exact syntax is not important for us, but, as you can see, ASN.1 gives a precise definition of the structure of a certificate file. The basic encoding rules, or BER, describe precisely how to save this structure in a binary file. That is, BER describes how to encode integers, character strings, bit strings, and constructs such as SEQUENCE, CHOICE, and OPTIONAL. NOTE
Actually, the BER rules are not unique; there are several ways of specifying some elements. The distinguished encoding rules (DER) remove these ambiguities. For a readable description of the BER encoding format, we recommend A Layman's Guide to a Subset of ASN.1, BER, and DER by Burton S. Kaliski, Jr., available from ftp://ftp.rsa.com/pub/pkcs/ps/layman.ps. For the source code of a useful program for dumping BER encoded files, go to http://www.cs.auckland.ac.nz/~pgut001/dumpasn1.c. Figure 9-14. ASN.1 definition of X.509v3[Certificate ::= SEQUENCE { tbsCertificate TBSCertificate, signatureAlgorithm AlgorithmIdentifier, signature BIT STRING } TBSCertificate ::= SEQUENCE { version [0] EXPLICIT Version DEFAULT v1, serialNumber CertificateSerialNumber, signature AlgorithmIdentifier, issuer Name, validity Validity, subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo, issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version must be v2 or v3 subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL, -- If present, version must be v2 or v3 extensions [3] EXPLICIT Extensions OPTIONAL -- If present, version must be v3 } Version ::= INTEGER { v1(0), v2(1), v3(2) } CertificateSerialNumber ::= INTEGER Validity ::= SEQUENCE { notBefore CertificateValidityDate, notAfter CertificateValidityDate } CertificateValidityDate ::= CHOICE { utcTime UTCTime, generalTime GeneralizedTime } UniqueIdentifier ::= BIT STRING SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING } Extensions ::= SEQUENCE OF Extension Extension ::= SEQUENCE { extnID OBJECT IDENTIFIER, critical BOOLEAN DEFAULT FALSE, extnValue OCTET STRING } NOTE
Certificate GenerationThe JDK comes with the keytool program, which is a command-line tool to generate and manage a set of certificates. We expect that ultimately the functionality of this tool will be embedded in other, more user-friendly programs. But right now, we use keytool to show how Alice can sign a document and send it to Bob, and how Bob can verify that the document really was signed by Alice and not an impostor. We do not discuss all of the keytool featuressee the JDK documentation for complete information. The keytool program manages keystores, databases of certificates and private keys. Each entry in the keystore has an alias. Here is how Alice creates a keystore, alice.store, and generates a key pair with alias alice. keytool -genkey -keystore alice.store -alias alice When creating or opening a keystore, you are prompted for a keystore password. For this example, just use password. If you were to use the keytool-generated keystore for any serious purpose, you would need to choose a good password and safeguard this fileit contains private signature keys. When generating a key, you are prompted for the following information: Enter keystore password: password What is your first and last name? [Unknown]: Alice Lee What is the name of your organizational unit? [Unknown]: Engineering Department What is the name of your organization? [Unknown]: ACME Software What is the name of your City or Locality? [Unknown]: Cupertino What is the name of your State or Province? [Unknown]: California What is the two-letter country code for this unit? [Unknown]: US Is <CN=Alice Lee, OU=Engineering Department, O=ACME Software, L=Cupertino, ST=California, C=US> correct? [no]: Y The keytool uses X.500 distinguished names, with components Common Name (CN), Organizational Unit (OU), Organization (O), Location (L), State (ST), and Country (C) to identify key owners and certificate issuers. Finally, specify a key password, or press ENTER to use the keystore password as the key password. Suppose Alice wants to give her public key to Bob. She needs to export a certificate file: keytool -export -keystore alice.store -alias alice -file alice.cert Now Alice can send the certificate to Bob. When Bob receives the certificate, he can print it: keytool -printcert -file alice.cert The printout looks like this:
This certificate is self-signed. Therefore, Bob cannot use another trusted certificate to check that this certificate is valid. Instead, he can call Alice and have her read the certificate fingerprint over the phone. NOTE
Once Bob trusts the certificate, he can import it into his keystore. keytool -import -keystore bob.store -alias alice -file alice.cert CAUTION
Now Alice can start sending signed documents to Bob. The jarsigner tool signs and verifies JAR files. Alice simply adds the document to be signed into a JAR file. jar cvf document.jar document.txt Then she uses the jarsigner tool to add the signature to the file. She needs to specify the keystore, the JAR file, and the alias of the key to use. jarsigner -keystore alice.store document.jar alice When Bob receives the file, he uses the -verify option of the jarsigner program. jarsigner -verify -keystore bob.store document.jar Bob does not need to specify the key alias. The jarsigner program finds the X.500 name of the key owner in the digital signature and looks for matching certificates in the keystore. If the JAR file is not corrupted and the signature matches, then the jarsigner program prints jar verified. Otherwise, the program displays an error message. Certificate SigningIn the preceding section, you saw how to use a self-signed certificate to distribute a public key to another party. However, the recipient of the certificate needed to ensure that the certificate was valid by verifying the fingerprint with the issuer. More commonly, a certificate is signed by a trusted intermediary. The JDK does not contain tools for certificate signing. In this section, you see how to write a program that signs a certificate with a private key from a keystore. This program is useful in its own right, and it shows you how to write programs that access the contents of certificates and keystores. Before looking inside the program code, let's see how to put it to use. Suppose Alice wants to send her colleague Cindy a signed message. But Cindy doesn't want to call up everyone who sends her a signature file to verify the signature fingerprint. There needs to be an entity that Cindy trusts to verify signatures. In this example, we suppose that Cindy trusts the Information Resources Department at ACME Software to perform this service. To simulate this process, you'll need to create an added keystore acmesoft.store. Generate a key and export the self-signed certificate. keytool -genkey -keystore acmesoft.store -alias acmeroot keytool -export -alias acmeroot -keystore acmesoft.store -file acmeroot.cert Then add it to Cindy's keystore. keytool -import -alias acmeroot -keystore cindy.store -file acmeroot.cert Cindy still needs to verify the fingerprint of that root certificate, but from now on, she can simply accept all certificates that are signed by it. For Alice to send messages to Cindy and to everyone else at ACME Software, she needs to bring her certificate to the Information Resources Department and have it signed. However, the keytool program in the JDK does not have this functionality. That is where the certificate signer program in Example 9-19 comes in. The program reads a certificate file and signs it with a private key in a keystore. An authorized staff member at ACME Software would verify Alice's identity and generate a signed certificate as follows: java CertificateSigner -keystore acmesoft.store -alias acmeroot -infile alice.cert -outfile alice_signedby_acmeroot.cert The certificate signer program must have access to the ACME Software keystore, and the staff member must know the keystore password. Clearly, this is a sensitive operation. Now Alice gives the file alice_signedby_acmeroot.cert file to Cindy and to anyone else in ACME Software. Alternatively, ACME Software can simply store the file in a company directory. Remember, this file contains Alice's public key and an assertion by ACME Software that this key really belongs to Alice. NOTE
When Cindy imports the signed certificate into her keystore, the keystore verifies that the key was signed by a trusted root key that is already present in the keystore and she is not asked to verify the certificate fingerprint. Once Cindy has added the root certificate and the certificates of the people who regularly send her documents, she never has to worry about the keystore again. CAUTION
Now let us look at the source code of Example 9-19. First, we load the keystore. The getInstance factory method of the KeyStore class creates a keystore instance of the appropriate type. The keytool-generated keystore has a type of "JKS". The provider of this keystore type is "SUN". KeyStore store = KeyStore.getInstance("JKS", "SUN"); Now, we load the keystore data. The load method requires an input stream and a password. Note that the password is specified as a char[] array, not a string. The JVM can keep strings around for a long time before they are garbage-collected. Hackers could potentially find these strings, for example, by examining the contents of swap files. However, character arrays can be cleared immediately after they are used. Here is the code for loading the keystore. Note that we fill the password with spaces immediately after use. InputStream in = . . .; char[] password = . . .; store.load(in, password); Arrays.fill(password, ' '); in.close(); Next, we use the getKey method to retrieve the private key for signing. The getKey method requires the key alias and key password. Its return type is Key, and we cast it to PrivateKey since we know that the retrieved key is a private key. char[] keyPassword = . . .; PrivateKey issuerPrivateKey = (PrivateKey) store.getKey(alias, keyPassword); Arrays.fill(keyPassword, ' '); Now we are ready to read in the certificate that needs to be signed. The CertificateFactory class can read in certificates from an input stream. First, we get a factory of the appropriate type: CertificateFactory factory = CertificateFactory.getInstance("X.509"); Then, we call the generateCertificate method with an input stream: in = new FileInputStream(inname); X509Certificate inCert = (X509Certificate) factory.generateCertificate(in); in.close(); The return type of the generateCertificate method is the abstract Certificate class that is the superclass of concrete classes such as X509Certificate. Because we know that the input file actually contains an X509Certificate, we use a cast. CAUTION
The purpose of this program is to sign the bytes in the certificate. We retrieve the bytes with the getTBSCertificate method: byte[] inCertBytes = inCert.getTBSCertificate(); Next, we need the distinguished name of the issuer, which we insert into the signed certificate. The name is stored in the issuer certificate in the keystore. We fetch certificates from the keystore with the getCertificate method. Since certificates are public information, we supply only the alias, not a password. X509Certificate issuerCert = (X509Certificate) store.getCertificate(alias); The getCertificate method has return type Certificate, but once again we know that the returned value is actually X509Certificate. We obtain the issuer identity from the certificate by calling the getSubjectDN method. That method returns an object of some type that implements the Principal interface. Conceptually, a principal is a real-world entity such as a person, organization, or company. Principal issuer = issuerCert.getSubjectDN(); We also retrieve the name of the signing algorithm. String issuerSigAlg = issuerCert.getSigAlgName() Now we must leave the realm of the standard security library. The standard library contains no methods for generating new certificates. Libraries for certificate generation are available from third-party vendors such as RSA Security, Inc., and the Legion of Bouncy Castle (http://www.bouncycastle.org). However, we use the classes in the sun.security.x509 package. The usual caveats apply. This package might not be supplied by third-party vendors of Java technology, and Sun Microsystems might change the behavior at any time. The following code segment carries out these steps:
We do not discuss the use of these classes in detail because we do not recommend the use of Sun libraries for production code. A future version of the JDK may contain classes for certificate generation. In the meantime, you may want to rely on third-party libraries or simply use existing certificate generation software. Example 9-19. CertificateSigner.java1. import java.io.*; 2. import java.security.*; 3. import java.security.cert.*; 4. import java.util.*; 5. 6. import sun.security.x509.X509CertInfo; 7. import sun.security.x509.X509CertImpl; 8. import sun.security.x509.X500Name; 9. import sun.security.x509.CertificateIssuerName; 10. 11. /** 12. This program signs a certificate, using the private key of 13. another certificate in a keystore. 14. */ 15. public class CertificateSigner 16. { 17. public static void main(String[] args) 18. { 19. String ksname = null; // the keystore name 20. String alias = null; // the private key alias 21. String inname = null; // the input file name 22. String outname = null; // the output file name 23. for (int i = 0; i < args.length; i += 2) 24. { 25. if (args[i].equals("-keystore")) 26. ksname = args[i + 1]; 27. else if (args[i].equals("-alias")) 28. alias = args[i + 1]; 29. else if (args[i].equals("-infile")) 30. inname = args[i + 1]; 31. else if (args[i].equals("-outfile")) 32. outname = args[i + 1]; 33. else usage(); 34. } 35. 36. if (ksname == null || alias == null || inname == null || outname == null) usage(); 37. 38. try 39. { 40. PushbackReader console = new PushbackReader(new InputStreamReader(System.in)); 41. 42. KeyStore store = KeyStore.getInstance("JKS", "SUN"); 43. InputStream in = new FileInputStream(ksname); 44. System.out.print("Keystore password: "); 45. System.out.flush(); 46. char[] password = readPassword(console); 47. store.load(in, password); 48. Arrays.fill(password, ' '); 49. in.close(); 50. 51. System.out.print("Key password for " + alias + ": "); 52. System.out.flush(); 53. char[] keyPassword = readPassword(console); 54. PrivateKey issuerPrivateKey = (PrivateKey) store.getKey(alias, keyPassword); 55. Arrays.fill(keyPassword, ' '); 56. 57. if (issuerPrivateKey == null) error("No such private key"); 58. 59. in = new FileInputStream(inname); 60. 61. CertificateFactory factory = CertificateFactory.getInstance("X.509"); 62. 63. X509Certificate inCert = (X509Certificate) factory.generateCertificate(in); 64. in.close(); 65. byte[] inCertBytes = inCert.getTBSCertificate(); 66. 67. X509Certificate issuerCert = (X509Certificate) store.getCertificate(alias); 68. Principal issuer = issuerCert.getSubjectDN(); 69. String issuerSigAlg = issuerCert.getSigAlgName(); 70. 71. FileOutputStream out = new FileOutputStream(outname); 72. 73. X509CertInfo info = new X509CertInfo(inCertBytes); 74. info.set(X509CertInfo.ISSUER, new CertificateIssuerName((X500Name) issuer)); 75. 76. X509CertImpl outCert = new X509CertImpl(info); 77. outCert.sign(issuerPrivateKey, issuerSigAlg); 78. outCert.derEncode(out); 79. 80. out.close(); 81. } 82. catch (Exception e) 83. { 84. e.printStackTrace(); 85. } 86. } 87. 88. /** 89. Reads a password. 90. @param in the reader from which to read the password 91. @return an array of characters containing the password 92. */ 93. public static char[] readPassword(PushbackReader in) 94. throws IOException 95. { 96. final int MAX_PASSWORD_LENGTH = 100; 97. int length = 0; 98. char[] buffer = new char[MAX_PASSWORD_LENGTH]; 99. 100. while (true) 101. { 102. int ch = in.read(); 103. if (ch == '\r' || ch == '\n' || ch == -1 104. || length == MAX_PASSWORD_LENGTH) 105. { 106. if (ch == '\r') // handle DOS "\r\n" line ends 107. { 108. ch = in.read(); 109. if (ch != '\n' && ch != -1) in.unread(ch); 110. } 111. char[] password = new char[length]; 112. System.arraycopy(buffer, 0, password, 0, length); 113. Arrays.fill(buffer, ' '); 114. return password; 115. } 116. else 117. { 118. buffer[length] = (char) ch; 119. length++; 120. } 121. } 122. } 123. 124. /** 125. Prints an error message and exits. 126. @param message 127. */ 128. public static void error(String message) 129. { 130. System.out.println(message); 131. System.exit(1); 132. } 133. 134. /** 135. Prints a usage message and exits. 136. */ 137. public static void usage() 138. { 139. System.out.println("Usage: java CertificateSigner" 140. + " -keystore keyStore -alias issuerKeyAlias" 141. + " -infile inputFile -outfile outputFile"); 142. System.exit(1); 143. } 144. } java.security.KeyStore 1.2
java.security.cert.CertificateFactory 1.2
java.security.cert.Certificate 1.2
java.security.cert.X509Certificate 1.2
|
|