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 new SecurityException(Messages.getString(
130                        "archive.32", new Object[] { //$NON-NLS-1$
131                        JarFile.MANIFEST_NAME, name, jarName }));
132            }
133            verifiedEntries.put(name, certificates);
134        }
135
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"); //$NON-NLS-1$
201        if (algorithms == null) {
202            algorithms = "SHA SHA1"; //$NON-NLS-1$
203        }
204        StringTokenizer tokens = new StringTokenizer(algorithms);
205        while (tokens.hasMoreTokens()) {
206            String algorithm = tokens.nextToken();
207            String hash = attributes.getValue(algorithm + "-Digest"); //$NON-NLS-1$
208            if (hash == null) {
209                continue;
210            }
211            byte[] hashBytes;
212            try {
213                hashBytes = hash.getBytes("ISO-8859-1"); //$NON-NLS-1$
214            } catch (UnsupportedEncodingException e) {
215                throw new RuntimeException(e.toString());
216            }
217
218            try {
219                // BEGIN android-changed
220                return new VerifierEntry(name, OpenSSLMessageDigestJDK.getInstance(algorithm),
221                        hashBytes, certificatesArray);
222                // END android-changed
223            } catch (NoSuchAlgorithmException e) {
224                // ignored
225            }
226        }
227        return null;
228    }
229
230    /**
231     * Add a new meta entry to the internal collection of data held on each JAR
232     * entry in the {@code META-INF} directory including the manifest
233     * file itself. Files associated with the signing of a JAR would also be
234     * added to this collection.
235     *
236     * @param name
237     *            the name of the file located in the {@code META-INF}
238     *            directory.
239     * @param buf
240     *            the file bytes for the file called {@code name}.
241     * @see #removeMetaEntries()
242     */
243    void addMetaEntry(String name, byte[] buf) {
244        metaEntries.put(Util.toASCIIUpperCase(name), buf);
245    }
246
247    /**
248     * If the associated JAR file is signed, check on the validity of all of the
249     * known signatures.
250     *
251     * @return {@code true} if the associated JAR is signed and an internal
252     *         check verifies the validity of the signature(s). {@code false} if
253     *         the associated JAR file has no entries at all in its {@code
254     *         META-INF} directory. This situation is indicative of an invalid
255     *         JAR file.
256     *         <p>
257     *         Will also return {@code true} if the JAR file is <i>not</i>
258     *         signed.
259     * @throws SecurityException
260     *             if the JAR file is signed and it is determined that a
261     *             signature block file contains an invalid signature for the
262     *             corresponding signature file.
263     */
264    synchronized boolean readCertificates() {
265        if (metaEntries == null) {
266            return false;
267        }
268        Iterator<String> it = metaEntries.keySet().iterator();
269        while (it.hasNext()) {
270            String key = it.next();
271            if (key.endsWith(".DSA") || key.endsWith(".RSA")) { //$NON-NLS-1$ //$NON-NLS-2$
272                verifyCertificate(key);
273                // Check for recursive class load
274                if (metaEntries == null) {
275                    return false;
276                }
277                it.remove();
278            }
279        }
280        return true;
281    }
282
283    /**
284     * @param certFile
285     */
286    private void verifyCertificate(String certFile) {
287        // Found Digital Sig, .SF should already have been read
288        String signatureFile = certFile.substring(0, certFile.lastIndexOf('.'))
289                + ".SF"; //$NON-NLS-1$
290        byte[] sfBytes = metaEntries.get(signatureFile);
291        if (sfBytes == null) {
292            return;
293        }
294
295        byte[] manifest = metaEntries.get(JarFile.MANIFEST_NAME);
296        // Manifest entry is required for any verifications.
297        if (manifest == null) {
298            return;
299        }
300
301        byte[] sBlockBytes = metaEntries.get(certFile);
302        try {
303            Certificate[] signerCertChain = JarUtils.verifySignature(
304                    new ByteArrayInputStream(sfBytes),
305                    new ByteArrayInputStream(sBlockBytes));
306            /*
307             * Recursive call in loading security provider related class which
308             * is in a signed JAR.
309             */
310            if (null == metaEntries) {
311                return;
312            }
313            if (signerCertChain != null) {
314                certificates.put(signatureFile, signerCertChain);
315            }
316        } catch (IOException e) {
317            return;
318        } catch (GeneralSecurityException e) {
319            /* [MSG "archive.31", "{0} failed verification of {1}"] */
320            throw new SecurityException(Messages.getString(
321                    "archive.31", jarName, signatureFile)); //$NON-NLS-1$
322        }
323
324        // Verify manifest hash in .sf file
325        Attributes attributes = new Attributes();
326        HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
327        try {
328            InitManifest im = new InitManifest(sfBytes, attributes, Attributes.Name.SIGNATURE_VERSION);
329            im.initEntries(entries, null);
330        } catch (IOException e) {
331            return;
332        }
333
334        boolean createdBySigntool = false;
335        String createdBy = attributes.getValue("Created-By"); //$NON-NLS-1$
336        if (createdBy != null) {
337            createdBySigntool = createdBy.indexOf("signtool") != -1; //$NON-NLS-1$
338        }
339
340        // Use .SF to verify the mainAttributes of the manifest
341        // If there is no -Digest-Manifest-Main-Attributes entry in .SF
342        // file, such as those created before java 1.5, then we ignore
343        // such verification.
344        if (mainAttributesEnd > 0 && !createdBySigntool) {
345            String digestAttribute = "-Digest-Manifest-Main-Attributes"; //$NON-NLS-1$
346            if (!verify(attributes, digestAttribute, manifest, 0,
347                    mainAttributesEnd, false, true)) {
348                /* [MSG "archive.31", "{0} failed verification of {1}"] */
349                throw new SecurityException(Messages.getString(
350                        "archive.31", jarName, signatureFile)); //$NON-NLS-1$
351            }
352        }
353
354        // Use .SF to verify the whole manifest.
355        String digestAttribute = createdBySigntool ? "-Digest" //$NON-NLS-1$
356                : "-Digest-Manifest"; //$NON-NLS-1$
357        if (!verify(attributes, digestAttribute, manifest, 0, manifest.length,
358                false, false)) {
359            Iterator<Map.Entry<String, Attributes>> it = entries.entrySet()
360                    .iterator();
361            while (it.hasNext()) {
362                Map.Entry<String, Attributes> entry = it.next();
363                Manifest.Chunk chunk = man.getChunk(entry.getKey());
364                if (chunk == null) {
365                    return;
366                }
367                if (!verify(entry.getValue(), "-Digest", manifest, //$NON-NLS-1$
368                        chunk.start, chunk.end, createdBySigntool, false)) {
369                    throw new SecurityException(Messages.getString(
370                            "archive.32", //$NON-NLS-1$
371                            new Object[] { signatureFile, entry.getKey(),
372                                    jarName }));
373                }
374            }
375        }
376        metaEntries.put(signatureFile, null);
377        signatures.put(signatureFile, entries);
378    }
379
380    /**
381     * Associate this verifier with the specified {@link Manifest} object.
382     *
383     * @param mf
384     *            a {@code java.util.jar.Manifest} object.
385     */
386    void setManifest(Manifest mf) {
387        man = mf;
388    }
389
390    /**
391     * Returns a <code>boolean</code> indication of whether or not the
392     * associated jar file is signed.
393     *
394     * @return {@code true} if the JAR is signed, {@code false}
395     *         otherwise.
396     */
397    boolean isSignedJar() {
398        return certificates.size() > 0;
399    }
400
401    private boolean verify(Attributes attributes, String entry, byte[] data,
402            int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
403        String algorithms = attributes.getValue("Digest-Algorithms"); //$NON-NLS-1$
404        if (algorithms == null) {
405            algorithms = "SHA SHA1"; //$NON-NLS-1$
406        }
407        StringTokenizer tokens = new StringTokenizer(algorithms);
408        while (tokens.hasMoreTokens()) {
409            String algorithm = tokens.nextToken();
410            String hash = attributes.getValue(algorithm + entry);
411            if (hash == null) {
412                continue;
413            }
414
415            MessageDigest md;
416            try {
417                // BEGIN android-changed
418                md = OpenSSLMessageDigestJDK.getInstance(algorithm);
419                // END android-changed
420            } catch (NoSuchAlgorithmException e) {
421                continue;
422            }
423            if (ignoreSecondEndline && data[end - 1] == '\n'
424                    && data[end - 2] == '\n') {
425                md.update(data, start, end - 1 - start);
426            } else {
427                md.update(data, start, end - start);
428            }
429            byte[] b = md.digest();
430            byte[] hashBytes;
431            try {
432                hashBytes = hash.getBytes("ISO-8859-1"); //$NON-NLS-1$
433            } catch (UnsupportedEncodingException e) {
434                throw new RuntimeException(e.toString());
435            }
436            return MessageDigest.isEqual(b, Base64.decode(hashBytes));
437        }
438        return ignorable;
439    }
440
441    /**
442     * Returns all of the {@link java.security.cert.Certificate} instances that
443     * were used to verify the signature on the JAR entry called
444     * {@code name}.
445     *
446     * @param name
447     *            the name of a JAR entry.
448     * @return an array of {@link java.security.cert.Certificate}.
449     */
450    Certificate[] getCertificates(String name) {
451        Certificate[] verifiedCerts = verifiedEntries.get(name);
452        if (verifiedCerts == null) {
453            return null;
454        }
455        return verifiedCerts.clone();
456    }
457
458    /**
459     * Remove all entries from the internal collection of data held about each
460     * JAR entry in the {@code META-INF} directory.
461     *
462     * @see #addMetaEntry(String, byte[])
463     */
464    void removeMetaEntries() {
465        metaEntries = null;
466    }
467
468    /**
469     * Returns a {@code Vector} of all of the
470     * {@link java.security.cert.Certificate}s that are associated with the
471     * signing of the named signature file.
472     *
473     * @param signatureFileName
474     *            the name of a signature file.
475     * @param certificates
476     *            a {@code Map} of all of the certificate chains discovered so
477     *            far while attempting to verify the JAR that contains the
478     *            signature file {@code signatureFileName}. This object is
479     *            previously set in the course of one or more calls to
480     *            {@link #verifyJarSignatureFile(String, String, String, Map, Map)}
481     *            where it was passed as the last argument.
482     * @return all of the {@code Certificate} entries for the signer of the JAR
483     *         whose actions led to the creation of the named signature file.
484     */
485    public static Vector<Certificate> getSignerCertificates(
486            String signatureFileName, Map<String, Certificate[]> certificates) {
487        Vector<Certificate> result = new Vector<Certificate>();
488        Certificate[] certChain = certificates.get(signatureFileName);
489        if (certChain != null) {
490            for (Certificate element : certChain) {
491                result.add(element);
492            }
493        }
494        return result;
495    }
496}
497