JarVerifier.java revision 7365de1056414750d0a7d1fdd26025fd247f0d04
1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package java.util.jar;
19
20import java.io.ByteArrayInputStream;
21import java.io.IOException;
22import java.io.OutputStream;
23import java.nio.charset.Charsets;
24import java.security.GeneralSecurityException;
25import java.security.MessageDigest;
26import java.security.NoSuchAlgorithmException;
27import java.security.cert.Certificate;
28import java.util.HashMap;
29import java.util.Hashtable;
30import java.util.Iterator;
31import java.util.Map;
32import java.util.StringTokenizer;
33import java.util.Vector;
34import org.apache.harmony.luni.util.Base64;
35import org.apache.harmony.luni.util.Util;
36import org.apache.harmony.security.utils.JarUtils;
37
38/**
39 * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
40 * the verification of signed JARs. {@code JarFile} and {@code JarInputStream}
41 * objects are expected to have a {@code JarVerifier} instance member which
42 * can be used to carry out the tasks associated with verifying a signed JAR.
43 * These tasks would typically include:
44 * <ul>
45 * <li>verification of all signed signature files
46 * <li>confirmation that all signed data was signed only by the party or parties
47 * specified in the signature block data
48 * <li>verification that the contents of all signature files (i.e. {@code .SF}
49 * files) agree with the JAR entries information found in the JAR manifest.
50 * </ul>
51 */
52class JarVerifier {
53
54    private final String jarName;
55
56    private Manifest man;
57
58    private HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>(5);
59
60    private final Hashtable<String, HashMap<String, Attributes>> signatures = new Hashtable<String, HashMap<String, Attributes>>(
61            5);
62
63    private final Hashtable<String, Certificate[]> certificates = new Hashtable<String, Certificate[]>(
64            5);
65
66    private final Hashtable<String, Certificate[]> verifiedEntries = new Hashtable<String, Certificate[]>();
67
68    int mainAttributesEnd;
69
70    /**
71     * Stores and a hash and a message digest and verifies that massage digest
72     * matches the hash.
73     */
74    class VerifierEntry extends OutputStream {
75
76        private String name;
77
78        private MessageDigest digest;
79
80        private byte[] hash;
81
82        private Certificate[] certificates;
83
84        VerifierEntry(String name, MessageDigest digest, byte[] hash,
85                Certificate[] certificates) {
86            this.name = name;
87            this.digest = digest;
88            this.hash = hash;
89            this.certificates = certificates;
90        }
91
92        /**
93         * Updates a digest with one byte.
94         */
95        @Override
96        public void write(int value) {
97            digest.update((byte) value);
98        }
99
100        /**
101         * Updates a digest with byte array.
102         */
103        @Override
104        public void write(byte[] buf, int off, int nbytes) {
105            digest.update(buf, off, nbytes);
106        }
107
108        /**
109         * Verifies that the digests stored in the manifest match the decrypted
110         * digests from the .SF file. This indicates the validity of the
111         * signing, not the integrity of the file, as it's digest must be
112         * calculated and verified when its contents are read.
113         *
114         * @throws SecurityException
115         *             if the digest value stored in the manifest does <i>not</i>
116         *             agree with the decrypted digest as recovered from the
117         *             <code>.SF</code> file.
118         */
119        void verify() {
120            byte[] d = digest.digest();
121            if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
122                throw invalidDigest(JarFile.MANIFEST_NAME, name, jarName);
123            }
124            verifiedEntries.put(name, certificates);
125        }
126
127    }
128
129    private SecurityException invalidDigest(String signatureFile, String name, String jarName) {
130        throw new SecurityException(signatureFile + " has invalid digest for " + name +
131                " in " + jarName);
132    }
133
134    private SecurityException failedVerification(String jarName, String signatureFile) {
135        throw new SecurityException(jarName + " failed verification of " + signatureFile);
136    }
137
138    /**
139     * Constructs and returns a new instance of {@code JarVerifier}.
140     *
141     * @param name
142     *            the name of the JAR file being verified.
143     */
144    JarVerifier(String name) {
145        jarName = name;
146    }
147
148    /**
149     * Invoked for each new JAR entry read operation from the input
150     * stream. This method constructs and returns a new {@link VerifierEntry}
151     * which contains the certificates used to sign the entry and its hash value
152     * as specified in the JAR MANIFEST format.
153     *
154     * @param name
155     *            the name of an entry in a JAR file which is <b>not</b> in the
156     *            {@code META-INF} directory.
157     * @return a new instance of {@link VerifierEntry} which can be used by
158     *         callers as an {@link OutputStream}.
159     */
160    VerifierEntry initEntry(String name) {
161        // If no manifest is present by the time an entry is found,
162        // verification cannot occur. If no signature files have
163        // been found, do not verify.
164        if (man == null || signatures.size() == 0) {
165            return null;
166        }
167
168        Attributes attributes = man.getAttributes(name);
169        // entry has no digest
170        if (attributes == null) {
171            return null;
172        }
173
174        Vector<Certificate> certs = new Vector<Certificate>();
175        Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures
176                .entrySet().iterator();
177        while (it.hasNext()) {
178            Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
179            HashMap<String, Attributes> hm = entry.getValue();
180            if (hm.get(name) != null) {
181                // Found an entry for entry name in .SF file
182                String signatureFile = entry.getKey();
183
184                Vector<Certificate> newCerts = getSignerCertificates(
185                        signatureFile, certificates);
186                Iterator<Certificate> iter = newCerts.iterator();
187                while (iter.hasNext()) {
188                    certs.add(iter.next());
189                }
190            }
191        }
192
193        // entry is not signed
194        if (certs.size() == 0) {
195            return null;
196        }
197        Certificate[] certificatesArray = new Certificate[certs.size()];
198        certs.toArray(certificatesArray);
199
200        String algorithms = attributes.getValue("Digest-Algorithms");
201        if (algorithms == null) {
202            algorithms = "SHA SHA1";
203        }
204        StringTokenizer tokens = new StringTokenizer(algorithms);
205        while (tokens.hasMoreTokens()) {
206            String algorithm = tokens.nextToken();
207            String hash = attributes.getValue(algorithm + "-Digest");
208            if (hash == null) {
209                continue;
210            }
211            byte[] hashBytes = hash.getBytes(Charsets.ISO_8859_1);
212
213            try {
214                return new VerifierEntry(name, MessageDigest
215                        .getInstance(algorithm), hashBytes, certificatesArray);
216            } catch (NoSuchAlgorithmException e) {
217                // ignored
218            }
219        }
220        return null;
221    }
222
223    /**
224     * Add a new meta entry to the internal collection of data held on each JAR
225     * entry in the {@code META-INF} directory including the manifest
226     * file itself. Files associated with the signing of a JAR would also be
227     * added to this collection.
228     *
229     * @param name
230     *            the name of the file located in the {@code META-INF}
231     *            directory.
232     * @param buf
233     *            the file bytes for the file called {@code name}.
234     * @see #removeMetaEntries()
235     */
236    void addMetaEntry(String name, byte[] buf) {
237        metaEntries.put(Util.toASCIIUpperCase(name), buf);
238    }
239
240    /**
241     * If the associated JAR file is signed, check on the validity of all of the
242     * known signatures.
243     *
244     * @return {@code true} if the associated JAR is signed and an internal
245     *         check verifies the validity of the signature(s). {@code false} if
246     *         the associated JAR file has no entries at all in its {@code
247     *         META-INF} directory. This situation is indicative of an invalid
248     *         JAR file.
249     *         <p>
250     *         Will also return {@code true} if the JAR file is <i>not</i>
251     *         signed.
252     * @throws SecurityException
253     *             if the JAR file is signed and it is determined that a
254     *             signature block file contains an invalid signature for the
255     *             corresponding signature file.
256     */
257    synchronized boolean readCertificates() {
258        if (metaEntries == null) {
259            return false;
260        }
261        Iterator<String> it = metaEntries.keySet().iterator();
262        while (it.hasNext()) {
263            String key = it.next();
264            if (key.endsWith(".DSA") || key.endsWith(".RSA")) {
265                verifyCertificate(key);
266                // Check for recursive class load
267                if (metaEntries == null) {
268                    return false;
269                }
270                it.remove();
271            }
272        }
273        return true;
274    }
275
276    /**
277     * @param certFile
278     */
279    private void verifyCertificate(String certFile) {
280        // Found Digital Sig, .SF should already have been read
281        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.'))
282                + ".SF";
283        byte[] sfBytes = metaEntries.get(signatureFile);
284        if (sfBytes == null) {
285            return;
286        }
287
288        byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
289        // Manifest entry is required for any verifications.
290        if (manifest == null) {
291            return;
292        }
293
294        byte[] sBlockBytes = metaEntries.get(certFile);
295        try {
296            Certificate[] signerCertChain = JarUtils.verifySignature(
297                    new ByteArrayInputStream(sfBytes),
298                    new ByteArrayInputStream(sBlockBytes));
299            /*
300             * Recursive call in loading security provider related class which
301             * is in a signed JAR.
302             */
303            if (null == metaEntries) {
304                return;
305            }
306            if (signerCertChain != null) {
307                certificates.put(signatureFile, signerCertChain);
308            }
309        } catch (IOException e) {
310            return;
311        } catch (GeneralSecurityException e) {
312            throw failedVerification(jarName, signatureFile);
313        }
314
315        // Verify manifest hash in .sf file
316        Attributes attributes = new Attributes();
317        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
318        try {
319            InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION);
320            im.initEntries(entries, null);
321        } catch (IOException e) {
322            return;
323        }
324
325        boolean createdBySigntool = false;
326        String createdBy = attributes.getValue("Created-By");
327        if (createdBy != null) {
328            createdBySigntool = createdBy.indexOf("signtool") != -1;
329        }
330
331        // Use .SF to verify the mainAttributes of the manifest
332        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
333        // file, such as those created before java 1.5, then we ignore
334        // such verification.
335        if (mainAttributesEnd > 0 && !createdBySigntool) {
336            String digestAttribute = "-Digest-Manifest-Main-Attributes";
337            if (!verify(attributes, digestAttribute, manifest, 0, mainAttributesEnd, false, true)) {
338                throw failedVerification(jarName, signatureFile);
339            }
340        }
341
342        // Use .SF to verify the whole manifest.
343        String digestAttribute = createdBySigntool ? "-Digest"
344                : "-Digest-Manifest";
345        if (!verify(attributes, digestAttribute, manifest, 0, manifest.length,
346                false, false)) {
347            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet()
348                    .iterator();
349            while (it.hasNext()) {
350                Map.Entry<String, Attributes> entry = it.next();
351                Manifest.Chunk chunk = man.getChunk(entry.getKey());
352                if (chunk == null) {
353                    return;
354                }
355                if (!verify(entry.getValue(), "-Digest", manifest,
356                        chunk.start, chunk.end, createdBySigntool, false)) {
357                    throw invalidDigest(signatureFile, entry.getKey(), jarName);
358                }
359            }
360        }
361        metaEntries.put(signatureFile, null);
362        signatures.put(signatureFile, entries);
363    }
364
365    /**
366     * Associate this verifier with the specified {@link Manifest} object.
367     *
368     * @param mf
369     *            a {@code java.util.jar.Manifest} object.
370     */
371    void setManifest(Manifest mf) {
372        man = mf;
373    }
374
375    /**
376     * Returns a <code>boolean</code> indication of whether or not the
377     * associated jar file is signed.
378     *
379     * @return {@code true} if the JAR is signed, {@code false}
380     *         otherwise.
381     */
382    boolean isSignedJar() {
383        return certificates.size() > 0;
384    }
385
386    private boolean verify(Attributes attributes, String entry, byte[] data,
387            int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
388        String algorithms = attributes.getValue("Digest-Algorithms");
389        if (algorithms == null) {
390            algorithms = "SHA SHA1";
391        }
392        StringTokenizer tokens = new StringTokenizer(algorithms);
393        while (tokens.hasMoreTokens()) {
394            String algorithm = tokens.nextToken();
395            String hash = attributes.getValue(algorithm + entry);
396            if (hash == null) {
397                continue;
398            }
399
400            MessageDigest md;
401            try {
402                md = MessageDigest.getInstance(algorithm);
403            } catch (NoSuchAlgorithmException e) {
404                continue;
405            }
406            if (ignoreSecondEndline && data[end - 1] == '\n'
407                    && data[end - 2] == '\n') {
408                md.update(data, start, end - 1 - start);
409            } else {
410                md.update(data, start, end - start);
411            }
412            byte[] b = md.digest();
413            byte[] hashBytes = hash.getBytes(Charsets.ISO_8859_1);
414            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
415        }
416        return ignorable;
417    }
418
419    /**
420     * Returns all of the {@link java.security.cert.Certificate} instances that
421     * were used to verify the signature on the JAR entry called
422     * {@code name}.
423     *
424     * @param name
425     *            the name of a JAR entry.
426     * @return an array of {@link java.security.cert.Certificate}.
427     */
428    Certificate[] getCertificates(String name) {
429        Certificate[] verifiedCerts = verifiedEntries.get(name);
430        if (verifiedCerts == null) {
431            return null;
432        }
433        return verifiedCerts.clone();
434    }
435
436    /**
437     * Remove all entries from the internal collection of data held about each
438     * JAR entry in the {@code META-INF} directory.
439     *
440     * @see #addMetaEntry(String, byte[])
441     */
442    void removeMetaEntries() {
443        metaEntries = null;
444    }
445
446    /**
447     * Returns a {@code Vector} of all of the
448     * {@link java.security.cert.Certificate}s that are associated with the
449     * signing of the named signature file.
450     *
451     * @param signatureFileName
452     *            the name of a signature file.
453     * @param certificates
454     *            a {@code Map} of all of the certificate chains discovered so
455     *            far while attempting to verify the JAR that contains the
456     *            signature file {@code signatureFileName}. This object is
457     *            previously set in the course of one or more calls to
458     *            {@link #verifyJarSignatureFile(String, String, String, Map, Map)}
459     *            where it was passed as the last argument.
460     * @return all of the {@code Certificate} entries for the signer of the JAR
461     *         whose actions led to the creation of the named signature file.
462     */
463    public static Vector<Certificate> getSignerCertificates(
464            String signatureFileName, Map<String, Certificate[]> certificates) {
465        Vector<Certificate> result = new Vector<Certificate>();
466        Certificate[] certChain = certificates.get(signatureFileName);
467        if (certChain != null) {
468            for (Certificate element : certChain) {
469                result.add(element);
470            }
471        }
472        return result;
473    }
474}
475