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