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