diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-09-22 14:15:34 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-09-22 14:15:34 +0000 |
commit | 82cb095a14d225b3dfc0bdf40c1981ecf865d931 (patch) | |
tree | 9e31880da55d7e9cc5b405cab5c2112d8cc452d1 | |
parent | 86cbfc1ec45ef3490990e3b3a86bc3f568adaebc (diff) | |
parent | 3a417211e8f11480be69b714309c1576d3082489 (diff) | |
download | apksig-82cb095a14d225b3dfc0bdf40c1981ecf865d931.tar.gz |
Snap for 9092047 from 3a417211e8f11480be69b714309c1576d3082489 to studio-flamingo-releasestudio-2022.2.1-canary7studio-2022.2.1-canary2studio-2022.2.1-canary10studio-2022.2.1-beta5studio-2022.2.1-beta1studio-2022.2.1studio-2022.1.1-canary2
Change-Id: Ic8dadb5a996f16b5cbb7f53c307b6308215c1f5a
140 files changed, 9468 insertions, 1414 deletions
@@ -2,7 +2,7 @@ licenses(["notice"]) # Apache License 2.0 -load("//tools/base/bazel:coverage.bzl", "coverage_java_test", "coverage_java_library") +load("//tools/base/bazel:coverage.bzl", "coverage_java_library", "coverage_java_test") # Public API of the apksig library coverage_java_library( @@ -69,5 +69,7 @@ coverage_java_test( deps = [ ":apksig-all", "@maven//:junit.junit", + "@maven//:org.bouncycastle.bcprov-jdk15on", + "@maven//:org.conscrypt.conscrypt-openjdk-uber", ], ) diff --git a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java index e1d976e..4e5fa85 100644 --- a/src/apksigner/java/com/android/apksigner/ApkSignerTool.java +++ b/src/apksigner/java/com/android/apksigner/ApkSignerTool.java @@ -22,6 +22,7 @@ import com.android.apksig.SigningCertificateLineage; import com.android.apksig.SigningCertificateLineage.SignerCapabilities; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.MinSdkVersionException; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.util.DataSource; import com.android.apksig.util.DataSources; @@ -48,6 +49,7 @@ import java.security.interfaces.ECKey; import java.security.interfaces.RSAKey; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.List; /** @@ -62,6 +64,8 @@ public class ApkSignerTool { private static final String HELP_PAGE_VERIFY = "help_verify.txt"; private static final String HELP_PAGE_ROTATE = "help_rotate.txt"; private static final String HELP_PAGE_LINEAGE = "help_lineage.txt"; + private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----"; + private static final String END_CERTIFICATE = "-----END CERTIFICATE-----"; private static MessageDigest sha256 = null; private static MessageDigest sha1 = null; @@ -78,6 +82,7 @@ public class ApkSignerTool { return; } + String cmd = params[0]; try { if ("sign".equals(cmd)) { @@ -109,6 +114,7 @@ public class ApkSignerTool { } } + private static void sign(String[] params) throws Exception { if (params.length == 0) { printUsage(HELP_PAGE_SIGN); @@ -122,14 +128,21 @@ public class ApkSignerTool { boolean v2SigningEnabled = true; boolean v3SigningEnabled = true; boolean v4SigningEnabled = false; + boolean forceSourceStampOverwrite = false; + boolean sourceStampTimestampEnabled = true; + boolean alignFileSize = false; + boolean verityEnabled = false; boolean debuggableApkPermitted = true; int minSdkVersion = 1; boolean minSdkVersionSpecified = false; int maxSdkVersion = Integer.MAX_VALUE; + int rotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + boolean rotationTargetsDevRelease = false; 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); @@ -137,6 +150,8 @@ public class ApkSignerTool { String optionOriginalForm = null; boolean v4SigningFlagFound = false; boolean sourceStampFlagFound = false; + boolean deterministicDsaSigning = false; + boolean otherSignersSignaturesPreserved = false; while ((optionName = optionsParser.nextOption()) != null) { optionOriginalForm = optionsParser.getOptionOriginalForm(); if (("help".equals(optionName)) || ("h".equals(optionName))) { @@ -151,7 +166,13 @@ public class ApkSignerTool { minSdkVersionSpecified = true; } else if ("max-sdk-version".equals(optionName)) { maxSdkVersion = optionsParser.getRequiredIntValue("Maximum API Level"); - } else if ("v1-signing-enabled".equals(optionName)) { + } else if ("rotation-min-sdk-version".equals(optionName)) { + rotationMinSdkVersion = optionsParser.getRequiredIntValue( + "Minimum API Level for Rotation"); + } else if ("rotation-targets-dev-release".equals(optionName)) { + rotationTargetsDevRelease = optionsParser.getOptionalBooleanValue(true); + } + else if ("v1-signing-enabled".equals(optionName)) { v1SigningEnabled = optionsParser.getOptionalBooleanValue(true); } else if ("v2-signing-enabled".equals(optionName)) { v2SigningEnabled = optionsParser.getOptionalBooleanValue(true); @@ -160,6 +181,14 @@ public class ApkSignerTool { } else if ("v4-signing-enabled".equals(optionName)) { v4SigningEnabled = optionsParser.getOptionalBooleanValue(true); v4SigningFlagFound = true; + } else if ("force-stamp-overwrite".equals(optionName)) { + forceSourceStampOverwrite = optionsParser.getOptionalBooleanValue(true); + } else if ("stamp-timestamp-enabled".equals(optionName)) { + sourceStampTimestampEnabled = optionsParser.getOptionalBooleanValue(true); + } else if ("align-file-size".equals(optionName)) { + alignFileSize = true; + } else if ("verity-enabled".equals(optionName)) { + verityEnabled = optionsParser.getOptionalBooleanValue(true); } else if ("debuggable-apk-permitted".equals(optionName)) { debuggableApkPermitted = optionsParser.getOptionalBooleanValue(true); } else if ("next-signer".equals(optionName)) { @@ -229,6 +258,14 @@ 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 if ("deterministic-dsa-signing".equals(optionName)) { + deterministicDsaSigning = optionsParser.getOptionalBooleanValue(false); + } else if ("append-signature".equals(optionName)) { + otherSignersSignaturesPreserved = optionsParser.getOptionalBooleanValue(true); } else { throw new ParameterException( "Unsupported option: " + optionOriginalForm + ". See --help for supported" @@ -285,7 +322,8 @@ public class ApkSignerTool { for (SignerParams signer : signers) { signerNumber++; signer.setName("signer #" + signerNumber); - ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever); + ApkSigner.SignerConfig signerConfig = getSignerConfig(signer, passwordRetriever, + deterministicDsaSigning); if (signerConfig == null) { return; } @@ -294,7 +332,8 @@ public class ApkSignerTool { if (sourceStampFlagFound) { sourceStampSignerParams.setName("stamp signer"); sourceStampSignerConfig = - getSignerConfig(sourceStampSignerParams, passwordRetriever); + getSignerConfig(sourceStampSignerParams, passwordRetriever, + deterministicDsaSigning); if (sourceStampSignerConfig == null) { return; } @@ -315,14 +354,20 @@ public class ApkSignerTool { new ApkSigner.Builder(signerConfigs) .setInputApk(inputApk) .setOutputApk(tmpOutputApk) - .setOtherSignersSignaturesPreserved(false) + .setOtherSignersSignaturesPreserved(otherSignersSignaturesPreserved) .setV1SigningEnabled(v1SigningEnabled) .setV2SigningEnabled(v2SigningEnabled) .setV3SigningEnabled(v3SigningEnabled) .setV4SigningEnabled(v4SigningEnabled) + .setForceSourceStampOverwrite(forceSourceStampOverwrite) + .setSourceStampTimestampEnabled(sourceStampTimestampEnabled) + .setAlignFileSize(alignFileSize) + .setVerityEnabled(verityEnabled) .setV4ErrorReportingEnabled(v4SigningEnabled && v4SigningFlagFound) .setDebuggableApkPermitted(debuggableApkPermitted) - .setSigningCertificateLineage(lineage); + .setSigningCertificateLineage(lineage) + .setMinSdkVersionForRotation(rotationMinSdkVersion) + .setRotationTargetsDevRelease(rotationTargetsDevRelease); if (minSdkVersionSpecified) { apkSignerBuilder.setMinSdkVersion(minSdkVersion); } @@ -333,7 +378,8 @@ public class ApkSignerTool { apkSignerBuilder.setV4SignatureOutputFile(outputV4SignatureFile); } if (sourceStampSignerConfig != null) { - apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig); + apkSignerBuilder.setSourceStampSignerConfig(sourceStampSignerConfig) + .setSourceStampSigningCertificateLineage(sourceStampLineage); } ApkSigner apkSigner = apkSignerBuilder.build(); try { @@ -358,8 +404,8 @@ public class ApkSignerTool { } } - private static ApkSigner.SignerConfig getSignerConfig( - SignerParams signer, PasswordRetriever passwordRetriever) { + private static ApkSigner.SignerConfig getSignerConfig(SignerParams signer, + PasswordRetriever passwordRetriever, boolean deterministicDsaSigning) { try { signer.loadPrivateKeyAndCerts(passwordRetriever); } catch (ParameterException e) { @@ -391,7 +437,8 @@ public class ApkSignerTool { } ApkSigner.SignerConfig signerConfig = new ApkSigner.SignerConfig.Builder( - v1SigBasename, signer.getPrivateKey(), signer.getCerts()) + v1SigBasename, signer.getPrivateKey(), signer.getCerts(), + deterministicDsaSigning) .build(); return signerConfig; } @@ -408,12 +455,15 @@ public class ApkSignerTool { int maxSdkVersion = Integer.MAX_VALUE; boolean maxSdkVersionSpecified = false; boolean printCerts = false; + boolean printCertsPem = 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)) { @@ -424,6 +474,13 @@ public class ApkSignerTool { maxSdkVersionSpecified = true; } else if ("print-certs".equals(optionName)) { printCerts = optionsParser.getOptionalBooleanValue(true); + } else if ("print-certs-pem".equals(optionName)) { + printCertsPem = optionsParser.getOptionalBooleanValue(true); + // If the PEM output of the certs is requested, this implicitly implies the + // cert details should be printed. + if (printCertsPem && !printCerts) { + printCerts = true; + } } else if (("v".equals(optionName)) || ("verbose".equals(optionName))) { verbose = optionsParser.getOptionalBooleanValue(true); } else if ("Werr".equals(optionName)) { @@ -436,6 +493,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" @@ -488,7 +550,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(".")) { @@ -499,8 +563,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(); @@ -516,16 +581,48 @@ public class ApkSignerTool { "Verified using v3 scheme (APK Signature Scheme v3): " + result.isVerifiedUsingV3Scheme()); System.out.println( + "Verified using v3.1 scheme (APK Signature Scheme v3.1): " + + result.isVerifiedUsingV31Scheme()); + System.out.println( "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; - for (X509Certificate signerCert : signerCerts) { - signerNumber++; - printCertificate(signerCert, "Signer #" + signerNumber, verbose); + // The v3.1 signature scheme allows key rotation to target T+ while the original + // signing key can still be used with v3.0; if a v3.1 block is present then also + // include the target SDK versions for both rotation and the original signing key. + if (result.isVerifiedUsingV31Scheme()) { + for (ApkVerifier.Result.V3SchemeSignerInfo signer : + result.getV31SchemeSigners()) { + + printCertificate(signer.getCertificate(), + "Signer (minSdkVersion=" + signer.getMinSdkVersion() + + (signer.getRotationTargetsDevRelease() + ? " (dev release=true)" : "") + + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")", + verbose, printCertsPem); + } + for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) { + printCertificate(signer.getCertificate(), + "Signer (minSdkVersion=" + signer.getMinSdkVersion() + + ", maxSdkVersion=" + signer.getMaxSdkVersion() + ")", + verbose, printCertsPem); + } + } else { + int signerNumber = 0; + for (X509Certificate signerCert : signerCerts) { + signerNumber++; + printCertificate(signerCert, "Signer #" + signerNumber, verbose, + printCertsPem); + } + } + if (sourceStampInfo != null) { + printCertificate(sourceStampInfo.getCertificate(), "Source Stamp Signer", + verbose, printCertsPem); } } } else { @@ -537,7 +634,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); @@ -576,8 +673,21 @@ public class ApkSignerTool { "WARNING: APK Signature Scheme v3 " + signerName + ": " + warning); } } + for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) { + String signerName = "signer #" + (signer.getIndex() + 1) + "(minSdkVersion=" + + signer.getMinSdkVersion() + ", maxSdkVersion=" + signer.getMaxSdkVersion() + + ")"; + for (ApkVerifier.IssueWithParams error : signer.getErrors()) { + System.err.println( + "ERROR: APK Signature Scheme v3.1 " + signerName + ": " + error); + } + for (ApkVerifier.IssueWithParams warning : signer.getWarnings()) { + warningsEncountered = true; + warningsOut.println( + "WARNING: APK Signature Scheme v3.1 " + signerName + ": " + warning); + } + } - ApkVerifier.Result.SourceStampInfo sourceStampInfo = result.getSourceStampInfo(); if (sourceStampInfo != null) { for (ApkVerifier.IssueWithParams error : sourceStampInfo.getErrors()) { System.err.println("ERROR: SourceStamp: " + error); @@ -585,6 +695,9 @@ public class ApkSignerTool { for (ApkVerifier.IssueWithParams warning : sourceStampInfo.getWarnings()) { warningsOut.println("WARNING: SourceStamp: " + warning); } + for (ApkVerifier.IssueWithParams infoMessage : sourceStampInfo.getInfoMessages()) { + System.out.println("INFO: SourceStamp: " + infoMessage); + } } if (!verified) { @@ -737,6 +850,7 @@ public class ApkSignerTool { boolean verbose = false; boolean printCerts = false; + boolean printCertsPem = false; boolean lineageUpdated = false; File inputKeyLineage = null; File outputKeyLineage = null; @@ -758,6 +872,13 @@ public class ApkSignerTool { verbose = optionsParser.getOptionalBooleanValue(true); } else if ("print-certs".equals(optionName)) { printCerts = optionsParser.getOptionalBooleanValue(true); + } else if ("print-certs-pem".equals(optionName)) { + printCertsPem = optionsParser.getOptionalBooleanValue(true); + // If the PEM output of the certs is requested, this implicitly implies the + // cert details should be printed. + if (printCertsPem && !printCerts) { + printCerts = true; + } } else { throw new ParameterException( "Unsupported option: " + optionsParser.getOptionOriginalForm() @@ -818,7 +939,8 @@ public class ApkSignerTool { for (int i = 0; i < signingCerts.size(); i++) { X509Certificate signerCert = signingCerts.get(i); SignerCapabilities signerCapabilities = lineage.getSignerCapabilities(signerCert); - printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose); + printCertificate(signerCert, "Signer #" + (i + 1) + " in lineage", verbose, + printCertsPem); printCapabilities(signerCapabilities); } } @@ -936,10 +1058,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); @@ -950,20 +1072,29 @@ public class ApkSignerTool { } /** + * @see #printCertificate(X509Certificate, String, boolean, boolean) + */ + public static void printCertificate(X509Certificate cert, String name, boolean verbose) + throws NoSuchAlgorithmException, CertificateEncodingException { + printCertificate(cert, name, verbose, false); + } + + /** * Prints details from the provided certificate to stdout. * * @param cert the certificate to be displayed. * @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. - * + * @param pemOutput boolean indicating whether the PEM encoding of 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 * certificate. */ - public static void printCertificate(X509Certificate cert, String name, boolean verbose) - throws NoSuchAlgorithmException, CertificateEncodingException { + public static void printCertificate(X509Certificate cert, String name, boolean verbose, + boolean pemOutput) throws NoSuchAlgorithmException, CertificateEncodingException { if (cert == null) { throw new NullPointerException("cert == null"); } @@ -1008,6 +1139,18 @@ public class ApkSignerTool { System.out.println( name + " public key MD5 digest: " + HexEncoding.encode(md5.digest(encodedKey))); } + + if (pemOutput) { + System.out.println(BEGIN_CERTIFICATE); + final int lineWidth = 64; + String pemEncodedCert = Base64.getEncoder().encodeToString(cert.getEncoded()); + for (int i = 0; i < pemEncodedCert.length(); i += lineWidth) { + System.out.println(pemEncodedCert.substring(i, i + lineWidth > pemEncodedCert.length() + ? pemEncodedCert.length() + : i + lineWidth)); + } + System.out.println(END_CERTIFICATE); + } } /** @@ -1046,10 +1189,19 @@ public class ApkSignerTool { } Provider provider; if (constructorParam != null) { - // Single-arg Provider constructor - provider = - (Provider) providerClass.getConstructor(String.class) - .newInstance(constructorParam); + try { + // Single-arg Provider constructor + provider = + (Provider) providerClass.getConstructor(String.class) + .newInstance(constructorParam); + } catch (NoSuchMethodException e) { + // Starting from JDK 9 the single-arg constructor accepting the configuration + // has been replaced by a configure(String) method to be invoked after + // instantiating the Provider with the no-arg constructor. + provider = (Provider) providerClass.getConstructor().newInstance(); + provider = (Provider) providerClass.getMethod("configure", String.class) + .invoke(provider, constructorParam); + } } else { // No-arg Provider constructor provider = (Provider) providerClass.getConstructor().newInstance(); diff --git a/src/apksigner/java/com/android/apksigner/SignerParams.java b/src/apksigner/java/com/android/apksigner/SignerParams.java index 8c8b550..515cd41 100644 --- a/src/apksigner/java/com/android/apksigner/SignerParams.java +++ b/src/apksigner/java/com/android/apksigner/SignerParams.java @@ -209,10 +209,19 @@ public class SignerParams { } Provider ksProvider; if (keystoreProviderArg != null) { - // Single-arg Provider constructor - ksProvider = - (Provider) ksProviderClass.getConstructor(String.class) - .newInstance(keystoreProviderArg); + try { + // Single-arg Provider constructor + ksProvider = + (Provider) ksProviderClass.getConstructor(String.class) + .newInstance(keystoreProviderArg); + } catch (NoSuchMethodException e) { + // Starting from JDK 9 the single-arg constructor accepting the configuration + // has been replaced by a configure(String) method to be invoked after + // instantiating the Provider with the no-arg constructor. + ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); + ksProvider = (Provider) ksProviderClass.getMethod("configure", + String.class).invoke(ksProvider, keystoreProviderArg); + } } else { // No-arg Provider constructor ksProvider = (Provider) ksProviderClass.getConstructor().newInstance(); diff --git a/src/apksigner/java/com/android/apksigner/help_lineage.txt b/src/apksigner/java/com/android/apksigner/help_lineage.txt index 3f4922d..8fe410b 100644 --- a/src/apksigner/java/com/android/apksigner/help_lineage.txt +++ b/src/apksigner/java/com/android/apksigner/help_lineage.txt @@ -19,6 +19,10 @@ has been migrated to the new signing certificate. --print-certs Show information about the signing certificates and their capabilities in the SigningCertificateLineage. +--print-certs-pem Show information about the signing certificates and their capabilities + in the SigningCertificateLineage; prints the PEM encoding of each signing + certificate to stdout. + -v, --verbose Verbose output mode. -h, --help Show help about this command and exit. diff --git a/src/apksigner/java/com/android/apksigner/help_rotate.txt b/src/apksigner/java/com/android/apksigner/help_rotate.txt index ff58372..d19136b 100644 --- a/src/apksigner/java/com/android/apksigner/help_rotate.txt +++ b/src/apksigner/java/com/android/apksigner/help_rotate.txt @@ -43,10 +43,17 @@ used in some situations on the platform even though the APK is now being signed by a newer signing certificate. By default, the new signer will have all capabilities, but the capability options can be specified for the new signer during rotation to act as a default level of trust when moving to a newer -signing certificate.The capability options accept an optional boolean value of +signing certificate. The capability options accept an optional boolean value of true or false; if this value is not specified then the option will default to true. +Prior to Android 12, if multiple apps shared a common signer in their signing lineage +with distinct capabilities assigned, a bug in the platform would cause the capabilities +declared for this signer in one of the app's signing lineage to be assigned to this same +common signer in the lineage of the rest of the apps. Apps that use the default capabilities, +or that assign the same capabilities to a common signer in their lineage, are not impacted +by this bug. + --ks Load private key and certificate chain from the Java KeyStore initialized from the specified file. NONE means diff --git a/src/apksigner/java/com/android/apksigner/help_sign.txt b/src/apksigner/java/com/android/apksigner/help_sign.txt index 92ab99c..dc5f6cc 100644 --- a/src/apksigner/java/com/android/apksigner/help_sign.txt +++ b/src/apksigner/java/com/android/apksigner/help_sign.txt @@ -46,6 +46,15 @@ certificate. enabled based on min and max SDK version (see --min-sdk-version and --max-sdk-version). +--force-stamp-overwrite Whether to overwrite existing source stamp in the + APK, if found. By default, it is set to false. It has no + effect if no source stamp signer config is provided. + +--align-file-size Produces APK file sized as multiples of 4K bytes. + +--verity-enabled Whether to enable the verity signature algorithm for the + v2 and v3 signature schemes. + --min-sdk-version Lowest API Level on which this APK's signatures will be verified. By default, the value from AndroidManifest.xml is used. The higher the value, the stronger security @@ -54,6 +63,24 @@ certificate. --max-sdk-version Highest API Level on which this APK's signatures will be verified. By default, the highest possible value is used. +--rotation-min-sdk-version Lowest API Level for which an APK's rotated signing + key should be used to produce the APK's signature. The + original signing key for the APK will be used for all + previous platform versions. Specifying a value <= 32 + (Android Sv2) will result in the original V3 signing block + being used without platform targeting. By default, + rotated signing keys will be used with the V3.1 signing + block which supports Android T+. + +--rotation-targets-dev-release The specified rotation-min-sdk-version is intended + for a platform release under development. During development + of a new platform, the API Level of the previously released + platform is used as the API Level of the development + platform until the SDK is finalized. This flag allows + targeting signing key rotation to a development platform + with API Level X while preventing the rotated key from being + used on the latest release platform with API Level X. + --debuggable-apk-permitted Whether to permit signing android:debuggable="true" APKs. Android disables some of its security protections for such apps. For example, anybody with ADB shell access @@ -83,6 +110,18 @@ certificate. can also be specified; the lineage will then be read from the signed data in the APK. +--deterministic-dsa-signing When signing with the DSA signature algorithm, + whether to use the deterministic version as specified in + RFC 6979. + +--append-signature Appends the current signature to any signatures that + already exist within the APK. This option can be used + when an APK is signed by multiple independent signers to + allow each to add their own signature without needing to + share their private key. This option can also be used to + preserve existing key / value blocks that exist within the + APK signing block. + -h, --help Show help about this command and exit diff --git a/src/apksigner/java/com/android/apksigner/help_verify.txt b/src/apksigner/java/com/android/apksigner/help_verify.txt index c5cf663..bc70924 100644 --- a/src/apksigner/java/com/android/apksigner/help_verify.txt +++ b/src/apksigner/java/com/android/apksigner/help_verify.txt @@ -11,6 +11,9 @@ range of API Levels. --print-certs Show information about the APK's signing certificates +--print-certs-pem Show information about the APK's signing certificates and prints the PEM + encoding of each signing certificate to stdout. + -v, --verbose Verbose output mode --min-sdk-version Lowest API Level on which this APK's signatures will be diff --git a/src/main/java/com/android/apksig/ApkSigner.java b/src/main/java/com/android/apksig/ApkSigner.java index e60fd69..46e03e8 100644 --- a/src/main/java/com/android/apksig/ApkSigner.java +++ b/src/main/java/com/android/apksig/ApkSigner.java @@ -17,11 +17,14 @@ package com.android.apksig; import static com.android.apksig.apk.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkSigningBlockNotFoundException; import com.android.apksig.apk.ApkUtils; import com.android.apksig.apk.MinSdkVersionException; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.zip.CentralDirectoryRecord; import com.android.apksig.internal.zip.EocdRecord; @@ -46,6 +49,7 @@ import java.security.PrivateKey; import java.security.SignatureException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -80,16 +84,25 @@ public class ApkSigner { private static final short ANDROID_COMMON_PAGE_ALIGNMENT_BYTES = 4096; + private static final short ANDROID_FILE_ALIGNMENT_BYTES = 4096; + /** Name of the Android manifest ZIP entry in APKs. */ private static final String ANDROID_MANIFEST_ZIP_ENTRY_NAME = "AndroidManifest.xml"; private final List<SignerConfig> mSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; + private final boolean mForceSourceStampOverwrite; + private final boolean mSourceStampTimestampEnabled; private final Integer mMinSdkVersion; + private final int mRotationMinSdkVersion; + private final boolean mRotationTargetsDevRelease; private final boolean mV1SigningEnabled; private final boolean mV2SigningEnabled; private final boolean mV3SigningEnabled; private final boolean mV4SigningEnabled; + private final boolean mAlignFileSize; + private final boolean mVerityEnabled; private final boolean mV4ErrorReportingEnabled; private final boolean mDebuggableApkPermitted; private final boolean mOtherSignersSignaturesPreserved; @@ -111,11 +124,18 @@ public class ApkSigner { private ApkSigner( List<SignerConfig> signerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, + boolean forceSourceStampOverwrite, + boolean sourceStampTimestampEnabled, Integer minSdkVersion, + int rotationMinSdkVersion, + boolean rotationTargetsDevRelease, boolean v1SigningEnabled, boolean v2SigningEnabled, boolean v3SigningEnabled, boolean v4SigningEnabled, + boolean alignFileSize, + boolean verityEnabled, boolean v4ErrorReportingEnabled, boolean debuggableApkPermitted, boolean otherSignersSignaturesPreserved, @@ -131,11 +151,18 @@ public class ApkSigner { mSignerConfigs = signerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + mForceSourceStampOverwrite = forceSourceStampOverwrite; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; mMinSdkVersion = minSdkVersion; + mRotationMinSdkVersion = rotationMinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; mV1SigningEnabled = v1SigningEnabled; mV2SigningEnabled = v2SigningEnabled; mV3SigningEnabled = v3SigningEnabled; mV4SigningEnabled = v4SigningEnabled; + mAlignFileSize = alignFileSize; + mVerityEnabled = verityEnabled; mV4ErrorReportingEnabled = v4ErrorReportingEnabled; mDebuggableApkPermitted = debuggableApkPermitted; mOtherSignersSignaturesPreserved = otherSignersSignaturesPreserved; @@ -170,7 +197,7 @@ public class ApkSigner { */ public void sign() throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException, IllegalStateException { + SignatureException, IllegalStateException { Closeable in = null; DataSource inputApk; try { @@ -216,7 +243,7 @@ public class ApkSigner { private void sign(DataSource inputApk, DataSink outputApkOut, DataSource outputApkIn) throws IOException, ApkFormatException, NoSuchAlgorithmException, InvalidKeyException, - SignatureException { + SignatureException { // Step 1. Find input APK's main ZIP sections ApkUtils.ZipSections inputZipSections; try { @@ -272,9 +299,10 @@ public class ApkSigner { for (SignerConfig signerConfig : mSignerConfigs) { engineSignerConfigs.add( new DefaultApkSignerEngine.SignerConfig.Builder( - signerConfig.getName(), - signerConfig.getPrivateKey(), - signerConfig.getCertificates()) + signerConfig.getName(), + signerConfig.getPrivateKey(), + signerConfig.getCertificates(), + signerConfig.getDeterministicDsaSigning()) .build()); } DefaultApkSignerEngine.Builder signerEngineBuilder = @@ -282,19 +310,28 @@ public class ApkSigner { .setV1SigningEnabled(mV1SigningEnabled) .setV2SigningEnabled(mV2SigningEnabled) .setV3SigningEnabled(mV3SigningEnabled) + .setVerityEnabled(mVerityEnabled) .setDebuggableApkPermitted(mDebuggableApkPermitted) .setOtherSignersSignaturesPreserved(mOtherSignersSignaturesPreserved) - .setSigningCertificateLineage(mSigningCertificateLineage); + .setSigningCertificateLineage(mSigningCertificateLineage) + .setMinSdkVersionForRotation(mRotationMinSdkVersion) + .setRotationTargetsDevRelease(mRotationTargetsDevRelease); if (mCreatedBy != null) { signerEngineBuilder.setCreatedBy(mCreatedBy); } if (mSourceStampSignerConfig != null) { signerEngineBuilder.setStampSignerConfig( new DefaultApkSignerEngine.SignerConfig.Builder( - mSourceStampSignerConfig.getName(), - mSourceStampSignerConfig.getPrivateKey(), - mSourceStampSignerConfig.getCertificates()) + mSourceStampSignerConfig.getName(), + mSourceStampSignerConfig.getPrivateKey(), + mSourceStampSignerConfig.getCertificates(), + mSourceStampSignerConfig.getDeterministicDsaSigning()) .build()); + signerEngineBuilder.setSourceStampTimestampEnabled(mSourceStampTimestampEnabled); + } + if (mSourceStampSigningCertificateLineage != null) { + signerEngineBuilder.setSourceStampSigningCertificateLineage( + mSourceStampSigningCertificateLineage); } signerEngine = signerEngineBuilder.build(); } @@ -317,6 +354,7 @@ public class ApkSigner { int lastModifiedTimeForNewEntries = -1; long inputOffset = 0; long outputOffset = 0; + byte[] sourceStampCertificateDigest = null; Map<String, CentralDirectoryRecord> outputCdRecordsByName = new HashMap<>(inputCdRecords.size()); for (final CentralDirectoryRecord inputCdRecord : inputCdRecordsSortedByLfhOffset) { @@ -324,6 +362,16 @@ public class ApkSigner { if (Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME.equals(entryName)) { continue; // We'll re-add below if needed. } + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals(entryName)) { + try { + sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + inputApkLfhSection, inputCdRecord, inputApkLfhSection.size()); + } catch (ZipFormatException ex) { + throw new ApkFormatException("Bad source stamp entry"); + } + continue; // Existing source stamp is handled below as needed. + } ApkSignerEngine.InputJarEntryInstructions entryInstructions = signerEngine.inputJarEntry(entryName); boolean shouldOutput; @@ -375,7 +423,7 @@ public class ApkSigner { if ((lastModifiedDateForNewEntries == -1) || (lastModifiedDate > lastModifiedDateForNewEntries) || ((lastModifiedDate == lastModifiedDateForNewEntries) - && (lastModifiedTime > lastModifiedTimeForNewEntries))) { + && (lastModifiedTime > lastModifiedTimeForNewEntries))) { lastModifiedDateForNewEntries = lastModifiedDate; lastModifiedTimeForNewEntries = lastModifiedTime; } @@ -462,15 +510,48 @@ public class ApkSigner { // records. if (signerEngine.isEligibleForSourceStamp()) { byte[] uncompressedData = signerEngine.generateSourceStampCertificateDigest(); + if (mForceSourceStampOverwrite + || sourceStampCertificateDigest == null + || Arrays.equals(uncompressedData, sourceStampCertificateDigest)) { + outputOffset += + outputDataToOutputApk( + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); + } else { + throw new ApkFormatException( + String.format( + "Cannot generate SourceStamp. APK contains an existing entry with" + + " the name: %s, and it is different than the provided source" + + " stamp certificate", + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME)); + } + } + + // Step 7.5. Generate pinlist.meta file if necessary. + // This has to be before the step 8 so that the file is signed. + if (pinByteRanges != null) { + // Covers JAR signature and zip central dir entry. + // The signature files don't have to be pinned, but pinning them isn't that wasteful + // since the total size is small. + pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); + String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; + byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); + + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); outputOffset += - outputDataToOutputApk( - SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME, - uncompressedData, - outputOffset, - outputCdRecords, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - outputApkOut); + outputDataToOutputApk( + entryName, + uncompressedData, + outputOffset, + outputCdRecords, + lastModifiedTimeForNewEntries, + lastModifiedDateForNewEntries, + outputApkOut); } // Step 8. Generate and output JAR signatures, if necessary. This may output more Local File @@ -483,15 +564,7 @@ public class ApkSigner { String entryName = entry.getName(); byte[] uncompressedData = entry.getData(); - ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = - signerEngine.outputJarEntry(entryName); - if (inspectEntryRequest != null) { - inspectEntryRequest - .getDataSink() - .consume(uncompressedData, 0, uncompressedData.length); - inspectEntryRequest.done(); - } - + requestOutputEntryInspection(signerEngine, entryName, uncompressedData); outputOffset += outputDataToOutputApk( entryName, @@ -505,21 +578,6 @@ public class ApkSigner { outputJarSignatureRequest.done(); } - if (pinByteRanges != null) { - pinByteRanges.add(new Hints.ByteRange(outputOffset, Long.MAX_VALUE)); // central dir - String entryName = Hints.PIN_BYTE_RANGE_ZIP_ENTRY_NAME; - byte[] uncompressedData = Hints.encodeByteRangeList(pinByteRanges); - outputOffset += - outputDataToOutputApk( - entryName, - uncompressedData, - outputOffset, - outputCdRecords, - lastModifiedTimeForNewEntries, - lastModifiedDateForNewEntries, - outputApkOut); - } - // Step 9. Construct output ZIP Central Directory in an in-memory buffer long outputCentralDirSizeBytes = 0; for (CentralDirectoryRecord record : outputCdRecords) { @@ -541,6 +599,9 @@ public class ApkSigner { int outputCentralDirRecordCount = outputCdRecords.size(); // Step 10. Construct output ZIP End of Central Directory record in an in-memory buffer + // because it can be adjusted in Step 11 due to signing block. + // - CD offset (it's shifted by signing block) + // - Comments (when the output file needs to be sized 4k-aligned) ByteBuffer outputEocd = EocdRecord.createWithModifiedCentralDirectoryInfo( inputZipSections.getZipEndOfCentralDirectory(), @@ -559,13 +620,39 @@ public class ApkSigner { if (outputApkSigningBlockRequest != null) { int padding = outputApkSigningBlockRequest.getPaddingSizeBeforeApkSigningBlock(); - outputApkOut.consume(ByteBuffer.allocate(padding)); byte[] outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + + long fileSize = + outputCentralDirStartOffset + + outputCentralDirDataSource.size() + + padding + + outputApkSigningBlock.length + + outputEocd.remaining(); + if (mAlignFileSize && (fileSize % ANDROID_FILE_ALIGNMENT_BYTES != 0)) { + int eocdPadding = + (int) + (ANDROID_FILE_ALIGNMENT_BYTES + - fileSize % ANDROID_FILE_ALIGNMENT_BYTES); + // Replace EOCD with padding one so that output file size can be the multiples of + // alignment. + outputEocd = EocdRecord.createWithPaddedComment(outputEocd, eocdPadding); + + // Since EoCD has changed, we need to regenerate signing block as well. + outputApkSigningBlockRequest = + signerEngine.outputZipSections2( + outputApkIn, + new ByteBufferDataSource(outputCentralDir), + DataSources.asDataSource(outputEocd)); + outputApkSigningBlock = outputApkSigningBlockRequest.getApkSigningBlock(); + outputApkSigningBlockRequest.done(); + } + + outputApkOut.consume(ByteBuffer.allocate(padding)); outputApkOut.consume(outputApkSigningBlock, 0, outputApkSigningBlock.length); ZipUtils.setZipEocdCentralDirectoryOffset( outputEocd, outputCentralDirStartOffset + padding + outputApkSigningBlock.length); - outputApkSigningBlockRequest.done(); } // Step 12. Output ZIP Central Directory and ZIP End of Central Directory @@ -579,6 +666,20 @@ public class ApkSigner { } } + private static void requestOutputEntryInspection( + ApkSignerEngine signerEngine, + String entryName, + byte[] uncompressedData) + throws IOException { + ApkSignerEngine.InspectJarEntryRequest inspectEntryRequest = + signerEngine.outputJarEntry(entryName); + if (inspectEntryRequest != null) { + inspectEntryRequest.getDataSink().consume( + uncompressedData, 0, uncompressedData.length); + inspectEntryRequest.done(); + } + } + private static long outputDataToOutputApk( String entryName, byte[] uncompressedData, @@ -651,7 +752,7 @@ public class ApkSigner { int dataAlignmentMultiple = getInputJarEntryDataAlignmentMultiple(inputRecord); if ((dataAlignmentMultiple <= 1) || ((inputOffset % dataAlignmentMultiple) - == (outputOffset % dataAlignmentMultiple))) { + == (outputOffset % dataAlignmentMultiple))) { // This record's data will be aligned same as in the input APK. return new OutputSizeAndDataOffset( inputRecord.outputRecord(inputLfhSection, outputLfhSection), @@ -917,14 +1018,18 @@ public class ApkSigner { private final String mName; private final PrivateKey mPrivateKey; private final List<X509Certificate> mCertificates; + private boolean mDeterministicDsaSigning; private SignerConfig( - String name, PrivateKey privateKey, List<X509Certificate> certificates) { + String name, + PrivateKey privateKey, + List<X509Certificate> certificates, + boolean deterministicDsaSigning) { mName = name; mPrivateKey = privateKey; mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + mDeterministicDsaSigning = deterministicDsaSigning; } - /** Returns the name of this signer. */ public String getName() { return mName; @@ -943,11 +1048,36 @@ public class ApkSigner { return mCertificates; } + + /** + * If this signer is a DSA signer, whether or not the signing is done deterministically. + */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + /** Builder of {@link SignerConfig} instances. */ public static class Builder { private final String mName; private final PrivateKey mPrivateKey; private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + */ + public Builder( + String name, + PrivateKey privateKey, + List<X509Certificate> certificates) { + this(name, privateKey, certificates, false); + } /** * Constructs a new {@code Builder}. @@ -957,14 +1087,21 @@ public class ApkSigner { * @param privateKey signing key * @param certificates list of one or more X.509 certificates. The subject public key of * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic variant (RFC6979) should be used. */ - public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) { + public Builder( + String name, + PrivateKey privateKey, + List<X509Certificate> certificates, + boolean deterministicDsaSigning) { if (name.isEmpty()) { throw new IllegalArgumentException("Empty name"); } mName = name; mPrivateKey = privateKey; mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; } /** @@ -972,7 +1109,8 @@ public class ApkSigner { * this builder. */ public SignerConfig build() { - return new SignerConfig(mName, mPrivateKey, mCertificates); + return new SignerConfig(mName, mPrivateKey, mCertificates, + mDeterministicDsaSigning); } } } @@ -992,15 +1130,22 @@ public class ApkSigner { public static class Builder { private final List<SignerConfig> mSignerConfigs; private SignerConfig mSourceStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; + private boolean mForceSourceStampOverwrite = false; + private boolean mSourceStampTimestampEnabled = true; private boolean mV1SigningEnabled = true; private boolean mV2SigningEnabled = true; private boolean mV3SigningEnabled = true; private boolean mV4SigningEnabled = false; + private boolean mAlignFileSize = false; + private boolean mVerityEnabled = false; private boolean mV4ErrorReportingEnabled = false; private boolean mDebuggableApkPermitted = true; private boolean mOtherSignersSignaturesPreserved; private String mCreatedBy; private Integer mMinSdkVersion; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; private final ApkSignerEngine mSignerEngine; @@ -1069,6 +1214,35 @@ 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 + */ + public Builder setForceSourceStampOverwrite(boolean force) { + mForceSourceStampOverwrite = force; + return this; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** * Sets the APK to be signed. * * @see #setInputApk(DataSource) @@ -1189,6 +1363,58 @@ public class ApkSigner { } /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. This method is useful for overriding the + * default behavior where Android T is set as the minimum API level for rotation. + * + * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + checkInitializedWithoutEngine(); + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + checkInitializedWithoutEngine(); + mRotationTargetsDevRelease = enabled; + return this; + } + + /** * Sets whether the APK should be signed using JAR signing (aka v1 signature scheme). * * <p>By default, whether APK is signed using JAR signing is determined by {@code @@ -1273,7 +1499,7 @@ public class ApkSigner { * <p>V4 signing requires that the APK be v2 or v3 signed. * * @param enabled {@code true} to require the APK to be signed using APK Signature Scheme v2 - * or v3 and generate an v4 signature file + * or v3 and generate an v4 signature file */ public Builder setV4SigningEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1291,7 +1517,7 @@ public class ApkSigner { * the user did not explicitly request the v4 signing. * * @param enabled {@code false} to prevent errors encountered during the V4 signing from - * halting the signing process + * halting the signing process */ public Builder setV4ErrorReportingEnabled(boolean enabled) { checkInitializedWithoutEngine(); @@ -1299,6 +1525,34 @@ public class ApkSigner { return this; } + /** + * Sets whether the output APK files should be sized as multiples of 4K. + * + * <p><em>Note:</em> This method may only be invoked when this builder is not initialized + * with an {@link ApkSignerEngine}. + * + * @throws IllegalStateException if this builder was initialized with an {@link + * ApkSignerEngine} + */ + public Builder setAlignFileSize(boolean alignFileSize) { + checkInitializedWithoutEngine(); + mAlignFileSize = alignFileSize; + return this; + } + + /** + * Sets whether to enable the verity signature algorithm for the v2 and v3 signature + * schemes. + * + * @param enabled {@code true} to enable the verity signature algorithm for inclusion in the + * v2 and v3 signature blocks. + */ + public Builder setVerityEnabled(boolean enabled) { + checkInitializedWithoutEngine(); + mVerityEnabled = enabled; + return this; + } + /** * Sets whether the APK should be signed even if it is marked as debuggable ({@code * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward @@ -1400,8 +1654,8 @@ public class ApkSigner { mV4SigningEnabled = false; } else { throw new IllegalStateException( - "APK Signature Scheme v4 signing requires at least " - + "v2 or v3 signing to be enabled"); + "APK Signature Scheme v4 signing requires at least " + + "v2 or v3 signing to be enabled"); } } @@ -1410,11 +1664,18 @@ public class ApkSigner { return new ApkSigner( mSignerConfigs, mSourceStampSignerConfig, + mSourceStampSigningCertificateLineage, + mForceSourceStampOverwrite, + mSourceStampTimestampEnabled, mMinSdkVersion, + mRotationMinSdkVersion, + mRotationTargetsDevRelease, mV1SigningEnabled, mV2SigningEnabled, mV3SigningEnabled, mV4SigningEnabled, + mAlignFileSize, + mVerityEnabled, mV4ErrorReportingEnabled, mDebuggableApkPermitted, mOtherSignersSignaturesPreserved, 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..fa2b7aa --- /dev/null +++ b/src/main/java/com/android/apksig/ApkVerificationIssue.java @@ -0,0 +1,173 @@ +/* + * 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; + /** The source stamp timestamp attribute has an invalid value. */ + public static final int SOURCE_STAMP_INVALID_TIMESTAMP = 38; + + 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 9275290..4d92638 100644 --- a/src/main/java/com/android/apksig/ApkVerifier.java +++ b/src/main/java/com/android/apksig/ApkVerifier.java @@ -17,16 +17,30 @@ 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.V1SchemeConstants.MANIFEST_ENTRY_NAME; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; 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.stamp.SourceStampVerifier; +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; @@ -48,6 +62,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; @@ -65,12 +80,15 @@ import java.util.Set; * * @see <a href="https://source.android.com/security/apksigning/index.html">Application Signing</a> */ +// This class is not used in any Android apps; apksig is only used in Android for source stamp +// verification provided by the apksig-stamp-verifier target. +@SuppressWarnings("AndroidJdkLibsChecker") 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"); @@ -111,12 +129,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 { @@ -146,25 +164,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; @@ -176,26 +182,11 @@ 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<ContentDigestAlgorithm, byte[]> apkContentDigests = new HashMap<>(); + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests = + new HashMap<>(); // The SUPPORTED_APK_SIG_SCHEME_NAMES contains the mapping from version number to scheme // name, but the verifiers use this parameter as the schemes supported by the target SDK @@ -206,41 +197,66 @@ 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. Set<Integer> foundApkSigSchemeIds = new HashSet<>(2); if (maxSdkVersion >= AndroidSdkVersion.N) { RunnablesExecutor executor = RunnablesExecutor.SINGLE_THREADED; + // Android T and newer attempts to verify APKs using APK Signature Scheme V3.1. v3.0 + // also includes stripping protection for the minimum SDK version on which the rotated + // signing key should be used. + int rotationMinSdkVersion = 0; + if (maxSdkVersion >= MIN_SDK_WITH_V31_SUPPORT) { + try { + ApkSigningBlockUtils.Result v31Result = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, MIN_SDK_WITH_V31_SUPPORT), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .build() + .verify(); + foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31); + rotationMinSdkVersion = v31Result.signers.stream().mapToInt( + signer -> signer.minSdkVersion).min().orElse(0); + result.mergeFrom(v31Result); + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31, + getApkContentDigestsFromSigningSchemeResult(v31Result)); + } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // v3.1 signature not required + } + if (result.containsErrors()) { + return result; + } + } // Android P and newer attempts to verify APKs using APK Signature Scheme v3 - if (maxSdkVersion >= AndroidSdkVersion.P) { + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT || foundApkSigSchemeIds.isEmpty()) { try { - ApkSigningBlockUtils.Result v3Result = - V3SchemeVerifier.verify( - executor, - apk, - zipSections, - Math.max(minSdkVersion, AndroidSdkVersion.P), - maxSdkVersion); + V3SchemeVerifier.Builder builder = new V3SchemeVerifier.Builder(apk, + zipSections, Math.max(minSdkVersion, AndroidSdkVersion.P), + maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (rotationMinSdkVersion > 0) { + builder.setRotationMinSdkVersion(rotationMinSdkVersion); + } + ApkSigningBlockUtils.Result v3Result = builder.build().verify(); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); result.mergeFrom(v3Result); - if (apkContentDigests.isEmpty()) { - apkContentDigests.putAll( - getApkContentDigestsFromSigningSchemeResult(v3Result)); - } + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3, + getApkContentDigestsFromSigningSchemeResult(v3Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { - // v3 signature not required + // v3 signature not required unless a v3.1 signature was found as a v3.1 + // signature is intended to support key rotation on T+ with the v3 signature + // containing the original signing key. + if (foundApkSigSchemeIds.contains( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31)) { + result.addError(Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK); + } } if (result.containsErrors()) { return result; @@ -264,10 +280,9 @@ public class ApkVerifier { maxSdkVersion); foundApkSigSchemeIds.add(ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); result.mergeFrom(v2Result); - if (apkContentDigests.isEmpty()) { - apkContentDigests.putAll( - getApkContentDigestsFromSigningSchemeResult(v2Result)); - } + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2, + getApkContentDigestsFromSigningSchemeResult(v2Result)); } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { // v2 signature not required } @@ -276,46 +291,6 @@ public class ApkVerifier { } } - if (maxSdkVersion >= AndroidSdkVersion.R) { - try { - 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 SourceStamp file is found inside the APK, there must be a SourceStamp - // block in the APK signing block as well. - if (sourceStampCdRecord != null) { - byte[] sourceStampCertificateDigest = - LocalFileRecord.getUncompressedData( - apk, - sourceStampCdRecord, - zipSections.getZipCentralDirectoryOffset()); - ApkSigningBlockUtils.Result sourceStampResult = - SourceStampVerifier.verify( - apk, - zipSections, - sourceStampCertificateDigest, - apkContentDigests, - Math.max(minSdkVersion, AndroidSdkVersion.R), - maxSdkVersion); - result.mergeFrom(sourceStampResult); - } - } catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { - result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); - } catch (ZipFormatException e) { - throw new ApkFormatException("Failed to read APK", e); - } - if (result.containsErrors()) { - return result; - } - } - // If v4 file is specified, use additional verification on it if (mV4SignatureFile != null) { final ApkSigningBlockUtils.Result v4Result = @@ -346,6 +321,9 @@ public class ApkVerifier { } } + List<CentralDirectoryRecord> cdRecords = + V1SchemeVerifier.parseZipCentralDirectory(apk, zipSections); + // Attempt to verify the APK using JAR signing if necessary. Platforms prior to Android N // ignore APK Signature Scheme v2 signatures and always attempt to verify JAR signatures. // Android N onwards verifies JAR signatures only if no APK Signature Scheme v2 (or newer @@ -360,6 +338,46 @@ public class ApkVerifier { minSdkVersion, maxSdkVersion); result.mergeFrom(v1Result); + signatureSchemeApkContentDigests.put( + ApkSigningBlockUtils.VERSION_JAR_SIGNATURE_SCHEME, + getApkContentDigestFromV1SigningScheme(cdRecords, apk, zipSections)); + } + if (result.containsErrors()) { + return result; + } + + // Verify the SourceStamp, if found in the APK. + try { + CentralDirectoryRecord sourceStampCdRecord = null; + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME.equals( + cdRecord.getName())) { + sourceStampCdRecord = cdRecord; + break; + } + } + // If SourceStamp file is found inside the APK, there must be a SourceStamp + // block in the APK signing block as well. + if (sourceStampCdRecord != null) { + byte[] sourceStampCertificateDigest = + LocalFileRecord.getUncompressedData( + apk, + sourceStampCdRecord, + zipSections.getZipCentralDirectoryOffset()); + ApkSigResult sourceStampResult = + V2SourceStampVerifier.verify( + apk, + zipSections, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + Math.max(minSdkVersion, AndroidSdkVersion.R), + maxSdkVersion); + result.mergeFrom(sourceStampResult); + } + } catch (SignatureNotFoundException ignored) { + result.addWarning(Issue.SOURCE_STAMP_SIG_MISSING); + } catch (ZipFormatException e) { + throw new ApkFormatException("Failed to read APK", e); } if (result.containsErrors()) { return result; @@ -446,7 +464,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) { @@ -475,9 +493,6 @@ public class ApkVerifier { // The apkDigest field in the v4 signature should match the selected v2/v3. if (result.isVerifiedUsingV4Scheme()) { List<Result.V4SchemeSignerInfo> v4Signers = result.getV4SchemeSigners(); - if (v4Signers.size() != 1) { - result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); - } List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> digestsFromV4 = v4Signers.get(0).getContentDigests(); @@ -487,21 +502,22 @@ public class ApkVerifier { final byte[] digestFromV4 = digestsFromV4.get(0).getValue(); if (result.isVerifiedUsingV3Scheme()) { - List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners(); - if (v3Signers.size() != 1) { + int expectedSize = result.isVerifiedUsingV31Scheme() ? 2 : 1; + if (v4Signers.size() != expectedSize) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); } - // Compare certificates. - checkV4Certificate(v4Signers.get(0).mCerts, v3Signers.get(0).mCerts, result); - - // Compare digests. - final byte[] digestFromV3 = pickBestDigestForV4( - v3Signers.get(0).getContentDigests()); - if (!Arrays.equals(digestFromV4, digestFromV3)) { - result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); + checkV4Signer(result.getV3SchemeSigners(), v4Signers.get(0).mCerts, digestFromV4, + result); + if (result.isVerifiedUsingV31Scheme()) { + checkV4Signer(result.getV31SchemeSigners(), v4Signers.get(1).mCerts, + digestFromV4, result); } } else if (result.isVerifiedUsingV2Scheme()) { + if (v4Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + List<Result.V2SchemeSignerInfo> v2Signers = result.getV2SchemeSigners(); if (v2Signers.size() != 1) { result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); @@ -521,13 +537,53 @@ 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. + 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() || result.isVerifiedUsingV31Scheme()) { + break; + } + result.addError(Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET, + targetSdkVersion, + minSchemeVersion); + } + } + } + if (result.containsErrors()) { return result; } // Verified result.setVerified(); - if (result.isVerifiedUsingV3Scheme()) { + if (result.isVerifiedUsingV31Scheme()) { + List<Result.V3SchemeSignerInfo> v31Signers = result.getV31SchemeSigners(); + result.addSignerCertificate(v31Signers.get(v31Signers.size() - 1).getCertificate()); + } else if (result.isVerifiedUsingV3Scheme()) { List<Result.V3SchemeSignerInfo> v3Signers = result.getV3SchemeSigners(); result.addSignerCertificate(v3Signers.get(v3Signers.size() - 1).getCertificate()); } else if (result.isVerifiedUsingV2Scheme()) { @@ -546,7 +602,357 @@ 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 checkV4Signer(List<Result.V3SchemeSignerInfo> v3Signers, + List<X509Certificate> v4Certs, byte[] digestFromV4, Result result) { + if (v3Signers.size() != 1) { + result.addError(Issue.V4_SIG_MULTIPLE_SIGNERS); + } + + // Compare certificates. + checkV4Certificate(v4Certs, v3Signers.get(0).mCerts, result); + + // Compare digests. + final byte[] digestFromV3 = pickBestDigestForV4(v3Signers.get(0).getContentDigests()); + if (!Arrays.equals(digestFromV4, digestFromV3)) { + result.addError(Issue.V4_SIG_V2_V3_DIGESTS_MISMATCH); + } + } + + 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(); @@ -558,7 +964,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); @@ -573,7 +980,41 @@ public class ApkVerifier { return apkContentDigests; } - private static void collectApkContentDigests(List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> contentDigests, Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + private static Map<ContentDigestAlgorithm, byte[]> getApkContentDigestFromV1SigningScheme( + List<CentralDirectoryRecord> cdRecords, + DataSource apk, + ApkUtils.ZipSections zipSections) + throws IOException, ApkFormatException { + CentralDirectoryRecord manifestCdRecord = null; + Map<ContentDigestAlgorithm, byte[]> v1ContentDigest = new EnumMap<>( + ContentDigestAlgorithm.class); + for (CentralDirectoryRecord cdRecord : cdRecords) { + if (MANIFEST_ENTRY_NAME.equals(cdRecord.getName())) { + manifestCdRecord = cdRecord; + break; + } + } + 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; + } + 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); + } + } + + 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()); @@ -589,7 +1030,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 { @@ -601,67 +1042,11 @@ 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; - - /** - * 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 the value of the android:targetSandboxVersion attribute of the top-level manifest - // element - try { - AndroidBinXmlParser parser = new AndroidBinXmlParser(androidManifestContents); - int eventType = parser.getEventType(); - while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { - if ((eventType == AndroidBinXmlParser.EVENT_START_ELEMENT) - && (parser.getDepth() == 1) - && ("manifest".equals(parser.getName())) - && (parser.getNamespace().isEmpty())) { - // In each manifest element, targetSandboxVersion defaults to 1 - int result = 1; - for (int i = 0; i < parser.getAttributeCount(); i++) { - if (parser.getAttributeNameResourceId(i) - == TARGET_SANDBOX_VERSION_ATTR_ID) { - 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 target sandbox version" - + ": unsupported value type of" - + " AndroidManifest.xml" - + " android:targetSandboxVersion" - + ". Only integer values supported."); - } - break; - } - } - return result; - } - eventType = parser.next(); - } - throw new ApkFormatException( - "Failed to determine APK's target sandbox version" - + " : no manifest element in AndroidManifest.xml"); - } catch (AndroidBinXmlParser.XmlParserException e) { - throw new ApkFormatException( - "Failed to determine APK's target sandbox version" - + ": malformed AndroidManifest.xml", - e); + private static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdkVersion) { + if (targetSdkVersion >= AndroidSdkVersion.R) { + return VERSION_APK_SIGNATURE_SCHEME_V2; } + return VERSION_JAR_SIGNATURE_SCHEME; } /** @@ -676,6 +1061,7 @@ public class ApkVerifier { private final List<V1SchemeSignerInfo> mV1SchemeIgnoredSigners = new ArrayList<>(); private final List<V2SchemeSignerInfo> mV2SchemeSigners = new ArrayList<>(); private final List<V3SchemeSignerInfo> mV3SchemeSigners = new ArrayList<>(); + private final List<V3SchemeSignerInfo> mV31SchemeSigners = new ArrayList<>(); private final List<V4SchemeSignerInfo> mV4SchemeSigners = new ArrayList<>(); private SourceStampInfo mSourceStampInfo; @@ -683,8 +1069,10 @@ public class ApkVerifier { private boolean mVerifiedUsingV1Scheme; private boolean mVerifiedUsingV2Scheme; private boolean mVerifiedUsingV3Scheme; + private boolean mVerifiedUsingV31Scheme; private boolean mVerifiedUsingV4Scheme; private boolean mSourceStampVerified; + private boolean mWarningsAsErrors; private SigningCertificateLineage mSigningCertificateLineage; /** @@ -720,6 +1108,13 @@ public class ApkVerifier { } /** + * Returns {@code true} if the APK's APK Signature Scheme v3.1 signature verified. + */ + public boolean isVerifiedUsingV31Scheme() { + return mVerifiedUsingV31Scheme; + } + + /** * Returns {@code true} if the APK's APK Signature Scheme v4 signature verified. */ public boolean isVerifiedUsingV4Scheme() { @@ -785,7 +1180,23 @@ public class ApkVerifier { return mV3SchemeSigners; } - private List<V4SchemeSignerInfo> getV4SchemeSigners() { + /** + * Returns information about APK Signature Scheme v3.1 signers associated with the APK's + * signature. + * + * <note> Multiple signers represent different targeted platform versions, not + * a signing identity of multiple signers. APK Signature Scheme v3.1 only supports single + * signer identities.</note> + */ + public List<V3SchemeSignerInfo> getV31SchemeSigners() { + return mV31SchemeSigners; + } + + /** + * Returns information about APK Signature Scheme v4 signers associated with the APK's + * signature. + */ + public List<V4SchemeSignerInfo> getV4SchemeSigners() { return mV4SchemeSigners; } @@ -813,10 +1224,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; + } } /** @@ -838,6 +1263,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: @@ -851,6 +1291,16 @@ public class ApkVerifier { for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { mV3SchemeSigners.add(new V3SchemeSignerInfo(signer)); } + // Do not overwrite a previously set lineage from a v3.1 signing block. + if (mSigningCertificateLineage == null) { + mSigningCertificateLineage = source.signingCertificateLineage; + } + break; + case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31: + mVerifiedUsingV31Scheme = source.verified; + for (ApkSigningBlockUtils.Result.SignerInfo signer : source.signers) { + mV31SchemeSigners.add(new V3SchemeSignerInfo(signer)); + } mSigningCertificateLineage = source.signingCertificateLineage; break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4: @@ -868,8 +1318,6 @@ public class ApkVerifier { default: throw new IllegalArgumentException("Unknown Signing Block Scheme Id"); } - mErrors.addAll(source.getErrors()); - mWarnings.addAll(source.getWarnings()); } /** @@ -880,11 +1328,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()) { @@ -892,6 +1346,9 @@ public class ApkVerifier { if (signer.containsErrors()) { return true; } + if (mWarningsAsErrors && !signer.getWarnings().isEmpty()) { + return true; + } } } if (!mV3SchemeSigners.isEmpty()) { @@ -899,16 +1356,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 { @@ -1080,6 +1588,9 @@ public class ApkVerifier { private final List<IssueWithParams> mWarnings; private final List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> mContentDigests; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final boolean mRotationTargetsDevRelease; private V3SchemeSignerInfo(ApkSigningBlockUtils.Result.SignerInfo result) { mIndex = result.index; @@ -1087,6 +1598,11 @@ public class ApkVerifier { mErrors = result.getErrors(); mWarnings = result.getWarnings(); mContentDigests = result.contentDigests; + mMinSdkVersion = result.minSdkVersion; + mMaxSdkVersion = result.maxSdkVersion; + mRotationTargetsDevRelease = result.additionalAttributes.stream().mapToInt( + attribute -> attribute.getId()).anyMatch( + attrId -> attrId == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); } /** @@ -1132,6 +1648,33 @@ public class ApkVerifier { public List<ApkSigningBlockUtils.Result.SignerInfo.ContentDigest> getContentDigests() { return mContentDigests; } + + /** + * Returns the minimum SDK version on which this signer should be verified. + */ + public int getMinSdkVersion() { + return mMinSdkVersion; + } + + /** + * Returns the maximum SDK version on which this signer should be verified. + */ + public int getMaxSdkVersion() { + return mMaxSdkVersion; + } + + /** + * Returns whether rotation is targeting a development release. + * + * <p>A development release uses the SDK version of the previously released platform + * until the SDK of the development release is finalized. To allow rotation to target + * a development release after T, this attribute must be set to ensure rotation is + * used on the development release but ignored on the released platform with the same + * API level. + */ + public boolean getRotationTargetsDevRelease() { + return mRotationTargetsDevRelease; + } } /** @@ -1204,15 +1747,58 @@ 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 final List<IssueWithParams> mInfoMessages; + + private final SourceStampVerificationStatus mSourceStampVerificationStatus; - private SourceStampInfo(ApkSigningBlockUtils.Result.SignerInfo result) { + private final long mTimestamp; + + 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()); + mInfoMessages = ApkVerificationIssueAdapter.getIssuesFromVerificationIssues( + result.getInfoMessages()); + if (mErrors.isEmpty() && mWarnings.isEmpty()) { + mSourceStampVerificationStatus = SourceStampVerificationStatus.STAMP_VERIFIED; + } else { + mSourceStampVerificationStatus = + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED; + } + mTimestamp = result.timestamp; + } + + SourceStampInfo(SourceStampVerificationStatus sourceStampVerificationStatus) { + mCertificates = Collections.emptyList(); + mCertificateLineage = Collections.emptyList(); + mErrors = Collections.emptyList(); + mWarnings = Collections.emptyList(); + mInfoMessages = Collections.emptyList(); + mSourceStampVerificationStatus = sourceStampVerificationStatus; + mTimestamp = 0; } /** @@ -1226,10 +1812,25 @@ 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(); } + /** + * Returns {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.isEmpty(); + } + public List<IssueWithParams> getErrors() { return mErrors; } @@ -1237,6 +1838,30 @@ public class ApkVerifier { public List<IssueWithParams> getWarnings() { return mWarnings; } + + /** + * Returns a {@code List} of {@link IssueWithParams} representing info messages + * that were encountered during verification of the source stamp. + */ + public List<IssueWithParams> getInfoMessages() { + return mInfoMessages; + } + + /** + * 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; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } } } @@ -1583,6 +2208,19 @@ public class ApkVerifier { + " %1$d"), /** + * APK is targeting an SDK version that requires a minimum signature scheme version, but the + * APK is not signed with that version or later. + * + * <ul> + * <li>Parameter 1: target SDK Version (@code Integer})</li> + * <li>Parameter 2: minimum signature scheme version ((@code Integer})</li> + * </ul> + */ + MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET( + "Target SDK version %1$d requires a minimum of signature scheme v%2$d; the APK is" + + " not signed with this or a later signature scheme"), + + /** * APK which is both JAR-signed and signed using APK Signature Scheme v2 contains a JAR * signature from this signer, but does not contain an APK Signature Scheme v2 signature * from this signer. @@ -2047,6 +2685,61 @@ public class ApkVerifier { + " using APK Signature Scheme v3 are not all a part of the same overall lineage."), /** + * The v3 stripping protection attribute for rotation is present, but a v3.1 signing block + * was not found. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + * </ul> + */ + V31_BLOCK_MISSING( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but a v3.1 block was not found"), + + /** + * The v3 stripping protection attribute for rotation does not match the minimum SDK version + * targeting rotation in the v3.1 signer block. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from attribute ({@code Integer}) + * <li>Parameter 2: min SDK version supporting rotation from v3.1 block ({@code Integer}) + * </ul> + */ + V31_ROTATION_MIN_SDK_MISMATCH( + "The v3 signer indicates key rotation should be supported starting from SDK " + + "version %1$s, but the v3.1 block targets %2$s for rotation"), + + /** + * The APK supports key rotation with SDK version targeting using v3.1, but the rotation min + * SDK version stripping protection attribute was not written to the v3 signer. + * + * <ul> + * <li>Parameter 1: min SDK version supporting rotation from v3.1 block ({@code Integer}) + * </ul> + */ + V31_ROTATION_MIN_SDK_ATTR_MISSING( + "APK supports key rotation starting from SDK version %1$s, but the v3 signer does" + + " not contain the attribute to detect if this signature is stripped"), + + /** + * The APK contains a v3.1 signing block without a v3.0 block. The v3.1 block should only + * be used for targeting rotation for a later SDK version; if an APK's minSdkVersion is the + * same as the SDK version for rotation then this should be written to a v3.0 block. + */ + V31_BLOCK_FOUND_WITHOUT_V3_BLOCK( + "The APK contains a v3.1 signing block without a v3.0 base block"), + + /** + * The APK contains a v3.0 signing block with a rotation-targets-dev-release attribute in + * the signer; this attribute is only intended for v3.1 signers to indicate they should be + * targeting the next development release that is using the SDK version of the previously + * released platform SDK version. + */ + V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER( + "The rotation-targets-dev-release attribute is only supported on v3.1 signers; " + + "this attribute will be ignored by the platform in a v3.0 signer"), + + /** * APK Signing Block contains an unknown entry. * * <ul> @@ -2201,6 +2894,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"), @@ -2247,8 +2948,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 @@ -2263,7 +2972,97 @@ 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 source stamp timestamp attribute has an invalid value (<= 0). + * <ul> + * <li>Parameter 1: The invalid timestamp value. + * </ul> + */ + SOURCE_STAMP_INVALID_TIMESTAMP( + "The source stamp" + + " timestamp attribute has an invalid value: %1$d"), + + /** + * 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; @@ -2284,7 +3083,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; @@ -2293,6 +3092,7 @@ public class ApkVerifier { * parameters. */ public IssueWithParams(Issue issue, Object[] params) { + super(issue.mFormat, params); mIssue = issue; mParams = params; } @@ -2407,7 +3207,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) { @@ -2423,7 +3222,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) { @@ -2449,4 +3247,120 @@ 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); + sVerificationIssueIdToIssue.put(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + /** + * 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..f64064c --- /dev/null +++ b/src/main/java/com/android/apksig/Constants.java @@ -0,0 +1,55 @@ +/* + * 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_V31 = 31; + 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 APK_SIGNATURE_SCHEME_V31_BLOCK_ID = + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_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; + + public static final String OID_RSA_ENCRYPTION = "1.2.840.113549.1.1.1"; +} diff --git a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java index c89f4db..0525886 100644 --- a/src/main/java/com/android/apksig/DefaultApkSignerEngine.java +++ b/src/main/java/com/android/apksig/DefaultApkSignerEngine.java @@ -17,17 +17,26 @@ 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.internal.apk.ApkSigningBlockUtils.VERITY_PADDING_BLOCK_ID; +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.v3.V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT; +import static com.android.apksig.internal.apk.v3.V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT; 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.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; -import com.android.apksig.internal.apk.stamp.SourceStampSigner; +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; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; import com.android.apksig.internal.apk.v4.V4SchemeSigner; import com.android.apksig.internal.apk.v4.V4Signature; @@ -59,9 +68,10 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; /** @@ -89,14 +99,22 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private final boolean mV1SigningEnabled; private final boolean mV2SigningEnabled; private final boolean mV3SigningEnabled; + private final boolean mVerityEnabled; private final boolean mDebuggableApkPermitted; private final boolean mOtherSignersSignaturesPreserved; private final String mCreatedBy; private final List<SignerConfig> mSignerConfigs; private final SignerConfig mSourceStampSignerConfig; + private final SigningCertificateLineage mSourceStampSigningCertificateLineage; + private final boolean mSourceStampTimestampEnabled; + private final int mRotationMinSdkVersion; + private final boolean mRotationTargetsDevRelease; private final int mMinSdkVersion; private final SigningCertificateLineage mSigningCertificateLineage; + private List<byte[]> mPreservedV2Signers = Collections.emptyList(); + private List<Pair<byte[], Integer>> mPreservedSignatureBlocks = Collections.emptyList(); + private List<V1SchemeSigner.SignerConfig> mV1SignerConfigs = Collections.emptyList(); private DigestAlgorithm mV1ContentDigestAlgorithm; @@ -153,13 +171,33 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + /** + * A Set of block IDs to be discarded when requesting to preserve the original signatures. + */ + private static final Set<Integer> DISCARDED_SIGNATURE_BLOCK_IDS; + static { + DISCARDED_SIGNATURE_BLOCK_IDS = new HashSet<>(3); + // The verity padding block is recomputed on an + // ApkSigningBlockUtils.ANDROID_COMMON_PAGE_ALIGNMENT_BYTES boundary. + DISCARDED_SIGNATURE_BLOCK_IDS.add(VERITY_PADDING_BLOCK_ID); + // The source stamp block is not currently preserved; appending a new signature scheme + // block will invalidate the previous source stamp. + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V1_SOURCE_STAMP_BLOCK_ID); + DISCARDED_SIGNATURE_BLOCK_IDS.add(Constants.V2_SOURCE_STAMP_BLOCK_ID); + } + private DefaultApkSignerEngine( List<SignerConfig> signerConfigs, SignerConfig sourceStampSignerConfig, + SigningCertificateLineage sourceStampSigningCertificateLineage, + boolean sourceStampTimestampEnabled, int minSdkVersion, + int rotationMinSdkVersion, + boolean rotationTargetsDevRelease, boolean v1SigningEnabled, boolean v2SigningEnabled, boolean v3SigningEnabled, + boolean verityEnabled, boolean debuggableApkPermitted, boolean otherSignersSignaturesPreserved, String createdBy, @@ -168,14 +206,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { if (signerConfigs.isEmpty()) { throw new IllegalArgumentException("At least one signer config must be provided"); } - if (otherSignersSignaturesPreserved) { - throw new UnsupportedOperationException( - "Preserving other signer's signatures is not yet implemented"); - } mV1SigningEnabled = v1SigningEnabled; mV2SigningEnabled = v2SigningEnabled; mV3SigningEnabled = v3SigningEnabled; + mVerityEnabled = verityEnabled; mV1SignaturePending = v1SigningEnabled; mV2SignaturePending = v2SigningEnabled; mV3SignaturePending = v3SigningEnabled; @@ -184,7 +219,11 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { mCreatedBy = createdBy; mSignerConfigs = signerConfigs; mSourceStampSignerConfig = sourceStampSignerConfig; + mSourceStampSigningCertificateLineage = sourceStampSigningCertificateLineage; + mSourceStampTimestampEnabled = sourceStampTimestampEnabled; mMinSdkVersion = minSdkVersion; + mRotationMinSdkVersion = rotationMinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; mSigningCertificateLineage = signingCertificateLineage; if (v1SigningEnabled) { @@ -245,6 +284,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { v1SignerConfig.privateKey = signerConfig.getPrivateKey(); v1SignerConfig.certificates = certificates; v1SignerConfig.signatureDigestAlgorithm = v1SignatureDigestAlgorithm; + v1SignerConfig.deterministicDsaSigning = signerConfig.getDeterministicDsaSigning(); // For digesting contents of APK entries and of MANIFEST.MF, pick the algorithm // of comparable strength to the digest algorithm used for computing the signature. // When there are multiple signers, pick the strongest digest algorithm out of their @@ -301,12 +341,28 @@ 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 boolean signingLineageHas31Support() { + return mSigningCertificateLineage != null + && mRotationMinSdkVersion >= MIN_SDK_WITH_V31_SUPPORT + && mMinSdkVersion < mRotationMinSdkVersion; + } + + private List<ApkSigningBlockUtils.SignerConfig> processV3Configs( + List<ApkSigningBlockUtils.SignerConfig> rawConfigs) throws InvalidKeyException { + // While the V3 signature scheme supports rotation, it is possible for a caller to specify + // a minimum SDK version for rotation that is >= the first SDK version that supports V3.1; + // in this case the V3.1 signing block will contain the rotated key, and the V3.0 block + // will use the original signing key. + if (signingLineageHas31Support()) { + SigningCertificateLineage subLineage = mSigningCertificateLineage + .getSubLineage(mSignerConfigs.get(0).mCertificates.get(0)); + if (subLineage.size() != 1) { + throw new IllegalArgumentException( + "v3.1 signing enabled but the oldest signer in the SigningCertificateLineage" + + " for the v3.0 signing block is missing. Please provide" + + " the oldest signer to enable v3.1 signing."); + } + } List<ApkSigningBlockUtils.SignerConfig> processedConfigs = new ArrayList<>(); @@ -331,20 +387,43 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { // this needs to change config.maxSdkVersion = Integer.MAX_VALUE; } else { - // otherwise, we only want to use this signer up to the minimum platform version - // on which a newer one is acceptable - config.maxSdkVersion = currentMinSdk - 1; + if (mRotationTargetsDevRelease && currentMinSdk == mRotationMinSdkVersion) { + // The currentMinSdk is both the SDK version for the active development release + // as well as the most recent released platform. To ensure the v3.0 signer will + // target the released platform, overlap the maxSdkVersion for the v3.0 signer + // with the minSdkVersion of the rotated signer in the v3.1 block + config.maxSdkVersion = currentMinSdk; + } else { + // otherwise, we only want to use this signer up to the minimum platform version + // on which a newer one is acceptable + config.maxSdkVersion = currentMinSdk - 1; + } } config.minSdkVersion = getMinSdkFromV3SignatureAlgorithms(config.signatureAlgorithms); - if (mSigningCertificateLineage != null) { + // Only use a rotated key and signing lineage if the config's max SDK version is greater + // than that requested to support rotation. + if (mSigningCertificateLineage != null + && ((mRotationTargetsDevRelease + ? config.maxSdkVersion > mRotationMinSdkVersion + : config.maxSdkVersion >= mRotationMinSdkVersion))) { config.mSigningCertificateLineage = mSigningCertificateLineage.getSubLineage(config.certificates.get(0)); + if (config.minSdkVersion < mRotationMinSdkVersion) { + config.minSdkVersion = mRotationMinSdkVersion; + } } // we know that this config will be used, so add it to our result, order doesn't matter // at this point (and likely only one will be needed processedConfigs.add(config); currentMinSdk = config.minSdkVersion; - if (currentMinSdk <= mMinSdkVersion || currentMinSdk <= AndroidSdkVersion.P) { + // If the rotation is targeting a development release and this is the v3.1 signer, then + // the minSdkVersion of this signer should equal the maxSdkVersion of the next signer; + // this ensures a package with the minSdkVersion set to the mRotationMinSdkVersion has + // a v3.0 block with the min / max SDK version set to this same minSdkVersion from the + // v3.1 block. + if ((mRotationTargetsDevRelease && currentMinSdk < mMinSdkVersion) + || (!mRotationTargetsDevRelease && currentMinSdk <= mMinSdkVersion) + || currentMinSdk <= AndroidSdkVersion.P) { // this satisfies all we need, stop here break; } @@ -355,26 +434,65 @@ 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); - if (configs.size() != 1) { - throw new IllegalStateException("Only accepting one signer config for V4 Signature."); + private List<ApkSigningBlockUtils.SignerConfig> createV3SignerConfigs( + boolean apkSigningBlockPaddingSupported) throws InvalidKeyException { + return processV3Configs(createSigningBlockSignerConfigs(apkSigningBlockPaddingSupported, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3)); + } + + private List<ApkSigningBlockUtils.SignerConfig> processV31SignerConfigs( + List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs) { + // If the signing key has been rotated, the caller has requested to use the rotated + // signing key starting from an SDK version where v3.1 is supported, and the minimum + // SDK version for the APK is less than the requested rotation minimum, then the APK + // should be signed with both the v3.1 signing scheme with the rotated key, and the v3.0 + // scheme with the original signing key. If the APK's minSdkVersion is >= the requested + // SDK version for rotation then just use the v3.0 signing block for this. + if (!signingLineageHas31Support()) { + return null; + } + + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = new ArrayList<>(); + Iterator<ApkSigningBlockUtils.SignerConfig> v3SignerIterator = + v3SignerConfigs.iterator(); + while (v3SignerIterator.hasNext()) { + ApkSigningBlockUtils.SignerConfig signerConfig = v3SignerIterator.next(); + // All signing configs with a min SDK version that supports v3.1 should be used + // in the v3.1 signing block and removed from the v3.0 block. + if (signerConfig.minSdkVersion >= mRotationMinSdkVersion) { + v31SignerConfigs.add(signerConfig); + v3SignerIterator.remove(); + } + } + return v31SignerConfigs; + } + + private V4SchemeSigner.SignerConfig createV4SignerConfig() throws InvalidKeyException { + List<ApkSigningBlockUtils.SignerConfig> v4Configs = createSigningBlockSignerConfigs(true, + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4); + if (v4Configs.size() != 1) { + // V4 uses signer config to connect back to v3. Use the same filtering logic. + v4Configs = processV3Configs(v4Configs); } - return configs.get(0); + List<ApkSigningBlockUtils.SignerConfig> v41configs = processV31SignerConfigs(v4Configs); + return new V4SchemeSigner.SignerConfig(v4Configs, v41configs); } private ApkSigningBlockUtils.SignerConfig createSourceStampSignerConfig() throws InvalidKeyException { - return createSigningBlockSignerConfig( + ApkSigningBlockUtils.SignerConfig config = createSigningBlockSignerConfig( mSourceStampSignerConfig, - /* apkSigningBlockPaddingSupported= */ true, + /* 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) { @@ -420,13 +538,19 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2: newSignerConfig.signatureAlgorithms = V2SchemeSigner.getSuggestedSignatureAlgorithms( - publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported); + publicKey, + mMinSdkVersion, + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); break; case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: try { newSignerConfig.signatureAlgorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( - publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported); + publicKey, + mMinSdkVersion, + apkSigningBlockPaddingSupported && mVerityEnabled, + signerConfig.getDeterministicDsaSigning()); } catch (InvalidKeyException e) { // It is possible for a signer used for v1/v2 signing to not be allowed for use @@ -439,8 +563,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V4: try { newSignerConfig.signatureAlgorithms = - V4SchemeSigner.getSuggestedSignatureAlgorithms(publicKey, - mMinSdkVersion, apkSigningBlockPaddingSupported); + V4SchemeSigner.getSuggestedSignatureAlgorithms( + publicKey, mMinSdkVersion, apkSigningBlockPaddingSupported, + signerConfig.getDeterministicDsaSigning()); } catch (InvalidKeyException e) { // V4 is an optional signing schema, ok to proceed without. newSignerConfig.signatureAlgorithms = null; @@ -449,7 +574,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { case ApkSigningBlockUtils.VERSION_SOURCE_STAMP: newSignerConfig.signatureAlgorithms = Collections.singletonList( - SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256); + SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); break; default: throw new IllegalArgumentException("Unknown APK Signature Scheme ID requested"); @@ -479,9 +604,9 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { @Override @SuppressWarnings("AndroidJdkLibsChecker") public Set<String> initWith(byte[] manifestBytes, Set<String> entryNames) { - V1SchemeVerifier.Result dummyResult = new V1SchemeVerifier.Result(); + V1SchemeVerifier.Result result = new V1SchemeVerifier.Result(); Pair<ManifestParser.Section, Map<String, ManifestParser.Section>> sections = - V1SchemeVerifier.parseManifest(manifestBytes, entryNames, dummyResult); + V1SchemeVerifier.parseManifest(manifestBytes, entryNames, result); String alg = V1SchemeSigner.getJcaMessageDigestAlgorithm(mV1ContentDigestAlgorithm); for (Map.Entry<String, ManifestParser.Section> entry : sections.getSecond().entrySet()) { String entryName = entry.getKey(); @@ -520,11 +645,92 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } if (mOtherSignersSignaturesPreserved) { - // TODO: Preserve blocks other than APK Signature Scheme v2 blocks of signers configured - // in this engine. + boolean schemeSignatureBlockPreserved = false; + mPreservedSignatureBlocks = new ArrayList<>(); + try { + List<Pair<byte[], Integer>> signatureBlocks = + ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock); + for (Pair<byte[], Integer> signatureBlock : signatureBlocks) { + if (signatureBlock.getSecond() == Constants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID) { + // If a V2 signature block is found and the engine is configured to use V2 + // then save any of the previous signers that are not part of the current + // signing request. + if (mV2SigningEnabled) { + List<Pair<List<X509Certificate>, byte[]>> v2Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + mPreservedV2Signers = new ArrayList<>(v2Signers.size()); + for (Pair<List<X509Certificate>, byte[]> v2Signer : v2Signers) { + if (!isConfiguredWithSigner(v2Signer.getFirst())) { + mPreservedV2Signers.add(v2Signer.getSecond()); + schemeSignatureBlockPreserved = true; + } + } + } else { + // else V2 signing is not enabled; save the entire signature block to be + // added to the final APK signing block. + mPreservedSignatureBlocks.add(signatureBlock); + schemeSignatureBlockPreserved = true; + } + } else if (signatureBlock.getSecond() + == Constants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) { + // Preserving other signers in the presence of a V3 signature block is only + // supported if the engine is configured to resign the APK with the V3 + // signature scheme, and the V3 signer in the signature block is the same + // as the engine is configured to use. + if (!mV3SigningEnabled) { + throw new IllegalStateException( + "Preserving an existing V3 signature is not supported"); + } + List<Pair<List<X509Certificate>, byte[]>> v3Signers = + ApkSigningBlockUtils.getApkSignatureBlockSigners( + signatureBlock.getFirst()); + if (v3Signers.size() > 1) { + throw new IllegalArgumentException( + "The provided APK signing block contains " + v3Signers.size() + + " V3 signers; the V3 signature scheme only supports" + + " one signer"); + } + // If there is only a single V3 signer then ensure it is the signer + // configured to sign the APK. + if (v3Signers.size() == 1 + && !isConfiguredWithSigner(v3Signers.get(0).getFirst())) { + throw new IllegalStateException( + "The V3 signature scheme only supports one signer; a request " + + "was made to preserve the existing V3 signature, " + + "but the engine is configured to sign with a " + + "different signer"); + } + } else if (!DISCARDED_SIGNATURE_BLOCK_IDS.contains( + signatureBlock.getSecond())) { + mPreservedSignatureBlocks.add(signatureBlock); + } + } + } catch (ApkFormatException | CertificateException | IOException e) { + throw new IllegalArgumentException("Unable to parse the provided signing block", e); + } + // Signature scheme V3+ only support a single signer; if the engine is configured to + // sign with V3+ then ensure no scheme signature blocks have been preserved. + if (mV3SigningEnabled && schemeSignatureBlockPreserved) { + throw new IllegalStateException( + "Signature scheme V3+ only supports a single signer and cannot be " + + "appended to the existing signature scheme blocks"); + } return; } - // TODO: Preserve blocks other than APK Signature Scheme v2 blocks. + } + + /** + * Returns whether the engine is configured to sign the APK with a signer using the specified + * {@code signerCerts}. + */ + private boolean isConfiguredWithSigner(List<X509Certificate> signerCerts) { + for (SignerConfig signerConfig : mSignerConfigs) { + if (signerCerts.containsAll(signerConfig.getCertificates())) { + return true; + } + } + return false; } @Override @@ -539,7 +745,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); @@ -607,7 +813,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 { @@ -742,7 +948,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 { @@ -825,7 +1031,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { checkNotClosed(); checkV1SigningDoneIfEnabled(); - if (!mV2SigningEnabled && !mV3SigningEnabled) { + if (!mV2SigningEnabled && !mV3SigningEnabled && !isEligibleForSourceStamp()) { return null; } checkOutputApkNotDebuggableIfDebuggableMustBeRejected(); @@ -841,6 +1047,13 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { List<Pair<byte[], Integer>> signingSchemeBlocks = new ArrayList<>(); ApkSigningBlockUtils.SigningSchemeBlockAndDigests v2SigningSchemeBlockAndDigests = null; ApkSigningBlockUtils.SigningSchemeBlockAndDigests v3SigningSchemeBlockAndDigests = null; + // If the engine is configured to preserve previous signature blocks and any were found in + // the existing APK signing block then add them to the list to be used to generate the + // new APK signing block. + if (mOtherSignersSignaturesPreserved && mPreservedSignatureBlocks != null + && !mPreservedSignatureBlocks.isEmpty()) { + signingSchemeBlocks.addAll(mPreservedSignatureBlocks); + } // create APK Signature Scheme V2 Signature if requested if (mV2SigningEnabled) { @@ -854,32 +1067,84 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { zipCentralDirectory, eocd, v2SignerConfigs, - mV3SigningEnabled); + mV3SigningEnabled, + mOtherSignersSignaturesPreserved ? mPreservedV2Signers : null); signingSchemeBlocks.add(v2SigningSchemeBlockAndDigests.signingSchemeBlock); } if (mV3SigningEnabled) { invalidateV3Signature(); List<ApkSigningBlockUtils.SignerConfig> v3SignerConfigs = createV3SignerConfigs(apkSigningBlockPaddingSupported); + List<ApkSigningBlockUtils.SignerConfig> v31SignerConfigs = processV31SignerConfigs( + v3SignerConfigs); + if (v31SignerConfigs != null && v31SignerConfigs.size() > 0) { + ApkSigningBlockUtils.SigningSchemeBlockAndDigests + v31SigningSchemeBlockAndDigests = + new V3SchemeSigner.Builder(beforeCentralDir, zipCentralDirectory, eocd, + v31SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) + .setRotationTargetsDevRelease(mRotationTargetsDevRelease) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); + signingSchemeBlocks.add(v31SigningSchemeBlockAndDigests.signingSchemeBlock); + } + V3SchemeSigner.Builder builder = new V3SchemeSigner.Builder(beforeCentralDir, + zipCentralDirectory, eocd, v3SignerConfigs) + .setRunnablesExecutor(mExecutor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + if (signingLineageHas31Support()) { + builder.setRotationMinSdkVersion(mRotationMinSdkVersion); + } v3SigningSchemeBlockAndDigests = - V3SchemeSigner.generateApkSignatureSchemeV3Block( - mExecutor, - beforeCentralDir, - zipCentralDirectory, - eocd, - v3SignerConfigs); + builder.build().generateApkSignatureSchemeV3BlockAndDigests(); signingSchemeBlocks.add(v3SigningSchemeBlockAndDigests.signingSchemeBlock); } if (isEligibleForSourceStamp()) { ApkSigningBlockUtils.SignerConfig sourceStampSignerConfig = createSourceStampSignerConfig(); - Map<ContentDigestAlgorithm, byte[]> digestInfo = - mV3SigningEnabled - ? v3SigningSchemeBlockAndDigests.digestInfo - : v2SigningSchemeBlockAndDigests.digestInfo; - signingSchemeBlocks.add( - SourceStampSigner.generateSourceStampBlock( - sourceStampSignerConfig, digestInfo)); + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos = + new HashMap<>(); + if (mV3SigningEnabled) { + signatureSchemeDigestInfos.put( + VERSION_APK_SIGNATURE_SCHEME_V3, v3SigningSchemeBlockAndDigests.digestInfo); + } + if (mV2SigningEnabled) { + signatureSchemeDigestInfos.put( + VERSION_APK_SIGNATURE_SCHEME_V2, v2SigningSchemeBlockAndDigests.digestInfo); + } + if (mV1SigningEnabled) { + Map<ContentDigestAlgorithm, byte[]> v1SigningSchemeDigests = new HashMap<>(); + try { + // Jar signing related variables must have been already populated at this point + // if V1 signing is enabled since it is happening before computations on the APK + // signing block (V2/V3/V4/SourceStamp signing). + byte[] inputJarManifest = + (mInputJarManifestEntryDataRequest != null) + ? mInputJarManifestEntryDataRequest.getData() + : null; + byte[] jarManifest = + V1SchemeSigner.generateManifestFile( + mV1ContentDigestAlgorithm, + mOutputJarEntryDigests, + inputJarManifest) + .contents; + // The digest of the jar manifest does not need to be computed in chunks due to + // the small size of the manifest. + v1SigningSchemeDigests.put( + ContentDigestAlgorithm.SHA256, computeSha256DigestBytes(jarManifest)); + } catch (ApkFormatException e) { + throw new RuntimeException("Failed to generate manifest file", e); + } + signatureSchemeDigestInfos.put( + VERSION_JAR_SIGNATURE_SCHEME, v1SigningSchemeDigests); + } + V2SourceStampSigner v2SourceStampSigner = + new V2SourceStampSigner.Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos) + .setSourceStampTimestampEnabled(mSourceStampTimestampEnabled) + .build(); + signingSchemeBlocks.add(v2SourceStampSigner.generateSourceStampBlock()); } // create APK Signing Block with v2 and/or v3 and/or SourceStamp blocks @@ -907,7 +1172,7 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { throw new SignatureException("Missing V4 output file."); } try { - ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig(); + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig, outputFile); } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { if (ignoreFailures) { @@ -918,27 +1183,26 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } /** For external use only to generate V4 & tree separately. */ - public byte[] produceV4Signature( - DataSource dataSource, - OutputStream sigOutput) - throws SignatureException { - if (sigOutput == null) { - throw new SignatureException("Missing V4 output streams."); - } - try { - ApkSigningBlockUtils.SignerConfig v4SignerConfig = createV4SignerConfig(); - Pair<V4Signature, byte[]> pair = - V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig); - pair.getFirst().writeTo(sigOutput); - return pair.getSecond(); - } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { - throw new SignatureException("V4 signing failed", e); - } + public byte[] produceV4Signature(DataSource dataSource, OutputStream sigOutput) + throws SignatureException { + if (sigOutput == null) { + throw new SignatureException("Missing V4 output streams."); + } + try { + V4SchemeSigner.SignerConfig v4SignerConfig = createV4SignerConfig(); + Pair<V4Signature, byte[]> pair = + V4SchemeSigner.generateV4Signature(dataSource, v4SignerConfig); + pair.getFirst().writeTo(sigOutput); + return pair.getSecond(); + } catch (InvalidKeyException | IOException | NoSuchAlgorithmException e) { + throw new SignatureException("V4 signing failed", e); + } } @Override public boolean isEligibleForSourceStamp() { - return mSourceStampSignerConfig != null && (mV2SigningEnabled || mV3SigningEnabled); + return mSourceStampSignerConfig != null + && (mV2SigningEnabled || mV3SigningEnabled || mV1SigningEnabled); } @Override @@ -1113,17 +1377,6 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return InputJarEntryInstructions.OutputPolicy.SKIP; } - private 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(); - } - private static class OutputJarSignatureRequestImpl implements OutputJarSignatureRequest { private final List<JarEntry> mAdditionalJarEntries; private volatile boolean mDone; @@ -1379,12 +1632,15 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { private final String mName; private final PrivateKey mPrivateKey; private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; private SignerConfig( - String name, PrivateKey privateKey, List<X509Certificate> certificates) { + String name, PrivateKey privateKey, List<X509Certificate> certificates, + boolean deterministicDsaSigning) { mName = name; mPrivateKey = privateKey; mCertificates = Collections.unmodifiableList(new ArrayList<>(certificates)); + mDeterministicDsaSigning = deterministicDsaSigning; } /** Returns the name of this signer. */ @@ -1405,11 +1661,19 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return mCertificates; } + /** + * If this signer is a DSA signer, whether or not the signing is done deterministically. + */ + public boolean getDeterministicDsaSigning() { + return mDeterministicDsaSigning; + } + /** Builder of {@link SignerConfig} instances. */ public static class Builder { private final String mName; private final PrivateKey mPrivateKey; private final List<X509Certificate> mCertificates; + private final boolean mDeterministicDsaSigning; /** * Constructs a new {@code Builder}. @@ -1421,12 +1685,29 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { * the first certificate must correspond to the {@code privateKey}. */ public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates) { + this(name, privateKey, certificates, false); + } + + /** + * Constructs a new {@code Builder}. + * + * @param name signer's name. The name is reflected in the name of files comprising the + * JAR signature of the APK. + * @param privateKey signing key + * @param certificates list of one or more X.509 certificates. The subject public key of + * the first certificate must correspond to the {@code privateKey}. + * @param deterministicDsaSigning When signing using DSA, whether or not the + * deterministic signing algorithm variant (RFC6979) should be used. + */ + public Builder(String name, PrivateKey privateKey, List<X509Certificate> certificates, + boolean deterministicDsaSigning) { if (name.isEmpty()) { throw new IllegalArgumentException("Empty name"); } mName = name; mPrivateKey = privateKey; mCertificates = new ArrayList<>(certificates); + mDeterministicDsaSigning = deterministicDsaSigning; } /** @@ -1434,7 +1715,8 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { * this builder. */ public SignerConfig build() { - return new SignerConfig(mName, mPrivateKey, mCertificates); + return new SignerConfig(mName, mPrivateKey, mCertificates, + mDeterministicDsaSigning); } } } @@ -1443,11 +1725,16 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { public static class Builder { private List<SignerConfig> mSignerConfigs; private SignerConfig mStampSignerConfig; + private SigningCertificateLineage mSourceStampSigningCertificateLineage; + private boolean mSourceStampTimestampEnabled = true; private final int mMinSdkVersion; private boolean mV1SigningEnabled = true; private boolean mV2SigningEnabled = true; private boolean mV3SigningEnabled = true; + private int mRotationMinSdkVersion = V3SchemeConstants.DEFAULT_ROTATION_MIN_SDK_VERSION; + private boolean mRotationTargetsDevRelease = false; + private boolean mVerityEnabled = false; private boolean mDebuggableApkPermitted = true; private boolean mOtherSignersSignaturesPreserved; private String mCreatedBy = "1.0 (Android)"; @@ -1533,10 +1820,15 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { return new DefaultApkSignerEngine( mSignerConfigs, mStampSignerConfig, + mSourceStampSigningCertificateLineage, + mSourceStampTimestampEnabled, mMinSdkVersion, + mRotationMinSdkVersion, + mRotationTargetsDevRelease, mV1SigningEnabled, mV2SigningEnabled, mV3SigningEnabled, + mVerityEnabled, mDebuggableApkPermitted, mOtherSignersSignaturesPreserved, mCreatedBy, @@ -1550,6 +1842,25 @@ 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 source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + 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. @@ -1587,6 +1898,18 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } /** + * Sets whether the APK should be signed using the verity signature algorithm in the v2 and + * v3 signature blocks. + * + * <p>By default, the APK will be signed using the verity signature algorithm for the v2 and + * v3 signature schemes. + */ + public Builder setVerityEnabled(boolean enabled) { + mVerityEnabled = enabled; + return this; + } + + /** * Sets whether the APK should be signed even if it is marked as debuggable ({@code * android:debuggable="true"} in its {@code AndroidManifest.xml}). For backward * compatibility reasons, the default value of this setting is {@code true}. @@ -1633,5 +1956,49 @@ public class DefaultApkSignerEngine implements ApkSignerEngine { } return this; } + + /** + * Sets the minimum Android platform version (API Level) for which an APK's rotated signing + * key should be used to produce the APK's signature. The original signing key for the APK + * will be used for all previous platform versions. If a rotated key with signing lineage is + * not provided then this method is a noop. + * + * <p>By default, if a signing lineage is specified with {@link + * #setSigningCertificateLineage(SigningCertificateLineage)}, then the APK Signature Scheme + * V3.1 will be used to only apply the rotation on devices running Android T+. + * + * <p><em>Note:</em>Specifying a {@code minSdkVersion} value <= 32 (Android Sv2) will result + * in the original V3 signing block being used without platform targeting. + */ + public Builder setMinSdkVersionForRotation(int minSdkVersion) { + // If the provided SDK version does not support v3.1, then use the default SDK version + // with rotation support. + if (minSdkVersion < MIN_SDK_WITH_V31_SUPPORT) { + mRotationMinSdkVersion = MIN_SDK_WITH_V3_SUPPORT; + } else { + mRotationMinSdkVersion = minSdkVersion; + } + return this; + } + + /** + * Sets whether the rotation-min-sdk-version is intended to target a development release; + * this is primarily required after the T SDK is finalized, and an APK needs to target U + * during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. Specifying a rotation-min-sdk-version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } } } diff --git a/src/main/java/com/android/apksig/SigningCertificateLineage.java b/src/main/java/com/android/apksig/SigningCertificateLineage.java index 54340d7..43b7f5e 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; @@ -123,6 +124,11 @@ public class SigningCertificateLineage { return signingCertificateLineage.spawnDescendant(parent, child, childCapabilities); } + public static SigningCertificateLineage readFromBytes(byte[] lineageBytes) + throws IOException { + return readFromDataSource(DataSources.asDataSource(ByteBuffer.wrap(lineageBytes))); + } + public static SigningCertificateLineage readFromFile(File file) throws IOException { if (file == null) { @@ -185,41 +191,62 @@ public class SigningCertificateLineage { */ public static SigningCertificateLineage readFromApkDataSource(DataSource apk) throws IOException, ApkFormatException { - SignatureInfo signatureInfo; + ApkUtils.ZipSections zipSections; + try { + zipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException(e.getMessage()); + } + + List<SignatureInfo> signatureInfoList = new ArrayList<>(); + try { + ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( + ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31); + signatureInfoList.add( + ApkSigningBlockUtils.findSignature(apk, zipSections, + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, result)); + } + catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if there's only a V3 signature block. + } try { - ApkUtils.ZipSections zipSections = ApkUtils.findZipSections(apk); ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); - signatureInfo = + signatureInfoList.add( ApkSigningBlockUtils.findSignature(apk, zipSections, - V3SchemeSigner.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); - } catch (ZipFormatException e) { - throw new ApkFormatException(e.getMessage()); - } catch (ApkSigningBlockUtils.SignatureNotFoundException e) { + V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result)); + } + catch (ApkSigningBlockUtils.SignatureNotFoundException ignored) { + // This could be expected if the provided APK is not signed with the v3 signature scheme + } + if (signatureInfoList.isEmpty()) { throw new IllegalArgumentException( "The provided APK does not contain a valid V3 signature block."); } - // FORMAT: - // * length-prefixed sequence of length-prefixed signers: - // * length-prefixed signed data - // * minSDK - // * maxSDK - // * length-prefixed sequence of length-prefixed signatures - // * length-prefixed public key - ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); List<SigningCertificateLineage> lineages = new ArrayList<>(1); - while (signers.hasRemaining()) { - ByteBuffer signer = getLengthPrefixedSlice(signers); - ByteBuffer signedData = getLengthPrefixedSlice(signer); - try { - SigningCertificateLineage lineage = readFromSignedData(signedData); - lineages.add(lineage); - } catch (IllegalArgumentException ignored) { - // The current signer block does not contain a valid lineage, but it is possible - // another block will. + for (SignatureInfo signatureInfo : signatureInfoList) { + // FORMAT: + // * length-prefixed sequence of length-prefixed signers: + // * length-prefixed signed data + // * minSDK + // * maxSDK + // * length-prefixed sequence of length-prefixed signatures + // * length-prefixed public key + ByteBuffer signers = getLengthPrefixedSlice(signatureInfo.signatureBlock); + while (signers.hasRemaining()) { + ByteBuffer signer = getLengthPrefixedSlice(signers); + ByteBuffer signedData = getLengthPrefixedSlice(signer); + try { + SigningCertificateLineage lineage = readFromSignedData(signedData); + lineages.add(lineage); + } catch (IllegalArgumentException ignored) { + // The current signer block does not contain a valid lineage, but it is possible + // another block will. + } } } + SigningCertificateLineage result; if (lineages.isEmpty()) { throw new IllegalArgumentException( @@ -263,7 +290,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); @@ -282,6 +309,10 @@ public class SigningCertificateLineage { return result; } + public byte[] getBytes() { + return write().array(); + } + public void writeToFile(File file) throws IOException { if (file == null) { throw new NullPointerException("file == null"); @@ -401,7 +432,8 @@ public class SigningCertificateLineage { // TODO switch to one signature algorithm selection, or add support for multiple algorithms List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( - publicKey, mMinSdkVersion, false /* padding support */); + publicKey, mMinSdkVersion, false /* verityEnabled */, + false /* deterministicDsaSigning */); return algorithms.get(0); } @@ -491,20 +523,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..98da68e --- /dev/null +++ b/src/main/java/com/android/apksig/SourceStampVerifier.java @@ -0,0 +1,911 @@ +/* + * 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<>(); + private final List<ApkVerificationIssue> mInfoMessages = new ArrayList<>(); + + private final long mTimestamp; + + /* + * 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()); + mInfoMessages.addAll(result.getInfoMessages()); + mTimestamp = result.timestamp; + } + + /** + * 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 {@code true} if any info messages were encountered during verification of + * this source stamp. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.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; + } + + /** + * Returns a {@code List} of {@link ApkVerificationIssue} representing info messages + * that were encountered during source stamp verification. + */ + public List<ApkVerificationIssue> getInfoMessages() { + return mInfoMessages; + } + + /** + * Returns the epoch timestamp in seconds representing the time this source stamp block + * was signed, or 0 if the timestamp is not available. + */ + public long getTimestampEpochSeconds() { + return mTimestamp; + } + } + } + + /** + * 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 870971f..156ea17 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,6 +25,7 @@ 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; @@ -42,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() {} @@ -54,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); } } @@ -167,10 +96,37 @@ 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; + /** + * Updates the length of EOCD comment. + * + * @param zipEndOfCentralDirectory APK's ZIP End of Central Directory record + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + ByteBuffer eocd = zipEndOfCentralDirectory.slice(); + eocd.order(ByteOrder.LITTLE_ENDIAN); + ZipUtils.updateZipEocdCommentLen(eocd); + } + + /** + * Returns the APK Signing Block of the provided {@code apk}. + * + * @throws ApkFormatException if the APK is not a valid ZIP archive + * @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) + throws ApkFormatException, IOException, ApkSigningBlockNotFoundException { + ApkUtils.ZipSections inputZipSections; + try { + inputZipSections = ApkUtils.findZipSections(apk); + } catch (ZipFormatException e) { + throw new ApkFormatException("Malformed APK: not a ZIP archive", e); + } + return findApkSigningBlock(apk, inputZipSections); + } /** * Returns the APK Signing Block of the provided APK. @@ -178,74 +134,20 @@ public abstract class ApkUtils { * @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}. * @@ -254,23 +156,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); } } @@ -322,6 +208,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}. * @@ -604,4 +514,157 @@ public abstract class ApkUtils { e); } } + + /** + * 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 { + 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); + } + } + + 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..3e79341 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/ApkSignerInfo.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.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +/** + * Base implementation of an APK signer. + */ +public class ApkSignerInfo { + public int index; + public long timestamp; + public List<X509Certificate> certs = new ArrayList<>(); + public List<X509Certificate> certificateLineage = new ArrayList<>(); + + private final List<ApkVerificationIssue> mInfoMessages = 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)); + } + + /** + * Adds a new {@link ApkVerificationIssue} as an info message to this signer config using the + * provided {@code issueId} and {@code params}. + */ + public void addInfoMessage(int issueId, Object... params) { + mInfoMessages.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 {@code true} if any info messages were encountered during verification of this + * signer. + */ + public boolean containsInfoMessages() { + return !mInfoMessages.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; + } + + /** + * Returns the info messages encountered during verification of this signer. + */ + public List<? extends ApkVerificationIssue> getInfoMessages() { + return mInfoMessages; + } +} 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 bff38ac..44dcc79 100644 --- a/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java +++ b/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java @@ -16,6 +16,7 @@ package com.android.apksig.internal.apk; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA256; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.CHUNKED_SHA512; import static com.android.apksig.internal.apk.ContentDigestAlgorithm.VERITY_CHUNKED_SHA256; @@ -23,7 +24,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; @@ -40,8 +40,10 @@ import com.android.apksig.internal.pkcs7.SignerIdentifier; import com.android.apksig.internal.pkcs7.SignerInfo; import com.android.apksig.internal.util.ByteBufferDataSource; import com.android.apksig.internal.util.ChainedDataSource; +import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; import com.android.apksig.internal.util.Pair; import com.android.apksig.internal.util.VerityTreeBuilder; +import com.android.apksig.internal.util.X509CertificateUtils; import com.android.apksig.internal.x509.RSAPublicKey; import com.android.apksig.internal.x509.SubjectPublicKeyInfo; import com.android.apksig.internal.zip.ZipUtils; @@ -53,7 +55,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; @@ -67,6 +68,7 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; @@ -86,15 +88,14 @@ 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; - public static final byte[] APK_SIGNING_BLOCK_MAGIC = + private static final byte[] APK_SIGNING_BLOCK_MAGIC = new byte[] { 0x41, 0x50, 0x4b, 0x20, 0x53, 0x69, 0x67, 0x20, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x20, 0x34, 0x32, }; - private static final int VERITY_PADDING_BLOCK_ID = 0x42726577; + public static final int VERITY_PADDING_BLOCK_ID = 0x42726577; private static final ContentDigestAlgorithm[] V4_CONTENT_DIGEST_ALGORITHMS = {CHUNKED_SHA512, VERITY_CHUNKED_SHA256, CHUNKED_SHA256}; @@ -103,6 +104,7 @@ public class ApkSigningBlockUtils { 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_V31 = 31; public static final int VERSION_APK_SIGNATURE_SCHEME_V4 = 4; /** @@ -110,58 +112,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 +233,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( @@ -767,6 +593,7 @@ public class ApkSigningBlockUtils { } } + @SuppressWarnings("ByteBufferBackingArray") private static void computeApkVerityDigest(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, Map<ContentDigestAlgorithm, byte[]> outputContentDigests) throws IOException, NoSuchAlgorithmException { @@ -810,6 +637,7 @@ public class ApkSigningBlockUtils { } } + @SuppressWarnings("ByteBufferBackingArray") public static VerityTreeAndDigest computeChunkVerityTreeAndDigest(DataSource dataSource) throws IOException, NoSuchAlgorithmException { ByteBuffer encoded = createVerityDigestBuffer(false); @@ -840,7 +668,8 @@ public class ApkSigningBlockUtils { if ("X.509".equals(publicKey.getFormat())) { encodedPublicKey = publicKey.getEncoded(); // if the key is an RSA key check for a negative modulus - if ("RSA".equals(publicKey.getAlgorithm())) { + String keyAlgorithm = publicKey.getAlgorithm(); + if ("RSA".equals(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { try { // Parse the encoded public key into the separate elements of the // SubjectPublicKeyInfo to obtain the SubjectPublicKey. @@ -944,20 +773,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); } /** @@ -974,30 +791,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()); } /** @@ -1051,7 +849,7 @@ public class ApkSigningBlockUtils { // uint64: size (excluding this field) // uint32: ID // (size - 4) bytes: value - // (extra dummy ID-value for padding to make block size a multiple of 4096 bytes) + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) // uint64: size (same as the one above) // uint128: magic @@ -1085,7 +883,6 @@ public class ApkSigningBlockUtils { long blockSizeFieldValue = resultSize - 8L; result.putLong(blockSizeFieldValue); - for (Pair<byte[], Integer> schemeBlockPair : apkSignatureSchemeBlockPairs) { byte[] apkSignatureSchemeBlock = schemeBlockPair.getFirst(); int apkSignatureSchemeId = schemeBlockPair.getSecond(); @@ -1106,6 +903,116 @@ public class ApkSigningBlockUtils { } /** + * Returns the individual APK signature blocks within the provided {@code apkSigningBlock} in a + * {@code List} of {@code Pair} instances where the first element in the {@code Pair} is the + * contents / value of the signature block and the second element is the ID of the block. + * + * @throws IOException if an error is encountered reading the provided {@code apkSigningBlock} + */ + public static List<Pair<byte[], Integer>> getApkSignatureBlocks( + DataSource apkSigningBlock) throws IOException { + // FORMAT: + // uint64: size (excluding this field) + // repeated ID-value pairs: + // uint64: size (excluding this field) + // uint32: ID + // (size - 4) bytes: value + // (extra verity ID-value for padding to make block size a multiple of 4096 bytes) + // uint64: size (same as the one above) + // uint128: magic + long apkSigningBlockSize = apkSigningBlock.size(); + if (apkSigningBlock.size() > Integer.MAX_VALUE || apkSigningBlockSize < 32) { + throw new IllegalArgumentException( + "APK signing block size out of range: " + apkSigningBlockSize); + } + // Remove the header and footer from the signing block to iterate over only the repeated + // ID-value pairs. + ByteBuffer apkSigningBlockBuffer = apkSigningBlock.getByteBuffer(8, + (int) apkSigningBlock.size() - 32); + apkSigningBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + List<Pair<byte[], Integer>> signatureBlocks = new ArrayList<>(); + while (apkSigningBlockBuffer.hasRemaining()) { + long blockLength = apkSigningBlockBuffer.getLong(); + if (blockLength > Integer.MAX_VALUE || blockLength < 4) { + throw new IllegalArgumentException( + "Block index " + (signatureBlocks.size() + 1) + " size out of range: " + + blockLength); + } + int blockId = apkSigningBlockBuffer.getInt(); + // Since the block ID has already been read from the signature block read the next + // blockLength - 4 bytes as the value. + byte[] blockValue = new byte[(int) blockLength - 4]; + apkSigningBlockBuffer.get(blockValue); + signatureBlocks.add(Pair.of(blockValue, blockId)); + } + return signatureBlocks; + } + + /** + * Returns the individual APK signers within the provided {@code signatureBlock} in a {@code + * List} of {@code Pair} instances where the first element is a {@code List} of {@link + * X509Certificate}s and the second element is a byte array of the individual signer's block. + * + * <p>This method supports any signature block that adheres to the following format up to the + * signing certificate(s): + * <pre> + * * length-prefixed sequence of length-prefixed signers + * * 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). + * </pre> + * + * <p>Note, this is a convenience method to obtain any signers from an existing signature block; + * the signature of each signer will not be verified. + * + * @throws ApkFormatException if an error is encountered while parsing the provided {@code + * signatureBlock} + * @throws CertificateException if the signing certificate(s) within an individual signer block + * cannot be parsed + */ + public static List<Pair<List<X509Certificate>, byte[]>> getApkSignatureBlockSigners( + byte[] signatureBlock) throws ApkFormatException, CertificateException { + ByteBuffer signatureBlockBuffer = ByteBuffer.wrap(signatureBlock); + signatureBlockBuffer.order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer signersBuffer = getLengthPrefixedSlice(signatureBlockBuffer); + List<Pair<List<X509Certificate>, byte[]>> signers = new ArrayList<>(); + while (signersBuffer.hasRemaining()) { + // Parse the next signer block, save all of its bytes for the resulting List, and + // rewind the buffer to allow the signing certificate(s) to be parsed. + ByteBuffer signer = getLengthPrefixedSlice(signersBuffer); + byte[] signerBytes = new byte[signer.remaining()]; + signer.get(signerBytes); + signer.rewind(); + + ByteBuffer signedData = getLengthPrefixedSlice(signer); + // The first length prefixed slice is the sequence of digests which are not required + // when obtaining the signing certificate(s). + getLengthPrefixedSlice(signedData); + ByteBuffer certificatesBuffer = getLengthPrefixedSlice(signedData); + List<X509Certificate> certificates = new ArrayList<>(); + while (certificatesBuffer.hasRemaining()) { + int certLength = certificatesBuffer.getInt(); + byte[] certBytes = new byte[certLength]; + if (certLength > certificatesBuffer.remaining()) { + throw new IllegalArgumentException( + "Cert index " + (certificates.size() + 1) + " under signer index " + + (signers.size() + 1) + " size out of range: " + certLength); + } + certificatesBuffer.get(certBytes); + GuaranteedEncodedFormX509Certificate signerCert = + new GuaranteedEncodedFormX509Certificate( + X509CertificateUtils.generateCertificate(certBytes), certBytes); + certificates.add(signerCert); + } + signers.add(Pair.of(certificates, signerBytes)); + } + return signers; + } + + /** * Computes the digests of the given APK components according to the algorithms specified in the * given SignerConfigs. * @@ -1171,57 +1078,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); } @@ -1384,19 +1273,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() { @@ -1435,17 +1319,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<>(); @@ -1539,13 +1423,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/ContentDigestAlgorithm.java b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java index e463743..b806d1e 100644 --- a/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java +++ b/src/main/java/com/android/apksig/internal/apk/ContentDigestAlgorithm.java @@ -25,7 +25,10 @@ public enum ContentDigestAlgorithm { CHUNKED_SHA512(2, "SHA-512", 512 / 8), /** SHA2-256 over 4 KB chunks for APK verity. */ - VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8); + VERITY_CHUNKED_SHA256(3, "SHA-256", 256 / 8), + + /** Non-chunk SHA2-256. */ + SHA256(4, "SHA-256", 256 / 8); private final int mId; private final String mJcaMessageDigestAlgorithm; 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..804eb37 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,20 @@ public enum SignatureAlgorithm { ContentDigestAlgorithm.CHUNKED_SHA256, "DSA", Pair.of("SHA256withDSA", null), - AndroidSdkVersion.N), + AndroidSdkVersion.N, + AndroidSdkVersion.INITIAL_RELEASE), + + /** + * DSA with SHA2-256 digest, content digested using SHA2-256 in 1 MB chunks. Signing is done + * deterministically according to RFC 6979. + */ + DETDSA_WITH_SHA256( + 0x0301, + ContentDigestAlgorithm.CHUNKED_SHA256, + "DSA", + Pair.of("SHA256withDetDSA", null), + 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 +123,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 +136,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 +149,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 +206,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..2a949ad --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampConstants.java @@ -0,0 +1,34 @@ +/* + * 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; + /** + * The source stamp timestamp attribute value is an 8-byte little-endian encoded long + * representing the epoch time in seconds when the stamp block was signed. The first 8 bytes + * of the attribute value buffer will be used to read the timestamp, and any additional buffer + * space will be ignored. + */ + public static final int STAMP_TIME_ATTR_ID = 0xe43c5946; +} 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 3590c71..ef6da2f 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 @@ -13,27 +13,27 @@ * 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.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; -import static com.android.apksig.internal.apk.stamp.SourceStampSigner.SOURCE_STAMP_BLOCK_ID; +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.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.ContentDigestAlgorithm; +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.SignatureInfo; +import com.android.apksig.internal.util.ByteBufferUtils; import com.android.apksig.internal.util.GuaranteedEncodedFormX509Certificate; -import com.android.apksig.internal.util.Pair; -import com.android.apksig.internal.util.X509CertificateUtils; -import com.android.apksig.util.DataSource; -import java.io.IOException; +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; @@ -47,12 +47,9 @@ import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * Source Stamp verifier. @@ -64,122 +61,139 @@ import java.util.stream.Collectors; * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing * block. */ -public abstract class SourceStampVerifier { - +class SourceStampVerifier { /** Hidden constructor to prevent instantiation. */ - private SourceStampVerifier() {} + private SourceStampVerifier() { + } /** - * 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 - * {@code true}. If verification fails, the result will contain errors -- see {@link - * ApkSigningBlockUtils.Result#getErrors()}. + * Parses the SourceStamp block and populates the {@code result}. + * + * <p>This verifies signatures over digest provided. * - * @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 - * found - * @throws IOException if an I/O error occurs when reading 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. */ - public static ApkSigningBlockUtils.Result verify( - DataSource apk, - ApkUtils.ZipSections zipSections, + public static void verifyV1SourceStamp( + ByteBuffer sourceStampBlockData, + CertificateFactory certFactory, + ApkSignerInfo result, + byte[] apkDigest, byte[] sourceStampCertificateDigest, - Map<ContentDigestAlgorithm, byte[]> apkContentDigests, int minSdkVersion, int maxSdkVersion) - throws IOException, NoSuchAlgorithmException, - ApkSigningBlockUtils.SignatureNotFoundException { - ApkSigningBlockUtils.Result result = - new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); - SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature(apk, zipSections, SOURCE_STAMP_BLOCK_ID, result); + throws ApkFormatException, NoSuchAlgorithmException { + X509Certificate sourceStampCertificate = + verifySourceStampCertificate( + sourceStampBlockData, certFactory, sourceStampCertificateDigest, result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } - verify( - signatureInfo.signatureBlock, - sourceStampCertificateDigest, - apkContentDigests, + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(sourceStampBlockData); + verifySourceStampSignature( + apkDigest, minSdkVersion, maxSdkVersion, + sourceStampCertificate, + apkDigestSignatures, result); - return result; - } - - /** - * 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 - * more information about the contract of this method. - */ - private static void verify( - ByteBuffer sourceStampBlock, - byte[] sourceStampCertificateDigest, - Map<ContentDigestAlgorithm, byte[]> apkContentDigests, - int minSdkVersion, - int maxSdkVersion, - ApkSigningBlockUtils.Result result) - throws NoSuchAlgorithmException { - ApkSigningBlockUtils.Result.SignerInfo signerInfo = - new ApkSigningBlockUtils.Result.SignerInfo(); - result.signers.add(signerInfo); - try { - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - ByteBuffer sourceStampBlockData = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); - parseSourceStamp( - sourceStampBlockData, - certFactory, - signerInfo, - apkContentDigests, - sourceStampCertificateDigest, - minSdkVersion, - maxSdkVersion); - result.verified = !result.containsErrors() && !result.containsWarnings(); - } 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); - } } /** * Parses the SourceStamp block and populates the {@code result}. * - * <p>This verifies signatures over digests contained in the APK signing block. + * <p>This verifies signatures over digest of multiple signature schemes provided. * * <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 parseSourceStamp( + public static void verifyV2SourceStamp( ByteBuffer sourceStampBlockData, CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, - Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + ApkSignerInfo result, + Map<Integer, byte[]> signatureSchemeApkDigests, byte[] sourceStampCertificateDigest, int minSdkVersion, int maxSdkVersion) throws ApkFormatException, NoSuchAlgorithmException { - List<Pair<Integer, byte[]>> digests = new ArrayList<>(); - for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest : - apkContentDigests.entrySet()) { - digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); + X509Certificate sourceStampCertificate = + verifySourceStampCertificate( + sourceStampBlockData, certFactory, sourceStampCertificateDigest, result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } + + // Parse signed signature schemes block. + ByteBuffer signedSignatureSchemes = getLengthPrefixedSlice(sourceStampBlockData); + Map<Integer, ByteBuffer> signedSignatureSchemeData = new HashMap<>(); + while (signedSignatureSchemes.hasRemaining()) { + ByteBuffer signedSignatureScheme = getLengthPrefixedSlice(signedSignatureSchemes); + int signatureSchemeId = signedSignatureScheme.getInt(); + ByteBuffer apkDigestSignatures = getLengthPrefixedSlice(signedSignatureScheme); + signedSignatureSchemeData.put(signatureSchemeId, apkDigestSignatures); + } + + for (Map.Entry<Integer, byte[]> signatureSchemeApkDigest : + signatureSchemeApkDigests.entrySet()) { + // TODO(b/192301300): Should the new v3.1 be included in the source stamp, or since a + // v3.0 block must always be present with a v3.1 block is it sufficient to just use the + // v3.0 block? + if (signatureSchemeApkDigest.getKey() + == Constants.VERSION_APK_SIGNATURE_SCHEME_V31) { + continue; + } + if (!signedSignatureSchemeData.containsKey(signatureSchemeApkDigest.getKey())) { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); + return; + } + verifySourceStampSignature( + signatureSchemeApkDigest.getValue(), + minSdkVersion, + maxSdkVersion, + sourceStampCertificate, + signedSignatureSchemeData.get(signatureSchemeApkDigest.getKey()), + result); + if (result.containsWarnings() || result.containsErrors()) { + return; + } } - Collections.sort(digests, Comparator.comparing(Pair::getFirst)); - byte[] digestBytes = - encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests); + 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); + } + } + + private static X509Certificate verifySourceStampCertificate( + ByteBuffer sourceStampBlockData, + CertificateFactory certFactory, + byte[] sourceStampCertificateDigest, + 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); - return; + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_CERTIFICATE, e); + return null; } // 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 @@ -189,7 +203,6 @@ public abstract class SourceStampVerifier { new GuaranteedEncodedFormX509Certificate( sourceStampCertificate, sourceStampEncodedCertificate); result.certs.add(sourceStampCertificate); - // Verify the SourceStamp certificate found in the signing block is the same as the // SourceStamp certificate found in the APK. MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); @@ -197,55 +210,71 @@ public abstract 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)); - return; + toHex(sourceStampBlockCertificateDigest), + toHex(sourceStampCertificateDigest)); + return null; } + return sourceStampCertificate; + } + private static void verifySourceStampSignature( + byte[] data, + int minSdkVersion, + int maxSdkVersion, + X509Certificate sourceStampCertificate, + ByteBuffer signatures, + ApkSignerInfo result) { // Parse the signatures block and identify supported signatures - ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); 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); - result.signatures.add( - new ApkSigningBlockUtils.Result.SignerInfo.Signature( - sigAlgorithmId, sigBytes)); + byte[] sigBytes = readLengthPrefixedByteArray(signature); SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.findById(sigAlgorithmId); if (signatureAlgorithm == null) { - result.addWarning( - ApkVerifier.Issue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM, sigAlgorithmId); + result.addInfoMessage( + 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); + result.addWarning( + ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE, signatureCount); return; } } - if (result.signatures.isEmpty()) { - result.addWarning(ApkVerifier.Issue.SOURCE_STAMP_NO_SIGNATURE); + if (supportedSignatures.isEmpty()) { + 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(); @@ -258,21 +287,78 @@ public abstract class SourceStampVerifier { if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } - sig.update(digestBytes); + 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; } - result.verifiedSignatures.put(signatureAlgorithm, sigBytes); } catch (InvalidKeyException | InvalidAlgorithmParameterException - | SignatureException e) { + | 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 if (id == SourceStampConstants.STAMP_TIME_ATTR_ID) { + long timestamp = ByteBuffer.wrap(value).order( + ByteOrder.LITTLE_ENDIAN).getLong(); + if (timestamp > 0) { + result.timestamp = timestamp; + } else { + result.addWarning(ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP, + timestamp); + } + } else { + result.addInfoMessage(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/SourceStampSigner.java b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java index d7e10e7..dee24bd 100644 --- a/src/main/java/com/android/apksig/internal/apk/stamp/SourceStampSigner.java +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampSigner.java @@ -34,7 +34,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; /** * SourceStamp signer. @@ -45,13 +44,15 @@ import java.util.stream.Collectors; * * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing * block. + * + * <p>V1 of the source stamp allows signing the digest of at most one signature scheme only. */ -public abstract class SourceStampSigner { - - public static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e; +public abstract class V1SourceStampSigner { + public static final int V1_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V1_SOURCE_STAMP_BLOCK_ID; /** Hidden constructor to prevent instantiation. */ - private SourceStampSigner() {} + private V1SourceStampSigner() {} public static Pair<byte[], Integer> generateSourceStampBlock( SignerConfig sourceStampSignerConfig, Map<ContentDigestAlgorithm, byte[]> digestInfo) @@ -97,8 +98,8 @@ public abstract class SourceStampSigner { // FORMAT: // * length-prefixed stamp block. - return Pair.of( - encodeAsLengthPrefixedElement(sourceStampSignerBlock), 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 new file mode 100644 index 0000000..c3fdeec --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V1SourceStampVerifier.java @@ -0,0 +1,139 @@ +/* + * 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.ApkSigningBlockUtils.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +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; +import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ContentDigestAlgorithm; +import com.android.apksig.internal.apk.SignatureInfo; +import com.android.apksig.internal.util.Pair; +import com.android.apksig.util.DataSource; + +import java.io.IOException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + * Source Stamp verifier. + * + * <p>V1 of the source stamp verifies the stamp signature of at most one signature scheme. + */ +public abstract class V1SourceStampVerifier { + + /** Hidden constructor to prevent instantiation. */ + private V1SourceStampVerifier() {} + + /** + * 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 + * {@code true}. If verification fails, the result will contain errors -- see {@link + * ApkSigningBlockUtils.Result#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 + * found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigningBlockUtils.Result verify( + DataSource apk, + ApkUtils.ZipSections zipSections, + byte[] sourceStampCertificateDigest, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + int minSdkVersion, + int maxSdkVersion) + throws IOException, NoSuchAlgorithmException, + ApkSigningBlockUtils.SignatureNotFoundException { + ApkSigningBlockUtils.Result result = + new ApkSigningBlockUtils.Result(ApkSigningBlockUtils.VERSION_SOURCE_STAMP); + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature( + apk, zipSections, V1_SOURCE_STAMP_BLOCK_ID, result); + + verify( + signatureInfo.signatureBlock, + sourceStampCertificateDigest, + apkContentDigests, + minSdkVersion, + maxSdkVersion, + result); + return result; + } + + /** + * 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 + * more information about the contract of this method. + */ + private static void verify( + ByteBuffer sourceStampBlock, + byte[] sourceStampCertificateDigest, + Map<ContentDigestAlgorithm, byte[]> apkContentDigests, + int minSdkVersion, + int maxSdkVersion, + ApkSigningBlockUtils.Result result) + throws NoSuchAlgorithmException { + ApkSigningBlockUtils.Result.SignerInfo signerInfo = + new ApkSigningBlockUtils.Result.SignerInfo(); + result.signers.add(signerInfo); + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + byte[] digestBytes = + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + getApkDigests(apkContentDigests)); + SourceStampVerifier.verifyV1SourceStamp( + sourceStampBlockData, + certFactory, + signerInfo, + digestBytes, + sourceStampCertificateDigest, + minSdkVersion, + maxSdkVersion); + result.verified = !result.containsErrors() && !result.containsWarnings(); + } 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); + } + } + + private static List<Pair<Integer, byte[]>> getApkDigests( + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest : + apkContentDigests.entrySet()) { + digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); + } + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); + return digests; + } +} 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 new file mode 100644 index 0000000..ee3f840 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampSigner.java @@ -0,0 +1,286 @@ +/* + * 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.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.ApkSigningBlockUtils.encodeAsLengthPrefixedElement; +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.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * SourceStamp signer. + * + * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution. + * + * <p>The stamp is part of the APK that is protected by the signing block. + * + * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing + * block. + * + * <p>V2 of the source stamp allows signing the digests of more than one signature schemes. + */ +public class V2SourceStampSigner { + public static final int V2_SOURCE_STAMP_BLOCK_ID = + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + + private final SignerConfig mSourceStampSignerConfig; + private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos; + private final boolean mSourceStampTimestampEnabled; + + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampSigner(Builder builder) { + mSourceStampSignerConfig = builder.mSourceStampSignerConfig; + mSignatureSchemeDigestInfos = builder.mSignatureSchemeDigestInfos; + mSourceStampTimestampEnabled = builder.mSourceStampTimestampEnabled; + } + + public static Pair<byte[], Integer> generateSourceStampBlock( + SignerConfig sourceStampSignerConfig, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + return new Builder(sourceStampSignerConfig, + signatureSchemeDigestInfos).build().generateSourceStampBlock(); + } + + public Pair<byte[], Integer> generateSourceStampBlock() + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + if (mSourceStampSignerConfig.certificates.isEmpty()) { + throw new SignatureException("No certificates configured for signer"); + } + + // Extract the digests for signature schemes. + List<Pair<Integer, byte[]>> signatureSchemeDigests = new ArrayList<>(); + getSignedDigestsFor( + VERSION_APK_SIGNATURE_SCHEME_V3, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + getSignedDigestsFor( + VERSION_APK_SIGNATURE_SCHEME_V2, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + getSignedDigestsFor( + VERSION_JAR_SIGNATURE_SCHEME, + mSignatureSchemeDigestInfos, + mSourceStampSignerConfig, + signatureSchemeDigests); + Collections.sort(signatureSchemeDigests, Comparator.comparing(Pair::getFirst)); + + SourceStampBlock sourceStampBlock = new SourceStampBlock(); + + try { + sourceStampBlock.stampCertificate = + mSourceStampSignerConfig.certificates.get(0).getEncoded(); + } catch (CertificateEncodingException e) { + throw new SignatureException( + "Retrieving the encoded form of the stamp certificate failed", e); + } + + sourceStampBlock.signedDigests = signatureSchemeDigests; + + sourceStampBlock.stampAttributes = encodeStampAttributes( + generateStampAttributes(mSourceStampSignerConfig.mSigningCertificateLineage)); + sourceStampBlock.signedStampAttributes = + ApkSigningBlockUtils.generateSignaturesOverData(mSourceStampSignerConfig, + 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), + sourceStampBlock.stampAttributes, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + sourceStampBlock.signedStampAttributes), + }); + + // FORMAT: + // * length-prefixed stamp block. + return Pair.of(encodeAsLengthPrefixedElement(sourceStampSignerBlock), + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + } + + private static void getSignedDigestsFor( + int signatureSchemeVersion, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos, + SignerConfig mSourceStampSignerConfig, + List<Pair<Integer, byte[]>> signatureSchemeDigests) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { + if (!mSignatureSchemeDigestInfos.containsKey(signatureSchemeVersion)) { + return; + } + + Map<ContentDigestAlgorithm, byte[]> digestInfo = + mSignatureSchemeDigestInfos.get(signatureSchemeVersion); + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> digest : digestInfo.entrySet()) { + digests.add(Pair.of(digest.getKey().getId(), digest.getValue())); + } + Collections.sort(digests, Comparator.comparing(Pair::getFirst)); + + // FORMAT: + // * length-prefixed sequence of length-prefixed digests: + // * uint32: digest algorithm id + // * length-prefixed bytes: digest of the respective digest algorithm + byte[] digestBytes = + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(digests); + + // FORMAT: + // * length-prefixed sequence of length-prefixed signed digests: + // * uint32: signature algorithm id + // * length-prefixed bytes: signed digest for the respective signature algorithm + List<Pair<Integer, byte[]>> signedDigest = + ApkSigningBlockUtils.generateSignaturesOverData( + mSourceStampSignerConfig, digestBytes); + + // FORMAT: + // * length-prefixed sequence of length-prefixed signed signature scheme digests: + // * uint32: signature scheme id + // * length-prefixed bytes: signed digests for the respective signature scheme + signatureSchemeDigests.add( + Pair.of( + signatureSchemeVersion, + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( + 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 Map<Integer, byte[]> generateStampAttributes(SigningCertificateLineage lineage) { + HashMap<Integer, byte[]> stampAttributes = new HashMap<>(); + + if (mSourceStampTimestampEnabled) { + // Write the current epoch time as the timestamp for the source stamp. + long timestamp = Instant.now().getEpochSecond(); + if (timestamp > 0) { + ByteBuffer attributeBuffer = ByteBuffer.allocate(8); + attributeBuffer.order(ByteOrder.LITTLE_ENDIAN); + attributeBuffer.putLong(timestamp); + stampAttributes.put(SourceStampConstants.STAMP_TIME_ATTR_ID, + attributeBuffer.array()); + } else { + // The epoch time should never be <= 0, and since security decisions can potentially + // be made based on the value in the timestamp, throw an Exception to ensure the + // issues with the environment are resolved before allowing the signing. + throw new IllegalStateException( + "Received an invalid value from Instant#getTimestamp: " + timestamp); + } + } + + 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; + } + + /** Builder of {@link V2SourceStampSigner} instances. */ + public static class Builder { + private final SignerConfig mSourceStampSignerConfig; + private final Map<Integer, Map<ContentDigestAlgorithm, byte[]>> mSignatureSchemeDigestInfos; + private boolean mSourceStampTimestampEnabled = true; + + /** + * Instantiates a new {@code Builder} with the provided {@code sourceStampSignerConfig} + * and the {@code signatureSchemeDigestInfos}. + */ + public Builder(SignerConfig sourceStampSignerConfig, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeDigestInfos) { + mSourceStampSignerConfig = sourceStampSignerConfig; + mSignatureSchemeDigestInfos = signatureSchemeDigestInfos; + } + + /** + * Sets whether the source stamp should contain the timestamp attribute with the time + * at which the source stamp was signed. + */ + public Builder setSourceStampTimestampEnabled(boolean value) { + mSourceStampTimestampEnabled = value; + return this; + } + + /** + * Builds a new V2SourceStampSigner that can be used to generate a new source stamp + * block signed with the specified signing config. + */ + public V2SourceStampSigner build() { + return new V2SourceStampSigner(this); + } + } +} 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 new file mode 100644 index 0000000..a215b98 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/stamp/V2SourceStampVerifier.java @@ -0,0 +1,159 @@ +/* + * 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.encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes; +import static com.android.apksig.internal.apk.stamp.SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID; + +import com.android.apksig.ApkVerificationIssue; +import com.android.apksig.Constants; +import com.android.apksig.apk.ApkFormatException; +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; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Source Stamp verifier. + * + * <p>V2 of the source stamp verifies the stamp signature of more than one signature schemes. + */ +public abstract class V2SourceStampVerifier { + + /** Hidden constructor to prevent instantiation. */ + private V2SourceStampVerifier() {} + + /** + * Verifies the provided APK's SourceStamp signatures and returns the result of verification. + * The APK must be considered verified only if {@link ApkSigResult#verified} is + * {@code true}. If verification fails, the result will contain errors -- see {@link + * ApkSigResult#getErrors()}. + * + * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a + * required cryptographic algorithm implementation is missing + * @throws SignatureNotFoundException if no SourceStamp signatures are + * found + * @throws IOException if an I/O error occurs when reading the APK + */ + public static ApkSigResult verify( + DataSource apk, + ZipSections zipSections, + byte[] sourceStampCertificateDigest, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, + int minSdkVersion, + int maxSdkVersion) + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + ApkSigResult result = + new ApkSigResult(Constants.VERSION_SOURCE_STAMP); + SignatureInfo signatureInfo = + ApkSigningBlockUtilsLite.findSignature( + apk, zipSections, V2_SOURCE_STAMP_BLOCK_ID); + + verify( + signatureInfo.signatureBlock, + sourceStampCertificateDigest, + signatureSchemeApkContentDigests, + minSdkVersion, + maxSdkVersion, + result); + return result; + } + + /** + * 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, ZipSections, byte[], Map, int, int)} for + * more information about the contract of this method. + */ + private static void verify( + ByteBuffer sourceStampBlock, + byte[] sourceStampCertificateDigest, + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests, + int minSdkVersion, + int maxSdkVersion, + ApkSigResult result) + throws NoSuchAlgorithmException { + ApkSignerInfo signerInfo = new ApkSignerInfo(); + result.mSigners.add(signerInfo); + try { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtilsLite.getLengthPrefixedSlice(sourceStampBlock); + SourceStampVerifier.verifyV2SourceStamp( + sourceStampBlockData, + certFactory, + signerInfo, + getSignatureSchemeDigests(signatureSchemeApkContentDigests), + sourceStampCertificateDigest, + minSdkVersion, + maxSdkVersion); + result.verified = !result.containsErrors() && !result.containsWarnings(); + } catch (CertificateException e) { + throw new IllegalStateException("Failed to obtain X.509 CertificateFactory", e); + } catch (ApkFormatException | BufferUnderflowException e) { + signerInfo.addWarning(ApkVerificationIssue.SOURCE_STAMP_MALFORMED_SIGNATURE); + } + } + + private static Map<Integer, byte[]> getSignatureSchemeDigests( + Map<Integer, Map<ContentDigestAlgorithm, byte[]>> signatureSchemeApkContentDigests) { + Map<Integer, byte[]> digests = new HashMap<>(); + for (Map.Entry<Integer, Map<ContentDigestAlgorithm, byte[]>> + signatureSchemeApkContentDigest : signatureSchemeApkContentDigests.entrySet()) { + List<Pair<Integer, byte[]>> apkDigests = + getApkDigests(signatureSchemeApkContentDigest.getValue()); + digests.put( + signatureSchemeApkContentDigest.getKey(), + encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes(apkDigests)); + } + return digests; + } + + private static List<Pair<Integer, byte[]>> getApkDigests( + Map<ContentDigestAlgorithm, byte[]> apkContentDigests) { + List<Pair<Integer, byte[]>> digests = new ArrayList<>(); + for (Map.Entry<ContentDigestAlgorithm, byte[]> apkContentDigest : + apkContentDigests.entrySet()) { + digests.add(Pair.of(apkContentDigest.getKey().getId(), apkContentDigest.getValue())); + } + Collections.sort(digests, new Comparator<Pair<Integer, byte[]>>() { + @Override + public int compare(Pair<Integer, byte[]> pair1, Pair<Integer, byte[]> pair2) { + return pair1.getFirst() - pair2.getFirst(); + } + }); + return digests; + } +} 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..ee3c9d8 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 @@ -16,6 +16,7 @@ package com.android.apksig.internal.apk.v1; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoDigestAlgorithmOid; import static com.android.apksig.internal.pkcs7.AlgorithmIdentifier.getSignerInfoSignatureAlgorithm; @@ -59,17 +60,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. @@ -91,6 +90,11 @@ public abstract class V1SchemeSigner { * Digest algorithm used for the signature. */ public DigestAlgorithm signatureDigestAlgorithm; + + /** + * If DSA is the signing algorithm, whether or not deterministic DSA signing should be used. + */ + public boolean deterministicDsaSigning; } /** Hidden constructor to prevent instantiation. */ @@ -108,7 +112,7 @@ public abstract class V1SchemeSigner { public static DigestAlgorithm getSuggestedSignatureDigestAlgorithm( PublicKey signingKey, int minSdkVersion) throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals((keyAlgorithm))) { // Prior to API Level 18, only SHA-1 can be used with RSA. if (minSdkVersion < 18) { return DigestAlgorithm.SHA1; @@ -303,7 +307,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 +325,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; } @@ -497,7 +501,8 @@ public abstract class V1SchemeSigner { PublicKey publicKey = signingCert.getPublicKey(); DigestAlgorithm digestAlgorithm = signerConfig.signatureDigestAlgorithm; Pair<String, AlgorithmIdentifier> signatureAlgs = - getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm); + getSignerInfoSignatureAlgorithm(publicKey, digestAlgorithm, + signerConfig.deterministicDsaSigning); String jcaSignatureAlgorithm = signatureAlgs.getFirst(); // Generate the cryptographic signature of the signature file 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..0ebef0e 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 @@ -26,6 +26,7 @@ import com.android.apksig.ApkVerifier.Issue; import com.android.apksig.ApkVerifier.IssueWithParams; import com.android.apksig.apk.ApkFormatException; import com.android.apksig.apk.ApkUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils; import com.android.apksig.internal.asn1.Asn1BerParser; import com.android.apksig.internal.asn1.Asn1Class; import com.android.apksig.internal.asn1.Asn1DecodingException; @@ -46,21 +47,25 @@ 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.KeyFactory; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -82,9 +87,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 +233,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; } @@ -679,7 +682,27 @@ public abstract class V1SchemeVerifier { String jcaSignatureAlgorithm = getJcaSignatureAlgorithm(digestAlgorithmOid, signatureAlgorithmOid); Signature s = Signature.getInstance(jcaSignatureAlgorithm); - s.initVerify(signingCertificate.getPublicKey()); + PublicKey publicKey = signingCertificate.getPublicKey(); + try { + s.initVerify(publicKey); + } catch (InvalidKeyException e) { + // An InvalidKeyException could be caught if the PublicKey in the certificate is not + // properly encoded; attempt to resolve any encoding errors, generate a new public + // key, and reattempt the initVerify with the newly encoded key. + try { + byte[] encodedPublicKey = ApkSigningBlockUtils.encodePublicKey(publicKey); + publicKey = KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic( + new X509EncodedKeySpec(encodedPublicKey)); + } catch (InvalidKeySpecException ikse) { + // If an InvalidKeySpecException is caught then throw the original Exception + // since the key couldn't be properly re-encoded, and the original Exception + // will have more useful debugging info. + throw e; + } + s = Signature.getInstance(jcaSignatureAlgorithm); + s.initVerify(publicKey); + } + if (signerInfo.signedAttrs != null) { // Signed attributes present -- verify signature against the ASN.1 DER encoded form // of signed attributes. This verifies integrity of the signature file because @@ -939,7 +962,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 +1072,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 +1262,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 +1366,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 10488ed..b69b7d3 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() {} @@ -84,8 +85,8 @@ public abstract class V2SchemeSigner { * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK * Signature Scheme v2 */ - public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms( - PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported) + public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); if ("RSA".equalsIgnoreCase(keyAlgorithm)) { @@ -99,7 +100,7 @@ public abstract class V2SchemeSigner { // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. List<SignatureAlgorithm> algorithms = new ArrayList<>(); algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256); } return algorithms; @@ -111,8 +112,11 @@ public abstract class V2SchemeSigner { } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { // DSA is supported only with SHA-256. List<SignatureAlgorithm> algorithms = new ArrayList<>(); - algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); } return algorithms; @@ -123,7 +127,7 @@ public abstract class V2SchemeSigner { // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. List<SignatureAlgorithm> algorithms = new ArrayList<>(); algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256); } return algorithms; @@ -138,13 +142,27 @@ public abstract class V2SchemeSigner { } public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests + generateApkSignatureSchemeV2Block(RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs, + boolean v3SigningEnabled) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, + SignatureException { + return generateApkSignatureSchemeV2Block(executor, beforeCentralDir, centralDir, eocd, + signerConfigs, v3SigningEnabled, null); + } + + public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests generateApkSignatureSchemeV2Block( RunnablesExecutor executor, DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, List<SignerConfig> signerConfigs, - boolean v3SigningEnabled) + boolean v3SigningEnabled, + List<byte[]> preservedV2SignerBlocks) throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo = @@ -152,19 +170,24 @@ public abstract class V2SchemeSigner { executor, beforeCentralDir, centralDir, eocd, signerConfigs); return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests( generateApkSignatureSchemeV2Block( - digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled), + digestInfo.getFirst(), digestInfo.getSecond(), v3SigningEnabled, + preservedV2SignerBlocks), digestInfo.getSecond()); } private static Pair<byte[], Integer> generateApkSignatureSchemeV2Block( List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests, - boolean v3SigningEnabled) + boolean v3SigningEnabled, + List<byte[]> preservedV2SignerBlocks) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size()); + if (preservedV2SignerBlocks != null && preservedV2SignerBlocks.size() > 0) { + signerBlocks.addAll(preservedV2SignerBlocks); + } int signerNumber = 0; for (SignerConfig signerConfig : signerConfigs) { signerNumber++; @@ -184,7 +207,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( @@ -264,9 +287,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): @@ -277,7 +297,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..6963dd3 --- /dev/null +++ b/src/main/java/com/android/apksig/internal/apk/v3/V3SchemeConstants.java @@ -0,0 +1,55 @@ +/* + * 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; + +import com.android.apksig.internal.util.AndroidSdkVersion; + +/** 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 APK_SIGNATURE_SCHEME_V31_BLOCK_ID = 0x1b93ad61; + public static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; + + public static final int MIN_SDK_WITH_V3_SUPPORT = AndroidSdkVersion.P; + public static final int MIN_SDK_WITH_V31_SUPPORT = AndroidSdkVersion.T; + /** + * By default, APK signing key rotation will target T, but packages that have previously + * rotated can continue rotating on pre-T by specifying an SDK version <= 32 as the + * --rotation-min-sdk-version parameter when using apksigner or when invoking + * {@link com.android.apksig.ApkSigner.Builder#setMinSdkVersionForRotation(int)}. + */ + public static final int DEFAULT_ROTATION_MIN_SDK_VERSION = AndroidSdkVersion.T; + + /** + * This attribute is intended to be written to the V3.0 signer block as an additional attribute + * whose value is the minimum SDK version supported for rotation by the V3.1 signing block. If + * this value is set to X and a v3.1 signing block does not exist, or the minimum SDK version + * for rotation in the v3.1 signing block is not X, then the APK should be rejected. + */ + public static final int ROTATION_MIN_SDK_VERSION_ATTR_ID = 0x559f8b02; + + /** + * This attribute is written to the V3.1 signer block as an additional attribute to signify that + * the rotation-min-sdk-version is targeting a development release. This is required to support + * testing rotation on new development releases as the previous platform release SDK version + * is used as the development release SDK version until the development release SDK is + * finalized. + */ + public static final int ROTATION_ON_DEV_RELEASE_ATTR_ID = 0xc2a6b3ba; +} 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 15d5481..ee5d3b4 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 @@ -24,12 +24,14 @@ import static com.android.apksig.internal.apk.ApkSigningBlockUtils.encodePublicK import com.android.apksig.SigningCertificateLineage; import com.android.apksig.internal.apk.ApkSigningBlockUtils; +import com.android.apksig.internal.apk.ApkSigningBlockUtils.SigningSchemeBlockAndDigests; import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignerConfig; import com.android.apksig.internal.apk.ContentDigestAlgorithm; import com.android.apksig.internal.apk.SignatureAlgorithm; import com.android.apksig.internal.util.Pair; import com.android.apksig.util.DataSource; import com.android.apksig.util.RunnablesExecutor; + import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -44,6 +46,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.OptionalInt; /** * APK Signature Scheme v3 signer. @@ -56,12 +59,37 @@ import java.util.Map; * SigningCertificateLineage}, which enables an APK to change its signing certificate as long as * it can prove the new siging certificate was signed by the old. */ -public abstract class V3SchemeSigner { +public class V3SchemeSigner { + 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 APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; + private final RunnablesExecutor mExecutor; + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List<SignerConfig> mSignerConfigs; + private final int mBlockId; + private final OptionalInt mOptionalRotationMinSdkVersion; + private final boolean mRotationTargetsDevRelease; - /** Hidden constructor to prevent instantiation. */ - private V3SchemeSigner() {} + private V3SchemeSigner(DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs, + RunnablesExecutor executor, + int blockId, + OptionalInt optionalRotationMinSdkVersion, + boolean rotationTargetsDevRelease) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + mExecutor = executor; + mBlockId = blockId; + mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion; + mRotationTargetsDevRelease = rotationTargetsDevRelease; + } /** * Gets the APK Signature Scheme v3 signature algorithms to be used for signing an APK using the @@ -72,8 +100,8 @@ public abstract class V3SchemeSigner { * @throws InvalidKeyException if the provided key is not suitable for signing APKs using APK * Signature Scheme v3 */ - public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms( - PublicKey signingKey, int minSdkVersion, boolean apkSigningBlockPaddingSupported) + public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, + int minSdkVersion, boolean verityEnabled, boolean deterministicDsaSigning) throws InvalidKeyException { String keyAlgorithm = signingKey.getAlgorithm(); if ("RSA".equalsIgnoreCase(keyAlgorithm)) { @@ -87,7 +115,7 @@ public abstract class V3SchemeSigner { // 3072-bit RSA is roughly 128-bit strong, meaning SHA-256 is a good fit. List<SignatureAlgorithm> algorithms = new ArrayList<>(); algorithms.add(SignatureAlgorithm.RSA_PKCS1_V1_5_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_RSA_PKCS1_V1_5_WITH_SHA256); } return algorithms; @@ -99,8 +127,11 @@ public abstract class V3SchemeSigner { } else if ("DSA".equalsIgnoreCase(keyAlgorithm)) { // DSA is supported only with SHA-256. List<SignatureAlgorithm> algorithms = new ArrayList<>(); - algorithms.add(SignatureAlgorithm.DSA_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + algorithms.add( + deterministicDsaSigning ? + SignatureAlgorithm.DETDSA_WITH_SHA256 : + SignatureAlgorithm.DSA_WITH_SHA256); + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_DSA_WITH_SHA256); } return algorithms; @@ -111,7 +142,7 @@ public abstract class V3SchemeSigner { // 256-bit Elliptic Curve is roughly 128-bit strong, meaning SHA-256 is a good fit. List<SignatureAlgorithm> algorithms = new ArrayList<>(); algorithms.add(SignatureAlgorithm.ECDSA_WITH_SHA256); - if (apkSigningBlockPaddingSupported) { + if (verityEnabled) { algorithms.add(SignatureAlgorithm.VERITY_ECDSA_WITH_SHA256); } return algorithms; @@ -125,31 +156,92 @@ public abstract class V3SchemeSigner { } } - public static ApkSigningBlockUtils.SigningSchemeBlockAndDigests - generateApkSignatureSchemeV3Block( - RunnablesExecutor executor, - DataSource beforeCentralDir, - DataSource centralDir, - DataSource eocd, - List<SignerConfig> signerConfigs) - throws IOException, InvalidKeyException, NoSuchAlgorithmException, - SignatureException { + public static SigningSchemeBlockAndDigests generateApkSignatureSchemeV3Block( + RunnablesExecutor executor, + DataSource beforeCentralDir, + DataSource centralDir, + DataSource eocd, + List<SignerConfig> signerConfigs) + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { + return new V3SchemeSigner.Builder(beforeCentralDir, centralDir, eocd, signerConfigs) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .generateApkSignatureSchemeV3BlockAndDigests(); + } + + 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 byte[] generateV3RotationMinSdkVersionStrippingProtectionAttribute( + int rotationMinSdkVersion) { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - int32 representing minimum SDK version for rotation + int payloadSize = 4 + 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID); + result.putInt(rotationMinSdkVersion); + return result.array(); + } + + private static byte[] generateV31RotationTargetsDevReleaseAttribute() { + // FORMAT (little endian): + // * length-prefixed bytes: attribute pair + // * uint32: ID + // * bytes: value - No value is used for this attribute + int payloadSize = 4 + 4; + ByteBuffer result = ByteBuffer.allocate(payloadSize); + result.order(ByteOrder.LITTLE_ENDIAN); + result.putInt(payloadSize - 4); + result.putInt(V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID); + return result.array(); + } + + /** + * Generates and returns a new {@link SigningSchemeBlockAndDigests} containing the V3.x + * signing scheme block and digests based on the parameters provided to the {@link Builder}. + * + * @throws IOException if an I/O error occurs + * @throws NoSuchAlgorithmException if a required cryptographic algorithm implementation is + * missing + * @throws InvalidKeyException if the X.509 encoded form of the public key cannot be obtained + * @throws SignatureException if an error occurs when computing digests or generating + * signatures + */ + public SigningSchemeBlockAndDigests generateApkSignatureSchemeV3BlockAndDigests() + throws IOException, InvalidKeyException, NoSuchAlgorithmException, SignatureException { Pair<List<SignerConfig>, Map<ContentDigestAlgorithm, byte[]>> digestInfo = ApkSigningBlockUtils.computeContentDigests( - executor, beforeCentralDir, centralDir, eocd, signerConfigs); - return new ApkSigningBlockUtils.SigningSchemeBlockAndDigests( - generateApkSignatureSchemeV3Block(digestInfo.getFirst(), digestInfo.getSecond()), - digestInfo.getSecond()); + mExecutor, mBeforeCentralDir, mCentralDir, mEocd, mSignerConfigs); + return new SigningSchemeBlockAndDigests( + generateApkSignatureSchemeV3Block(digestInfo.getSecond()), digestInfo.getSecond()); } - private static Pair<byte[], Integer> generateApkSignatureSchemeV3Block( - List<SignerConfig> signerConfigs, Map<ContentDigestAlgorithm, byte[]> contentDigests) + private Pair<byte[], Integer> generateApkSignatureSchemeV3Block( + Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { // FORMAT: // * length-prefixed sequence of length-prefixed signer blocks. - List<byte[]> signerBlocks = new ArrayList<>(signerConfigs.size()); + List<byte[]> signerBlocks = new ArrayList<>(mSignerConfigs.size()); int signerNumber = 0; - for (SignerConfig signerConfig : signerConfigs) { + for (SignerConfig signerConfig : mSignerConfigs) { signerNumber++; byte[] signerBlock; try { @@ -167,10 +259,10 @@ public abstract class V3SchemeSigner { new byte[][] { encodeAsSequenceOfLengthPrefixedElements(signerBlocks), }), - APK_SIGNATURE_SCHEME_V3_BLOCK_ID); + mBlockId); } - private static byte[] generateSignerBlock( + private byte[] generateSignerBlock( SignerConfig signerConfig, Map<ContentDigestAlgorithm, byte[]> contentDigests) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { if (signerConfig.certificates.isEmpty()) { @@ -220,7 +312,7 @@ public abstract class V3SchemeSigner { return encodeSigner(signer); } - private static byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) { + private byte[] encodeSigner(V3SignatureSchemeBlock.Signer signer) { byte[] signedData = encodeAsLengthPrefixedElement(signer.signedData); byte[] signatures = encodeAsLengthPrefixedElement( @@ -249,7 +341,7 @@ public abstract class V3SchemeSigner { return result.array(); } - private static byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) { + private byte[] encodeSignedData(V3SignatureSchemeBlock.SignedData signedData) { byte[] digests = encodeAsLengthPrefixedElement( encodeAsSequenceOfLengthPrefixedPairsOfIntAndLengthPrefixedBytes( @@ -285,13 +377,26 @@ 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]; + private byte[] generateAdditionalAttributes(SignerConfig signerConfig) { + if (signerConfig.mSigningCertificateLineage != null) { + byte[] lineageAttr = generateV3SignerAttribute(signerConfig.mSigningCertificateLineage); + // If this rotation is not targeting a development release, or if this is not a v3.1 + // signer block then just return the lineage attribute. + if (!mRotationTargetsDevRelease + || mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + return lineageAttr; + } + byte[] devReleaseRotationAttr = generateV31RotationTargetsDevReleaseAttribute(); + byte[] attributes = new byte[lineageAttr.length + devReleaseRotationAttr.length]; + System.arraycopy(lineageAttr, 0, attributes, 0, lineageAttr.length); + System.arraycopy(devReleaseRotationAttr, 0, attributes, lineageAttr.length, + devReleaseRotationAttr.length); + return attributes; + } else if (mOptionalRotationMinSdkVersion.isPresent()) { + return generateV3RotationMinSdkVersionStrippingProtectionAttribute( + mOptionalRotationMinSdkVersion.getAsInt()); } - return signerConfig.mSigningCertificateLineage.generateV3SignerAttribute(); + return new byte[0]; } private static final class V3SignatureSchemeBlock { @@ -311,4 +416,97 @@ public abstract class V3SchemeSigner { public byte[] additionalAttributes; } } + + /** Builder of {@link V3SchemeSigner} instances. */ + public static class Builder { + private final DataSource mBeforeCentralDir; + private final DataSource mCentralDir; + private final DataSource mEocd; + private final List<SignerConfig> mSignerConfigs; + + private RunnablesExecutor mExecutor = RunnablesExecutor.MULTI_THREADED; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty(); + private boolean mRotationTargetsDevRelease = false; + + /** + * Instantiates a new {@code Builder} with an APK's {@code beforeCentralDir}, {@code + * centralDir}, and {@code eocd}, along with a {@link List} of {@code signerConfigs} to + * be used to sign the APK. + */ + public Builder(DataSource beforeCentralDir, DataSource centralDir, DataSource eocd, + List<SignerConfig> signerConfigs) { + mBeforeCentralDir = beforeCentralDir; + mCentralDir = centralDir; + mEocd = eocd; + mSignerConfigs = signerConfigs; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when computing the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the {@code blockId} to be used for the V3 signature block. + * + * <p>This {@code V3SchemeSigner} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be written as an additional attribute in each + * signer's block. + * + * <p>This value provides stripping protection to ensure a v3.1 signing block with rotation + * is not modified or removed from the APK's signature block. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion); + return this; + } + + /** + * Sets whether the minimum SDK version of a signer is intended to target a development + * release; this is primarily required after the T SDK is finalized, and an APK needs to + * target U during its development cycle for rotation. + * + * <p>This is only required after the T SDK is finalized since S and earlier releases do + * not know about the V3.1 block ID, but once T is released and work begins on U, U will + * use the SDK version of T during development. A signer with a minimum SDK version of T's + * SDK version along with setting {@code enabled} to true will allow an APK to use the + * rotated key on a device running U while causing this to be bypassed for T. + * + * <p><em>Note:</em>If the rotation-min-sdk-version is less than or equal to 32 (Android + * Sv2), then the rotated signing key will be used in the v3.0 signing block and this call + * will be a noop. + */ + public Builder setRotationTargetsDevRelease(boolean enabled) { + mRotationTargetsDevRelease = enabled; + return this; + } + + /** + * Returns a new {@link V3SchemeSigner} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeSigner build() { + return new V3SchemeSigner(mBeforeCentralDir, + mCentralDir, + mEocd, + mSignerConfigs, + mExecutor, + mBlockId, + mOptionalRotationMinSdkVersion, + mRotationTargetsDevRelease); + } + } } 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..956027f 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 @@ -28,15 +28,16 @@ import com.android.apksig.internal.apk.ApkSigningBlockUtils.SignatureNotFoundExc 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.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; +import java.nio.ByteOrder; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; @@ -53,7 +54,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.Map; +import java.util.OptionalInt; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -67,12 +68,42 @@ 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() {} +public class V3SchemeVerifier { + private final RunnablesExecutor mExecutor; + private final DataSource mApk; + private final ApkUtils.ZipSections mZipSections; + private final ApkSigningBlockUtils.Result mResult; + private final Set<ContentDigestAlgorithm> mContentDigestsToVerify; + private final int mMinSdkVersion; + private final int mMaxSdkVersion; + private final int mBlockId; + private final OptionalInt mOptionalRotationMinSdkVersion; + private final boolean mFullVerification; + + private ByteBuffer mApkSignatureSchemeV3Block; + + private V3SchemeVerifier( + RunnablesExecutor executor, + DataSource apk, + ApkUtils.ZipSections zipSections, + Set<ContentDigestAlgorithm> contentDigestsToVerify, + ApkSigningBlockUtils.Result result, + int minSdkVersion, + int maxSdkVersion, + int blockId, + OptionalInt optionalRotationMinSdkVersion, + boolean fullVerification) { + mExecutor = executor; + mApk = apk; + mZipSections = zipSections; + mContentDigestsToVerify = contentDigestsToVerify; + mResult = result; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + mBlockId = blockId; + mOptionalRotationMinSdkVersion = optionalRotationMinSdkVersion; + mFullVerification = fullVerification; + } /** * Verifies the provided APK's APK Signature Scheme v3 signatures and returns the result of @@ -87,7 +118,10 @@ public abstract class V3SchemeVerifier { * this method returns a result with one or more errors and whose * {@code Result.verified == false}, or this method throws an exception. * - * @throws ApkFormatException if the APK is malformed + * <p>This method only verifies the v3.0 signing block without platform targeted rotation from + * a v3.1 signing block. To verify a v3.1 signing block, or a v3.0 signing block in the presence + * of a v3.1 block, configure a new {@link V3SchemeVerifier} using the {@code Builder}. + * * @throws NoSuchAlgorithmException if the APK's signatures cannot be verified because a * required cryptographic algorithm implementation is missing * @throws SignatureNotFoundException if no APK Signature Scheme v3 @@ -101,34 +135,11 @@ public abstract class V3SchemeVerifier { int minSdkVersion, int maxSdkVersion) throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { - ApkSigningBlockUtils.Result result = new ApkSigningBlockUtils.Result( - ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); - SignatureInfo signatureInfo = - ApkSigningBlockUtils.findSignature(apk, zipSections, - APK_SIGNATURE_SCHEME_V3_BLOCK_ID, result); - - DataSource beforeApkSigningBlock = apk.slice(0, signatureInfo.apkSigningBlockOffset); - DataSource centralDir = - apk.slice( - signatureInfo.centralDirOffset, - signatureInfo.eocdOffset - signatureInfo.centralDirOffset); - ByteBuffer eocd = signatureInfo.eocd; - - // v3 didn't exist prior to P, so make sure that we're only judging v3 on its supported - // platforms - if (minSdkVersion < AndroidSdkVersion.P) { - minSdkVersion = AndroidSdkVersion.P; - } - - verify(executor, - beforeApkSigningBlock, - signatureInfo.signatureBlock, - centralDir, - eocd, - minSdkVersion, - maxSdkVersion, - result); - return result; + return new V3SchemeVerifier.Builder(apk, zipSections, minSdkVersion, maxSdkVersion) + .setRunnablesExecutor(executor) + .setBlockId(V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID) + .build() + .verify(); } /** @@ -137,33 +148,40 @@ public abstract class V3SchemeVerifier { * {@code result}. See {@link #verify(RunnablesExecutor, DataSource, ApkUtils.ZipSections, int, * int)} for more information about the contract of this method. * - * @param result result populated by this method with interesting information about the APK, - * such as information about signers, and verification errors and warnings. + * @return {@link ApkSigningBlockUtils.Result} populated with interesting information about the + * APK, such as information about signers, and verification errors and warnings */ - private static void verify( - RunnablesExecutor executor, - DataSource beforeApkSigningBlock, - ByteBuffer apkSignatureSchemeV3Block, - DataSource centralDir, - ByteBuffer eocd, - int minSdkVersion, - int maxSdkVersion, - ApkSigningBlockUtils.Result result) - throws IOException, NoSuchAlgorithmException { - Set<ContentDigestAlgorithm> contentDigestsToVerify = new HashSet<>(1); - parseSigners(apkSignatureSchemeV3Block, contentDigestsToVerify, result); + public ApkSigningBlockUtils.Result verify() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { + if (mApk == null || mZipSections == null) { + throw new IllegalStateException( + "A non-null apk and zip sections must be specified to verify an APK's v3 " + + "signatures"); + } + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; - if (result.containsErrors()) { - return; + DataSource beforeApkSigningBlock = mApk.slice(0, signatureInfo.apkSigningBlockOffset); + DataSource centralDir = + mApk.slice( + signatureInfo.centralDirOffset, + signatureInfo.eocdOffset - signatureInfo.centralDirOffset); + ByteBuffer eocd = signatureInfo.eocd; + + parseSigners(); + + if (mResult.containsErrors()) { + return mResult; } - ApkSigningBlockUtils.verifyIntegrity( - executor, beforeApkSigningBlock, centralDir, eocd, contentDigestsToVerify, result); + ApkSigningBlockUtils.verifyIntegrity(mExecutor, beforeApkSigningBlock, centralDir, eocd, + mContentDigestsToVerify, mResult); // make sure that the v3 signers cover the entire targeted sdk version ranges and that the // longest SigningCertificateHistory, if present, corresponds to the newest platform // versions SortedMap<Integer, ApkSigningBlockUtils.Result.SignerInfo> sortedSigners = new TreeMap<>(); - for (ApkSigningBlockUtils.Result.SignerInfo signer : result.signers) { + for (ApkSigningBlockUtils.Result.SignerInfo signer : mResult.signers) { sortedSigners.put(signer.minSdkVersion, signer); } @@ -173,7 +191,7 @@ public abstract class V3SchemeVerifier { int lastLineageSize = 0; // while we're iterating through the signers, build up the list of lineages - List<SigningCertificateLineage> lineages = new ArrayList<>(result.signers.size()); + List<SigningCertificateLineage> lineages = new ArrayList<>(mResult.signers.size()); for (ApkSigningBlockUtils.Result.SignerInfo signer : sortedSigners.values()) { int currentMin = signer.minSdkVersion; @@ -183,7 +201,7 @@ public abstract class V3SchemeVerifier { firstMin = currentMin; } else { if (currentMin != lastMax + 1) { - result.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS); + mResult.addError(Issue.V3_INCONSISTENT_SDK_VERSIONS); break; } } @@ -193,7 +211,7 @@ public abstract class V3SchemeVerifier { if (signer.signingCertificateLineage != null) { int currLineageSize = signer.signingCertificateLineage.size(); if (currLineageSize < lastLineageSize) { - result.addError(Issue.V3_INCONSISTENT_LINEAGES); + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); break; } lastLineageSize = currLineageSize; @@ -201,20 +219,24 @@ public abstract class V3SchemeVerifier { } } - // make sure we support our desired sdk ranges - if (firstMin > minSdkVersion || lastMax < maxSdkVersion) { - result.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax); + // make sure we support our desired sdk ranges; if rotation is present in a v3.1 block + // then the max level only needs to support up to that sdk version for rotation. + if (firstMin > mMinSdkVersion + || lastMax < (mOptionalRotationMinSdkVersion.isPresent() + ? mOptionalRotationMinSdkVersion.getAsInt() - 1 : mMaxSdkVersion)) { + mResult.addError(Issue.V3_MISSING_SDK_VERSIONS, firstMin, lastMax); } try { - result.signingCertificateLineage = + mResult.signingCertificateLineage = SigningCertificateLineage.consolidateLineages(lineages); } catch (IllegalArgumentException e) { - result.addError(Issue.V3_INCONSISTENT_LINEAGES); + mResult.addError(Issue.V3_INCONSISTENT_LINEAGES); } - if (!result.containsErrors()) { - result.verified = true; + if (!mResult.containsErrors()) { + mResult.verified = true; } + return mResult; } /** @@ -233,16 +255,49 @@ public abstract class V3SchemeVerifier { ByteBuffer apkSignatureSchemeV3Block, Set<ContentDigestAlgorithm> contentDigestsToVerify, ApkSigningBlockUtils.Result result) throws NoSuchAlgorithmException { + try { + new V3SchemeVerifier.Builder(apkSignatureSchemeV3Block) + .setResult(result) + .setContentDigestsToVerify(contentDigestsToVerify) + .setFullVerification(false) + .build() + .parseSigners(); + } catch (IOException | SignatureNotFoundException e) { + // This should never occur since the apkSignatureSchemeV3Block was already provided. + throw new IllegalStateException("An exception was encountered when attempting to parse" + + " the signers from the provided APK Signature Scheme v3 block", e); + } + } + + /** + * Parses each signer in the APK Signature Scheme v3 block and populates corresponding + * {@link ApkSigningBlockUtils.Result.SignerInfo} instances in the + * returned {@link ApkSigningBlockUtils.Result}. + * + * <p>This verifies signatures over {@code signed-data} block contained in each signer block. + * However, this does not verify the integrity of the rest of the APK but rather simply reports + * the expected digests of the rest of the APK (see {@link Builder#setContentDigestsToVerify}). + * + * <p>This method adds one or more errors to the returned {@code Result} if a verification error + * is encountered when parsing the signers. + */ + public ApkSigningBlockUtils.Result parseSigners() + throws IOException, NoSuchAlgorithmException, SignatureNotFoundException { ByteBuffer signers; try { - signers = getLengthPrefixedSlice(apkSignatureSchemeV3Block); + if (mApkSignatureSchemeV3Block == null) { + SignatureInfo signatureInfo = + ApkSigningBlockUtils.findSignature(mApk, mZipSections, mBlockId, mResult); + mApkSignatureSchemeV3Block = signatureInfo.signatureBlock; + } + signers = getLengthPrefixedSlice(mApkSignatureSchemeV3Block); } catch (ApkFormatException e) { - result.addError(Issue.V3_SIG_MALFORMED_SIGNERS); - return; + mResult.addError(Issue.V3_SIG_MALFORMED_SIGNERS); + return mResult; } if (!signers.hasRemaining()) { - result.addError(Issue.V3_SIG_NO_SIGNERS); - return; + mResult.addError(Issue.V3_SIG_NO_SIGNERS); + return mResult; } CertificateFactory certFactory; @@ -258,15 +313,16 @@ public abstract class V3SchemeVerifier { ApkSigningBlockUtils.Result.SignerInfo signerInfo = new ApkSigningBlockUtils.Result.SignerInfo(); signerInfo.index = signerIndex; - result.signers.add(signerInfo); + mResult.signers.add(signerInfo); try { ByteBuffer signer = getLengthPrefixedSlice(signers); - parseSigner(signer, certFactory, signerInfo, contentDigestsToVerify); + parseSigner(signer, certFactory, signerInfo); } catch (ApkFormatException | BufferUnderflowException e) { signerInfo.addError(Issue.V3_SIG_MALFORMED_SIGNER); - return; + return mResult; } } + return mResult; } /** @@ -281,12 +337,9 @@ public abstract class V3SchemeVerifier { * expected to be encountered on an Android platform version in the * {@code [minSdkVersion, maxSdkVersion]} range. */ - private static void parseSigner( - ByteBuffer signerBlock, - CertificateFactory certFactory, - ApkSigningBlockUtils.Result.SignerInfo result, - Set<ContentDigestAlgorithm> contentDigestsToVerify) - throws ApkFormatException, NoSuchAlgorithmException { + private void parseSigner(ByteBuffer signerBlock, CertificateFactory certFactory, + ApkSigningBlockUtils.Result.SignerInfo result) + throws ApkFormatException, NoSuchAlgorithmException { ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); byte[] signedDataBytes = new byte[signedData.remaining()]; signedData.get(signedDataBytes); @@ -375,7 +428,7 @@ public abstract class V3SchemeVerifier { return; } result.verifiedSignatures.put(signatureAlgorithm, sigBytes); - contentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); + mContentDigestsToVerify.add(signatureAlgorithm.getContentDigestAlgorithm()); } catch (InvalidKeyException | InvalidAlgorithmParameterException | SignatureException e) { result.addError(Issue.V3_SIG_VERIFY_EXCEPTION, signatureAlgorithm, e); @@ -485,6 +538,7 @@ public abstract class V3SchemeVerifier { // Parse the additional attributes block. int additionalAttributeCount = 0; + boolean rotationAttrFound = false; while (additionalAttributes.hasRemaining()) { additionalAttributeCount++; try { @@ -494,7 +548,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 = @@ -512,6 +566,31 @@ public abstract class V3SchemeVerifier { } catch (Exception e) { result.addError(Issue.V3_SIG_MALFORMED_LINEAGE); } + } else if (id == V3SchemeConstants.ROTATION_MIN_SDK_VERSION_ATTR_ID) { + rotationAttrFound = true; + // API targeting for rotation was added with V3.1; if the maxSdkVersion + // does not support v3.1 then ignore this attribute. + if (mMaxSdkVersion >= V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + && mFullVerification) { + int attrRotationMinSdkVersion = ByteBuffer.wrap(value) + .order(ByteOrder.LITTLE_ENDIAN).getInt(); + if (mOptionalRotationMinSdkVersion.isPresent()) { + int rotationMinSdkVersion = mOptionalRotationMinSdkVersion.getAsInt(); + if (attrRotationMinSdkVersion != rotationMinSdkVersion) { + result.addError(Issue.V31_ROTATION_MIN_SDK_MISMATCH, + attrRotationMinSdkVersion, rotationMinSdkVersion); + } + } else { + result.addError(Issue.V31_BLOCK_MISSING, attrRotationMinSdkVersion); + } + } + } else if (id == V3SchemeConstants.ROTATION_ON_DEV_RELEASE_ATTR_ID) { + // This attribute should only be used by a v3.1 signer to indicate rotation + // is targeting the development release that is using the SDK version of the + // previously released platform version. + if (mBlockId != V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID) { + result.addWarning(Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER); + } } else { result.addWarning(Issue.V3_SIG_UNKNOWN_ADDITIONAL_ATTRIBUTE, id); } @@ -521,5 +600,168 @@ public abstract class V3SchemeVerifier { return; } } + if (mFullVerification && mOptionalRotationMinSdkVersion.isPresent() && !rotationAttrFound) { + result.addWarning(Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING, + mOptionalRotationMinSdkVersion.getAsInt()); + } + } + + /** Builder of {@link V3SchemeVerifier} instances. */ + public static class Builder { + private RunnablesExecutor mExecutor = RunnablesExecutor.SINGLE_THREADED; + private DataSource mApk; + private ApkUtils.ZipSections mZipSections; + private ByteBuffer mApkSignatureSchemeV3Block; + private Set<ContentDigestAlgorithm> mContentDigestsToVerify; + private ApkSigningBlockUtils.Result mResult; + private int mMinSdkVersion; + private int mMaxSdkVersion; + private int mBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; + private boolean mFullVerification = true; + private OptionalInt mOptionalRotationMinSdkVersion = OptionalInt.empty(); + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * verify the V3 signing block of the provided {@code apk} with the specified {@code + * zipSections} over the range from {@code minSdkVersion} to {@code maxSdkVersion}. + */ + public Builder(DataSource apk, ApkUtils.ZipSections zipSections, int minSdkVersion, + int maxSdkVersion) { + mApk = apk; + mZipSections = zipSections; + mMinSdkVersion = minSdkVersion; + mMaxSdkVersion = maxSdkVersion; + } + + /** + * Instantiates a new {@code Builder} for a {@code V3SchemeVerifier} that can be used to + * parse the {@link ApkSigningBlockUtils.Result.SignerInfo} instances from the {@code + * apkSignatureSchemeV3Block}. + * + * <note>Full verification of the v3 signature is not possible when instantiating a new + * {@code V3SchemeVerifier} with this method.</note> + */ + public Builder(ByteBuffer apkSignatureSchemeV3Block) { + mApkSignatureSchemeV3Block = apkSignatureSchemeV3Block; + } + + /** + * Sets the {@link RunnablesExecutor} to be used when verifying the APK's content digests. + */ + public Builder setRunnablesExecutor(RunnablesExecutor executor) { + mExecutor = executor; + return this; + } + + /** + * Sets the V3 {code blockId} to be verified in the provided APK. + * + * <p>This {@code V3SchemeVerifier} currently supports the block IDs for the {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V3_BLOCK_ID v3.0} and {@link + * V3SchemeConstants#APK_SIGNATURE_SCHEME_V31_BLOCK_ID v3.1} signature schemes. + */ + public Builder setBlockId(int blockId) { + mBlockId = blockId; + return this; + } + + /** + * Sets the {@code rotationMinSdkVersion} to be verified in the v3.0 signer's additional + * attribute. + * + * <p>This value can be obtained from the signers returned when verifying the v3.1 signing + * block of an APK; in the case of multiple signers targeting different SDK versions in the + * v3.1 signing block, the minimum SDK version from all the signers should be used. + */ + public Builder setRotationMinSdkVersion(int rotationMinSdkVersion) { + mOptionalRotationMinSdkVersion = OptionalInt.of(rotationMinSdkVersion); + return this; + } + + /** + * Sets the {@code result} instance to be used when returning verification results. + * + * <p>This method can be used when the caller already has a {@link + * ApkSigningBlockUtils.Result} and wants to store the verification results in this + * instance. + */ + public Builder setResult(ApkSigningBlockUtils.Result result) { + mResult = result; + return this; + } + + /** + * Sets the instance to be used to store the {@code contentDigestsToVerify}. + * + * <p>This method can be used when the caller needs access to the {@code + * contentDigestsToVerify} computed by this {@code V3SchemeVerifier}. + */ + public Builder setContentDigestsToVerify( + Set<ContentDigestAlgorithm> contentDigestsToVerify) { + mContentDigestsToVerify = contentDigestsToVerify; + return this; + } + + /** + * Sets whether full verification should be performed by the {@code V3SchemeVerifier} built + * from this instance. + * + * <note>{@link #verify()} will always verify the content digests for the APK, but this + * allows verification of the rotation minimum SDK version stripping attribute to be skipped + * for scenarios where this value may not have been parsed from a V3.1 signing block (such + * as when only {@link #parseSigners()} will be invoked.</note> + */ + public Builder setFullVerification(boolean fullVerification) { + mFullVerification = fullVerification; + return this; + } + + /** + * Returns a new {@link V3SchemeVerifier} built with the configuration provided to this + * {@code Builder}. + */ + public V3SchemeVerifier build() { + int sigSchemeVersion; + switch (mBlockId) { + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3; + mMinSdkVersion = Math.max(mMinSdkVersion, + V3SchemeConstants.MIN_SDK_WITH_V3_SUPPORT); + break; + case V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID: + sigSchemeVersion = ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V31; + // V3.1 supports targeting an SDK version later than that of the initial release + // in which it is supported; allow any range for V3.1 as long as V3.0 covers the + // rest of the range. + mMinSdkVersion = mMaxSdkVersion; + break; + default: + throw new IllegalArgumentException( + String.format("Unsupported APK Signature Scheme V3 block ID: 0x%08x", + mBlockId)); + } + if (mResult == null) { + mResult = new ApkSigningBlockUtils.Result(sigSchemeVersion); + } + if (mContentDigestsToVerify == null) { + mContentDigestsToVerify = new HashSet<>(1); + } + + V3SchemeVerifier verifier = new V3SchemeVerifier( + mExecutor, + mApk, + mZipSections, + mContentDigestsToVerify, + mResult, + mMinSdkVersion, + mMaxSdkVersion, + mBlockId, + mOptionalRotationMinSdkVersion, + mFullVerification); + if (mApkSignatureSchemeV3Block != null) { + verifier.mApkSignatureSchemeV3Block = mApkSignatureSchemeV3Block; + } + return verifier; + } } } 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..2f9ecb3 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,17 +17,16 @@ 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; 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.v2.V2SchemeVerifier; +import com.android.apksig.internal.apk.v3.V3SchemeConstants; import com.android.apksig.internal.apk.v3.V3SchemeSigner; import com.android.apksig.internal.apk.v3.V3SchemeVerifier; import com.android.apksig.internal.util.Pair; @@ -44,6 +43,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.SignatureException; import java.security.cert.CertificateEncodingException; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -62,7 +62,6 @@ import java.util.Set; * </p> * (optional) verityTree: integer size prepended bytes of the verity hash tree. * <p> - * TODO(schfan): Add v4 unit tests */ public abstract class V4SchemeSigner { /** @@ -71,15 +70,33 @@ public abstract class V4SchemeSigner { private V4SchemeSigner() { } + public static class SignerConfig { + final public ApkSigningBlockUtils.SignerConfig v4Config; + final public ApkSigningBlockUtils.SignerConfig v41Config; + + public SignerConfig(List<ApkSigningBlockUtils.SignerConfig> v4Configs, + List<ApkSigningBlockUtils.SignerConfig> v41Configs) throws InvalidKeyException { + if (v4Configs == null || v4Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4 Signature."); + } + if (v41Configs != null && v41Configs.size() != 1) { + throw new InvalidKeyException("Only accepting one signer config for V4.1 Signature."); + } + this.v4Config = v4Configs.get(0); + this.v41Config = v41Configs != null ? v41Configs.get(0) : null; + } + } + /** * Based on a public key, return a signing algorithm that supports verity. */ public static List<SignatureAlgorithm> getSuggestedSignatureAlgorithms(PublicKey signingKey, - int minSdkVersion, boolean apkSigningBlockPaddingSupported) + int minSdkVersion, boolean apkSigningBlockPaddingSupported, + boolean deterministicDsaSigning) throws InvalidKeyException { List<SignatureAlgorithm> algorithms = V3SchemeSigner.getSuggestedSignatureAlgorithms( signingKey, minSdkVersion, - apkSigningBlockPaddingSupported); + apkSigningBlockPaddingSupported, deterministicDsaSigning); // Keeping only supported algorithms. for (Iterator<SignatureAlgorithm> iter = algorithms.listIterator(); iter.hasNext(); ) { final SignatureAlgorithm algorithm = iter.next(); @@ -149,10 +166,10 @@ public abstract class V4SchemeSigner { return Pair.of(signature, tree); } - private static V4Signature generateSignature( - SignerConfig signerConfig, + private static V4Signature.SigningInfo generateSigningInfo( + ApkSigningBlockUtils.SignerConfig signerConfig, V4Signature.HashingInfo hashingInfo, - byte[] apkDigest, byte[] additionaData, long fileSize) + byte[] apkDigest, byte[] additionalData, long fileSize) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, CertificateEncodingException { if (signerConfig.certificates.isEmpty()) { @@ -169,9 +186,9 @@ public abstract class V4SchemeSigner { final byte[] encodedCertificate = encodedCertificates.get(0); final V4Signature.SigningInfo signingInfoNoSignature = new V4Signature.SigningInfo(apkDigest, - encodedCertificate, additionaData, publicKey.getEncoded(), -1, null); + encodedCertificate, additionalData, publicKey.getEncoded(), -1, null); - final byte[] data = V4Signature.getSigningData(fileSize, hashingInfo, + final byte[] data = V4Signature.getSignedData(fileSize, hashingInfo, signingInfoNoSignature); // Signing. @@ -184,12 +201,33 @@ public abstract class V4SchemeSigner { final int signatureAlgorithmId = signatures.get(0).getFirst(); final byte[] signature = signatures.get(0).getSecond(); - final V4Signature.SigningInfo signingInfo = new V4Signature.SigningInfo(apkDigest, - encodedCertificate, additionaData, publicKey.getEncoded(), signatureAlgorithmId, + return new V4Signature.SigningInfo(apkDigest, + encodedCertificate, additionalData, publicKey.getEncoded(), signatureAlgorithmId, signature); + } + + private static V4Signature generateSignature( + SignerConfig signerConfig, + V4Signature.HashingInfo hashingInfo, + byte[] apkDigest, byte[] additionalData, long fileSize) + throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, + CertificateEncodingException { + final V4Signature.SigningInfo signingInfo = generateSigningInfo(signerConfig.v4Config, + hashingInfo, apkDigest, additionalData, fileSize); + + final V4Signature.SigningInfos signingInfos; + if (signerConfig.v41Config != null) { + final V4Signature.SigningInfoBlock extSigningBlock = new V4Signature.SigningInfoBlock( + V3SchemeConstants.APK_SIGNATURE_SCHEME_V31_BLOCK_ID, + generateSigningInfo(signerConfig.v41Config, hashingInfo, apkDigest, + additionalData, fileSize).toByteArray()); + signingInfos = new V4Signature.SigningInfos(signingInfo, extSigningBlock); + } else { + signingInfos = new V4Signature.SigningInfos(signingInfo); + } return new V4Signature(V4Signature.CURRENT_VERSION, hashingInfo.toByteArray(), - signingInfo.toByteArray()); + signingInfos.toByteArray()); } // Get digest by parsing the V2/V3-signed apk and choosing the first digest of supported type. @@ -314,8 +352,6 @@ public abstract class V4SchemeSigner { return bestDigest; } - // Use the same order as in the ApkSignatureSchemeV3Verifier to make sure the digest - // verification in framework works. public static int digestAlgorithmSortingOrder(ContentDigestAlgorithm contentDigestAlgorithm) { switch (contentDigestAlgorithm) { case CHUNKED_SHA256: @@ -324,8 +360,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/apk/v4/V4SchemeVerifier.java b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java index 0a8484b..c0a9013 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java +++ b/src/main/java/com/android/apksig/internal/apk/v4/V4SchemeVerifier.java @@ -90,20 +90,37 @@ public abstract class V4SchemeVerifier { V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray( signature.hashingInfo); - V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( - signature.signingInfo); - final byte[] signedData = V4Signature.getSigningData(apk.size(), hashingInfo, signingInfo); + V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray( + signature.signingInfos); - // First, verify the signature over signedData. - ApkSigningBlockUtils.Result.SignerInfo signerInfo = parseAndVerifySignatureBlock( - signingInfo, signedData); - result.signers.add(signerInfo); - if (result.containsErrors()) { - return result; + final ApkSigningBlockUtils.Result.SignerInfo signerInfo; + + // Verify the primary signature over signedData. + { + V4Signature.SigningInfo signingInfo = signingInfos.signingInfo; + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + signerInfo = parseAndVerifySignatureBlock(signingInfo, signedData); + result.signers.add(signerInfo); + if (result.containsErrors()) { + return result; + } + } + + // Verify all subsequent signatures. + for (V4Signature.SigningInfoBlock signingInfoBlock : signingInfos.signingInfoBlocks) { + V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray( + signingInfoBlock.signingInfo); + final byte[] signedData = V4Signature.getSignedData(apk.size(), hashingInfo, + signingInfo); + result.signers.add(parseAndVerifySignatureBlock(signingInfo, signedData)); + if (result.containsErrors()) { + return result; + } } - // Second, check if the root hash and the tree are correct. + // Check if the root hash and the tree are correct. verifyRootHashAndTree(apk, signerInfo, hashingInfo.rawRootHash, tree); if (!result.containsErrors()) { result.verified = true; diff --git a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java index e36ed60..1eac5a2 100644 --- a/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java +++ b/src/main/java/com/android/apksig/internal/apk/v4/V4Signature.java @@ -22,6 +22,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; public class V4Signature { public static final int CURRENT_VERSION = 2; @@ -29,6 +31,8 @@ public class V4Signature { public static final int HASHING_ALGORITHM_SHA256 = 1; public static final byte LOG2_BLOCK_SIZE_4096_BYTES = 12; + public static final int MAX_SIGNING_INFOS_SIZE = 7168; + public static class HashingInfo { public final int hashAlgorithm; // only 1 == SHA256 supported public final byte log2BlockSize; // only 12 (block size 4096) supported now @@ -82,7 +86,10 @@ public class V4Signature { } static SigningInfo fromByteArray(byte[] bytes) throws IOException { - ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + return fromByteBuffer(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)); + } + + static SigningInfo fromByteBuffer(ByteBuffer buffer) throws IOException { byte[] apkDigest = readBytes(buffer); byte[] certificate = readBytes(buffer); byte[] additionalData = readBytes(buffer); @@ -108,14 +115,93 @@ public class V4Signature { } } - public final int version; // Always 2 for now. + public static class SigningInfoBlock { + public final int blockId; + public final byte[] signingInfo; + + public SigningInfoBlock(int blockId, byte[] signingInfo) { + this.blockId = blockId; + this.signingInfo = signingInfo; + } + + static SigningInfoBlock fromByteBuffer(ByteBuffer buffer) throws IOException { + int blockId = buffer.getInt(); + byte[] signingInfo = readBytes(buffer); + return new SigningInfoBlock(blockId, signingInfo); + } + + byte[] toByteArray() { + final int size = 4/*blockId*/ + bytesSize(this.signingInfo); + ByteBuffer buffer = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN); + buffer.putInt(this.blockId); + writeBytes(buffer, this.signingInfo); + return buffer.array(); + } + } + + public static class SigningInfos { + public final SigningInfo signingInfo; + public final SigningInfoBlock[] signingInfoBlocks; + + public SigningInfos(SigningInfo signingInfo) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = new SigningInfoBlock[0]; + } + + public SigningInfos(SigningInfo signingInfo, SigningInfoBlock... signingInfoBlocks) { + this.signingInfo = signingInfo; + this.signingInfoBlocks = signingInfoBlocks; + } + + public static SigningInfos fromByteArray(byte[] bytes) throws IOException { + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + SigningInfo signingInfo = SigningInfo.fromByteBuffer(buffer); + if (!buffer.hasRemaining()) { + return new SigningInfos(signingInfo); + } + ArrayList<SigningInfoBlock> signingInfoBlocks = new ArrayList<>(1); + while (buffer.hasRemaining()) { + signingInfoBlocks.add(SigningInfoBlock.fromByteBuffer(buffer)); + } + return new SigningInfos(signingInfo, + signingInfoBlocks.toArray(new SigningInfoBlock[signingInfoBlocks.size()])); + } + + byte[] toByteArray() { + byte[][] arrays = new byte[1 + this.signingInfoBlocks.length][]; + arrays[0] = this.signingInfo.toByteArray(); + int size = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + arrays[i + 1] = this.signingInfoBlocks[i].toByteArray(); + size += arrays[i + 1].length; + } + if (size > MAX_SIGNING_INFOS_SIZE) { + throw new IllegalArgumentException( + "Combined SigningInfos length exceeded limit of 7K: " + size); + } + + // Combine all arrays into one. + byte[] result = Arrays.copyOf(arrays[0], size); + int offset = arrays[0].length; + for (int i = 0, isize = this.signingInfoBlocks.length; i < isize; ++i) { + System.arraycopy(arrays[i + 1], 0, result, offset, arrays[i + 1].length); + offset += arrays[i + 1].length; + } + return result; + } + } + + // Always 2 for now. + public final int version; public final byte[] hashingInfo; - public final byte[] signingInfo; // Passed as-is to the kernel. Can be retrieved later. + // Can contain either SigningInfo or SigningInfo + one or multiple SigningInfoBlock. + // Passed as-is to the kernel. Can be retrieved later. + public final byte[] signingInfos; - V4Signature(int version, byte[] hashingInfo, byte[] signingInfo) { + V4Signature(int version, byte[] hashingInfo, byte[] signingInfos) { this.version = version; this.hashingInfo = hashingInfo; - this.signingInfo = signingInfo; + this.signingInfos = signingInfos; } static V4Signature readFrom(InputStream stream) throws IOException { @@ -131,10 +217,10 @@ public class V4Signature { public void writeTo(OutputStream stream) throws IOException { writeIntLE(stream, this.version); writeBytes(stream, this.hashingInfo); - writeBytes(stream, this.signingInfo); + writeBytes(stream, this.signingInfos); } - static byte[] getSigningData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { + static byte[] getSignedData(long fileSize, HashingInfo hashingInfo, SigningInfo signingInfo) { final int size = 4/*size*/ + 8/*fileSize*/ + 4/*hash_algorithm*/ + 1/*log2_blocksize*/ + bytesSize( hashingInfo.salt) + bytesSize(hashingInfo.rawRootHash) + bytesSize( diff --git a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java index d4a6fb6..160dc4e 100644 --- a/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java +++ b/src/main/java/com/android/apksig/internal/asn1/Asn1BerParser.java @@ -507,23 +507,22 @@ public final class Asn1BerParser { private static int integerToInt(ByteBuffer encoded) throws Asn1DecodingException { BigInteger value = integerToBigInteger(encoded); - try { - return value.intValueExact(); - } catch (ArithmeticException e) { + if (value.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) { throw new Asn1DecodingException( - String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value), e); + String.format("INTEGER cannot be represented as int: %1$d (0x%1$x)", value)); } + return value.intValue(); } private static long integerToLong(ByteBuffer encoded) throws Asn1DecodingException { BigInteger value = integerToBigInteger(encoded); - try { - return value.longValueExact(); - } catch (ArithmeticException e) { + if (value.compareTo(BigInteger.valueOf(Long.MIN_VALUE)) < 0 + || value.compareTo(BigInteger.valueOf(Long.MAX_VALUE)) > 0) { throw new Asn1DecodingException( - String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value), - e); + String.format("INTEGER cannot be represented as long: %1$d (0x%1$x)", value)); } + return value.longValue(); } private static List<AnnotatedField> getAnnotatedFields(Class<?> containerClass) diff --git a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java index c27c487..9712767 100644 --- a/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java +++ b/src/main/java/com/android/apksig/internal/pkcs7/AlgorithmIdentifier.java @@ -16,6 +16,7 @@ package com.android.apksig.internal.pkcs7; +import static com.android.apksig.Constants.OID_RSA_ENCRYPTION; import static com.android.apksig.internal.asn1.Asn1DerEncoder.ASN1_DER_NULL; import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA1; import static com.android.apksig.internal.oid.OidConstants.OID_DIGEST_SHA256; @@ -77,7 +78,8 @@ public class AlgorithmIdentifier { * when signing with the specified key and digest algorithm. */ public static Pair<String, AlgorithmIdentifier> getSignerInfoSignatureAlgorithm( - PublicKey publicKey, DigestAlgorithm digestAlgorithm) throws InvalidKeyException { + PublicKey publicKey, DigestAlgorithm digestAlgorithm, boolean deterministicDsaSigning) + throws InvalidKeyException { String keyAlgorithm = publicKey.getAlgorithm(); String jcaDigestPrefixForSigAlg; switch (digestAlgorithm) { @@ -91,7 +93,7 @@ public class AlgorithmIdentifier { throw new IllegalArgumentException( "Unexpected digest algorithm: " + digestAlgorithm); } - if ("RSA".equalsIgnoreCase(keyAlgorithm)) { + if ("RSA".equalsIgnoreCase(keyAlgorithm) || OID_RSA_ENCRYPTION.equals(keyAlgorithm)) { return Pair.of( jcaDigestPrefixForSigAlg + "withRSA", new AlgorithmIdentifier(OID_SIG_RSA, ASN1_DER_NULL)); @@ -115,7 +117,9 @@ public class AlgorithmIdentifier { throw new IllegalArgumentException( "Unexpected digest algorithm: " + digestAlgorithm); } - return Pair.of(jcaDigestPrefixForSigAlg + "withDSA", sigAlgId); + String signingAlgorithmName = + jcaDigestPrefixForSigAlg + (deterministicDsaSigning ? "withDetDSA" : "withDSA"); + return Pair.of(signingAlgorithmName, sigAlgId); } else if ("EC".equalsIgnoreCase(keyAlgorithm)) { return Pair.of( jcaDigestPrefixForSigAlg + "withECDSA", 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..bbead72 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; @@ -48,6 +54,18 @@ public abstract class AndroidSdkVersion { /** Android P. */ public static final int P = 28; + /** Android Q. */ + public static final int Q = 29; + /** Android R. */ public static final int R = 30; + + /** Android S. */ + public static final int S = 31; + + /** Android Sv2. */ + public static final int Sv2 = 32; + + /** Android T. */ + public static final int T = 33; } diff --git a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java index 8f9e1fd..2a890f6 100644 --- a/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java +++ b/src/main/java/com/android/apksig/internal/util/DelegatingX509Certificate.java @@ -34,6 +34,7 @@ import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Set; + import javax.security.auth.x500.X500Principal; /** @@ -210,6 +211,7 @@ public class DelegatingX509Certificate extends X509Certificate { } @Override + @SuppressWarnings("AndroidJdkLibsChecker") public void verify(PublicKey key, Provider sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, SignatureException { mDelegate.verify(key, sigProvider); diff --git a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java index 488df89..81026ba 100644 --- a/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java +++ b/src/main/java/com/android/apksig/internal/util/VerityTreeBuilder.java @@ -16,6 +16,8 @@ package com.android.apksig.internal.util; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + import com.android.apksig.internal.zip.ZipUtils; import com.android.apksig.util.DataSink; import com.android.apksig.util.DataSource; @@ -24,9 +26,10 @@ import com.android.apksig.util.DataSources; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; + +import java.util.ArrayList; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Phaser; @@ -76,7 +79,7 @@ public class VerityTreeBuilder implements AutoCloseable { private final ExecutorService mExecutor = new ThreadPoolExecutor(DIGEST_PARALLELISM, DIGEST_PARALLELISM, - 0L, TimeUnit.MILLISECONDS, + 0L, MILLISECONDS, new ArrayBlockingQueue<>(MAX_OUTSTANDING_CHUNKS), new ThreadPoolExecutor.CallerRunsPolicy()); @@ -220,9 +223,6 @@ public class VerityTreeBuilder implements AutoCloseable { final long size = dataSource.size(); final int chunks = (int) divideRoundup(size, CHUNK_SIZE); - /** Actual number of workers. */ - final int parallelism = Math.max( - Math.min(chunks / MIN_CHUNKS_PER_WORKER, DIGEST_PARALLELISM), 1); /** Single IO operation size, in chunks. */ final int ioSizeChunks = MAX_PREFETCH_CHUNKS; diff --git a/src/main/java/com/android/apksig/internal/zip/EocdRecord.java b/src/main/java/com/android/apksig/internal/zip/EocdRecord.java index 9c531f4..d2000b4 100644 --- a/src/main/java/com/android/apksig/internal/zip/EocdRecord.java +++ b/src/main/java/com/android/apksig/internal/zip/EocdRecord.java @@ -45,4 +45,13 @@ public class EocdRecord { ZipUtils.setUnsignedInt32(result, CD_OFFSET_OFFSET, centralDirectoryOffset); return result; } + + public static ByteBuffer createWithPaddedComment(ByteBuffer original, int padding) { + ByteBuffer result = ByteBuffer.allocate((int) original.remaining() + padding); + result.order(ByteOrder.LITTLE_ENDIAN); + result.put(original.slice()); + result.rewind(); + ZipUtils.updateZipEocdCommentLen(result); + return result; + } } 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..1c2e82c 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; @@ -64,6 +70,20 @@ public abstract class ZipUtils { } /** + * Sets the length of EOCD comment. + * + * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. + */ + public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) { + assertByteOrderLittleEndian(zipEndOfCentralDirectory); + int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE; + setUnsignedInt16( + zipEndOfCentralDirectory, + zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET, + commentLen); + } + + /** * Returns the offset of the start of the ZIP Central Directory in the archive. * * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian. @@ -247,6 +267,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/util/RunnablesExecutor.java b/src/main/java/com/android/apksig/util/RunnablesExecutor.java index b0e69e4..74017f8 100644 --- a/src/main/java/com/android/apksig/util/RunnablesExecutor.java +++ b/src/main/java/com/android/apksig/util/RunnablesExecutor.java @@ -16,16 +16,17 @@ package com.android.apksig.util; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Phaser; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; public interface RunnablesExecutor { - RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run(); + static final RunnablesExecutor SINGLE_THREADED = p -> p.createRunnable().run(); - RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() { + static final RunnablesExecutor MULTI_THREADED = new RunnablesExecutor() { private final int PARALLELISM = Math.min(32, Runtime.getRuntime().availableProcessors()); private final int QUEUE_SIZE = 4; @@ -33,7 +34,7 @@ public interface RunnablesExecutor { public void execute(RunnablesProvider provider) { final ExecutorService mExecutor = new ThreadPoolExecutor(PARALLELISM, PARALLELISM, - 0L, TimeUnit.MILLISECONDS, + 0L, MILLISECONDS, new ArrayBlockingQueue<>(QUEUE_SIZE), new ThreadPoolExecutor.CallerRunsPolicy()); 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 4d45892..6473440 100644 --- a/src/test/java/com/android/apksig/ApkSignerTest.java +++ b/src/test/java/com/android/apksig/ApkSignerTest.java @@ -21,6 +21,7 @@ import static com.android.apksig.apk.ApkUtils.findZipSections; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -31,41 +32,52 @@ 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.SourceStampSigner; +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.Pair; import com.android.apksig.internal.util.Resources; 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 java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +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.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Security; import java.security.SignatureException; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.HashSet; +import java.util.Set; @RunWith(JUnit4.class) public class ApkSignerTest { @@ -78,9 +90,11 @@ public class ApkSignerTest { // All signers with the same prefix and an _X suffix were signed with the private key of the // (X-1) signer. - private static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048"; - private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2"; - private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3"; + static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048"; + static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2"; + static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3"; + + private static final String EC_P256_SIGNER_RESOURCE_NAME = "ec-p256"; // This is the same cert as above with the modulus reencoded to remove the leading 0 sign bit. private static final String FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS = @@ -89,6 +103,14 @@ public class ApkSignerTest { private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME = "rsa-2048-lineage-2-signers"; + // These are the ID and value of an extra signature block within the APK signing block that + // can be preserved through the setOtherSignersSignaturesPreserved API. + private final int EXTRA_BLOCK_ID = 0x7e57c0de; + private final byte[] EXTRA_BLOCK_VALUE = {0, 1, 2, 3, 4, 5, 6, 7}; + + @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); @@ -100,8 +122,10 @@ public class ApkSignerTest { + ApkSignerTest.class.getSimpleName() + " into " + outDir); - if (!outDir.mkdirs()) { - throw new IOException("Failed to create directory: " + outDir); + if (!outDir.exists()) { + if (!outDir.mkdirs()) { + throw new IOException("Failed to create directory: " + outDir); + } } List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList( @@ -133,21 +157,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", @@ -200,7 +227,9 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); + signGolden( "golden-legacy-aligned-in.apk", new File(outDir, "golden-legacy-aligned-v3-lineage-out.apk"), @@ -208,6 +237,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( "golden-aligned-in.apk", @@ -216,6 +246,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( @@ -268,6 +299,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( "golden-legacy-aligned-in.apk", @@ -276,6 +308,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( "golden-aligned-in.apk", @@ -284,6 +317,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( @@ -314,6 +348,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( "golden-legacy-aligned-in.apk", @@ -322,6 +357,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( "golden-aligned-in.apk", @@ -330,6 +366,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); signGolden( @@ -348,6 +385,27 @@ public class ApkSignerTest { "original.apk", new File(outDir, "golden-rsa-minSdkVersion-24-out.apk"), new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(24)); + signGolden( + "original.apk", + new File(outDir, "golden-rsa-verity-out.apk"), + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setVerityEnabled(true)); + signGolden( + "original.apk", + new File(outDir, "golden-file-size-aligned.apk"), + new ApkSigner.Builder(rsa2048SignerConfig) + .setAlignFileSize(true)); + signGolden( + "pinsapp-unsigned.apk", + new File(outDir, "golden-pinsapp-signed.apk"), + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setVerityEnabled(true)); } private static void signGolden( @@ -356,7 +414,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 @@ -388,7 +452,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", @@ -410,6 +475,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-unaligned-in.apk", @@ -432,6 +498,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-unaligned-in.apk", @@ -447,6 +514,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); // Uncompressed entries in this input file are aligned by zero-padding the "extra" field, as @@ -463,7 +531,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", @@ -485,6 +554,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-legacy-aligned-in.apk", @@ -507,6 +577,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-legacy-aligned-in.apk", @@ -522,6 +593,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); // Uncompressed entries in this input file are aligned by padding the "extra" field, as @@ -537,7 +609,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", @@ -559,6 +632,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-aligned-in.apk", @@ -581,6 +655,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); assertGolden( "golden-aligned-in.apk", @@ -596,6 +671,7 @@ public class ApkSignerTest { .setV1SigningEnabled(true) .setV2SigningEnabled(true) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); } @@ -627,6 +703,35 @@ public class ApkSignerTest { } @Test + public void testVerityEnabled_Golden() throws Exception { + List<ApkSigner.SignerConfig> rsaSignerConfig = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + + assertGolden( + "original.apk", + "golden-rsa-verity-out.apk", + new ApkSigner.Builder(rsaSignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setVerityEnabled(true)); + } + + @Test + public void testAlignFileSize_Golden() throws Exception { + List<ApkSigner.SignerConfig> rsaSignerConfig = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + String goldenOutput = "golden-file-size-aligned.apk"; + assertGolden( + "original.apk", + goldenOutput, + new ApkSigner.Builder(rsaSignerConfig).setAlignFileSize(true)); + assertTrue(Resources.toByteArray(getClass(), goldenOutput).length % 4096 == 0); + } + + @Test public void testRsaSignedVerifies() throws Exception { List<ApkSigner.SignerConfig> signers = Collections.singletonList( @@ -634,7 +739,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+ @@ -652,7 +757,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+ @@ -663,15 +768,58 @@ public class ApkSignerTest { verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG); } + + @Test + public void testDeterministicDsaSignedVerifies() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + try { + List<ApkSigner.SignerConfig> signers = + Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048")); + String in = "original.apk"; + + // Sign so that the APK is guaranteed to verify on API Level 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+ + out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(21)); + assertVerified(verifyForMinSdkVersion(out, 21)); + // Does not verify on API Level 20 because DSA with SHA-256 not supported + assertVerificationFailure( + verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG); + } finally { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + } + + @Test + public void testDeterministicDsaSigningIsDeterministic() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + try { + List<ApkSigner.SignerConfig> signers = + Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048")); + String in = "original.apk"; + + ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signers).setMinSdkVersion(1); + File first = sign(in, apkSignerBuilder); + File second = sign(in, apkSignerBuilder); + + assertFileContentsEqual(first, second); + } finally { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + } + @Test public void testEcSignedVerifies() throws Exception { List<ApkSigner.SignerConfig> signers = - Collections.singletonList(getDefaultSignerConfigFromResources("ec-p256")); + Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); String in = "original.apk"; // 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( @@ -839,6 +987,7 @@ public class ApkSignerTest { .setV1SigningEnabled(false) .setV2SigningEnabled(false) .setV3SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) .setSigningCertificateLineage(lineage)); // Verifies that an intermediate signer in the lineage is not sufficient to satisfy the @@ -903,14 +1052,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))); @@ -933,7 +1081,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) @@ -960,13 +1108,15 @@ public class ApkSignerTest { getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME); assertThrows( - IllegalStateException.class, - () -> sign("original.apk", - new ApkSigner.Builder(Collections.singletonList(signer)) - .setV1SigningEnabled(true) - .setV2SigningEnabled(false) - .setV3SigningEnabled(false) - .setV4SigningEnabled(true))); + IllegalStateException.class, + () -> + sign( + "original.apk", + new ApkSigner.Builder(Collections.singletonList(signer)) + .setV1SigningEnabled(true) + .setV2SigningEnabled(false) + .setV3SigningEnabled(false) + .setV4SigningEnabled(true))); } @Test @@ -980,28 +1130,106 @@ 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 + public void testSignApk_existingStampFile_sameSourceStamp() throws Exception { + List<ApkSigner.SignerConfig> signers = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + ApkSigner.SignerConfig sourceStampSigner = + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + + File signedApk = + sign( + "original-with-stamp-file.apk", + new ApkSigner.Builder(signers) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setSourceStampSignerConfig(sourceStampSigner)); + + ApkVerifier.Result sourceStampVerificationResult = + verify(signedApk, /* minSdkVersionOverride= */ null); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); + } + + @Test + public void testSignApk_existingStampFile_differentSourceStamp() throws Exception { + List<ApkSigner.SignerConfig> signers = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + ApkSigner.SignerConfig sourceStampSigner = + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + + Exception exception = + assertThrows( + ApkFormatException.class, + () -> + sign( + "original-with-stamp-file.apk", + new ApkSigner.Builder(signers) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setSourceStampSignerConfig(sourceStampSigner))); + assertEquals( + String.format( + "Cannot generate SourceStamp. APK contains an existing entry with the" + + " name: %s, and it is different than the provided source stamp" + + " certificate", + SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME), + exception.getMessage()); + } + + @Test + public void testSignApk_existingStampFile_differentSourceStamp_forceOverwrite() + throws Exception { + List<ApkSigner.SignerConfig> signers = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + ApkSigner.SignerConfig sourceStampSigner = + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + + File signedApk = + sign( + "original-with-stamp-file.apk", + new ApkSigner.Builder(signers) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setForceSourceStampOverwrite(true) + .setSourceStampSignerConfig(sourceStampSigner)); + + ApkVerifier.Result sourceStampVerificationResult = + verify(signedApk, /* minSdkVersionOverride= */ null); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); } @Test @@ -1010,7 +1238,7 @@ public class ApkSignerTest { Collections.singletonList( getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); - DataSource signedApk = + File signedApkFile = sign( "original.apk", new ApkSigner.Builder(signersList) @@ -1018,45 +1246,44 @@ 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 - public void testSignApk_stampBlock_whenNoV2V3SignaturePresent() throws Exception { + public void testSignApk_stampBlock_whenV1SignaturePresent() 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); - DataSource signedApk = + File signedApk = sign( "original.apk", new ApkSigner.Builder(signersList) .setV1SigningEnabled(true) + .setV2SigningEnabled(false) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false) .setSourceStampSignerConfig(sourceStampSigner)); - 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)); + ApkVerifier.Result sourceStampVerificationResult = + verify(signedApk, /* minSdkVersionOverride= */ null); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); } @Test @@ -1067,20 +1294,18 @@ 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) + .setV1SigningEnabled(false) .setV2SigningEnabled(true) + .setV3SigningEnabled(false) .setSourceStampSignerConfig(sourceStampSigner)); - SignatureInfo signatureInfo = - getSignatureInfoFromApk( - signedApk, - ApkSigningBlockUtils.VERSION_SOURCE_STAMP, - SourceStampSigner.SOURCE_STAMP_BLOCK_ID); - assertNotNull(signatureInfo.signatureBlock); + ApkVerifier.Result sourceStampVerificationResult = + verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); } @Test @@ -1091,31 +1316,940 @@ 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) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) .setV3SigningEnabled(true) .setSourceStampSignerConfig(sourceStampSigner)); - SignatureInfo signatureInfo = - getSignatureInfoFromApk( - signedApk, - ApkSigningBlockUtils.VERSION_SOURCE_STAMP, - SourceStampSigner.SOURCE_STAMP_BLOCK_ID); - assertNotNull(signatureInfo.signatureBlock); + ApkVerifier.Result sourceStampVerificationResult = + verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N); + assertSourceStampVerified(signedApk, sourceStampVerificationResult); + } + + @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); + } + + @Test + public void testSignApk_Pinlist() throws Exception { + List<ApkSigner.SignerConfig> rsa2048SignerConfig = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + assertGolden( + "pinsapp-unsigned.apk", + "golden-pinsapp-signed.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setVerityEnabled(true)); + assertTrue("pinlist.meta file must be in the signed APK.", + resourceZipFileContains("golden-pinsapp-signed.apk", "pinlist.meta")); + } + + @Test + public void testOtherSignersSignaturesPreserved_extraSigBlock_signatureAppended() + throws Exception { + // The DefaultApkSignerEngine contains support to append a signature to an existing + // signing block; any existing signature blocks within the APK signing block should be + // left intact except for the original verity padding block (since this is regenerated) and + // the source stamp. This test verifies that an extra signature block is still in + // the APK signing block after appending a V2 signature. + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk", + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(false) + .setV2SigningEnabled(true) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)); + + ApkVerifier.Result result = verify(signedApk, null); + assertVerified(result); + assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + EC_P256_SIGNER_RESOURCE_NAME); + assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID)); + } + + @Test + public void testOtherSignersSignaturesPreserved_v1Only_signatureAppended() throws Exception { + // This test verifies appending an additional V1 signature to an existing V1 signer behaves + // similar to jarsigner where the APK is then verified as signed by both signers. + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("v1-only-with-rsa-2048.apk", + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(false) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)); + + ApkVerifier.Result result = verify(signedApk, null); + assertVerified(result); + assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + EC_P256_SIGNER_RESOURCE_NAME); + } + + @Test + public void testOtherSignersSignaturesPreserved_v3OnlyDifferentSigner_throwsException() + throws Exception { + // The V3 Signature Scheme only supports a single signer; if an attempt is made to append + // a different signer to a V3 signature then an exception should be thrown. + // The APK used for this test is signed with the ec-p256 signer so use the rsa-2048 to + // attempt to append a different signature. + List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + + assertThrows(IllegalStateException.class, () -> + sign("v3-only-with-stamp.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)) + ); + } + + @Test + public void testOtherSignersSignaturesPreserved_v2OnlyAppendV2V3SameSigner_signatureAppended() + throws Exception { + // A V2 and V3 signature can be appended to an existing V2 signature if the same signer is + // used to resign the APK; this could be used in a case where an APK was previously signed + // with just the V2 signature scheme along with additional non-APK signing scheme signature + // blocks and the signer wanted to preserve those existing blocks. + List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("v2-rsa-2048-with-extra-sig-block.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(false) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)); + + ApkVerifier.Result result = verify(signedApk, null); + assertVerified(result); + assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + assertSigningBlockContains(signedApk, Pair.of(EXTRA_BLOCK_VALUE, EXTRA_BLOCK_ID)); + } + + @Test + public void testOtherSignersSignaturesPreserved_v2OnlyAppendV3SameSigner_throwsException() + throws Exception { + // A V3 only signature cannot be appended to an existing V2 signature, even when using the + // same signer, since the V2 signature would then not contain the stripping protection for + // the V3 signature. If the same signer is being used then the signer should be configured + // to resign using the V2 signature scheme as well as the V3 signature scheme. + List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + + assertThrows(IllegalStateException.class, () -> + sign("v2-rsa-2048-with-extra-sig-block.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true))); + } + + @Test + public void testOtherSignersSignaturesPreserved_v1v2IndividuallySign_signaturesAppended() + throws Exception { + // One of the primary requirements for appending signatures is when an APK has already + // released with two signers; with the minimum signature scheme v2 requirement for target + // SDK version 30+ each signer must be able to append their signature to the existing + // signature block. This test verifies an APK with appended signatures verifies as expected + // after a series of appending V1 and V2 signatures. + List<ApkSigner.SignerConfig> rsa2048SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + // When two parties are signing an APK the first must sign with both V1 and V2; this will + // write the stripping-protection attribute to the V1 signature. + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false)); + + // The second party can then append their signature with both the V1 and V2 signature; this + // will invalidate the V2 signature of the initial signer since the APK itself will be + // modified with this signers V1 / jar signature. + signedApk = sign(signedApk, + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)); + + // The first party will then need to resign with just the V2 signature after its previous + // signature was invalidated by the V1 signature of the second signer; however since this + // signature is appended its previous V2 signature should be removed from the signature + // block and replaced with this new signature while preserving the V2 signature of the + // other signer. + signedApk = sign(signedApk, + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(false) + .setV2SigningEnabled(true) + .setV3SigningEnabled(false) + .setV4SigningEnabled(false) + .setOtherSignersSignaturesPreserved(true)); + + ApkVerifier.Result result = verify(signedApk, null); + assertVerified(result); + assertResultContainsSigners(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + EC_P256_SIGNER_RESOURCE_NAME); + } + + @Test + public void testSetMinSdkVersionForRotation_lessThanT_noV31Block() throws Exception { + // The V3.1 signing block is intended to allow APK signing key rotation to target T+, but + // a minimum SDK version can be explicitly set for rotation; if it is less than T than + // the rotated key will be included in the V3.0 block. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApkMinRotationP = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result resultMinRotationP = verify(signedApkMinRotationP, null); + // The V3.1 signature scheme was introduced in T; specifying an older SDK version as the + // minimum for rotation should cause the APK to still be signed with rotation in the V3.0 + // signing block. + File signedApkMinRotationS = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.S) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result resultMinRotationS = verify(signedApkMinRotationS, null); + + assertVerified(resultMinRotationP); + assertFalse(resultMinRotationP.isVerifiedUsingV31Scheme()); + assertEquals(1, resultMinRotationP.getV3SchemeSigners().size()); + assertResultContainsSigners(resultMinRotationP, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertVerified(resultMinRotationS); + assertFalse(resultMinRotationS.isVerifiedUsingV31Scheme()); + // While rotation is targeting S, signer blocks targeting specific SDK versions have not + // been tested in previous platform releases; ensure only a single signer block with the + // rotated key is in the V3 block. + assertEquals(1, resultMinRotationS.getV3SchemeSigners().size()); + assertResultContainsSigners(resultMinRotationS, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void testSetMinSdkVersionForRotation_TAndLater_v31Block() throws Exception { + // When T or later is specified as the minimum SDK version for rotation, then a new V3.1 + // signing block should be created with the new rotated key, and the V3.0 signing block + // should still be signed with the original key. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApkMinRotationT = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.T) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result resultMinRotationT = verify(signedApkMinRotationT, null); + // The API level for a release after T is not yet defined, so for now treat it as T + 1. + File signedApkMinRotationU = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.T + 1) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result resultMinRotationU = verify(signedApkMinRotationU, null); + + assertVerified(resultMinRotationT); + assertTrue(resultMinRotationT.isVerifiedUsingV31Scheme()); + assertResultContainsSigners(resultMinRotationT, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertV31SignerTargetsMinApiLevel(resultMinRotationT, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + AndroidSdkVersion.T); + assertVerified(resultMinRotationU); + assertTrue(resultMinRotationU.isVerifiedUsingV31Scheme()); + assertResultContainsSigners(resultMinRotationU, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertV31SignerTargetsMinApiLevel(resultMinRotationU, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + AndroidSdkVersion.T + 1); + } + + @Test + public void testSetMinSdkVersionForRotation_targetTNoOriginalSigner_fails() throws Exception { + // Similar to the V1 and V2 signatures schemes, if an app is targeting P or later with + // rotation targeting T, the original signer must be provided so that it can be used in the + // V3.0 signing block; if it is not provided the signer should throw an Exception. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List + .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + assertThrows(IllegalArgumentException.class, () -> + sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersion(28) + .setMinSdkVersionForRotation(AndroidSdkVersion.T) + .setSigningCertificateLineage(lineage))); + } + + @Test + public void testSetMinSdkVersionForRotation_targetTAndApkMinSdkT_onlySignsV3Block() + throws Exception { + // A V3.1 signing block should only exist alongside a V3.0 signing block; if an APK's + // min SDK version is greater than or equal to the SDK version for rotation then the + // original signer should not be required, and the rotated signing key should be in + // a V3.0 signing block. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = List + .of(getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersion(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT) + .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verifyForMinSdkVersion(signedApk, + V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT); + + assertVerified(result); + assertFalse(result.isVerifiedUsingV31Scheme()); + assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void testSetMinSdkVersionForRotation_targetTWithSourceStamp_noWarnings() + throws Exception { + // Source stamp verification will report a warning if a stamp signature is not found for any + // of the APK Signature Schemes used to sign the APK. This test verifies an APK signed with + // a rotated key in the v3.1 block and a source stamp successfully verifies, including the + // source stamp, without any warnings. + ApkSigner.SignerConfig rsa2048OriginalSignerConfig = getDefaultSignerConfigFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + rsa2048OriginalSignerConfig, + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.T) + .setSigningCertificateLineage(lineage) + .setSourceStampSignerConfig(rsa2048OriginalSignerConfig)); + ApkVerifier.Result result = verify(signedApk, null); + + assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + AndroidSdkVersion.T); + assertSourceStampVerified(signedApk, result); + } + + @Test + public void testSetRotationTargetsDevRelease_target34_v30SignerTargetsAtLeast34() + throws Exception { + // During development of a new platform release the new platform will use the SDK version + // of the previously released platform, so in order to test rotation on a new platform + // release it must target the SDK version of the previous platform. However an APK signed + // with the v3.1 signature scheme and targeting rotation on the previous platform release X + // would still use rotation if that APK were installed on a device running release version + // X. To support targeting rotation on the main branch, the v3.1 signature scheme supports + // a rotation-targets-dev-release attribute; this allows the APK to use the v3.1 signer + // block on a development platform with SDK version X while a release platform X will + // skip this signer block when it sees this additional attribute. To ensure that the APK + // will still target the released platform X, the v3.0 signer must have a maxSdkVersion + // of at least X for the signer. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + int rotationMinSdkVersion = V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT + 1; + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(rotationMinSdkVersion) + .setSigningCertificateLineage(lineage) + .setRotationTargetsDevRelease(true)); + ApkVerifier.Result result = verify(signedApk, null); + + assertVerified(result); + assertTrue(result.isVerifiedUsingV31Scheme()); + assertTrue(result.getV31SchemeSigners().get(0).getRotationTargetsDevRelease()); + assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertTrue(result.getV3SchemeSigners().get(0).getMaxSdkVersion() >= rotationMinSdkVersion); + } + + @Test + public void testV3_rotationMinSdkVersionLessThanTV3Only_origSignerNotRequired() + throws Exception { + // The v3.1 signature scheme allows a rotation-min-sdk-version be specified to target T+ + // for rotation; however if this value is less than the expected SDK version of T, then + // apksig should just use the rotated signing key in the v3.0 block. An APK that targets + // P+ that wants to use rotation in the v3.0 signing block should only need to provide + // the rotated signing key and lineage; this test ensures this behavior when the + // rotation-min-sdk-version is set to a value > P and < T. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApkRotationOnQ = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(false) + .setV2SigningEnabled(false) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersion(AndroidSdkVersion.P) + .setMinSdkVersionForRotation(AndroidSdkVersion.Q) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result resultRotationOnQ = verify(signedApkRotationOnQ, AndroidSdkVersion.P); + + assertVerified(resultRotationOnQ); + assertEquals(1, resultRotationOnQ.getV3SchemeSigners().size()); + assertFalse(resultRotationOnQ.isVerifiedUsingV31Scheme()); + assertResultContainsSigners(resultRotationOnQ, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void testV31_rotationMinSdkVersionEqualsMinSdkVersion_v3SignerPresent() + throws Exception { + // The SDK version for Sv2 (32) is used as the minSdkVersion for the V3.1 signature + // scheme to allow rotation to target the T development platform; this will be updated + // to the real SDK version of T once its SDK is finalized. This test verifies if a + // package has Sv2 as its minSdkVersion, the signing can complete as expected with the + // v3 block signed by the original signer and targeting just Sv2, and the v3.1 block + // signed by the rotated signer and targeting the dev release of Sv2 and all later releases. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original-minSdk32.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verify(signedApk, null); + + assertVerified(result); + assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion()); + } + + @Test + public void testV31_rotationMinSdkVersionTWithoutLineage_v30VerificationSucceeds() + throws Exception { + // apksig allows setting a rotation-min-sdk-version without providing a rotated signing + // key / lineage; however in the absence of rotation, the rotation-min-sdk-version should + // be a no-op, and the stripping protection attribute should not be written to the v3.0 + // signer. + List<ApkSigner.SignerConfig> rsa2048SignerConfig = + Collections.singletonList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT)); + ApkVerifier.Result result = verify(signedApk, null); + + assertVerified(result); + assertFalse(result.isVerifiedUsingV31Scheme()); + assertTrue(result.isVerifiedUsingV3Scheme()); + } + + @Test + public void testV31_rotationMinSdkVersionDefault_rotationTargetsT() throws Exception { + // The v3.1 signature scheme was introduced in T to allow developers to target T+ for + // rotation due to known issues with rotation on previous platform releases. This test + // verifies an APK signed with a rotated signing key defaults to the original signing + // key used in the v3 signing block for pre-T devices, and the rotated signing key used + // in the v3.1 signing block for T+ devices. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verify(signedApk, null); + + assertVerified(result); + assertTrue(result.isVerifiedUsingV3Scheme()); + assertTrue(result.isVerifiedUsingV31Scheme()); + assertEquals(AndroidSdkVersion.Sv2, result.getV3SchemeSigners().get(0).getMaxSdkVersion()); + assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, + AndroidSdkVersion.T); + } + + @Test + public void testV31_rotationMinSdkVersionP_rotationTargetsP() throws Exception { + // While the V3.1 signature scheme will target T by default, a package that has + // previously rotated can provide a rotation-min-sdk-version less than T to continue + // using the rotated signing key in the v3.0 block. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verify(signedApk, null); + + assertVerified(result); + assertTrue(result.isVerifiedUsingV3Scheme()); + assertFalse(result.isVerifiedUsingV31Scheme()); + } + + @Test + public void testV4_rotationMinSdkVersionLessThanT_signatureOnlyHasRotatedSigner() + throws Exception { + // To support SDK version targeting in the v3.1 signature scheme, apksig added a + // rotation-min-sdk-version option to allow the caller to specify the level from which + // the rotated signer should be used. A value less than T should result in a single + // rotated signer in the V3 block (along with the corresponding lineage), and the V4 + // signature should use this signer. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.P) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verify(signedApk, null); + + assertResultContainsV4Signers(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void testV4_rotationMinSdkVersionT_signatureHasOrigAndRotatedKey() throws Exception { + // When an APK is signed with a rotated key and the rotation-min-sdk-version X is set to T+, + // a V3.1 block will be signed with the rotated signing key targeting X and later, and + // a V3.0 block will be signed with the original signing key targeting P - X-1. The + // V4 signature should contain both the original signing key and the rotated signing + // key; this ensures if an APK is installed on a device running an SDK version less than X, + // the V4 signature will be verified using the original signing key which will be the only + // signing key visible to the platform. + List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = + Arrays.asList( + getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), + getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + SigningCertificateLineage lineage = + Resources.toSigningCertificateLineage( + ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(rsa2048SignerConfigWithLineage) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(true) + .setMinSdkVersionForRotation(AndroidSdkVersion.T) + .setSigningCertificateLineage(lineage)); + ApkVerifier.Result result = verify(signedApk, null); + + assertResultContainsV4Signers(result, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void + testSourceStampTimestamp_signWithSourceStampAndTimestampDefault_validTimestampValue() + throws Exception { + // Source stamps should include a timestamp attribute with the epoch time the stamp block + // was signed. This test verifies a standard signing with a source stamp includes a valid + // value for the source stamp timestamp attribute by default. + ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setSourceStampSignerConfig(rsa2048SignerConfig)); + ApkVerifier.Result result = verify(signedApk, null); + + assertSourceStampVerified(signedApk, result); + long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds(); + assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0); + } + + @Test + public void + testSourceStampTimestamp_signWithSourceStampAndTimestampEnabled_validTimestampValue() + throws Exception { + // Similar to above, this test verifies a valid timestamp value is written to the + // attribute when the caller explicitly requests to enable the source stamp timestamp. + ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setSourceStampSignerConfig(rsa2048SignerConfig) + .setSourceStampTimestampEnabled(true)); + ApkVerifier.Result result = verify(signedApk, null); + + assertSourceStampVerified(signedApk, result); + long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds(); + assertTrue("Invalid source stamp timestamp value: " + timestamp, timestamp > 0); + } + + @Test + public void + testSourceStampTimestamp_signWithSourceStampAndTimestampDisabled_defaultTimestampValue() + throws Exception { + // While source stamps should include a timestamp attribute indicating the time at which + // the stamp was signed, this can cause problems for reproducible builds. The + // ApkSigner.Builder#setSourceStampTimestampEnabled API allows the caller to specify + // whether the timestamp attribute should be written; this test verifies no timestamp is + // written to the source stamp if this API is used to disable the timestamp. + ApkSigner.SignerConfig rsa2048SignerConfig = getDefaultSignerConfigFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + List<ApkSigner.SignerConfig> ecP256SignerConfig = Collections.singletonList( + getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME)); + + File signedApk = sign("original.apk", + new ApkSigner.Builder(ecP256SignerConfig) + .setV1SigningEnabled(true) + .setV2SigningEnabled(true) + .setV3SigningEnabled(true) + .setV4SigningEnabled(false) + .setSourceStampSignerConfig(rsa2048SignerConfig) + .setSourceStampTimestampEnabled(false)); + ApkVerifier.Result result = verify(signedApk, null); + + assertSourceStampVerified(signedApk, result); + long timestamp = result.getSourceStampInfo().getTimestampEpochSeconds(); + assertEquals(0, timestamp); + } + + /** + * Asserts the provided {@code signedApk} contains a signature block with the expected + * {@code byte[]} value and block ID as specified in the {@code expectedBlock}. + */ + private static void assertSigningBlockContains(File signedApk, + Pair<byte[], Integer> expectedBlock) throws Exception { + try (RandomAccessFile apkFile = new RandomAccessFile(signedApk, "r")) { + ApkUtils.ApkSigningBlock apkSigningBlock = ApkUtils.findApkSigningBlock( + DataSources.asDataSource(apkFile)); + List<Pair<byte[], Integer>> signatureBlocks = + ApkSigningBlockUtils.getApkSignatureBlocks(apkSigningBlock.getContents()); + for (Pair<byte[], Integer> signatureBlock : signatureBlocks) { + if (signatureBlock.getSecond().equals(expectedBlock.getSecond())) { + if (Arrays.equals(signatureBlock.getFirst(), expectedBlock.getFirst())) { + return; + } + } + } + fail(String.format( + "The APK signing block did not contain the expected block with ID %08x", + expectedBlock.getSecond())); + } + } + + /** + * Asserts the provided verification {@code result} contains the expected {@code signers} for + * each scheme that was used to verify the APK's signature. + */ + static void assertResultContainsSigners(ApkVerifier.Result result, String... signers) + throws Exception { + assertResultContainsSigners(result, false, signers); + } + + /** + * Asserts the provided verification {@code result} contains the expected {@code signers} for + * each scheme that was used to verify the APK's signature; if {@code rotationExpected} is set + * to {@code true}, then the first element in {@code signers} is treated as the expected + * original signer for any V1, V2, and V3 (where applicable) signatures, and the last element + * is the rotated expected signer for V3+. + */ + static void assertResultContainsSigners(ApkVerifier.Result result, + boolean rotationExpected, String... signers) throws Exception { + // A result must be successfully verified before verifying any of the result's signers. + assertTrue(result.isVerified()); + + List<X509Certificate> expectedSigners = new ArrayList<>(); + for (String signer : signers) { + ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer); + expectedSigners.addAll(signerConfig.getCertificates()); + } + // If rotation is expected then the V1 and V2 signature should only be signed by the + // original signer. + List<X509Certificate> expectedV1Signers = + rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners; + List<X509Certificate> expectedV2Signers = + rotationExpected ? List.of(expectedSigners.get(0)) : expectedSigners; + // V3 only supports a single signer; if rotation is not expected or the V3.1 block contains + // the rotated signing key then the expected V3.0 signer should be the original signer. + List<X509Certificate> expectedV3Signers = + !rotationExpected || result.isVerifiedUsingV31Scheme() + ? List.of(expectedSigners.get(0)) + : List.of(expectedSigners.get(expectedSigners.size() - 1)); + + if (result.isVerifiedUsingV1Scheme()) { + Set<X509Certificate> v1Signers = new HashSet<>(); + for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { + v1Signers.add(signer.getCertificate()); + } + assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedV1Signers) + + ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers), + v1Signers.containsAll(expectedV1Signers)); + } + + if (result.isVerifiedUsingV2Scheme()) { + Set<X509Certificate> v2Signers = new HashSet<>(); + for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { + v2Signers.add(signer.getCertificate()); + } + assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedV2Signers) + + ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers), + v2Signers.containsAll(expectedV2Signers)); + } + + if (result.isVerifiedUsingV3Scheme()) { + Set<X509Certificate> v3Signers = new HashSet<>(); + for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) { + v3Signers.add(signer.getCertificate()); + } + assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedV3Signers) + + ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers), + v3Signers.containsAll(expectedV3Signers)); + } + + if (result.isVerifiedUsingV31Scheme()) { + Set<X509Certificate> v31Signers = new HashSet<>(); + for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) { + v31Signers.add(signer.getCertificate()); + } + // V3.1 only supports specifying signatures with a rotated signing key; if a V3.1 + // signing block was verified then ensure it contains the expected rotated signer. + List<X509Certificate> expectedV31Signers = List + .of(expectedSigners.get(expectedSigners.size() - 1)); + assertTrue("Expected V3.1 signers: " + getAllSubjectNamesFrom(expectedV31Signers) + + ", actual V3.1 signers: " + getAllSubjectNamesFrom(v31Signers), + v31Signers.containsAll(expectedV31Signers)); + } } - private RSAPublicKey getRSAPublicKeyFromSigningBlock(DataSource apk, int signatureVersionId) + /** + * Asserts the provided verification {@code result} contains the expected V4 {@code signers}. + */ + private static void assertResultContainsV4Signers(ApkVerifier.Result result, String... signers) + throws Exception { + assertTrue(result.isVerified()); + assertTrue(result.isVerifiedUsingV4Scheme()); + List<X509Certificate> expectedSigners = new ArrayList<>(); + for (String signer : signers) { + ApkSigner.SignerConfig signerConfig = getDefaultSignerConfigFromResources(signer); + expectedSigners.addAll(signerConfig.getCertificates()); + } + List<X509Certificate> v4Signers = new ArrayList<>(); + for (ApkVerifier.Result.V4SchemeSignerInfo signer : result.getV4SchemeSigners()) { + v4Signers.addAll(signer.getCertificates()); + } + assertTrue("Expected V4 signers: " + getAllSubjectNamesFrom(expectedSigners) + + ", actual V4 signers: " + getAllSubjectNamesFrom(v4Signers), + v4Signers.containsAll(expectedSigners)); + } + + /** + * Asserts the provided {@code result} contains the expected {@code signer} targeting + * {@code minSdkVersion} as the minimum version for rotation. + */ + static void assertV31SignerTargetsMinApiLevel(ApkVerifier.Result result, String signer, + int minSdkVersion) throws Exception { + assertTrue(result.isVerifiedUsingV31Scheme()); + ApkSigner.SignerConfig expectedSignerConfig = getDefaultSignerConfigFromResources(signer); + + for (ApkVerifier.Result.V3SchemeSignerInfo signerConfig : result.getV31SchemeSigners()) { + if (signerConfig.getCertificates() + .containsAll(expectedSignerConfig.getCertificates())) { + assertEquals("The signer, " + getAllSubjectNamesFrom(signerConfig.getCertificates()) + + ", is expected to target SDK version " + minSdkVersion + + ", instead it is targeting " + signerConfig.getMinSdkVersion(), + minSdkVersion, signerConfig.getMinSdkVersion()); + return; + } + } + fail("Did not find the expected signer, " + getAllSubjectNamesFrom( + expectedSignerConfig.getCertificates())); + } + + /** + * Returns a comma delimited {@code String} containing all of the Subject Names from the + * provided {@code certificates}. + */ + private static String getAllSubjectNamesFrom(Collection<X509Certificate> certificates) { + StringBuilder result = new StringBuilder(); + for (X509Certificate certificate : certificates) { + if (result.length() > 0) { + result.append(", "); + } + result.append(certificate.getSubjectDN().getName()); + } + return result.toString(); + } + + private static boolean resourceZipFileContains(String resourceName, String zipEntryName) + throws IOException { + ZipInputStream zip = new ZipInputStream( + Resources.toInputStream(ApkSignerTest.class, resourceName)); + while (true) { + ZipEntry entry = zip.getNextEntry(); + if (entry == null) { + break; + } + + if (entry.getName().equals(zipEntryName)) { + return true; + } + } + + return false; + } + + 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( @@ -1154,13 +2288,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); + } } /** @@ -1173,18 +2311,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"); } - ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size()); + byte[] outData = new byte[(int) out.length()]; + try (FileInputStream fis = new FileInputStream(out)) { + fis.read(outData); + } + 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()) { @@ -1198,47 +2340,57 @@ 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) - throws Exception { + private File sign(File inApkFile, ApkSigner.Builder apkSignerBuilder) throws Exception { + try (RandomAccessFile apkFile = new RandomAccessFile(inApkFile, "r")) { + DataSource in = DataSources.asDataSource(apkFile); + return sign(in, 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; + return sign(in, apkSignerBuilder); } - private static ApkVerifier.Result verifyForMinSdkVersion(DataSource apk, int minSdkVersion) + private File sign(DataSource in, ApkSigner.Builder apkSignerBuilder) throws Exception { + 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(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(); } @@ -1246,17 +2398,40 @@ public class ApkSignerTest { ApkVerifierTest.assertVerified(result); } + private static void assertSourceStampVerified(File signedApk, ApkVerifier.Result result) + throws ApkSigningBlockUtils.SignatureNotFoundException, IOException, + ZipFormatException { + SignatureInfo signatureInfo = + getSignatureInfoFromApk( + signedApk, + ApkSigningBlockUtils.VERSION_SOURCE_STAMP, + SourceStampConstants.V2_SOURCE_STAMP_BLOCK_ID); + assertNotNull(signatureInfo.signatureBlock); + assertTrue(result.isSourceStampVerified()); + } + private static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) { ApkVerifierTest.assertVerificationFailure(result, expectedIssue); } + private void assertFileContentsEqual(File first, File second) throws IOException { + assertArrayEquals(Files.readAllBytes(Paths.get(first.getPath())), + Files.readAllBytes(Paths.get(second.getPath()))); + } + private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources( String keyNameInResources) throws Exception { + return getDefaultSignerConfigFromResources(keyNameInResources, false); + } + + private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources( + String keyNameInResources, boolean deterministicDsaSigning) throws Exception { PrivateKey privateKey = Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8"); List<X509Certificate> certs = Resources.toCertificateChain(ApkSignerTest.class, keyNameInResources + ".x509.pem"); - return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build(); + return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs, + deterministicDsaSigning).build(); } private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources( @@ -1267,4 +2442,9 @@ public class ApkSignerTest { Resources.toCertificateChain(ApkSignerTest.class, certNameInResources); return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build(); } + + private static ApkSigner.SignerConfig getDeterministicDsaSignerConfigFromResources( + String keyNameInResources) throws Exception { + return getDefaultSignerConfigFromResources(keyNameInResources, true); + } } diff --git a/src/test/java/com/android/apksig/ApkVerifierTest.java b/src/test/java/com/android/apksig/ApkVerifierTest.java index 9369333..9de2b59 100644 --- a/src/test/java/com/android/apksig/ApkVerifierTest.java +++ b/src/test/java/com/android/apksig/ApkVerifierTest.java @@ -16,25 +16,35 @@ package com.android.apksig; +import static com.android.apksig.ApkSignerTest.FIRST_RSA_2048_SIGNER_RESOURCE_NAME; +import static com.android.apksig.ApkSignerTest.SECOND_RSA_2048_SIGNER_RESOURCE_NAME; +import static com.android.apksig.ApkSignerTest.assertResultContainsSigners; +import static com.android.apksig.ApkSignerTest.assertV31SignerTargetsMinApiLevel; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; 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.apk.v3.V3SchemeConstants; import com.android.apksig.internal.util.AndroidSdkVersion; import com.android.apksig.internal.util.HexEncoding; import com.android.apksig.internal.util.Resources; import com.android.apksig.util.DataSources; +import java.security.Provider; import org.junit.Assume; import org.junit.Test; 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 +70,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 @@ -781,6 +796,47 @@ public class ApkVerifierTest { } @Test + public void testTargetSdkMinSchemeVersionNotMet() throws Exception { + // Android 11 / SDK version 30 requires apps targeting this SDK version or higher must be + // signed with at least the V2 signature scheme. This test verifies if an app is targeting + // this SDK version and is only signed with a V1 signature then the verifier reports the + // platform will not accept it. + assertVerificationFailure(verify("v1-ec-p256-targetSdk-30.apk"), + Issue.MIN_SIG_SCHEME_FOR_TARGET_SDK_NOT_MET); + } + + @Test + public void testTargetSdkMinSchemeVersionMet() throws Exception { + // This test verifies if an app is signed with the minimum required signature scheme version + // for the target SDK version then the verifier reports the platform will accept it. + assertVerified(verify("v2-ec-p256-targetSdk-30.apk")); + + // If an app is only signed with a signature scheme higher than the required version for the + // target SDK the verifier should also report that the platform will accept it. + assertVerified(verify("v3-ec-p256-targetSdk-30.apk")); + } + + @Test + public void testTargetSdkMinSchemeVersionNotMetMaxLessThanTarget() throws Exception { + // If the minimum signature scheme for the target SDK version is not met but the maximum + // SDK version is less than the target then the verifier should report that the platform + // will accept it since the specified max SDK version does not know about the minimum + // signature scheme requirement. + verifyForMaxSdkVersion("v1-ec-p256-targetSdk-30.apk", 29); + } + + @Test + public void testTargetSdkNoUsesSdkElement() throws Exception { + // The target SDK minimum signature scheme version check will attempt to obtain the + // targetSdkVersion attribute value from the uses-sdk element in the AndroidManifest. If + // the targetSdkVersion is not specified then the verifier should behave the same as the + // platform; the minSdkVersion should be used when available and when neither the minimum or + // target SDK are specified a default value of 1 should be used. This test verifies that the + // verifier does not fail when the uses-sdk element is not specified. + verify("v1-only-no-uses-sdk.apk"); + } + + @Test public void testV1MultipleDigestAlgsInManifestAndSignatureFile() throws Exception { // MANIFEST.MF contains SHA-1 and SHA-256 digests for each entry, .SF contains only SHA-1 // digests. This file was obtained by: @@ -1023,6 +1079,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. @@ -1031,6 +1114,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. @@ -1041,14 +1132,128 @@ public class ApkVerifierTest { } @Test - public void testSourceStampBlock_apkHashMismatch() throws Exception { - ApkVerifier.Result verificationResult = verify("stamp-apk-hash-mismatch.apk"); + 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. + assertVerified(verificationResult); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @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. assertVerified(verificationResult); assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); } @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. + assertVerified(verificationResult); + assertSourceStampVerificationFailure(verificationResult, Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @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. @@ -1057,6 +1262,340 @@ 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 verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception { + // A timestamp attribute was added to the source stamp, but verification of APKs that were + // generated prior to the addition of the timestamp should still complete successfully, + // returning a default value of 0 for the timestamp. + ApkVerifier.Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk"); + + assertTrue(verificationResult.isSourceStampVerified()); + assertEquals( + "A value of 0 should be returned for the timestamp when the attribute is not " + + "present", + 0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception { + // Once an APK is signed with a source stamp that contains a valid value for the timestamp + // attribute, verification of the source stamp should result in the same value for the + // timestamp returned to the verifier. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-valid-timestamp-value.apk"); + + assertTrue(verificationResult.isSourceStampVerified()); + assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue() + throws Exception { + // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer + // as a little-endian long; while a larger buffer will not result in an error, any + // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a + // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read + // successfully. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-valid-timestamp-16-byte-buffer.apk"); + + assertTrue(verificationResult.isSourceStampVerified()); + assertEquals(1645126786, + verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails() + throws Exception { + // If the source stamp timestamp attribute exists and is <= 0, then a warning should be + // reported to notify the caller to the invalid attribute value. This test verifies a + // a warning is reported when the timestamp attribute value is 0. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-invalid-timestamp-value-zero.apk"); + + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + @Test + public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails() + throws Exception { + // If the source stamp timestamp attribute exists and is <= 0, then a warning should be + // reported to notify the caller to the invalid attribute value. This test verifies a + // a warning is reported when the timestamp attribute value is < 0. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-invalid-timestamp-value-less-than-zero.apk"); + + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + @Test + public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails() + throws Exception { + // The source stamp's timestamp attribute value is expected to be written to the first 8 + // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp + // value is not written as a little-endian long to the first 8 bytes of the buffer, then + // an error should be reported for the timestamp attribute since the rest of the buffer will + // be ignored. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-timestamp-in-last-8-of-16-byte-buffer.apk"); + + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + + @Test + public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception { + // Since the source stamp timestamp attribute value is a long, an attribute value with + // insufficient space to hold a long value should result in a warning reported to the user. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-int-timestamp-value.apk"); + + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); + } + + @Test + public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception { + // The source stamp timestamp attribute is part of the block's signed data; this test + // verifies if the value of the timestamp in the stamp block is modified then verification + // of the source stamp should fail. + ApkVerifier.Result verificationResult = verifySourceStamp( + "stamp-valid-timestamp-value-modified.apk"); + + assertSourceStampVerificationStatus(verificationResult, + SourceStampVerificationStatus.STAMP_VERIFICATION_FAILED); + assertSourceStampVerificationFailure(verificationResult, + Issue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @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()); + } + } + + @Test + public void verifySignature_negativeModulusConscryptProvider() throws Exception { + Provider conscryptProvider = null; + try { + conscryptProvider = new org.conscrypt.OpenSSLProvider(); + Security.insertProviderAt(conscryptProvider, 1); + assertVerified(verify("v1v2v3-rsa-2048-negmod-in-cert.apk")); + } catch (UnsatisfiedLinkError e) { + // If the library for conscrypt is not available then skip this test. + return; + } finally { + if (conscryptProvider != null) { + Security.removeProvider(conscryptProvider.getName()); + } + } + } + + @Test + public void verifyV31_rotationTarget34_containsExpectedSigners() throws Exception { + // This test verifies an APK targeting a specific SDK version for rotation properly reports + // that version for the rotated signer in the v3.1 block, and all other signing blocks + // use the original signing key. + ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk"); + + assertVerified(result); + assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34); + } + + @Test + public void verifyV31_missingStrippingAttr_warningReported() throws Exception { + // The v3.1 signing block supports targeting SDK versions; to protect against these target + // versions being modified the v3 signer contains a stripping protection attribute with the + // SDK version on which rotation should be applied. This test verifies a warning is reported + // when this attribute is not present in the v3 signer. + ApkVerifier.Result result = verify("v31-tgt-33-no-v3-attr.apk"); + + assertVerificationWarning(result, Issue.V31_ROTATION_MIN_SDK_ATTR_MISSING); + } + + @Test + public void verifyV31_strippingAttrMismatch_errorReportedOnSupportedVersions() + throws Exception { + // This test verifies if the stripping protection attribute does not properly match the + // minimum SDK version on which rotation is supported then the APK should fail verification. + ApkVerifier.Result result = verify("v31-tgt-34-v3-attr-value-33.apk"); + assertVerificationFailure(result, Issue.V31_ROTATION_MIN_SDK_MISMATCH); + + // SDK versions that do not support v3.1 should ignore the stripping protection attribute + // and the v3.1 signing block. + result = verifyForMaxSdkVersion("v31-tgt-34-v3-attr-value-33.apk", + V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1); + assertVerified(result); + } + + @Test + public void verifyV31_missingV31Block_errorReportedOnSupportedVersions() throws Exception { + // This test verifies if the stripping protection attribute contains a value for rotation + // but a v3.1 signing block was not found then the APK should fail verification. + ApkVerifier.Result result = verify("v31-block-stripped-v3-attr-value-33.apk"); + assertVerificationFailure(result, Issue.V31_BLOCK_MISSING); + + // SDK versions that do not support v3.1 should ignore the stripping protection attribute + // and the v3.1 signing block. + result = verifyForMaxSdkVersion("v31-block-stripped-v3-attr-value-33.apk", + V3SchemeConstants.MIN_SDK_WITH_V31_SUPPORT - 1); + assertVerified(result); + } + + @Test + public void verifyV31_v31BlockWithoutV3Block_reportsError() throws Exception { + // A v3.1 block must always exist alongside a v3.0 block; if an APK's minSdkVersion is the + // same as the version supporting rotation then it should be written to a v3.0 block. + ApkVerifier.Result result = verify("v31-tgt-33-no-v3-block.apk"); + assertVerificationFailure(result, Issue.V31_BLOCK_FOUND_WITHOUT_V3_BLOCK); + } + + @Test + public void verifyV31_rotationTargetsDevRelease_resultReportsDevReleaseFlag() throws Exception { + // Development releases use the SDK version of the previous release until the SDK is + // finalized. In order to only target the development release and later, the v3.1 signature + // scheme supports targeting development releases such that the SDK version X will install + // on a device running X with the system property ro.build.version.codename set to a new + // development codename (eg T); a release platform will have this set to "REL", and the + // platform will ignore the v3.1 signer if the minSdkVersion is X and the codename is "REL". + ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-dev-release.apk"); + + assertVerified(result); + assertV31SignerTargetsMinApiLevel(result, SECOND_RSA_2048_SIGNER_RESOURCE_NAME, 34); + assertResultContainsSigners(result, true, FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void verifyV3_v3RotatedSignerTargetsDevRelease_warningReported() throws Exception { + // While a v3.1 signer can target a development release, v3.0 does not support the same + // attribute since it is only intended for v3.1 with v3.0 using the original signer. This + // test verifies a warning is reported if an APK has this flag set on a v3.0 signer since it + // will be ignored by the platform. + ApkVerifier.Result result = verify("v3-rsa-2048_2-tgt-dev-release.apk"); + + assertVerificationWarning(result, Issue.V31_ROTATION_TARGETS_DEV_RELEASE_ATTR_ON_V3_SIGNER); + } + + @Test + public void verifyV31_rotationTargets34_resultContainsExpectedLineage() throws Exception { + // During verification of the v3.1 and v3.0 signing blocks, ApkVerifier will set the + // signing certificate lineage in the Result object; this test verifies a null lineage from + // a v3.0 signer does not overwrite a valid lineage from a v3.1 signer. + ApkVerifier.Result result = verify("v31-rsa-2048_2-tgt-34-1-tgt-28.apk"); + + assertNotNull(result.getSigningCertificateLineage()); + SigningCertificateLineageTest.assertLineageContainsExpectedSigners( + result.getSigningCertificateLineage(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME, + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + } + + @Test + public void verify31_minSdkVersionT_resultSuccessfullyVerified() throws Exception { + // When a min-sdk-version of 33 is explicitly specified, apksig will behave the same as a + // device running this API level and only verify a v3.1 signature if it exists. This test + // verifies this v3.1 signature is sufficient to report the APK as verified. + ApkVerifier.Result result = verifyForMinSdkVersion("v31-rsa-2048_2-tgt-33-1-tgt-28.apk", + 33); + + assertVerified(result); + assertTrue(result.isVerifiedUsingV31Scheme()); + } + + @Test + public void verify31_minSdkVersionTTargetSdk30_resultSuccessfullyVerified() throws Exception { + // This test verifies when a min-sdk-version of 33 is specified and the APK targets API + // level 30 or later, the v3.1 signature is sufficient to report the APK meets the + // requirement of a minimum v2 signature. + ApkVerifier.Result result = verifyForMinSdkVersion( + "v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk", 33); + + assertVerified(result); + assertTrue(result.isVerifiedUsingV31Scheme()); + } + private ApkVerifier.Result verify(String apkFilenameInResources) throws IOException, ApkFormatException, NoSuchAlgorithmException { return verify(apkFilenameInResources, null, null); @@ -1092,6 +1631,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"); } @@ -1165,13 +1734,27 @@ public class ApkVerifierTest { } static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) { - if (result.isVerified()) { + assertVerificationIssue(result, expectedIssue, true); + } + + static void assertVerificationWarning(ApkVerifier.Result result, Issue expectedIssue) { + assertVerificationIssue(result, expectedIssue, false); + } + + /** + * Asserts the provided {@code result} contains the {@code expectedIssue}; if {@code + * verifyError} is set to {@code true} then the specified {@link Issue} will be expected as an + * error, otherwise it will be expected as a warning. + */ + private static void assertVerificationIssue(ApkVerifier.Result result, Issue expectedIssue, + boolean verifyError) { + if (result.isVerified() && verifyError) { fail("APK verification succeeded instead of failing with " + expectedIssue); return; } StringBuilder msg = new StringBuilder(); - for (IssueWithParams issue : result.getErrors()) { + for (IssueWithParams issue : (verifyError ? result.getErrors() : result.getWarnings())) { if (expectedIssue.equals(issue.getIssue())) { return; } @@ -1182,7 +1765,8 @@ public class ApkVerifierTest { } for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { String signerName = signer.getName(); - for (ApkVerifier.IssueWithParams issue : signer.getErrors()) { + for (ApkVerifier.IssueWithParams issue : (verifyError ? signer.getErrors() + : signer.getWarnings())) { if (expectedIssue.equals(issue.getIssue())) { return; } @@ -1199,7 +1783,8 @@ public class ApkVerifierTest { } for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { String signerName = "signer #" + (signer.getIndex() + 1); - for (IssueWithParams issue : signer.getErrors()) { + for (IssueWithParams issue : (verifyError ? signer.getErrors() + : signer.getWarnings())) { if (expectedIssue.equals(issue.getIssue())) { return; } @@ -1214,7 +1799,8 @@ public class ApkVerifierTest { } for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) { String signerName = "signer #" + (signer.getIndex() + 1); - for (IssueWithParams issue : signer.getErrors()) { + for (IssueWithParams issue : (verifyError ? signer.getErrors() + : signer.getWarnings())) { if (expectedIssue.equals(issue.getIssue())) { return; } @@ -1227,6 +1813,22 @@ public class ApkVerifierTest { .append(issue); } } + for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV31SchemeSigners()) { + String signerName = "signer #" + (signer.getIndex() + 1); + for (IssueWithParams issue : (verifyError ? signer.getErrors() + : signer.getWarnings())) { + if (expectedIssue.equals(issue.getIssue())) { + return; + } + if (msg.length() > 0) { + msg.append('\n'); + } + msg.append("APK Signature Scheme v3.1 signer ") + .append(signerName) + .append(": ") + .append(issue); + } + } fail( "APK failed verification for the wrong reason" @@ -1287,6 +1889,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..07a48f1 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; @@ -94,6 +90,23 @@ public class SigningCertificateLineageTest { } @Test + public void testLineageFromBytesContainsExpectedSigners() throws Exception { + // This file contains the lineage with the three rsa-2048 signers + DataSource lineageDataSource = Resources.toDataSource(getClass(), + "rsa-2048-lineage-3-signers"); + SigningCertificateLineage lineage = SigningCertificateLineage.readFromBytes( + lineageDataSource.getByteBuffer(0, (int) lineageDataSource.size()).array()); + List<SignerConfig> signers = new ArrayList<>(3); + signers.add( + Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); + signers.add( + Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); + signers.add( + Resources.toLineageSignerConfig(getClass(), THIRD_RSA_2048_SIGNER_RESOURCE_NAME)); + assertLineageContainsExpectedSigners(lineage, signers); + } + + @Test public void testLineageFromFileContainsExpectedSigners() throws Exception { // This file contains the lineage with the three rsa-2048 signers DataSource lineageDataSource = Resources.toDataSource(getClass(), @@ -135,6 +148,17 @@ public class SigningCertificateLineageTest { } @Test + public void testLineageWrittenToBytesContainsExpectedSigners() throws Exception { + SigningCertificateLineage lineage = createLineageWithSignersFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + lineage = updateLineageWithSignerFromResources(lineage, + THIRD_RSA_2048_SIGNER_RESOURCE_NAME); + byte[] lineageBytes = lineage.getBytes(); + lineage = SigningCertificateLineage.readFromBytes(lineageBytes); + assertLineageContainsExpectedSigners(lineage, mSigners); + } + + @Test public void testLineageWrittenToFileContainsExpectedSigners() throws Exception { SigningCertificateLineage lineage = createLineageWithSignersFromResources( FIRST_RSA_2048_SIGNER_RESOURCE_NAME, SECOND_RSA_2048_SIGNER_RESOURCE_NAME); @@ -249,7 +273,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 +283,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); @@ -372,6 +397,21 @@ public class SigningCertificateLineageTest { assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners); } + @Test + public void testLineageFromAPKWithV31BlockContainsExpectedSigners() throws Exception { + SignerConfig firstSigner = getSignerConfigFromResources( + FIRST_RSA_2048_SIGNER_RESOURCE_NAME); + SignerConfig secondSigner = getSignerConfigFromResources( + SECOND_RSA_2048_SIGNER_RESOURCE_NAME); + List<SignerConfig> expectedSigners = Arrays.asList(firstSigner, secondSigner); + DataSource apkDataSource = Resources.toDataSource(getClass(), + "v31-rsa-2048_2-tgt-34-1-tgt-28.apk"); + SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource( + apkDataSource); + assertLineageContainsExpectedSigners(lineageFromApk, expectedSigners); + + } + @Test(expected = ApkFormatException.class) public void testLineageFromAPKWithInvalidZipCDSizeFails() throws Exception { // This test verifies that attempting to read the lineage from an APK where the zip @@ -510,7 +550,20 @@ public class SigningCertificateLineageTest { return lineage.spawnDescendant(oldSignerConfig, newSignerConfig); } - private void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage, + /** + * Asserts the provided {@code lineage} contains the {@code expectedSigners} from the test's + * resources. + */ + static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage, + String... expectedSigners) throws Exception { + List<SignerConfig> signers = new ArrayList<>(); + for (String expectedSigner : expectedSigners) { + signers.add(getSignerConfigFromResources(expectedSigner)); + } + assertLineageContainsExpectedSigners(lineage, signers); + } + + private static void assertLineageContainsExpectedSigners(SigningCertificateLineage lineage, List<SignerConfig> signers) { assertEquals("The lineage does not contain the expected number of signers", signers.size(), lineage.size()); 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..2186744 --- /dev/null +++ b/src/test/java/com/android/apksig/SourceStampVerifierTest.java @@ -0,0 +1,567 @@ +/* + * 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.assertTrue; +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.AndroidSdkVersion; +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); + } + + @Test + public void verifySourceStamp_noTimestamp_returnsDefaultValue() throws Exception { + // A timestamp attribute was added to the source stamp, but verification of APKs that were + // generated prior to the addition of the timestamp should still complete successfully, + // returning a default value of 0 for the timestamp. + Result verificationResult = verifySourceStamp("v3-only-with-stamp.apk", AndroidSdkVersion.P, + AndroidSdkVersion.P); + + assertVerified(verificationResult); + assertEquals( + "A value of 0 should be returned for the timestamp when the attribute is not " + + "present", + 0, verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_validTimestamp_returnsExpectedValue() throws Exception { + // Once an APK is signed with a source stamp that contains a valid value for the timestamp + // attribute, verification of the source stamp should result in the same value for the + // timestamp returned to the verifier. + Result verificationResult = verifySourceStamp("stamp-valid-timestamp-value.apk"); + + assertVerified(verificationResult); + assertEquals(1644886584, verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_validTimestampLargerBuffer_returnsExpectedValue() + throws Exception { + // The source stamp timestamp attribute value is expected to be written to an 8 byte buffer + // as a little-endian long; while a larger buffer will not result in an error, any + // additional space after the buffer's initial 8 bytes will be ignored. This test verifies a + // valid timestamp value written to the first 8 bytes of a 16 byte buffer can still be read + // successfully. + Result verificationResult = verifySourceStamp("stamp-valid-timestamp-16-byte-buffer.apk"); + + assertEquals(1645126786, + verificationResult.getSourceStampInfo().getTimestampEpochSeconds()); + } + + @Test + public void verifySourceStamp_invalidTimestampValueEqualsZero_verificationFails() + throws Exception { + // If the source stamp timestamp attribute exists and is <= 0, then a warning should be + // reported to notify the caller to the invalid attribute value. This test verifies a + // a warning is reported when the timestamp attribute value is 0. + Result verificationResult = verifySourceStamp("stamp-invalid-timestamp-value-zero.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + @Test + public void verifySourceStamp_invalidTimestampValueLessThanZero_verificationFails() + throws Exception { + // If the source stamp timestamp attribute exists and is <= 0, then a warning should be + // reported to notify the caller to the invalid attribute value. This test verifies a + // a warning is reported when the timestamp attribute value is < 0. + Result verificationResult = verifySourceStamp( + "stamp-invalid-timestamp-value-less-than-zero.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + @Test + public void verifySourceStamp_invalidTimestampZeroInFirst8BytesOfBuffer_verificationFails() + throws Exception { + // The source stamp's timestamp attribute value is expected to be written to the first 8 + // bytes of the attribute's value buffer; if a larger buffer is used and the timestamp + // value is not written as a little-endian long to the first 8 bytes of the buffer, then + // an error should be reported for the timestamp attribute since the rest of the buffer will + // be ignored. + Result verificationResult = verifySourceStamp( + "stamp-timestamp-in-last-8-of-16-byte-buffer.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_INVALID_TIMESTAMP); + } + + @Test + public void verifySourceStamp_intTimestampValue_verificationFails() throws Exception { + // Since the source stamp timestamp attribute value is a long, an attribute value with + // insufficient space to hold a long value should result in a warning reported to the user. + Result verificationResult = verifySourceStamp( + "stamp-int-timestamp-value.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_MALFORMED_ATTRIBUTE); + } + + @Test + public void verifySourceStamp_modifiedTimestampValue_verificationFails() throws Exception { + // The source stamp timestamp attribute is part of the block's signed data; this test + // verifies if the value of the timestamp in the stamp block is modified then verification + // of the source stamp should fail. + Result verificationResult = verifySourceStamp( + "stamp-valid-timestamp-value-modified.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_DID_NOT_VERIFY); + } + + @Test + public void verifySourceStamp_unknownAttribute_verificationSucceeds() throws Exception { + // When a new attribute is added to the source stamp, verifiers previously released to + // prod will not recognize this new attribute. This test verifies an unknown attribute + // will not cause the verification to fail by using an attribute with ID 0xe43c5945. + Result verificationResult = verifySourceStamp("stamp-unknown-attr.apk"); + + assertVerified(verificationResult); + assertTrue(verificationResult.getSourceStampInfo().containsInfoMessages()); + assertTrue(verificationResult.getSourceStampInfo().getInfoMessages().stream().anyMatch( + info -> info.getIssueId() == ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_ATTRIBUTE)); + } + + @Test + public void verifySourceStamp_unknownSigAlgorithm_verificationSucceeds() throws Exception { + // When a new signature algorithm is added to the source stamp, verifiers previously + // released to prod will not recognize the new algorithm. This test verifies an unknown + // signature algorithm will not cause the verification to fail as long as there is a + // known signature that can be verified; this test uses a signature algorithm with ID + // 0x1ee. + Result verificationResult = verifySourceStamp("stamp-unknown-sig.apk"); + + assertVerified(verificationResult); + assertTrue(verificationResult.getSourceStampInfo().containsInfoMessages()); + assertTrue(verificationResult.getSourceStampInfo().getInfoMessages().stream().anyMatch( + info -> info.getIssueId() + == ApkVerificationIssue.SOURCE_STAMP_UNKNOWN_SIG_ALGORITHM)); + } + + @Test + public void verifySourceStamp_onlyUnknownSigAlgorithms_verificationFails() throws Exception { + // When a new signature algorithm is added to the source stamp, previously supported + // signature algorithms should still be written to the stamp to ensure existing verifiers + // can continue verifying the stamp. This test verifies if a stamp only contains signature + // algorithms unknown to the verifier then the verification fails as it is not able to + // verify any signatures; this test uses signature algorithms with IDs 0x1ee and 0x1ef. + Result verificationResult = verifySourceStamp("stamp-only-unknown-sigs.apk"); + + assertSourceStampVerificationFailure(verificationResult, + ApkVerificationIssue.SOURCE_STAMP_NO_SIGNATURE); + } + + 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/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java index 85e9e90..8396d76 100644 --- a/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java +++ b/src/test/java/com/android/apksig/internal/util/VerityTreeBuilderTest.java @@ -79,7 +79,7 @@ public final class VerityTreeBuilderTest { return DataSources.asDataSource(ByteBuffer.wrap(data.getBytes(UTF_8))); } - @Test public void generateVerityTreeRootHashFromDummyDataSource() throws Exception { + @Test public void generateVerityTreeRootHashFromPlaceholderDataSource() throws Exception { // This sample was taken from src/test/resources/com/android/apksig/original.apk. byte[] sampleEoCDFromDisk = new byte[] { 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x79, 0x01, diff --git a/src/test/resources/com/android/apksig/golden-aligned-out.apk b/src/test/resources/com/android/apksig/golden-aligned-out.apk Binary files differindex 2396782..e82f67b 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk Binary files differindex 5133049..1c0edeb 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk Binary files differindex b9dc782..a273b0c 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk Binary files differindex 2396782..e82f67b 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v1v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk Binary files differindex d947e3c..69f5e64 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk Binary files differindex 88c571b..2a0d383 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk Binary files differindex 25f35cc..19931ed 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk Binary files differindex 30e1f72..4a4cda9 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk Binary files differindex f97cbeb..48f9aa3 100644 --- a/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-aligned-v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-file-size-aligned.apk b/src/test/resources/com/android/apksig/golden-file-size-aligned.apk Binary files differnew file mode 100644 index 0000000..8dd95fc --- /dev/null +++ b/src/test/resources/com/android/apksig/golden-file-size-aligned.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk Binary files differindex d177361..aef3280 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk Binary files differindex cc03744..cf04aa2 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk Binary files differindex e359da7..376b0e5 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk Binary files differindex d177361..aef3280 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v1v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk Binary files differindex 68f07ed..f34a8e5 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk Binary files differindex 4b51e4f..25ffa5b 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk Binary files differindex 7177862..9940c4f 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk Binary files differindex bd3e668..341d7ba 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk Binary files differindex 67a7d3f..80a70b9 100644 --- a/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-legacy-aligned-v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk Binary files differnew file mode 100644 index 0000000..43c39f1 --- /dev/null +++ b/src/test/resources/com/android/apksig/golden-pinsapp-signed.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk Binary files differindex 7289853..5ae642a 100644 --- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk +++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-1-out.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk Binary files differindex 232db96..5ce588a 100644 --- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk +++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-18-out.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk Binary files differindex 232db96..5ce588a 100644 --- a/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk +++ b/src/test/resources/com/android/apksig/golden-rsa-minSdkVersion-24-out.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk Binary files differnew file mode 100644 index 0000000..5ce588a --- /dev/null +++ b/src/test/resources/com/android/apksig/golden-rsa-no-verity-out.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-out.apk b/src/test/resources/com/android/apksig/golden-rsa-out.apk Binary files differindex 232db96..5ce588a 100644 --- a/src/test/resources/com/android/apksig/golden-rsa-out.apk +++ b/src/test/resources/com/android/apksig/golden-rsa-out.apk diff --git a/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk Binary files differnew file mode 100644 index 0000000..232db96 --- /dev/null +++ b/src/test/resources/com/android/apksig/golden-rsa-verity-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-out.apk Binary files differindex 0bd34c4..17a872d 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk Binary files differindex c708211..5194496 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk Binary files differindex dd6324b..5636723 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk Binary files differindex 0bd34c4..17a872d 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v1v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk Binary files differindex 4fdc18c..92e5099 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v2-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk Binary files differindex 4e523ca..4f21b4c 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk Binary files differindex 74e7dbc..c18c445 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v2v3-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk Binary files differindex 831c756..7514d20 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-lineage-out.apk diff --git a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk Binary files differindex 3196267..4db4579 100644 --- a/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk +++ b/src/test/resources/com/android/apksig/golden-unaligned-v3-out.apk diff --git a/src/test/resources/com/android/apksig/original-minSdk32.apk b/src/test/resources/com/android/apksig/original-minSdk32.apk Binary files differnew file mode 100644 index 0000000..1d50f27 --- /dev/null +++ b/src/test/resources/com/android/apksig/original-minSdk32.apk diff --git a/src/test/resources/com/android/apksig/original-with-stamp-file.apk b/src/test/resources/com/android/apksig/original-with-stamp-file.apk Binary files differnew file mode 100644 index 0000000..604fe6f --- /dev/null +++ b/src/test/resources/com/android/apksig/original-with-stamp-file.apk 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/pinsapp-unsigned.apk b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk Binary files differnew file mode 100755 index 0000000..b6a6e8f --- /dev/null +++ b/src/test/resources/com/android/apksig/pinsapp-unsigned.apk diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk Binary files differnew file mode 100644 index 0000000..add4aa0 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v1.apk diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk Binary files differnew file mode 100644 index 0000000..e55eb90 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v2.apk diff --git a/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk Binary files differnew file mode 100644 index 0000000..de23558 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-apk-hash-mismatch-v3.apk diff --git a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk Binary files differindex 562805c..f1105f9 100644 --- a/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk +++ b/src/test/resources/com/android/apksig/stamp-certificate-mismatch.apk diff --git a/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk Binary files differnew file mode 100644 index 0000000..0cd740c --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-int-timestamp-value.apk diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk Binary files differnew file mode 100644 index 0000000..f4a189e --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-less-than-zero.apk diff --git a/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.apk Binary files differnew file mode 100644 index 0000000..6cfd082 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-invalid-timestamp-value-zero.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-malformed-signature.apk b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk Binary files differindex 2723cc8..d28774a 100644 --- a/src/test/resources/com/android/apksig/stamp-malformed-signature.apk +++ b/src/test/resources/com/android/apksig/stamp-malformed-signature.apk diff --git a/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk Binary files differnew file mode 100644 index 0000000..7ec82eb --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-only-unknown-sigs.apk diff --git a/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk Binary files differnew file mode 100644 index 0000000..260b0ca --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-timestamp-in-last-8-of-16-byte-buffer.apk diff --git a/src/test/resources/com/android/apksig/stamp-unknown-attr.apk b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk Binary files differnew file mode 100644 index 0000000..68771a5 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-unknown-attr.apk diff --git a/src/test/resources/com/android/apksig/stamp-unknown-sig.apk b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk Binary files differnew file mode 100644 index 0000000..1c1557e --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-unknown-sig.apk diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk Binary files differnew file mode 100644 index 0000000..da9c34d --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-16-byte-buffer.apk diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk Binary files differnew file mode 100644 index 0000000..eefc148 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value-modified.apk diff --git a/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.apk Binary files differnew file mode 100644 index 0000000..3c6a501 --- /dev/null +++ b/src/test/resources/com/android/apksig/stamp-valid-timestamp-value.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/stamp-without-block.apk b/src/test/resources/com/android/apksig/stamp-without-block.apk Binary files differindex 9dec2f5..604fe6f 100644 --- a/src/test/resources/com/android/apksig/stamp-without-block.apk +++ b/src/test/resources/com/android/apksig/stamp-without-block.apk diff --git a/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk Binary files differnew file mode 100644 index 0000000..6a561c0 --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-ec-p256-targetSdk-30.apk diff --git a/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk Binary files differnew file mode 100644 index 0000000..714f9ff --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-only-no-uses-sdk.apk diff --git a/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.apk Binary files differnew file mode 100644 index 0000000..61f4122 --- /dev/null +++ b/src/test/resources/com/android/apksig/v1-only-with-rsa-2048.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/v1v2v3-rsa-2048-negmod-in-cert.apk b/src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apk Binary files differnew file mode 100644 index 0000000..6f73b92 --- /dev/null +++ b/src/test/resources/com/android/apksig/v1v2v3-rsa-2048-negmod-in-cert.apk diff --git a/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.apk Binary files differnew file mode 100644 index 0000000..a29dd6c --- /dev/null +++ b/src/test/resources/com/android/apksig/v2-ec-p256-targetSdk-30.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/v2-rsa-2048-with-extra-sig-block.apk b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk Binary files differnew file mode 100644 index 0000000..94b54c9 --- /dev/null +++ b/src/test/resources/com/android/apksig/v2-rsa-2048-with-extra-sig-block.apk diff --git a/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.apk Binary files differnew file mode 100644 index 0000000..c58ec8b --- /dev/null +++ b/src/test/resources/com/android/apksig/v3-ec-p256-targetSdk-30.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 diff --git a/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk Binary files differnew file mode 100644 index 0000000..d3b2c14 --- /dev/null +++ b/src/test/resources/com/android/apksig/v3-rsa-2048_2-tgt-dev-release.apk diff --git a/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk Binary files differnew file mode 100644 index 0000000..d091075 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-block-stripped-v3-attr-value-33.apk diff --git a/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk Binary files differnew file mode 100644 index 0000000..ad14731 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-ec-p256-2-tgt-33-1-tgt-28-targetSdk-30.apk diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk Binary files differnew file mode 100644 index 0000000..aeaec33 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-33-1-tgt-28.apk diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk Binary files differnew file mode 100644 index 0000000..ccf59d4 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-1-tgt-28.apk diff --git a/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk Binary files differnew file mode 100644 index 0000000..784f47e --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-rsa-2048_2-tgt-34-dev-release.apk diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk Binary files differnew file mode 100644 index 0000000..284cd7a --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-attr.apk diff --git a/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk Binary files differnew file mode 100644 index 0000000..1c797a7 --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-tgt-33-no-v3-block.apk diff --git a/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk Binary files differnew file mode 100644 index 0000000..ab87fcc --- /dev/null +++ b/src/test/resources/com/android/apksig/v31-tgt-34-v3-attr-value-33.apk diff --git a/src/test/resources/com/android/apksig/valid-stamp.apk b/src/test/resources/com/android/apksig/valid-stamp.apk Binary files differindex 8056e0b..2f2a592 100644 --- a/src/test/resources/com/android/apksig/valid-stamp.apk +++ b/src/test/resources/com/android/apksig/valid-stamp.apk |