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