--- /home/tromey/gnu/Nightly/classpath/classpath/java/util/jar/JarFile.java 2004-11-14 02:20:10.000000000 -0700 +++ java/util/jar/JarFile.java 2004-04-23 02:18:17.000000000 -0600 @@ -37,38 +37,11 @@ package java.util.jar; -import gnu.java.io.Base64InputStream; -import gnu.java.security.OID; -import gnu.java.security.pkcs.PKCS7SignedData; -import gnu.java.security.pkcs.SignerInfo; - -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; -import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; - -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.CRLException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; import java.util.Enumeration; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; @@ -79,11 +52,11 @@ * Note that this class is not a subclass of java.io.File but a subclass of * java.util.zip.ZipFile and you can only read JarFiles with it (although * there are constructors that take a File object). + *

+ * XXX - verification of Manifest signatures is not yet implemented. * * @since 1.2 * @author Mark Wielaard (mark@klomp.org) - * @author Casey Marshall (csm@gnu.org) wrote the certificate and entry - * verification code. */ public class JarFile extends ZipFile { @@ -92,29 +65,6 @@ /** The name of the manifest entry: META-INF/MANIFEST.MF */ public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; - /** The META-INF directory entry. */ - private static final String META_INF = "META-INF/"; - - /** The suffix for PKCS7 DSA signature entries. */ - private static final String PKCS7_DSA_SUFFIX = ".DSA"; - - /** The suffix for PKCS7 RSA signature entries. */ - private static final String PKCS7_RSA_SUFFIX = ".RSA"; - - /** The suffix for digest attributes. */ - private static final String DIGEST_KEY_SUFFIX = "-Digest"; - - /** The suffix for signature files. */ - private static final String SF_SUFFIX = ".SF"; - - // Signature OIDs. - private static final OID MD2_OID = new OID("1.2.840.113549.2.2"); - private static final OID MD4_OID = new OID("1.2.840.113549.2.4"); - private static final OID MD5_OID = new OID("1.2.840.113549.2.5"); - private static final OID SHA1_OID = new OID("1.3.14.3.2.26"); - private static final OID DSA_ENCRYPTION_OID = new OID("1.2.840.10040.4.1"); - private static final OID RSA_ENCRYPTION_OID = new OID("1.2.840.113549.1.1.1"); - /** * The manifest of this file, if any, otherwise null. * Read when first needed. @@ -122,34 +72,11 @@ private Manifest manifest; /** Whether to verify the manifest and all entries. */ - boolean verify; + private boolean verify; /** Whether the has already been loaded. */ private boolean manifestRead = false; - /** Whether the signature files have been loaded. */ - boolean signaturesRead = false; - - /** - * A map between entry names and booleans, signaling whether or - * not that entry has been verified. - * Only be accessed with lock on this JarFile*/ - HashMap verified = new HashMap(); - - /** - * A mapping from entry name to certificates, if any. - * Only accessed with lock on this JarFile. - */ - HashMap entryCerts; - - static boolean DEBUG = false; - static void debug(Object msg) - { - System.err.print(JarFile.class.getName()); - System.err.print(" >>> "); - System.err.println(msg); - } - // Constructors /** @@ -308,23 +235,22 @@ */ public Enumeration entries() throws IllegalStateException { - return new JarEnumeration(super.entries(), this); + return new JarEnumeration(super.entries()); } /** * Wraps a given Zip Entries Enumeration. For every zip entry a * JarEntry is created and the corresponding Attributes are looked up. + * XXX - Should also look up the certificates. */ - private static class JarEnumeration implements Enumeration + private class JarEnumeration implements Enumeration { private final Enumeration entries; - private final JarFile jarfile; - JarEnumeration(Enumeration e, JarFile f) + JarEnumeration(Enumeration e) { entries = e; - jarfile = f; } public boolean hasMoreElements() @@ -339,7 +265,7 @@ Manifest manifest; try { - manifest = jarfile.getManifest(); + manifest = getManifest(); } catch (IOException ioe) { @@ -350,36 +276,7 @@ { jar.attr = manifest.getAttributes(jar.getName()); } - - synchronized(jarfile) - { - if (!jarfile.signaturesRead) - try - { - jarfile.readSignatures(); - } - catch (IOException ioe) - { - if (JarFile.DEBUG) - { - JarFile.debug(ioe); - ioe.printStackTrace(); - } - jarfile.signaturesRead = true; // fudge it. - } - - // Include the certificates only if we have asserted that the - // signatures are valid. This means the certificates will not be - // available if the entry hasn't been read yet. - if (jarfile.entryCerts != null - && jarfile.verified.get(zip.getName()) == Boolean.TRUE) - { - Set certs = (Set) jarfile.entryCerts.get(jar.getName()); - if (certs != null) - jar.certs = (Certificate[]) - certs.toArray(new Certificate[certs.size()]); - } - } + // XXX jar.certs return jar; } } @@ -389,7 +286,7 @@ * It actually returns a JarEntry not a zipEntry * @param name XXX */ - public synchronized ZipEntry getEntry(String name) + public ZipEntry getEntry(String name) { ZipEntry entry = super.getEntry(name); if (entry != null) @@ -408,33 +305,7 @@ if (manifest != null) { jarEntry.attr = manifest.getAttributes(name); - } - - if (!signaturesRead) - try - { - readSignatures(); - } - catch (IOException ioe) - { - if (DEBUG) - { - debug(ioe); - ioe.printStackTrace(); - } - signaturesRead = true; - } - // See the comments in the JarEnumeration for why we do this - // check. - if (DEBUG) - debug("entryCerts=" + entryCerts + " verified " + name - + " ? " + verified.get(name)); - if (entryCerts != null && verified.get(name) == Boolean.TRUE) - { - Set certs = (Set) entryCerts.get(name); - if (certs != null) - jarEntry.certs = (Certificate[]) - certs.toArray(new Certificate[certs.size()]); + // XXX jarEntry.certs } return jarEntry; } @@ -442,32 +313,15 @@ } /** - * Returns an input stream for the given entry. If configured to - * verify entries, the input stream returned will verify them while - * the stream is read, but only on the first time. - * - * @param entry The entry to get the input stream for. + * XXX should verify the inputstream + * @param entry XXX * @exception ZipException XXX * @exception IOException XXX */ public synchronized InputStream getInputStream(ZipEntry entry) throws ZipException, IOException { - // If we haven't verified the hash, do it now. - if (!verified.containsKey(entry.getName()) && verify) - { - if (DEBUG) - debug("reading and verifying " + entry); - return new EntryInputStream(entry, super.getInputStream(entry), this); - } - else - { - if (DEBUG) - debug("reading already verified entry " + entry); - if (verify && verified.get(entry.getName()) == Boolean.FALSE) - throw new ZipException("digest for " + entry + " is invalid"); - return super.getInputStream(entry); - } + return super.getInputStream(entry); // XXX verify } /** @@ -488,566 +342,11 @@ * Returns the manifest for this JarFile or null when the JarFile does not * contain a manifest file. */ - public synchronized Manifest getManifest() throws IOException + public Manifest getManifest() throws IOException { if (!manifestRead) manifest = readManifest(); return manifest; } - - // Only called with lock on this JarFile. - private void readSignatures() throws IOException - { - Map pkcs7Dsa = new HashMap(); - Map pkcs7Rsa = new HashMap(); - Map sigFiles = new HashMap(); - - // Phase 1: Read all signature files. These contain the user - // certificates as well as the signatures themselves. - for (Enumeration e = super.entries(); e.hasMoreElements(); ) - { - ZipEntry ze = (ZipEntry) e.nextElement(); - String name = ze.getName(); - if (name.startsWith(META_INF)) - { - String alias = name.substring(META_INF.length()); - if (alias.lastIndexOf('.') >= 0) - alias = alias.substring(0, alias.lastIndexOf('.')); - - if (name.endsWith(PKCS7_DSA_SUFFIX) || name.endsWith(PKCS7_RSA_SUFFIX)) - { - if (DEBUG) - debug("reading PKCS7 info from " + name + ", alias=" + alias); - PKCS7SignedData sig = null; - try - { - sig = new PKCS7SignedData(super.getInputStream(ze)); - } - catch (CertificateException ce) - { - IOException ioe = new IOException("certificate parsing error"); - ioe.initCause(ce); - throw ioe; - } - catch (CRLException crle) - { - IOException ioe = new IOException("CRL parsing error"); - ioe.initCause(crle); - throw ioe; - } - if (name.endsWith(PKCS7_DSA_SUFFIX)) - pkcs7Dsa.put(alias, sig); - else if (name.endsWith(PKCS7_RSA_SUFFIX)) - pkcs7Rsa.put(alias, sig); - } - else if (name.endsWith(SF_SUFFIX)) - { - if (DEBUG) - debug("reading signature file for " + alias + ": " + name); - Manifest sf = new Manifest(super.getInputStream(ze)); - sigFiles.put(alias, sf); - if (DEBUG) - debug("result: " + sf); - } - } - } - - // Phase 2: verify the signatures on any signature files. - Set validCerts = new HashSet(); - Map entryCerts = new HashMap(); - for (Iterator it = sigFiles.entrySet().iterator(); it.hasNext(); ) - { - int valid = 0; - Map.Entry e = (Map.Entry) it.next(); - String alias = (String) e.getKey(); - - PKCS7SignedData sig = (PKCS7SignedData) pkcs7Dsa.get(alias); - if (sig != null) - { - Certificate[] certs = sig.getCertificates(); - Set signerInfos = sig.getSignerInfos(); - for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); ) - verify(certs, (SignerInfo) it2.next(), alias, validCerts); - } - - sig = (PKCS7SignedData) pkcs7Rsa.get(alias); - if (sig != null) - { - Certificate[] certs = sig.getCertificates(); - Set signerInfos = sig.getSignerInfos(); - for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); ) - verify(certs, (SignerInfo) it2.next(), alias, validCerts); - } - - // It isn't a signature for anything. Punt it. - if (validCerts.isEmpty()) - { - it.remove(); - continue; - } - - entryCerts.put(e.getValue(), new HashSet(validCerts)); - validCerts.clear(); - } - - // Phase 3: verify the signature file signatures against the manifest, - // mapping the entry name to the target certificates. - this.entryCerts = new HashMap(); - for (Iterator it = entryCerts.entrySet().iterator(); it.hasNext(); ) - { - Map.Entry e = (Map.Entry) it.next(); - Manifest sigfile = (Manifest) e.getKey(); - Map entries = sigfile.getEntries(); - Set certificates = (Set) e.getValue(); - - for (Iterator it2 = entries.entrySet().iterator(); it2.hasNext(); ) - { - Map.Entry e2 = (Map.Entry) it2.next(); - String entryname = String.valueOf(e2.getKey()); - Attributes attr = (Attributes) e2.getValue(); - if (verifyHashes(entryname, attr)) - { - if (DEBUG) - debug("entry " + entryname + " has certificates " + certificates); - Set s = (Set) this.entryCerts.get(entryname); - if (s != null) - s.addAll(certificates); - else - this.entryCerts.put(entryname, new HashSet(certificates)); - } - } - } - - signaturesRead = true; - } - - /** - * Tell if the given signer info is over the given alias's signature file, - * given one of the certificates specified. - */ - private void verify(Certificate[] certs, SignerInfo signerInfo, - String alias, Set validCerts) - { - Signature sig = null; - try - { - OID alg = signerInfo.getDigestEncryptionAlgorithmId(); - if (alg.equals(DSA_ENCRYPTION_OID)) - { - if (!signerInfo.getDigestAlgorithmId().equals(SHA1_OID)) - return; - sig = Signature.getInstance("SHA1withDSA"); - } - else if (alg.equals(RSA_ENCRYPTION_OID)) - { - OID hash = signerInfo.getDigestAlgorithmId(); - if (hash.equals(MD2_OID)) - sig = Signature.getInstance("md2WithRsaEncryption"); - else if (hash.equals(MD4_OID)) - sig = Signature.getInstance("md4WithRsaEncryption"); - else if (hash.equals(MD5_OID)) - sig = Signature.getInstance("md5WithRsaEncryption"); - else if (hash.equals(SHA1_OID)) - sig = Signature.getInstance("sha1WithRsaEncryption"); - else - return; - } - else - { - if (DEBUG) - debug("unsupported signature algorithm: " + alg); - return; - } - } - catch (NoSuchAlgorithmException nsae) - { - if (DEBUG) - { - debug(nsae); - nsae.printStackTrace(); - } - return; - } - ZipEntry sigFileEntry = super.getEntry(META_INF + alias + SF_SUFFIX); - if (sigFileEntry == null) - return; - for (int i = 0; i < certs.length; i++) - { - if (!(certs[i] instanceof X509Certificate)) - continue; - X509Certificate cert = (X509Certificate) certs[i]; - if (!cert.getIssuerX500Principal().equals(signerInfo.getIssuer()) || - !cert.getSerialNumber().equals(signerInfo.getSerialNumber())) - continue; - try - { - sig.initVerify(cert.getPublicKey()); - InputStream in = super.getInputStream(sigFileEntry); - if (in == null) - continue; - byte[] buf = new byte[1024]; - int len = 0; - while ((len = in.read(buf)) != -1) - sig.update(buf, 0, len); - if (sig.verify(signerInfo.getEncryptedDigest())) - { - if (DEBUG) - debug("signature for " + cert.getSubjectDN() + " is good"); - validCerts.add(cert); - } - } - catch (IOException ioe) - { - continue; - } - catch (InvalidKeyException ike) - { - continue; - } - catch (SignatureException se) - { - continue; - } - } - } - - /** - * Verifies that the digest(s) in a signature file were, in fact, made - * over the manifest entry for ENTRY. - * - * @param entry The entry name. - * @param attr The attributes from the signature file to verify. - */ - private boolean verifyHashes(String entry, Attributes attr) - { - int verified = 0; - - // The bytes for ENTRY's manifest entry, which are signed in the - // signature file. - byte[] entryBytes = null; - try - { - entryBytes = readManifestEntry(super.getEntry(entry)); - } - catch (IOException ioe) - { - if (DEBUG) - { - debug(ioe); - ioe.printStackTrace(); - } - return false; - } - - for (Iterator it = attr.entrySet().iterator(); it.hasNext(); ) - { - Map.Entry e = (Map.Entry) it.next(); - String key = String.valueOf(e.getKey()); - if (!key.endsWith(DIGEST_KEY_SUFFIX)) - continue; - String alg = key.substring(0, key.length() - DIGEST_KEY_SUFFIX.length()); - try - { - byte[] hash = Base64InputStream.decode((String) e.getValue()); - MessageDigest md = MessageDigest.getInstance(alg); - md.update(entryBytes); - byte[] hash2 = md.digest(); - if (DEBUG) - debug("verifying SF entry " + entry + " alg: " + md.getAlgorithm() - + " expect=" + new java.math.BigInteger(hash).toString(16) - + " comp=" + new java.math.BigInteger(hash2).toString(16)); - if (!Arrays.equals(hash, hash2)) - return false; - verified++; - } - catch (IOException ioe) - { - if (DEBUG) - { - debug(ioe); - ioe.printStackTrace(); - } - return false; - } - catch (NoSuchAlgorithmException nsae) - { - if (DEBUG) - { - debug(nsae); - nsae.printStackTrace(); - } - return false; - } - } - - // We have to find at least one valid digest. - return verified > 0; - } - - /** - * Read the raw bytes that comprise a manifest entry. We can't use the - * Manifest object itself, because that loses information (such as line - * endings, and order of entries). - */ - private byte[] readManifestEntry(ZipEntry entry) throws IOException - { - InputStream in = super.getInputStream(super.getEntry(MANIFEST_NAME)); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - byte[] target = ("Name: " + entry.getName()).getBytes(); - int t = 0, c, prev = -1, state = 0, l = -1; - - while ((c = in.read()) != -1) - { -// if (DEBUG) -// debug("read " -// + (c == '\n' ? "\\n" : (c == '\r' ? "\\r" : String.valueOf((char) c))) -// + " state=" + state + " prev=" -// + (prev == '\n' ? "\\n" : (prev == '\r' ? "\\r" : String.valueOf((char) prev))) -// + " t=" + t + (t < target.length ? (" target[t]=" + (char) target[t]) : "") -// + " l=" + l); - switch (state) - { - - // Step 1: read until we find the "target" bytes: the start - // of the entry we need to read. - case 0: - if (((byte) c) != target[t]) - t = 0; - else - { - t++; - if (t == target.length) - { - out.write(target); - state = 1; - } - } - break; - - // Step 2: assert that there is a newline character after - // the "target" bytes. - case 1: - if (c != '\n' && c != '\r') - { - out.reset(); - t = 0; - state = 0; - } - else - { - out.write(c); - state = 2; - } - break; - - // Step 3: read this whole entry, until we reach an empty - // line. - case 2: - if (c == '\n') - { - out.write(c); - // NL always terminates a line. - if (l == 0 || (l == 1 && prev == '\r')) - return out.toByteArray(); - l = 0; - } - else - { - // Here we see a blank line terminated by a CR, - // followed by the next entry. Technically, `c' should - // always be 'N' at this point. - if (l == 1 && prev == '\r') - return out.toByteArray(); - out.write(c); - l++; - } - prev = c; - break; - - default: - throw new RuntimeException("this statement should be unreachable"); - } - } - - // The last entry, with a single CR terminating the line. - if (state == 2 && prev == '\r' && l == 0) - return out.toByteArray(); - - // We should not reach this point, we didn't find the entry (or, possibly, - // it is the last entry and is malformed). - throw new IOException("could not find " + entry + " in manifest"); - } - - /** - * A utility class that verifies jar entries as they are read. - */ - private static class EntryInputStream extends FilterInputStream - { - private final JarFile jarfile; - private final long length; - private long pos; - private final ZipEntry entry; - private final byte[][] hashes; - private final MessageDigest[] md; - private boolean checked; - - EntryInputStream(final ZipEntry entry, - final InputStream in, - final JarFile jar) - throws IOException - { - super(in); - this.entry = entry; - this.jarfile = jar; - - length = entry.getSize(); - pos = 0; - checked = false; - - Attributes attr; - Manifest manifest = jarfile.getManifest(); - if (manifest != null) - attr = manifest.getAttributes(entry.getName()); - else - attr = null; - if (DEBUG) - debug("verifying entry " + entry + " attr=" + attr); - if (attr == null) - { - hashes = new byte[0][]; - md = new MessageDigest[0]; - } - else - { - List hashes = new LinkedList(); - List md = new LinkedList(); - for (Iterator it = attr.entrySet().iterator(); it.hasNext(); ) - { - Map.Entry e = (Map.Entry) it.next(); - String key = String.valueOf(e.getKey()); - if (key == null) - continue; - if (!key.endsWith(DIGEST_KEY_SUFFIX)) - continue; - hashes.add(Base64InputStream.decode((String) e.getValue())); - try - { - md.add(MessageDigest.getInstance - (key.substring(0, key.length() - DIGEST_KEY_SUFFIX.length()))); - } - catch (NoSuchAlgorithmException nsae) - { - IOException ioe = new IOException("no such message digest: " + key); - ioe.initCause(nsae); - throw ioe; - } - } - if (DEBUG) - debug("digests=" + md); - this.hashes = (byte[][]) hashes.toArray(new byte[hashes.size()][]); - this.md = (MessageDigest[]) md.toArray(new MessageDigest[md.size()]); - } - } - - public boolean markSupported() - { - return false; - } - - public void mark(int readLimit) - { - } - - public void reset() - { - } - - public int read() throws IOException - { - int b = super.read(); - if (b == -1) - { - eof(); - return -1; - } - for (int i = 0; i < md.length; i++) - md[i].update((byte) b); - pos++; - if (length > 0 && pos >= length) - eof(); - return b; - } - - public int read(byte[] buf, int off, int len) throws IOException - { - int count = super.read(buf, off, (int) Math.min(len, (length != 0 - ? length - pos - : Integer.MAX_VALUE))); - if (count == -1 || (length > 0 && pos >= length)) - { - eof(); - return -1; - } - for (int i = 0; i < md.length; i++) - md[i].update(buf, off, count); - pos += count; - if (length != 0 && pos >= length) - eof(); - return count; - } - - public int read(byte[] buf) throws IOException - { - return read(buf, 0, buf.length); - } - - public long skip(long bytes) throws IOException - { - byte[] b = new byte[1024]; - long amount = 0; - while (amount < bytes) - { - int l = read(b, 0, (int) Math.min(b.length, bytes - amount)); - if (l == -1) - break; - amount += l; - } - return amount; - } - - private void eof() throws IOException - { - if (checked) - return; - checked = true; - for (int i = 0; i < md.length; i++) - { - byte[] hash = md[i].digest(); - if (DEBUG) - debug("verifying " + md[i].getAlgorithm() + " expect=" - + new java.math.BigInteger(hashes[i]).toString(16) - + " comp=" + new java.math.BigInteger(hash).toString(16)); - if (!Arrays.equals(hash, hashes[i])) - { - synchronized(jarfile) - { - if (DEBUG) - debug(entry + " could NOT be verified"); - jarfile.verified.put(entry.getName(), Boolean.FALSE); - } - return; - // XXX ??? what do we do here? - // throw new ZipException("message digest mismatch"); - } - } - - synchronized(jarfile) - { - if (DEBUG) - debug(entry + " has been VERIFIED"); - jarfile.verified.put(entry.getName(), Boolean.TRUE); - } - } - } }