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