| /* JarFile.java - Representation of a jar file | |
| Copyright (C) 2000, 2003, 2004, 2005, 2006 Free Software Foundation, Inc. | |
| This file is part of GNU Classpath. | |
| GNU Classpath is free software; you can redistribute it and/or modify | |
| it under the terms of the GNU General Public License as published by | |
| the Free Software Foundation; either version 2, or (at your option) | |
| any later version. | |
| GNU Classpath is distributed in the hope that it will be useful, but | |
| WITHOUT ANY WARRANTY; without even the implied warranty of | |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| General Public License for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with GNU Classpath; see the file COPYING. If not, write to the | |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
| 02110-1301 USA. | |
| Linking this library statically or dynamically with other modules is | |
| making a combined work based on this library. Thus, the terms and | |
| conditions of the GNU General Public License cover the whole | |
| combination. | |
| As a special exception, the copyright holders of this library give you | |
| permission to link this library with independent modules to produce an | |
| executable, regardless of the license terms of these independent | |
| modules, and to copy and distribute the resulting executable under | |
| terms of your choice, provided that you also meet, for each linked | |
| independent module, the terms and conditions of the license of that | |
| module. An independent module is a module which is not derived from | |
| or based on this library. If you modify this library, you may extend | |
| this exception to your version of the library, but you are not | |
| obligated to do so. If you do not wish to do so, delete this | |
| exception statement from your version. */ | |
| 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 gnu.java.security.provider.Gnu; | |
| 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.Enumeration; | |
| import java.util.HashMap; | |
| import java.util.HashSet; | |
| import java.util.Iterator; | |
| 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; | |
| /** | |
| * Representation of a jar file. | |
| * <p> | |
| * 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). | |
| * | |
| * @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 | |
| { | |
| // Fields | |
| /** 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"; | |
| /** | |
| * The security provider to use for signature verification. | |
| * We need a known fallback to be able to read any signed jar file | |
| * (which might contain the user selected security provider). | |
| * This is package-private to avoid accessor methods for inner classes. | |
| */ | |
| static final Gnu provider = new Gnu(); | |
| // 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. | |
| */ | |
| private Manifest manifest; | |
| /** Whether to verify the manifest and all entries. */ | |
| 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 | |
| /** | |
| * Creates a new JarFile. All jar entries are verified (when a Manifest file | |
| * for this JarFile exists). You need to actually open and read the complete | |
| * jar entry (with <code>getInputStream()</code>) to check its signature. | |
| * | |
| * @param fileName the name of the file to open | |
| * @exception FileNotFoundException if the fileName cannot be found | |
| * @exception IOException if another IO exception occurs while reading | |
| */ | |
| public JarFile(String fileName) throws FileNotFoundException, IOException | |
| { | |
| this(fileName, true); | |
| } | |
| /** | |
| * Creates a new JarFile. If verify is true then all jar entries are | |
| * verified (when a Manifest file for this JarFile exists). You need to | |
| * actually open and read the complete jar entry | |
| * (with <code>getInputStream()</code>) to check its signature. | |
| * | |
| * @param fileName the name of the file to open | |
| * @param verify checks manifest and entries when true and a manifest | |
| * exists, when false no checks are made | |
| * @exception FileNotFoundException if the fileName cannot be found | |
| * @exception IOException if another IO exception occurs while reading | |
| */ | |
| public JarFile(String fileName, boolean verify) throws | |
| FileNotFoundException, IOException | |
| { | |
| super(fileName); | |
| if (verify) | |
| { | |
| manifest = readManifest(); | |
| verify(); | |
| } | |
| } | |
| /** | |
| * Creates a new JarFile. All jar entries are verified (when a Manifest file | |
| * for this JarFile exists). You need to actually open and read the complete | |
| * jar entry (with <code>getInputStream()</code>) to check its signature. | |
| * | |
| * @param file the file to open as a jar file | |
| * @exception FileNotFoundException if the file does not exits | |
| * @exception IOException if another IO exception occurs while reading | |
| */ | |
| public JarFile(File file) throws FileNotFoundException, IOException | |
| { | |
| this(file, true); | |
| } | |
| /** | |
| * Creates a new JarFile. If verify is true then all jar entries are | |
| * verified (when a Manifest file for this JarFile exists). You need to | |
| * actually open and read the complete jar entry | |
| * (with <code>getInputStream()</code>) to check its signature. | |
| * | |
| * @param file the file to open to open as a jar file | |
| * @param verify checks manifest and entries when true and a manifest | |
| * exists, when false no checks are made | |
| * @exception FileNotFoundException if file does not exist | |
| * @exception IOException if another IO exception occurs while reading | |
| */ | |
| public JarFile(File file, boolean verify) throws FileNotFoundException, | |
| IOException | |
| { | |
| super(file); | |
| if (verify) | |
| { | |
| manifest = readManifest(); | |
| verify(); | |
| } | |
| } | |
| /** | |
| * Creates a new JarFile with the indicated mode. If verify is true then | |
| * all jar entries are verified (when a Manifest file for this JarFile | |
| * exists). You need to actually open and read the complete jar entry | |
| * (with <code>getInputStream()</code>) to check its signature. | |
| * manifest and if the manifest exists and verify is true verfies it. | |
| * | |
| * @param file the file to open to open as a jar file | |
| * @param verify checks manifest and entries when true and a manifest | |
| * exists, when false no checks are made | |
| * @param mode either ZipFile.OPEN_READ or | |
| * (ZipFile.OPEN_READ | ZipFile.OPEN_DELETE) | |
| * @exception FileNotFoundException if the file does not exist | |
| * @exception IOException if another IO exception occurs while reading | |
| * @exception IllegalArgumentException when given an illegal mode | |
| * | |
| * @since 1.3 | |
| */ | |
| public JarFile(File file, boolean verify, int mode) throws | |
| FileNotFoundException, IOException, IllegalArgumentException | |
| { | |
| super(file, mode); | |
| if (verify) | |
| { | |
| manifest = readManifest(); | |
| verify(); | |
| } | |
| } | |
| // Methods | |
| /** | |
| * XXX - should verify the manifest file | |
| */ | |
| private void verify() | |
| { | |
| // only check if manifest is not null | |
| if (manifest == null) | |
| { | |
| verify = false; | |
| return; | |
| } | |
| verify = true; | |
| // XXX - verify manifest | |
| } | |
| /** | |
| * Parses and returns the manifest if it exists, otherwise returns null. | |
| */ | |
| private Manifest readManifest() | |
| { | |
| try | |
| { | |
| ZipEntry manEntry = super.getEntry(MANIFEST_NAME); | |
| if (manEntry != null) | |
| { | |
| InputStream in = super.getInputStream(manEntry); | |
| manifestRead = true; | |
| return new Manifest(in); | |
| } | |
| else | |
| { | |
| manifestRead = true; | |
| return null; | |
| } | |
| } | |
| catch (IOException ioe) | |
| { | |
| manifestRead = true; | |
| return null; | |
| } | |
| } | |
| /** | |
| * Returns a enumeration of all the entries in the JarFile. | |
| * Note that also the Jar META-INF entries are returned. | |
| * | |
| * @exception IllegalStateException when the JarFile is already closed | |
| */ | |
| public Enumeration entries() throws IllegalStateException | |
| { | |
| return new JarEnumeration(super.entries(), this); | |
| } | |
| /** | |
| * Wraps a given Zip Entries Enumeration. For every zip entry a | |
| * JarEntry is created and the corresponding Attributes are looked up. | |
| */ | |
| private static class JarEnumeration implements Enumeration | |
| { | |
| private final Enumeration entries; | |
| private final JarFile jarfile; | |
| JarEnumeration(Enumeration e, JarFile f) | |
| { | |
| entries = e; | |
| jarfile = f; | |
| } | |
| public boolean hasMoreElements() | |
| { | |
| return entries.hasMoreElements(); | |
| } | |
| public Object nextElement() | |
| { | |
| ZipEntry zip = (ZipEntry) entries.nextElement(); | |
| JarEntry jar = new JarEntry(zip); | |
| Manifest manifest; | |
| try | |
| { | |
| manifest = jarfile.getManifest(); | |
| } | |
| catch (IOException ioe) | |
| { | |
| manifest = null; | |
| } | |
| if (manifest != null) | |
| { | |
| jar.attr = manifest.getAttributes(jar.getName()); | |
| } | |
| synchronized(jarfile) | |
| { | |
| if (jarfile.verify && !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()]); | |
| } | |
| } | |
| return jar; | |
| } | |
| } | |
| /** | |
| * XXX | |
| * It actually returns a JarEntry not a zipEntry | |
| * @param name XXX | |
| */ | |
| public synchronized ZipEntry getEntry(String name) | |
| { | |
| ZipEntry entry = super.getEntry(name); | |
| if (entry != null) | |
| { | |
| JarEntry jarEntry = new JarEntry(entry); | |
| Manifest manifest; | |
| try | |
| { | |
| manifest = getManifest(); | |
| } | |
| catch (IOException ioe) | |
| { | |
| manifest = null; | |
| } | |
| if (manifest != null) | |
| { | |
| jarEntry.attr = manifest.getAttributes(name); | |
| } | |
| if (verify && !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()]); | |
| } | |
| return jarEntry; | |
| } | |
| return null; | |
| } | |
| /** | |
| * 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. | |
| * @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); | |
| } | |
| } | |
| /** | |
| * Returns the JarEntry that belongs to the name if such an entry | |
| * exists in the JarFile. Returns null otherwise | |
| * Convenience method that just casts the result from <code>getEntry</code> | |
| * to a JarEntry. | |
| * | |
| * @param name the jar entry name to look up | |
| * @return the JarEntry if it exists, null otherwise | |
| */ | |
| public JarEntry getJarEntry(String name) | |
| { | |
| return (JarEntry) getEntry(name); | |
| } | |
| /** | |
| * Returns the manifest for this JarFile or null when the JarFile does not | |
| * contain a manifest file. | |
| */ | |
| public synchronized Manifest getManifest() throws IOException | |
| { | |
| if (!manifestRead) | |
| manifest = readManifest(); | |
| return manifest; | |
| } | |
| // Only called with lock on this JarFile. | |
| // Package private for use in inner classes. | |
| 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", provider); | |
| } | |
| else if (alg.equals(RSA_ENCRYPTION_OID)) | |
| { | |
| OID hash = signerInfo.getDigestAlgorithmId(); | |
| if (hash.equals(MD2_OID)) | |
| sig = Signature.getInstance("md2WithRsaEncryption", provider); | |
| else if (hash.equals(MD4_OID)) | |
| sig = Signature.getInstance("md4WithRsaEncryption", provider); | |
| else if (hash.equals(MD5_OID)) | |
| sig = Signature.getInstance("md5WithRsaEncryption", provider); | |
| else if (hash.equals(SHA1_OID)) | |
| sig = Signature.getInstance("sha1WithRsaEncryption", provider); | |
| 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 | |
| { | |
| ZipEntry e = super.getEntry(entry); | |
| if (e == null) | |
| { | |
| if (DEBUG) | |
| debug("verifyHashes: no entry '" + entry + "'"); | |
| return false; | |
| } | |
| entryBytes = readManifestEntry(e); | |
| } | |
| 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, provider); | |
| 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 | |
| { | |
| int length = key.length() - DIGEST_KEY_SUFFIX.length(); | |
| String alg = key.substring(0, length); | |
| md.add(MessageDigest.getInstance(alg, provider)); | |
| } | |
| 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); | |
| } | |
| } | |
| } | |
| } |