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