diff options
Diffstat (limited to 'src/test/java/com/android/apksig/ApkSignerTest.java')
-rw-r--r-- | src/test/java/com/android/apksig/ApkSignerTest.java | 390 |
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); + } } |