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