blob: 513023e2f1691fa992429de90f9225079afd7932 [file] [log] [blame]
/*
* 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.fail;
import com.android.apksig.ApkVerifier.Issue;
import com.android.apksig.apk.ApkFormatException;
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.cert.X509Certificate;
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;
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("rsa-2048"));
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));
signGolden(
"golden-legacy-aligned-in.apk",
new File(outDir, "golden-legacy-aligned-v1-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false));
signGolden(
"golden-aligned-in.apk",
new File(outDir, "golden-aligned-v1-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(false));
signGolden(
"golden-unaligned-in.apk",
new File(outDir, "golden-unaligned-v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
signGolden(
"golden-legacy-aligned-in.apk",
new File(outDir, "golden-legacy-aligned-v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
signGolden(
"golden-aligned-in.apk",
new File(outDir, "golden-aligned-v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
signGolden(
"golden-unaligned-in.apk",
new File(outDir, "golden-unaligned-v1v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
signGolden(
"golden-legacy-aligned-in.apk",
new File(outDir, "golden-legacy-aligned-v1v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
signGolden(
"golden-aligned-in.apk",
new File(outDir, "golden-aligned-v1v2-out.apk"),
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
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("rsa-2048"));
// 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));
assertGolden(
"golden-unaligned-in.apk", "golden-unaligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
assertGolden(
"golden-unaligned-in.apk", "golden-unaligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
// 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));
assertGolden(
"golden-legacy-aligned-in.apk", "golden-legacy-aligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
assertGolden(
"golden-legacy-aligned-in.apk", "golden-legacy-aligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
// 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));
assertGolden(
"golden-aligned-in.apk", "golden-aligned-v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(false)
.setV2SigningEnabled(true));
assertGolden(
"golden-aligned-in.apk", "golden-aligned-v1v2-out.apk",
new ApkSigner.Builder(rsa2048SignerConfig)
.setV1SigningEnabled(true)
.setV2SigningEnabled(true));
}
@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("rsa-2048"));
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("rsa-2048"));
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("rsa-2048"));
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("rsa-2048"));
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("rsa-2048"));
sign("mismatched-compression-method.apk", new ApkSigner.Builder(signers));
}
/**
* 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();
}
}