1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.apksig.internal.apk.v1; 18 19 import static com.android.apksig.Constants.MAX_APK_SIGNERS; 20 import static com.android.apksig.internal.oid.OidConstants.getSigAlgSupportedApiLevels; 21 import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaDigestAlgorithm; 22 import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getJcaSignatureAlgorithm; 23 import static com.android.apksig.internal.x509.Certificate.findCertificate; 24 import static com.android.apksig.internal.x509.Certificate.parseCertificates; 25 26 import com.android.apksig.ApkVerifier.Issue; 27 import com.android.apksig.ApkVerifier.IssueWithParams; 28 import com.android.apksig.apk.ApkFormatException; 29 import com.android.apksig.apk.ApkUtils; 30 import com.android.apksig.internal.apk.ApkSigningBlockUtils; 31 import com.android.apksig.internal.asn1.Asn1BerParser; 32 import com.android.apksig.internal.asn1.Asn1Class; 33 import com.android.apksig.internal.asn1.Asn1DecodingException; 34 import com.android.apksig.internal.asn1.Asn1Field; 35 import com.android.apksig.internal.asn1.Asn1OpaqueObject; 36 import com.android.apksig.internal.asn1.Asn1Type; 37 import com.android.apksig.internal.jar.ManifestParser; 38 import com.android.apksig.internal.oid.OidConstants; 39 import com.android.apksig.internal.pkcs7.Attribute; 40 import com.android.apksig.internal.pkcs7.ContentInfo; 41 import com.android.apksig.internal.pkcs7.Pkcs7Constants; 42 import com.android.apksig.internal.pkcs7.Pkcs7DecodingException; 43 import com.android.apksig.internal.pkcs7.SignedData; 44 import com.android.apksig.internal.pkcs7.SignerInfo; 45 import com.android.apksig.internal.util.AndroidSdkVersion; 46 import com.android.apksig.internal.util.ByteBufferUtils; 47 import com.android.apksig.internal.util.InclusiveIntRange; 48 import com.android.apksig.internal.util.Pair; 49 import com.android.apksig.internal.zip.CentralDirectoryRecord; 50 import com.android.apksig.internal.zip.LocalFileRecord; 51 import com.android.apksig.internal.zip.ZipUtils; 52 import com.android.apksig.util.DataSinks; 53 import com.android.apksig.util.DataSource; 54 import com.android.apksig.zip.ZipFormatException; 55 56 import java.io.IOException; 57 import java.nio.ByteBuffer; 58 import java.security.InvalidKeyException; 59 import java.security.KeyFactory; 60 import java.security.MessageDigest; 61 import java.security.NoSuchAlgorithmException; 62 import java.security.Principal; 63 import java.security.PublicKey; 64 import java.security.Signature; 65 import java.security.SignatureException; 66 import java.security.cert.CertificateException; 67 import java.security.cert.X509Certificate; 68 import java.security.spec.InvalidKeySpecException; 69 import java.security.spec.X509EncodedKeySpec; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.Base64; 73 import java.util.Base64.Decoder; 74 import java.util.Collection; 75 import java.util.Collections; 76 import java.util.HashMap; 77 import java.util.HashSet; 78 import java.util.List; 79 import java.util.Locale; 80 import java.util.Map; 81 import java.util.Set; 82 import java.util.StringTokenizer; 83 import java.util.jar.Attributes; 84 85 /** 86 * APK verifier which uses JAR signing (aka v1 signing scheme). 87 * 88 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a> 89 */ 90 public abstract class V1SchemeVerifier { V1SchemeVerifier()91 private V1SchemeVerifier() {} 92 93 /** 94 * Verifies the provided APK's JAR signatures and returns the result of verification. APK is 95 * considered verified only if {@link Result#verified} is {@code true}. If verification fails, 96 * the result will contain errors -- see {@link Result#getErrors()}. 97 * 98 * <p>Verification succeeds iff the APK's JAR signatures are expected to verify on all Android 99 * platform versions in the {@code [minSdkVersion, maxSdkVersion]} range. If the APK's signature 100 * is expected to not verify on any of the specified platform versions, this method returns a 101 * result with one or more errors and whose {@code Result.verified == false}, or this method 102 * throws an exception. 103 * 104 * @throws ApkFormatException if the APK is malformed 105 * @throws IOException if an I/O error occurs when reading the APK 106 * @throws NoSuchAlgorithmException if the APK's JAR signatures cannot be verified because a 107 * required cryptographic algorithm implementation is missing 108 */ verify( DataSource apk, ApkUtils.ZipSections apkSections, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion)109 public static Result verify( 110 DataSource apk, 111 ApkUtils.ZipSections apkSections, 112 Map<Integer, String> supportedApkSigSchemeNames, 113 Set<Integer> foundApkSigSchemeIds, 114 int minSdkVersion, 115 int maxSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException { 116 if (minSdkVersion > maxSdkVersion) { 117 throw new IllegalArgumentException( 118 "minSdkVersion (" + minSdkVersion + ") > maxSdkVersion (" + maxSdkVersion 119 + ")"); 120 } 121 122 Result result = new Result(); 123 124 // Parse the ZIP Central Directory and check that there are no entries with duplicate names. 125 List<CentralDirectoryRecord> cdRecords = parseZipCentralDirectory(apk, apkSections); 126 Set<String> cdEntryNames = checkForDuplicateEntries(cdRecords, result); 127 if (result.containsErrors()) { 128 return result; 129 } 130 131 // Verify JAR signature(s). 132 Signers.verify( 133 apk, 134 apkSections.getZipCentralDirectoryOffset(), 135 cdRecords, 136 cdEntryNames, 137 supportedApkSigSchemeNames, 138 foundApkSigSchemeIds, 139 minSdkVersion, 140 maxSdkVersion, 141 result); 142 143 return result; 144 } 145 146 /** 147 * Returns the set of entry names and reports any duplicate entry names in the {@code result} 148 * as errors. 149 */ checkForDuplicateEntries( List<CentralDirectoryRecord> cdRecords, Result result)150 private static Set<String> checkForDuplicateEntries( 151 List<CentralDirectoryRecord> cdRecords, Result result) { 152 Set<String> cdEntryNames = new HashSet<>(cdRecords.size()); 153 Set<String> duplicateCdEntryNames = null; 154 for (CentralDirectoryRecord cdRecord : cdRecords) { 155 String entryName = cdRecord.getName(); 156 if (!cdEntryNames.add(entryName)) { 157 // This is an error. Report this once per duplicate name. 158 if (duplicateCdEntryNames == null) { 159 duplicateCdEntryNames = new HashSet<>(); 160 } 161 if (duplicateCdEntryNames.add(entryName)) { 162 result.addError(Issue.JAR_SIG_DUPLICATE_ZIP_ENTRY, entryName); 163 } 164 } 165 } 166 return cdEntryNames; 167 } 168 169 /** 170 * Parses raw representation of MANIFEST.MF file into a pair of main entry manifest section 171 * representation and a mapping between entry name and its manifest section representation. 172 * 173 * @param manifestBytes raw representation of Manifest.MF 174 * @param cdEntryNames expected set of entry names 175 * @param result object to keep track of errors that happened during the parsing 176 * @return a pair of main entry manifest section representation and a mapping between entry name 177 * and its manifest section representation 178 */ parseManifest( byte[] manifestBytes, Set<String> cdEntryNames, Result result)179 public static Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> parseManifest( 180 byte[] manifestBytes, Set<String> cdEntryNames, Result result) { 181 ManifestParser manifest = new ManifestParser(manifestBytes); 182 ManifestParser.Section manifestMainSection = manifest.readSection(); 183 List<ManifestParser.Section> manifestIndividualSections = manifest.readAllSections(); 184 Map<String, ManifestParser.Section> entryNameToManifestSection = 185 new HashMap<>(manifestIndividualSections.size()); 186 int manifestSectionNumber = 0; 187 for (ManifestParser.Section manifestSection : manifestIndividualSections) { 188 manifestSectionNumber++; 189 String entryName = manifestSection.getName(); 190 if (entryName == null) { 191 result.addError(Issue.JAR_SIG_UNNNAMED_MANIFEST_SECTION, manifestSectionNumber); 192 continue; 193 } 194 if (entryNameToManifestSection.put(entryName, manifestSection) != null) { 195 result.addError(Issue.JAR_SIG_DUPLICATE_MANIFEST_SECTION, entryName); 196 continue; 197 } 198 if (!cdEntryNames.contains(entryName)) { 199 result.addError( 200 Issue.JAR_SIG_MISSING_ZIP_ENTRY_REFERENCED_IN_MANIFEST, entryName); 201 continue; 202 } 203 } 204 return Pair.of(manifestMainSection, entryNameToManifestSection); 205 } 206 207 /** 208 * All JAR signers of an APK. 209 */ 210 private static class Signers { 211 212 /** 213 * Verifies JAR signatures of the provided APK and populates the provided result container 214 * with errors, warnings, and information about signers. The APK is considered verified if 215 * the {@link Result#verified} is {@code true}. 216 */ verify( DataSource apk, long cdStartOffset, List<CentralDirectoryRecord> cdRecords, Set<String> cdEntryNames, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion, Result result)217 private static void verify( 218 DataSource apk, 219 long cdStartOffset, 220 List<CentralDirectoryRecord> cdRecords, 221 Set<String> cdEntryNames, 222 Map<Integer, String> supportedApkSigSchemeNames, 223 Set<Integer> foundApkSigSchemeIds, 224 int minSdkVersion, 225 int maxSdkVersion, 226 Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { 227 228 // Find JAR manifest and signature block files. 229 CentralDirectoryRecord manifestEntry = null; 230 Map<String, CentralDirectoryRecord> sigFileEntries = new HashMap<>(1); 231 List<CentralDirectoryRecord> sigBlockEntries = new ArrayList<>(1); 232 for (CentralDirectoryRecord cdRecord : cdRecords) { 233 String entryName = cdRecord.getName(); 234 if (!entryName.startsWith("META-INF/")) { 235 continue; 236 } 237 if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals( 238 entryName))) { 239 manifestEntry = cdRecord; 240 continue; 241 } 242 if (entryName.endsWith(".SF")) { 243 sigFileEntries.put(entryName, cdRecord); 244 continue; 245 } 246 if ((entryName.endsWith(".RSA")) 247 || (entryName.endsWith(".DSA")) 248 || (entryName.endsWith(".EC"))) { 249 sigBlockEntries.add(cdRecord); 250 continue; 251 } 252 } 253 if (manifestEntry == null) { 254 result.addError(Issue.JAR_SIG_NO_MANIFEST); 255 return; 256 } 257 258 // Parse the JAR manifest and check that all JAR entries it references exist in the APK. 259 byte[] manifestBytes; 260 try { 261 manifestBytes = 262 LocalFileRecord.getUncompressedData(apk, manifestEntry, cdStartOffset); 263 } catch (ZipFormatException e) { 264 throw new ApkFormatException("Malformed ZIP entry: " + manifestEntry.getName(), e); 265 } 266 267 Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> manifestSections = 268 parseManifest(manifestBytes, cdEntryNames, result); 269 270 if (result.containsErrors()) { 271 return; 272 } 273 274 ManifestParser.Section manifestMainSection = manifestSections.getFirst(); 275 Map<String, ManifestParser.Section> entryNameToManifestSection = 276 manifestSections.getSecond(); 277 278 // STATE OF AFFAIRS: 279 // * All JAR entries listed in JAR manifest are present in the APK. 280 281 // Identify signers 282 List<Signer> signers = new ArrayList<>(sigBlockEntries.size()); 283 for (CentralDirectoryRecord sigBlockEntry : sigBlockEntries) { 284 String sigBlockEntryName = sigBlockEntry.getName(); 285 int extensionDelimiterIndex = sigBlockEntryName.lastIndexOf('.'); 286 if (extensionDelimiterIndex == -1) { 287 throw new RuntimeException( 288 "Signature block file name does not contain extension: " 289 + sigBlockEntryName); 290 } 291 String sigFileEntryName = 292 sigBlockEntryName.substring(0, extensionDelimiterIndex) + ".SF"; 293 CentralDirectoryRecord sigFileEntry = sigFileEntries.get(sigFileEntryName); 294 if (sigFileEntry == null) { 295 result.addWarning( 296 Issue.JAR_SIG_MISSING_FILE, sigBlockEntryName, sigFileEntryName); 297 continue; 298 } 299 String signerName = sigBlockEntryName.substring("META-INF/".length()); 300 Result.SignerInfo signerInfo = 301 new Result.SignerInfo( 302 signerName, sigBlockEntryName, sigFileEntry.getName()); 303 Signer signer = new Signer(signerName, sigBlockEntry, sigFileEntry, signerInfo); 304 signers.add(signer); 305 } 306 if (signers.isEmpty()) { 307 result.addError(Issue.JAR_SIG_NO_SIGNATURES); 308 return; 309 } 310 if (signers.size() > MAX_APK_SIGNERS) { 311 result.addError(Issue.JAR_SIG_MAX_SIGNATURES_EXCEEDED, MAX_APK_SIGNERS, 312 signers.size()); 313 return; 314 } 315 316 // Verify each signer's signature block file .(RSA|DSA|EC) against the corresponding 317 // signature file .SF. Any error encountered for any signer terminates verification, to 318 // mimic Android's behavior. 319 for (Signer signer : signers) { 320 signer.verifySigBlockAgainstSigFile( 321 apk, cdStartOffset, minSdkVersion, maxSdkVersion); 322 if (signer.getResult().containsErrors()) { 323 result.signers.add(signer.getResult()); 324 } 325 } 326 if (result.containsErrors()) { 327 return; 328 } 329 // STATE OF AFFAIRS: 330 // * All JAR entries listed in JAR manifest are present in the APK. 331 // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). 332 333 // Verify each signer's signature file (.SF) against the JAR manifest. 334 List<Signer> remainingSigners = new ArrayList<>(signers.size()); 335 for (Signer signer : signers) { 336 signer.verifySigFileAgainstManifest( 337 manifestBytes, 338 manifestMainSection, 339 entryNameToManifestSection, 340 supportedApkSigSchemeNames, 341 foundApkSigSchemeIds, 342 minSdkVersion, 343 maxSdkVersion); 344 if (signer.isIgnored()) { 345 result.ignoredSigners.add(signer.getResult()); 346 } else { 347 if (signer.getResult().containsErrors()) { 348 result.signers.add(signer.getResult()); 349 } else { 350 remainingSigners.add(signer); 351 } 352 } 353 } 354 if (result.containsErrors()) { 355 return; 356 } 357 signers = remainingSigners; 358 if (signers.isEmpty()) { 359 result.addError(Issue.JAR_SIG_NO_SIGNATURES); 360 return; 361 } 362 // STATE OF AFFAIRS: 363 // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). 364 // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. 365 // * All JAR entries listed in JAR manifest are present in the APK. 366 367 // Verify data of JAR entries against JAR manifest and .SF files. On Android, an APK's 368 // JAR entry is considered signed by signers associated with an .SF file iff the entry 369 // is mentioned in the .SF file and the entry's digest(s) mentioned in the JAR manifest 370 // match theentry's uncompressed data. Android requires that all such JAR entries are 371 // signed by the same set of signers. This set may be smaller than the set of signers 372 // we've identified so far. 373 Set<Signer> apkSigners = 374 verifyJarEntriesAgainstManifestAndSigners( 375 apk, 376 cdStartOffset, 377 cdRecords, 378 entryNameToManifestSection, 379 signers, 380 minSdkVersion, 381 maxSdkVersion, 382 result); 383 if (result.containsErrors()) { 384 return; 385 } 386 // STATE OF AFFAIRS: 387 // * All signature files (.SF) verify against corresponding block files (.RSA|.DSA|.EC). 388 // * Contents of all JAR manifest sections listed in .SF files verify against .SF files. 389 // * All JAR entries listed in JAR manifest are present in the APK. 390 // * All JAR entries present in the APK and supposed to be covered by JAR signature 391 // (i.e., reside outside of META-INF/) are covered by signatures from the same set 392 // of signers. 393 394 // Report any JAR entries which aren't covered by signature. 395 Set<String> signatureEntryNames = new HashSet<>(1 + result.signers.size() * 2); 396 signatureEntryNames.add(manifestEntry.getName()); 397 for (Signer signer : apkSigners) { 398 signatureEntryNames.add(signer.getSignatureBlockEntryName()); 399 signatureEntryNames.add(signer.getSignatureFileEntryName()); 400 } 401 for (CentralDirectoryRecord cdRecord : cdRecords) { 402 String entryName = cdRecord.getName(); 403 if ((entryName.startsWith("META-INF/")) 404 && (!entryName.endsWith("/")) 405 && (!signatureEntryNames.contains(entryName))) { 406 result.addWarning(Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY, entryName); 407 } 408 } 409 410 // Reflect the sets of used signers and ignored signers in the result. 411 for (Signer signer : signers) { 412 if (apkSigners.contains(signer)) { 413 result.signers.add(signer.getResult()); 414 } else { 415 result.ignoredSigners.add(signer.getResult()); 416 } 417 } 418 419 result.verified = true; 420 } 421 } 422 423 static class Signer { 424 private final String mName; 425 private final Result.SignerInfo mResult; 426 private final CentralDirectoryRecord mSignatureFileEntry; 427 private final CentralDirectoryRecord mSignatureBlockEntry; 428 private boolean mIgnored; 429 430 private byte[] mSigFileBytes; 431 private Set<String> mSigFileEntryNames; 432 Signer( String name, CentralDirectoryRecord sigBlockEntry, CentralDirectoryRecord sigFileEntry, Result.SignerInfo result)433 private Signer( 434 String name, 435 CentralDirectoryRecord sigBlockEntry, 436 CentralDirectoryRecord sigFileEntry, 437 Result.SignerInfo result) { 438 mName = name; 439 mResult = result; 440 mSignatureBlockEntry = sigBlockEntry; 441 mSignatureFileEntry = sigFileEntry; 442 } 443 getName()444 public String getName() { 445 return mName; 446 } 447 getSignatureFileEntryName()448 public String getSignatureFileEntryName() { 449 return mSignatureFileEntry.getName(); 450 } 451 getSignatureBlockEntryName()452 public String getSignatureBlockEntryName() { 453 return mSignatureBlockEntry.getName(); 454 } 455 setIgnored()456 void setIgnored() { 457 mIgnored = true; 458 } 459 isIgnored()460 public boolean isIgnored() { 461 return mIgnored; 462 } 463 getSigFileEntryNames()464 public Set<String> getSigFileEntryNames() { 465 return mSigFileEntryNames; 466 } 467 getResult()468 public Result.SignerInfo getResult() { 469 return mResult; 470 } 471 verifySigBlockAgainstSigFile( DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion)472 public void verifySigBlockAgainstSigFile( 473 DataSource apk, long cdStartOffset, int minSdkVersion, int maxSdkVersion) 474 throws IOException, ApkFormatException, NoSuchAlgorithmException { 475 // Obtain the signature block from the APK 476 byte[] sigBlockBytes; 477 try { 478 sigBlockBytes = 479 LocalFileRecord.getUncompressedData( 480 apk, mSignatureBlockEntry, cdStartOffset); 481 } catch (ZipFormatException e) { 482 throw new ApkFormatException( 483 "Malformed ZIP entry: " + mSignatureBlockEntry.getName(), e); 484 } 485 // Obtain the signature file from the APK 486 try { 487 mSigFileBytes = 488 LocalFileRecord.getUncompressedData( 489 apk, mSignatureFileEntry, cdStartOffset); 490 } catch (ZipFormatException e) { 491 throw new ApkFormatException( 492 "Malformed ZIP entry: " + mSignatureFileEntry.getName(), e); 493 } 494 495 // Extract PKCS #7 SignedData from the signature block 496 SignedData signedData; 497 try { 498 ContentInfo contentInfo = 499 Asn1BerParser.parse(ByteBuffer.wrap(sigBlockBytes), ContentInfo.class); 500 if (!Pkcs7Constants.OID_SIGNED_DATA.equals(contentInfo.contentType)) { 501 throw new Asn1DecodingException( 502 "Unsupported ContentInfo.contentType: " + contentInfo.contentType); 503 } 504 signedData = 505 Asn1BerParser.parse(contentInfo.content.getEncoded(), SignedData.class); 506 } catch (Asn1DecodingException e) { 507 e.printStackTrace(); 508 mResult.addError( 509 Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); 510 return; 511 } 512 513 if (signedData.signerInfos.isEmpty()) { 514 mResult.addError(Issue.JAR_SIG_NO_SIGNERS, mSignatureBlockEntry.getName()); 515 return; 516 } 517 518 // Find the first SignedData.SignerInfos element which verifies against the signature 519 // file 520 SignerInfo firstVerifiedSignerInfo = null; 521 X509Certificate firstVerifiedSignerInfoSigningCertificate = null; 522 // Prior to Android N, Android attempts to verify only the first SignerInfo. From N 523 // onwards, Android attempts to verify all SignerInfos and then picks the first verified 524 // SignerInfo. 525 List<SignerInfo> unverifiedSignerInfosToTry; 526 if (minSdkVersion < AndroidSdkVersion.N) { 527 unverifiedSignerInfosToTry = 528 Collections.singletonList(signedData.signerInfos.get(0)); 529 } else { 530 unverifiedSignerInfosToTry = signedData.signerInfos; 531 } 532 List<X509Certificate> signedDataCertificates = null; 533 for (SignerInfo unverifiedSignerInfo : unverifiedSignerInfosToTry) { 534 // Parse SignedData.certificates -- they are needed to verify SignerInfo 535 if (signedDataCertificates == null) { 536 try { 537 signedDataCertificates = parseCertificates(signedData.certificates); 538 } catch (CertificateException e) { 539 mResult.addError( 540 Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); 541 return; 542 } 543 } 544 545 // Verify SignerInfo 546 X509Certificate signingCertificate; 547 try { 548 signingCertificate = 549 verifySignerInfoAgainstSigFile( 550 signedData, 551 signedDataCertificates, 552 unverifiedSignerInfo, 553 mSigFileBytes, 554 minSdkVersion, 555 maxSdkVersion); 556 if (mResult.containsErrors()) { 557 return; 558 } 559 if (signingCertificate != null) { 560 // SignerInfo verified 561 if (firstVerifiedSignerInfo == null) { 562 firstVerifiedSignerInfo = unverifiedSignerInfo; 563 firstVerifiedSignerInfoSigningCertificate = signingCertificate; 564 } 565 } 566 } catch (Pkcs7DecodingException e) { 567 mResult.addError( 568 Issue.JAR_SIG_PARSE_EXCEPTION, mSignatureBlockEntry.getName(), e); 569 return; 570 } catch (InvalidKeyException | SignatureException e) { 571 mResult.addError( 572 Issue.JAR_SIG_VERIFY_EXCEPTION, 573 mSignatureBlockEntry.getName(), 574 mSignatureFileEntry.getName(), 575 e); 576 return; 577 } 578 } 579 if (firstVerifiedSignerInfo == null) { 580 // No SignerInfo verified 581 mResult.addError( 582 Issue.JAR_SIG_DID_NOT_VERIFY, 583 mSignatureBlockEntry.getName(), 584 mSignatureFileEntry.getName()); 585 return; 586 } 587 // Verified 588 List<X509Certificate> signingCertChain = 589 getCertificateChain( 590 signedDataCertificates, firstVerifiedSignerInfoSigningCertificate); 591 mResult.certChain.clear(); 592 mResult.certChain.addAll(signingCertChain); 593 } 594 595 /** 596 * Returns the signing certificate if the provided {@link SignerInfo} verifies against the 597 * contents of the provided signature file, or {@code null} if it does not verify. 598 */ verifySignerInfoAgainstSigFile( SignedData signedData, Collection<X509Certificate> signedDataCertificates, SignerInfo signerInfo, byte[] signatureFile, int minSdkVersion, int maxSdkVersion)599 private X509Certificate verifySignerInfoAgainstSigFile( 600 SignedData signedData, 601 Collection<X509Certificate> signedDataCertificates, 602 SignerInfo signerInfo, 603 byte[] signatureFile, 604 int minSdkVersion, 605 int maxSdkVersion) 606 throws Pkcs7DecodingException, NoSuchAlgorithmException, 607 InvalidKeyException, SignatureException { 608 String digestAlgorithmOid = signerInfo.digestAlgorithm.algorithm; 609 String signatureAlgorithmOid = signerInfo.signatureAlgorithm.algorithm; 610 InclusiveIntRange desiredApiLevels = 611 InclusiveIntRange.fromTo(minSdkVersion, maxSdkVersion); 612 List<InclusiveIntRange> apiLevelsWhereDigestAndSigAlgorithmSupported = 613 getSigAlgSupportedApiLevels(digestAlgorithmOid, signatureAlgorithmOid); 614 List<InclusiveIntRange> apiLevelsWhereDigestAlgorithmNotSupported = 615 desiredApiLevels.getValuesNotIn(apiLevelsWhereDigestAndSigAlgorithmSupported); 616 if (!apiLevelsWhereDigestAlgorithmNotSupported.isEmpty()) { 617 String digestAlgorithmUserFriendly = 618 OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( 619 digestAlgorithmOid); 620 if (digestAlgorithmUserFriendly == null) { 621 digestAlgorithmUserFriendly = digestAlgorithmOid; 622 } 623 String signatureAlgorithmUserFriendly = 624 OidConstants.OidToUserFriendlyNameMapper.getUserFriendlyNameForOid( 625 signatureAlgorithmOid); 626 if (signatureAlgorithmUserFriendly == null) { 627 signatureAlgorithmUserFriendly = signatureAlgorithmOid; 628 } 629 StringBuilder apiLevelsUserFriendly = new StringBuilder(); 630 for (InclusiveIntRange range : apiLevelsWhereDigestAlgorithmNotSupported) { 631 if (apiLevelsUserFriendly.length() > 0) { 632 apiLevelsUserFriendly.append(", "); 633 } 634 if (range.getMin() == range.getMax()) { 635 apiLevelsUserFriendly.append(String.valueOf(range.getMin())); 636 } else if (range.getMax() == Integer.MAX_VALUE) { 637 apiLevelsUserFriendly.append(range.getMin() + "+"); 638 } else { 639 apiLevelsUserFriendly.append(range.getMin() + "-" + range.getMax()); 640 } 641 } 642 mResult.addError( 643 Issue.JAR_SIG_UNSUPPORTED_SIG_ALG, 644 mSignatureBlockEntry.getName(), 645 digestAlgorithmOid, 646 signatureAlgorithmOid, 647 apiLevelsUserFriendly.toString(), 648 digestAlgorithmUserFriendly, 649 signatureAlgorithmUserFriendly); 650 return null; 651 } 652 653 // From the bag of certs, obtain the certificate referenced by the SignerInfo, 654 // and verify the cryptographic signature in the SignerInfo against the certificate. 655 656 // Locate the signing certificate referenced by the SignerInfo 657 X509Certificate signingCertificate = 658 findCertificate(signedDataCertificates, signerInfo.sid); 659 if (signingCertificate == null) { 660 throw new SignatureException( 661 "Signing certificate referenced in SignerInfo not found in" 662 + " SignedData"); 663 } 664 665 // Check whether the signing certificate is acceptable. Android performs these 666 // checks explicitly, instead of delegating this to 667 // Signature.initVerify(Certificate). 668 if (signingCertificate.hasUnsupportedCriticalExtension()) { 669 throw new SignatureException( 670 "Signing certificate has unsupported critical extensions"); 671 } 672 boolean[] keyUsageExtension = signingCertificate.getKeyUsage(); 673 if (keyUsageExtension != null) { 674 boolean digitalSignature = 675 (keyUsageExtension.length >= 1) && (keyUsageExtension[0]); 676 boolean nonRepudiation = 677 (keyUsageExtension.length >= 2) && (keyUsageExtension[1]); 678 if ((!digitalSignature) && (!nonRepudiation)) { 679 throw new SignatureException( 680 "Signing certificate not authorized for use in digital signatures" 681 + ": keyUsage extension missing digitalSignature and" 682 + " nonRepudiation"); 683 } 684 } 685 686 // Verify the cryptographic signature in SignerInfo against the certificate's 687 // public key 688 String jcaSignatureAlgorithm = 689 getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid); 690 Signature s = Signature.getInstance(jcaSignatureAlgorithm); 691 PublicKey publicKey = signingCertificate.getPublicKey(); 692 try { 693 s.initVerify(publicKey); 694 } catch (InvalidKeyException e) { 695 // An InvalidKeyException could be caught if the PublicKey in the certificate is not 696 // properly encoded; attempt to resolve any encoding errors, generate a new public 697 // key, and reattempt the initVerify with the newly encoded key. 698 try { 699 byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey); 700 publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic( 701 new X509EncodedKeySpec(encodedPublicKey)); 702 } catch (InvalidKeySpecException ikse) { 703 // If an InvalidKeySpecException is caught then throw the original Exception 704 // since the key couldn't be properly re-encoded, and the original Exception 705 // will have more useful debugging info. 706 throw e; 707 } 708 s = Signature.getInstance(jcaSignatureAlgorithm); 709 s.initVerify(publicKey); 710 } 711 712 if (signerInfo.signedAttrs != null) { 713 // Signed attributes present -- verify signature against the ASN.1 DER encoded form 714 // of signed attributes. This verifies integrity of the signature file because 715 // signed attributes must contain the digest of the signature file. 716 if (minSdkVersion < AndroidSdkVersion.KITKAT) { 717 // Prior to Android KitKat, APKs with signed attributes are unsafe: 718 // * The APK's contents are not protected by the JAR signature because the 719 // digest in signed attributes is not verified. This means an attacker can 720 // arbitrarily modify the APK without invalidating its signature. 721 // * Luckily, the signature over signed attributes was verified incorrectly 722 // (over the verbatim IMPLICIT [0] form rather than over re-encoded 723 // UNIVERSAL SET form) which means that JAR signatures which would verify on 724 // pre-KitKat Android and yet do not protect the APK from modification could 725 // be generated only by broken tools or on purpose by the entity signing the 726 // APK. 727 // 728 // We thus reject such unsafe APKs, even if they verify on platforms before 729 // KitKat. 730 throw new SignatureException( 731 "APKs with Signed Attributes broken on platforms with API Level < " 732 + AndroidSdkVersion.KITKAT); 733 } 734 try { 735 List<Attribute> signedAttributes = 736 Asn1BerParser.parseImplicitSetOf( 737 signerInfo.signedAttrs.getEncoded(), Attribute.class); 738 SignedAttributes signedAttrs = new SignedAttributes(signedAttributes); 739 if (maxSdkVersion >= AndroidSdkVersion.N) { 740 // Content Type attribute is checked only on Android N and newer 741 String contentType = 742 signedAttrs.getSingleObjectIdentifierValue( 743 Pkcs7Constants.OID_CONTENT_TYPE); 744 if (contentType == null) { 745 throw new SignatureException("No Content Type in signed attributes"); 746 } 747 if (!contentType.equals(signedData.encapContentInfo.contentType)) { 748 // Did not verify: Content type signed attribute does not match 749 // SignedData.encapContentInfo.eContentType. This fails verification of 750 // this SignerInfo but should not prevent verification of other 751 // SignerInfos. Hence, no exception is thrown. 752 return null; 753 } 754 } 755 byte[] expectedSignatureFileDigest = 756 signedAttrs.getSingleOctetStringValue( 757 Pkcs7Constants.OID_MESSAGE_DIGEST); 758 if (expectedSignatureFileDigest == null) { 759 throw new SignatureException("No content digest in signed attributes"); 760 } 761 byte[] actualSignatureFileDigest = 762 MessageDigest.getInstance( 763 getJcaDigestAlgorithm(digestAlgorithmOid)) 764 .digest(signatureFile); 765 if (!Arrays.equals( 766 expectedSignatureFileDigest, actualSignatureFileDigest)) { 767 // Skip verification: signature file digest in signed attributes does not 768 // match the signature file. This fails verification of 769 // this SignerInfo but should not prevent verification of other 770 // SignerInfos. Hence, no exception is thrown. 771 return null; 772 } 773 } catch (Asn1DecodingException e) { 774 throw new SignatureException("Failed to parse signed attributes", e); 775 } 776 // PKCS #7 requires that signature is over signed attributes re-encoded as 777 // ASN.1 DER. However, Android does not re-encode except for changing the 778 // first byte of encoded form from IMPLICIT [0] to UNIVERSAL SET. We do the 779 // same for maximum compatibility. 780 ByteBuffer signedAttrsOriginalEncoding = signerInfo.signedAttrs.getEncoded(); 781 s.update((byte) 0x31); // UNIVERSAL SET 782 signedAttrsOriginalEncoding.position(1); 783 s.update(signedAttrsOriginalEncoding); 784 } else { 785 // No signed attributes present -- verify signature against the contents of the 786 // signature file 787 s.update(signatureFile); 788 } 789 byte[] sigBytes = ByteBufferUtils.toByteArray(signerInfo.signature.slice()); 790 if (!s.verify(sigBytes)) { 791 // Cryptographic signature did not verify. This fails verification of this 792 // SignerInfo but should not prevent verification of other SignerInfos. Hence, no 793 // exception is thrown. 794 return null; 795 } 796 // Cryptographic signature verified 797 return signingCertificate; 798 } 799 800 801 getCertificateChain( List<X509Certificate> certs, X509Certificate leaf)802 public static List<X509Certificate> getCertificateChain( 803 List<X509Certificate> certs, X509Certificate leaf) { 804 List<X509Certificate> unusedCerts = new ArrayList<>(certs); 805 List<X509Certificate> result = new ArrayList<>(1); 806 result.add(leaf); 807 unusedCerts.remove(leaf); 808 X509Certificate root = leaf; 809 while (!root.getSubjectDN().equals(root.getIssuerDN())) { 810 Principal targetDn = root.getIssuerDN(); 811 boolean issuerFound = false; 812 for (int i = 0; i < unusedCerts.size(); i++) { 813 X509Certificate unusedCert = unusedCerts.get(i); 814 if (targetDn.equals(unusedCert.getSubjectDN())) { 815 issuerFound = true; 816 unusedCerts.remove(i); 817 result.add(unusedCert); 818 root = unusedCert; 819 break; 820 } 821 } 822 if (!issuerFound) { 823 break; 824 } 825 } 826 return result; 827 } 828 829 830 831 verifySigFileAgainstManifest( byte[] manifestBytes, ManifestParser.Section manifestMainSection, Map<String, ManifestParser.Section> entryNameToManifestSection, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, int maxSdkVersion)832 public void verifySigFileAgainstManifest( 833 byte[] manifestBytes, 834 ManifestParser.Section manifestMainSection, 835 Map<String, ManifestParser.Section> entryNameToManifestSection, 836 Map<Integer, String> supportedApkSigSchemeNames, 837 Set<Integer> foundApkSigSchemeIds, 838 int minSdkVersion, 839 int maxSdkVersion) throws NoSuchAlgorithmException { 840 // Inspect the main section of the .SF file. 841 ManifestParser sf = new ManifestParser(mSigFileBytes); 842 ManifestParser.Section sfMainSection = sf.readSection(); 843 if (sfMainSection.getAttributeValue(Attributes.Name.SIGNATURE_VERSION) == null) { 844 mResult.addError( 845 Issue.JAR_SIG_MISSING_VERSION_ATTR_IN_SIG_FILE, 846 mSignatureFileEntry.getName()); 847 setIgnored(); 848 return; 849 } 850 851 if (maxSdkVersion >= AndroidSdkVersion.N) { 852 // Android N and newer rejects APKs whose .SF file says they were supposed to be 853 // signed with APK Signature Scheme v2 (or newer) and yet no such signature was 854 // found. 855 checkForStrippedApkSignatures( 856 sfMainSection, supportedApkSigSchemeNames, foundApkSigSchemeIds); 857 if (mResult.containsErrors()) { 858 return; 859 } 860 } 861 862 boolean createdBySigntool = false; 863 String createdBy = sfMainSection.getAttributeValue("Created-By"); 864 if (createdBy != null) { 865 createdBySigntool = createdBy.indexOf("signtool") != -1; 866 } 867 boolean manifestDigestVerified = 868 verifyManifestDigest( 869 sfMainSection, 870 createdBySigntool, 871 manifestBytes, 872 minSdkVersion, 873 maxSdkVersion); 874 if (!createdBySigntool) { 875 verifyManifestMainSectionDigest( 876 sfMainSection, 877 manifestMainSection, 878 manifestBytes, 879 minSdkVersion, 880 maxSdkVersion); 881 } 882 if (mResult.containsErrors()) { 883 return; 884 } 885 886 // Inspect per-entry sections of .SF file. Technically, if the digest of JAR manifest 887 // verifies, per-entry sections should be ignored. However, most Android platform 888 // implementations require that such sections exist. 889 List<ManifestParser.Section> sfSections = sf.readAllSections(); 890 Set<String> sfEntryNames = new HashSet<>(sfSections.size()); 891 int sfSectionNumber = 0; 892 for (ManifestParser.Section sfSection : sfSections) { 893 sfSectionNumber++; 894 String entryName = sfSection.getName(); 895 if (entryName == null) { 896 mResult.addError( 897 Issue.JAR_SIG_UNNNAMED_SIG_FILE_SECTION, 898 mSignatureFileEntry.getName(), 899 sfSectionNumber); 900 setIgnored(); 901 return; 902 } 903 if (!sfEntryNames.add(entryName)) { 904 mResult.addError( 905 Issue.JAR_SIG_DUPLICATE_SIG_FILE_SECTION, 906 mSignatureFileEntry.getName(), 907 entryName); 908 setIgnored(); 909 return; 910 } 911 if (manifestDigestVerified) { 912 // No need to verify this entry's corresponding JAR manifest entry because the 913 // JAR manifest verifies in full. 914 continue; 915 } 916 // Whole-file digest of JAR manifest hasn't been verified. Thus, we need to verify 917 // the digest of the JAR manifest section corresponding to this .SF section. 918 ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); 919 if (manifestSection == null) { 920 mResult.addError( 921 Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, 922 entryName, 923 mSignatureFileEntry.getName()); 924 setIgnored(); 925 continue; 926 } 927 verifyManifestIndividualSectionDigest( 928 sfSection, 929 createdBySigntool, 930 manifestSection, 931 manifestBytes, 932 minSdkVersion, 933 maxSdkVersion); 934 } 935 mSigFileEntryNames = sfEntryNames; 936 } 937 938 939 /** 940 * Returns {@code true} if the whole-file digest of the manifest against the main section of 941 * the .SF file. 942 */ verifyManifestDigest( ManifestParser.Section sfMainSection, boolean createdBySigntool, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)943 private boolean verifyManifestDigest( 944 ManifestParser.Section sfMainSection, 945 boolean createdBySigntool, 946 byte[] manifestBytes, 947 int minSdkVersion, 948 int maxSdkVersion) throws NoSuchAlgorithmException { 949 Collection<NamedDigest> expectedDigests = 950 getDigestsToVerify( 951 sfMainSection, 952 ((createdBySigntool) ? "-Digest" : "-Digest-Manifest"), 953 minSdkVersion, 954 maxSdkVersion); 955 boolean digestFound = !expectedDigests.isEmpty(); 956 if (!digestFound) { 957 mResult.addWarning( 958 Issue.JAR_SIG_NO_MANIFEST_DIGEST_IN_SIG_FILE, 959 mSignatureFileEntry.getName()); 960 return false; 961 } 962 963 boolean verified = true; 964 for (NamedDigest expectedDigest : expectedDigests) { 965 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; 966 byte[] actual = digest(jcaDigestAlgorithm, manifestBytes); 967 byte[] expected = expectedDigest.digest; 968 if (!Arrays.equals(expected, actual)) { 969 mResult.addWarning( 970 Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, 971 V1SchemeConstants.MANIFEST_ENTRY_NAME, 972 jcaDigestAlgorithm, 973 mSignatureFileEntry.getName(), 974 Base64.getEncoder().encodeToString(actual), 975 Base64.getEncoder().encodeToString(expected)); 976 verified = false; 977 } 978 } 979 return verified; 980 } 981 982 /** 983 * Verifies the digest of the manifest's main section against the main section of the .SF 984 * file. 985 */ verifyManifestMainSectionDigest( ManifestParser.Section sfMainSection, ManifestParser.Section manifestMainSection, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)986 private void verifyManifestMainSectionDigest( 987 ManifestParser.Section sfMainSection, 988 ManifestParser.Section manifestMainSection, 989 byte[] manifestBytes, 990 int minSdkVersion, 991 int maxSdkVersion) throws NoSuchAlgorithmException { 992 Collection<NamedDigest> expectedDigests = 993 getDigestsToVerify( 994 sfMainSection, 995 "-Digest-Manifest-Main-Attributes", 996 minSdkVersion, 997 maxSdkVersion); 998 if (expectedDigests.isEmpty()) { 999 return; 1000 } 1001 1002 for (NamedDigest expectedDigest : expectedDigests) { 1003 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; 1004 byte[] actual = 1005 digest( 1006 jcaDigestAlgorithm, 1007 manifestBytes, 1008 manifestMainSection.getStartOffset(), 1009 manifestMainSection.getSizeBytes()); 1010 byte[] expected = expectedDigest.digest; 1011 if (!Arrays.equals(expected, actual)) { 1012 mResult.addError( 1013 Issue.JAR_SIG_MANIFEST_MAIN_SECTION_DIGEST_DID_NOT_VERIFY, 1014 jcaDigestAlgorithm, 1015 mSignatureFileEntry.getName(), 1016 Base64.getEncoder().encodeToString(actual), 1017 Base64.getEncoder().encodeToString(expected)); 1018 } 1019 } 1020 } 1021 1022 /** 1023 * Verifies the digest of the manifest's individual section against the corresponding 1024 * individual section of the .SF file. 1025 */ verifyManifestIndividualSectionDigest( ManifestParser.Section sfIndividualSection, boolean createdBySigntool, ManifestParser.Section manifestIndividualSection, byte[] manifestBytes, int minSdkVersion, int maxSdkVersion)1026 private void verifyManifestIndividualSectionDigest( 1027 ManifestParser.Section sfIndividualSection, 1028 boolean createdBySigntool, 1029 ManifestParser.Section manifestIndividualSection, 1030 byte[] manifestBytes, 1031 int minSdkVersion, 1032 int maxSdkVersion) throws NoSuchAlgorithmException { 1033 String entryName = sfIndividualSection.getName(); 1034 Collection<NamedDigest> expectedDigests = 1035 getDigestsToVerify( 1036 sfIndividualSection, "-Digest", minSdkVersion, maxSdkVersion); 1037 if (expectedDigests.isEmpty()) { 1038 mResult.addError( 1039 Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_SIG_FILE, 1040 entryName, 1041 mSignatureFileEntry.getName()); 1042 return; 1043 } 1044 1045 int sectionStartIndex = manifestIndividualSection.getStartOffset(); 1046 int sectionSizeBytes = manifestIndividualSection.getSizeBytes(); 1047 if (createdBySigntool) { 1048 int sectionEndIndex = sectionStartIndex + sectionSizeBytes; 1049 if ((manifestBytes[sectionEndIndex - 1] == '\n') 1050 && (manifestBytes[sectionEndIndex - 2] == '\n')) { 1051 sectionSizeBytes--; 1052 } 1053 } 1054 for (NamedDigest expectedDigest : expectedDigests) { 1055 String jcaDigestAlgorithm = expectedDigest.jcaDigestAlgorithm; 1056 byte[] actual = 1057 digest( 1058 jcaDigestAlgorithm, 1059 manifestBytes, 1060 sectionStartIndex, 1061 sectionSizeBytes); 1062 byte[] expected = expectedDigest.digest; 1063 if (!Arrays.equals(expected, actual)) { 1064 mResult.addError( 1065 Issue.JAR_SIG_MANIFEST_SECTION_DIGEST_DID_NOT_VERIFY, 1066 entryName, 1067 jcaDigestAlgorithm, 1068 mSignatureFileEntry.getName(), 1069 Base64.getEncoder().encodeToString(actual), 1070 Base64.getEncoder().encodeToString(expected)); 1071 } 1072 } 1073 } 1074 checkForStrippedApkSignatures( ManifestParser.Section sfMainSection, Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds)1075 private void checkForStrippedApkSignatures( 1076 ManifestParser.Section sfMainSection, 1077 Map<Integer, String> supportedApkSigSchemeNames, 1078 Set<Integer> foundApkSigSchemeIds) { 1079 String signedWithApkSchemes = 1080 sfMainSection.getAttributeValue( 1081 V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); 1082 // This field contains a comma-separated list of APK signature scheme IDs which were 1083 // used to sign this APK. Android rejects APKs where an ID is known to the platform but 1084 // the APK didn't verify using that scheme. 1085 1086 if (signedWithApkSchemes == null) { 1087 // APK signature (e.g., v2 scheme) stripping protections not enabled. 1088 if (!foundApkSigSchemeIds.isEmpty()) { 1089 // APK is signed with an APK signature scheme such as v2 scheme. 1090 mResult.addWarning( 1091 Issue.JAR_SIG_NO_APK_SIG_STRIP_PROTECTION, 1092 mSignatureFileEntry.getName()); 1093 } 1094 return; 1095 } 1096 1097 if (supportedApkSigSchemeNames.isEmpty()) { 1098 return; 1099 } 1100 1101 Set<Integer> supportedApkSigSchemeIds = supportedApkSigSchemeNames.keySet(); 1102 Set<Integer> supportedExpectedApkSigSchemeIds = new HashSet<>(1); 1103 StringTokenizer tokenizer = new StringTokenizer(signedWithApkSchemes, ","); 1104 while (tokenizer.hasMoreTokens()) { 1105 String idText = tokenizer.nextToken().trim(); 1106 if (idText.isEmpty()) { 1107 continue; 1108 } 1109 int id; 1110 try { 1111 id = Integer.parseInt(idText); 1112 } catch (Exception ignored) { 1113 continue; 1114 } 1115 // This APK was supposed to be signed with the APK signature scheme having 1116 // this ID. 1117 if (supportedApkSigSchemeIds.contains(id)) { 1118 supportedExpectedApkSigSchemeIds.add(id); 1119 } else { 1120 mResult.addWarning( 1121 Issue.JAR_SIG_UNKNOWN_APK_SIG_SCHEME_ID, 1122 mSignatureFileEntry.getName(), 1123 id); 1124 } 1125 } 1126 1127 for (int id : supportedExpectedApkSigSchemeIds) { 1128 if (!foundApkSigSchemeIds.contains(id)) { 1129 String apkSigSchemeName = supportedApkSigSchemeNames.get(id); 1130 mResult.addError( 1131 Issue.JAR_SIG_MISSING_APK_SIG_REFERENCED, 1132 mSignatureFileEntry.getName(), 1133 id, 1134 apkSigSchemeName); 1135 } 1136 } 1137 } 1138 } 1139 getDigestsToVerify( ManifestParser.Section section, String digestAttrSuffix, int minSdkVersion, int maxSdkVersion)1140 public static Collection<NamedDigest> getDigestsToVerify( 1141 ManifestParser.Section section, 1142 String digestAttrSuffix, 1143 int minSdkVersion, 1144 int maxSdkVersion) { 1145 Decoder base64Decoder = Base64.getDecoder(); 1146 List<NamedDigest> result = new ArrayList<>(1); 1147 if (minSdkVersion < AndroidSdkVersion.JELLY_BEAN_MR2) { 1148 // Prior to JB MR2, Android platform's logic for picking a digest algorithm to verify is 1149 // to rely on the ancient Digest-Algorithms attribute which contains 1150 // whitespace-separated list of digest algorithms (defaulting to SHA-1) to try. The 1151 // first digest attribute (with supported digest algorithm) found using the list is 1152 // used. 1153 String algs = section.getAttributeValue("Digest-Algorithms"); 1154 if (algs == null) { 1155 algs = "SHA SHA1"; 1156 } 1157 StringTokenizer tokens = new StringTokenizer(algs); 1158 while (tokens.hasMoreTokens()) { 1159 String alg = tokens.nextToken(); 1160 String attrName = alg + digestAttrSuffix; 1161 String digestBase64 = section.getAttributeValue(attrName); 1162 if (digestBase64 == null) { 1163 // Attribute not found 1164 continue; 1165 } 1166 alg = getCanonicalJcaMessageDigestAlgorithm(alg); 1167 if ((alg == null) 1168 || (getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile(alg) 1169 > minSdkVersion)) { 1170 // Unsupported digest algorithm 1171 continue; 1172 } 1173 // Supported digest algorithm 1174 result.add(new NamedDigest(alg, base64Decoder.decode(digestBase64))); 1175 break; 1176 } 1177 // No supported digests found -- this will fail to verify on pre-JB MR2 Androids. 1178 if (result.isEmpty()) { 1179 return result; 1180 } 1181 } 1182 1183 if (maxSdkVersion >= AndroidSdkVersion.JELLY_BEAN_MR2) { 1184 // On JB MR2 and newer, Android platform picks the strongest algorithm out of: 1185 // SHA-512, SHA-384, SHA-256, SHA-1. 1186 for (String alg : JB_MR2_AND_NEWER_DIGEST_ALGS) { 1187 String attrName = getJarDigestAttributeName(alg, digestAttrSuffix); 1188 String digestBase64 = section.getAttributeValue(attrName); 1189 if (digestBase64 == null) { 1190 // Attribute not found 1191 continue; 1192 } 1193 byte[] digest = base64Decoder.decode(digestBase64); 1194 byte[] digestInResult = getDigest(result, alg); 1195 if ((digestInResult == null) || (!Arrays.equals(digestInResult, digest))) { 1196 result.add(new NamedDigest(alg, digest)); 1197 } 1198 break; 1199 } 1200 } 1201 1202 return result; 1203 } 1204 1205 private static final String[] JB_MR2_AND_NEWER_DIGEST_ALGS = { 1206 "SHA-512", 1207 "SHA-384", 1208 "SHA-256", 1209 "SHA-1", 1210 }; 1211 getCanonicalJcaMessageDigestAlgorithm(String algorithm)1212 private static String getCanonicalJcaMessageDigestAlgorithm(String algorithm) { 1213 return UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.get(algorithm.toUpperCase(Locale.US)); 1214 } 1215 getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( String jcaAlgorithmName)1216 public static int getMinSdkVersionFromWhichSupportedInManifestOrSignatureFile( 1217 String jcaAlgorithmName) { 1218 Integer result = 1219 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.get( 1220 jcaAlgorithmName.toUpperCase(Locale.US)); 1221 return (result != null) ? result : Integer.MAX_VALUE; 1222 } 1223 getJarDigestAttributeName( String jcaDigestAlgorithm, String attrNameSuffix)1224 private static String getJarDigestAttributeName( 1225 String jcaDigestAlgorithm, String attrNameSuffix) { 1226 if ("SHA-1".equalsIgnoreCase(jcaDigestAlgorithm)) { 1227 return "SHA1" + attrNameSuffix; 1228 } else { 1229 return jcaDigestAlgorithm + attrNameSuffix; 1230 } 1231 } 1232 1233 private static final Map<String, String> UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL; 1234 static { 1235 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL = new HashMap<>(8); 1236 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("MD5", "MD5"); 1237 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA", "SHA-1"); 1238 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA1", "SHA-1"); 1239 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-1", "SHA-1"); 1240 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-256", "SHA-256"); 1241 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-384", "SHA-384"); 1242 UPPER_CASE_JCA_DIGEST_ALG_TO_CANONICAL.put("SHA-512", "SHA-512"); 1243 } 1244 1245 private static final Map<String, Integer> 1246 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST; 1247 static { 1248 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST = new HashMap<>(5); 1249 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("MD5", 0); 1250 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-1", 0); 1251 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put("SHA-256", 0); 1252 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( 1253 "SHA-384", AndroidSdkVersion.GINGERBREAD); 1254 MIN_SDK_VESION_FROM_WHICH_DIGEST_SUPPORTED_IN_MANIFEST.put( 1255 "SHA-512", AndroidSdkVersion.GINGERBREAD); 1256 } 1257 getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm)1258 private static byte[] getDigest(Collection<NamedDigest> digests, String jcaDigestAlgorithm) { 1259 for (NamedDigest digest : digests) { 1260 if (digest.jcaDigestAlgorithm.equalsIgnoreCase(jcaDigestAlgorithm)) { 1261 return digest.digest; 1262 } 1263 } 1264 return null; 1265 } 1266 parseZipCentralDirectory( DataSource apk, ApkUtils.ZipSections apkSections)1267 public static List<CentralDirectoryRecord> parseZipCentralDirectory( 1268 DataSource apk, 1269 ApkUtils.ZipSections apkSections) 1270 throws IOException, ApkFormatException { 1271 return ZipUtils.parseZipCentralDirectory(apk, apkSections); 1272 } 1273 1274 /** 1275 * Returns {@code true} if the provided JAR entry must be mentioned in signed JAR archive's 1276 * manifest for the APK to verify on Android. 1277 */ isJarEntryDigestNeededInManifest(String entryName)1278 private static boolean isJarEntryDigestNeededInManifest(String entryName) { 1279 // NOTE: This logic is different from what's required by the JAR signing scheme. This is 1280 // because Android's APK verification logic differs from that spec. In particular, JAR 1281 // signing spec includes into JAR manifest all files in subdirectories of META-INF and 1282 // any files inside META-INF not related to signatures. 1283 if (entryName.startsWith("META-INF/")) { 1284 return false; 1285 } 1286 return !entryName.endsWith("/"); 1287 } 1288 verifyJarEntriesAgainstManifestAndSigners( DataSource apk, long cdOffsetInApk, Collection<CentralDirectoryRecord> cdRecords, Map<String, ManifestParser.Section> entryNameToManifestSection, List<Signer> signers, int minSdkVersion, int maxSdkVersion, Result result)1289 private static Set<Signer> verifyJarEntriesAgainstManifestAndSigners( 1290 DataSource apk, 1291 long cdOffsetInApk, 1292 Collection<CentralDirectoryRecord> cdRecords, 1293 Map<String, ManifestParser.Section> entryNameToManifestSection, 1294 List<Signer> signers, 1295 int minSdkVersion, 1296 int maxSdkVersion, 1297 Result result) throws ApkFormatException, IOException, NoSuchAlgorithmException { 1298 // Iterate over APK contents as sequentially as possible to improve performance. 1299 List<CentralDirectoryRecord> cdRecordsSortedByLocalFileHeaderOffset = 1300 new ArrayList<>(cdRecords); 1301 Collections.sort( 1302 cdRecordsSortedByLocalFileHeaderOffset, 1303 CentralDirectoryRecord.BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR); 1304 List<Signer> firstSignedEntrySigners = null; 1305 String firstSignedEntryName = null; 1306 for (CentralDirectoryRecord cdRecord : cdRecordsSortedByLocalFileHeaderOffset) { 1307 String entryName = cdRecord.getName(); 1308 if (!isJarEntryDigestNeededInManifest(entryName)) { 1309 continue; 1310 } 1311 1312 ManifestParser.Section manifestSection = entryNameToManifestSection.get(entryName); 1313 if (manifestSection == null) { 1314 result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); 1315 continue; 1316 } 1317 1318 List<Signer> entrySigners = new ArrayList<>(signers.size()); 1319 for (Signer signer : signers) { 1320 if (signer.getSigFileEntryNames().contains(entryName)) { 1321 entrySigners.add(signer); 1322 } 1323 } 1324 if (entrySigners.isEmpty()) { 1325 result.addError(Issue.JAR_SIG_ZIP_ENTRY_NOT_SIGNED, entryName); 1326 continue; 1327 } 1328 if (firstSignedEntrySigners == null) { 1329 firstSignedEntrySigners = entrySigners; 1330 firstSignedEntryName = entryName; 1331 } else if (!entrySigners.equals(firstSignedEntrySigners)) { 1332 result.addError( 1333 Issue.JAR_SIG_ZIP_ENTRY_SIGNERS_MISMATCH, 1334 firstSignedEntryName, 1335 getSignerNames(firstSignedEntrySigners), 1336 entryName, 1337 getSignerNames(entrySigners)); 1338 continue; 1339 } 1340 1341 List<NamedDigest> expectedDigests = 1342 new ArrayList<>( 1343 getDigestsToVerify( 1344 manifestSection, "-Digest", minSdkVersion, maxSdkVersion)); 1345 if (expectedDigests.isEmpty()) { 1346 result.addError(Issue.JAR_SIG_NO_ZIP_ENTRY_DIGEST_IN_MANIFEST, entryName); 1347 continue; 1348 } 1349 1350 MessageDigest[] mds = new MessageDigest[expectedDigests.size()]; 1351 for (int i = 0; i < expectedDigests.size(); i++) { 1352 mds[i] = getMessageDigest(expectedDigests.get(i).jcaDigestAlgorithm); 1353 } 1354 1355 try { 1356 LocalFileRecord.outputUncompressedData( 1357 apk, 1358 cdRecord, 1359 cdOffsetInApk, 1360 DataSinks.asDataSink(mds)); 1361 } catch (ZipFormatException e) { 1362 throw new ApkFormatException("Malformed ZIP entry: " + entryName, e); 1363 } catch (IOException e) { 1364 throw new IOException("Failed to read entry: " + entryName, e); 1365 } 1366 1367 for (int i = 0; i < expectedDigests.size(); i++) { 1368 NamedDigest expectedDigest = expectedDigests.get(i); 1369 byte[] actualDigest = mds[i].digest(); 1370 if (!Arrays.equals(expectedDigest.digest, actualDigest)) { 1371 result.addError( 1372 Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, 1373 entryName, 1374 expectedDigest.jcaDigestAlgorithm, 1375 V1SchemeConstants.MANIFEST_ENTRY_NAME, 1376 Base64.getEncoder().encodeToString(actualDigest), 1377 Base64.getEncoder().encodeToString(expectedDigest.digest)); 1378 } 1379 } 1380 } 1381 1382 if (firstSignedEntrySigners == null) { 1383 result.addError(Issue.JAR_SIG_NO_SIGNED_ZIP_ENTRIES); 1384 return Collections.emptySet(); 1385 } else { 1386 return new HashSet<>(firstSignedEntrySigners); 1387 } 1388 } 1389 getSignerNames(List<Signer> signers)1390 private static List<String> getSignerNames(List<Signer> signers) { 1391 if (signers.isEmpty()) { 1392 return Collections.emptyList(); 1393 } 1394 List<String> result = new ArrayList<>(signers.size()); 1395 for (Signer signer : signers) { 1396 result.add(signer.getName()); 1397 } 1398 return result; 1399 } 1400 getMessageDigest(String algorithm)1401 private static MessageDigest getMessageDigest(String algorithm) 1402 throws NoSuchAlgorithmException { 1403 return MessageDigest.getInstance(algorithm); 1404 } 1405 digest(String algorithm, byte[] data, int offset, int length)1406 private static byte[] digest(String algorithm, byte[] data, int offset, int length) 1407 throws NoSuchAlgorithmException { 1408 MessageDigest md = getMessageDigest(algorithm); 1409 md.update(data, offset, length); 1410 return md.digest(); 1411 } 1412 digest(String algorithm, byte[] data)1413 private static byte[] digest(String algorithm, byte[] data) throws NoSuchAlgorithmException { 1414 return getMessageDigest(algorithm).digest(data); 1415 } 1416 1417 public static class NamedDigest { 1418 public final String jcaDigestAlgorithm; 1419 public final byte[] digest; 1420 NamedDigest(String jcaDigestAlgorithm, byte[] digest)1421 private NamedDigest(String jcaDigestAlgorithm, byte[] digest) { 1422 this.jcaDigestAlgorithm = jcaDigestAlgorithm; 1423 this.digest = digest; 1424 } 1425 } 1426 1427 public static class Result { 1428 1429 /** Whether the APK's JAR signature verifies. */ 1430 public boolean verified; 1431 1432 /** List of APK's signers. These signers are used by Android. */ 1433 public final List<SignerInfo> signers = new ArrayList<>(); 1434 1435 /** 1436 * Signers encountered in the APK but not included in the set of the APK's signers. These 1437 * signers are ignored by Android. 1438 */ 1439 public final List<SignerInfo> ignoredSigners = new ArrayList<>(); 1440 1441 private final List<IssueWithParams> mWarnings = new ArrayList<>(); 1442 private final List<IssueWithParams> mErrors = new ArrayList<>(); 1443 containsErrors()1444 private boolean containsErrors() { 1445 if (!mErrors.isEmpty()) { 1446 return true; 1447 } 1448 for (SignerInfo signer : signers) { 1449 if (signer.containsErrors()) { 1450 return true; 1451 } 1452 } 1453 return false; 1454 } 1455 addError(Issue msg, Object... parameters)1456 private void addError(Issue msg, Object... parameters) { 1457 mErrors.add(new IssueWithParams(msg, parameters)); 1458 } 1459 addWarning(Issue msg, Object... parameters)1460 private void addWarning(Issue msg, Object... parameters) { 1461 mWarnings.add(new IssueWithParams(msg, parameters)); 1462 } 1463 getErrors()1464 public List<IssueWithParams> getErrors() { 1465 return mErrors; 1466 } 1467 getWarnings()1468 public List<IssueWithParams> getWarnings() { 1469 return mWarnings; 1470 } 1471 1472 public static class SignerInfo { 1473 public final String name; 1474 public final String signatureFileName; 1475 public final String signatureBlockFileName; 1476 public final List<X509Certificate> certChain = new ArrayList<>(); 1477 1478 private final List<IssueWithParams> mWarnings = new ArrayList<>(); 1479 private final List<IssueWithParams> mErrors = new ArrayList<>(); 1480 SignerInfo( String name, String signatureBlockFileName, String signatureFileName)1481 private SignerInfo( 1482 String name, String signatureBlockFileName, String signatureFileName) { 1483 this.name = name; 1484 this.signatureBlockFileName = signatureBlockFileName; 1485 this.signatureFileName = signatureFileName; 1486 } 1487 containsErrors()1488 private boolean containsErrors() { 1489 return !mErrors.isEmpty(); 1490 } 1491 addError(Issue msg, Object... parameters)1492 private void addError(Issue msg, Object... parameters) { 1493 mErrors.add(new IssueWithParams(msg, parameters)); 1494 } 1495 addWarning(Issue msg, Object... parameters)1496 private void addWarning(Issue msg, Object... parameters) { 1497 mWarnings.add(new IssueWithParams(msg, parameters)); 1498 } 1499 getErrors()1500 public List<IssueWithParams> getErrors() { 1501 return mErrors; 1502 } 1503 getWarnings()1504 public List<IssueWithParams> getWarnings() { 1505 return mWarnings; 1506 } 1507 } 1508 } 1509 1510 private static class SignedAttributes { 1511 private Map<String, List<Asn1OpaqueObject>> mAttrs; 1512 SignedAttributes(Collection<Attribute> attrs)1513 public SignedAttributes(Collection<Attribute> attrs) throws Pkcs7DecodingException { 1514 Map<String, List<Asn1OpaqueObject>> result = new HashMap<>(attrs.size()); 1515 for (Attribute attr : attrs) { 1516 if (result.put(attr.attrType, attr.attrValues) != null) { 1517 throw new Pkcs7DecodingException("Duplicate signed attribute: " + attr.attrType); 1518 } 1519 } 1520 mAttrs = result; 1521 } 1522 getSingleValue(String attrOid)1523 private Asn1OpaqueObject getSingleValue(String attrOid) throws Pkcs7DecodingException { 1524 List<Asn1OpaqueObject> values = mAttrs.get(attrOid); 1525 if ((values == null) || (values.isEmpty())) { 1526 return null; 1527 } 1528 if (values.size() > 1) { 1529 throw new Pkcs7DecodingException("Attribute " + attrOid + " has multiple values"); 1530 } 1531 return values.get(0); 1532 } 1533 getSingleObjectIdentifierValue(String attrOid)1534 public String getSingleObjectIdentifierValue(String attrOid) throws Pkcs7DecodingException { 1535 Asn1OpaqueObject value = getSingleValue(attrOid); 1536 if (value == null) { 1537 return null; 1538 } 1539 try { 1540 return Asn1BerParser.parse(value.getEncoded(), ObjectIdentifierChoice.class).value; 1541 } catch (Asn1DecodingException e) { 1542 throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); 1543 } 1544 } 1545 getSingleOctetStringValue(String attrOid)1546 public byte[] getSingleOctetStringValue(String attrOid) throws Pkcs7DecodingException { 1547 Asn1OpaqueObject value = getSingleValue(attrOid); 1548 if (value == null) { 1549 return null; 1550 } 1551 try { 1552 return Asn1BerParser.parse(value.getEncoded(), OctetStringChoice.class).value; 1553 } catch (Asn1DecodingException e) { 1554 throw new Pkcs7DecodingException("Failed to decode OBJECT IDENTIFIER", e); 1555 } 1556 } 1557 } 1558 1559 @Asn1Class(type = Asn1Type.CHOICE) 1560 public static class OctetStringChoice { 1561 @Asn1Field(type = Asn1Type.OCTET_STRING) 1562 public byte[] value; 1563 } 1564 1565 @Asn1Class(type = Asn1Type.CHOICE) 1566 public static class ObjectIdentifierChoice { 1567 @Asn1Field(type = Asn1Type.OBJECT_IDENTIFIER) 1568 public String value; 1569 } 1570 } 1571