1 /* JarFile.java - Representation of a jar file
2 Copyright (C) 2000, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
4 This file is part of GNU Classpath.
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING. If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library. Thus, the terms and
23 conditions of the GNU General Public License cover the whole
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module. An independent module is a module which is not derived from
33 or based on this library. If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so. If you do not wish to do so, delete this
36 exception statement from your version. */
39 package java.util.jar;
41 import gnu.java.io.Base64InputStream;
42 import gnu.java.security.OID;
43 import gnu.java.security.pkcs.PKCS7SignedData;
44 import gnu.java.security.pkcs.SignerInfo;
45 import gnu.java.security.provider.Gnu;
47 import java.io.ByteArrayOutputStream;
49 import java.io.FileNotFoundException;
50 import java.io.FilterInputStream;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.security.InvalidKeyException;
54 import java.security.MessageDigest;
55 import java.security.NoSuchAlgorithmException;
56 import java.security.Signature;
57 import java.security.SignatureException;
58 import java.security.cert.CRLException;
59 import java.security.cert.Certificate;
60 import java.security.cert.CertificateException;
61 import java.security.cert.X509Certificate;
62 import java.util.Arrays;
63 import java.util.Enumeration;
64 import java.util.HashMap;
65 import java.util.HashSet;
66 import java.util.Iterator;
67 import java.util.LinkedList;
68 import java.util.List;
71 import java.util.regex.Matcher;
72 import java.util.regex.Pattern;
73 import java.util.zip.ZipEntry;
74 import java.util.zip.ZipException;
75 import java.util.zip.ZipFile;
78 * Representation of a jar file.
80 * Note that this class is not a subclass of java.io.File but a subclass of
81 * java.util.zip.ZipFile and you can only read JarFiles with it (although
82 * there are constructors that take a File object).
85 * @author Mark Wielaard (mark@klomp.org)
86 * @author Casey Marshall (csm@gnu.org) wrote the certificate and entry
89 public class JarFile extends ZipFile
93 /** The name of the manifest entry: META-INF/MANIFEST.MF */
94 public static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
96 /** The META-INF directory entry. */
97 private static final String META_INF = "META-INF/";
99 /** The suffix for PKCS7 DSA signature entries. */
100 private static final String PKCS7_DSA_SUFFIX = ".DSA";
102 /** The suffix for PKCS7 RSA signature entries. */
103 private static final String PKCS7_RSA_SUFFIX = ".RSA";
105 /** The suffix for digest attributes. */
106 private static final String DIGEST_KEY_SUFFIX = "-Digest";
108 /** The suffix for signature files. */
109 private static final String SF_SUFFIX = ".SF";
112 * The security provider to use for signature verification.
113 * We need a known fallback to be able to read any signed jar file
114 * (which might contain the user selected security provider).
115 * This is package-private to avoid accessor methods for inner classes.
117 static final Gnu provider = new Gnu();
120 private static final OID MD2_OID = new OID("1.2.840.113549.2.2");
121 private static final OID MD4_OID = new OID("1.2.840.113549.2.4");
122 private static final OID MD5_OID = new OID("1.2.840.113549.2.5");
123 private static final OID SHA1_OID = new OID("1.3.14.3.2.26");
124 private static final OID DSA_ENCRYPTION_OID = new OID("1.2.840.10040.4.1");
125 private static final OID RSA_ENCRYPTION_OID = new OID("1.2.840.113549.1.1.1");
128 * The manifest of this file, if any, otherwise null.
129 * Read when first needed.
131 private Manifest manifest;
133 /** Whether to verify the manifest and all entries. */
136 /** Whether the has already been loaded. */
137 private boolean manifestRead = false;
139 /** Whether the signature files have been loaded. */
140 boolean signaturesRead = false;
143 * A map between entry names and booleans, signaling whether or
144 * not that entry has been verified.
145 * Only be accessed with lock on this JarFile*/
146 HashMap verified = new HashMap();
149 * A mapping from entry name to certificates, if any.
150 * Only accessed with lock on this JarFile.
155 * A {@link Map} of message digest algorithm names to their implementation.
156 * Used to reduce object (algorithm implementation) instantiation.
158 private HashMap digestAlgorithms = new HashMap();
160 static boolean DEBUG = false;
161 static void debug(Object msg)
163 System.err.print(JarFile.class.getName());
164 System.err.print(" >>> ");
165 System.err.println(msg);
171 * Creates a new JarFile. All jar entries are verified (when a Manifest file
172 * for this JarFile exists). You need to actually open and read the complete
173 * jar entry (with <code>getInputStream()</code>) to check its signature.
175 * @param fileName the name of the file to open
176 * @exception FileNotFoundException if the fileName cannot be found
177 * @exception IOException if another IO exception occurs while reading
179 public JarFile(String fileName) throws FileNotFoundException, IOException
181 this(fileName, true);
185 * Creates a new JarFile. If verify is true then all jar entries are
186 * verified (when a Manifest file for this JarFile exists). You need to
187 * actually open and read the complete jar entry
188 * (with <code>getInputStream()</code>) to check its signature.
190 * @param fileName the name of the file to open
191 * @param verify checks manifest and entries when true and a manifest
192 * exists, when false no checks are made
193 * @exception FileNotFoundException if the fileName cannot be found
194 * @exception IOException if another IO exception occurs while reading
196 public JarFile(String fileName, boolean verify) throws
197 FileNotFoundException, IOException
202 manifest = readManifest();
208 * Creates a new JarFile. All jar entries are verified (when a Manifest file
209 * for this JarFile exists). You need to actually open and read the complete
210 * jar entry (with <code>getInputStream()</code>) to check its signature.
212 * @param file the file to open as a jar file
213 * @exception FileNotFoundException if the file does not exits
214 * @exception IOException if another IO exception occurs while reading
216 public JarFile(File file) throws FileNotFoundException, IOException
222 * Creates a new JarFile. If verify is true then all jar entries are
223 * verified (when a Manifest file for this JarFile exists). You need to
224 * actually open and read the complete jar entry
225 * (with <code>getInputStream()</code>) to check its signature.
227 * @param file the file to open to open as a jar file
228 * @param verify checks manifest and entries when true and a manifest
229 * exists, when false no checks are made
230 * @exception FileNotFoundException if file does not exist
231 * @exception IOException if another IO exception occurs while reading
233 public JarFile(File file, boolean verify) throws FileNotFoundException,
239 manifest = readManifest();
245 * Creates a new JarFile with the indicated mode. If verify is true then
246 * all jar entries are verified (when a Manifest file for this JarFile
247 * exists). You need to actually open and read the complete jar entry
248 * (with <code>getInputStream()</code>) to check its signature.
249 * manifest and if the manifest exists and verify is true verfies it.
251 * @param file the file to open to open as a jar file
252 * @param verify checks manifest and entries when true and a manifest
253 * exists, when false no checks are made
254 * @param mode either ZipFile.OPEN_READ or
255 * (ZipFile.OPEN_READ | ZipFile.OPEN_DELETE)
256 * @exception FileNotFoundException if the file does not exist
257 * @exception IOException if another IO exception occurs while reading
258 * @exception IllegalArgumentException when given an illegal mode
262 public JarFile(File file, boolean verify, int mode) throws
263 FileNotFoundException, IOException, IllegalArgumentException
268 manifest = readManifest();
276 * XXX - should verify the manifest file
278 private void verify()
280 // only check if manifest is not null
281 if (manifest == null)
288 // XXX - verify manifest
292 * Parses and returns the manifest if it exists, otherwise returns null.
294 private Manifest readManifest()
298 ZipEntry manEntry = super.getEntry(MANIFEST_NAME);
299 if (manEntry != null)
301 InputStream in = super.getInputStream(manEntry);
303 return new Manifest(in);
311 catch (IOException ioe)
319 * Returns a enumeration of all the entries in the JarFile.
320 * Note that also the Jar META-INF entries are returned.
322 * @exception IllegalStateException when the JarFile is already closed
324 public Enumeration<JarEntry> entries() throws IllegalStateException
326 return new JarEnumeration(super.entries(), this);
330 * Wraps a given Zip Entries Enumeration. For every zip entry a
331 * JarEntry is created and the corresponding Attributes are looked up.
333 private static class JarEnumeration implements Enumeration<JarEntry>
336 private final Enumeration<? extends ZipEntry> entries;
337 private final JarFile jarfile;
339 JarEnumeration(Enumeration<? extends ZipEntry> e, JarFile f)
345 public boolean hasMoreElements()
347 return entries.hasMoreElements();
350 public JarEntry nextElement()
352 ZipEntry zip = (ZipEntry) entries.nextElement();
353 JarEntry jar = new JarEntry(zip);
357 manifest = jarfile.getManifest();
359 catch (IOException ioe)
364 if (manifest != null)
366 jar.attr = manifest.getAttributes(jar.getName());
369 synchronized(jarfile)
371 if (jarfile.verify && !jarfile.signaturesRead)
374 jarfile.readSignatures();
376 catch (IOException ioe)
381 ioe.printStackTrace();
383 jarfile.signaturesRead = true; // fudge it.
386 jar.jarfile = jarfile;
393 * It actually returns a JarEntry not a zipEntry
396 public synchronized ZipEntry getEntry(String name)
398 ZipEntry entry = super.getEntry(name);
401 JarEntry jarEntry = new JarEntry(entry);
405 manifest = getManifest();
407 catch (IOException ioe)
412 if (manifest != null)
414 jarEntry.attr = manifest.getAttributes(name);
417 if (verify && !signaturesRead)
422 catch (IOException ioe)
427 ioe.printStackTrace();
429 signaturesRead = true;
431 jarEntry.jarfile = this;
438 * Returns an input stream for the given entry. If configured to
439 * verify entries, the input stream returned will verify them while
440 * the stream is read, but only on the first time.
442 * @param entry The entry to get the input stream for.
443 * @exception ZipException XXX
444 * @exception IOException XXX
446 public synchronized InputStream getInputStream(ZipEntry entry) throws
447 ZipException, IOException
449 // If we haven't verified the hash, do it now.
450 if (!verified.containsKey(entry.getName()) && verify)
453 debug("reading and verifying " + entry);
454 return new EntryInputStream(entry, super.getInputStream(entry), this);
459 debug("reading already verified entry " + entry);
460 if (verify && verified.get(entry.getName()) == Boolean.FALSE)
461 throw new ZipException("digest for " + entry + " is invalid");
462 return super.getInputStream(entry);
467 * Returns the JarEntry that belongs to the name if such an entry
468 * exists in the JarFile. Returns null otherwise
469 * Convenience method that just casts the result from <code>getEntry</code>
472 * @param name the jar entry name to look up
473 * @return the JarEntry if it exists, null otherwise
475 public JarEntry getJarEntry(String name)
477 return (JarEntry) getEntry(name);
481 * Returns the manifest for this JarFile or null when the JarFile does not
482 * contain a manifest file.
484 public synchronized Manifest getManifest() throws IOException
487 manifest = readManifest();
492 // Only called with lock on this JarFile.
493 // Package private for use in inner classes.
494 void readSignatures() throws IOException
496 Map pkcs7Dsa = new HashMap();
497 Map pkcs7Rsa = new HashMap();
498 Map sigFiles = new HashMap();
500 // Phase 1: Read all signature files. These contain the user
501 // certificates as well as the signatures themselves.
502 for (Enumeration e = super.entries(); e.hasMoreElements(); )
504 ZipEntry ze = (ZipEntry) e.nextElement();
505 String name = ze.getName();
506 if (name.startsWith(META_INF))
508 String alias = name.substring(META_INF.length());
509 if (alias.lastIndexOf('.') >= 0)
510 alias = alias.substring(0, alias.lastIndexOf('.'));
512 if (name.endsWith(PKCS7_DSA_SUFFIX) || name.endsWith(PKCS7_RSA_SUFFIX))
515 debug("reading PKCS7 info from " + name + ", alias=" + alias);
516 PKCS7SignedData sig = null;
519 sig = new PKCS7SignedData(super.getInputStream(ze));
521 catch (CertificateException ce)
523 IOException ioe = new IOException("certificate parsing error");
527 catch (CRLException crle)
529 IOException ioe = new IOException("CRL parsing error");
533 if (name.endsWith(PKCS7_DSA_SUFFIX))
534 pkcs7Dsa.put(alias, sig);
535 else if (name.endsWith(PKCS7_RSA_SUFFIX))
536 pkcs7Rsa.put(alias, sig);
538 else if (name.endsWith(SF_SUFFIX))
541 debug("reading signature file for " + alias + ": " + name);
542 Manifest sf = new Manifest(super.getInputStream(ze));
543 sigFiles.put(alias, sf);
545 debug("result: " + sf);
550 // Phase 2: verify the signatures on any signature files.
551 Set validCerts = new HashSet();
552 Map entryCerts = new HashMap();
553 for (Iterator it = sigFiles.entrySet().iterator(); it.hasNext(); )
556 Map.Entry e = (Map.Entry) it.next();
557 String alias = (String) e.getKey();
559 PKCS7SignedData sig = (PKCS7SignedData) pkcs7Dsa.get(alias);
562 Certificate[] certs = sig.getCertificates();
563 Set signerInfos = sig.getSignerInfos();
564 for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); )
565 verify(certs, (SignerInfo) it2.next(), alias, validCerts);
568 sig = (PKCS7SignedData) pkcs7Rsa.get(alias);
571 Certificate[] certs = sig.getCertificates();
572 Set signerInfos = sig.getSignerInfos();
573 for (Iterator it2 = signerInfos.iterator(); it2.hasNext(); )
574 verify(certs, (SignerInfo) it2.next(), alias, validCerts);
577 // It isn't a signature for anything. Punt it.
578 if (validCerts.isEmpty())
584 entryCerts.put(e.getValue(), new HashSet(validCerts));
588 // Read the manifest into a HashMap (String fileName, String entry)
589 // The fileName might be split into multiple lines in the manifest.
590 // Such additional lines will start with a space.
591 InputStream in = super.getInputStream(super.getEntry(MANIFEST_NAME));
592 ByteArrayOutputStream baStream = new ByteArrayOutputStream();
593 byte[] ba = new byte[1024];
596 int len = in.read(ba);
599 baStream.write(ba, 0, len);
603 HashMap hmManifestEntries = new HashMap();
604 Pattern p = Pattern.compile("Name: (.+?\r?\n(?: .+?\r?\n)*)"
605 + ".+?-Digest: .+?\r?\n\r?\n");
606 Matcher m = p.matcher(baStream.toString());
609 String fileName = m.group(1).replaceAll("\r?\n ?", "");
610 hmManifestEntries.put(fileName, m.group());
613 // Phase 3: verify the signature file signatures against the manifest,
614 // mapping the entry name to the target certificates.
615 this.entryCerts = new HashMap();
616 for (Iterator it = entryCerts.entrySet().iterator(); it.hasNext(); )
618 Map.Entry e = (Map.Entry) it.next();
619 Manifest sigfile = (Manifest) e.getKey();
620 Map entries = sigfile.getEntries();
621 Set certificates = (Set) e.getValue();
623 for (Iterator it2 = entries.entrySet().iterator(); it2.hasNext(); )
625 Map.Entry e2 = (Map.Entry) it2.next();
626 String entryname = String.valueOf(e2.getKey());
627 Attributes attr = (Attributes) e2.getValue();
628 if (verifyHashes(entryname, attr, hmManifestEntries))
631 debug("entry " + entryname + " has certificates " + certificates);
632 Set s = (Set) this.entryCerts.get(entryname);
634 s.addAll(certificates);
636 this.entryCerts.put(entryname, new HashSet(certificates));
641 signaturesRead = true;
645 * Tell if the given signer info is over the given alias's signature file,
646 * given one of the certificates specified.
648 private void verify(Certificate[] certs, SignerInfo signerInfo,
649 String alias, Set validCerts)
651 Signature sig = null;
654 OID alg = signerInfo.getDigestEncryptionAlgorithmId();
655 if (alg.equals(DSA_ENCRYPTION_OID))
657 if (!signerInfo.getDigestAlgorithmId().equals(SHA1_OID))
659 sig = Signature.getInstance("SHA1withDSA", provider);
661 else if (alg.equals(RSA_ENCRYPTION_OID))
663 OID hash = signerInfo.getDigestAlgorithmId();
664 if (hash.equals(MD2_OID))
665 sig = Signature.getInstance("md2WithRsaEncryption", provider);
666 else if (hash.equals(MD4_OID))
667 sig = Signature.getInstance("md4WithRsaEncryption", provider);
668 else if (hash.equals(MD5_OID))
669 sig = Signature.getInstance("md5WithRsaEncryption", provider);
670 else if (hash.equals(SHA1_OID))
671 sig = Signature.getInstance("sha1WithRsaEncryption", provider);
678 debug("unsupported signature algorithm: " + alg);
682 catch (NoSuchAlgorithmException nsae)
687 nsae.printStackTrace();
691 ZipEntry sigFileEntry = super.getEntry(META_INF + alias + SF_SUFFIX);
692 if (sigFileEntry == null)
694 for (int i = 0; i < certs.length; i++)
696 if (!(certs[i] instanceof X509Certificate))
698 X509Certificate cert = (X509Certificate) certs[i];
699 if (!cert.getIssuerX500Principal().equals(signerInfo.getIssuer()) ||
700 !cert.getSerialNumber().equals(signerInfo.getSerialNumber()))
704 sig.initVerify(cert.getPublicKey());
705 InputStream in = super.getInputStream(sigFileEntry);
708 byte[] buf = new byte[1024];
710 while ((len = in.read(buf)) != -1)
711 sig.update(buf, 0, len);
712 if (sig.verify(signerInfo.getEncryptedDigest()))
715 debug("signature for " + cert.getSubjectDN() + " is good");
716 validCerts.add(cert);
719 catch (IOException ioe)
723 catch (InvalidKeyException ike)
727 catch (SignatureException se)
735 * Verifies that the digest(s) in a signature file were, in fact, made over
736 * the manifest entry for ENTRY.
738 * @param entry The entry name.
739 * @param attr The attributes from the signature file to verify.
740 * @param hmManifestEntries Mappings of Jar file entry names to their manifest
741 * entry text; i.e. the base-64 encoding of their
743 private boolean verifyHashes(String entry, Attributes attr,
744 HashMap hmManifestEntries)
748 String stringEntry = (String) hmManifestEntries.get(entry);
749 if (stringEntry == null)
752 debug("could not find " + entry + " in manifest");
755 // The bytes for ENTRY's manifest entry, which are signed in the
757 byte[] entryBytes = stringEntry.getBytes();
759 for (Iterator it = attr.entrySet().iterator(); it.hasNext(); )
761 Map.Entry e = (Map.Entry) it.next();
762 String key = String.valueOf(e.getKey());
763 if (!key.endsWith(DIGEST_KEY_SUFFIX))
765 String alg = key.substring(0, key.length() - DIGEST_KEY_SUFFIX.length());
768 byte[] hash = Base64InputStream.decode((String) e.getValue());
769 MessageDigest md = (MessageDigest) digestAlgorithms.get(alg);
772 md = MessageDigest.getInstance(alg, provider);
773 digestAlgorithms.put(alg, md);
776 byte[] hash2 = md.digest(entryBytes);
778 debug("verifying SF entry " + entry + " alg: " + md.getAlgorithm()
779 + " expect=" + new java.math.BigInteger(hash).toString(16)
780 + " comp=" + new java.math.BigInteger(hash2).toString(16));
781 if (!Arrays.equals(hash, hash2))
785 catch (IOException ioe)
790 ioe.printStackTrace();
794 catch (NoSuchAlgorithmException nsae)
799 nsae.printStackTrace();
805 // We have to find at least one valid digest.
810 * A utility class that verifies jar entries as they are read.
812 private static class EntryInputStream extends FilterInputStream
814 private final JarFile jarfile;
815 private final long length;
817 private final ZipEntry entry;
818 private final byte[][] hashes;
819 private final MessageDigest[] md;
820 private boolean checked;
822 EntryInputStream(final ZipEntry entry,
823 final InputStream in,
831 length = entry.getSize();
836 Manifest manifest = jarfile.getManifest();
837 if (manifest != null)
838 attr = manifest.getAttributes(entry.getName());
842 debug("verifying entry " + entry + " attr=" + attr);
845 hashes = new byte[0][];
846 md = new MessageDigest[0];
850 List hashes = new LinkedList();
851 List md = new LinkedList();
852 for (Iterator it = attr.entrySet().iterator(); it.hasNext(); )
854 Map.Entry e = (Map.Entry) it.next();
855 String key = String.valueOf(e.getKey());
858 if (!key.endsWith(DIGEST_KEY_SUFFIX))
860 hashes.add(Base64InputStream.decode((String) e.getValue()));
863 int length = key.length() - DIGEST_KEY_SUFFIX.length();
864 String alg = key.substring(0, length);
865 md.add(MessageDigest.getInstance(alg, provider));
867 catch (NoSuchAlgorithmException nsae)
869 IOException ioe = new IOException("no such message digest: " + key);
875 debug("digests=" + md);
876 this.hashes = (byte[][]) hashes.toArray(new byte[hashes.size()][]);
877 this.md = (MessageDigest[]) md.toArray(new MessageDigest[md.size()]);
881 public boolean markSupported()
886 public void mark(int readLimit)
894 public int read() throws IOException
896 int b = super.read();
902 for (int i = 0; i < md.length; i++)
903 md[i].update((byte) b);
905 if (length > 0 && pos >= length)
910 public int read(byte[] buf, int off, int len) throws IOException
912 int count = super.read(buf, off, (int) Math.min(len, (length != 0
914 : Integer.MAX_VALUE)));
915 if (count == -1 || (length > 0 && pos >= length))
920 for (int i = 0; i < md.length; i++)
921 md[i].update(buf, off, count);
923 if (length != 0 && pos >= length)
928 public int read(byte[] buf) throws IOException
930 return read(buf, 0, buf.length);
933 public long skip(long bytes) throws IOException
935 byte[] b = new byte[1024];
937 while (amount < bytes)
939 int l = read(b, 0, (int) Math.min(b.length, bytes - amount));
947 private void eof() throws IOException
952 for (int i = 0; i < md.length; i++)
954 byte[] hash = md[i].digest();
956 debug("verifying " + md[i].getAlgorithm() + " expect="
957 + new java.math.BigInteger(hashes[i]).toString(16)
958 + " comp=" + new java.math.BigInteger(hash).toString(16));
959 if (!Arrays.equals(hash, hashes[i]))
961 synchronized(jarfile)
964 debug(entry + " could NOT be verified");
965 jarfile.verified.put(entry.getName(), Boolean.FALSE);
968 // XXX ??? what do we do here?
969 // throw new ZipException("message digest mismatch");
973 synchronized(jarfile)
976 debug(entry + " has been VERIFIED");
977 jarfile.verified.put(entry.getName(), Boolean.TRUE);