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