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