| /* |
| * 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 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.internal.util.ByteBufferDataSource; |
| import com.android.apksig.internal.util.Resources; |
| import com.android.apksig.util.DataSinks; |
| import com.android.apksig.util.DataSource; |
| import com.android.apksig.util.DataSources; |
| import com.android.apksig.util.ReadableDataSink; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.channels.ByteChannel; |
| import java.nio.file.Files; |
| import java.nio.file.StandardOpenOption; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PrivateKey; |
| import java.security.SignatureException; |
| import java.security.cert.X509Certificate; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| @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. |
| private static final String FIRST_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048"; |
| private static final String SECOND_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_2"; |
| private static final String THIRD_RSA_2048_SIGNER_RESOURCE_NAME = "rsa-2048_3"; |
| |
| private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME = |
| "rsa-2048-lineage-2-signers"; |
| |
| 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.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)); |
| 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)); |
| signGolden( |
| "golden-aligned-in.apk", |
| new File(outDir, "golden-aligned-v1-out.apk"), |
| new ApkSigner.Builder(rsa2048SignerConfig) |
| .setV1SigningEnabled(true) |
| .setV2SigningEnabled(false) |
| .setV3SigningEnabled(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) |
| .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) |
| .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) |
| .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) |
| .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) |
| .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) |
| .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) |
| .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) |
| .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) |
| .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)); |
| } |
| |
| 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) |
| .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)); |
| 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) |
| .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) |
| .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) |
| .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)); |
| 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) |
| .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) |
| .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) |
| .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)); |
| 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) |
| .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) |
| .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) |
| .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 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+ |
| DataSource 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+ |
| DataSource 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 testEcSignedVerifies() throws Exception { |
| List<ApkSigner.SignerConfig> signers = |
| Collections.singletonList(getDefaultSignerConfigFromResources("ec-p256")); |
| String in = "original.apk"; |
| |
| // NOTE: EC APK signatures are not supported prior to API Level 18 |
| // Sign so that the APK is guaranteed to verify on API Level 18+ |
| DataSource out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(18)); |
| 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)); |
| try { |
| sign("v1-only-with-cr-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true)); |
| fail(); |
| } catch (ApkFormatException expected) {} |
| |
| try { |
| sign("v1-only-with-lf-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true)); |
| fail(); |
| } catch (ApkFormatException expected) {} |
| |
| try { |
| sign("v1-only-with-nul-in-entry-name.apk", |
| new ApkSigner.Builder(signers).setV1SigningEnabled(true)); |
| fail(); |
| } catch (ApkFormatException expected) {} |
| } |
| |
| @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 |
| String apk = "debuggable-boolean.apk"; |
| List<ApkSigner.SignerConfig> signers = Collections.singletonList( |
| getDefaultSignerConfigFromResources(FIRST_RSA_2048_SIGNER_RESOURCE_NAME)); |
| // Signing debuggable APKs is permitted by default |
| sign(apk, new ApkSigner.Builder(signers)); |
| // Signing debuggable APK succeeds when explicitly requested |
| sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(true)); |
| // Signing debuggable APK fails when requested |
| try { |
| sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)); |
| fail(); |
| } catch (SignatureException expected) {} |
| |
| // APK which uses a reference value, pointing to boolean "false", in its android:debuggable |
| apk = "debuggable-resource.apk"; |
| // When we permit signing regardless of whether the APK is debuggable, the value of |
| // android:debuggable should be ignored. |
| sign(apk, 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. |
| try { |
| sign(apk, new ApkSigner.Builder(signers).setDebuggableApkPermitted(false)); |
| fail(); |
| } catch (SignatureException expected) {} |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| 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"); |
| 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) |
| .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(expected = IllegalStateException.class) |
| 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); |
| 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)); |
| DataSource out = sign("original.apk", new ApkSigner.Builder(signerConfigs) |
| .setV3SigningEnabled(true) |
| .setSigningCertificateLineage(lineage)); |
| SigningCertificateLineage lineageFromApk = SigningCertificateLineage.readFromApkDataSource( |
| 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))); |
| } |
| |
| /** |
| * 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 |
| DataSource out = sign(inResourceName, apkSignerBuilder); |
| |
| // Assert that the output is identical to the provided golden output |
| if (out.size() > Integer.MAX_VALUE) { |
| throw new RuntimeException("Output too large: " + out.size() + " bytes"); |
| } |
| ByteBuffer actualOutBuf = out.getByteBuffer(0, (int) out.size()); |
| |
| ByteBuffer expectedOutBuf = |
| ByteBuffer.wrap(Resources.toByteArray(getClass(), expectedOutResourceName)); |
| |
| int actualStartPos = actualOutBuf.position(); |
| boolean identical = false; |
| if (actualOutBuf.remaining() == expectedOutBuf.remaining()) { |
| while (actualOutBuf.hasRemaining()) { |
| if (actualOutBuf.get() != expectedOutBuf.get()) { |
| break; |
| } |
| } |
| identical = !actualOutBuf.hasRemaining(); |
| } |
| |
| if (identical) { |
| return; |
| } |
| actualOutBuf.position(actualStartPos); |
| |
| if (KEEP_FAILING_OUTPUT_AS_FILES) { |
| File tmp = File.createTempFile(getClass().getSimpleName(), ".apk"); |
| try (ByteChannel outChannel = |
| Files.newByteChannel( |
| tmp.toPath(), |
| StandardOpenOption.WRITE, |
| StandardOpenOption.CREATE, |
| StandardOpenOption.TRUNCATE_EXISTING)) { |
| while (actualOutBuf.hasRemaining()) { |
| outChannel.write(actualOutBuf); |
| } |
| } |
| fail(tmp + " differs from " + expectedOutResourceName); |
| } else { |
| fail("Output differs from " + expectedOutResourceName); |
| } |
| } |
| |
| private DataSource sign( |
| String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception { |
| DataSource in = |
| DataSources.asDataSource( |
| ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName))); |
| ReadableDataSink out = DataSinks.newInMemoryDataSink(); |
| apkSignerBuilder |
| .setInputApk(in) |
| .setOutputApk(out) |
| .build() |
| .sign(); |
| return out; |
| } |
| |
| private static ApkVerifier.Result verifyForMinSdkVersion(DataSource apk, int minSdkVersion) |
| throws IOException, ApkFormatException, NoSuchAlgorithmException { |
| return verify(apk, minSdkVersion); |
| } |
| |
| private static ApkVerifier.Result verify(DataSource apk, Integer minSdkVersionOverride) |
| throws IOException, ApkFormatException, NoSuchAlgorithmException { |
| ApkVerifier.Builder builder = new ApkVerifier.Builder(apk); |
| if (minSdkVersionOverride != null) { |
| builder.setMinCheckedPlatformVersion(minSdkVersionOverride); |
| } |
| return builder.build().verify(); |
| } |
| |
| private static void assertVerified(ApkVerifier.Result result) { |
| ApkVerifierTest.assertVerified(result); |
| } |
| |
| private static void assertVerificationFailure(ApkVerifier.Result result, Issue expectedIssue) { |
| ApkVerifierTest.assertVerificationFailure(result, expectedIssue); |
| } |
| |
| private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources( |
| String keyNameInResources) throws Exception { |
| PrivateKey privateKey = |
| Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8"); |
| List<X509Certificate> certs = |
| Resources.toCertificateChain(ApkSignerTest.class, keyNameInResources + ".x509.pem"); |
| return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build(); |
| } |
| } |