aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/com/android/apksig/ApkSignerTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/com/android/apksig/ApkSignerTest.java')
-rw-r--r--src/test/java/com/android/apksig/ApkSignerTest.java390
1 files changed, 386 insertions, 4 deletions
diff --git a/src/test/java/com/android/apksig/ApkSignerTest.java b/src/test/java/com/android/apksig/ApkSignerTest.java
index 40255a4..d799201 100644
--- a/src/test/java/com/android/apksig/ApkSignerTest.java
+++ b/src/test/java/com/android/apksig/ApkSignerTest.java
@@ -37,6 +37,7 @@ 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;
@@ -46,6 +47,9 @@ 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;
@@ -59,14 +63,20 @@ 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 {
@@ -83,6 +93,8 @@ public class ApkSignerTest {
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 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";
@@ -90,6 +102,11 @@ public class ApkSignerTest {
private static final String LINEAGE_RSA_2048_2_SIGNERS_RESOURCE_NAME =
"rsa-2048-lineage-2-signers";
+ // These are the ID and value of an extra signature block within the APK signing block that
+ // can be preserved through the setOtherSignersSignaturesPreserved API.
+ private final int EXTRA_BLOCK_ID = 0x7e57c0de;
+ private final byte[] EXTRA_BLOCK_VALUE = {0, 1, 2, 3, 4, 5, 6, 7};
+
@Rule
public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
@@ -365,6 +382,15 @@ public class ApkSignerTest {
.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(
@@ -705,10 +731,53 @@ public class ApkSignerTest {
verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
}
+
+ @Test
+ public void testDeterministicDsaSignedVerifies() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ // Sign so that the APK is guaranteed to verify on API Level 1+
+ File out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(1));
+ assertVerified(verifyForMinSdkVersion(out, 1));
+
+ // Sign so that the APK is guaranteed to verify on API Level 21+
+ out = sign(in, new ApkSigner.Builder(signers).setMinSdkVersion(21));
+ assertVerified(verifyForMinSdkVersion(out, 21));
+ // Does not verify on API Level 20 because DSA with SHA-256 not supported
+ assertVerificationFailure(
+ verifyForMinSdkVersion(out, 20), Issue.JAR_SIG_UNSUPPORTED_SIG_ALG);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
+ @Test
+ public void testDeterministicDsaSigningIsDeterministic() throws Exception {
+ Security.addProvider(new BouncyCastleProvider());
+ try {
+ List<ApkSigner.SignerConfig> signers =
+ Collections.singletonList(getDeterministicDsaSignerConfigFromResources("dsa-2048"));
+ String in = "original.apk";
+
+ ApkSigner.Builder apkSignerBuilder = new ApkSigner.Builder(signers).setMinSdkVersion(1);
+ File first = sign(in, apkSignerBuilder);
+ File second = sign(in, apkSignerBuilder);
+
+ assertFileContentsEqual(first, second);
+ } finally {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ }
+ }
+
@Test
public void testEcSignedVerifies() throws Exception {
List<ApkSigner.SignerConfig> signers =
- Collections.singletonList(getDefaultSignerConfigFromResources("ec-p256"));
+ Collections.singletonList(
+ getDefaultSignerConfigFromResources(EC_P256_SIGNER_RESOURCE_NAME));
String in = "original.apk";
// NOTE: EC APK signatures are not supported prior to API Level 18
@@ -1249,6 +1318,293 @@ public class ApkSignerTest {
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);
+ }
+
+ /**
+ * 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.
+ */
+ private static void assertResultContainsSigners(ApkVerifier.Result result, 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 (result.isVerifiedUsingV1Scheme()) {
+ Set<X509Certificate> v1Signers = new HashSet<>();
+ for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
+ v1Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v1Signers.size());
+ assertTrue("Expected V1 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V1 signers: " + getAllSubjectNamesFrom(v1Signers),
+ v1Signers.containsAll(expectedSigners));
+ }
+
+ if (result.isVerifiedUsingV2Scheme()) {
+ Set<X509Certificate> v2Signers = new HashSet<>();
+ for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
+ v2Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v2Signers.size());
+ assertTrue("Expected V2 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V2 signers: " + getAllSubjectNamesFrom(v2Signers),
+ v2Signers.containsAll(expectedSigners));
+ }
+
+ if (result.isVerifiedUsingV3Scheme()) {
+ Set<X509Certificate> v3Signers = new HashSet<>();
+ for (ApkVerifier.Result.V3SchemeSignerInfo signer : result.getV3SchemeSigners()) {
+ v3Signers.add(signer.getCertificate());
+ }
+ assertEquals(expectedSigners.size(), v3Signers.size());
+ assertTrue("Expected V3 signers: " + getAllSubjectNamesFrom(expectedSigners)
+ + ", actual V3 signers: " + getAllSubjectNamesFrom(v3Signers),
+ v3Signers.containsAll(expectedSigners));
+ }
+ }
+
+ /**
+ * 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;
@@ -1358,11 +1714,21 @@ public class ApkSignerTest {
}
}
- private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder)
- throws Exception {
+ private File sign(File inApkFile, ApkSigner.Builder apkSignerBuilder) throws Exception {
+ try (RandomAccessFile apkFile = new RandomAccessFile(inApkFile, "r")) {
+ DataSource in = DataSources.asDataSource(apkFile);
+ return sign(in, apkSignerBuilder);
+ }
+ }
+
+ private File sign(String inResourceName, ApkSigner.Builder apkSignerBuilder) throws Exception {
DataSource in =
DataSources.asDataSource(
ByteBuffer.wrap(Resources.toByteArray(getClass(), inResourceName)));
+ return sign(in, apkSignerBuilder);
+ }
+
+ private File sign(DataSource in, ApkSigner.Builder apkSignerBuilder) throws Exception {
File outFile = mTemporaryFolder.newFile();
apkSignerBuilder.setInputApk(in).setOutputApk(outFile);
@@ -1412,13 +1778,24 @@ public class ApkSignerTest {
ApkVerifierTest.assertVerificationFailure(result, expectedIssue);
}
+ private void assertFileContentsEqual(File first, File second) throws IOException {
+ assertArrayEquals(Files.readAllBytes(Paths.get(first.getPath())),
+ Files.readAllBytes(Paths.get(second.getPath())));
+ }
+
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, false);
+ }
+
+ private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
+ String keyNameInResources, boolean deterministicDsaSigning) throws Exception {
PrivateKey privateKey =
Resources.toPrivateKey(ApkSignerTest.class, keyNameInResources + ".pk8");
List<X509Certificate> certs =
Resources.toCertificateChain(ApkSignerTest.class, keyNameInResources + ".x509.pem");
- return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
+ return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs,
+ deterministicDsaSigning).build();
}
private static ApkSigner.SignerConfig getDefaultSignerConfigFromResources(
@@ -1429,4 +1806,9 @@ public class ApkSignerTest {
Resources.toCertificateChain(ApkSignerTest.class, certNameInResources);
return new ApkSigner.SignerConfig.Builder(keyNameInResources, privateKey, certs).build();
}
+
+ private static ApkSigner.SignerConfig getDeterministicDsaSignerConfigFromResources(
+ String keyNameInResources) throws Exception {
+ return getDefaultSignerConfigFromResources(keyNameInResources, true);
+ }
}