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