/*
* Copyright (c) 2004 David Flanagan. All rights reserved.
* This code is from the book Java Examples in a Nutshell, 3nd Edition.
* It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
* You may study, use, and modify it for any non-commercial purpose,
* including teaching and use in open-source projects.
* You may distribute it non-commercially as long as you retain this notice.
* For a commercial use license, or to purchase the book,
* please visit http://www.davidflanagan.com/javaexamples3.
*/
package je3.security;
import java.security.*;
import java.io.*;
import java.util.*;
/**
* This program creates a manifest file for the specified files, or verifies
* an existing manifest file. By default the manifest file is named
* MANIFEST, but the -m option can be used to override this. The -v
* option specifies that the manifest should be verified. Verification is
* also the default option if no files are specified.
**/
public class Manifest {
public static void main(String[] args) throws Exception {
// Set the default values of the command-line arguments
boolean verify = false; // Verify manifest or create one?
String manifestfile = "MANIFEST"; // Manifest file name
String digestAlgorithm = "MD5"; // Algorithm for message digests
String signername = null; // Signer. No sig. by default
String signatureAlgorithm = "DSA"; // Algorithm for digital sig.
String password = null; // Private keys are protected
File keystoreFile = null; // Where are keys stored
String keystoreType = null; // What kind of keystore
String keystorePassword = null; // How to access keystore
List filelist = new ArrayList(); // The files to digest
// Parse the command-line arguments, overriding the defaults above
for(int i = 0; i < args.length; i++) {
if (args[i].equals("-v")) verify = true;
else if (args[i].equals("-m")) manifestfile = args[++i];
else if (args[i].equals("-da")&& !verify)
digestAlgorithm = args[++i];
else if (args[i].equals("-s")&& !verify)
signername = args[++i];
else if (args[i].equals("-sa")&& !verify)
signatureAlgorithm = args[++i];
else if (args[i].equals("-p"))
password = args[++i];
else if (args[i].equals("-keystore"))
keystoreFile = new File(args[++i]);
else if (args[i].equals("-keystoreType"))
keystoreType = args[++i];
else if (args[i].equals("-keystorePassword"))
keystorePassword = args[++i];
else if (!verify) filelist.add(args[i]);
else throw new IllegalArgumentException(args[i]);
}
// If certain arguments weren't supplied, get default values.
if (keystoreFile == null) {
File dir = new File(System.getProperty("user.home"));
keystoreFile = new File(dir, ".keystore");
}
if (keystoreType == null) keystoreType = KeyStore.getDefaultType();
if (keystorePassword == null) keystorePassword = password;
if (!verify && signername != null && password == null) {
System.out.println("Use -p to specify a password.");
return;
}
// Get the keystore we'll use for signing or verifying signatures
// If no password was provided, then assume we won't be dealing with
// signatures, and skip the keystore.
KeyStore keystore = null;
if (keystorePassword != null) {
keystore = KeyStore.getInstance(keystoreType);
InputStream in =
new BufferedInputStream(new FileInputStream(keystoreFile));
keystore.load(in, keystorePassword.toCharArray());
}
// If -v was specified or no file were given, verify a manifest
// Otherwise, create a new manifest for the specified files
if (verify || (filelist.size() == 0)) verify(manifestfile, keystore);
else create(manifestfile, digestAlgorithm,
signername, signatureAlgorithm,
keystore, password, filelist);
}
/**
* This method creates a manifest file with the specified name, for
* the specified vector of files, using the named message digest
* algorithm. If signername is non-null, it adds a digital signature
* to the manifest, using the named signature algorithm. This method can
* throw a bunch of exceptions.
**/
public static void create(String manifestfile, String digestAlgorithm,
String signername, String signatureAlgorithm,
KeyStore keystore, String password,
List filelist)
throws NoSuchAlgorithmException, InvalidKeyException,
SignatureException, KeyStoreException,
UnrecoverableKeyException, IOException
{
// For computing a signature, we have to process the files in a fixed,
// repeatable order, so sort them alphabetically.
Collections.sort(filelist);
int numfiles = filelist.size();
Properties manifest = new Properties(), metadata = new Properties();
MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
Signature signature = null;
byte[] digest;
// If a signer name was specified, then prepare to sign the manifest
if (signername != null) {
// Get a Signature object
signature = Signature.getInstance(signatureAlgorithm);
// Look up the private key of the signer from the keystore
PrivateKey key = (PrivateKey)
keystore.getKey(signername, password.toCharArray());
// No prepare to create a signature for the specified signer
signature.initSign(key);
}
// Now, loop through the files, in a well-known alphabetical order
System.out.print("Computing message digests");
for(int i = 0; i < numfiles; i++) {
String filename = (String)filelist.get(i);
// Compute the digest for each, and skip files that don't exist.
try { digest = getFileDigest(filename, md); }
catch (IOException e) {
System.err.println("\nSkipping " + filename + ": " + e);
continue;
}
// If we're computing a signature, use the bytes of the filename
// and of the digest as part of the data to sign.
if (signature != null) {
signature.update(filename.getBytes());
signature.update(digest);
}
// Store the filename and the encoded digest bytes in the manifest
manifest.put(filename, hexEncode(digest));
System.out.print('.');
System.out.flush();
}
// If a signer was specified, compute signature for the manifest
byte[] signaturebytes = null;
if (signature != null) {
System.out.print("done\nComputing digital signature...");
System.out.flush();
// Compute the digital signature by encrypting a message digest of
// all the bytes passed to the update() method using the private
// key of the signer. This is a time consuming operation.
signaturebytes = signature.sign();
}
// Tell the user what comes next
System.out.print("done\nWriting manifest...");
System.out.flush();
// Store some metadata about this manifest, including the name of the
// message digest algorithm it uses
metadata.put("__META.DIGESTALGORITHM", digestAlgorithm);
// If we're signing the manifest, store some more metadata
if (signername != null) {
// Store the name of the signer
metadata.put("__META.SIGNER", signername);
// Store the name of the algorithm
metadata.put("__META.SIGNATUREALGORITHM", signatureAlgorithm);
// And generate the signature, encode it, and store it
metadata.put("__META.SIGNATURE", hexEncode(signaturebytes));
}
// Now, save the manifest data and the metadata to the manifest file
FileOutputStream f = new FileOutputStream(manifestfile);
manifest.store(f, "Manifest message digests");
metadata.store(f, "Manifest metadata");
System.out.println("done");
}
/**
* This method verifies the digital signature of the named manifest
* file, if it has one, and if that verification succeeds, it verifies
* the message digest of each file in filelist that is also named in the
* manifest. This method can throw a bunch of exceptions
**/
public static void verify(String manifestfile, KeyStore keystore)
throws NoSuchAlgorithmException, SignatureException,
InvalidKeyException, KeyStoreException, IOException
{
Properties manifest = new Properties();
manifest.load(new FileInputStream(manifestfile));
String digestAlgorithm =
manifest.getProperty("__META.DIGESTALGORITHM");
String signername = manifest.getProperty("__META.SIGNER");
String signatureAlgorithm =
manifest.getProperty("__META.SIGNATUREALGORITHM");
String hexsignature = manifest.getProperty("__META.SIGNATURE");
// Get a list of filenames in the manifest.
List files = new ArrayList();
Enumeration names = manifest.propertyNames();
while(names.hasMoreElements()) {
String s = (String)names.nextElement();
if (!s.startsWith("__META")) files.add(s);
}
int numfiles = files.size();
// If we've got a signature but no keystore, warn the user
if (signername != null && keystore == null)
System.out.println("Can't verify digital signature without " +
"a keystore.");
// If the manifest contained metadata about a digital signature, then
// verify that signature first
if (signername != null && keystore != null) {
System.out.print("Verifying digital signature...");
System.out.flush();
// To verify the signature, we must process the files in exactly
// the same order we did when we created the signature. We
// guarantee this order by sorting the filenames.
Collections.sort(files);
// Create a Signature object to do signature verification with.
// Initialize it with the signer's public key from the keystore
Signature signature = Signature.getInstance(signatureAlgorithm);
PublicKey publickey =
keystore.getCertificate(signername).getPublicKey();
signature.initVerify(publickey);
// Now loop through these files in their known sorted order For
// each one, send the bytes of the filename and of the digest to
// the signature object for use in computing the signature. It is
// important that this be done in exactly the same order when
// verifying the signature as it was done when creating the
// signature.
for(int i = 0; i < numfiles; i++) {
String filename = (String) files.get(i);
signature.update(filename.getBytes());
signature.update(hexDecode(manifest.getProperty(filename)));
}
// Now decode the signature read from the manifest file and pass
// it to the verify() method of the signature object. If the
// signature is not verified, print an error message and exit.
if (!signature.verify(hexDecode(hexsignature))) {
System.out.println("\nManifest has an invalid signature");
System.exit(0);
}
// Tell the user we're done with this lengthy computation
System.out.println("verified.");
}
// Tell the user we're starting the next phase of verification
System.out.print("Verifying file message digests");
System.out.flush();
// Get a MessageDigest object to compute digests
MessageDigest md = MessageDigest.getInstance(digestAlgorithm);
// Loop through all files
for(int i = 0; i < numfiles; i++) {
String filename = (String)files.get(i);
// Look up the encoded digest from the manifest file
String hexdigest = manifest.getProperty(filename);
// Compute the digest for the file.
byte[] digest;
try { digest = getFileDigest(filename, md); }
catch (IOException e) {
System.out.println("\nSkipping " + filename + ": " + e);
continue;
}
// Encode the computed digest and compare it to the encoded digest
// from the manifest. If they are not equal, print an error
// message.
if (!hexdigest.equals(hexEncode(digest)))
System.out.println("\nFile '" + filename +
"' failed verification.");
// Send one dot of output for each file we process. Since
// computing message digests takes some time, this lets the user
// know that the program is functioning and making progress
System.out.print(".");
System.out.flush();
}
// And tell the user we're done with verification.
System.out.println("done.");
}
/**
* This convenience method is used by both create() and verify(). It
* reads the contents of a named file and computes a message digest
* for it, using the specified MessageDigest object.
**/
public static byte[] getFileDigest(String filename, MessageDigest md)
throws IOException {
// Make sure there is nothing left behind in the MessageDigest
md.reset();
// Create a stream to read from the file and compute the digest
DigestInputStream in =
new DigestInputStream(new FileInputStream(filename),md);
// Read to the end of the file, discarding everything we read.
// The DigestInputStream automatically passes all the bytes read to
// the update() method of the MessageDigest
while(in.read(buffer) != -1) /* do nothing */ ;
// Finally, compute and return the digest value.
return md.digest();
}
/** This static buffer is used by getFileDigest() above */
public static byte[] buffer = new byte[4096];
/** This array is used to convert from bytes to hexadecimal numbers */
static final char[] digits = { '0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* A convenience method to convert an array of bytes to a String. We do
* this simply by converting each byte to two hexadecimal digits.
* Something like Base 64 encoding is more compact, but harder to encode.
**/
public static String hexEncode(byte[] bytes) {
StringBuffer s = new StringBuffer(bytes.length * 2);
for(int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
s.append(digits[(b& 0xf0) >> 4]);
s.append(digits[b& 0x0f]);
}
return s.toString();
}
/**
* A convenience method to convert in the other direction, from a string
* of hexadecimal digits to an array of bytes.
**/
public static byte[] hexDecode(String s) throws IllegalArgumentException {
try {
int len = s.length();
byte[] r = new byte[len/2];
for(int i = 0; i < r.length; i++) {
int digit1 = s.charAt(i*2), digit2 = s.charAt(i*2 + 1);
if ((digit1 >= '0')&& (digit1 <= '9')) digit1 -= '0';
else if ((digit1 >= 'a')&& (digit1 <= 'f')) digit1 -= 'a' - 10;
if ((digit2 >= '0')&& (digit2 <= '9')) digit2 -= '0';
else if ((digit2 >= 'a')&& (digit2 <= 'f')) digit2 -= 'a' - 10;
r[i] = (byte)((digit1 << 4) + digit2);
}
return r;
}
catch (Exception e) {
throw new IllegalArgumentException("hexDecode(): invalid input");
}
}
}
|