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