blob: cb71ecc1da8b33b5080dd63a8a6a74536177eeed [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;
Alex Klyubin29045202016-05-31 16:04:49 -070033import java.util.List;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000034import java.util.Locale;
35import java.util.Map;
Alex Klyubine4157182016-01-05 13:27:05 -080036import java.util.Set;
37import java.util.StringTokenizer;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000038import java.util.jar.Attributes;
39import java.util.jar.JarFile;
Alex Klyubine4157182016-01-05 13:27:05 -080040import android.util.ArraySet;
41import android.util.apk.ApkSignatureSchemeV2Verifier;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000042import libcore.io.Base64;
43import sun.security.jca.Providers;
44import sun.security.pkcs.PKCS7;
Alex Klyubin29045202016-05-31 16:04:49 -070045import sun.security.pkcs.SignerInfo;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000046
47/**
48 * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
49 * the verification of signed JARs. {@code JarFile} and {@code JarInputStream}
50 * objects are expected to have a {@code JarVerifier} instance member which
51 * can be used to carry out the tasks associated with verifying a signed JAR.
52 * These tasks would typically include:
53 * <ul>
54 * <li>verification of all signed signature files
55 * <li>confirmation that all signed data was signed only by the party or parties
56 * specified in the signature block data
57 * <li>verification that the contents of all signature files (i.e. {@code .SF}
58 * files) agree with the JAR entries information found in the JAR manifest.
59 * </ul>
60 */
61class StrictJarVerifier {
62 /**
63 * List of accepted digest algorithms. This list is in order from most
64 * preferred to least preferred.
65 */
66 private static final String[] DIGEST_ALGORITHMS = new String[] {
67 "SHA-512",
68 "SHA-384",
69 "SHA-256",
70 "SHA1",
71 };
72
73 private final String jarName;
74 private final StrictJarManifest manifest;
75 private final HashMap<String, byte[]> metaEntries;
76 private final int mainAttributesEnd;
Alex Klyubin9b59bc42016-03-24 12:02:20 -070077 private final boolean signatureSchemeRollbackProtectionsEnforced;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +000078
79 private final Hashtable<String, HashMap<String, Attributes>> signatures =
80 new Hashtable<String, HashMap<String, Attributes>>(5);
81
82 private final Hashtable<String, Certificate[]> certificates =
83 new Hashtable<String, Certificate[]>(5);
84
85 private final Hashtable<String, Certificate[][]> verifiedEntries =
86 new Hashtable<String, Certificate[][]>();
87
88 /**
89 * Stores and a hash and a message digest and verifies that massage digest
90 * matches the hash.
91 */
92 static class VerifierEntry extends OutputStream {
93
94 private final String name;
95
96 private final MessageDigest digest;
97
98 private final byte[] hash;
99
100 private final Certificate[][] certChains;
101
102 private final Hashtable<String, Certificate[][]> verifiedEntries;
103
104 VerifierEntry(String name, MessageDigest digest, byte[] hash,
105 Certificate[][] certChains, Hashtable<String, Certificate[][]> verifedEntries) {
106 this.name = name;
107 this.digest = digest;
108 this.hash = hash;
109 this.certChains = certChains;
110 this.verifiedEntries = verifedEntries;
111 }
112
113 /**
114 * Updates a digest with one byte.
115 */
116 @Override
117 public void write(int value) {
118 digest.update((byte) value);
119 }
120
121 /**
122 * Updates a digest with byte array.
123 */
124 @Override
125 public void write(byte[] buf, int off, int nbytes) {
126 digest.update(buf, off, nbytes);
127 }
128
129 /**
130 * Verifies that the digests stored in the manifest match the decrypted
131 * digests from the .SF file. This indicates the validity of the
132 * signing, not the integrity of the file, as its digest must be
133 * calculated and verified when its contents are read.
134 *
135 * @throws SecurityException
136 * if the digest value stored in the manifest does <i>not</i>
137 * agree with the decrypted digest as recovered from the
138 * <code>.SF</code> file.
139 */
140 void verify() {
141 byte[] d = digest.digest();
142 if (!MessageDigest.isEqual(d, Base64.decode(hash))) {
143 throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
144 }
145 verifiedEntries.put(name, certChains);
146 }
147 }
148
149 private static SecurityException invalidDigest(String signatureFile, String name,
150 String jarName) {
151 throw new SecurityException(signatureFile + " has invalid digest for " + name +
152 " in " + jarName);
153 }
154
155 private static SecurityException failedVerification(String jarName, String signatureFile) {
156 throw new SecurityException(jarName + " failed verification of " + signatureFile);
157 }
158
159 private static SecurityException failedVerification(String jarName, String signatureFile,
160 Throwable e) {
161 throw new SecurityException(jarName + " failed verification of " + signatureFile, e);
162 }
163
164
165 /**
166 * Constructs and returns a new instance of {@code JarVerifier}.
167 *
168 * @param name
169 * the name of the JAR file being verified.
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700170 *
171 * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
172 * stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
173 * {@code false} to ignore any such protections.
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000174 */
175 StrictJarVerifier(String name, StrictJarManifest manifest,
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700176 HashMap<String, byte[]> metaEntries, boolean signatureSchemeRollbackProtectionsEnforced) {
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000177 jarName = name;
178 this.manifest = manifest;
179 this.metaEntries = metaEntries;
180 this.mainAttributesEnd = manifest.getMainAttributesEnd();
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700181 this.signatureSchemeRollbackProtectionsEnforced =
182 signatureSchemeRollbackProtectionsEnforced;
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000183 }
184
185 /**
186 * Invoked for each new JAR entry read operation from the input
187 * stream. This method constructs and returns a new {@link VerifierEntry}
188 * which contains the certificates used to sign the entry and its hash value
189 * as specified in the JAR MANIFEST format.
190 *
191 * @param name
192 * the name of an entry in a JAR file which is <b>not</b> in the
193 * {@code META-INF} directory.
194 * @return a new instance of {@link VerifierEntry} which can be used by
195 * callers as an {@link OutputStream}.
196 */
197 VerifierEntry initEntry(String name) {
198 // If no manifest is present by the time an entry is found,
199 // verification cannot occur. If no signature files have
200 // been found, do not verify.
201 if (manifest == null || signatures.isEmpty()) {
202 return null;
203 }
204
205 Attributes attributes = manifest.getAttributes(name);
206 // entry has no digest
207 if (attributes == null) {
208 return null;
209 }
210
211 ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
212 Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
213 while (it.hasNext()) {
214 Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
215 HashMap<String, Attributes> hm = entry.getValue();
216 if (hm.get(name) != null) {
217 // Found an entry for entry name in .SF file
218 String signatureFile = entry.getKey();
219 Certificate[] certChain = certificates.get(signatureFile);
220 if (certChain != null) {
221 certChains.add(certChain);
222 }
223 }
224 }
225
226 // entry is not signed
227 if (certChains.isEmpty()) {
228 return null;
229 }
230 Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);
231
232 for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
233 final String algorithm = DIGEST_ALGORITHMS[i];
234 final String hash = attributes.getValue(algorithm + "-Digest");
235 if (hash == null) {
236 continue;
237 }
238 byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
239
240 try {
241 return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
242 certChainsArray, verifiedEntries);
243 } catch (NoSuchAlgorithmException ignored) {
244 }
245 }
246 return null;
247 }
248
249 /**
250 * Add a new meta entry to the internal collection of data held on each JAR
251 * entry in the {@code META-INF} directory including the manifest
252 * file itself. Files associated with the signing of a JAR would also be
253 * added to this collection.
254 *
255 * @param name
256 * the name of the file located in the {@code META-INF}
257 * directory.
258 * @param buf
259 * the file bytes for the file called {@code name}.
260 * @see #removeMetaEntries()
261 */
262 void addMetaEntry(String name, byte[] buf) {
263 metaEntries.put(name.toUpperCase(Locale.US), buf);
264 }
265
266 /**
267 * If the associated JAR file is signed, check on the validity of all of the
268 * known signatures.
269 *
270 * @return {@code true} if the associated JAR is signed and an internal
271 * check verifies the validity of the signature(s). {@code false} if
272 * the associated JAR file has no entries at all in its {@code
273 * META-INF} directory. This situation is indicative of an invalid
274 * JAR file.
275 * <p>
276 * Will also return {@code true} if the JAR file is <i>not</i>
277 * signed.
278 * @throws SecurityException
279 * if the JAR file is signed and it is determined that a
280 * signature block file contains an invalid signature for the
281 * corresponding signature file.
282 */
283 synchronized boolean readCertificates() {
284 if (metaEntries.isEmpty()) {
285 return false;
286 }
287
288 Iterator<String> it = metaEntries.keySet().iterator();
289 while (it.hasNext()) {
290 String key = it.next();
291 if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
292 verifyCertificate(key);
293 it.remove();
294 }
295 }
296 return true;
297 }
298
299 /**
300 * Verifies that the signature computed from {@code sfBytes} matches
301 * that specified in {@code blockBytes} (which is a PKCS7 block). Returns
302 * certificates listed in the PKCS7 block. Throws a {@code GeneralSecurityException}
303 * if something goes wrong during verification.
304 */
305 static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)
306 throws GeneralSecurityException {
307
308 Object obj = null;
309 try {
310
311 obj = Providers.startJarVerification();
312 PKCS7 block = new PKCS7(blockBytes);
Alex Klyubin29045202016-05-31 16:04:49 -0700313 SignerInfo[] verifiedSignerInfos = block.verify(sfBytes);
314 if ((verifiedSignerInfos == null) || (verifiedSignerInfos.length == 0)) {
315 throw new GeneralSecurityException(
316 "Failed to verify signature: no verified SignerInfos");
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000317 }
Alex Klyubin29045202016-05-31 16:04:49 -0700318 // Ignore any SignerInfo other than the first one, to be compatible with older Android
319 // platforms which have been doing this for years. See
320 // libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java
321 // verifySignature method of older platforms.
322 SignerInfo verifiedSignerInfo = verifiedSignerInfos[0];
323 List<X509Certificate> verifiedSignerCertChain =
324 verifiedSignerInfo.getCertificateChain(block);
325 if (verifiedSignerCertChain == null) {
326 // Should never happen
327 throw new GeneralSecurityException(
328 "Failed to find verified SignerInfo certificate chain");
329 } else if (verifiedSignerCertChain.isEmpty()) {
330 // Should never happen
331 throw new GeneralSecurityException(
332 "Verified SignerInfo certificate chain is emtpy");
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000333 }
Alex Klyubin29045202016-05-31 16:04:49 -0700334 return verifiedSignerCertChain.toArray(
335 new X509Certificate[verifiedSignerCertChain.size()]);
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000336 } catch (IOException e) {
337 throw new GeneralSecurityException("IO exception verifying jar cert", e);
338 } finally {
339 Providers.stopJarVerification(obj);
340 }
341 }
342
343 /**
344 * @param certFile
345 */
346 private void verifyCertificate(String certFile) {
347 // Found Digital Sig, .SF should already have been read
348 String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
349 byte[] sfBytes = metaEntries.get(signatureFile);
350 if (sfBytes == null) {
351 return;
352 }
353
354 byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
355 // Manifest entry is required for any verifications.
356 if (manifestBytes == null) {
357 return;
358 }
359
360 byte[] sBlockBytes = metaEntries.get(certFile);
361 try {
362 Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes);
363 if (signerCertChain != null) {
364 certificates.put(signatureFile, signerCertChain);
365 }
366 } catch (GeneralSecurityException e) {
367 throw failedVerification(jarName, signatureFile, e);
368 }
369
370 // Verify manifest hash in .sf file
371 Attributes attributes = new Attributes();
372 HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
373 try {
374 StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes);
375 im.readEntries(entries, null);
376 } catch (IOException e) {
377 return;
378 }
379
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700380 // If requested, check whether APK Signature Scheme v2 signature was stripped.
381 if (signatureSchemeRollbackProtectionsEnforced) {
382 String apkSignatureSchemeIdList =
383 attributes.getValue(
384 ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
385 if (apkSignatureSchemeIdList != null) {
386 // This field contains a comma-separated list of APK signature scheme IDs which
387 // were used to sign this APK. If an ID is known to us, it means signatures of that
388 // scheme were stripped from the APK because otherwise we wouldn't have fallen back
389 // to verifying the APK using the JAR signature scheme.
390 boolean v2SignatureGenerated = false;
391 StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
392 while (tokenizer.hasMoreTokens()) {
393 String idText = tokenizer.nextToken().trim();
394 if (idText.isEmpty()) {
395 continue;
396 }
397 int id;
398 try {
399 id = Integer.parseInt(idText);
400 } catch (Exception ignored) {
401 continue;
402 }
403 if (id == ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
404 // This APK was supposed to be signed with APK Signature Scheme v2 but no
405 // such signature was found.
406 v2SignatureGenerated = true;
407 break;
408 }
Alex Klyubine4157182016-01-05 13:27:05 -0800409 }
Alex Klyubine4157182016-01-05 13:27:05 -0800410
Alex Klyubin9b59bc42016-03-24 12:02:20 -0700411 if (v2SignatureGenerated) {
412 throw new SecurityException(signatureFile + " indicates " + jarName
413 + " is signed using APK Signature Scheme v2, but no such signature was"
414 + " found. Signature stripped?");
415 }
Alex Klyubine4157182016-01-05 13:27:05 -0800416 }
417 }
418
Przemyslaw Szczepaniak8a7c1602015-11-03 09:47:56 +0000419 // Do we actually have any signatures to look at?
420 if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
421 return;
422 }
423
424 boolean createdBySigntool = false;
425 String createdBy = attributes.getValue("Created-By");
426 if (createdBy != null) {
427 createdBySigntool = createdBy.indexOf("signtool") != -1;
428 }
429
430 // Use .SF to verify the mainAttributes of the manifest
431 // If there is no -Digest-Manifest-Main-Attributes entry in .SF
432 // file, such as those created before java 1.5, then we ignore
433 // such verification.
434 if (mainAttributesEnd > 0 && !createdBySigntool) {
435 String digestAttribute = "-Digest-Manifest-Main-Attributes";
436 if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
437 throw failedVerification(jarName, signatureFile);
438 }
439 }
440
441 // Use .SF to verify the whole manifest.
442 String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
443 if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
444 Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
445 while (it.hasNext()) {
446 Map.Entry<String, Attributes> entry = it.next();
447 StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
448 if (chunk == null) {
449 return;
450 }
451 if (!verify(entry.getValue(), "-Digest", manifestBytes,
452 chunk.start, chunk.end, createdBySigntool, false)) {
453 throw invalidDigest(signatureFile, entry.getKey(), jarName);
454 }
455 }
456 }
457 metaEntries.put(signatureFile, null);
458 signatures.put(signatureFile, entries);
459 }
460
461 /**
462 * Returns a <code>boolean</code> indication of whether or not the
463 * associated jar file is signed.
464 *
465 * @return {@code true} if the JAR is signed, {@code false}
466 * otherwise.
467 */
468 boolean isSignedJar() {
469 return certificates.size() > 0;
470 }
471
472 private boolean verify(Attributes attributes, String entry, byte[] data,
473 int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
474 for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
475 String algorithm = DIGEST_ALGORITHMS[i];
476 String hash = attributes.getValue(algorithm + entry);
477 if (hash == null) {
478 continue;
479 }
480
481 MessageDigest md;
482 try {
483 md = MessageDigest.getInstance(algorithm);
484 } catch (NoSuchAlgorithmException e) {
485 continue;
486 }
487 if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
488 md.update(data, start, end - 1 - start);
489 } else {
490 md.update(data, start, end - start);
491 }
492 byte[] b = md.digest();
493 byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
494 return MessageDigest.isEqual(b, Base64.decode(hashBytes));
495 }
496 return ignorable;
497 }
498
499 /**
500 * Returns all of the {@link java.security.cert.Certificate} chains that
501 * were used to verify the signature on the JAR entry called
502 * {@code name}. Callers must not modify the returned arrays.
503 *
504 * @param name
505 * the name of a JAR entry.
506 * @return an array of {@link java.security.cert.Certificate} chains.
507 */
508 Certificate[][] getCertificateChains(String name) {
509 return verifiedEntries.get(name);
510 }
511
512 /**
513 * Remove all entries from the internal collection of data held about each
514 * JAR entry in the {@code META-INF} directory.
515 */
516 void removeMetaEntries() {
517 metaEntries.clear();
518 }
519}