diff options
author | android-build-team Robot <android-build-team-robot@google.com> | 2020-11-03 18:05:42 +0000 |
---|---|---|
committer | android-build-team Robot <android-build-team-robot@google.com> | 2020-11-03 18:05:42 +0000 |
commit | c73e6d6d85cfe88508f93ebf18161e74e5602fc0 (patch) | |
tree | bc77c65ac96177d0ea7105734008db689a906a5c | |
parent | fdd4d6e88a25b47687a54818e05e7a42ea80afbd (diff) | |
parent | 1c02e15450fc0c6e9e481685c0fdf5cd3a6418c7 (diff) | |
download | apksig-android-platform-11.0.0_r4.tar.gz |
Snap for 6948038 from 1c02e15450fc0c6e9e481685c0fdf5cd3a6418c7 to rvc-platform-releaseandroid-platform-11.0.0_r9android-platform-11.0.0_r8android-platform-11.0.0_r7android-platform-11.0.0_r6android-platform-11.0.0_r5android-platform-11.0.0_r4android-platform-11.0.0_r31android-platform-11.0.0_r30android-platform-11.0.0_r3android-platform-11.0.0_r29android-platform-11.0.0_r28android-platform-11.0.0_r27android-platform-11.0.0_r26android-platform-11.0.0_r25android-platform-11.0.0_r24android-platform-11.0.0_r23android-platform-11.0.0_r22android-platform-11.0.0_r21android-platform-11.0.0_r20android-platform-11.0.0_r19android-platform-11.0.0_r18android-platform-11.0.0_r17android-platform-11.0.0_r16android-platform-11.0.0_r15android-platform-11.0.0_r14android-platform-11.0.0_r13android-platform-11.0.0_r12android-platform-11.0.0_r11android-platform-11.0.0_r10
Change-Id: I2a2de9e0b10434d652896a097bcd1c5bbbf2d057
54 files changed, 4718 insertions, 987 deletions
@@ -21,6 +21,7 @@ java_library_host { srcs: [ "src/main/java/**/*.java", ], + java_version: "1.8", } // apksigner command-line tool for signing APKs and verifying their signatures @@ -36,4 +37,5 @@ java_binary_host { "conscrypt-unbundled", ], required: ["libconscrypt_openjdk_jni"], + java_version: "1.8", } diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java index 2f4e680..c7cb660 100644 --- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java +++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java @@ -147,8 +147,9 @@ public class ApkSignerTool { int maxSdkVersion = Integer.MAX_VALUE; List<SignerParams> signers = new ArrayList<>(1); SignerParams signerParams = new SignerParams(); - SignerParams sourceStampSignerParams = new SignerParams(); SigningCertificateLineage lineage = null; + SignerParams sourceStampSignerParams = new SignerParams(); + SigningCertificateLineage sourceStampLineage = null; List<ProviderInstallSpec> providers = new ArrayList<>(); ProviderInstallSpec providerParams = new ProviderInstallSpec(); OptionsParser optionsParser = new OptionsParser(params); @@ -252,6 +253,10 @@ public class ApkSignerTool { } else if ("stamp-signer".equals(optionName)) { sourceStampFlagFound = true; sourceStampSignerParams = processSignerParams(optionsParser); + } else if ("stamp-lineage".equals(optionName)) { + File stampLineageFile = new File( + optionsParser.getRequiredValue("Stamp Lineage File")); + sourceStampLineage = getLineageFromInputFile(stampLineageFile); } else { throw new ParameterException( "Unsupported option: " + optionOriginalForm + ". See --help for supported" @@ -358,7 +363,8 @@ public class ApkSignerTool { apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile); } if (sourceStampSignerConfig != null) { - apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig); + apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig) + .setSourceStampSigningCertificateLineage(sourceStampLineage); } ApkSigner apkSigner = apkSignerBuilder.build(); try { @@ -435,10 +441,12 @@ public class ApkSignerTool { boolean printCerts = false; boolean verbose = false; boolean warningsTreatedAsErrors = false; + boolean verifySourceStamp = false; File v4SignatureFile = null; OptionsParser optionsParser = new OptionsParser(params); String optionName; String optionOriginalForm = null; + String sourceCertDigest = null; while ((optionName = optionsParser.nextOption()) != null) { optionOriginalForm = optionsParser.getOptionOriginalForm(); if ("min-sdk-version".equals(optionName)) { @@ -461,6 +469,11 @@ public class ApkSignerTool { "Input V4 Signature File")); } else if ("in".equals(optionName)) { inputApk = new File(optionsParser.getRequiredValue("Input APK file")); + } else if ("verify-source-stamp".equals(optionName)) { + verifySourceStamp = optionsParser.getOptionalBooleanValue(true); + } else if ("stamp-cert-digest".equals(optionName)) { + sourceCertDigest = optionsParser.getRequiredValue( + "Expected source stamp certificate digest"); } else { throw new ParameterException( "Unsupported option: " + optionOriginalForm + ". See --help for supported" @@ -513,7 +526,9 @@ public class ApkSignerTool { ApkVerifier apkVerifier = apkVerifierBuilder.build(); ApkVerifier.Result result; try { - result = apkVerifier.verify(); + result = verifySourceStamp + ? apkVerifier.verifySourceStamp(sourceCertDigest) + : apkVerifier.verify(); } catch (MinSdkVersionException e) { String msg = e.getMessage(); if (!msg.endsWith(".")) { @@ -524,8 +539,9 @@ public class ApkSignerTool { + ". Use --min-sdk-version to override", e); } - boolean verified = result.isVerified(); + boolean verified = result.isVerified(); + ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo(); boolean warningsEncountered = false; if (verified) { List<X509Certificate> signerCerts = result.getSignerCertificates(); @@ -544,7 +560,9 @@ public class ApkSignerTool { "Verified using v4 scheme (APK Signature Scheme v4): " + result.isVerifiedUsingV4Scheme()); System.out.println("Verified for SourceStamp: " + result.isSourceStampVerified()); - System.out.println("Number of signers: " + signerCerts.size()); + if (!verifySourceStamp) { + System.out.println("Number of signers: " + signerCerts.size()); + } } if (printCerts) { int signerNumber = 0; @@ -552,6 +570,10 @@ public class ApkSignerTool { signerNumber++; printCertificate(signerCert, "Signer #" + signerNumber, verbose); } + if (sourceStampInfo != null) { + printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer", + verbose); + } } } else { System.err.println("DOES NOT VERIFY"); @@ -562,7 +584,7 @@ public class ApkSignerTool { } @SuppressWarnings("resource") // false positive -- this resource is not opened here - PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out; + PrintStream warningsOut = warningsTreatedAsErrors ? System.err : System.out; for (ApkVerifier.IssueWithParams warning : result.getWarnings()) { warningsEncountered = true; warningsOut.println("WARNING: " + warning); @@ -602,7 +624,6 @@ public class ApkSignerTool { } } - ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo(); if (sourceStampInfo != null) { for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) { System.err.println("ERROR: SourceStamp: " + error); @@ -961,10 +982,10 @@ public class ApkSignerTool { private static void printUsage(String page) { try (BufferedReader in = - new BufferedReader( - new InputStreamReader( - ApkSignerTool.class.getResourceAsStream(page), - StandardCharsets.UTF_8))) { + new BufferedReader( + new InputStreamReader( + ApkSignerTool.class.getResourceAsStream(page), + StandardCharsets.UTF_8))) { String line; while ((line = in.readLine()) != null) { System.out.println(line); @@ -981,7 +1002,6 @@ public class ApkSignerTool { * @param name the name to be used to identify the certificate. * @param verbose boolean indicating whether public key details from the certificate should be * displayed. - * * @throws NoSuchAlgorithmException if an instance of MD5, SHA-1, or SHA-256 cannot be * obtained. * @throws CertificateEncodingException if an error is encountered when encoding the diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java index 154e917..d4da569 100644 --- a/src/main/java/com/android/apksig/ApkSigner.java +++ b/src/main/java/com/android/apksig/ApkSigner.java @@ -86,6 +86,7 @@ public class ApkSigner { private final List<SignerConfig> mSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; private final boolean mForceSourceStampOverwrite; private final Integer mMinSdkVersion; private final boolean mV1SigningEnabled; @@ -114,6 +115,7 @@ public class ApkSigner { private ApkSigner( List<SignerConfig> signerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, boolean forceSourceStampOverwrite, Integer minSdkVersion, boolean v1SigningEnabled, @@ -136,6 +138,7 @@ public class ApkSigner { mSignerConfigs = signerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; mForceSourceStampOverwrite = forceSourceStampOverwrite; mMinSdkVersion = minSdkVersion; mV1SigningEnabled = v1SigningEnabled; @@ -304,6 +307,10 @@ public class ApkSigner { mSourceStampSignerConfig.getCertificates()) .build()); } + if (mSourceStampSigningCertificateLineage != null) { + signerEngineBuilder.setSourceStampSigningCertificateLineage( + mSourceStampSigningCertificateLineage); + } signerEngine = signerEngineBuilder.build(); } @@ -1022,6 +1029,7 @@ public class ApkSigner { public static class Builder { private final List<SignerConfig> mSignerConfigs; private SignerConfig mSourceStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; private boolean mForceSourceStampOverwrite = false; private boolean mV1SigningEnabled = true; private boolean mV2SigningEnabled = true; @@ -1101,6 +1109,16 @@ public class ApkSigner { } /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + + /** * Sets whether the APK should overwrite existing source stamp, if found. * * @param force {@code true} to require the APK to be overwrite existing source stamp @@ -1465,6 +1483,7 @@ public class ApkSigner { return new ApkSigner( mSignerConfigs, mSourceStampSignerConfig, + mSourceStampSigningCertificateLineage, mForceSourceStampOverwrite, mMinSdkVersion, mV1SigningEnabled, diff --git a/src/main/java/com/android/apksig/ApkVerificationIssue.java b/src/main/java/com/android/apksig/ApkVerificationIssue.java new file mode 100644 index 0000000..2aa9d0b --- /dev/null +++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +/** + * This class is intended as a lightweight representation of an APK signature verification issue + * where the client does not require the additional textual details provided by a subclass. + */ +public class ApkVerificationIssue { + /* The V2 signer(s) could not be read from the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNERS = 1; + /* A V2 signature block exists without any V2 signers */ + public static final int V2_SIG_NO_SIGNERS = 2; + /* Failed to parse a signer's block in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNER = 3; + /* Failed to parse the signer's signature record in the V2 signature block */ + public static final int V2_SIG_MALFORMED_SIGNATURE = 4; + /* The V2 signer contained no signatures */ + public static final int V2_SIG_NO_SIGNATURES = 5; + /* The V2 signer's certificate could not be parsed */ + public static final int V2_SIG_MALFORMED_CERTIFICATE = 6; + /* No signing certificates exist for the V2 signer */ + public static final int V2_SIG_NO_CERTIFICATES = 7; + /* Failed to parse the V2 signer's digest record */ + public static final int V2_SIG_MALFORMED_DIGEST = 8; + /* The V3 signer(s) could not be read from the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNERS = 9; + /* A V3 signature block exists without any V3 signers */ + public static final int V3_SIG_NO_SIGNERS = 10; + /* Failed to parse a signer's block in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNER = 11; + /* Failed to parse the signer's signature record in the V3 signature block */ + public static final int V3_SIG_MALFORMED_SIGNATURE = 12; + /* The V3 signer contained no signatures */ + public static final int V3_SIG_NO_SIGNATURES = 13; + /* The V3 signer's certificate could not be parsed */ + public static final int V3_SIG_MALFORMED_CERTIFICATE = 14; + /* No signing certificates exist for the V3 signer */ + public static final int V3_SIG_NO_CERTIFICATES = 15; + /* Failed to parse the V3 signer's digest record */ + public static final int V3_SIG_MALFORMED_DIGEST = 16; + /* The source stamp signer contained no signatures */ + public static final int SOURCE_STAMP_NO_SIGNATURE = 17; + /* The source stamp signer's certificate could not be parsed */ + public static final int SOURCE_STAMP_MALFORMED_CERTIFICATE = 18; + /* The source stamp contains a signature produced using an unknown algorithm */ + public static final int SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM = 19; + /* Failed to parse the signer's signature in the source stamp signature block */ + public static final int SOURCE_STAMP_MALFORMED_SIGNATURE = 20; + /* The source stamp's signature block failed verification */ + public static final int SOURCE_STAMP_DID_NOT_VERIFY = 21; + /* An exception was encountered when verifying the source stamp */ + public static final int SOURCE_STAMP_VERIFY_EXCEPTION = 22; + /* The certificate digest in the APK does not match the expected digest */ + public static final int SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH = 23; + /* + * The APK contains a source stamp signature block without a corresponding stamp certificate + * digest in the APK contents. + */ + public static final int SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST = 24; + /* + * The APK does not contain the source stamp certificate digest file nor the source stamp + * signature block. + */ + public static final int SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING = 25; + /* + * None of the signatures provided by the source stamp were produced with a known signature + * algorithm. + */ + public static final int SOURCE_STAMP_NO_SUPPORTED_SIGNATURE = 26; + /* + * The source stamp signer's certificate in the signing block does not match the certificate in + * the APK. + */ + public static final int SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK = 27; + /* The APK could not be properly parsed due to a ZIP or APK format exception */ + public static final int MALFORMED_APK = 28; + /* An unexpected exception was caught when attempting to verify the APK's signatures */ + public static final int UNEXPECTED_EXCEPTION = 29; + /* The APK contains the certificate digest file but does not contain a stamp signature block */ + public static final int SOURCE_STAMP_SIG_MISSING = 30; + /* Source stamp block contains a malformed attribute. */ + public static final int SOURCE_STAMP_MALFORMED_ATTRIBUTE = 31; + /* Source stamp block contains an unknown attribute. */ + public static final int SOURCE_STAMP_UNKNOWN_ATTRIBUTE = 32; + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + public static final int SOURCE_STAMP_MALFORMED_LINEAGE = 33; + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + public static final int SOURCE_STAMP_POR_CERT_MISMATCH = 34; + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + public static final int SOURCE_STAMP_POR_DID_NOT_VERIFY = 35; + /** No V1 / jar signing signature blocks were found in the APK. */ + public static final int JAR_SIG_NO_SIGNATURES = 36; + /** An exception was encountered when parsing the V1 / jar signer in the signature block. */ + public static final int JAR_SIG_PARSE_EXCEPTION = 37; + + private final int mIssueId; + private final String mFormat; + private final Object[] mParams; + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code format} string and + * {@code params}. + */ + public ApkVerificationIssue(String format, Object... params) { + mIssueId = -1; + mFormat = format; + mParams = params; + } + + /** + * Constructs a new {@code ApkVerificationIssue} using the provided {@code issueId} and {@code + * params}. + */ + public ApkVerificationIssue(int issueId, Object... params) { + mIssueId = issueId; + mFormat = null; + mParams = params; + } + + /** + * Returns the numeric ID for this issue. + */ + public int getIssueId() { + return mIssueId; + } + + /** + * Returns the optional parameters for this issue. + */ + public Object[] getParams() { + return mParams; + } + + @Override + public String toString() { + // If this instance was created by a subclass with a format string then return the same + // formatted String as the subclass. + if (mFormat != null) { + return String.format(mFormat, mParams); + } + StringBuilder result = new StringBuilder("mIssueId: ").append(mIssueId); + for (Object param : mParams) { + result.append(", ").append(param.toString()); + } + return result.toString(); + } +} diff --git a/src/main/java/com/android/apksig/ApkVerifier.java b/src/main/java/com/android/apksig/ApkVerifier.java index f2d0fbc..354dfbd 100644 --- a/src/main/java/com/android/apksig/ApkVerifier.java +++ b/src/main/java/com/android/apksig/ApkVerifier.java @@ -18,20 +18,28 @@ package com.android.apksig; import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; import static com.android.apksig.apk.ApkUtils.computeSha256DigestBytes; +import static com.android.apksig.apk.ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest; +import static com.android.apksig.apk.ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME; -import static com.android.apksig.internal.apk.v1.V1SchemeSigner.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; -import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; import com.android.apksig.internal.apk.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.apk.v4.V4SchemeVerifier; import com.android.apksig.internal.util.AndroidSdkVersion; @@ -53,6 +61,7 @@ import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -75,7 +84,7 @@ public class ApkVerifier { private static final Map<Integer, String> SUPPORTED_APK_SIG_SCHEME_NAMES = loadSupportedApkSigSchemeNames(); - private static Map<Integer,String> loadSupportedApkSigSchemeNames() { + private static Map<Integer, String> loadSupportedApkSigSchemeNames() { Map<Integer, String> supportedMap = new HashMap<>(2); supportedMap.put( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, "APK Signature Scheme v2"); @@ -116,12 +125,12 @@ public class ApkVerifier { * or more errors and whose {@link Result#isVerified()} returns {@code false}, or this method * throws an exception. * - * @throws IOException if an I/O error is encountered while reading the APK - * @throws ApkFormatException if the APK is malformed + * @throws IOException if an I/O error is encountered while reading the APK + * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing - * @throws IllegalStateException if this verifier's configuration is missing required - * information. + * required cryptographic algorithm implementation is missing + * @throws IllegalStateException if this verifier's configuration is missing required + * information. */ public Result verify() throws IOException, ApkFormatException, NoSuchAlgorithmException, IllegalStateException { @@ -151,25 +160,13 @@ public class ApkVerifier { * The verification result also includes errors, warnings, and information about signers. * * @param apk APK file contents - * - * @throws IOException if an I/O error is encountered while reading the APK - * @throws ApkFormatException if the APK is malformed + * @throws IOException if an I/O error is encountered while reading the APK + * @throws ApkFormatException if the APK is malformed * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a - * required cryptographic algorithm implementation is missing + * required cryptographic algorithm implementation is missing */ private Result verify(DataSource apk) throws IOException, ApkFormatException, NoSuchAlgorithmException { - if (mMinSdkVersion != null) { - if (mMinSdkVersion < 0) { - throw new IllegalArgumentException( - "minSdkVersion must not be negative: " + mMinSdkVersion); - } - if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { - throw new IllegalArgumentException( - "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion - + ")"); - } - } int maxSdkVersion = mMaxSdkVersion; ApkUtils.ZipSections zipSections; @@ -181,23 +178,7 @@ public class ApkVerifier { ByteBuffer androidManifest = null; - int minSdkVersion; - if (mMinSdkVersion != null) { - // No need to obtain minSdkVersion from the APK's AndroidManifest.xml - minSdkVersion = mMinSdkVersion; - } else { - // Need to obtain minSdkVersion from the APK's AndroidManifest.xml - if (androidManifest == null) { - androidManifest = getAndroidManifestFromApk(apk, zipSections); - } - minSdkVersion = - ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); - if (minSdkVersion > mMaxSdkVersion) { - throw new IllegalArgumentException( - "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" - + mMaxSdkVersion + ")"); - } - } + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); Result result = new Result(); Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = @@ -212,17 +193,8 @@ public class ApkVerifier { // verification, but the SUPPORTED_APK_SIG_SCHEME_NAMES contains version 3, so when the V2 // verification is performed it would see the stripping protection attribute, see that V3 // is in the list of supported signatures, and report a stripped signature. - Map<Integer, String> supportedSchemeNames; - if (maxSdkVersion >= AndroidSdkVersion.P) { - supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; - } else if (maxSdkVersion >= AndroidSdkVersion.N) { - supportedSchemeNames = new HashMap<>(1); - supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, - SUPPORTED_APK_SIG_SCHEME_NAMES.get( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); - } else { - supportedSchemeNames = Collections.emptyMap(); - } + Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(maxSdkVersion); + // Android N and newer attempts to verify APKs using the APK Signing Block, which can // include v2 and/or v3 signatures. If none is found, it falls back to JAR signature // verification. If the signature is found but does not verify, the APK is rejected. @@ -353,7 +325,7 @@ public class ApkVerifier { apk, sourceStampCdRecord, zipSections.getZipCentralDirectoryOffset()); - ApkSigningBlockUtils.Result sourceStampResult = + ApkSigResult sourceStampResult = V2SourceStampVerifier.verify( apk, zipSections, @@ -363,7 +335,7 @@ public class ApkVerifier { maxSdkVersion); result.mergeFrom(sourceStampResult); } - } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + } catch (SignatureNotFoundException ignored) { result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); } catch (ZipFormatException e) { throw new ApkFormatException("Failed to read APK", e); @@ -453,7 +425,7 @@ public class ApkVerifier { } try { if (!Arrays.equals(oldSignerCert.getEncoded(), - v3Signers.get(0).mCerts.get(0).getEncoded())) { + v3Signers.get(0).mCerts.get(0).getEncoded())) { result.addError(Issue.V3_SIG_PAST_SIGNERS_MISMATCH); } } catch (CertificateEncodingException e) { @@ -530,29 +502,38 @@ public class ApkVerifier { // If the targetSdkVersion has a minimum required signature scheme version then verify // that the APK was signed with at least that version. - if (androidManifest == null) { - androidManifest = getAndroidManifestFromApk(apk, zipSections); - } - int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( - androidManifest.slice()); - int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); - // The platform currently only enforces a single minimum signature scheme version, but when - // later platform versions support another minimum version this will need to be expanded to - // verify the minimum based on the target and maximum SDK version. - if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME && maxSdkVersion >= targetSdkVersion) { - switch(minSchemeVersion) { - case VERSION_APK_SIGNATURE_SCHEME_V2: - if (result.isVerifiedUsingV2Scheme()) { - break; - } - // Allow this case to fall through to the next as a signature satisfying a later - // scheme version will also satisfy this requirement. - case VERSION_APK_SIGNATURE_SCHEME_V3: - if (result.isVerifiedUsingV3Scheme()) { - break; - } - result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, targetSdkVersion, - minSchemeVersion); + try { + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + } catch (ApkFormatException e) { + // If the manifest is not available then skip the minimum signature scheme requirement + // to support bundle verification. + } + if (androidManifest != null) { + int targetSdkVersion = getTargetSdkVersionFromBinaryAndroidManifest( + androidManifest.slice()); + int minSchemeVersion = getMinimumSignatureSchemeVersionForTargetSdk(targetSdkVersion); + // The platform currently only enforces a single minimum signature scheme version, but + // when later platform versions support another minimum version this will need to be + // expanded to verify the minimum based on the target and maximum SDK version. + if (minSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME + && maxSdkVersion >= targetSdkVersion) { + switch (minSchemeVersion) { + case VERSION_APK_SIGNATURE_SCHEME_V2: + if (result.isVerifiedUsingV2Scheme()) { + break; + } + // Allow this case to fall through to the next as a signature satisfying a + // later scheme version will also satisfy this requirement. + case VERSION_APK_SIGNATURE_SCHEME_V3: + if (result.isVerifiedUsingV3Scheme()) { + break; + } + result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, + targetSdkVersion, + minSchemeVersion); + } } } @@ -581,7 +562,341 @@ public class ApkVerifier { return result; } - private static void checkV4Certificate(List<X509Certificate> v4Certs, List<X509Certificate> v2v3Certs, Result result) { + /** + * Verifies and returns the minimum SDK version, either as provided to the builder or as read + * from the {@code apk}'s AndroidManifest.xml. + */ + private int verifyAndGetMinSdkVersion(DataSource apk, ApkUtils.ZipSections zipSections) + throws ApkFormatException, IOException { + if (mMinSdkVersion != null) { + if (mMinSdkVersion < 0) { + throw new IllegalArgumentException( + "minSdkVersion must not be negative: " + mMinSdkVersion); + } + if ((mMinSdkVersion != null) && (mMinSdkVersion > mMaxSdkVersion)) { + throw new IllegalArgumentException( + "minSdkVersion (" + mMinSdkVersion + ") > maxSdkVersion (" + mMaxSdkVersion + + ")"); + } + return mMinSdkVersion; + } + + ByteBuffer androidManifest = null; + // Need to obtain minSdkVersion from the APK's AndroidManifest.xml + if (androidManifest == null) { + androidManifest = getAndroidManifestFromApk(apk, zipSections); + } + int minSdkVersion = + ApkUtils.getMinSdkVersionFromBinaryAndroidManifest(androidManifest.slice()); + if (minSdkVersion > mMaxSdkVersion) { + throw new IllegalArgumentException( + "minSdkVersion from APK (" + minSdkVersion + ") > maxSdkVersion (" + + mMaxSdkVersion + ")"); + } + return minSdkVersion; + } + + /** + * Returns the mapping of signature scheme version to signature scheme name for all signature + * schemes starting from V2 supported by the {@code maxSdkVersion}. + */ + private static Map<Integer, String> getSupportedSchemeNames(int maxSdkVersion) { + Map<Integer, String> supportedSchemeNames; + if (maxSdkVersion >= AndroidSdkVersion.P) { + supportedSchemeNames = SUPPORTED_APK_SIG_SCHEME_NAMES; + } else if (maxSdkVersion >= AndroidSdkVersion.N) { + supportedSchemeNames = new HashMap<>(1); + supportedSchemeNames.put(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + SUPPORTED_APK_SIG_SCHEME_NAMES.get( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2)); + } else { + supportedSchemeNames = Collections.emptyMap(); + } + return supportedSchemeNames; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + * <p>The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified} returns {@code true}. The details of the source stamp verification can + * be obtained from the result's {@link Result#getSourceStampInfo()}} including the success or + * failure cause from {@link Result.SourceStampInfo#getSourceStampVerificationStatus()}. If the + * verification fails additional details regarding the failure can be obtained from {@link + * Result#getAllErrors()}}. + */ + public Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private Result verifySourceStamp(DataSource apk, String expectedCertDigest) { + try { + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + int minSdkVersion = verifyAndGetMinSdkVersion(apk, zipSections); + + // Attempt to obtain the source stamp's certificate digest from the APK. + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + ApkSigningBlockUtils.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID, result); + stampSigningBlockFound = true; + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + if (stampSigningBlockFound) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + } else { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + } + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtils.toHex(sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus + .CERT_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, actualCertDigest, + expectedCertDigest); + } + } + + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); + Map<Integer, String> supportedSchemeNames = getSupportedSchemeNames(mMaxSdkVersion); + Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); + + Result result = new Result(); + ApkSigningBlockUtils.Result v3Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + v3Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V3, + Math.max(minSdkVersion, AndroidSdkVersion.P)); + if (v3Result != null && v3Result.containsErrors()) { + result.mergeFrom(v3Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + ApkSigningBlockUtils.Result v2Result = null; + if (mMaxSdkVersion >= AndroidSdkVersion.N && (minSdkVersion < AndroidSdkVersion.P + || foundApkSigSchemeIds.isEmpty())) { + v2Result = getApkContentDigests(apk, zipSections, foundApkSigSchemeIds, + supportedSchemeNames, signatureSchemeApkContentDigests, + VERSION_APK_SIGNATURE_SCHEME_V2, + Math.max(minSdkVersion, AndroidSdkVersion.N)); + if (v2Result != null && v2Result.containsErrors()) { + result.mergeFrom(v2Result); + return mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + result); + } + } + + if (minSdkVersion < AndroidSdkVersion.N || foundApkSigSchemeIds.isEmpty()) { + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + minSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + // Since the caller is only seeking to verify the source stamp the Result can be marked + // as verified if the source stamp verification was successful. + if (sourceStampResult.verified) { + result.setVerified(); + } else { + // To prevent APK signature verification with a failed / missing source stamp the + // source stamp verification will only log warnings; to allow the caller to capture + // the failure reason treat all warnings as errors. + result.setWarningsAsErrors(true); + } + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.VERIFICATION_ERROR, + Issue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + return createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus.STAMP_NOT_VERIFIED, + Issue.SOURCE_STAMP_SIG_MISSING); + } + } + + /** + * Creates and returns a {@code Result} that can be returned for source stamp verification + * with the provided source stamp {@code verificationStatus}, and logs an error for the + * specified {@code issue} and {@code params}. + */ + private static Result createSourceStampResultWithError( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, Issue issue, + Object... params) { + Result result = new Result(); + result.addError(issue, params); + return mergeSourceStampResult(verificationStatus, result); + } + + /** + * Creates a new {@link Result.SourceStampInfo} under the provided {@code result} and sets the + * source stamp status to the provided {@code verificationStatus}. + */ + private static Result mergeSourceStampResult( + Result.SourceStampInfo.SourceStampVerificationStatus verificationStatus, + Result result) { + result.mSourceStampInfo = new Result.SourceStampInfo(verificationStatus); + return result; + } + + /** + * Obtains the APK content digest(s) and adds them to the provided {@code + * sigSchemeApkContentDigests}, returning an {@code ApkSigningBlockUtils.Result} that can be + * merged with a {@code Result} to notify the client of any errors. + * + * <p>Note, this method currently only supports signature scheme V2 and V3; to obtain the + * content digests for V1 signatures use {@link + * #getApkContentDigestFromV1SigningScheme(List, DataSource, ApkUtils.ZipSections)}. If a + * signature scheme version other than V2 or V3 is provided a {@code null} value will be + * returned. + */ + private ApkSigningBlockUtils.Result getApkContentDigests(DataSource apk, + ApkUtils.ZipSections zipSections, Set<Integer> foundApkSigSchemeIds, + Map<Integer, String> supportedSchemeNames, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> sigSchemeApkContentDigests, + int apkSigSchemeVersion, int minSdkVersion) + throws IOException, NoSuchAlgorithmException { + if (!(apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2 + || apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3)) { + return null; + } + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(apkSigSchemeVersion); + SignatureInfo signatureInfo; + try { + int sigSchemeBlockId = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V3 + ? V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID + : V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, + sigSchemeBlockId, result); + } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + return null; + } + foundApkSigSchemeIds.add(apkSigSchemeVersion); + + Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); + if (apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2) { + V2SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, supportedSchemeNames, + foundApkSigSchemeIds, minSdkVersion, mMaxSdkVersion, result); + } else { + V3SchemeVerifier.parseSigners(signatureInfo.signatureBlock, + contentDigestsToVerify, result); + } + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + for (ApkSigningBlockUtils.Result.SignerInfo signerInfo : result.signers) { + for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : + signerInfo.contentDigests) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById( + contentDigest.getSignatureAlgorithmId()); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), + contentDigest.getValue()); + } + } + sigSchemeApkContentDigests.put(apkSigSchemeVersion, apkContentDigests); + return result; + } + + private static void checkV4Certificate(List<X509Certificate> v4Certs, + List<X509Certificate> v2v3Certs, Result result) { try { byte[] v4Cert = v4Certs.get(0).getEncoded(); byte[] cert = v2v3Certs.get(0).getEncoded(); @@ -593,7 +908,8 @@ public class ApkVerifier { } } - private static byte[] pickBestDigestForV4(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) { + private static byte[] pickBestDigestForV4( + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests) { Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>(); collectApkContentDigests(contentDigests, apkContentDigests); return ApkSigningBlockUtils.pickBestDigestForV4(apkContentDigests); @@ -614,7 +930,8 @@ public class ApkVerifier { ApkUtils.ZipSections zipSections) throws IOException, ApkFormatException { CentralDirectoryRecord manifestCdRecord = null; - Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new HashMap<>(); + Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); for (CentralDirectoryRecord cdRecord : cdRecords) { if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) { manifestCdRecord = cdRecord; @@ -639,7 +956,9 @@ public class ApkVerifier { } } - private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + private static void collectApkContentDigests( + List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { for (ApkSigningBlockUtils.Result.SignerInfo.ContentDigest contentDigest : contentDigests) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(contentDigest.getSignatureAlgorithmId()); @@ -655,7 +974,7 @@ public class ApkVerifier { private static ByteBuffer getAndroidManifestFromApk( DataSource apk, ApkUtils.ZipSections zipSections) - throws IOException, ApkFormatException { + throws IOException, ApkFormatException { List<CentralDirectoryRecord> cdRecords = V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); try { @@ -667,120 +986,6 @@ public class ApkVerifier { } } - /** - * Android resource ID of the {@code android:targetSandboxVersion} attribute in - * AndroidManifest.xml. - */ - private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; - private static final String TARGET_SANDBOX_VERSION_ELEMENT_NAME = "manifest"; - - /** - * Android resource ID of the {@code android:targetSdkVersion} attribute in - * AndroidManifest.xml. - */ - private static final int MIN_SDK_VERSION_ATTR_ID = 0x0101020c; - private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; - private static final String USES_SDK_ELEMENT_NAME = "uses-sdk"; - - /** - * Returns the security sandbox version targeted by an APK with the provided - * {@code AndroidManifest.xml}. - * - * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format - * - * @throws ApkFormatException if an error occurred while determining the version - */ - private static int getTargetSandboxVersionFromBinaryAndroidManifest( - ByteBuffer androidManifestContents) throws ApkFormatException { - return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - TARGET_SANDBOX_VERSION_ELEMENT_NAME, TARGET_SANDBOX_VERSION_ATTR_ID); - } - - /** - * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. - * - * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android - * resource format - * @throws ApkFormatException if an error occurred while determining the version - */ - private static int getTargetSdkVersionFromBinaryAndroidManifest( - ByteBuffer androidManifestContents) { - // If the targetSdkVersion is not specified then the platform will use the value of the - // minSdkVersion; if neither is specified then the platform will use a value of 1. - int minSdkVersion = 1; - try { - return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - USES_SDK_ELEMENT_NAME, TARGET_SDK_VERSION_ATTR_ID); - } catch (ApkFormatException e) { - // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk - // element is not specified at all. - } - androidManifestContents.rewind(); - try { - minSdkVersion = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, - USES_SDK_ELEMENT_NAME, MIN_SDK_VERSION_ATTR_ID); - } catch (ApkFormatException e) { - // Similar to above, expected if the APK does not contain a minSdkVersion attribute or - // the uses-sdk element is not specified at all. - } - return minSdkVersion; - } - - /** - * Returns the integer value of the requested {@code attributeId} in the specified {@code - * elementName} from the provided {@code androidManifestContents} in binary Android resource - * format. - * - * @throws ApkFormatException if an error occurred while attempting to obtain the attribute - */ - private static int getAttributeValueFromBinaryAndroidManifest( - ByteBuffer androidManifestContents, String elementName, int attributeId) - throws ApkFormatException { - // Return the value of the requested attribute from the specified element. - try { - AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); - int eventType = parser.getEventType(); - while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { - if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) - && (elementName.equals(parser.getName())) - && (parser.getNamespace().isEmpty())) { - int result = 1; - for (int i = 0; i < parser.getAttributeCount(); i++) { - if (parser.getAttributeNameResourceId(i) == attributeId) { - int valueType = parser.getAttributeValueType(i); - switch (valueType) { - case AndroidBinXmlParser.VALUE_TYPE_INT: - result = parser.getAttributeIntValue(i); - break; - default: - throw new ApkFormatException( - "Failed to determine APK's " - + elementName + " attribute" - + ": unsupported value type of" - + " AndroidManifest.xml " - + String.format("0x%08X", attributeId) - + ". Only integer values supported."); - } - break; - } - } - return result; - } - eventType = parser.next(); - } - throw new ApkFormatException( - "Failed to determine APK's " + elementName + " attribute " - + String.format("0x%08X", attributeId) - + " : no " + elementName + " element in AndroidManifest.xml"); - } catch (AndroidBinXmlParser.XmlParserException e) { - throw new ApkFormatException( - "Failed to determine APK's " + elementName + " attribute " - + String.format("0x%08X", attributeId) - + ": malformed AndroidManifest.xml", e); - } - } - private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { if (targetSdkVersion >= AndroidSdkVersion.R) { return VERSION_APK_SIGNATURE_SCHEME_V2; @@ -809,6 +1014,7 @@ public class ApkVerifier { private boolean mVerifiedUsingV3Scheme; private boolean mVerifiedUsingV4Scheme; private boolean mSourceStampVerified; + private boolean mWarningsAsErrors; private SigningCertificateLineage mSigningCertificateLineage; /** @@ -937,10 +1143,24 @@ public class ApkVerifier { } /** + * Sets whether warnings should be treated as errors. + */ + void setWarningsAsErrors(boolean value) { + mWarningsAsErrors = value; + } + + /** * Returns errors encountered while verifying the APK's signatures. */ public List<IssueWithParams> getErrors() { - return mErrors; + if (!mWarningsAsErrors) { + return mErrors; + } else { + List<IssueWithParams> allErrors = new ArrayList<>(); + allErrors.addAll(mErrors); + allErrors.addAll(mWarnings); + return allErrors; + } } /** @@ -962,6 +1182,21 @@ public class ApkVerifier { } } + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case ApkSigningBlockUtils.VERSION_SOURCE_STAMP: + mSourceStampVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + private void mergeFrom(ApkSigningBlockUtils.Result source) { switch (source.signatureSchemeVersion) { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: @@ -992,8 +1227,6 @@ public class ApkVerifier { default: throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); } - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); } /** @@ -1004,11 +1237,17 @@ public class ApkVerifier { if (!mErrors.isEmpty()) { return true; } + if (mWarningsAsErrors && !mWarnings.isEmpty()) { + return true; + } if (!mV1SchemeSigners.isEmpty()) { for (V1SchemeSignerInfo signer : mV1SchemeSigners) { if (signer.containsErrors()) { return true; } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } } } if (!mV2SchemeSigners.isEmpty()) { @@ -1016,6 +1255,9 @@ public class ApkVerifier { if (signer.containsErrors()) { return true; } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } } } if (!mV3SchemeSigners.isEmpty()) { @@ -1023,16 +1265,67 @@ public class ApkVerifier { if (signer.containsErrors()) { return true; } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } } } - if (mSourceStampInfo != null && mSourceStampInfo.containsErrors()) { - return true; + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + if (mWarningsAsErrors && !mSourceStampInfo.getWarnings().isEmpty()) { + return true; + } } return false; } /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List<IssueWithParams> getAllErrors() { + List<IssueWithParams> errors = new ArrayList<>(); + errors.addAll(mErrors); + if (mWarningsAsErrors) { + errors.addAll(mWarnings); + } + if (!mV1SchemeSigners.isEmpty()) { + for (V1SchemeSignerInfo signer : mV1SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV2SchemeSigners.isEmpty()) { + for (V2SchemeSignerInfo signer : mV2SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (!mV3SchemeSigners.isEmpty()) { + for (V3SchemeSignerInfo signer : mV3SchemeSigners) { + errors.addAll(signer.mErrors); + if (mWarningsAsErrors) { + errors.addAll(signer.getWarnings()); + } + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + if (mWarningsAsErrors) { + errors.addAll(mSourceStampInfo.getWarnings()); + } + } + return errors; + } + + /** * Information about a JAR signer associated with the APK's signature. */ public static class V1SchemeSignerInfo { @@ -1328,15 +1621,50 @@ public class ApkVerifier { * Information about SourceStamp associated with the APK's signature. */ public static class SourceStampInfo { + public enum SourceStampVerificationStatus { + /** The stamp is present and was successfully verified. */ + STAMP_VERIFIED, + /** The stamp is present but failed verification. */ + STAMP_VERIFICATION_FAILED, + /** The expected cert digest did not match the digest in the APK. */ + CERT_DIGEST_MISMATCH, + /** The stamp is not present at all. */ + STAMP_MISSING, + /** The stamp is at least partially present, but was not able to be verified. */ + STAMP_NOT_VERIFIED, + /** The stamp was not able to be verified due to an unexpected error. */ + VERIFICATION_ERROR + } + private final List<X509Certificate> mCertificates; + private final List<X509Certificate> mCertificateLineage; private final List<IssueWithParams> mErrors; private final List<IssueWithParams> mWarnings; - private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + private final SourceStampVerificationStatus mSourceStampVerificationStatus; + + private SourceStampInfo(ApkSignerInfo result) { mCertificates = result.certs; - mErrors = result.getErrors(); - mWarnings = result.getWarnings(); + mCertificateLineage = result.certificateLineage; + mErrors = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getErrors()); + mWarnings = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getWarnings()); + if (mErrors.isEmpty() && mWarnings.isEmpty()) { + mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED; + } else { + mSourceStampVerificationStatus = + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED; + } + } + + SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) { + mCertificates = Collections.emptyList(); + mCertificateLineage = Collections.emptyList(); + mErrors = Collections.emptyList(); + mWarnings = Collections.emptyList(); + mSourceStampVerificationStatus = sourceStampVerificationStatus; } /** @@ -1350,6 +1678,13 @@ public class ApkVerifier { return mCertificates.isEmpty() ? null : mCertificates.get(0); } + /** + * Returns a list containing all of the certificates in the stamp certificate lineage. + */ + public List<X509Certificate> getCertificatesInLineage() { + return mCertificateLineage; + } + public boolean containsErrors() { return !mErrors.isEmpty(); } @@ -1361,6 +1696,14 @@ public class ApkVerifier { public List<IssueWithParams> getWarnings() { return mWarnings; } + + /** + * Returns the reason for any source stamp verification failures, or {@code + * STAMP_VERIFIED} if the source stamp was successfully verified. + */ + public SourceStampVerificationStatus getSourceStampVerificationStatus() { + return mSourceStampVerificationStatus; + } } } @@ -2338,6 +2681,14 @@ public class ApkVerifier { "V4 signature format version %1$d is different from the tool's current " + "version %2$d"), + /** + * The APK does not contain the source stamp certificate digest file nor the signature block + * when verification expected a source stamp to be present. + */ + SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING( + "Neither the source stamp certificate digest file nor the signature block are " + + "present in the APK"), + /** APK contains SourceStamp file, but does not contain a SourceStamp signature. */ SOURCE_STAMP_SIG_MISSING("No SourceStamp signature"), @@ -2384,8 +2735,16 @@ public class ApkVerifier { /** SourceStamp offers no signatures. */ SOURCE_STAMP_NO_SIGNATURE("No signature"), - /** SourceStamp offers an unsupported signature. */ - SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature not supported"), + /** + * SourceStamp offers an unsupported signature. + * <ul> + * <li>Parameter 1: list of {@link SignatureAlgorithm}s in the source stamp + * signing block. + * <li>Parameter 2: {@code Exception} caught when attempting to obtain the list of + * supported signatures. + * </ul> + */ + SOURCE_STAMP_NO_SUPPORTED_SIGNATURE("Signature(s) {%1$s} not supported: %2$s"), /** * SourceStamp's certificate listed in the APK signing block does not match the certificate @@ -2400,7 +2759,87 @@ public class ApkVerifier { */ SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK( "Certificate mismatch between SourceStamp block in APK signing block and" - + " SourceStamp file in APK: <%1$s> vs <%2$s>"); + + " SourceStamp file in APK: <%1$s> vs <%2$s>"), + + /** + * The APK contains a source stamp signature block without the expected certificate digest + * in the APK contents. + */ + SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST( + "A source stamp signature block was found without a corresponding certificate " + + "digest in the APK"), + + /** + * When verifying just the source stamp, the certificate digest in the APK does not match + * the expected digest. + * <ul> + * <li>Parameter 1: SHA-256 digest of the source stamp certificate in the APK. + * <li>Parameter 2: SHA-256 digest of the expected source stamp certificate. + * </ul> + */ + SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH( + "The source stamp certificate digest in the APK, %1$s, does not match the " + + "expected digest, %2$s"), + + /** + * Source stamp block contains a malformed attribute. + * + * <ul> + * <li>Parameter 1: attribute number (first attribute is {@code 1}) {@code Integer})</li> + * </ul> + */ + SOURCE_STAMP_MALFORMED_ATTRIBUTE("Malformed stamp attribute #%1$d"), + + /** + * Source stamp block contains an unknown attribute. + * + * <ul> + * <li>Parameter 1: attribute ID ({@code Integer})</li> + * </ul> + */ + SOURCE_STAMP_UNKNOWN_ATTRIBUTE("Unknown stamp attribute: ID %1$#x"), + + /** + * Failed to parse the SigningCertificateLineage structure in the source stamp + * attributes section. + */ + SOURCE_STAMP_MALFORMED_LINEAGE("Failed to parse the SigningCertificateLineage " + + "structure in the source stamp attributes section."), + + /** + * The source stamp certificate does not match the terminal node in the provided + * proof-of-rotation structure describing the stamp certificate history. + */ + SOURCE_STAMP_POR_CERT_MISMATCH( + "APK signing certificate differs from the associated certificate found in the " + + "signer's SigningCertificateLineage."), + + /** + * The source stamp SigningCertificateLineage attribute contains a proof-of-rotation record + * with signature(s) that did not verify. + */ + SOURCE_STAMP_POR_DID_NOT_VERIFY("Source stamp SigningCertificateLineage attribute " + + "contains a proof-of-rotation record with signature(s) that did not verify."), + + /** + * The APK could not be properly parsed due to a ZIP or APK format exception. + * <ul> + * <li>Parameter 1: The {@code Exception} caught when attempting to parse the APK. + * </ul> + */ + MALFORMED_APK( + "Malformed APK; the following exception was caught when attempting to parse the " + + "APK: %1$s"), + + /** + * An unexpected exception was caught when attempting to verify the signature(s) within the + * APK. + * <ul> + * <li>Parameter 1: The {@code Exception} caught during verification. + * </ul> + */ + UNEXPECTED_EXCEPTION( + "An unexpected exception was caught when verifying the signature: %1$s"); private final String mFormat; @@ -2421,7 +2860,7 @@ public class ApkVerifier { * {@link Issue} with associated parameters. {@link #toString()} produces a readable formatted * form. */ - public static class IssueWithParams { + public static class IssueWithParams extends ApkVerificationIssue { private final Issue mIssue; private final Object[] mParams; @@ -2430,6 +2869,7 @@ public class ApkVerifier { * parameters. */ public IssueWithParams(Issue issue, Object[] params) { + super(issue.mFormat, params); mIssue = issue; mParams = params; } @@ -2544,7 +2984,6 @@ public class ApkVerifier { * {@code android:minSdkVersion} attributes in the APK's {@code AndroidManifest.xml}. * * @param minSdkVersion API Level of the oldest platform for which to verify the APK - * * @see #setMinCheckedPlatformVersion(int) */ public Builder setMinCheckedPlatformVersion(int minSdkVersion) { @@ -2560,7 +2999,6 @@ public class ApkVerifier { * {@link #setMinCheckedPlatformVersion(int)}. * * @param maxSdkVersion API Level of the newest platform for which to verify the APK - * * @see #setMinCheckedPlatformVersion(int) */ public Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { @@ -2586,4 +3024,118 @@ public class ApkVerifier { mMaxSdkVersion); } } + + /** + * Adapter for converting base {@link ApkVerificationIssue} instances to their {@link + * IssueWithParams} equivalent. + */ + public static class ApkVerificationIssueAdapter { + private ApkVerificationIssueAdapter() { + } + + // This field is visible for testing + static final Map<Integer, Issue> sVerificationIssueIdToIssue = new HashMap<>(); + + static { + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS, + Issue.V2_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNERS, + Issue.V2_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER, + Issue.V2_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_SIGNATURE, + Issue.V2_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_SIGNATURES, + Issue.V2_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE, + Issue.V2_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_NO_CERTIFICATES, + Issue.V2_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST, + Issue.V2_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS, + Issue.V3_SIG_MALFORMED_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNERS, + Issue.V3_SIG_NO_SIGNERS); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER, + Issue.V3_SIG_MALFORMED_SIGNER); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_SIGNATURE, + Issue.V3_SIG_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_SIGNATURES, + Issue.V3_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE, + Issue.V3_SIG_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_NO_CERTIFICATES, + Issue.V3_SIG_NO_CERTIFICATES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST, + Issue.V3_SIG_MALFORMED_DIGEST); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE, + Issue.SOURCE_STAMP_NO_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, + Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, + Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, + Issue.SOURCE_STAMP_VERIFY_EXCEPTION); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST, + Issue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING, + Issue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); + sVerificationIssueIdToIssue.put( + ApkVerificationIssue + .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, + Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.MALFORMED_APK, + Issue.MALFORMED_APK); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.UNEXPECTED_EXCEPTION, + Issue.UNEXPECTED_EXCEPTION); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING, + Issue.SOURCE_STAMP_SIG_MISSING); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, + Issue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE, + Issue.SOURCE_STAMP_MALFORMED_LINEAGE); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH, + Issue.SOURCE_STAMP_POR_CERT_MISMATCH); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY, + Issue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES, + Issue.JAR_SIG_NO_SIGNATURES); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + Issue.JAR_SIG_PARSE_EXCEPTION); + } + + /** + * Converts the provided {@code verificationIssues} to a {@code List} of corresponding + * {@link IssueWithParams} instances. + */ + public static List<IssueWithParams> getIssuesFromVerificationIssues( + List<? extends ApkVerificationIssue> verificationIssues) { + List<IssueWithParams> result = new ArrayList<>(verificationIssues.size()); + for (ApkVerificationIssue issue : verificationIssues) { + if (issue instanceof IssueWithParams) { + result.add((IssueWithParams) issue); + } else { + result.add( + new IssueWithParams(sVerificationIssueIdToIssue.get(issue.getIssueId()), + issue.getParams())); + } + } + return result; + } + } } diff --git a/src/main/java/com/android/apksig/Constants.java b/src/main/java/com/android/apksig/Constants.java new file mode 100644 index 0000000..680c5c3 --- /dev/null +++ b/src/main/java/com/android/apksig/Constants.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; + +/** + * Exports internally defined constants to allow clients to reference these values without relying + * on internal code. + */ +public class Constants { + private Constants() {} + + public static final int VERSION_SOURCE_STAMP = 0; + public static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + public static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + public static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; + public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; + + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; + + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; +} diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index f0796fb..90f2a6d 100644 --- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java +++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -29,6 +29,7 @@ import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.stamp.V2SourceStampSigner; import com.android.apksig.internal.apk.v1.DigestAlgorithm; +import com.android.apksig.internal.apk.v1.V1SchemeConstants; import com.android.apksig.internal.apk.v1.V1SchemeSigner; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.apk.v2.V2SchemeSigner; @@ -98,6 +99,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private final String mCreatedBy; private final List<SignerConfig> mSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; private final int mMinSdkVersion; private final SigningCertificateLineage mSigningCertificateLineage; @@ -160,6 +162,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private DefaultApkSignerEngine( List<SignerConfig> signerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, int minSdkVersion, boolean v1SigningEnabled, boolean v2SigningEnabled, @@ -190,6 +193,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { mCreatedBy = createdBy; mSignerConfigs = signerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; mMinSdkVersion = minSdkVersion; mSigningCertificateLineage = signingCertificateLineage; @@ -307,13 +311,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } } - private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs( - boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { - List<ApkSigningBlockUtils.SignerConfig> rawConfigs = - createSigningBlockSignerConfigs( - apkSigningBlockPaddingSupported, - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); - + private List<ApkSigningBlockUtils.SignerConfig> processV3Configs( + List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException { List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>(); // we have our configs, now touch them up to appropriately cover all SDK levels since APK @@ -361,26 +360,40 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { "Provided key algorithms not supported on all desired " + "Android SDK versions"); } + return processedConfigs; } - private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() - throws InvalidKeyException, IllegalStateException { - List<ApkSigningBlockUtils.SignerConfig> configs = - createSigningBlockSignerConfigs( - true, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3)); + } + + private ApkSigningBlockUtils.SignerConfig createV4SignerConfig() throws InvalidKeyException { + List<ApkSigningBlockUtils.SignerConfig> configs = createSigningBlockSignerConfigs(true, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); if (configs.size() != 1) { - throw new IllegalStateException("Only accepting one signer config for V4 Signature."); + // V4 only uses signer config to connect back to v3. Use the same filtering logic. + configs = processV3Configs(configs); + } + if (configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4 Signature."); } return configs.get(0); } private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig() throws InvalidKeyException { - return createSigningBlockSignerConfig( + ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig( mSourceStampSignerConfig, /* apkSigningBlockPaddingSupported= */ false, ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + if (mSourceStampSigningCertificateLineage != null) { + config.mSigningCertificateLineage = mSourceStampSigningCertificateLineage.getSubLineage( + config.certificates.get(0)); + } + return config; } private int getMinSdkFromV3SignatureAlgorithms(List<SignatureAlgorithm> algorithms) { @@ -549,7 +562,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { case OUTPUT: return new InputJarEntryInstructions(InputJarEntryInstructions.OutputPolicy.OUTPUT); case OUTPUT_BY_ENGINE: - if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { // We copy the main section of the JAR manifest from input to output. Thus, this // invalidates v1 signature and we need to see the entry's data. mInputJarManifestEntryDataRequest = new GetJarEntryDataRequest(entryName); @@ -617,7 +630,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // the entry's data is as output by the engine. invalidateV1Signature(); GetJarEntryDataRequest dataRequest; - if (V1SchemeSigner.MANIFEST_ENTRY_NAME.equals(entryName)) { + if (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals(entryName)) { dataRequest = new GetJarEntryDataRequest(entryName); mInputJarManifestEntryDataRequest = dataRequest; } else { @@ -752,7 +765,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { V1SchemeSigner.generateManifestFile( mV1ContentDigestAlgorithm, mOutputJarEntryDigests, inputJarManifest); byte[] emittedSignatureManifest = - mEmittedSignatureJarEntryData.get(V1SchemeSigner.MANIFEST_ENTRY_NAME); + mEmittedSignatureJarEntryData.get(V1SchemeConstants.MANIFEST_ENTRY_NAME); if (!Arrays.equals(newManifest.contents, emittedSignatureManifest)) { // Emitted v1 signature is no longer valid. try { @@ -1473,6 +1486,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { public static class Builder { private List<SignerConfig> mSignerConfigs; private SignerConfig mStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; private final int mMinSdkVersion; private boolean mV1SigningEnabled = true; @@ -1564,6 +1578,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return new DefaultApkSignerEngine( mSignerConfigs, mStampSignerConfig, + mSourceStampSigningCertificateLineage, mMinSdkVersion, mV1SigningEnabled, mV2SigningEnabled, @@ -1582,6 +1597,16 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } /** + * Sets the source stamp {@link SigningCertificateLineage}. This structure provides proof of + * signing certificate rotation for certificates previously used to sign source stamps. + */ + public Builder setSourceStampSigningCertificateLineage( + SigningCertificateLineage sourceStampSigningCertificateLineage) { + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + return this; + } + + /** * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). * * <p>By default, the APK will be signed using this scheme. diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java index 54340d7..b8f1f8b 100644 --- a/src/main/java/com/android/apksig/SigningCertificateLineage.java +++ b/src/main/java/com/android/apksig/SigningCertificateLineage.java @@ -23,6 +23,7 @@ import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage; import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage.SigningCertificateNode; @@ -192,7 +193,7 @@ public class SigningCertificateLineage { ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, - V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); } catch (ZipFormatException e) { throw new ApkFormatException(e.getMessage()); } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { @@ -263,7 +264,7 @@ public class SigningCertificateLineage { while (additionalAttributes.hasRemaining()) { ByteBuffer attribute = getLengthPrefixedSlice(additionalAttributes); int id = attribute.getInt(); - if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) { + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { byte[] value = ByteBufferUtils.toByteArray(attribute); SigningCertificateLineage lineage = readFromV3AttributeValue(value); lineages.add(lineage); @@ -491,20 +492,8 @@ public class SigningCertificateLineage { return result; } - public byte[] generateV3SignerAttribute() { - // FORMAT (little endian): - // * length-prefixed bytes: attribute pair - // * uint32: ID - // * bytes: value - encoded V3 SigningCertificateLineage - byte[] encodedLineage = - V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); - int payloadSize = 4 + 4 + encodedLineage.length; - ByteBuffer result = ByteBuffer.allocate(payloadSize); - result.order(ByteOrder.LITTLE_ENDIAN); - result.putInt(4 + encodedLineage.length); - result.putInt(V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID); - result.put(encodedLineage); - return result.array(); + public byte[] encodeSigningCertificateLineage() { + return V3SigningCertificateLineage.encodeSigningCertificateLineage(mSigningLineage); } public List<DefaultApkSignerEngine.SignerConfig> sortSignerConfigs( diff --git a/src/main/java/com/android/apksig/SourceStampVerifier.java b/src/main/java/com/android/apksig/SourceStampVerifier.java new file mode 100644 index 0000000..587cbd3 --- /dev/null +++ b/src/main/java/com/android/apksig/SourceStampVerifier.java @@ -0,0 +1,882 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V2; +import static com.android.apksig.Constants.VERSION_APK_SIGNATURE_SCHEME_V3; +import static com.android.apksig.Constants.VERSION_JAR_SIGNATURE_SCHEME; +import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v1.V1SchemeConstants.MANIFEST_ENTRY_NAME; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; +import com.android.apksig.internal.apk.stamp.V2SourceStampVerifier; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; +import com.android.apksig.internal.util.AndroidSdkVersion; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.zip.CentralDirectoryRecord; +import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.util.DataSources; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.ByteArrayInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * APK source stamp verifier intended only to verify the validity of the stamp signature. + * + * <p>Note, this verifier does not validate the signatures of the jar signing / APK signature blocks + * when obtaining the digests for verification. This verifier should only be used in cases where + * another mechanism has already been used to verify the APK signatures. + */ +public class SourceStampVerifier { + private final File mApkFile; + private final DataSource mApkDataSource; + + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + + private SourceStampVerifier( + File apkFile, + DataSource apkDataSource, + int minSdkVersion, + int maxSdkVersion) { + mApkFile = apkFile; + mApkDataSource = apkDataSource; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Verifies the APK's source stamp signature and returns the result of the verification. + * + * <p>The APK's source stamp can be considered verified if the result's {@link + * Result#isVerified()} returns {@code true}. If source stamp verification fails all of the + * resulting errors can be obtained from {@link Result#getAllErrors()}, or individual errors + * can be obtained as follows: + * <ul> + * <li>Obtain the generic errors via {@link Result#getErrors()} + * <li>Obtain the V2 signers via {@link Result#getV2SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + * <li>Obtain the V3 signers via {@link Result#getV3SchemeSigners()}, then for each signer + * query for any errors with {@link Result.SignerInfo#getErrors()} + * <li>Obtain the source stamp signer via {@link Result#getSourceStampInfo()}, then query + * for any stamp errors with {@link Result.SourceStampInfo#getErrors()} + * </ul> + */ + public SourceStampVerifier.Result verifySourceStamp() { + return verifySourceStamp(null); + } + + /** + * Verifies the APK's source stamp signature, including verification that the SHA-256 digest of + * the stamp signing certificate matches the {@code expectedCertDigest}, and returns the result + * of the verification. + * + * <p>A value of {@code null} for the {@code expectedCertDigest} will verify the source stamp, + * if present, without verifying the actual source stamp certificate used to sign the source + * stamp. This can be used to verify an APK contains a properly signed source stamp without + * verifying a particular signer. + * + * @see #verifySourceStamp() + */ + public SourceStampVerifier.Result verifySourceStamp(String expectedCertDigest) { + Closeable in = null; + try { + DataSource apk; + if (mApkDataSource != null) { + apk = mApkDataSource; + } else if (mApkFile != null) { + RandomAccessFile f = new RandomAccessFile(mApkFile, "r"); + in = f; + apk = DataSources.asDataSource(f, 0, f.length()); + } else { + throw new IllegalStateException("APK not provided"); + } + return verifySourceStamp(apk, expectedCertDigest); + } catch (IOException e) { + Result result = new Result(); + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + return result; + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException ignored) { + } + } + } + } + + /** + * Verifies the provided {@code apk}'s source stamp signature, including verification of the + * SHA-256 digest of the stamp signing certificate matches the {@code expectedCertDigest}, and + * returns the result of the verification. + * + * @see #verifySourceStamp(String) + */ + private SourceStampVerifier.Result verifySourceStamp(DataSource apk, + String expectedCertDigest) { + Result result = new Result(); + try { + ZipSections zipSections = ApkUtilsLite.findZipSections(apk); + // Attempt to obtain the source stamp's certificate digest from the APK. + List<CentralDirectoryRecord> cdRecords = + ZipUtils.parseZipCentralDirectory(apk, zipSections); + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + + // If the source stamp's certificate digest is not available within the APK then the + // source stamp cannot be verified; check if a source stamp signing block is in the + // APK's signature block to determine the appropriate status to return. + if (sourceStampCdRecord == null) { + boolean stampSigningBlockFound; + try { + ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + stampSigningBlockFound = true; + } catch (SignatureNotFoundException e) { + stampSigningBlockFound = false; + } + result.addVerificationError(stampSigningBlockFound + ? ApkVerificationIssue.SOURCE_STAMP_SIGNATURE_BLOCK_WITHOUT_CERT_DIGEST + : ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + return result; + } + + // Verify that the contents of the source stamp certificate digest match the expected + // value, if provided. + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + if (expectedCertDigest != null) { + String actualCertDigest = ApkSigningBlockUtilsLite.toHex( + sourceStampCertificateDigest); + if (!expectedCertDigest.equalsIgnoreCase(actualCertDigest)) { + result.addVerificationError( + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH, + actualCertDigest, expectedCertDigest); + return result; + } + } + + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); + if (mMaxSdkVersion >= AndroidSdkVersion.P) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V3, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V3, apkContentDigests); + } + } + + if (mMaxSdkVersion >= AndroidSdkVersion.N && (mMinSdkVersion < AndroidSdkVersion.P || + signatureSchemeApkContentDigests.isEmpty())) { + SignatureInfo signatureInfo; + try { + signatureInfo = ApkSigningBlockUtilsLite.findSignature(apk, zipSections, + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + } catch (SignatureNotFoundException e) { + signatureInfo = null; + } + if (signatureInfo != null) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = new EnumMap<>( + ContentDigestAlgorithm.class); + parseSigners(signatureInfo.signatureBlock, VERSION_APK_SIGNATURE_SCHEME_V2, + apkContentDigests, result); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V2, apkContentDigests); + } + } + + if (mMinSdkVersion < AndroidSdkVersion.N + || signatureSchemeApkContentDigests.isEmpty()) { + Map<ContentDigestAlgorithm, byte[]> apkContentDigests = + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections, result); + signatureSchemeApkContentDigests.put(VERSION_JAR_SIGNATURE_SCHEME, + apkContentDigests); + } + + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + mMinSdkVersion, + mMaxSdkVersion); + result.mergeFrom(sourceStampResult); + return result; + } catch (ApkFormatException | IOException | ZipFormatException e) { + result.addVerificationError(ApkVerificationIssue.MALFORMED_APK, e); + } catch (NoSuchAlgorithmException e) { + result.addVerificationError(ApkVerificationIssue.UNEXPECTED_EXCEPTION, e); + } catch (SignatureNotFoundException e) { + result.addVerificationError(ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING); + } + return result; + } + + /** + * Parses each signer in the provided APK V2 / V3 signature block and populates corresponding + * {@code SignerInfo} of the provided {@code result} and their {@code apkContentDigests}. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + public static void parseSigners( + ByteBuffer apkSignatureSchemeBlock, + int apkSigSchemeVersion, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + Result result) { + boolean isV2Block = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signature blocks contain the following: + // * length-prefixed sequence of length-prefixed signers + ByteBuffer signers; + try { + signers = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(apkSignatureSchemeBlock); + } catch (ApkFormatException e) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNERS + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNERS); + return; + } + if (!signers.hasRemaining()) { + result.addVerificationWarning(isV2Block ? ApkVerificationIssue.V2_SIG_NO_SIGNERS + : ApkVerificationIssue.V3_SIG_NO_SIGNERS); + return; + } + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + while (signers.hasRemaining()) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + if (isV2Block) { + result.addV2Signer(signerInfo); + } else { + result.addV3Signer(signerInfo); + } + try { + ByteBuffer signer = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signers); + parseSigner( + signer, + apkSigSchemeVersion, + certFactory, + apkContentDigests, + signerInfo); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Block ? ApkVerificationIssue.V2_SIG_MALFORMED_SIGNER + : ApkVerificationIssue.V3_SIG_MALFORMED_SIGNER); + return; + } + } + } + + /** + * Parses the provided signer block and populates the {@code result}. + * + * <p>This verifies signatures over {@code signed-data} contained in this block but does not + * verify the integrity of the rest of the APK. To facilitate APK integrity verification, this + * method adds the {@code contentDigestsToVerify}. These digests can then be used to verify the + * integrity of the APK. + * + * <p>This method adds one or more errors to the {@code result} if a verification error is + * expected to be encountered on an Android platform version in the + * {@code [minSdkVersion, maxSdkVersion]} range. + */ + private static void parseSigner( + ByteBuffer signerBlock, + int apkSigSchemeVersion, + CertificateFactory certFactory, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + Result.SignerInfo signerInfo) + throws ApkFormatException { + boolean isV2Signer = apkSigSchemeVersion == VERSION_APK_SIGNATURE_SCHEME_V2; + // Both the V2 and V3 signer blocks contain the following: + // * length-prefixed signed data + // * length-prefixed sequence of length-prefixed digests: + // * uint32: signature algorithm ID + // * length-prefixed bytes: digest of contents + // * length-prefixed sequence of certificates: + // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded). + ByteBuffer signedData = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signerBlock); + ByteBuffer digests = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + ByteBuffer certificates = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(signedData); + + // Parse the digests block + while (digests.hasRemaining()) { + try { + ByteBuffer digest = ApkSigningBlockUtilsLite.getLengthPrefixedSlice(digests); + int sigAlgorithmId = digest.getInt(); + byte[] digestBytes = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(digest); + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); + if (signatureAlgorithm == null) { + continue; + } + apkContentDigests.put(signatureAlgorithm.getContentDigestAlgorithm(), digestBytes); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_DIGEST + : ApkVerificationIssue.V3_SIG_MALFORMED_DIGEST); + return; + } + } + + // Parse the certificates block + if (certificates.hasRemaining()) { + byte[] encodedCert = ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(certificates); + X509Certificate certificate; + try { + certificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + } catch (CertificateException e) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_MALFORMED_CERTIFICATE + : ApkVerificationIssue.V3_SIG_MALFORMED_CERTIFICATE); + return; + } + // Wrap the cert so that the result's getEncoded returns exactly the original encoded + // form. Without this, getEncoded may return a different form from what was stored in + // the signature. This is because some X509Certificate(Factory) implementations + // re-encode certificates. + certificate = new GuaranteedEncodedFormX509Certificate(certificate, encodedCert); + signerInfo.setSigningCertificate(certificate); + } + + if (signerInfo.getSigningCertificate() == null) { + signerInfo.addVerificationWarning( + isV2Signer ? ApkVerificationIssue.V2_SIG_NO_CERTIFICATES + : ApkVerificationIssue.V3_SIG_NO_CERTIFICATES); + return; + } + } + + /** + * Returns a mapping of the {@link ContentDigestAlgorithm} to the {@code byte[]} digest of the + * V1 / jar signing META-INF/MANIFEST.MF; if this file is not found then an empty {@code Map} is + * returned. + * + * <p>If any errors are encountered while parsing the V1 signers the provided {@code result} + * will be updated to include a warning, but the source stamp verification can still proceed. + */ + private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme( + List<CentralDirectoryRecord> cdRecords, + DataSource apk, + ZipSections zipSections, + Result result) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + List<CentralDirectoryRecord> signatureBlockRecords = new ArrayList<>(1); + Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + String cdRecordName = cdRecord.getName(); + if (cdRecordName == null) { + continue; + } + if (manifestCdRecord == null && MANIFEST_ENTRY_NAME.equals(cdRecordName)) { + manifestCdRecord = cdRecord; + continue; + } + if (cdRecordName.startsWith("META-INF/") + && (cdRecordName.endsWith(".RSA") + || cdRecordName.endsWith(".DSA") + || cdRecordName.endsWith(".EC"))) { + signatureBlockRecords.add(cdRecord); + } + } + if (manifestCdRecord == null) { + // No JAR signing manifest file found. For SourceStamp verification, returning an empty + // digest is enough since this would affect the final digest signed by the stamp, and + // thus an empty digest will invalidate that signature. + return v1ContentDigest; + } + if (signatureBlockRecords.isEmpty()) { + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_NO_SIGNATURES); + } else { + for (CentralDirectoryRecord signatureBlockRecord : signatureBlockRecords) { + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + byte[] signatureBlockBytes = LocalFileRecord.getUncompressedData(apk, + signatureBlockRecord, zipSections.getZipCentralDirectoryOffset()); + for (Certificate certificate : certFactory.generateCertificates( + new ByteArrayInputStream(signatureBlockBytes))) { + // If multiple certificates are found within the signature block only the + // first is used as the signer of this block. + if (certificate instanceof X509Certificate) { + Result.SignerInfo signerInfo = new Result.SignerInfo(); + signerInfo.setSigningCertificate((X509Certificate) certificate); + result.addV1Signer(signerInfo); + break; + } + } + } catch (CertificateException e) { + // Log a warning for the parsing exception but still proceed with the stamp + // verification. + result.addVerificationWarning(ApkVerificationIssue.JAR_SIG_PARSE_EXCEPTION, + signatureBlockRecord.getName(), e); + break; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + } + try { + byte[] manifestBytes = + LocalFileRecord.getUncompressedData( + apk, manifestCdRecord, zipSections.getZipCentralDirectoryOffset()); + v1ContentDigest.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(manifestBytes)); + return v1ContentDigest; + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); + } + } + + /** + * Result of verifying the APK's source stamp signature; this signature can only be considered + * verified if {@link #isVerified()} returns true. + */ + public static class Result { + private final List<SignerInfo> mV1SchemeSigners = new ArrayList<>(); + private final List<SignerInfo> mV2SchemeSigners = new ArrayList<>(); + private final List<SignerInfo> mV3SchemeSigners = new ArrayList<>(); + private final List<List<SignerInfo>> mAllSchemeSigners = Arrays.asList(mV1SchemeSigners, + mV2SchemeSigners, mV3SchemeSigners); + private SourceStampInfo mSourceStampInfo; + + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + + private boolean mVerified; + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + private void addV1Signer(SignerInfo signerInfo) { + mV1SchemeSigners.add(signerInfo); + } + + private void addV2Signer(SignerInfo signerInfo) { + mV2SchemeSigners.add(signerInfo); + } + + private void addV3Signer(SignerInfo signerInfo) { + mV3SchemeSigners.add(signerInfo); + } + + /** + * Returns {@code true} if the APK's source stamp signature + */ + public boolean isVerified() { + return mVerified; + } + + private void mergeFrom(ApkSigResult source) { + switch (source.signatureSchemeVersion) { + case Constants.VERSION_SOURCE_STAMP: + mVerified = source.verified; + if (!source.mSigners.isEmpty()) { + mSourceStampInfo = new SourceStampInfo(source.mSigners.get(0)); + } + break; + default: + throw new IllegalArgumentException( + "Unknown ApkSigResult Signing Block Scheme Id " + + source.signatureSchemeVersion); + } + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V1 signers of the + * provided APK. + */ + public List<SignerInfo> getV1SchemeSigners() { + return mV1SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V2 signers of the + * provided APK. + */ + public List<SignerInfo> getV2SchemeSigners() { + return mV2SchemeSigners; + } + + /** + * Returns a {@code List} of {@link SignerInfo} objects representing the V3 signers of the + * provided APK. + */ + public List<SignerInfo> getV3SchemeSigners() { + return mV3SchemeSigners; + } + + /** + * Returns the {@link SourceStampInfo} instance representing the source stamp signer for the + * APK, or null if the source stamp signature verification failed before the stamp signature + * block could be fully parsed. + */ + public SourceStampInfo getSourceStampInfo() { + return mSourceStampInfo; + } + + /** + * Returns {@code true} if an error was encountered while verifying the APK. + * + * <p>Any error prevents the APK from being considered verified. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + if (signer.containsErrors()) { + return true; + } + } + } + if (mSourceStampInfo != null) { + if (mSourceStampInfo.containsErrors()) { + return true; + } + } + return false; + } + + /** + * Returns the errors encountered while verifying the APK's source stamp. + */ + public List<ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered while verifying the APK's source stamp. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns all errors for this result, including any errors from signature scheme signers + * and the source stamp. + */ + public List<ApkVerificationIssue> getAllErrors() { + List<ApkVerificationIssue> errors = new ArrayList<>(); + errors.addAll(mErrors); + + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + errors.addAll(signer.getErrors()); + } + } + if (mSourceStampInfo != null) { + errors.addAll(mSourceStampInfo.getErrors()); + } + return errors; + } + + /** + * Returns all warnings for this result, including any warnings from signature scheme + * signers and the source stamp. + */ + public List<ApkVerificationIssue> getAllWarnings() { + List<ApkVerificationIssue> warnings = new ArrayList<>(); + warnings.addAll(mWarnings); + + for (List<SignerInfo> signers : mAllSchemeSigners) { + for (SignerInfo signer : signers) { + warnings.addAll(signer.getWarnings()); + } + } + if (mSourceStampInfo != null) { + warnings.addAll(mSourceStampInfo.getWarnings()); + } + return warnings; + } + + /** + * Contains information about an APK's signer and any errors encountered while parsing the + * corresponding signature block. + */ + public static class SignerInfo { + private X509Certificate mSigningCertificate; + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + + void setSigningCertificate(X509Certificate signingCertificate) { + mSigningCertificate = signingCertificate; + } + + void addVerificationError(int errorId, Object... params) { + mErrors.add(new ApkVerificationIssue(errorId, params)); + } + + void addVerificationWarning(int warningId, Object... params) { + mWarnings.add(new ApkVerificationIssue(warningId, params)); + } + + /** + * Returns the current signing certificate used by this signer. + */ + public X509Certificate getSigningCertificate() { + return mSigningCertificate; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing errors + * encountered during processing of this signer's signature block. + */ + public List<ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns a {@link List} of {@link ApkVerificationIssue} objects representing warnings + * encountered during processing of this signer's signature block. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + + /** + * Returns {@code true} if any errors were encountered while parsing this signer's + * signature block. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + } + + /** + * Contains information about an APK's source stamp and any errors encountered while + * parsing the stamp signature block. + */ + public static class SourceStampInfo { + private final List<X509Certificate> mCertificates; + private final List<X509Certificate> mCertificateLineage; + + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + + /* + * Since this utility is intended just to verify the source stamp, and the source stamp + * currently only logs warnings to prevent failing the APK signature verification, treat + * all warnings as errors. If the stamp verification is updated to log errors this + * should be set to false to ensure only errors trigger a failure verifying the source + * stamp. + */ + private static final boolean mWarningsAsErrors = true; + + private SourceStampInfo(ApkSignerInfo result) { + mCertificates = result.certs; + mCertificateLineage = result.certificateLineage; + mErrors.addAll(result.getErrors()); + mWarnings.addAll(result.getWarnings()); + } + + /** + * Returns the SourceStamp's signing certificate or {@code null} if not available. The + * certificate is guaranteed to be available if no errors were encountered during + * verification (see {@link #containsErrors()}. + * + * <p>This certificate contains the SourceStamp's public key. + */ + public X509Certificate getCertificate() { + return mCertificates.isEmpty() ? null : mCertificates.get(0); + } + + /** + * Returns a {@code List} of {@link X509Certificate} instances representing the source + * stamp signer's lineage with the oldest signer at element 0, or an empty {@code List} + * if the stamp's signing certificate has not been rotated. + */ + public List<X509Certificate> getCertificatesInLineage() { + return mCertificateLineage; + } + + /** + * Returns whether any errors were encountered during the source stamp verification. + */ + public boolean containsErrors() { + return !mErrors.isEmpty() || (mWarningsAsErrors && !mWarnings.isEmpty()); + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing errors that were + * encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getErrors() { + if (!mWarningsAsErrors) { + return mErrors; + } + List<ApkVerificationIssue> result = new ArrayList<>(); + result.addAll(mErrors); + result.addAll(mWarnings); + return result; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing warnings that + * were encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getWarnings() { + return mWarnings; + } + } + } + + /** + * Builder of {@link SourceStampVerifier} instances. + * + * <p> The resulting verifier, by default, checks whether the APK's source stamp signature will + * verify on all platform versions. The APK's {@code android:minSdkVersion} attribute is not + * queried to determine the APK's minimum supported level, so the caller should specify a lower + * bound with {@link #setMinCheckedPlatformVersion(int)}. + */ + public static class Builder { + private final File mApkFile; + private final DataSource mApkDataSource; + + private int mMinSdkVersion = 1; + private int mMaxSdkVersion = Integer.MAX_VALUE; + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(File apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkFile = apk; + mApkDataSource = null; + } + + /** + * Constructs a new {@code Builder} for source stamp verification of the provided {@code + * apk}. + */ + public Builder(DataSource apk) { + if (apk == null) { + throw new NullPointerException("apk == null"); + } + mApkDataSource = apk; + mApkFile = null; + } + + /** + * Sets the oldest Android platform version for which the APK's source stamp is verified. + * + * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all Android platforms starting from the platform version with the provided {@code + * minSdkVersion}. The upper end of the platform versions range can be modified via + * {@link #setMaxCheckedPlatformVersion(int)}. + * + * @param minSdkVersion API Level of the oldest platform for which to verify the APK + */ + public SourceStampVerifier.Builder setMinCheckedPlatformVersion(int minSdkVersion) { + mMinSdkVersion = minSdkVersion; + return this; + } + + /** + * Sets the newest Android platform version for which the APK's source stamp is verified. + * + * <p>APK source stamp verification will confirm that the APK's stamp is expected to verify + * on all platform versions up to and including the proviced {@code maxSdkVersion}. The + * lower end of the platform versions range can be modified via {@link + * #setMinCheckedPlatformVersion(int)}. + * + * @param maxSdkVersion API Level of the newest platform for which to verify the APK + * @see #setMinCheckedPlatformVersion(int) + */ + public SourceStampVerifier.Builder setMaxCheckedPlatformVersion(int maxSdkVersion) { + mMaxSdkVersion = maxSdkVersion; + return this; + } + + /** + * Returns a {@link SourceStampVerifier} initialized according to the configuration of this + * builder. + */ + public SourceStampVerifier build() { + return new SourceStampVerifier( + mApkFile, + mApkDataSource, + mMinSdkVersion, + mMaxSdkVersion); + } + } +} diff --git a/src/main/java/com/android/apksig/apk/ApkUtils.java b/src/main/java/com/android/apksig/apk/ApkUtils.java index c6cbf5f..69399a7 100644 --- a/src/main/java/com/android/apksig/apk/ApkUtils.java +++ b/src/main/java/com/android/apksig/apk/ApkUtils.java @@ -17,6 +17,7 @@ package com.android.apksig.apk; import com.android.apksig.internal.apk.AndroidBinXmlParser; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.CentralDirectoryRecord; @@ -24,11 +25,10 @@ import com.android.apksig.internal.zip.LocalFileRecord; import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -44,7 +44,8 @@ public abstract class ApkUtils { public static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ - public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = + SourceStampConstants.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; private ApkUtils() {} @@ -56,101 +57,27 @@ public abstract class ApkUtils { */ public static ZipSections findZipSections(DataSource apk) throws IOException, ZipFormatException { - Pair<ByteBuffer, Long> eocdAndOffsetInFile = - ZipUtils.findZipEndOfCentralDirectoryRecord(apk); - if (eocdAndOffsetInFile == null) { - throw new ZipFormatException("ZIP End of Central Directory record not found"); - } - - ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); - long eocdOffset = eocdAndOffsetInFile.getSecond(); - eocdBuf.order(ByteOrder.LITTLE_ENDIAN); - long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); - if (cdStartOffset > eocdOffset) { - throw new ZipFormatException( - "ZIP Central Directory start offset out of range: " + cdStartOffset - + ". ZIP End of Central Directory offset: " + eocdOffset); - } - - long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); - long cdEndOffset = cdStartOffset + cdSizeBytes; - if (cdEndOffset > eocdOffset) { - throw new ZipFormatException( - "ZIP Central Directory overlaps with End of Central Directory" - + ". CD end: " + cdEndOffset - + ", EoCD start: " + eocdOffset); - } - - int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); - + com.android.apksig.zip.ZipSections zipSections = ApkUtilsLite.findZipSections(apk); return new ZipSections( - cdStartOffset, - cdSizeBytes, - cdRecordCount, - eocdOffset, - eocdBuf); + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipCentralDirectorySizeBytes(), + zipSections.getZipCentralDirectoryRecordCount(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); } /** * Information about the ZIP sections of an APK. */ - public static class ZipSections { - private final long mCentralDirectoryOffset; - private final long mCentralDirectorySizeBytes; - private final int mCentralDirectoryRecordCount; - private final long mEocdOffset; - private final ByteBuffer mEocd; - + public static class ZipSections extends com.android.apksig.zip.ZipSections { public ZipSections( long centralDirectoryOffset, long centralDirectorySizeBytes, int centralDirectoryRecordCount, long eocdOffset, ByteBuffer eocd) { - mCentralDirectoryOffset = centralDirectoryOffset; - mCentralDirectorySizeBytes = centralDirectorySizeBytes; - mCentralDirectoryRecordCount = centralDirectoryRecordCount; - mEocdOffset = eocdOffset; - mEocd = eocd; - } - - /** - * Returns the start offset of the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public long getZipCentralDirectoryOffset() { - return mCentralDirectoryOffset; - } - - /** - * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public long getZipCentralDirectorySizeBytes() { - return mCentralDirectorySizeBytes; - } - - /** - * Returns the number of records in the ZIP Central Directory. This value is taken from the - * ZIP End of Central Directory record. - */ - public int getZipCentralDirectoryRecordCount() { - return mCentralDirectoryRecordCount; - } - - /** - * Returns the start offset of the ZIP End of Central Directory record. The record extends - * until the very end of the APK. - */ - public long getZipEndOfCentralDirectoryOffset() { - return mEocdOffset; - } - - /** - * Returns the contents of the ZIP End of Central Directory. - */ - public ByteBuffer getZipEndOfCentralDirectory() { - return mEocd; + super(centralDirectoryOffset, centralDirectorySizeBytes, centralDirectoryRecordCount, + eocdOffset, eocd); } } @@ -169,85 +96,26 @@ public abstract class ApkUtils { ZipUtils.setZipEocdCentralDirectoryOffset(eocd, offset); } - // See https://source.android.com/security/apksigning/v2.html - private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; - private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; - private static final int APK_SIG_BLOCK_MIN_SIZE = 32; - /** * Returns the APK Signing Block of the provided APK. * * @throws IOException if an I/O error occurs * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK * - * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 + * </a> */ public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) throws IOException, ApkSigningBlockNotFoundException { - // FORMAT (see https://source.android.com/security/apksigning/v2.html): - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes payload - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - - long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); - long centralDirEndOffset = - centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); - long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); - if (centralDirEndOffset != eocdStartOffset) { - throw new ApkSigningBlockNotFoundException( - "ZIP Central Directory is not immediately followed by End of Central Directory" - + ". CD end: " + centralDirEndOffset - + ", EoCD start: " + eocdStartOffset); - } - - if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { - throw new ApkSigningBlockNotFoundException( - "APK too small for APK Signing Block. ZIP Central Directory offset: " - + centralDirStartOffset); - } - // Read the magic and offset in file from the footer section of the block: - // * uint64: size of block - // * 16 bytes: magic - ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); - footer.order(ByteOrder.LITTLE_ENDIAN); - if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) - || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { - throw new ApkSigningBlockNotFoundException( - "No APK Signing Block before ZIP Central Directory"); - } - // Read and compare size fields - long apkSigBlockSizeInFooter = footer.getLong(0); - if ((apkSigBlockSizeInFooter < footer.capacity()) - || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); - } - int totalSize = (int) (apkSigBlockSizeInFooter + 8); - long apkSigBlockOffset = centralDirStartOffset - totalSize; - if (apkSigBlockOffset < 0) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block offset out of range: " + apkSigBlockOffset); - } - ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); - apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); - long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); - if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { - throw new ApkSigningBlockNotFoundException( - "APK Signing Block sizes in header and footer do not match: " - + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); - } - return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); + ApkUtilsLite.ApkSigningBlock apkSigningBlock = ApkUtilsLite.findApkSigningBlock(apk, + zipSections); + return new ApkSigningBlock(apkSigningBlock.getStartOffset(), apkSigningBlock.getContents()); } /** * Information about the location of the APK Signing Block inside an APK. */ - public static class ApkSigningBlock { - private final long mStartOffsetInApk; - private final DataSource mContents; - + public static class ApkSigningBlock extends ApkUtilsLite.ApkSigningBlock { /** * Constructs a new {@code ApkSigningBlock}. * @@ -256,23 +124,7 @@ public abstract class ApkUtils { * @param contents contents of the APK Signing Block */ public ApkSigningBlock(long startOffsetInApk, DataSource contents) { - mStartOffsetInApk = startOffsetInApk; - mContents = contents; - } - - /** - * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. - */ - public long getStartOffset() { - return mStartOffsetInApk; - } - - /** - * Returns the data source which provides the full contents of the APK Signing Block, - * including its footer. - */ - public DataSource getContents() { - return mContents; + super(startOffsetInApk, contents); } } @@ -324,6 +176,30 @@ public abstract class ApkUtils { private static final int DEBUGGABLE_ATTR_ID = 0x0101000f; /** + * Android resource ID of the {@code android:targetSandboxVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SANDBOX_VERSION_ATTR_ID = 0x0101054c; + + /** + * Android resource ID of the {@code android:targetSdkVersion} attribute in + * AndroidManifest.xml. + */ + private static final int TARGET_SDK_VERSION_ATTR_ID = 0x01010270; + private static final String USES_SDK_ELEMENT_TAG = "uses-sdk"; + + /** + * Android resource ID of the {@code android:versionCode} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_ATTR_ID = 0x0101021b; + private static final String MANIFEST_ELEMENT_TAG = "manifest"; + + /** + * Android resource ID of the {@code android:versionCodeMajor} attribute in AndroidManifest.xml. + */ + private static final int VERSION_CODE_MAJOR_ATTR_ID = 0x01010576; + + /** * Returns the lowest Android platform version (API Level) supported by an APK with the * provided {@code AndroidManifest.xml}. * @@ -607,14 +483,156 @@ public abstract class ApkUtils { } } - public static byte[] computeSha256DigestBytes(byte[] data) { - MessageDigest messageDigest; + /** + * Returns the security sandbox version targeted by an APK with the provided + * {@code AndroidManifest.xml}. + * + * <p>If the security sandbox version is not specified in the manifest a default value of 1 is + * returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + */ + public static int getTargetSandboxVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { + try { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, TARGET_SANDBOX_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // An ApkFormatException indicates the target sandbox is not specified in the manifest; + // return a default value of 1. + return 1; + } + } + + /** + * Returns the SDK version targeted by an APK with the provided {@code AndroidManifest.xml}. + * + * <p>If the targetSdkVersion is not specified the minimumSdkVersion is returned. If neither + * value is specified then a value of 1 is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + */ + public static int getTargetSdkVersionFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) { + // If the targetSdkVersion is not specified then the platform will use the value of the + // minSdkVersion; if neither is specified then the platform will use a value of 1. + int minSdkVersion = 1; + try { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + USES_SDK_ELEMENT_TAG, TARGET_SDK_VERSION_ATTR_ID); + } catch (ApkFormatException e) { + // Expected if the APK does not contain a targetSdkVersion attribute or the uses-sdk + // element is not specified at all. + } + androidManifestContents.rewind(); + try { + minSdkVersion = getMinSdkVersionFromBinaryAndroidManifest(androidManifestContents); + } catch (ApkFormatException e) { + // Similar to above, expected if the APK does not contain a minSdkVersion attribute, or + // the uses-sdk element is not specified at all. + } + return minSdkVersion; + } + + /** + * Returns the versionCode of the APK according to its {@code AndroidManifest.xml}. + * + * <p>If the versionCode is not specified in the {@code AndroidManifest.xml} or is not a valid + * integer an ApkFormatException is thrown. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the versionCode, or if the + * versionCode attribute value is not available. + */ + public static int getVersionCodeFromBinaryAndroidManifest(ByteBuffer androidManifestContents) + throws ApkFormatException { + return getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_ATTR_ID); + } + + /** + * Returns the versionCode and versionCodeMajor of the APK according to its {@code + * AndroidManifest.xml} combined together as a single long value. + * + * <p>The versionCodeMajor is placed in the upper 32 bits, and the versionCode is in the lower + * 32 bits. If the versionCodeMajor is not specified then the versionCode is returned. + * + * @param androidManifestContents contents of {@code AndroidManifest.xml} in binary Android + * resource format + * @throws ApkFormatException if an error occurred while determining the version, or if the + * versionCode attribute value is not available. + */ + public static long getLongVersionCodeFromBinaryAndroidManifest( + ByteBuffer androidManifestContents) throws ApkFormatException { + // If the versionCode is not found then allow the ApkFormatException to be thrown to notify + // the caller that the versionCode is not available. + int versionCode = getVersionCodeFromBinaryAndroidManifest(androidManifestContents); + long versionCodeMajor = 0; + try { + androidManifestContents.rewind(); + versionCodeMajor = getAttributeValueFromBinaryAndroidManifest(androidManifestContents, + MANIFEST_ELEMENT_TAG, VERSION_CODE_MAJOR_ATTR_ID); + } catch (ApkFormatException e) { + // This is expected if the versionCodeMajor has not been defined for the APK; in this + // case the return value is just the versionCode. + } + return (versionCodeMajor << 32) | versionCode; + } + + /** + * Returns the integer value of the requested {@code attributeId} in the specified {@code + * elementName} from the provided {@code androidManifestContents} in binary Android resource + * format. + * + * @throws ApkFormatException if an error occurred while attempting to obtain the attribute, or + * if the requested attribute is not found. + */ + private static int getAttributeValueFromBinaryAndroidManifest( + ByteBuffer androidManifestContents, String elementName, int attributeId) + throws ApkFormatException { + if (elementName == null) { + throw new NullPointerException("elementName cannot be null"); + } try { - messageDigest = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("SHA-256 is not found", e); + AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); + int eventType = parser.getEventType(); + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) + && (elementName.equals(parser.getName()))) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + if (parser.getAttributeNameResourceId(i) == attributeId) { + int valueType = parser.getAttributeValueType(i); + switch (valueType) { + case AndroidBinXmlParser.VALUE_TYPE_INT: + case AndroidBinXmlParser.VALUE_TYPE_STRING: + return parser.getAttributeIntValue(i); + default: + throw new ApkFormatException( + "Unsupported value type, " + valueType + + ", for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName); + + } + } + } + } + eventType = parser.next(); + } + throw new ApkFormatException( + "Failed to determine APK's " + elementName + " attribute " + + String.format("0x%08X", attributeId) + " value"); + } catch (AndroidBinXmlParser.XmlParserException e) { + throw new ApkFormatException( + "Unable to determine value for attribute " + String.format("0x%08X", + attributeId) + " under element " + elementName + + "; malformed binary resource: " + ANDROID_MANIFEST_ZIP_ENTRY_NAME, e); } - messageDigest.update(data); - return messageDigest.digest(); + } + + public static byte[] computeSha256DigestBytes(byte[] data) { + return ApkUtilsLite.computeSha256DigestBytes(data); } } diff --git a/src/main/java/com/android/apksig/apk/ApkUtilsLite.java b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java new file mode 100644 index 0000000..13f2301 --- /dev/null +++ b/src/main/java/com/android/apksig/apk/ApkUtilsLite.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.apk; + +import com.android.apksig.internal.util.Pair; +import com.android.apksig.internal.zip.ZipUtils; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Lightweight version of the ApkUtils for clients that only require a subset of the utility + * functionality. + */ +public class ApkUtilsLite { + private ApkUtilsLite() {} + + /** + * Finds the main ZIP sections of the provided APK. + * + * @throws IOException if an I/O error occurred while reading the APK + * @throws ZipFormatException if the APK is malformed + */ + public static ZipSections findZipSections(DataSource apk) + throws IOException, ZipFormatException { + Pair<ByteBuffer, Long> eocdAndOffsetInFile = + ZipUtils.findZipEndOfCentralDirectoryRecord(apk); + if (eocdAndOffsetInFile == null) { + throw new ZipFormatException("ZIP End of Central Directory record not found"); + } + + ByteBuffer eocdBuf = eocdAndOffsetInFile.getFirst(); + long eocdOffset = eocdAndOffsetInFile.getSecond(); + eocdBuf.order(ByteOrder.LITTLE_ENDIAN); + long cdStartOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocdBuf); + if (cdStartOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory start offset out of range: " + cdStartOffset + + ". ZIP End of Central Directory offset: " + eocdOffset); + } + + long cdSizeBytes = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocdBuf); + long cdEndOffset = cdStartOffset + cdSizeBytes; + if (cdEndOffset > eocdOffset) { + throw new ZipFormatException( + "ZIP Central Directory overlaps with End of Central Directory" + + ". CD end: " + cdEndOffset + + ", EoCD start: " + eocdOffset); + } + + int cdRecordCount = ZipUtils.getZipEocdCentralDirectoryTotalRecordCount(eocdBuf); + + return new ZipSections( + cdStartOffset, + cdSizeBytes, + cdRecordCount, + eocdOffset, + eocdBuf); + } + + // See https://source.android.com/security/apksigning/v2.html + private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L; + private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L; + private static final int APK_SIG_BLOCK_MIN_SIZE = 32; + + /** + * Returns the APK Signing Block of the provided APK. + * + * @throws IOException if an I/O error occurs + * @throws ApkSigningBlockNotFoundException if there is no APK Signing Block in the APK + * + * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2 + * </a> + */ + public static ApkSigningBlock findApkSigningBlock(DataSource apk, ZipSections zipSections) + throws IOException, ApkSigningBlockNotFoundException { + // FORMAT (see https://source.android.com/security/apksigning/v2.html): + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes payload + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + + long centralDirStartOffset = zipSections.getZipCentralDirectoryOffset(); + long centralDirEndOffset = + centralDirStartOffset + zipSections.getZipCentralDirectorySizeBytes(); + long eocdStartOffset = zipSections.getZipEndOfCentralDirectoryOffset(); + if (centralDirEndOffset != eocdStartOffset) { + throw new ApkSigningBlockNotFoundException( + "ZIP Central Directory is not immediately followed by End of Central Directory" + + ". CD end: " + centralDirEndOffset + + ", EoCD start: " + eocdStartOffset); + } + + if (centralDirStartOffset < APK_SIG_BLOCK_MIN_SIZE) { + throw new ApkSigningBlockNotFoundException( + "APK too small for APK Signing Block. ZIP Central Directory offset: " + + centralDirStartOffset); + } + // Read the magic and offset in file from the footer section of the block: + // * uint64: size of block + // * 16 bytes: magic + ByteBuffer footer = apk.getByteBuffer(centralDirStartOffset - 24, 24); + footer.order(ByteOrder.LITTLE_ENDIAN); + if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO) + || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) { + throw new ApkSigningBlockNotFoundException( + "No APK Signing Block before ZIP Central Directory"); + } + // Read and compare size fields + long apkSigBlockSizeInFooter = footer.getLong(0); + if ((apkSigBlockSizeInFooter < footer.capacity()) + || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block size out of range: " + apkSigBlockSizeInFooter); + } + int totalSize = (int) (apkSigBlockSizeInFooter + 8); + long apkSigBlockOffset = centralDirStartOffset - totalSize; + if (apkSigBlockOffset < 0) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block offset out of range: " + apkSigBlockOffset); + } + ByteBuffer apkSigBlock = apk.getByteBuffer(apkSigBlockOffset, 8); + apkSigBlock.order(ByteOrder.LITTLE_ENDIAN); + long apkSigBlockSizeInHeader = apkSigBlock.getLong(0); + if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) { + throw new ApkSigningBlockNotFoundException( + "APK Signing Block sizes in header and footer do not match: " + + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter); + } + return new ApkSigningBlock(apkSigBlockOffset, apk.slice(apkSigBlockOffset, totalSize)); + } + + /** + * Information about the location of the APK Signing Block inside an APK. + */ + public static class ApkSigningBlock { + private final long mStartOffsetInApk; + private final DataSource mContents; + + /** + * Constructs a new {@code ApkSigningBlock}. + * + * @param startOffsetInApk start offset (in bytes, relative to start of file) of the APK + * Signing Block inside the APK file + * @param contents contents of the APK Signing Block + */ + public ApkSigningBlock(long startOffsetInApk, DataSource contents) { + mStartOffsetInApk = startOffsetInApk; + mContents = contents; + } + + /** + * Returns the start offset (in bytes, relative to start of file) of the APK Signing Block. + */ + public long getStartOffset() { + return mStartOffsetInApk; + } + + /** + * Returns the data source which provides the full contents of the APK Signing Block, + * including its footer. + */ + public DataSource getContents() { + return mContents; + } + } + + public static byte[] computeSha256DigestBytes(byte[] data) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 is not found", e); + } + messageDigest.update(data); + return messageDigest.digest(); + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java new file mode 100644 index 0000000..6151351 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/ApkSigResult.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signature verification result. + */ +public class ApkSigResult { + public final int signatureSchemeVersion; + + /** Whether the APK's Signature Scheme signature verifies. */ + public boolean verified; + + public final List<ApkSignerInfo> mSigners = new ArrayList<>(); + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + + public ApkSigResult(int signatureSchemeVersion) { + this.signatureSchemeVersion = signatureSchemeVersion; + } + + /** + * Returns {@code true} if this result encountered errors during verification. + */ + public boolean containsErrors() { + if (!mErrors.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsErrors()) { + return true; + } + } + } + return false; + } + + /** + * Returns {@code true} if this result encountered warnings during verification. + */ + public boolean containsWarnings() { + if (!mWarnings.isEmpty()) { + return true; + } + if (!mSigners.isEmpty()) { + for (ApkSignerInfo signer : mSigners) { + if (signer.containsWarnings()) { + return true; + } + } + } + return false; + } + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... parameters) { + mErrors.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this result using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... parameters) { + mWarnings.add(new ApkVerificationIssue(issueId, parameters)); + } + + /** + * Returns the errors encountered during verification. + */ + public List<? extends ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification. + */ + public List<? extends ApkVerificationIssue> getWarnings() { + return mWarnings; + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java new file mode 100644 index 0000000..e0ea365 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.ApkVerificationIssue; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signer. + */ +public class ApkSignerInfo { + public int index; + public List<X509Certificate> certs = new ArrayList<>(); + public List<X509Certificate> certificateLineage = new ArrayList<>(); + + private final List<ApkVerificationIssue> mWarnings = new ArrayList<>(); + private final List<ApkVerificationIssue> mErrors = new ArrayList<>(); + + /** + * Adds a new {@link ApkVerificationIssue} as an error to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addError(int issueId, Object... params) { + mErrors.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as a warning to this signer using the provided {@code + * issueId} and {@code params}. + */ + public void addWarning(int issueId, Object... params) { + mWarnings.add(new ApkVerificationIssue(issueId, params)); + } + + /** + * Returns {@code true} if any errors were encountered during verification for this signer. + */ + public boolean containsErrors() { + return !mErrors.isEmpty(); + } + + /** + * Returns {@code true} if any warnings were encountered during verification for this signer. + */ + public boolean containsWarnings() { + return !mWarnings.isEmpty(); + } + + /** + * Returns the errors encountered during verification for this signer. + */ + public List<? extends ApkVerificationIssue> getErrors() { + return mErrors; + } + + /** + * Returns the warnings encountered during verification for this signer. + */ + public List<? extends ApkVerificationIssue> getWarnings() { + return mWarnings; + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java index f027525..e8f6fc0 100644 --- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java +++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java @@ -23,7 +23,6 @@ import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUN import com.android.apksig.ApkVerifier; import com.android.apksig.SigningCertificateLineage; import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.apk.ApkSigningBlockNotFoundException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.asn1.Asn1BerParser; import com.android.apksig.internal.asn1.Asn1DecodingException; @@ -53,7 +52,6 @@ import com.android.apksig.util.RunnablesExecutor; import java.io.IOException; import java.math.BigInteger; -import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.DigestException; @@ -86,7 +84,6 @@ import javax.security.auth.x500.X500Principal; public class ApkSigningBlockUtils { - private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); private static final long CONTENT_DIGESTED_CHUNK_MAX_SIZE_BYTES = 1024 * 1024; public static final int ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; private static final byte[] APK_SIGNING_BLOCK_MAGIC = @@ -110,58 +107,10 @@ public class ApkSigningBlockUtils { * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. */ public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { - ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); - ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); - return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + return ApkSigningBlockUtilsLite.compareSignatureAlgorithm(alg1, alg2); } /** - * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number - * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. - */ - private static int compareContentDigestAlgorithm( - ContentDigestAlgorithm alg1, - ContentDigestAlgorithm alg2) { - switch (alg1) { - case CHUNKED_SHA256: - switch (alg2) { - case CHUNKED_SHA256: - return 0; - case CHUNKED_SHA512: - case VERITY_CHUNKED_SHA256: - return -1; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - case CHUNKED_SHA512: - switch (alg2) { - case CHUNKED_SHA256: - case VERITY_CHUNKED_SHA256: - return 1; - case CHUNKED_SHA512: - return 0; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - case VERITY_CHUNKED_SHA256: - switch (alg2) { - case CHUNKED_SHA256: - return 1; - case VERITY_CHUNKED_SHA256: - return 0; - case CHUNKED_SHA512: - return -1; - default: - throw new IllegalArgumentException("Unknown alg2: " + alg2); - } - default: - throw new IllegalArgumentException("Unknown alg1: " + alg1); - } - } - - - - /** * Verifies integrity of the APK outside of the APK Signing Block by computing digests of the * APK and comparing them against the digests listed in APK Signing Block. The expected digests * are taken from {@code SignerInfos} of the provided {@code result}. @@ -279,155 +228,27 @@ public class ApkSigningBlockUtils { ByteBuffer apkSigningBlock, int blockId, Result result) throws SignatureNotFoundException { - checkByteOrderLittleEndian(apkSigningBlock); - // FORMAT: - // OFFSET DATA TYPE DESCRIPTION - // * @+0 bytes uint64: size in bytes (excluding this field) - // * @+8 bytes pairs - // * @-24 bytes uint64: size in bytes (same as the one above) - // * @-16 bytes uint128: magic - ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); - - int entryCount = 0; - while (pairs.hasRemaining()) { - entryCount++; - if (pairs.remaining() < 8) { - throw new SignatureNotFoundException( - "Insufficient data to read size of APK Signing Block entry #" + entryCount); - } - long lenLong = pairs.getLong(); - if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount - + " size out of range: " + lenLong); - } - int len = (int) lenLong; - int nextEntryPos = pairs.position() + len; - if (len > pairs.remaining()) { - throw new SignatureNotFoundException( - "APK Signing Block entry #" + entryCount + " size out of range: " + len - + ", available: " + pairs.remaining()); - } - int id = pairs.getInt(); - if (id == blockId) { - return getByteBuffer(pairs, len - 4); - } - pairs.position(nextEntryPos); - } - - throw new SignatureNotFoundException( - "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); - } - - public static void checkByteOrderLittleEndian(ByteBuffer buffer) { - if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { - throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); - } - } - - /** - * Returns new byte buffer whose content is a shared subsequence of this buffer's content - * between the specified start (inclusive) and end (exclusive) positions. As opposed to - * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source - * buffer's byte order. - */ - private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { - if (start < 0) { - throw new IllegalArgumentException("start: " + start); - } - if (end < start) { - throw new IllegalArgumentException("end < start: " + end + " < " + start); - } - int capacity = source.capacity(); - if (end > source.capacity()) { - throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); - } - int originalLimit = source.limit(); - int originalPosition = source.position(); try { - source.position(0); - source.limit(end); - source.position(start); - ByteBuffer result = source.slice(); - result.order(source.order()); - return result; - } finally { - source.position(0); - source.limit(originalLimit); - source.position(originalPosition); + return ApkSigningBlockUtilsLite.findApkSignatureSchemeBlock(apkSigningBlock, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); } } - /** - * Relative <em>get</em> method for reading {@code size} number of bytes from the current - * position of this buffer. - * - * <p>This method reads the next {@code size} bytes at this buffer's current position, - * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to - * {@code size}, byte order set to this buffer's byte order; and then increments the position by - * {@code size}. - */ - private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { - if (size < 0) { - throw new IllegalArgumentException("size: " + size); - } - int originalLimit = source.limit(); - int position = source.position(); - int limit = position + size; - if ((limit < position) || (limit > originalLimit)) { - throw new BufferUnderflowException(); - } - source.limit(limit); - try { - ByteBuffer result = source.slice(); - result.order(source.order()); - source.position(limit); - return result; - } finally { - source.limit(originalLimit); - } + public static void checkByteOrderLittleEndian(ByteBuffer buffer) { + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(buffer); } public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { - if (source.remaining() < 4) { - throw new ApkFormatException( - "Remaining buffer too short to contain length of length-prefixed field" - + ". Remaining: " + source.remaining()); - } - int len = source.getInt(); - if (len < 0) { - throw new IllegalArgumentException("Negative length"); - } else if (len > source.remaining()) { - throw new ApkFormatException( - "Length-prefixed field longer than remaining buffer" - + ". Field length: " + len + ", remaining: " + source.remaining()); - } - return getByteBuffer(source, len); + return ApkSigningBlockUtilsLite.getLengthPrefixedSlice(source); } public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { - int len = buf.getInt(); - if (len < 0) { - throw new ApkFormatException("Negative length"); - } else if (len > buf.remaining()) { - throw new ApkFormatException( - "Underflow while reading length-prefixed value. Length: " + len - + ", available: " + buf.remaining()); - } - byte[] result = new byte[len]; - buf.get(result); - return result; + return ApkSigningBlockUtilsLite.readLengthPrefixedByteArray(buf); } public static String toHex(byte[] value) { - StringBuilder sb = new StringBuilder(value.length * 2); - int len = value.length; - for (int i = 0; i < len; i++) { - int hi = (value[i] & 0xff) >>> 4; - int lo = value[i] & 0x0f; - sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); - } - return sb.toString(); + return ApkSigningBlockUtilsLite.toHex(value); } public static Map<ContentDigestAlgorithm, byte[]> computeContentDigests( @@ -946,20 +767,8 @@ public class ApkSigningBlockUtils { public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( List<Pair<Integer, byte[]>> sequence) { - int resultSize = 0; - for (Pair<Integer, byte[]> element : sequence) { - resultSize += 12 + element.getSecond().length; - } - ByteBuffer result = ByteBuffer.allocate(resultSize); - result.order(ByteOrder.LITTLE_ENDIAN); - for (Pair<Integer, byte[]> element : sequence) { - byte[] second = element.getSecond(); - result.putInt(8 + second.length); - result.putInt(element.getFirst()); - result.putInt(second.length); - result.put(second); - } - return result.array(); + return ApkSigningBlockUtilsLite + .encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(sequence); } /** @@ -976,30 +785,11 @@ public class ApkSigningBlockUtils { public static SignatureInfo findSignature( DataSource apk, ApkUtils.ZipSections zipSections, int blockId, Result result) throws IOException, SignatureNotFoundException { - // Find the APK Signing Block. - DataSource apkSigningBlock; - long apkSigningBlockOffset; try { - ApkUtils.ApkSigningBlock apkSigningBlockInfo = - ApkUtils.findApkSigningBlock(apk, zipSections); - apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); - apkSigningBlock = apkSigningBlockInfo.getContents(); - } catch (ApkSigningBlockNotFoundException e) { - throw new SignatureNotFoundException(e.getMessage(), e); + return ApkSigningBlockUtilsLite.findSignature(apk, zipSections, blockId); + } catch (com.android.apksig.internal.apk.SignatureNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage()); } - ByteBuffer apkSigningBlockBuf = - apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); - apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); - - // Find the APK Signature Scheme Block inside the APK Signing Block. - ByteBuffer apkSignatureSchemeBlock = - findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId, result); - return new SignatureInfo( - apkSignatureSchemeBlock, - apkSigningBlockOffset, - zipSections.getZipCentralDirectoryOffset(), - zipSections.getZipEndOfCentralDirectoryOffset(), - zipSections.getZipEndOfCentralDirectory()); } /** @@ -1173,57 +963,39 @@ public class ApkSigningBlockUtils { * @throws NoSupportedSignaturesException if no supported signatures were * found for an Android platform version in the range. */ - public static List<SupportedSignature> getSignaturesToVerify( - List<SupportedSignature> signatures, int minSdkVersion, int maxSdkVersion) + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion) throws NoSupportedSignaturesException { - // Pick the signature with the strongest algorithm at all required SDK versions, to mimic - // Android's behavior on those versions. - // - // Here we assume that, once introduced, a signature algorithm continues to be supported in - // all future Android versions. We also assume that the better-than relationship between - // algorithms is exactly the same on all Android platform versions (except that older - // platforms might support fewer algorithms). If these assumption are no longer true, the - // logic here will need to change accordingly. - Map<Integer, SupportedSignature> bestSigAlgorithmOnSdkVersion = new HashMap<>(); - int minProvidedSignaturesVersion = Integer.MAX_VALUE; - for (SupportedSignature sig : signatures) { - SignatureAlgorithm sigAlgorithm = sig.algorithm; - int sigMinSdkVersion = sigAlgorithm.getMinSdkVersion(); - if (sigMinSdkVersion > maxSdkVersion) { - continue; - } - if (sigMinSdkVersion < minProvidedSignaturesVersion) { - minProvidedSignaturesVersion = sigMinSdkVersion; - } - - SupportedSignature candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); - if ((candidate == null) - || (compareSignatureAlgorithm( - sigAlgorithm, candidate.algorithm) > 0)) { - bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); - } - } + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } - // Must have some supported signature algorithms for minSdkVersion. - if (minSdkVersion < minProvidedSignaturesVersion) { - throw new NoSupportedSignaturesException( - "Minimum provided signature version " + minProvidedSignaturesVersion + - " > minSdkVersion " + minSdkVersion); - } - if (bestSigAlgorithmOnSdkVersion.isEmpty()) { - throw new NoSupportedSignaturesException("No supported signature"); + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws NoSupportedSignaturesException { + try { + return ApkSigningBlockUtilsLite.getSignaturesToVerify(signatures, minSdkVersion, + maxSdkVersion, onlyRequireJcaSupport); + } catch (NoApkSupportedSignaturesException e) { + throw new NoSupportedSignaturesException(e.getMessage()); } - List<SupportedSignature> signaturesToVerify = - new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); - Collections.sort( - signaturesToVerify, - (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); - return signaturesToVerify; } - public static class NoSupportedSignaturesException extends Exception { - private static final long serialVersionUID = 1L; - + public static class NoSupportedSignaturesException extends NoApkSupportedSignaturesException { public NoSupportedSignaturesException(String message) { super(message); } @@ -1386,19 +1158,14 @@ public class ApkSigningBlockUtils { public SigningCertificateLineage mSigningCertificateLineage; } - public static class Result { - public final int signatureSchemeVersion; - - /** Whether the APK's APK Signature Scheme signature verifies. */ - public boolean verified; - - public final List<Result.SignerInfo> signers = new ArrayList<>(); + public static class Result extends ApkSigResult { public SigningCertificateLineage signingCertificateLineage = null; + public final List<Result.SignerInfo> signers = new ArrayList<>(); private final List<ApkVerifier.IssueWithParams> mWarnings = new ArrayList<>(); private final List<ApkVerifier.IssueWithParams> mErrors = new ArrayList<>(); public Result(int signatureSchemeVersion) { - this.signatureSchemeVersion = signatureSchemeVersion; + super(signatureSchemeVersion); } public boolean containsErrors() { @@ -1437,17 +1204,17 @@ public class ApkSigningBlockUtils { mWarnings.add(new ApkVerifier.IssueWithParams(msg, parameters)); } + @Override public List<ApkVerifier.IssueWithParams> getErrors() { return mErrors; } + @Override public List<ApkVerifier.IssueWithParams> getWarnings() { return mWarnings; } - public static class SignerInfo { - public int index; - public List<X509Certificate> certs = new ArrayList<>(); + public static class SignerInfo extends ApkSignerInfo { public List<ContentDigest> contentDigests = new ArrayList<>(); public Map<ContentDigestAlgorithm, byte[]> verifiedContentDigests = new HashMap<>(); public List<Signature> signatures = new ArrayList<>(); @@ -1541,13 +1308,9 @@ public class ApkSigningBlockUtils { } } - public static class SupportedSignature { - public final SignatureAlgorithm algorithm; - public final byte[] signature; - + public static class SupportedSignature extends ApkSupportedSignature { public SupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { - this.algorithm = algorithm; - this.signature = signature; + super(algorithm, signature); } } diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java new file mode 100644 index 0000000..40ae947 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtilsLite.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.apk.ApkSigningBlockNotFoundException; +import com.android.apksig.apk.ApkUtilsLite; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Lightweight version of the ApkSigningBlockUtils for clients that only require a subset of the + * utility functionality. + */ +public class ApkSigningBlockUtilsLite { + private ApkSigningBlockUtilsLite() {} + + private static final char[] HEX_DIGITS = "0123456789abcdef".toCharArray(); + /** + * Returns the APK Signature Scheme block contained in the provided APK file for the given ID + * and the additional information relevant for verifying the block against the file. + * + * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs + * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 + * block ID. + * + * @throws SignatureNotFoundException if the APK is not signed using given APK Signature Scheme + * @throws IOException if an I/O error occurs while reading the APK + */ + public static SignatureInfo findSignature( + DataSource apk, ZipSections zipSections, int blockId) + throws IOException, SignatureNotFoundException { + // Find the APK Signing Block. + DataSource apkSigningBlock; + long apkSigningBlockOffset; + try { + ApkUtilsLite.ApkSigningBlock apkSigningBlockInfo = + ApkUtilsLite.findApkSigningBlock(apk, zipSections); + apkSigningBlockOffset = apkSigningBlockInfo.getStartOffset(); + apkSigningBlock = apkSigningBlockInfo.getContents(); + } catch (ApkSigningBlockNotFoundException e) { + throw new SignatureNotFoundException(e.getMessage(), e); + } + ByteBuffer apkSigningBlockBuf = + apkSigningBlock.getByteBuffer(0, (int) apkSigningBlock.size()); + apkSigningBlockBuf.order(ByteOrder.LITTLE_ENDIAN); + + // Find the APK Signature Scheme Block inside the APK Signing Block. + ByteBuffer apkSignatureSchemeBlock = + findApkSignatureSchemeBlock(apkSigningBlockBuf, blockId); + return new SignatureInfo( + apkSignatureSchemeBlock, + apkSigningBlockOffset, + zipSections.getZipCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectoryOffset(), + zipSections.getZipEndOfCentralDirectory()); + } + + public static ByteBuffer findApkSignatureSchemeBlock( + ByteBuffer apkSigningBlock, + int blockId) throws SignatureNotFoundException { + checkByteOrderLittleEndian(apkSigningBlock); + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint64: size in bytes (excluding this field) + // * @+8 bytes pairs + // * @-24 bytes uint64: size in bytes (same as the one above) + // * @-16 bytes uint128: magic + ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24); + + int entryCount = 0; + while (pairs.hasRemaining()) { + entryCount++; + if (pairs.remaining() < 8) { + throw new SignatureNotFoundException( + "Insufficient data to read size of APK Signing Block entry #" + entryCount); + } + long lenLong = pairs.getLong(); + if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + + " size out of range: " + lenLong); + } + int len = (int) lenLong; + int nextEntryPos = pairs.position() + len; + if (len > pairs.remaining()) { + throw new SignatureNotFoundException( + "APK Signing Block entry #" + entryCount + " size out of range: " + len + + ", available: " + pairs.remaining()); + } + int id = pairs.getInt(); + if (id == blockId) { + return getByteBuffer(pairs, len - 4); + } + pairs.position(nextEntryPos); + } + + throw new SignatureNotFoundException( + "No APK Signature Scheme block in APK Signing Block with ID: " + blockId); + } + + public static void checkByteOrderLittleEndian(ByteBuffer buffer) { + if (buffer.order() != ByteOrder.LITTLE_ENDIAN) { + throw new IllegalArgumentException("ByteBuffer byte order must be little endian"); + } + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion) + throws NoApkSupportedSignaturesException { + return getSignaturesToVerify(signatures, minSdkVersion, maxSdkVersion, false); + } + + /** + * Returns the subset of signatures which are expected to be verified by at least one Android + * platform version in the {@code [minSdkVersion, maxSdkVersion]} range. The returned result is + * guaranteed to contain at least one signature. + * + * <p>{@code onlyRequireJcaSupport} can be set to true for cases that only require verifying a + * signature within the signing block using the standard JCA. + * + * <p>Each Android platform version typically verifies exactly one signature from the provided + * {@code signatures} set. This method returns the set of these signatures collected over all + * requested platform versions. As a result, the result may contain more than one signature. + * + * @throws NoApkSupportedSignaturesException if no supported signatures were + * found for an Android platform version in the range. + */ + public static <T extends ApkSupportedSignature> List<T> getSignaturesToVerify( + List<T> signatures, int minSdkVersion, int maxSdkVersion, + boolean onlyRequireJcaSupport) throws + NoApkSupportedSignaturesException { + // Pick the signature with the strongest algorithm at all required SDK versions, to mimic + // Android's behavior on those versions. + // + // Here we assume that, once introduced, a signature algorithm continues to be supported in + // all future Android versions. We also assume that the better-than relationship between + // algorithms is exactly the same on all Android platform versions (except that older + // platforms might support fewer algorithms). If these assumption are no longer true, the + // logic here will need to change accordingly. + Map<Integer, T> + bestSigAlgorithmOnSdkVersion = new HashMap<>(); + int minProvidedSignaturesVersion = Integer.MAX_VALUE; + for (T sig : signatures) { + SignatureAlgorithm sigAlgorithm = sig.algorithm; + int sigMinSdkVersion = onlyRequireJcaSupport ? sigAlgorithm.getJcaSigAlgMinSdkVersion() + : sigAlgorithm.getMinSdkVersion(); + if (sigMinSdkVersion > maxSdkVersion) { + continue; + } + if (sigMinSdkVersion < minProvidedSignaturesVersion) { + minProvidedSignaturesVersion = sigMinSdkVersion; + } + + T candidate = bestSigAlgorithmOnSdkVersion.get(sigMinSdkVersion); + if ((candidate == null) + || (compareSignatureAlgorithm( + sigAlgorithm, candidate.algorithm) > 0)) { + bestSigAlgorithmOnSdkVersion.put(sigMinSdkVersion, sig); + } + } + + // Must have some supported signature algorithms for minSdkVersion. + if (minSdkVersion < minProvidedSignaturesVersion) { + throw new NoApkSupportedSignaturesException( + "Minimum provided signature version " + minProvidedSignaturesVersion + + " > minSdkVersion " + minSdkVersion); + } + if (bestSigAlgorithmOnSdkVersion.isEmpty()) { + throw new NoApkSupportedSignaturesException("No supported signature"); + } + List<T> signaturesToVerify = + new ArrayList<>(bestSigAlgorithmOnSdkVersion.values()); + Collections.sort( + signaturesToVerify, + (sig1, sig2) -> Integer.compare(sig1.algorithm.getId(), sig2.algorithm.getId())); + return signaturesToVerify; + } + + /** + * Returns positive number if {@code alg1} is preferred over {@code alg2}, {@code -1} if + * {@code alg2} is preferred over {@code alg1}, and {@code 0} if there is no preference. + */ + public static int compareSignatureAlgorithm(SignatureAlgorithm alg1, SignatureAlgorithm alg2) { + ContentDigestAlgorithm digestAlg1 = alg1.getContentDigestAlgorithm(); + ContentDigestAlgorithm digestAlg2 = alg2.getContentDigestAlgorithm(); + return compareContentDigestAlgorithm(digestAlg1, digestAlg2); + } + + /** + * Returns a positive number if {@code alg1} is preferred over {@code alg2}, a negative number + * if {@code alg2} is preferred over {@code alg1}, or {@code 0} if there is no preference. + */ + private static int compareContentDigestAlgorithm( + ContentDigestAlgorithm alg1, + ContentDigestAlgorithm alg2) { + switch (alg1) { + case CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + case VERITY_CHUNKED_SHA256: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case CHUNKED_SHA512: + switch (alg2) { + case CHUNKED_SHA256: + case VERITY_CHUNKED_SHA256: + return 1; + case CHUNKED_SHA512: + return 0; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + case VERITY_CHUNKED_SHA256: + switch (alg2) { + case CHUNKED_SHA256: + return 1; + case VERITY_CHUNKED_SHA256: + return 0; + case CHUNKED_SHA512: + return -1; + default: + throw new IllegalArgumentException("Unknown alg2: " + alg2); + } + default: + throw new IllegalArgumentException("Unknown alg1: " + alg1); + } + } + + /** + * Returns new byte buffer whose content is a shared subsequence of this buffer's content + * between the specified start (inclusive) and end (exclusive) positions. As opposed to + * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source + * buffer's byte order. + */ + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + + /** + * Relative <em>get</em> method for reading {@code size} number of bytes from the current + * position of this buffer. + * + * <p>This method reads the next {@code size} bytes at this buffer's current position, + * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to + * {@code size}, byte order set to this buffer's byte order; and then increments the position by + * {@code size}. + */ + private static ByteBuffer getByteBuffer(ByteBuffer source, int size) { + if (size < 0) { + throw new IllegalArgumentException("size: " + size); + } + int originalLimit = source.limit(); + int position = source.position(); + int limit = position + size; + if ((limit < position) || (limit > originalLimit)) { + throw new BufferUnderflowException(); + } + source.limit(limit); + try { + ByteBuffer result = source.slice(); + result.order(source.order()); + source.position(limit); + return result; + } finally { + source.limit(originalLimit); + } + } + + public static String toHex(byte[] value) { + StringBuilder sb = new StringBuilder(value.length * 2); + int len = value.length; + for (int i = 0; i < len; i++) { + int hi = (value[i] & 0xff) >>> 4; + int lo = value[i] & 0x0f; + sb.append(HEX_DIGITS[hi]).append(HEX_DIGITS[lo]); + } + return sb.toString(); + } + + public static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws ApkFormatException { + if (source.remaining() < 4) { + throw new ApkFormatException( + "Remaining buffer too short to contain length of length-prefixed field" + + ". Remaining: " + source.remaining()); + } + int len = source.getInt(); + if (len < 0) { + throw new IllegalArgumentException("Negative length"); + } else if (len > source.remaining()) { + throw new ApkFormatException( + "Length-prefixed field longer than remaining buffer" + + ". Field length: " + len + ", remaining: " + source.remaining()); + } + return getByteBuffer(source, len); + } + + public static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws ApkFormatException { + int len = buf.getInt(); + if (len < 0) { + throw new ApkFormatException("Negative length"); + } else if (len > buf.remaining()) { + throw new ApkFormatException( + "Underflow while reading length-prefixed value. Length: " + len + + ", available: " + buf.remaining()); + } + byte[] result = new byte[len]; + buf.get(result); + return result; + } + + public static byte[] encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + List<Pair<Integer, byte[]>> sequence) { + int resultSize = 0; + for (Pair<Integer, byte[]> element : sequence) { + resultSize += 12 + element.getSecond().length; + } + ByteBuffer result = ByteBuffer.allocate(resultSize); + result.order(ByteOrder.LITTLE_ENDIAN); + for (Pair<Integer, byte[]> element : sequence) { + byte[] second = element.getSecond(); + result.putInt(8 + second.length); + result.putInt(element.getFirst()); + result.putInt(second.length); + result.put(second); + } + return result.array(); + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java new file mode 100644 index 0000000..61652a4 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/ApkSupportedSignature.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base implementation of a supported signature for an APK. + */ +public class ApkSupportedSignature { + public final SignatureAlgorithm algorithm; + public final byte[] signature; + + /** + * Constructs a new supported signature using the provided {@code algorithm} and {@code + * signature} bytes. + */ + public ApkSupportedSignature(SignatureAlgorithm algorithm, byte[] signature) { + this.algorithm = algorithm; + this.signature = signature; + } + +} diff --git a/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java new file mode 100644 index 0000000..52c6085 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/NoApkSupportedSignaturesException.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base exception that is thrown when there are no signatures that support the full range of + * requested platform versions. + */ +public class NoApkSupportedSignaturesException extends Exception { + public NoApkSupportedSignaturesException(String message) { + super(message); + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java index 0db8cb8..d54f1e0 100644 --- a/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java +++ b/src/main/java/com/android/apksig/internal/apk/SignatureAlgorithm.java @@ -38,7 +38,8 @@ public enum SignatureAlgorithm { Pair.of("SHA256withRSA/PSS", new PSSParameterSpec( "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1)), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.M), /** * RSASSA-PSS with SHA2-512 digest, SHA2-512 MGF1, 64 bytes of salt, trailer: 0xbc, content @@ -52,7 +53,8 @@ public enum SignatureAlgorithm { "SHA512withRSA/PSS", new PSSParameterSpec( "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1)), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.M), /** RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ RSA_PKCS1_V1_5_WITH_SHA256( @@ -60,7 +62,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA256, "RSA", Pair.of("SHA256withRSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), /** RSASSA-PKCS1-v1_5 with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ RSA_PKCS1_V1_5_WITH_SHA512( @@ -68,7 +71,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA512, "RSA", Pair.of("SHA512withRSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), /** ECDSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ ECDSA_WITH_SHA256( @@ -76,7 +80,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA256, "EC", Pair.of("SHA256withECDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), /** ECDSA with SHA2-512 digest, content digested using SHA2-512 in 1 MB chunks. */ ECDSA_WITH_SHA512( @@ -84,7 +89,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA512, "EC", Pair.of("SHA512withECDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.HONEYCOMB), /** DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. */ DSA_WITH_SHA256( @@ -92,7 +98,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA256, "DSA", Pair.of("SHA256withDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), /** * RSASSA-PKCS1-v1_5 with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in @@ -104,7 +111,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "RSA", Pair.of("SHA256withRSA", null), - AndroidSdkVersion.P), + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE), /** * ECDSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way @@ -116,7 +124,8 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "EC", Pair.of("SHA256withECDSA", null), - AndroidSdkVersion.P), + AndroidSdkVersion.P, + AndroidSdkVersion.HONEYCOMB), /** * DSA with SHA2-256 digest, content digested using SHA2-256 in 4 KB chunks, in the same way @@ -128,24 +137,28 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.VERITY_CHUNKED_SHA256, "DSA", Pair.of("SHA256withDSA", null), - AndroidSdkVersion.P); + AndroidSdkVersion.P, + AndroidSdkVersion.INITIAL_RELEASE); private final int mId; private final String mJcaKeyAlgorithm; private final ContentDigestAlgorithm mContentDigestAlgorithm; private final Pair<String, ? extends AlgorithmParameterSpec> mJcaSignatureAlgAndParams; private final int mMinSdkVersion; + private final int mJcaSigAlgMinSdkVersion; SignatureAlgorithm(int id, ContentDigestAlgorithm contentDigestAlgorithm, String jcaKeyAlgorithm, Pair<String, ? extends AlgorithmParameterSpec> jcaSignatureAlgAndParams, - int minSdkVersion) { + int minSdkVersion, + int jcaSigAlgMinSdkVersion) { mId = id; mContentDigestAlgorithm = contentDigestAlgorithm; mJcaKeyAlgorithm = jcaKeyAlgorithm; mJcaSignatureAlgAndParams = jcaSignatureAlgAndParams; mMinSdkVersion = minSdkVersion; + mJcaSigAlgMinSdkVersion = jcaSigAlgMinSdkVersion; } /** @@ -181,6 +194,13 @@ public enum SignatureAlgorithm { return mMinSdkVersion; } + /** + * Returns the minimum SDK version that supports the JCA signature algorithm. + */ + public int getJcaSigAlgMinSdkVersion() { + return mJcaSigAlgMinSdkVersion; + } + public static SignatureAlgorithm findById(int id) { for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.getId() == id) { diff --git a/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java new file mode 100644 index 0000000..95f06ef --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/SignatureNotFoundException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +/** + * Base exception that is thrown when the APK is not signed with the requested signature scheme. + */ +public class SignatureNotFoundException extends Exception { + public SignatureNotFoundException(String message) { + super(message); + } + + public SignatureNotFoundException(String message, Throwable cause) { + super(message, cause); + } +}
\ No newline at end of file diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java new file mode 100644 index 0000000..93627ff --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampCertificateLineage.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; + +import com.android.apksig.apk.ApkFormatException; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; +import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +/** Lightweight version of the V3SigningCertificateLineage to be used for source stamps. */ +public class SourceStampCertificateLineage { + + private final static int FIRST_VERSION = 1; + private final static int CURRENT_VERSION = FIRST_VERSION; + + /** + * Deserializes the binary representation of a SourceStampCertificateLineage. Also + * verifies that the structure is well-formed, e.g. that the signature for each node is from its + * parent. + */ + public static List<SigningCertificateNode> readSigningCertificateLineage(ByteBuffer inputBytes) + throws IOException { + List<SigningCertificateNode> result = new ArrayList<>(); + int nodeCount = 0; + if (inputBytes == null || !inputBytes.hasRemaining()) { + return null; + } + + ApkSigningBlockUtilsLite.checkByteOrderLittleEndian(inputBytes); + + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } + + // FORMAT (little endian): + // * uint32: version code + // * sequence of length-prefixed (uint32): nodes + // * length-prefixed bytes: signed data + // * length-prefixed bytes: certificate + // * uint32: signature algorithm id + // * uint32: flags + // * uint32: signature algorithm id (used by to sign next cert in lineage) + // * length-prefixed bytes: signature over above signed data + + X509Certificate lastCert = null; + int lastSigAlgorithmId = 0; + + try { + int version = inputBytes.getInt(); + if (version != CURRENT_VERSION) { + // we only have one version to worry about right now, so just check it + throw new IllegalArgumentException("Encoded SigningCertificateLineage has a version" + + " different than any of which we are aware"); + } + HashSet<X509Certificate> certHistorySet = new HashSet<>(); + while (inputBytes.hasRemaining()) { + nodeCount++; + ByteBuffer nodeBytes = getLengthPrefixedSlice(inputBytes); + ByteBuffer signedData = getLengthPrefixedSlice(nodeBytes); + int flags = nodeBytes.getInt(); + int sigAlgorithmId = nodeBytes.getInt(); + SignatureAlgorithm sigAlgorithm = SignatureAlgorithm.findById(lastSigAlgorithmId); + byte[] signature = readLengthPrefixedByteArray(nodeBytes); + + if (lastCert != null) { + // Use previous level cert to verify current level + String jcaSignatureAlgorithm = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); + AlgorithmParameterSpec jcaSignatureAlgorithmParams = + sigAlgorithm.getJcaSignatureAlgorithmAndParams().getSecond(); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(jcaSignatureAlgorithm); + sig.initVerify(publicKey); + if (jcaSignatureAlgorithmParams != null) { + sig.setParameter(jcaSignatureAlgorithmParams); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + nodeCount + " using " + jcaSignatureAlgorithm + " when verifying" + + " SourceStampCertificateLineage object"); + } + } + + signedData.rewind(); + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithmId != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + nodeBytes + " when verifying SourceStampCertificateLineage object"); + } + lastCert = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(encodedCert)); + lastCert = new GuaranteedEncodedFormX509Certificate(lastCert, encodedCert); + if (certHistorySet.contains(lastCert)) { + throw new SecurityException("Encountered duplicate entries in " + + "SigningCertificateLineage at certificate #" + nodeCount + ". All " + + "signing certificates should be unique"); + } + certHistorySet.add(lastCert); + lastSigAlgorithmId = sigAlgorithmId; + result.add(new SigningCertificateNode( + lastCert, SignatureAlgorithm.findById(signedSigAlgorithm), + SignatureAlgorithm.findById(sigAlgorithmId), signature, flags)); + } + } catch(ApkFormatException | BufferUnderflowException e){ + throw new IOException("Failed to parse SourceStampCertificateLineage object", e); + } catch(NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e){ + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } catch(CertificateException e){ + throw new SecurityException("Failed to decode certificate #" + nodeCount + + " when parsing SourceStampCertificateLineage object", e); + } + return result; + } + + /** + * Represents one signing certificate in the SourceStampCertificateLineage, which + * generally means it is/was used at some point to sign source stamps. + */ + public static class SigningCertificateNode { + + public SigningCertificateNode( + X509Certificate signingCert, + SignatureAlgorithm parentSigAlgorithm, + SignatureAlgorithm sigAlgorithm, + byte[] signature, + int flags) { + this.signingCert = signingCert; + this.parentSigAlgorithm = parentSigAlgorithm; + this.sigAlgorithm = sigAlgorithm; + this.signature = signature; + this.flags = flags; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SigningCertificateNode)) return false; + + SigningCertificateNode that = (SigningCertificateNode) o; + if (!signingCert.equals(that.signingCert)) return false; + if (parentSigAlgorithm != that.parentSigAlgorithm) return false; + if (sigAlgorithm != that.sigAlgorithm) return false; + if (!Arrays.equals(signature, that.signature)) return false; + if (flags != that.flags) return false; + + // we made it + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((signingCert == null) ? 0 : signingCert.hashCode()); + result = prime * result + + ((parentSigAlgorithm == null) ? 0 : parentSigAlgorithm.hashCode()); + result = prime * result + ((sigAlgorithm == null) ? 0 : sigAlgorithm.hashCode()); + result = prime * result + Arrays.hashCode(signature); + result = prime * result + flags; + return result; + } + + /** + * the signing cert for this node. This is part of the data signed by the parent node. + */ + public final X509Certificate signingCert; + + /** + * the algorithm used by this node's parent to bless this data. Its ID value is part of + * the data signed by the parent node. {@code null} for first node. + */ + public final SignatureAlgorithm parentSigAlgorithm; + + /** + * the algorithm used by this node to bless the next node's data. Its ID value is part + * of the signed data of the next node. {@code null} for the last node. + */ + public SignatureAlgorithm sigAlgorithm; + + /** + * signature over the signed data (above). The signature is from this node's parent + * signing certificate, which should correspond to the signing certificate used to sign an + * APK before rotating to this one, and is formed using {@code signatureAlgorithm}. + */ + public final byte[] signature; + + /** + * the flags detailing how the platform should treat this signing cert + */ + public int flags; + } +} diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java new file mode 100644 index 0000000..465fbb0 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.stamp; + +/** Constants used for source stamp signing and verification. */ +public class SourceStampConstants { + private SourceStampConstants() {} + + public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + public static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; +} diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java index 2f4c3ba..b4ae71a 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampVerifier.java @@ -15,15 +15,25 @@ */ package com.android.apksig.internal.apk.stamp; -import com.android.apksig.ApkVerifier; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getLengthPrefixedSlice; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.getSignaturesToVerify; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.readLengthPrefixedByteArray; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex; + +import com.android.apksig.ApkVerificationIssue; import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSupportedSignature; +import com.android.apksig.internal.apk.NoApkSupportedSignaturesException; import com.android.apksig.internal.apk.SignatureAlgorithm; +import com.android.apksig.internal.apk.v3.V3SigningCertificateLineage; +import com.android.apksig.internal.util.ByteBufferUtils; import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; -import com.android.apksig.internal.util.X509CertificateUtils; +import java.io.ByteArrayInputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; @@ -53,7 +63,8 @@ import java.util.Map; */ class SourceStampVerifier { /** Hidden constructor to prevent instantiation. */ - private SourceStampVerifier() {} + private SourceStampVerifier() { + } /** * Parses the SourceStamp block and populates the {@code result}. @@ -67,7 +78,7 @@ class SourceStampVerifier { public static void verifyV1SourceStamp( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, + ApkSignerInfo result, byte[] apkDigest, byte[] sourceStampCertificateDigest, int minSdkVersion, @@ -80,12 +91,13 @@ class SourceStampVerifier { return; } + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData); verifySourceStampSignature( apkDigest, minSdkVersion, maxSdkVersion, sourceStampCertificate, - sourceStampBlockData, + apkDigestSignatures, result); } @@ -101,7 +113,7 @@ class SourceStampVerifier { public static void verifyV2SourceStamp( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, + ApkSignerInfo result, Map<Integer, byte[]> signatureSchemeApkDigests, byte[] sourceStampCertificateDigest, int minSdkVersion, @@ -115,20 +127,19 @@ class SourceStampVerifier { } // Parse signed signature schemes block. - ByteBuffer signedSignatureSchemes = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData); Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>(); while (signedSignatureSchemes.hasRemaining()) { - ByteBuffer signedSignatureScheme = - ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); + ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes); int signatureSchemeId = signedSignatureScheme.getInt(); - signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme); + signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures); } for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest : signatureSchemeApkDigests.entrySet()) { if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); return; } verifySourceStampSignature( @@ -138,9 +149,26 @@ class SourceStampVerifier { sourceStampCertificate, signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()), result); - if (result.containsWarnings() || result.containsWarnings()) { + if (result.containsWarnings() || result.containsErrors()) { + return; + } + } + + if (sourceStampBlockData.hasRemaining()) { + // The stamp block contains some additional attributes. + ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData); + + byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()]; + stampAttributeData.get(stampAttributeBytes); + stampAttributeData.flip(); + + verifySourceStampSignature(stampAttributeBytes, minSdkVersion, maxSdkVersion, + sourceStampCertificate, stampAttributeDataSignatures, result); + if (result.containsErrors() || result.containsWarnings()) { return; } + parseStampAttributes(stampAttributeData, sourceStampCertificate, result); } } @@ -148,18 +176,16 @@ class SourceStampVerifier { ByteBuffer sourceStampBlockData, CertificateFactory certFactory, byte[] sourceStampCertificateDigest, - ApkSigningBlockUtils.Result.SignerInfo result) + ApkSignerInfo result) throws NoSuchAlgorithmException, ApkFormatException { // Parse the SourceStamp certificate. - byte[] sourceStampEncodedCertificate = - ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); + byte[] sourceStampEncodedCertificate = readLengthPrefixedByteArray(sourceStampBlockData); X509Certificate sourceStampCertificate; try { - sourceStampCertificate = - X509CertificateUtils.generateCertificate( - sourceStampEncodedCertificate, certFactory); + sourceStampCertificate = (X509Certificate) certFactory.generateCertificate( + new ByteArrayInputStream(sourceStampEncodedCertificate)); } catch (CertificateException e) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); return null; } // Wrap the cert so that the result's getEncoded returns exactly the original encoded @@ -177,62 +203,71 @@ class SourceStampVerifier { byte[] sourceStampBlockCertificateDigest = messageDigest.digest(); if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { result.addWarning( - ApkVerifier.Issue + ApkVerificationIssue .SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK, - ApkSigningBlockUtils.toHex(sourceStampBlockCertificateDigest), - ApkSigningBlockUtils.toHex(sourceStampCertificateDigest)); + toHex(sourceStampBlockCertificateDigest), + toHex(sourceStampCertificateDigest)); return null; } return sourceStampCertificate; } private static void verifySourceStampSignature( - byte[] apkDigest, + byte[] data, int minSdkVersion, int maxSdkVersion, X509Certificate sourceStampCertificate, - ByteBuffer signedData, - ApkSigningBlockUtils.Result.SignerInfo result) - throws ApkFormatException { + ByteBuffer signatures, + ApkSignerInfo result) { // Parse the signatures block and identify supported signatures - ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedData); int signatureCount = 0; - List<ApkSigningBlockUtils.SupportedSignature> supportedSignatures = new ArrayList<>(1); + List<ApkSupportedSignature> supportedSignatures = new ArrayList<>(1); while (signatures.hasRemaining()) { signatureCount++; try { - ByteBuffer signature = ApkSigningBlockUtils.getLengthPrefixedSlice(signatures); + ByteBuffer signature = getLengthPrefixedSlice(signatures); int sigAlgorithmId = signature.getInt(); - byte[] sigBytes = ApkSigningBlockUtils.readLengthPrefixedByteArray(signature); + byte[] sigBytes = readLengthPrefixedByteArray(signature); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); if (signatureAlgorithm == null) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, + sigAlgorithmId); continue; } supportedSignatures.add( - new ApkSigningBlockUtils.SupportedSignature(signatureAlgorithm, sigBytes)); + new ApkSupportedSignature(signatureAlgorithm, sigBytes)); } catch (ApkFormatException | BufferUnderflowException e) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); + ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); return; } } if (supportedSignatures.isEmpty()) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE); + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); return; } // Verify signatures over digests using the SourceStamp's certificate. - List<ApkSigningBlockUtils.SupportedSignature> signaturesToVerify; + List<ApkSupportedSignature> signaturesToVerify; try { signaturesToVerify = - ApkSigningBlockUtils.getSignaturesToVerify( - supportedSignatures, minSdkVersion, maxSdkVersion); - } catch (ApkSigningBlockUtils.NoSupportedSignaturesException e) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE); + getSignaturesToVerify( + supportedSignatures, minSdkVersion, maxSdkVersion, true); + } catch (NoApkSupportedSignaturesException e) { + // To facilitate debugging capture the signature algorithms and resulting exception in + // the warning. + StringBuilder signatureAlgorithms = new StringBuilder(); + for (ApkSupportedSignature supportedSignature : supportedSignatures) { + if (signatureAlgorithms.length() > 0) { + signatureAlgorithms.append(", "); + } + signatureAlgorithms.append(supportedSignature.algorithm); + } + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SUPPORTED_SIGNATURE, + signatureAlgorithms.toString(), e); return; } - for (ApkSigningBlockUtils.SupportedSignature signature : signaturesToVerify) { + for (ApkSupportedSignature signature : signaturesToVerify) { SignatureAlgorithm signatureAlgorithm = signature.algorithm; String jcaSignatureAlgorithm = signatureAlgorithm.getJcaSignatureAlgorithmAndParams().getFirst(); @@ -245,11 +280,11 @@ class SourceStampVerifier { if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } - sig.update(apkDigest); + sig.update(data); byte[] sigBytes = signature.signature; if (!sig.verify(sigBytes)) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm); + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY, signatureAlgorithm); return; } } catch (InvalidKeyException @@ -257,9 +292,57 @@ class SourceStampVerifier { | SignatureException | NoSuchAlgorithmException e) { result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e); + ApkVerificationIssue.SOURCE_STAMP_VERIFY_EXCEPTION, signatureAlgorithm, e); return; } } } + + private static void parseStampAttributes(ByteBuffer stampAttributeData, + X509Certificate sourceStampCertificate, ApkSignerInfo result) + throws ApkFormatException { + ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); + int stampAttributeCount = 0; + while (stampAttributes.hasRemaining()) { + stampAttributeCount++; + try { + ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); + int id = attribute.getInt(); + byte[] value = ByteBufferUtils.toByteArray(attribute); + if (id == SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID) { + readStampCertificateLineage(value, sourceStampCertificate, result); + } else { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE, id); + } + } catch (ApkFormatException | BufferUnderflowException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE, + stampAttributeCount); + return; + } + } + } + + private static void readStampCertificateLineage(byte[] lineageBytes, + X509Certificate sourceStampCertificate, ApkSignerInfo result) { + try { + // SourceStampCertificateLineage is verified when built + List<SourceStampCertificateLineage.SigningCertificateNode> nodes = + SourceStampCertificateLineage.readSigningCertificateLineage( + ByteBuffer.wrap(lineageBytes).order(ByteOrder.LITTLE_ENDIAN)); + for (int i = 0; i < nodes.size(); i++) { + result.certificateLineage.add(nodes.get(i).signingCert); + } + // Make sure that the last cert in the chain matches this signer cert + if (!sourceStampCertificate.equals( + result.certificateLineage.get(result.certificateLineage.size() - 1))) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } + } catch (SecurityException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_DID_NOT_VERIFY); + } catch (IllegalArgumentException e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } catch (Exception e) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_LINEAGE); + } + } } diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java index dacd0be..dee24bd 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java @@ -48,8 +48,8 @@ import java.util.Map; * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only. */ public abstract class V1SourceStampSigner { - - public static final int V1_SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; /** Hidden constructor to prevent instantiation. */ private V1SourceStampSigner() {} @@ -98,8 +98,8 @@ public abstract class V1SourceStampSigner { // FORMAT: // * length-prefixed stamp block. - return Pair.of( - encodeAsLengthPrefixedElement(sourceStampSignerBlock), V1_SOURCE_STAMP_BLOCK_ID); + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID); } private static final class SourceStampBlock { diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java index 8a3e776..c3fdeec 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java @@ -16,7 +16,7 @@ package com.android.apksig.internal.apk.stamp; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import static com.android.apksig.internal.apk.stamp.V1SourceStampSigner.V1_SOURCE_STAMP_BLOCK_ID; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; import com.android.apksig.ApkVerifier; import com.android.apksig.apk.ApkFormatException; diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java index 16062bf..1c1570a 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java @@ -23,17 +23,22 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsLengt import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedElements; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import com.android.apksig.SigningCertificateLineage; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.util.Pair; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,11 +55,12 @@ import java.util.Map; * <p>V2 of the source stamp allows signing the digests of more than one signature schemes. */ public abstract class V2SourceStampSigner { - - public static final int V2_SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; /** Hidden constructor to prevent instantiation. */ - private V2SourceStampSigner() {} + private V2SourceStampSigner() { + } public static Pair<byte[], Integer> generateSourceStampBlock( SignerConfig sourceStampSignerConfig, @@ -81,7 +87,7 @@ public abstract class V2SourceStampSigner { signatureSchemeDigestInfos, sourceStampSignerConfig, signatureSchemeDigests); - signatureSchemeDigests.sort(Comparator.comparing(Pair::getFirst)); + Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst)); SourceStampBlock sourceStampBlock = new SourceStampBlock(); @@ -95,23 +101,36 @@ public abstract class V2SourceStampSigner { sourceStampBlock.signedDigests = signatureSchemeDigests; + sourceStampBlock.stampAttributes = encodeStampAttributes( + generateStampAttributes(sourceStampSignerConfig.mSigningCertificateLineage)); + sourceStampBlock.signedStampAttributes = + ApkSigningBlockUtils.generateSignaturesOverData(sourceStampSignerConfig, + sourceStampBlock.stampAttributes); + // FORMAT: // * length-prefixed bytes: X.509 certificate (ASN.1 DER encoded) // * length-prefixed sequence of length-prefixed signed signature scheme digests: // * uint32: signature scheme id // * length-prefixed bytes: signed digests for the respective signature scheme + // * length-prefixed bytes: encoded stamp attributes + // * length-prefixed sequence of length-prefixed signed stamp attributes: + // * uint32: signature algorithm id + // * length-prefixed bytes: signed stamp attributes for the respective signature algorithm byte[] sourceStampSignerBlock = encodeAsSequenceOfLengthPrefixedElements( - new byte[][] { - sourceStampBlock.stampCertificate, - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( - sourceStampBlock.signedDigests), + new byte[][]{ + sourceStampBlock.stampCertificate, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedDigests), + sourceStampBlock.stampAttributes, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedStampAttributes), }); // FORMAT: // * length-prefixed stamp block. - return Pair.of( - encodeAsLengthPrefixedElement(sourceStampSignerBlock), V2_SOURCE_STAMP_BLOCK_ID); + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); } private static void getSignedDigestsFor( @@ -130,7 +149,7 @@ public abstract class V2SourceStampSigner { for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) { digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); } - digests.sort(Comparator.comparing(Pair::getFirst)); + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); // FORMAT: // * length-prefixed sequence of length-prefixed digests: @@ -158,8 +177,43 @@ public abstract class V2SourceStampSigner { signedDigest))); } + private static byte[] encodeStampAttributes(Map<Integer, byte[]> stampAttributes) { + int payloadSize = 0; + for (byte[] attributeValue : stampAttributes.values()) { + // Pair size + Attribute ID + Attribute value + payloadSize += 4 + 4 + attributeValue.length; + } + + // FORMAT (little endian): + // * length-prefixed bytes: pair + // * uint32: ID + // * bytes: value + ByteBuffer result = ByteBuffer.allocate(4 + payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize); + for (Map.Entry<Integer, byte[]> stampAttribute : stampAttributes.entrySet()) { + // Pair size + result.putInt(4 + stampAttribute.getValue().length); + result.putInt(stampAttribute.getKey()); + result.put(stampAttribute.getValue()); + } + return result.array(); + } + + private static Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) { + HashMap<Integer, byte[]> stampAttributes = new HashMap<>(); + if (lineage != null) { + stampAttributes.put(SourceStampConstants.PROOF_OF_ROTATION_ATTR_ID, + lineage.encodeSigningCertificateLineage()); + } + return stampAttributes; + } + private static final class SourceStampBlock { public byte[] stampCertificate; public List<Pair<Integer, byte[]>> signedDigests; + // Optional stamp attributes that are not required for verification. + public byte[] stampAttributes; + public List<Pair<Integer, byte[]>> signedStampAttributes; } } diff --git a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java index 8a776fc..5ba3618 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java @@ -16,17 +16,21 @@ package com.android.apksig.internal.apk.stamp; -import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import static com.android.apksig.internal.apk.stamp.V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; -import com.android.apksig.ApkVerifier; +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.Constants; import com.android.apksig.apk.ApkFormatException; -import com.android.apksig.apk.ApkUtils; -import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigResult; +import com.android.apksig.internal.apk.ApkSignerInfo; +import com.android.apksig.internal.apk.ApkSigningBlockUtilsLite; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.apk.SignatureNotFoundException; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipSections; import java.io.IOException; import java.nio.BufferUnderflowException; @@ -53,30 +57,29 @@ public abstract class V2SourceStampVerifier { /** * Verifies the provided APK's SourceStamp signatures and returns the result of verification. - * The APK must be considered verified only if {@link ApkSigningBlockUtils.Result#verified} is + * The APK must be considered verified only if {@link ApkSigResult#verified} is * {@code true}. If verification fails, the result will contain errors -- see {@link - * ApkSigningBlockUtils.Result#getErrors()}. + * ApkSigResult#getErrors()}. * * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing - * @throws ApkSigningBlockUtils.SignatureNotFoundException if no SourceStamp signatures are + * @throws SignatureNotFoundException if no SourceStamp signatures are * found * @throws IOException if an I/O error occurs when reading the APK */ - public static ApkSigningBlockUtils.Result verify( + public static ApkSigResult verify( DataSource apk, - ApkUtils.ZipSections zipSections, + ZipSections zipSections, byte[] sourceStampCertificateDigest, Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, int minSdkVersion, int maxSdkVersion) - throws IOException, NoSuchAlgorithmException, - ApkSigningBlockUtils.SignatureNotFoundException { - ApkSigningBlockUtils.Result result = - new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + ApkSigResult result = + new ApkSigResult(Constants.VERSION_SOURCE_STAMP); SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature( - apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID, result); + ApkSigningBlockUtilsLite.findSignature( + apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID); verify( signatureInfo.signatureBlock, @@ -91,7 +94,7 @@ public abstract class V2SourceStampVerifier { /** * Verifies the provided APK's SourceStamp signatures and outputs the results into the provided * {@code result}. APK is considered verified only if there are no errors reported in the {@code - * result}. See {@link #verify(DataSource, ApkUtils.ZipSections, byte[], Map, int, int)} for + * result}. See {@link #verify(DataSource, ZipSections, byte[], Map, int, int)} for * more information about the contract of this method. */ private static void verify( @@ -100,15 +103,14 @@ public abstract class V2SourceStampVerifier { Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, int minSdkVersion, int maxSdkVersion, - ApkSigningBlockUtils.Result result) + ApkSigResult result) throws NoSuchAlgorithmException { - ApkSigningBlockUtils.Result.SignerInfo signerInfo = - new ApkSigningBlockUtils.Result.SignerInfo(); - result.signers.add(signerInfo); + ApkSignerInfo signerInfo = new ApkSignerInfo(); + result.mSigners.add(signerInfo); try { CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); ByteBuffer sourceStampBlockData = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock); SourceStampVerifier.verifyV2SourceStamp( sourceStampBlockData, certFactory, @@ -121,7 +123,7 @@ public abstract class V2SourceStampVerifier { } catch (CertificateException e) { throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); } catch (ApkFormatException | BufferUnderflowException e) { - signerInfo.addWarning(ApkVerifier.Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE); } } diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java new file mode 100644 index 0000000..db1d15f --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v1; + +/** Constants used by the Jar Signing / V1 Signature Scheme signing and verification. */ +public class V1SchemeConstants { + private V1SchemeConstants() {} + + public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + public static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = + "X-Android-APK-Signed"; +} diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java index 89f16d5..6e9e0c3 100644 --- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeSigner.java @@ -59,17 +59,15 @@ import java.util.jar.Manifest; * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a> */ public abstract class V1SchemeSigner { - - public static final String MANIFEST_ENTRY_NAME = "META-INF/MANIFEST.MF"; + public static final String MANIFEST_ENTRY_NAME = V1SchemeConstants.MANIFEST_ENTRY_NAME; private static final Attributes.Name ATTRIBUTE_NAME_CREATED_BY = new Attributes.Name("Created-By"); private static final String ATTRIBUTE_VALUE_MANIFEST_VERSION = "1.0"; private static final String ATTRIBUTE_VALUE_SIGNATURE_VERSION = "1.0"; - static final String SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR = "X-Android-APK-Signed"; private static final Attributes.Name SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME = - new Attributes.Name(SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + new Attributes.Name(V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); /** * Signer configuration. @@ -303,7 +301,7 @@ public abstract class V1SchemeSigner { signatureJarEntries.add( Pair.of(signatureBlockFileName, signatureBlock)); } - signatureJarEntries.add(Pair.of(MANIFEST_ENTRY_NAME, manifest.contents)); + signatureJarEntries.add(Pair.of(V1SchemeConstants.MANIFEST_ENTRY_NAME, manifest.contents)); return signatureJarEntries; } @@ -321,7 +319,7 @@ public abstract class V1SchemeSigner { + publicKey.getAlgorithm().toUpperCase(Locale.US); result.add(signatureBlockFileName); } - result.add(MANIFEST_ENTRY_NAME); + result.add(V1SchemeConstants.MANIFEST_ENTRY_NAME); return result; } diff --git a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java index 111ac71..6d7e997 100644 --- a/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/v1/V1SchemeVerifier.java @@ -46,13 +46,13 @@ import com.android.apksig.internal.util.InclusiveIntRange; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; +import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSinks; import com.android.apksig.util.DataSource; import com.android.apksig.zip.ZipFormatException; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -82,9 +82,6 @@ import java.util.jar.Attributes; * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html#Signed_JAR_File">Signed JAR File</a> */ public abstract class V1SchemeVerifier { - - private static final String MANIFEST_ENTRY_NAME = V1SchemeSigner.MANIFEST_ENTRY_NAME; - private V1SchemeVerifier() {} /** @@ -231,7 +228,8 @@ public abstract class V1SchemeVerifier { if (!entryName.startsWith("META-INF/")) { continue; } - if ((manifestEntry == null) && (MANIFEST_ENTRY_NAME.equals(entryName))) { + if ((manifestEntry == null) && (V1SchemeConstants.MANIFEST_ENTRY_NAME.equals( + entryName))) { manifestEntry = cdRecord; continue; } @@ -939,7 +937,7 @@ public abstract class V1SchemeVerifier { if (!Arrays.equals(expected, actual)) { mResult.addWarning( Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, - V1SchemeSigner.MANIFEST_ENTRY_NAME, + V1SchemeConstants.MANIFEST_ENTRY_NAME, jcaDigestAlgorithm, mSignatureFileEntry.getName(), Base64.getEncoder().encodeToString(actual), @@ -1049,7 +1047,7 @@ public abstract class V1SchemeVerifier { Set<Integer> foundApkSigSchemeIds) { String signedWithApkSchemes = sfMainSection.getAttributeValue( - V1SchemeSigner.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); + V1SchemeConstants.SF_ATTRIBUTE_NAME_ANDROID_APK_SIGNED_NAME_STR); // This field contains a comma-separated list of APK signature scheme IDs which were // used to sign this APK. Android rejects APKs where an ID is known to the platform but // the APK didn't verify using that scheme. @@ -1239,40 +1237,7 @@ public abstract class V1SchemeVerifier { DataSource apk, ApkUtils.ZipSections apkSections) throws IOException, ApkFormatException { - // Read the ZIP Central Directory - long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); - if (cdSizeBytes > Integer.MAX_VALUE) { - throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); - } - long cdOffset = apkSections.getZipCentralDirectoryOffset(); - ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); - cd.order(ByteOrder.LITTLE_ENDIAN); - - // Parse the ZIP Central Directory - int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); - List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); - for (int i = 0; i < expectedCdRecordCount; i++) { - CentralDirectoryRecord cdRecord; - int offsetInsideCd = cd.position(); - try { - cdRecord = CentralDirectoryRecord.getRecord(cd); - } catch (ZipFormatException e) { - throw new ApkFormatException( - "Malformed ZIP Central Directory record #" + (i + 1) - + " at file offset " + (cdOffset + offsetInsideCd), - e); - } - String entryName = cdRecord.getName(); - if (entryName.endsWith("/")) { - // Ignore directory entries - continue; - } - cdRecords.add(cdRecord); - } - // There may be more data in Central Directory, but we don't warn or throw because Android - // ignores unused CD data. - - return cdRecords; + return ZipUtils.parseZipCentralDirectory(apk, apkSections); } /** @@ -1376,7 +1341,7 @@ public abstract class V1SchemeVerifier { Issue.JAR_SIG_ZIP_ENTRY_DIGEST_DID_NOT_VERIFY, entryName, expectedDigest.jcaDigestAlgorithm, - V1SchemeSigner.MANIFEST_ENTRY_NAME, + V1SchemeConstants.MANIFEST_ENTRY_NAME, Base64.getEncoder().encodeToString(actualDigest), Base64.getEncoder().encodeToString(expectedDigest.digest)); } diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java new file mode 100644 index 0000000..0e244c8 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v2; + +/** Constants used by the V2 Signature Scheme signing and verification. */ +public class V2SchemeConstants { + private V2SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + public static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; +} diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java index e812c3f..c870a9e 100644 --- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeSigner.java @@ -70,7 +70,8 @@ public abstract class V2SchemeSigner { * protected by signatures inside the block. */ - public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; + public static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; /** Hidden constructor to prevent instantiation. */ private V2SchemeSigner() {} @@ -183,7 +184,7 @@ public abstract class V2SchemeSigner { new byte[][] { encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }), - APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID); } private static byte[] generateSignerBlock( @@ -263,9 +264,6 @@ public abstract class V2SchemeSigner { }); } - // Attribute to check whether a newer APK Signature Scheme signature was stripped - protected static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d; - private static byte[] generateAdditionalAttributes(boolean v3SigningEnabled) { if (v3SigningEnabled) { // FORMAT (little endian): @@ -276,7 +274,7 @@ public abstract class V2SchemeSigner { ByteBuffer result = ByteBuffer.allocate(payloadSize); result.order(ByteOrder.LITTLE_ENDIAN); result.putInt(payloadSize - 4); - result.putInt(STRIPPING_PROTECTION_ATTR_ID); + result.putInt(V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID); result.putInt(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); return result.array(); } else { diff --git a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java index 9b821a7..f367908 100644 --- a/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/v2/V2SchemeVerifier.java @@ -61,9 +61,6 @@ import java.util.Set; * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> */ public abstract class V2SchemeVerifier { - - private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; - /** Hidden constructor to prevent instantiation. */ private V2SchemeVerifier() {} @@ -101,7 +98,7 @@ public abstract class V2SchemeVerifier { ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result); + V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID , result); DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); DataSource centralDir = @@ -247,8 +244,7 @@ public abstract class V2SchemeVerifier { Map<Integer, String> supportedApkSigSchemeNames, Set<Integer> foundApkSigSchemeIds, int minSdkVersion, - int maxSdkVersion) - throws ApkFormatException, NoSuchAlgorithmException { + int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException { ByteBuffer signedData = ApkSigningBlockUtils.getLengthPrefixedSlice(signerBlock); byte[] signedDataBytes = new byte[signedData.remaining()]; signedData.get(signedDataBytes); @@ -435,7 +431,7 @@ public abstract class V2SchemeVerifier { result.additionalAttributes.add( new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); switch (id) { - case V2SchemeSigner.STRIPPING_PROTECTION_ATTR_ID: + case V2SchemeConstants.STRIPPING_PROTECTION_ATTR_ID: // stripping protection added when signing with a newer scheme int foundId = ByteBuffer.wrap(value).order( ByteOrder.LITTLE_ENDIAN).getInt(); diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java new file mode 100644 index 0000000..3b70aa0 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk.v3; + +/** Constants used by the V3 Signature Scheme signing and verification. */ +public class V3SchemeConstants { + private V3SchemeConstants() {} + + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; +} diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java index 56ab60e..cab2a47 100644 --- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeSigner.java @@ -57,8 +57,9 @@ import java.util.Map; * it can prove the new siging certificate was signed by the old. */ public abstract class V3SchemeSigner { - - public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + public static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + public static final int PROOF_OF_ROTATION_ATTR_ID = V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID; /** Hidden constructor to prevent instantiation. */ private V3SchemeSigner() {} @@ -141,6 +142,22 @@ public abstract class V3SchemeSigner { digestInfo.getSecond()); } + public static byte[] generateV3SignerAttribute( + SigningCertificateLineage signingCertificateLineage) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - encoded V3 SigningCertificateLineage + byte[] encodedLineage = signingCertificateLineage.encodeSigningCertificateLineage(); + int payloadSize = 4 + 4 + encodedLineage.length; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(4 + encodedLineage.length); + result.putInt(V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID); + result.put(encodedLineage); + return result.array(); + } + private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block( List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { @@ -166,7 +183,7 @@ public abstract class V3SchemeSigner { new byte[][] { encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }), - APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); } private static byte[] generateSignerBlock( @@ -284,13 +301,11 @@ public abstract class V3SchemeSigner { return result.array(); } - public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; - private static byte[] generateAdditionalAttributes(SignerConfig signerConfig) { if (signerConfig.mSigningCertificateLineage == null) { return new byte[0]; } - return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute(); + return generateV3SignerAttribute(signerConfig.mSigningCertificateLineage); } private static final class V3SignatureSchemeBlock { diff --git a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java index 659d379..ea93194 100644 --- a/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeVerifier.java @@ -30,10 +30,11 @@ import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.apk.SignatureInfo; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.ByteBufferUtils; -import com.android.apksig.internal.util.X509CertificateUtils; import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; +import com.android.apksig.internal.util.X509CertificateUtils; import com.android.apksig.util.DataSource; import com.android.apksig.util.RunnablesExecutor; + import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; @@ -53,7 +54,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -68,9 +68,6 @@ import java.util.TreeMap; * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a> */ public abstract class V3SchemeVerifier { - - private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; - /** Hidden constructor to prevent instantiation. */ private V3SchemeVerifier() {} @@ -105,7 +102,7 @@ public abstract class V3SchemeVerifier { ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); DataSource centralDir = @@ -494,7 +491,7 @@ public abstract class V3SchemeVerifier { byte[] value = ByteBufferUtils.toByteArray(attribute); result.additionalAttributes.add( new ApkSigningBlockUtils.Result.SignerInfo.AdditionalAttribute(id, value)); - if (id == V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID) { + if (id == V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID) { try { // SigningCertificateLineage is verified when built result.signingCertificateLineage = diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java index 73ba46f..1a1ad93 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeSigner.java @@ -17,10 +17,9 @@ package com.android.apksig.internal.apk.v4; import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodeCertificates; -import static com.android.apksig.internal.apk.v2.V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; -import static com.android.apksig.internal.apk.v3.V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; +import static com.android.apksig.internal.apk.v2.V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; -import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; @@ -324,8 +323,9 @@ public abstract class V4SchemeSigner { return 1; case CHUNKED_SHA512: return 2; + default: + return -1; } - return -1; } private static boolean isSupported(final ContentDigestAlgorithm contentDigestAlgorithm, diff --git a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java index 4ef67c7..87eae48 100644 --- a/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java +++ b/src/main/java/com/android/apksig/internal/util/AndroidSdkVersion.java @@ -24,9 +24,15 @@ public abstract class AndroidSdkVersion { /** Hidden constructor to prevent instantiation. */ private AndroidSdkVersion() {} + /** Android 1.0 */ + public static final int INITIAL_RELEASE = 1; + /** Android 2.3. */ public static final int GINGERBREAD = 9; + /** Android 3.0 */ + public static final int HONEYCOMB = 11; + /** Android 4.3. The revenge of the beans. */ public static final int JELLY_BEAN_MR2 = 18; diff --git a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java index 272015a..9d9da15 100644 --- a/src/main/java/com/android/apksig/internal/zip/ZipUtils.java +++ b/src/main/java/com/android/apksig/internal/zip/ZipUtils.java @@ -16,12 +16,18 @@ package com.android.apksig.internal.zip; +import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; +import com.android.apksig.zip.ZipFormatException; +import com.android.apksig.zip.ZipSections; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; import java.util.zip.CRC32; import java.util.zip.Deflater; @@ -247,6 +253,46 @@ public abstract class ZipUtils { return buffer.getShort() & 0xffff; } + public static List<CentralDirectoryRecord> parseZipCentralDirectory( + DataSource apk, + ZipSections apkSections) + throws IOException, ApkFormatException { + // Read the ZIP Central Directory + long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes(); + if (cdSizeBytes > Integer.MAX_VALUE) { + throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes); + } + long cdOffset = apkSections.getZipCentralDirectoryOffset(); + ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes); + cd.order(ByteOrder.LITTLE_ENDIAN); + + // Parse the ZIP Central Directory + int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount(); + List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount); + for (int i = 0; i < expectedCdRecordCount; i++) { + CentralDirectoryRecord cdRecord; + int offsetInsideCd = cd.position(); + try { + cdRecord = CentralDirectoryRecord.getRecord(cd); + } catch (ZipFormatException e) { + throw new ApkFormatException( + "Malformed ZIP Central Directory record #" + (i + 1) + + " at file offset " + (cdOffset + offsetInsideCd), + e); + } + String entryName = cdRecord.getName(); + if (entryName.endsWith("/")) { + // Ignore directory entries + continue; + } + cdRecords.add(cdRecord); + } + // There may be more data in Central Directory, but we don't warn or throw because Android + // ignores unused CD data. + + return cdRecords; + } + static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) { if ((value < 0) || (value > 0xffff)) { throw new IllegalArgumentException("uint16 value of out range: " + value); diff --git a/src/main/java/com/android/apksig/zip/ZipSections.java b/src/main/java/com/android/apksig/zip/ZipSections.java new file mode 100644 index 0000000..17bce05 --- /dev/null +++ b/src/main/java/com/android/apksig/zip/ZipSections.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.zip; + +import java.nio.ByteBuffer; + +/** + * Base representation of an APK's zip sections containing the central directory's offset, the size + * of the central directory in bytes, the number of records in the central directory, the offset + * of the end of central directory, and a ByteBuffer containing the end of central directory + * contents. + */ +public class ZipSections { + private final long mCentralDirectoryOffset; + private final long mCentralDirectorySizeBytes; + private final int mCentralDirectoryRecordCount; + private final long mEocdOffset; + private final ByteBuffer mEocd; + + public ZipSections( + long centralDirectoryOffset, + long centralDirectorySizeBytes, + int centralDirectoryRecordCount, + long eocdOffset, + ByteBuffer eocd) { + mCentralDirectoryOffset = centralDirectoryOffset; + mCentralDirectorySizeBytes = centralDirectorySizeBytes; + mCentralDirectoryRecordCount = centralDirectoryRecordCount; + mEocdOffset = eocdOffset; + mEocd = eocd; + } + + /** + * Returns the start offset of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectoryOffset() { + return mCentralDirectoryOffset; + } + + /** + * Returns the size (in bytes) of the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public long getZipCentralDirectorySizeBytes() { + return mCentralDirectorySizeBytes; + } + + /** + * Returns the number of records in the ZIP Central Directory. This value is taken from the + * ZIP End of Central Directory record. + */ + public int getZipCentralDirectoryRecordCount() { + return mCentralDirectoryRecordCount; + } + + /** + * Returns the start offset of the ZIP End of Central Directory record. The record extends + * until the very end of the APK. + */ + public long getZipEndOfCentralDirectoryOffset() { + return mEocdOffset; + } + + /** + * Returns the contents of the ZIP End of Central Directory. + */ + public ByteBuffer getZipEndOfCentralDirectory() { + return mEocd; + } +}
\ No newline at end of file diff --git a/src/test/java/com/android/apksig/AllTests.java b/src/test/java/com/android/apksig/AllTests.java index 4a9243d..3cb1052 100644 --- a/src/test/java/com/android/apksig/AllTests.java +++ b/src/test/java/com/android/apksig/AllTests.java @@ -24,6 +24,7 @@ import org.junit.runners.Suite; ApkSignerTest.class, ApkVerifierTest.class, SigningCertificateLineageTest.class, + SourceStampVerifierTest.class, com.android.apksig.apk.AllTests.class, com.android.apksig.internal.AllTests.class, com.android.apksig.util.AllTests.class, diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java index 560202c..40255a4 100644 --- a/src/test/java/com/android/apksig/ApkSignerTest.java +++ b/src/test/java/com/android/apksig/ApkSignerTest.java @@ -31,10 +31,10 @@ import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.apk.SignatureInfo; -import com.android.apksig.internal.apk.stamp.V2SourceStampSigner; +import com.android.apksig.internal.apk.stamp.SourceStampConstants; import com.android.apksig.internal.apk.v1.V1SchemeVerifier; -import com.android.apksig.internal.apk.v2.V2SchemeSigner; -import com.android.apksig.internal.apk.v3.V3SchemeSigner; +import com.android.apksig.internal.apk.v2.V2SchemeConstants; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.asn1.Asn1BerParser; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.Resources; @@ -42,23 +42,23 @@ import com.android.apksig.internal.x509.RSAPublicKey; import com.android.apksig.internal.x509.SubjectPublicKeyInfo; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.LocalFileRecord; -import com.android.apksig.util.DataSinks; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; -import com.android.apksig.util.ReadableDataSink; import com.android.apksig.zip.ZipFormatException; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.RandomAccessFile; import java.math.BigInteger; import java.nio.ByteBuffer; -import java.nio.channels.ByteChannel; import java.nio.file.Files; -import java.nio.file.StandardOpenOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; @@ -90,6 +90,9 @@ public class ApkSignerTest { private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME = "rsa-2048-lineage-2-signers"; + @Rule + public TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + public static void main(String[] params) throws Exception { File outDir = (params.length > 0) ? new File(params[0]) : new File("."); generateGoldenFiles(outDir); @@ -136,21 +139,24 @@ public class ApkSignerTest { new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); signGolden( "golden-legacy-aligned-in.apk", new File(outDir, "golden-legacy-aligned-v1-out.apk"), new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); signGolden( "golden-aligned-in.apk", new File(outDir, "golden-aligned-v1-out.apk"), new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); signGolden( "golden-unaligned-in.apk", @@ -367,7 +373,13 @@ public class ApkSignerTest { DataSource in = DataSources.asDataSource( ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName))); - apkSignerBuilder.setInputApk(in).setOutputApk(outFile).build().sign(); + apkSignerBuilder.setInputApk(in).setOutputApk(outFile); + + File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig"); + apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig); + apkSignerBuilder.setV4ErrorReportingEnabled(true); + + apkSignerBuilder.build().sign(); } @Test @@ -399,7 +411,8 @@ public class ApkSignerTest { new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); assertGolden( "golden-unaligned-in.apk", "golden-unaligned-v2-out.apk", @@ -474,7 +487,8 @@ public class ApkSignerTest { new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); assertGolden( "golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2-out.apk", @@ -548,7 +562,8 @@ public class ApkSignerTest { new ApkSigner.Builder(rsa2048SignerConfig) .setV1SigningEnabled(true) .setV2SigningEnabled(false) - .setV3SigningEnabled(false)); + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); assertGolden( "golden-aligned-in.apk", "golden-aligned-v2-out.apk", @@ -661,7 +676,7 @@ public class ApkSignerTest { String in = "original.apk"; // Sign so that the APK is guaranteed to verify on API Level 1+ - DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1)); + File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1)); assertVerified(verifyForMinSdkVersion(out, 1)); // Sign so that the APK is guaranteed to verify on API Level 18+ @@ -679,7 +694,7 @@ public class ApkSignerTest { String in = "original.apk"; // Sign so that the APK is guaranteed to verify on API Level 1+ - DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1)); + File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1)); assertVerified(verifyForMinSdkVersion(out, 1)); // Sign so that the APK is guaranteed to verify on API Level 21+ @@ -698,7 +713,7 @@ public class ApkSignerTest { // NOTE: EC APK signatures are not supported prior to API Level 18 // Sign so that the APK is guaranteed to verify on API Level 18+ - DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18)); + File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18)); assertVerified(verifyForMinSdkVersion(out, 18)); // Does not verify on API Level 17 because EC not supported assertVerificationFailure( @@ -930,14 +945,13 @@ public class ApkSignerTest { Arrays.asList( getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); - DataSource out = + File out = sign( "original.apk", new ApkSigner.Builder(signerConfigs) .setV3SigningEnabled(true) .setSigningCertificateLineage(lineage)); - SigningCertificateLineage lineageFromApk = - SigningCertificateLineage.readFromApkDataSource(out); + SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkFile(out); assertTrue( "The first signer was not in the lineage from the signed APK", lineageFromApk.isSignerInLineage((firstSigner))); @@ -960,7 +974,7 @@ public class ApkSignerTest { getDefaultSignerConfigFromResources( FIRST_RSA_2048_SIGNER_RESOURCE_NAME, FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS)); - DataSource signedApk = + File signedApk = sign( "original.apk", new ApkSigner.Builder(signersList) @@ -1009,28 +1023,32 @@ public class ApkSignerTest { messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded()); byte[] expectedStampCertificateDigest = messageDigest.digest(); - DataSource signedApk = + File signedApkFile = sign( "original.apk", new ApkSigner.Builder(signers) .setV1SigningEnabled(true) .setSourceStampSignerConfig(sourceStampSigner)); - ApkUtils.ZipSections zipSections = findZipSections(signedApk); - List<CentralDirectoryRecord> cdRecords = - V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections); - CentralDirectoryRecord stampCdRecord = null; - for (CentralDirectoryRecord cdRecord : cdRecords) { - if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { - stampCdRecord = cdRecord; - break; + try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) { + DataSource signedApk = DataSources.asDataSource(f, 0, f.length()); + + ApkUtils.ZipSections zipSections = findZipSections(signedApk); + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(signedApk, zipSections); + CentralDirectoryRecord stampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(cdRecord.getName())) { + stampCdRecord = cdRecord; + break; + } } + assertNotNull(stampCdRecord); + byte[] actualStampCertificateDigest = + LocalFileRecord.getUncompressedData( + signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset()); + assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest); } - assertNotNull(stampCdRecord); - byte[] actualStampCertificateDigest = - LocalFileRecord.getUncompressedData( - signedApk, stampCdRecord, zipSections.getZipCentralDirectoryOffset()); - assertArrayEquals(expectedStampCertificateDigest, actualStampCertificateDigest); } @Test @@ -1041,7 +1059,7 @@ public class ApkSignerTest { ApkSigner.SignerConfig sourceStampSigner = getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME); - DataSource signedApk = + File signedApk = sign( "original-with-stamp-file.apk", new ApkSigner.Builder(signers) @@ -1092,7 +1110,7 @@ public class ApkSignerTest { ApkSigner.SignerConfig sourceStampSigner = getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); - DataSource signedApk = + File signedApk = sign( "original-with-stamp-file.apk", new ApkSigner.Builder(signers) @@ -1113,7 +1131,7 @@ public class ApkSignerTest { Collections.singletonList( getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); - DataSource signedApk = + File signedApkFile = sign( "original.apk", new ApkSigner.Builder(signersList) @@ -1121,17 +1139,21 @@ public class ApkSignerTest { .setV2SigningEnabled(true) .setV3SigningEnabled(true)); - ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk); - ApkSigningBlockUtils.Result result = - new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); - assertThrows( - ApkSigningBlockUtils.SignatureNotFoundException.class, - () -> - ApkSigningBlockUtils.findSignature( - signedApk, - zipSections, - ApkSigningBlockUtils.VERSION_SOURCE_STAMP, - result)); + try (RandomAccessFile f = new RandomAccessFile(signedApkFile, "r")) { + DataSource signedApk = DataSources.asDataSource(f, 0, f.length()); + + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(signedApk); + ApkSigningBlockUtils.Result result = + new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + assertThrows( + ApkSigningBlockUtils.SignatureNotFoundException.class, + () -> + ApkSigningBlockUtils.findSignature( + signedApk, + zipSections, + ApkSigningBlockUtils.VERSION_SOURCE_STAMP, + result)); + } } @Test @@ -1142,13 +1164,14 @@ public class ApkSignerTest { ApkSigner.SignerConfig sourceStampSigner = getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); - DataSource signedApk = + File signedApk = sign( "original.apk", new ApkSigner.Builder(signersList) .setV1SigningEnabled(true) .setV2SigningEnabled(false) .setV3SigningEnabled(false) + .setV4SigningEnabled(false) .setSourceStampSignerConfig(sourceStampSigner)); ApkVerifier.Result sourceStampVerificationResult = @@ -1164,7 +1187,7 @@ public class ApkSignerTest { ApkSigner.SignerConfig sourceStampSigner = getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); - DataSource signedApk = + File signedApk = sign( "original.apk", new ApkSigner.Builder(signersList) @@ -1186,7 +1209,7 @@ public class ApkSignerTest { ApkSigner.SignerConfig sourceStampSigner = getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); - DataSource signedApk = + File signedApk = sign( "original.apk", new ApkSigner.Builder(signersList) @@ -1200,15 +1223,41 @@ public class ApkSignerTest { assertSourceStampVerified(signedApk, sourceStampVerificationResult); } - private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId) + @Test + public void testSignApk_stampBlock_withStampLineage() throws Exception { + List<ApkSigner.SignerConfig> signersList = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + ApkSigner.SignerConfig sourceStampSigner = + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + SigningCertificateLineage sourceStampLineage = + Resources.toSigningCertificateLineage( + getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = + sign( + "original.apk", + new ApkSigner.Builder(signersList) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setSourceStampSignerConfig(sourceStampSigner) + .setSourceStampSigningCertificateLineage(sourceStampLineage)); + + ApkVerifier.Result sourceStampVerificationResult = + verify(signedApk, /* minSdkVersion= */ null); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); + } + + private RSAPublicKey getRSAPublicKeyFromSigningBlock(File apk, int signatureVersionId) throws Exception { int signatureVersionBlockId; switch (signatureVersionId) { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: - signatureVersionBlockId = V2SchemeSigner.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; + signatureVersionBlockId = V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: - signatureVersionBlockId = V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + signatureVersionBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; break; default: throw new Exception( @@ -1247,13 +1296,17 @@ public class ApkSignerTest { } private static SignatureInfo getSignatureInfoFromApk( - DataSource apk, int signatureVersionId, int signatureVersionBlockId) + File apkFile, int signatureVersionId, int signatureVersionBlockId) throws IOException, ZipFormatException, - ApkSigningBlockUtils.SignatureNotFoundException { - ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); - ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result(signatureVersionId); - return ApkSigningBlockUtils.findSignature( - apk, zipSections, signatureVersionBlockId, result); + ApkSigningBlockUtils.SignatureNotFoundException { + try (RandomAccessFile f = new RandomAccessFile(apkFile, "r")) { + DataSource apk = DataSources.asDataSource(f, 0, f.length()); + ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + signatureVersionId); + return ApkSigningBlockUtils.findSignature(apk, zipSections, signatureVersionBlockId, + result); + } } /** @@ -1266,18 +1319,22 @@ public class ApkSignerTest { ApkSigner.Builder apkSignerBuilder) throws Exception { // Sign the provided golden input - DataSource out = sign(inResourceName, apkSignerBuilder); + File out = sign(inResourceName, apkSignerBuilder); + assertVerified(verify(out, AndroidSdkVersion.P)); // Assert that the output is identical to the provided golden output - if (out.size() > Integer.MAX_VALUE) { - throw new RuntimeException("Output too large: " + out.size() + " bytes"); + if (out.length() > Integer.MAX_VALUE) { + throw new RuntimeException("Output too large: " + out.length() + " bytes"); + } + byte[] outData = new byte[(int) out.length()]; + try (FileInputStream fis = new FileInputStream(out)) { + fis.read(outData); } - ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size()); + ByteBuffer actualOutBuf = ByteBuffer.wrap(outData); ByteBuffer expectedOutBuf = ByteBuffer.wrap(Resources.toByteArray(getClass(), expectedOutResourceName)); - int actualStartPos = actualOutBuf.position(); boolean identical = false; if (actualOutBuf.remaining() == expectedOutBuf.remaining()) { while (actualOutBuf.hasRemaining()) { @@ -1291,47 +1348,47 @@ public class ApkSignerTest { if (identical) { return; } - actualOutBuf.position(actualStartPos); if (KEEP_FAILING_OUTPUT_AS_FILES) { File tmp = File.createTempFile(getClass().getSimpleName(), ".apk"); - try (ByteChannel outChannel = - Files.newByteChannel( - tmp.toPath(), - StandardOpenOption.WRITE, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING)) { - while (actualOutBuf.hasRemaining()) { - outChannel.write(actualOutBuf); - } - } + Files.copy(out.toPath(), tmp.toPath()); fail(tmp + " differs from " + expectedOutResourceName); } else { fail("Output differs from " + expectedOutResourceName); } } - private DataSource sign(String inResourceName, ApkSigner.Builder apkSignerBuilder) + private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception { DataSource in = DataSources.asDataSource( ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName))); - ReadableDataSink out = DataSinks.newInMemoryDataSink(); - apkSignerBuilder.setInputApk(in).setOutputApk(out).build().sign(); - return out; + File outFile = mTemporaryFolder.newFile(); + apkSignerBuilder.setInputApk(in).setOutputApk(outFile); + + File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig"); + apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig); + apkSignerBuilder.setV4ErrorReportingEnabled(true); + + apkSignerBuilder.build().sign(); + return outFile; } - private static ApkVerifier.Result verifyForMinSdkVersion(DataSource apk, int minSdkVersion) + private static ApkVerifier.Result verifyForMinSdkVersion(File apk, int minSdkVersion) throws IOException, ApkFormatException, NoSuchAlgorithmException { return verify(apk, minSdkVersion); } - private static ApkVerifier.Result verify(DataSource apk, Integer minSdkVersionOverride) + private static ApkVerifier.Result verify(File apk, Integer minSdkVersionOverride) throws IOException, ApkFormatException, NoSuchAlgorithmException { ApkVerifier.Builder builder = new ApkVerifier.Builder(apk); if (minSdkVersionOverride != null) { builder.setMinCheckedPlatformVersion(minSdkVersionOverride); } + File idSig = new File(apk.getCanonicalPath() + ".idsig"); + if (idSig.exists()) { + builder.setV4SignatureFile(idSig); + } return builder.build().verify(); } @@ -1339,14 +1396,14 @@ public class ApkSignerTest { ApkVerifierTest.assertVerified(result); } - private static void assertSourceStampVerified(DataSource signedApk, ApkVerifier.Result result) + private static void assertSourceStampVerified(File signedApk, ApkVerifier.Result result) throws ApkSigningBlockUtils.SignatureNotFoundException, IOException, - ZipFormatException { + ZipFormatException { SignatureInfo signatureInfo = getSignatureInfoFromApk( signedApk, ApkSigningBlockUtils.VERSION_SOURCE_STAMP, - V2SourceStampSigner.V2_SOURCE_STAMP_BLOCK_ID); + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); assertNotNull(signatureInfo.signatureBlock); assertTrue(result.isSourceStampVerified()); } diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java index ed154c5..9e1a75e 100644 --- a/src/test/java/com/android/apksig/ApkVerifierTest.java +++ b/src/test/java/com/android/apksig/ApkVerifierTest.java @@ -23,6 +23,7 @@ import static org.junit.Assume.assumeNoException; import com.android.apksig.ApkVerifier.Issue; import com.android.apksig.ApkVerifier.IssueWithParams; +import com.android.apksig.ApkVerifier.Result.SourceStampInfo.SourceStampVerificationStatus; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.HexEncoding; @@ -35,6 +36,8 @@ import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.ByteBuffer; import java.security.InvalidKeyException; import java.security.MessageDigest; @@ -60,9 +63,14 @@ public class ApkVerifierTest { private static final String[] EC_KEY_NAMES = {"p256", "p384", "p521"}; private static final String[] RSA_KEY_NAMES = {"1024", "2048", "3072", "4096", "8192", "16384"}; private static final String[] RSA_KEY_NAMES_2048_AND_LARGER = { - "2048", "3072", "4096", "8192", "16384" + "2048", "3072", "4096", "8192", "16384" }; + private static final String RSA_2048_CERT_SHA256_DIGEST = + "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8"; + private static final String EC_P256_CERT_SHA256_DIGEST = + "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599"; + @Test public void testOriginalAccepted() throws Exception { // APK signed with v1 and v2 schemes. Obtained by building @@ -1064,6 +1072,33 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_correctSignature() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("valid-stamp.apk"); + // Since the API is only verifying the source stamp the result itself should be marked as + // verified. + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + // The source stamp can also be verified by platform version; confirm the verification works + // using just the max signature scheme version supported by that platform version. + verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test public void testSourceStampBlock_signatureMissing() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-without-block.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1072,6 +1107,14 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_signatureMissing() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-without-block.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_NOT_VERIFIED); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_SIG_MISSING); + } + + @Test public void testSourceStampBlock_certificateMismatch() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-certificate-mismatch.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1082,6 +1125,80 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_certificateMismatch() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-certificate-mismatch.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure( + verificationResult, + Issue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); + } + + @Test + public void testSourceStampBlock_v1OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verify("v1-only-with-stamp.apk"); + assertVerified(verificationResult); + assertTrue(verificationResult.isSourceStampVerified()); + } + + @Test + public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk"); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + // Confirm that the source stamp verification succeeds when specifying platform versions + // that supported later signature scheme versions. + verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test + public void testSourceStampBlock_v2OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verify("v2-only-with-stamp.apk"); + assertVerified(verificationResult); + assertTrue(verificationResult.isSourceStampVerified()); + } + + @Test + public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk"); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + + // Confirm that the source stamp verification succeeds when specifying a platform version + // that supports a later signature scheme version. + verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test + public void testSourceStampBlock_v3OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verify("v3-only-with-stamp.apk"); + assertVerified(verificationResult); + assertTrue(verificationResult.isSourceStampVerified()); + } + + @Test + public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk"); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test public void testSourceStampBlock_apkHashMismatch_v1SignatureScheme() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v1.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1090,6 +1207,14 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v1.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test public void testSourceStampBlock_apkHashMismatch_v2SignatureScheme() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v2.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1098,6 +1223,14 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v2.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test public void testSourceStampBlock_apkHashMismatch_v3SignatureScheme() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch-v3.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1106,6 +1239,14 @@ public class ApkVerifierTest { } @Test + public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-apk-hash-mismatch-v3.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test public void testSourceStampBlock_malformedSignature() throws Exception { ApkVerifier.Result verificationResult = verify("stamp-malformed-signature.apk"); // A broken stamp should not block a signing scheme verified APK. @@ -1114,6 +1255,81 @@ public class ApkVerifierTest { verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); } + @Test + public void verifySourceStamp_malformedSignature() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-malformed-signature.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure( + verificationResult, Issue.SOURCE_STAMP_MALFORMED_SIGNATURE); + } + + @Test + public void verifySourceStamp_expectedDigestMatchesActual() throws Exception { + // The ApkVerifier provides an API to specify the expected certificate digest; this test + // verifies that the test runs through to completion when the actual digest matches the + // provided value. + ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", + RSA_2048_CERT_SHA256_DIGEST); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test + public void verifySourceStamp_expectedDigestMismatch() throws Exception { + // If the caller requests source stamp verification with an expected cert digest that does + // not match the actual digest in the APK the verifier should report the mismatch. + ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", + EC_P256_CERT_SHA256_DIGEST); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.CERT_DIGEST_MISMATCH); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); + } + + @Test + public void verifySourceStamp_validStampLineage() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-valid.apk"); + assertVerified(verificationResult); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFIED); + } + + @Test + public void verifySourceStamp_invalidStampLineage() throws Exception { + ApkVerifier.Result verificationResult = verifySourceStamp("stamp-lineage-invalid.apk"); + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_POR_CERT_MISMATCH); + } + + @Test + public void apkVerificationIssueAdapter_verifyAllBaseIssuesMapped() throws Exception { + Field[] fields = ApkVerificationIssue.class.getFields(); + StringBuilder msg = new StringBuilder(); + for (Field field : fields) { + // All public static int fields in the ApkVerificationIssue class should be issue IDs; + // if any are added that are not intended as IDs a filter set should be applied to this + // test. + if (Modifier.isStatic(field.getModifiers()) && field.getType() == int.class) { + if (!ApkVerifier.ApkVerificationIssueAdapter + .sVerificationIssueIdToIssue.containsKey(field.get(null))) { + if (msg.length() > 0) { + msg.append('\n'); + } + msg.append( + "A mapping is required from ApkVerificationIssue." + field.getName() + + " to an ApkVerifier.Issue in ApkVerificationIssueAdapter"); + } + } + } + if (msg.length() > 0) { + fail(msg.toString()); + } + } + private ApkVerifier.Result verify(String apkFilenameInResources) throws IOException, ApkFormatException, NoSuchAlgorithmException { return verify(apkFilenameInResources, null, null); @@ -1149,6 +1365,36 @@ public class ApkVerifierTest { return builder.build().verify(); } + private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources) throws Exception { + return verifySourceStamp(apkFilenameInResources, null, null, null); + } + + private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources, + String expectedCertDigest) throws Exception { + return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null); + } + + private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources, + Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception { + return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride, + maxSdkVersionOverride); + } + + private ApkVerifier.Result verifySourceStamp(String apkFilenameInResources, + String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride) + throws Exception { + byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources); + ApkVerifier.Builder builder = new ApkVerifier.Builder( + DataSources.asDataSource(ByteBuffer.wrap(apkBytes))); + if (minSdkVersionOverride != null) { + builder.setMinCheckedPlatformVersion(minSdkVersionOverride); + } + if (maxSdkVersionOverride != null) { + builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride); + } + return builder.build().verifySourceStamp(expectedCertDigest); + } + static void assertVerified(ApkVerifier.Result result) { assertVerified(result, "APK"); } @@ -1344,6 +1590,12 @@ public class ApkVerifierTest { + msg); } + private static void assertSourceStampVerificationStatus(ApkVerifier.Result result, + SourceStampVerificationStatus verificationStatus) throws Exception { + assertEquals(verificationStatus, + result.getSourceStampInfo().getSourceStampVerificationStatus()); + } + private void assertVerificationFailure( String apkFilenameInResources, ApkVerifier.Issue expectedIssue) throws Exception { assertVerificationFailure(verify(apkFilenameInResources), expectedIssue); diff --git a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java index 2038421..14cab83 100644 --- a/src/test/java/com/android/apksig/SigningCertificateLineageTest.java +++ b/src/test/java/com/android/apksig/SigningCertificateLineageTest.java @@ -21,27 +21,23 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.android.apksig.SigningCertificateLineage.SignerCapabilities; +import com.android.apksig.SigningCertificateLineage.SignerConfig; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; -import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.util.ByteBufferUtils; import com.android.apksig.internal.util.Resources; - -import com.android.apksig.SigningCertificateLineage.SignerConfig; -import com.android.apksig.SigningCertificateLineage.SignerCapabilities; - import com.android.apksig.util.DataSource; -import java.io.IOException; -import java.nio.ByteBuffer; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.File; +import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -249,7 +245,8 @@ public class SigningCertificateLineageTest { // * length-prefixed bytes: attribute pair // * uint32: ID // * bytes: value - encoded V3 SigningCertificateLineage - ByteBuffer v3SignerAttribute = ByteBuffer.wrap(lineage.generateV3SignerAttribute()); + ByteBuffer v3SignerAttribute = ByteBuffer.wrap( + V3SchemeSigner.generateV3SignerAttribute(lineage)); v3SignerAttribute.order(ByteOrder.LITTLE_ENDIAN); ByteBuffer attribute = ApkSigningBlockUtils.getLengthPrefixedSlice(v3SignerAttribute); // The generateV3SignerAttribute method should only use the PROOF_OF_ROTATION_ATTR_ID @@ -258,7 +255,7 @@ public class SigningCertificateLineageTest { assertEquals( "The ID of the v3SignerAttribute ByteBuffer is not the expected " + "PROOF_OF_ROTATION_ATTR_ID", - V3SchemeSigner.PROOF_OF_ROTATION_ATTR_ID, id); + V3SchemeConstants.PROOF_OF_ROTATION_ATTR_ID, id); lineage = SigningCertificateLineage.readFromV3AttributeValue( ByteBufferUtils.toByteArray(attribute)); assertLineageContainsExpectedSigners(lineage, mSigners); diff --git a/src/test/java/com/android/apksig/SourceStampVerifierTest.java b/src/test/java/com/android/apksig/SourceStampVerifierTest.java new file mode 100644 index 0000000..f5020cc --- /dev/null +++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig; + +import static com.android.apksig.apk.ApkUtilsLite.computeSha256DigestBytes; +import static com.android.apksig.internal.apk.ApkSigningBlockUtilsLite.toHex; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import com.android.apksig.SourceStampVerifier.Result; +import com.android.apksig.SourceStampVerifier.Result.SignerInfo; +import com.android.apksig.internal.util.Resources; +import com.android.apksig.util.DataSources; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.nio.ByteBuffer; +import java.security.cert.X509Certificate; +import java.util.List; + +@RunWith(JUnit4.class) +public class SourceStampVerifierTest { + private static final String RSA_2048_CERT_SHA256_DIGEST = + "fb5dbd3c669af9fc236c6991e6387b7f11ff0590997f22d0f5c74ff40e04fca8"; + private static final String RSA_2048_2_CERT_SHA256_DIGEST = + "681b0e56a796350c08647352a4db800cc44b2adc8f4c72fa350bd05d4d50264d"; + private static final String RSA_2048_3_CERT_SHA256_DIGEST = + "bb77a72efc60e66501ab75953af735874f82cfe52a70d035186a01b3482180f3"; + private static final String EC_P256_CERT_SHA256_DIGEST = + "6a8b96e278e58f62cfe3584022cec1d0527fcb85a9e5d2e1694eb0405be5b599"; + private static final String EC_P256_2_CERT_SHA256_DIGEST = + "d78405f761ff6236cc9b570347a570aba0c62a129a3ac30c831c64d09ad95469"; + + @Test + public void verifySourceStamp_correctSignature() throws Exception { + Result verificationResult = verifySourceStamp("valid-stamp.apk"); + // Since the API is only verifying the source stamp the result itself should be marked as + // verified. + assertVerified(verificationResult); + + // The source stamp can also be verified by platform version; confirm the verification works + // using just the max signature scheme version supported by that platform version. + verificationResult = verifySourceStamp("valid-stamp.apk", 18, 18); + assertVerified(verificationResult); + + verificationResult = verifySourceStamp("valid-stamp.apk", 24, 24); + assertVerified(verificationResult); + + verificationResult = verifySourceStamp("valid-stamp.apk", 28, 28); + assertVerified(verificationResult); + } + + @Test + public void verifySourceStamp_rotatedV3Key_signingCertDigestsMatch() throws Exception { + // The SourceStampVerifier should return a result that includes all of the latest signing + // certificates for each of the signature schemes that are applicable to the specified + // min / max SDK versions. + + // Verify when platform versions that support the V1 - V3 signature schemes are specified + // that an APK signed with all signature schemes has its expected signers returned in the + // result. + Result verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 23, + 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, + EC_P256_CERT_SHA256_DIGEST, EC_P256_2_CERT_SHA256_DIGEST); + + // Verify when the specified platform versions only support a single signature scheme that + // scheme's signer is the only one in the result. + verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 18, 18); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null); + + verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 24, 24); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null); + + verificationResult = verifySourceStamp("v1v2v3-rotated-v3-key-valid-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, null, EC_P256_2_CERT_SHA256_DIGEST); + } + + @Test + public void verifySourceStamp_signatureMissing() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-without-block.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_SIG_MISSING); + } + + @Test + public void verifySourceStamp_certificateMismatch() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-certificate-mismatch.apk"); + assertSourceStampVerificationFailure( + verificationResult, + ApkVerificationIssue.SOURCE_STAMP_CERTIFICATE_MISMATCH_BETWEEN_SIGNATURE_BLOCK_AND_APK); + } + + @Test + public void verifySourceStamp_v1OnlySignatureValidStamp() throws Exception { + Result verificationResult = verifySourceStamp("v1-only-with-stamp.apk"); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null); + + // Confirm that the source stamp verification succeeds when specifying platform versions + // that supported later signature scheme versions. + verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null); + + verificationResult = verifySourceStamp("v1-only-with-stamp.apk", 24, 24); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, EC_P256_CERT_SHA256_DIGEST, null, null); + } + + @Test + public void verifySourceStamp_v2OnlySignatureValidStamp() throws Exception { + // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so + // set the min / max versions to prevent failure due to a missing V1 signature. + Result verificationResult = verifySourceStamp("v2-only-with-stamp.apk", + 24, 24); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null); + + // Confirm that the source stamp verification succeeds when specifying a platform version + // that supports a later signature scheme version. + verificationResult = verifySourceStamp("v2-only-with-stamp.apk", 28, 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, EC_P256_CERT_SHA256_DIGEST, null); + } + + @Test + public void verifySourceStamp_v3OnlySignatureValidStamp() throws Exception { + // The SourceStampVerifier will not query the APK's manifest for the minSdkVersion, so + // set the min / max versions to prevent failure due to a missing V1 signature. + Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", + 28, 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, null, EC_P256_CERT_SHA256_DIGEST); + } + + @Test + public void verifySourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-apk-hash-mismatch-v1.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test + public void verifySourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-apk-hash-mismatch-v2.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test + public void verifySourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-apk-hash-mismatch-v3.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test + public void verifySourceStamp_malformedSignature() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-malformed-signature.apk"); + assertSourceStampVerificationFailure( + verificationResult, ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE); + } + + @Test + public void verifySourceStamp_expectedDigestMatchesActual() throws Exception { + // The ApkVerifier provides an API to specify the expected certificate digest; this test + // verifies that the test runs through to completion when the actual digest matches the + // provided value. + Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", + RSA_2048_CERT_SHA256_DIGEST, 28, 28); + assertVerified(verificationResult); + } + + @Test + public void verifySourceStamp_expectedDigestMismatch() throws Exception { + // If the caller requests source stamp verification with an expected cert digest that does + // not match the actual digest in the APK the verifier should report the mismatch. + Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", + EC_P256_CERT_SHA256_DIGEST); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_EXPECTED_DIGEST_MISMATCH); + } + + @Test + public void verifySourceStamp_noStampCertDigestNorSignatureBlock() throws Exception { + // The caller of this API expects that the provided APK should be signed with a source + // stamp; if no artifacts of the stamp are present ensure that the API fails indicating the + // missing stamp. + Result verificationResult = verifySourceStamp("original.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_CERT_DIGEST_AND_SIG_BLOCK_MISSING); + } + + @Test + public void verifySourceStamp_validStampLineage() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-lineage-valid.apk"); + assertVerified(verificationResult); + assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST, + RSA_2048_2_CERT_SHA256_DIGEST); + } + + @Test + public void verifySourceStamp_invalidStampLineage() throws Exception { + Result verificationResult = verifySourceStamp( + "stamp-lineage-invalid.apk"); + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_POR_CERT_MISMATCH); + } + + @Test + public void verifySourceStamp_multipleSignersInLineage() throws Exception { + Result verificationResult = verifySourceStamp("stamp-lineage-with-3-signers.apk", 18, 28); + assertVerified(verificationResult); + assertSigningCertificatesInLineage(verificationResult, RSA_2048_CERT_SHA256_DIGEST, + RSA_2048_2_CERT_SHA256_DIGEST, RSA_2048_3_CERT_SHA256_DIGEST); + } + + @Test + public void verifySourceStamp_noSignersInLineage_returnsEmptyLineage() throws Exception { + // If the source stamp's signer has not yet been rotated then an empty lineage should be + // returned. + Result verificationResult = verifySourceStamp("valid-stamp.apk"); + assertSigningCertificatesInLineage(verificationResult); + } + + @Test + public void verifySourceStamp_noApkSignature_succeeds() + throws Exception { + // The SourceStampVerifier is designed to verify an APK's source stamp with minimal + // verification of the APK signature schemes. This test verifies if just the MANIFEST.MF + // is present without any other APK signatures the stamp signature can still be successfully + // verified. + Result verificationResult = verifySourceStamp("stamp-without-apk-signature.apk", 18, 28); + assertVerified(verificationResult); + assertSigningCertificates(verificationResult, null, null, null); + // While the source stamp verification should succeed a warning should still be logged to + // notify the caller that there were no signers. + assertSourceStampVerificationWarning(verificationResult, + ApkVerificationIssue.JAR_SIG_NO_SIGNATURES); + } + + private Result verifySourceStamp(String apkFilenameInResources) + throws Exception { + return verifySourceStamp(apkFilenameInResources, null, null, null); + } + + private Result verifySourceStamp(String apkFilenameInResources, + String expectedCertDigest) throws Exception { + return verifySourceStamp(apkFilenameInResources, expectedCertDigest, null, null); + } + + private Result verifySourceStamp(String apkFilenameInResources, + Integer minSdkVersionOverride, Integer maxSdkVersionOverride) throws Exception { + return verifySourceStamp(apkFilenameInResources, null, minSdkVersionOverride, + maxSdkVersionOverride); + } + + private Result verifySourceStamp(String apkFilenameInResources, + String expectedCertDigest, Integer minSdkVersionOverride, Integer maxSdkVersionOverride) + throws Exception { + byte[] apkBytes = Resources.toByteArray(getClass(), apkFilenameInResources); + SourceStampVerifier.Builder builder = new SourceStampVerifier.Builder( + DataSources.asDataSource(ByteBuffer.wrap(apkBytes))); + if (minSdkVersionOverride != null) { + builder.setMinCheckedPlatformVersion(minSdkVersionOverride); + } + if (maxSdkVersionOverride != null) { + builder.setMaxCheckedPlatformVersion(maxSdkVersionOverride); + } + return builder.build().verifySourceStamp(expectedCertDigest); + } + + private static void assertVerified(Result result) { + if (result.isVerified()) { + return; + } + StringBuilder msg = new StringBuilder(); + for (ApkVerificationIssue error : result.getAllErrors()) { + if (msg.length() > 0) { + msg.append('\n'); + } + msg.append(error.toString()); + } + fail("APK failed source stamp verification: " + msg.toString()); + } + + private static void assertSourceStampVerificationFailure(Result result, int expectedIssueId) { + if (result.isVerified()) { + fail( + "APK source stamp verification succeeded instead of failing with " + + expectedIssueId); + return; + } + assertSourceStampVerificationIssue(result.getAllErrors(), expectedIssueId); + } + + private static void assertSourceStampVerificationWarning(Result result, int expectedIssueId) { + assertSourceStampVerificationIssue(result.getAllWarnings(), expectedIssueId); + } + + private static void assertSourceStampVerificationIssue(List<ApkVerificationIssue> issues, + int expectedIssueId) { + StringBuilder msg = new StringBuilder(); + for (ApkVerificationIssue issue : issues) { + if (issue.getIssueId() == expectedIssueId) { + return; + } + if (msg.length() > 0) { + msg.append('\n'); + } + msg.append(issue.toString()); + } + + fail( + "APK source stamp verification did not report the expected issue. " + + "Expected error ID: " + + expectedIssueId + + ", actual: " + + (msg.length() > 0 ? msg.toString() : "No reported issues")); + } + + /** + * Asserts that the provided {@code expectedCertDigests} match their respective signing + * certificate digest in the specified {@code result}. + * + * <p>{@code expectedCertDigests} should be provided in order of the signature schemes with V1 + * being the first element, V2 the second, etc. If a signer is not expected to be present for + * a signature scheme version a {@code null} value should be provided; for instance if only a V3 + * signing certificate is expected the following should be provided: {@code null, null, + * v3ExpectedCertDigest}. + * + * <p>Note, this method only supports a single signer per signature scheme; if an expected + * certificate digest is provided for a signature scheme and multiple signers are found an + * assertion exception will be thrown. + */ + private static void assertSigningCertificates(Result result, String... expectedCertDigests) + throws Exception { + for (int i = 0; i < expectedCertDigests.length; i++) { + List<SignerInfo> signers = null; + switch (i) { + case 0: + signers = result.getV1SchemeSigners(); + break; + case 1: + signers = result.getV2SchemeSigners(); + break; + case 2: + signers = result.getV3SchemeSigners(); + break; + default: + fail("This method only supports verification of the signing certificates up " + + "through the V3 Signature Scheme"); + } + if (expectedCertDigests[i] == null) { + assertEquals( + "Did not expect any V" + (i + 1) + " signers, found " + signers.size(), 0, + signers.size()); + continue; + } + if (signers.size() != 1) { + fail("Expected one V" + (i + 1) + " signer with certificate digest " + + expectedCertDigests[i] + ", found " + signers.size() + " V" + (i + 1) + + " signers"); + } + X509Certificate signingCertificate = signers.get(0).getSigningCertificate(); + assertNotNull(signingCertificate); + assertEquals(expectedCertDigests[i], + toHex(computeSha256DigestBytes(signingCertificate.getEncoded()))); + } + } + + /** + * Asserts that the provided {@code expectedCertDigests} match their respective certificate in + * the source stamp's lineage with the oldest signer at element 0. + * + * <p>If no values are provided for the expectedCertDigests, the source stamp's lineage will + * be checked for an empty {@code List} indicating the source stamp has not been rotated. + */ + private static void assertSigningCertificatesInLineage(Result result, + String... expectedCertDigests) throws Exception { + List<X509Certificate> lineageCertificates = + result.getSourceStampInfo().getCertificatesInLineage(); + assertEquals("Unexpected number of lineage certificates", expectedCertDigests.length, + lineageCertificates.size()); + for (int i = 0; i < expectedCertDigests.length; i++) { + assertEquals("Stamp lineage mismatch at signer " + i, expectedCertDigests[i], + toHex(computeSha256DigestBytes(lineageCertificates.get(i).getEncoded()))); + } + } +} diff --git a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java index 480dc1a..e8234c9 100644 --- a/src/test/java/com/android/apksig/apk/ApkUtilsTest.java +++ b/src/test/java/com/android/apksig/apk/ApkUtilsTest.java @@ -90,6 +90,49 @@ public class ApkUtilsTest { } @Test + public void testGetTargetSdkVersionFromBinaryAndroidManifest() throws Exception { + ByteBuffer manifest = getAndroidManifest("v3-ec-p256-targetSdk-30.apk"); + assertEquals(30, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetTargetSdkVersion_noUsesSdkElement_returnsDefault() throws Exception { + ByteBuffer manifest = getAndroidManifest("v1-only-no-uses-sdk.apk"); + assertEquals(1, ApkUtils.getTargetSdkVersionFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetTargetSandboxVersionFromBinaryAndroidManifest() throws Exception { + ByteBuffer manifest = getAndroidManifest("targetSandboxVersion-2.apk"); + assertEquals(2, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetTargetSandboxVersion_noTargetSandboxAttribute_returnsDefault() + throws Exception { + ByteBuffer manifest = getAndroidManifest("original.apk"); + assertEquals(1, ApkUtils.getTargetSandboxVersionFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetVersionCodeFromBinaryAndroidManifest() throws Exception { + ByteBuffer manifest = getAndroidManifest("original.apk"); + assertEquals(10, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetVersionCode_withVersionCodeMajor_returnsOnlyVersionCode() throws Exception { + ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk"); + assertEquals(25, ApkUtils.getVersionCodeFromBinaryAndroidManifest(manifest)); + } + + @Test + public void testGetLongVersionCodeFromBinaryAndroidManifest() throws Exception { + ByteBuffer manifest = getAndroidManifest("original-with-versionCodeMajor.apk"); + assertEquals(4294967321L, ApkUtils.getLongVersionCodeFromBinaryAndroidManifest(manifest)); + } + + @Test public void testGetAndroidManifest() throws Exception { ByteBuffer manifest = getAndroidManifest("original.apk"); MessageDigest md = MessageDigest.getInstance("SHA-256"); diff --git a/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk Binary files differnew file mode 100644 index 0000000..315254d --- /dev/null +++ b/src/test/resources/com/android/apksig/original-with-versionCodeMajor.apk diff --git a/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk Binary files differnew file mode 100644 index 0000000..f9777c3 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-lineage-invalid.apk diff --git a/src/test/resources/com/android/apksig/stamp-lineage-valid.apk b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk Binary files differnew file mode 100644 index 0000000..955652e --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-lineage-valid.apk diff --git a/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk Binary files differnew file mode 100644 index 0000000..c24fa98 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-lineage-with-3-signers.apk diff --git a/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk Binary files differnew file mode 100644 index 0000000..c2e6826 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-without-apk-signature.apk diff --git a/src/test/resources/com/android/apksig/v1-only-with-stamp.apk b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk Binary files differnew file mode 100644 index 0000000..745a7aa --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-only-with-stamp.apk diff --git a/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk Binary files differnew file mode 100644 index 0000000..5f1103a --- /dev/null +++ b/src/test/resources/com/android/apksig/v1v2v3-rotated-v3-key-valid-stamp.apk diff --git a/src/test/resources/com/android/apksig/v2-only-with-stamp.apk b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk Binary files differnew file mode 100644 index 0000000..ebd4021 --- /dev/null +++ b/src/test/resources/com/android/apksig/v2-only-with-stamp.apk diff --git a/src/test/resources/com/android/apksig/v3-only-with-stamp.apk b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk Binary files differnew file mode 100644 index 0000000..5f65214 --- /dev/null +++ b/src/test/resources/com/android/apksig/v3-only-with-stamp.apk |