| /* |
| * Copyright (C) 2017 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.ApkUtils.SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME; |
| 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; |
| import static org.junit.Assert.fail; |
| |
| import com.android.apksig.ApkVerifier.Issue; |
| 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.SourceStampConstants; |
| import com.android.apksig.internal.apk.v1.V1SchemeVerifier; |
| 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.DataSource; |
| import com.android.apksig.util.DataSources; |
| 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.file.Files; |
| 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 { |
| |
| /** |
| * Whether to preserve, as files, outputs of failed tests. This is useful for investigating test |
| * failures. |
| */ |
| private static final boolean KEEP_FAILING_OUTPUT_AS_FILES = false; |
| |
| // All signers with the same prefix and an _X suffix were signed with the private key of the |
| // (X-1) signer. |
| 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 = |
| "rsa-2048_negmod.x509.der"; |
| |
| 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); |
| } |
| |
| private static void generateGoldenFiles(File outDir) throws Exception { |
| System.out.println( |
| "Generating golden files " |
| + ApkSignerTest.class.getSimpleName() |
| + " into " |
| + outDir); |
| if (!outDir.exists()) { |
| if (!outDir.mkdirs()) { |
| throw new IOException("Failed to create directory: " + outDir); |
| } |
| } |
| List<ApkSigner.SignerConfig> rsa2048SignerConfig = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = |
| Arrays.asList( |
| rsa2048SignerConfig.get(0), |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); |
| SigningCertificateLineage lineage = |
| Resources.toSigningCertificateLineage( |
| ApkSignerTest.class, LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v1-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(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) |
| .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) |
| .setV4SigningEnabled(false)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .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"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v1v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v1v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v1v2-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v1v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v1v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v1v2v3-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| signGolden( |
| "golden-unaligned-in.apk", |
| new File(outDir, "golden-unaligned-v1v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| signGolden( |
| "golden-legacy-aligned-in.apk", |
| new File(outDir, "golden-legacy-aligned-v1v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v1v2v3-lineage-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| |
| signGolden( |
| "original.apk", |
| new File(outDir, "golden-rsa-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| signGolden( |
| "original.apk", |
| new File(outDir, "golden-rsa-minSdkVersion-1-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(1)); |
| signGolden( |
| "original.apk", |
| new File(outDir, "golden-rsa-minSdkVersion-18-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig).setMinSdkVersion(18)); |
| signGolden( |
| "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( |
| "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( |
| String inResourceName, File outFile, ApkSigner.Builder apkSignerBuilder) |
| throws Exception { |
| DataSource in = |
| DataSources.asDataSource( |
| ByteBuffer.wrap(Resources.toByteArray(ApkSigner.class, inResourceName))); |
| apkSignerBuilder.setInputApk(in).setOutputApk(outFile); |
| |
| File outFileIdSig = new File(outFile.getCanonicalPath() + ".idsig"); |
| apkSignerBuilder.setV4SignatureOutputFile(outFileIdSig); |
| apkSignerBuilder.setV4ErrorReportingEnabled(true); |
| |
| apkSignerBuilder.build().sign(); |
| } |
| |
| @Test |
| public void testAlignmentPreserved_Golden() throws Exception { |
| // Regression tests for preserving (mis)alignment of ZIP Local File Header data |
| // NOTE: Expected output files can be re-generated by running the "main" method. |
| |
| List<ApkSigner.SignerConfig> rsa2048SignerConfig = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| List<ApkSigner.SignerConfig> rsa2048SignerConfigWithLineage = |
| Arrays.asList( |
| rsa2048SignerConfig.get(0), |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); |
| SigningCertificateLineage lineage = |
| Resources.toSigningCertificateLineage( |
| getClass(), LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME); |
| // Uncompressed entries in this input file are not aligned -- the file was created using |
| // the jar utility. temp4.txt entry was then manually added into the archive. This entry's |
| // ZIP Local File Header "extra" field declares that the entry's data must be aligned to |
| // 4 kB boundary, but the data isn't actually aligned in the file. |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v1-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(false) |
| .setV4SigningEnabled(false)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v1v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v1v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-unaligned-in.apk", |
| "golden-unaligned-v1v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .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 |
| // performed by zipalign at the time of writing. This padding technique produces ZIP |
| // archives whose "extra" field are not compliant with APPNOTE.TXT. Hence, this technique |
| // was deprecated. |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v1-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(false) |
| .setV4SigningEnabled(false)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v1v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v1v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-legacy-aligned-in.apk", |
| "golden-legacy-aligned-v1v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .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 |
| // generated by signapk and apksigner. This padding technique produces "extra" fields which |
| // are compliant with APPNOTE.TXT. |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v1-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(false) |
| .setV4SigningEnabled(false)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v1v2-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v1v2v3-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| assertGolden( |
| "golden-aligned-in.apk", |
| "golden-aligned-v1v2v3-lineage-out.apk", |
| new ApkSigner.Builder(rsa2048SignerConfigWithLineage) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setMinSdkVersionForRotation(AndroidSdkVersion.P) |
| .setSigningCertificateLineage(lineage)); |
| } |
| |
| @Test |
| public void testMinSdkVersion_Golden() throws Exception { |
| // Regression tests for minSdkVersion-based signature/digest algorithm selection |
| // NOTE: Expected output files can be re-generated by running the "main" method. |
| |
| List<ApkSigner.SignerConfig> rsaSignerConfig = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| assertGolden("original.apk", "golden-rsa-out.apk", new ApkSigner.Builder(rsaSignerConfig)); |
| assertGolden( |
| "original.apk", |
| "golden-rsa-minSdkVersion-1-out.apk", |
| new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(1)); |
| assertGolden( |
| "original.apk", |
| "golden-rsa-minSdkVersion-18-out.apk", |
| new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(18)); |
| assertGolden( |
| "original.apk", |
| "golden-rsa-minSdkVersion-24-out.apk", |
| new ApkSigner.Builder(rsaSignerConfig).setMinSdkVersion(24)); |
| |
| // TODO: Add tests for DSA and ECDSA. This is non-trivial because the default |
| // implementations of these signature algorithms are non-deterministic which means output |
| // files always differ from golden files. |
| } |
| |
| @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 testRsaSignedVerifies() throws Exception { |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| 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 18+ |
| out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18)); |
| assertVerified(verifyForMinSdkVersion(out, 18)); |
| // Does not verify on API Level 17 because RSA with SHA-256 not supported |
| assertVerificationFailure( |
| verifyForMinSdkVersion(out, 17), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG); |
| } |
| |
| @Test |
| public void testDsaSignedVerifies() throws Exception { |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList(getDefaultSignerConfigFromResources("dsa-1024")); |
| 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); |
| } |
| |
| |
| @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_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+ |
| 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( |
| verifyForMinSdkVersion(out, 17), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG); |
| } |
| |
| @Test |
| public void testV1SigningRejectsInvalidZipEntryNames() throws Exception { |
| // ZIP/JAR entry name cannot contain CR, LF, or NUL characters when the APK is being |
| // JAR-signed. |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| |
| assertThrows( |
| ApkFormatException.class, |
| () -> |
| sign( |
| "v1-only-with-cr-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true))); |
| assertThrows( |
| ApkFormatException.class, |
| () -> |
| sign( |
| "v1-only-with-lf-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true))); |
| assertThrows( |
| ApkFormatException.class, |
| () -> |
| sign( |
| "v1-only-with-nul-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true))); |
| } |
| |
| @Test |
| public void testWeirdZipCompressionMethod() throws Exception { |
| // Any ZIP compression method other than STORED is treated as DEFLATED by Android. |
| // This APK declares compression method 21 (neither STORED nor DEFLATED) for CERT.RSA entry, |
| // but the entry is actually Deflate-compressed. |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| sign("weird-compression-method.apk", new ApkSigner.Builder(signers)); |
| } |
| |
| @Test |
| public void testZipCompressionMethodMismatchBetweenLfhAndCd() throws Exception { |
| // Android Package Manager ignores compressionMethod field in Local File Header and always |
| // uses the compressionMethod from Central Directory instead. |
| // In this APK, compression method of CERT.RSA is declared as STORED in Local File Header |
| // and as DEFLATED in Central Directory. The entry is actually Deflate-compressed. |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| sign("mismatched-compression-method.apk", new ApkSigner.Builder(signers)); |
| } |
| |
| @Test |
| public void testDebuggableApk() throws Exception { |
| // APK which uses a boolean value "true" in its android:debuggable |
| final String debuggableBooleanApk = "debuggable-boolean.apk"; |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| // Signing debuggable APKs is permitted by default |
| sign(debuggableBooleanApk, new ApkSigner.Builder(signers)); |
| // Signing debuggable APK succeeds when explicitly requested |
| sign(debuggableBooleanApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true)); |
| |
| // Signing debuggable APK fails when requested |
| assertThrows( |
| SignatureException.class, |
| () -> |
| sign( |
| debuggableBooleanApk, |
| new ApkSigner.Builder(signers).setDebuggableApkPermitted(false))); |
| |
| // APK which uses a reference value, pointing to boolean "false", in its android:debuggable |
| final String debuggableResourceApk = "debuggable-resource.apk"; |
| // When we permit signing regardless of whether the APK is debuggable, the value of |
| // android:debuggable should be ignored. |
| sign(debuggableResourceApk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true)); |
| |
| // When we disallow signing debuggable APKs, APKs with android:debuggable being a resource |
| // reference must be rejected, because there's no easy way to establish whether the resolved |
| // boolean value is the same for all resource configurations. |
| assertThrows( |
| SignatureException.class, |
| () -> |
| sign( |
| debuggableResourceApk, |
| new ApkSigner.Builder(signers).setDebuggableApkPermitted(false))); |
| } |
| |
| @Test |
| public void testV3SigningWithSignersNotInLineageFails() throws Exception { |
| // APKs signed with the v3 scheme after a key rotation must specify the lineage containing |
| // the proof of rotation. This test verifies that the signing will fail if the provided |
| // signers are not in the specified lineage. |
| List<ApkSigner.SignerConfig> signers = |
| Arrays.asList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); |
| SigningCertificateLineage lineage = |
| Resources.toSigningCertificateLineage(getClass(), "rsa-1024-lineage-2-signers"); |
| assertThrows( |
| IllegalStateException.class, |
| () -> |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setSigningCertificateLineage(lineage))); |
| } |
| |
| @Test |
| public void testSigningWithLineageRequiresOldestSignerForV1AndV2() throws Exception { |
| // After a key rotation the oldest signer must still be specified for v1 and v2 signing. |
| // The lineage contains the proof of rotation and will be used to determine the oldest |
| // signer. |
| ApkSigner.SignerConfig firstSigner = |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME); |
| ApkSigner.SignerConfig secondSigner = |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); |
| ApkSigner.SignerConfig thirdSigner = |
| getDefaultSignerConfigFromResources(THIRD_RSA_2048_SIGNER_RESOURCE_NAME); |
| SigningCertificateLineage lineage = |
| Resources.toSigningCertificateLineage(getClass(), "rsa-2048-lineage-3-signers"); |
| |
| // Verifies that the v1 signing scheme requires the oldest signer after a key rotation. |
| List<ApkSigner.SignerConfig> signers = Collections.singletonList(thirdSigner); |
| try { |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| fail( |
| "The signing should have failed due to the oldest signer in the lineage not" |
| + " being provided for v1 signing"); |
| } catch (IllegalArgumentException expected) { |
| } |
| |
| // Verifies that the v2 signing scheme requires the oldest signer after a key rotation. |
| try { |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| fail( |
| "The signing should have failed due to the oldest signer in the lineage not" |
| + " being provided for v2 signing"); |
| } catch (IllegalArgumentException expected) { |
| } |
| |
| // Verifies that when only the v3 signing scheme is requested the oldest signer does not |
| // need to be provided. |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .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 |
| // requirement that the oldest signer be provided for v1 and v2 signing. |
| signers = Arrays.asList(secondSigner, thirdSigner); |
| try { |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| fail( |
| "The signing should have failed due to the oldest signer in the lineage not" |
| + " being provided for v1/v2 signing"); |
| } catch (IllegalArgumentException expected) { |
| } |
| |
| // Verifies that the signing is successful when the oldest and newest signers are provided |
| // and that intermediate signers are not required. |
| signers = Arrays.asList(firstSigner, thirdSigner); |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| } |
| |
| @Test |
| public void testV3SigningWithMultipleSignersAndNoLineageFails() throws Exception { |
| // The v3 signing scheme does not support multiple signers; if multiple signers are provided |
| // it is assumed these signers are part of the lineage. This test verifies v3 signing |
| // fails if multiple signers are provided without a lineage. |
| ApkSigner.SignerConfig firstSigner = |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME); |
| ApkSigner.SignerConfig secondSigner = |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME); |
| List<ApkSigner.SignerConfig> signers = Arrays.asList(firstSigner, secondSigner); |
| assertThrows( |
| IllegalStateException.class, |
| () -> |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true))); |
| } |
| |
| @Test |
| public void testLineageCanBeReadAfterV3Signing() throws Exception { |
| SigningCertificateLineage.SignerConfig firstSigner = |
| Resources.toLineageSignerConfig(getClass(), FIRST_RSA_2048_SIGNER_RESOURCE_NAME); |
| SigningCertificateLineage.SignerConfig secondSigner = |
| Resources.toLineageSignerConfig(getClass(), SECOND_RSA_2048_SIGNER_RESOURCE_NAME); |
| SigningCertificateLineage lineage = |
| new SigningCertificateLineage.Builder(firstSigner, secondSigner).build(); |
| List<ApkSigner.SignerConfig> signerConfigs = |
| Arrays.asList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME), |
| getDefaultSignerConfigFromResources(SECOND_RSA_2048_SIGNER_RESOURCE_NAME)); |
| File out = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signerConfigs) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkFile(out); |
| assertTrue( |
| "The first signer was not in the lineage from the signed APK", |
| lineageFromApk.isSignerInLineage((firstSigner))); |
| assertTrue( |
| "The second signer was not in the lineage from the signed APK", |
| lineageFromApk.isSignerInLineage((secondSigner))); |
| } |
| |
| @Test |
| public void testPublicKeyHasPositiveModulusAfterSigning() throws Exception { |
| // The V2 and V3 signature schemes include the public key from the certificate in the |
| // signing block. If a certificate with an RSAPublicKey is improperly encoded with a |
| // negative modulus this was previously written to the signing block as is and failed on |
| // device verification since on device the public key in the certificate was reencoded with |
| // the correct encoding for the modulus. This test uses an improperly encoded certificate to |
| // sign an APK and verifies that the public key in the signing block is corrected with a |
| // positive modulus to allow on device installs / updates. |
| List<ApkSigner.SignerConfig> signersList = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources( |
| FIRST_RSA_2048_SIGNER_RESOURCE_NAME, |
| FIRST_RSA_2048_SIGNER_CERT_WITH_NEGATIVE_MODULUS)); |
| File signedApk = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signersList) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| RSAPublicKey v2PublicKey = |
| getRSAPublicKeyFromSigningBlock( |
| signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V2); |
| assertTrue( |
| "The modulus in the public key in the V2 signing block must not be negative", |
| v2PublicKey.modulus.compareTo(BigInteger.ZERO) > 0); |
| RSAPublicKey v3PublicKey = |
| getRSAPublicKeyFromSigningBlock( |
| signedApk, ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3); |
| assertTrue( |
| "The modulus in the public key in the V3 signing block must not be negative", |
| v3PublicKey.modulus.compareTo(BigInteger.ZERO) > 0); |
| } |
| |
| @Test |
| public void testV4State_disableV2V3EnableV4_fails() throws Exception { |
| ApkSigner.SignerConfig signer = |
| 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))); |
| } |
| |
| @Test |
| public void testSignApk_stampFile() 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); |
| MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); |
| messageDigest.update(sourceStampSigner.getCertificates().get(0).getEncoded()); |
| byte[] expectedStampCertificateDigest = messageDigest.digest(); |
| |
| File signedApkFile = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signers) |
| .setV1SigningEnabled(true) |
| .setSourceStampSignerConfig(sourceStampSigner)); |
| |
| 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); |
| } |
| } |
| |
| @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 |
| public void testSignApk_stampBlock_noStampGenerated() throws Exception { |
| List<ApkSigner.SignerConfig> signersList = |
| Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| |
| File signedApkFile = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signersList) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(true)); |
| |
| 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_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); |
| |
| File signedApk = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signersList) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(false) |
| .setV4SigningEnabled(false) |
| .setSourceStampSignerConfig(sourceStampSigner)); |
| |
| ApkVerifier.Result sourceStampVerificationResult = |
| verify(signedApk, /* minSdkVersionOverride= */ null); |
| assertSourceStampVerified(signedApk, sourceStampVerificationResult); |
| } |
| |
| @Test |
| public void testSignApk_stampBlock_whenV2SignaturePresent() 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); |
| |
| File signedApk = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signersList) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(true) |
| .setV3SigningEnabled(false) |
| .setSourceStampSignerConfig(sourceStampSigner)); |
| |
| ApkVerifier.Result sourceStampVerificationResult = |
| verifyForMinSdkVersion(signedApk, /* minSdkVersion= */ AndroidSdkVersion.N); |
| assertSourceStampVerified(signedApk, sourceStampVerificationResult); |
| } |
| |
| @Test |
| public void testSignApk_stampBlock_whenV3SignaturePresent() 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); |
| |
| File signedApk = |
| sign( |
| "original.apk", |
| new ApkSigner.Builder(signersList) |
| .setV1SigningEnabled(false) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(true) |
| .setSourceStampSignerConfig(sourceStampSigner)); |
| |
| 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_signWithSourceStamp_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. |
| 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); |
| } |
| |
| /** |
| * 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)); |
| } |
| } |
| |
| /** |
| * 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 = V2SchemeConstants.APK_SIGNATURE_SCHEME_V2_BLOCK_ID; |
| break; |
| case ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3: |
| signatureVersionBlockId = V3SchemeConstants.APK_SIGNATURE_SCHEME_V3_BLOCK_ID; |
| break; |
| default: |
| throw new Exception( |
| "Invalid signature version ID specified: " + signatureVersionId); |
| } |
| SignatureInfo signatureInfo = |
| getSignatureInfoFromApk(apk, signatureVersionId, signatureVersionBlockId); |
| // FORMAT: |
| // * length prefixed sequence of length prefixed signers |
| // * length-prefixed signed data |
| // * V3+ only - minSDK (uint32) |
| // * V3+ only - maxSDK (uint32) |
| // * length-prefixed sequence of length-prefixed signatures: |
| // * length-prefixed bytes: public key (X.509 SubjectPublicKeyInfo, ASN.1 DER encoded) |
| ByteBuffer signers = |
| ApkSigningBlockUtils.getLengthPrefixedSlice(signatureInfo.signatureBlock); |
| ByteBuffer signer = ApkSigningBlockUtils.getLengthPrefixedSlice(signers); |
| // Since all the data is read from the signer block the signedData and signatures are |
| // discarded. |
| ApkSigningBlockUtils.getLengthPrefixedSlice(signer); |
| // For V3+ signature version IDs discard the min / max SDKs as well |
| if (signatureVersionId >= ApkSigningBlockUtils.VERSION_APK_SIGNATURE_SCHEME_V3) { |
| signer.getInt(); |
| signer.getInt(); |
| } |
| ApkSigningBlockUtils.getLengthPrefixedSlice(signer); |
| ByteBuffer publicKey = ApkSigningBlockUtils.getLengthPrefixedSlice(signer); |
| SubjectPublicKeyInfo subjectPublicKeyInfo = |
| Asn1BerParser.parse(publicKey, SubjectPublicKeyInfo.class); |
| ByteBuffer subjectPublicKeyBuffer = subjectPublicKeyInfo.subjectPublicKey; |
| // The SubjectPublicKey is stored as a bit string in the SubjectPublicKeyInfo with the first |
| // byte indicating the number of padding bits in the public key. Read this first byte to |
| // allow parsing the rest of the RSAPublicKey as a sequence. |
| subjectPublicKeyBuffer.get(); |
| return Asn1BerParser.parse(subjectPublicKeyBuffer, RSAPublicKey.class); |
| } |
| |
| private static SignatureInfo getSignatureInfoFromApk( |
| File apkFile, int signatureVersionId, int signatureVersionBlockId) |
| throws IOException, ZipFormatException, |
| 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); |
| } |
| } |
| |
| /** |
| * Asserts that signing the specified golden input file using the provided signing configuration |
| * produces output identical to the specified golden output file. |
| */ |
| private void assertGolden( |
| String inResourceName, |
| String expectedOutResourceName, |
| ApkSigner.Builder apkSignerBuilder) |
| throws Exception { |
| // Sign the provided golden input |
| File out = sign(inResourceName, apkSignerBuilder); |
| assertVerified(verify(out, AndroidSdkVersion.P)); |
| |
| // Assert that the output is identical to the provided golden output |
| if (out.length() > Integer.MAX_VALUE) { |
| throw new RuntimeException("Output too large: " + out.length() + " bytes"); |
| } |
| byte[] outData = new byte[(int) out.length()]; |
| try (FileInputStream fis = new FileInputStream(out)) { |
| fis.read(outData); |
| } |
| ByteBuffer actualOutBuf = ByteBuffer.wrap(outData); |
| |
| ByteBuffer expectedOutBuf = |
| ByteBuffer.wrap(Resources.toByteArray(getClass(), expectedOutResourceName)); |
| |
| boolean identical = false; |
| if (actualOutBuf.remaining() == expectedOutBuf.remaining()) { |
| while (actualOutBuf.hasRemaining()) { |
| if (actualOutBuf.get() != expectedOutBuf.get()) { |
| break; |
| } |
| } |
| identical = !actualOutBuf.hasRemaining(); |
| } |
| |
| if (identical) { |
| return; |
| } |
| |
| if (KEEP_FAILING_OUTPUT_AS_FILES) { |
| File tmp = File.createTempFile(getClass().getSimpleName(), ".apk"); |
| Files.copy(out.toPath(), tmp.toPath()); |
| fail(tmp + " differs from " + expectedOutResourceName); |
| } else { |
| fail("Output differs from " + expectedOutResourceName); |
| } |
| } |
| |
| 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))); |
| return sign(in, apkSignerBuilder); |
| } |
| |
| 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(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(); |
| } |
| |
| private static void assertVerified(ApkVerifier.Result result) { |
| 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, |
| deterministicDsaSigning).build(); |
| } |
| |
| private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources( |
| String keyNameInResources, String certNameInResources) throws Exception { |
| PrivateKey privateKey = |
| Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8"); |
| List<X509Certificate> certs = |
| 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); |
| } |
| } |