diff --git a/core/java/android/util/apk/ApkSigningBlockUtils.java b/core/java/android/util/apk/ApkSigningBlockUtils.java index 2a4b65d23e647..6efe95cb9e92d 100644 --- a/core/java/android/util/apk/ApkSigningBlockUtils.java +++ b/core/java/android/util/apk/ApkSigningBlockUtils.java @@ -420,6 +420,7 @@ final class ApkSigningBlockUtils { static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3; + static final int CONTENT_DIGEST_SHA256 = 4; private static final int[] V4_CONTENT_DIGEST_ALGORITHMS = {CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256, diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java index a7ae32d1baa2d..5fc242353d519 100644 --- a/core/java/android/util/apk/SourceStampVerifier.java +++ b/core/java/android/util/apk/SourceStampVerifier.java @@ -16,6 +16,7 @@ package android.util.apk; +import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_SHA256; import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm; import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice; import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm; @@ -27,12 +28,10 @@ import android.util.Pair; import android.util.Slog; import android.util.jar.StrictJarFile; -import libcore.io.IoUtils; +import libcore.io.Streams; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.RandomAccessFile; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; @@ -49,11 +48,13 @@ import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; +import java.util.jar.JarFile; import java.util.zip.ZipEntry; /** @@ -74,7 +75,11 @@ public abstract class SourceStampVerifier { private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a; private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0; - private static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e; + private static final int SOURCE_STAMP_BLOCK_ID = 0x6dff800d; + + private static final int VERSION_JAR_SIGNATURE_SCHEME = 1; + private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; + private static final int VERSION_APK_SIGNATURE_SCHEME_V3 = 3; /** Name of the SourceStamp certificate hash ZIP entry in APKs. */ private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; @@ -115,7 +120,8 @@ public abstract class SourceStampVerifier { // SourceStamp present. return SourceStampVerificationResult.notPresent(); } - return verify(apk, sourceStampCertificateDigest); + byte[] manifestBytes = getManifestBytes(apkJar); + return verify(apk, sourceStampCertificateDigest, manifestBytes); } catch (IOException e) { // Any exception in reading the APK returns a non-present SourceStamp outcome // without affecting the outcome of any of the other signature schemes. @@ -126,22 +132,71 @@ public abstract class SourceStampVerifier { } private static SourceStampVerificationResult verify( - RandomAccessFile apk, byte[] sourceStampCertificateDigest) { + RandomAccessFile apk, byte[] sourceStampCertificateDigest, byte[] manifestBytes) { try { SignatureInfo signatureInfo = ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID); - Map apkContentDigests = getApkContentDigests(apk); - return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest); - } catch (IOException | SignatureNotFoundException e) { + Map> signatureSchemeApkContentDigests = + getSignatureSchemeApkContentDigests(apk, manifestBytes); + return verify( + signatureInfo, + getSignatureSchemeDigests(signatureSchemeApkContentDigests), + sourceStampCertificateDigest); + } catch (IOException | SignatureNotFoundException | RuntimeException e) { return SourceStampVerificationResult.notVerified(); } } private static SourceStampVerificationResult verify( SignatureInfo signatureInfo, - Map apkContentDigests, + Map signatureSchemeDigests, byte[] sourceStampCertificateDigest) throws SecurityException, IOException { + ByteBuffer sourceStampBlock = signatureInfo.signatureBlock; + ByteBuffer sourceStampBlockData = + ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); + + X509Certificate sourceStampCertificate = + verifySourceStampCertificate(sourceStampBlockData, sourceStampCertificateDigest); + + // Parse signed signature schemes block. + ByteBuffer signedSignatureSchemes = + ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); + Map signedSignatureSchemeData = new HashMap<>(); + while (signedSignatureSchemes.hasRemaining()) { + ByteBuffer signedSignatureScheme = + ApkSigningBlockUtils.getLengthPrefixedSlice(signedSignatureSchemes); + int signatureSchemeId = signedSignatureScheme.getInt(); + signedSignatureSchemeData.put(signatureSchemeId, signedSignatureScheme); + } + + for (Map.Entry signatureSchemeDigest : signatureSchemeDigests.entrySet()) { + if (!signedSignatureSchemeData.containsKey(signatureSchemeDigest.getKey())) { + throw new SecurityException( + String.format( + "No signatures found for signature scheme %d", + signatureSchemeDigest.getKey())); + } + verifySourceStampSignature( + signedSignatureSchemeData.get(signatureSchemeDigest.getKey()), + sourceStampCertificate, + signatureSchemeDigest.getValue()); + } + + return SourceStampVerificationResult.verified(sourceStampCertificate); + } + + /** + * Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp + * certificate found in the APK. It returns the verified certificate. + * + * @param sourceStampBlockData the source stamp block in the APK signing block which contains + * the certificate used to sign the stamp digests. + * @param sourceStampCertificateDigest the source stamp certificate digest found in the APK. + */ + private static X509Certificate verifySourceStampCertificate( + ByteBuffer sourceStampBlockData, byte[] sourceStampCertificateDigest) + throws IOException { CertificateFactory certFactory; try { certFactory = CertificateFactory.getInstance("X.509"); @@ -149,17 +204,6 @@ public abstract class SourceStampVerifier { throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); } - List> digests = - apkContentDigests.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .map(e -> Pair.create(e.getKey(), e.getValue())) - .collect(Collectors.toList()); - byte[] digestBytes = encodeApkContentDigests(digests); - - ByteBuffer sourceStampBlock = signatureInfo.signatureBlock; - ByteBuffer sourceStampBlockData = - ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock); - // Parse the SourceStamp certificate. byte[] sourceStampEncodedCertificate = ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData); @@ -172,24 +216,30 @@ public abstract class SourceStampVerifier { } catch (CertificateException e) { throw new SecurityException("Failed to decode certificate", e); } - sourceStampCertificate = - new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate); - // Verify the SourceStamp certificate found in the signing block is the same as the - // SourceStamp certificate found in the APK. - try { - MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); - messageDigest.update(sourceStampEncodedCertificate); - byte[] sourceStampBlockCertificateDigest = messageDigest.digest(); - if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { - throw new SecurityException("Certificate mismatch between APK and signature block"); - } - } catch (NoSuchAlgorithmException e) { - throw new SecurityException("Failed to find SHA-256", e); + byte[] sourceStampBlockCertificateDigest = + computeSha256Digest(sourceStampEncodedCertificate); + if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) { + throw new SecurityException("Certificate mismatch between APK and signature block"); } + return new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate); + } + + /** + * Verify the SourceStamp signature found in the signing block is signed by the SourceStamp + * certificate found in the APK. + * + * @param signedBlockData the source stamp block in the APK signing block which contains the + * stamp signed digests. + * @param sourceStampCertificate the source stamp certificate used to sign the stamp digests. + * @param digest the digest to be verified being signed by the source stamp certificate. + */ + private static void verifySourceStampSignature( + ByteBuffer signedBlockData, X509Certificate sourceStampCertificate, byte[] digest) + throws IOException { // Parse the signatures block and identify supported signatures - ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedBlockData); int signatureCount = 0; int bestSigAlgorithm = -1; byte[] bestSigAlgorithmSignatureBytes = null; @@ -235,7 +285,7 @@ public abstract class SourceStampVerifier { if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } - sig.update(digestBytes); + sig.update(digest); sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); } catch (InvalidKeyException | InvalidAlgorithmParameterException @@ -247,27 +297,44 @@ public abstract class SourceStampVerifier { if (!sigVerified) { throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify"); } - - return SourceStampVerificationResult.verified(sourceStampCertificate); } - private static Map getApkContentDigests(RandomAccessFile apk) - throws IOException, SignatureNotFoundException { - // Retrieve APK content digests in V3 signing block. If a V3 signature is not found, the APK - // content digests would be re-tried from V2 signature. + private static Map> getSignatureSchemeApkContentDigests( + RandomAccessFile apk, byte[] manifestBytes) throws IOException { + Map> signatureSchemeApkContentDigests = new HashMap<>(); + + // Retrieve APK content digests in V3 signing block. try { SignatureInfo v3SignatureInfo = ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID); - return getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V3, + getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock)); } catch (SignatureNotFoundException e) { // It's fine not to find a V3 signature. } - // Retrieve APK content digests in V2 signing block. If a V2 signature is not found, the - // process of retrieving APK content digests stops, and the stamp is considered un-verified. - SignatureInfo v2SignatureInfo = - ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); - return getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock); + // Retrieve APK content digests in V2 signing block. + try { + SignatureInfo v2SignatureInfo = + ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID); + signatureSchemeApkContentDigests.put( + VERSION_APK_SIGNATURE_SCHEME_V2, + getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock)); + } catch (SignatureNotFoundException e) { + // It's fine not to find a V2 signature. + } + + // Retrieve manifest digest. + if (manifestBytes != null) { + Map jarSignatureSchemeApkContentDigests = new HashMap<>(); + jarSignatureSchemeApkContentDigests.put( + CONTENT_DIGEST_SHA256, computeSha256Digest(manifestBytes)); + signatureSchemeApkContentDigests.put( + VERSION_JAR_SIGNATURE_SCHEME, jarSignatureSchemeApkContentDigests); + } + + return signatureSchemeApkContentDigests; } private static Map getApkContentDigestsFromSignatureBlock( @@ -289,27 +356,45 @@ public abstract class SourceStampVerifier { return apkContentDigests; } - private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException { - InputStream inputStream = null; - try { - ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME); - if (zipEntry == null) { - // SourceStamp certificate hash file not found, which means that there is not - // SourceStamp present. - return null; - } - inputStream = apkJar.getInputStream(zipEntry); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - // Trying to read the certificate digest, which should be less than 1024 bytes. - byte[] buffer = new byte[1024]; - int count = inputStream.read(buffer, 0, buffer.length); - byteArrayOutputStream.write(buffer, 0, count); - - return byteArrayOutputStream.toByteArray(); - } finally { - IoUtils.closeQuietly(inputStream); + private static Map getSignatureSchemeDigests( + Map> signatureSchemeApkContentDigests) { + Map digests = new HashMap<>(); + for (Map.Entry> signatureSchemeApkContentDigest : + signatureSchemeApkContentDigests.entrySet()) { + List> apkDigests = + getApkDigests(signatureSchemeApkContentDigest.getValue()); + digests.put( + signatureSchemeApkContentDigest.getKey(), encodeApkContentDigests(apkDigests)); } + return digests; + } + + private static List> getApkDigests( + Map apkContentDigests) { + List> digests = new ArrayList<>(); + for (Map.Entry apkContentDigest : apkContentDigests.entrySet()) { + digests.add(Pair.create(apkContentDigest.getKey(), apkContentDigest.getValue())); + } + digests.sort(Comparator.comparing(pair -> pair.first)); + return digests; + } + + private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException { + ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME); + if (zipEntry == null) { + // SourceStamp certificate hash file not found, which means that there is not + // SourceStamp present. + return null; + } + return Streams.readFully(apkJar.getInputStream(zipEntry)); + } + + private static byte[] getManifestBytes(StrictJarFile apkJar) throws IOException { + ZipEntry zipEntry = apkJar.findEntry(JarFile.MANIFEST_NAME); + if (zipEntry == null) { + return null; + } + return Streams.readFully(apkJar.getInputStream(zipEntry)); } private static byte[] encodeApkContentDigests(List> apkContentDigests) { @@ -329,6 +414,16 @@ public abstract class SourceStampVerifier { return result.array(); } + private static byte[] computeSha256Digest(byte[] input) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(input); + return messageDigest.digest(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to find SHA-256", e); + } + } + private static void closeApkJar(StrictJarFile apkJar) { try { if (apkJar == null) { diff --git a/core/java/android/util/apk/TEST_MAPPING b/core/java/android/util/apk/TEST_MAPPING new file mode 100644 index 0000000000000..8544e82e04e03 --- /dev/null +++ b/core/java/android/util/apk/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "FrameworksCoreTests", + "options": [ + { + "include-filter": "android.util.apk.SourceStampVerifierTest" + } + ] + } + ] +} diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v1.apk similarity index 73% rename from core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk rename to core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v1.apk index 1dc1e998f1eec..add4aa0387083 100644 Binary files a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch.apk and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v1.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v2.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v2.apk new file mode 100644 index 0000000000000..e55eb903c68bb Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v2.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v3.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v3.apk new file mode 100644 index 0000000000000..de23558220939 Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-apk-hash-mismatch-v3.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk index 562805cf67eb0..f1105f96664c9 100644 Binary files a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-certificate-mismatch.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk index 2723cc8fdbeb5..d28774aa2b142 100644 Binary files a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-malformed-signature.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk index 9dec2f5911334..604fe6f435237 100644 Binary files a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-without-block.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk b/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk index 8056e0bf6e50d..2f2a5923d24aa 100644 Binary files a/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk and b/core/tests/coretests/assets/SourceStampVerifierTest/valid-stamp.apk differ diff --git a/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java index 37b2817928aa6..81d54b57486ca 100644 --- a/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java +++ b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java @@ -26,8 +26,11 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; import android.content.Context; -import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; +import libcore.io.Streams; + +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -46,8 +49,20 @@ import java.util.zip.ZipFile; @RunWith(JUnit4.class) public class SourceStampVerifierTest { - private final Context mContext = - InstrumentationRegistry.getInstrumentation().getTargetContext(); + private final Context mContext = ApplicationProvider.getApplicationContext(); + + private File mPrimaryApk; + private File mSecondaryApk; + + @After + public void tearDown() throws Exception { + if (mPrimaryApk != null) { + mPrimaryApk.delete(); + } + if (mSecondaryApk != null) { + mSecondaryApk.delete(); + } + } @Test public void testSourceStamp_noStamp() throws Exception { @@ -63,17 +78,11 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_correctSignature() throws Exception { - File testApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); - ZipFile apkZipFile = new ZipFile(testApk); - ZipEntry stampCertZipEntry = apkZipFile.getEntry("stamp-cert-sha256"); - int size = (int) stampCertZipEntry.getSize(); - byte[] expectedStampCertHash = new byte[size]; - try (InputStream inputStream = apkZipFile.getInputStream(stampCertZipEntry)) { - inputStream.read(expectedStampCertHash); - } + mPrimaryApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); + byte[] expectedStampCertHash = getSourceStampCertificateHashFromApk(mPrimaryApk); SourceStampVerificationResult result = - SourceStampVerifier.verify(testApk.getAbsolutePath()); + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); assertTrue(result.isPresent()); assertTrue(result.isVerified()); @@ -85,10 +94,10 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_signatureMissing() throws Exception { - File testApk = getApk("SourceStampVerifierTest/stamp-without-block.apk"); + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-without-block.apk"); SourceStampVerificationResult result = - SourceStampVerifier.verify(testApk.getAbsolutePath()); + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); assertTrue(result.isPresent()); assertFalse(result.isVerified()); @@ -97,10 +106,10 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_certificateMismatch() throws Exception { - File testApk = getApk("SourceStampVerifierTest/stamp-certificate-mismatch.apk"); + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-certificate-mismatch.apk"); SourceStampVerificationResult result = - SourceStampVerifier.verify(testApk.getAbsolutePath()); + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); assertTrue(result.isPresent()); assertFalse(result.isVerified()); @@ -108,11 +117,35 @@ public class SourceStampVerifierTest { } @Test - public void testSourceStamp_apkHashMismatch() throws Exception { - File testApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch.apk"); + public void testSourceStamp_apkHashMismatch_v1SignatureScheme() throws Exception { + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch-v1.apk"); SourceStampVerificationResult result = - SourceStampVerifier.verify(testApk.getAbsolutePath()); + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_apkHashMismatch_v2SignatureScheme() throws Exception { + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch-v2.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + } + + @Test + public void testSourceStamp_apkHashMismatch_v3SignatureScheme() throws Exception { + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch-v3.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); assertTrue(result.isPresent()); assertFalse(result.isVerified()); @@ -121,10 +154,10 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_malformedSignature() throws Exception { - File testApk = getApk("SourceStampVerifierTest/stamp-malformed-signature.apk"); + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-malformed-signature.apk"); SourceStampVerificationResult result = - SourceStampVerifier.verify(testApk.getAbsolutePath()); + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); assertTrue(result.isPresent()); assertFalse(result.isVerified()); @@ -133,21 +166,14 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_multiApk_validStamps() throws Exception { - File testApk1 = getApk("SourceStampVerifierTest/valid-stamp.apk"); - File testApk2 = getApk("SourceStampVerifierTest/valid-stamp.apk"); - ZipFile apkZipFile = new ZipFile(testApk1); - ZipEntry stampCertZipEntry = apkZipFile.getEntry("stamp-cert-sha256"); - int size = (int) stampCertZipEntry.getSize(); - byte[] expectedStampCertHash = new byte[size]; - try (InputStream inputStream = apkZipFile.getInputStream(stampCertZipEntry)) { - inputStream.read(expectedStampCertHash); - } + mPrimaryApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); + mSecondaryApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); + byte[] expectedStampCertHash = getSourceStampCertificateHashFromApk(mPrimaryApk); List apkFiles = new ArrayList<>(); - apkFiles.add(testApk1.getAbsolutePath()); - apkFiles.add(testApk2.getAbsolutePath()); + apkFiles.add(mPrimaryApk.getAbsolutePath()); + apkFiles.add(mSecondaryApk.getAbsolutePath()); - SourceStampVerificationResult result = - SourceStampVerifier.verify(apkFiles); + SourceStampVerificationResult result = SourceStampVerifier.verify(apkFiles); assertTrue(result.isPresent()); assertTrue(result.isVerified()); @@ -159,14 +185,13 @@ public class SourceStampVerifierTest { @Test public void testSourceStamp_multiApk_invalidStamps() throws Exception { - File testApk1 = getApk("SourceStampVerifierTest/valid-stamp.apk"); - File testApk2 = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch.apk"); + mPrimaryApk = getApk("SourceStampVerifierTest/valid-stamp.apk"); + mSecondaryApk = getApk("SourceStampVerifierTest/stamp-apk-hash-mismatch-v3.apk"); List apkFiles = new ArrayList<>(); - apkFiles.add(testApk1.getAbsolutePath()); - apkFiles.add(testApk2.getAbsolutePath()); + apkFiles.add(mPrimaryApk.getAbsolutePath()); + apkFiles.add(mSecondaryApk.getAbsolutePath()); - SourceStampVerificationResult result = - SourceStampVerifier.verify(apkFiles); + SourceStampVerificationResult result = SourceStampVerifier.verify(apkFiles); assertTrue(result.isPresent()); assertFalse(result.isVerified()); @@ -174,10 +199,16 @@ public class SourceStampVerifierTest { } private File getApk(String apkPath) throws IOException { - File testApk = File.createTempFile("SourceStampApk", ".apk"); + File apk = File.createTempFile("SourceStampApk", ".apk"); try (InputStream inputStream = mContext.getAssets().open(apkPath)) { - Files.copy(inputStream, testApk.toPath(), REPLACE_EXISTING); + Files.copy(inputStream, apk.toPath(), REPLACE_EXISTING); } - return testApk; + return apk; + } + + private byte[] getSourceStampCertificateHashFromApk(File apk) throws IOException { + ZipFile apkZipFile = new ZipFile(apk); + ZipEntry stampCertZipEntry = apkZipFile.getEntry("stamp-cert-sha256"); + return Streams.readFully(apkZipFile.getInputStream(stampCertZipEntry)); } } diff --git a/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/SourceStampTestApk.apk b/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/SourceStampTestApk.apk index 211e064399a80..bd871ae7488c7 100644 Binary files a/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/SourceStampTestApk.apk and b/services/tests/servicestests/assets/AppIntegrityManagerServiceImplTest/SourceStampTestApk.apk differ