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