diff --git a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java index 5f963b0193351..4d1402a0d6d65 100644 --- a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java +++ b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java @@ -25,6 +25,7 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyA import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; +import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct; import android.os.Build; import android.util.ArrayMap; @@ -53,7 +54,6 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Map; @@ -90,9 +90,10 @@ public class ApkSignatureSchemeV3Verifier { * associated with each signer. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. - * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not - * verify. - * @throws IOException if an I/O error occurs while reading the APK file. + * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does + * not + * verify. + * @throws IOException if an I/O error occurs while reading the APK file. */ public static VerifiedSigner verify(String apkFile) throws SignatureNotFoundException, SecurityException, IOException { @@ -106,7 +107,7 @@ public class ApkSignatureSchemeV3Verifier { * Block while gathering signer information. The APK contents are not verified. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. - * @throws IOException if an I/O error occurs while reading the APK file. + * @throws IOException if an I/O error occurs while reading the APK file. */ public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile) throws SignatureNotFoundException, SecurityException, IOException { @@ -125,9 +126,10 @@ public class ApkSignatureSchemeV3Verifier { * associated with each signer. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. - * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not - * verify. - * @throws IOException if an I/O error occurs while reading the APK file. + * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does + * not + * verify. + * @throws IOException if an I/O error occurs while reading the APK file. */ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity) throws SignatureNotFoundException, SecurityException, IOException { @@ -140,7 +142,7 @@ public class ApkSignatureSchemeV3Verifier { * additional information relevant for verifying the block against the file. * * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3. - * @throws IOException if an I/O error occurs while reading the APK file. + * @throws IOException if an I/O error occurs while reading the APK file. */ public static SignatureInfo findSignature(RandomAccessFile apk) throws IOException, SignatureNotFoundException { @@ -152,7 +154,7 @@ public class ApkSignatureSchemeV3Verifier { * Block. * * @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it - * against the APK file. + * against the APK file. */ private static VerifiedSigner verify( RandomAccessFile apk, @@ -160,7 +162,7 @@ public class ApkSignatureSchemeV3Verifier { boolean doVerifyIntegrity) throws SecurityException, IOException { int signerCount = 0; Map contentDigests = new ArrayMap<>(); - Pair result = null; + Pair result = null; CertificateFactory certFactory; try { certFactory = CertificateFactory.getInstance("X.509"); @@ -215,10 +217,11 @@ public class ApkSignatureSchemeV3Verifier { return new VerifiedSigner(result.first, result.second, verityRootHash, contentDigests); } - private static Pair verifySigner( - ByteBuffer signerBlock, - Map contentDigests, - CertificateFactory certFactory) + private static Pair + verifySigner( + ByteBuffer signerBlock, + Map contentDigests, + CertificateFactory certFactory) throws SecurityException, IOException, PlatformNotSupportedException { ByteBuffer signedData = getLengthPrefixedSlice(signerBlock); int minSdkVersion = signerBlock.getInt(); @@ -228,9 +231,9 @@ public class ApkSignatureSchemeV3Verifier { // this signature isn't meant to be used with this platform, skip it. throw new PlatformNotSupportedException( "Signer not supported by this platform " - + "version. This platform: " + Build.VERSION.SDK_INT - + ", signer minSdkVersion: " + minSdkVersion - + ", maxSdkVersion: " + maxSdkVersion); + + "version. This platform: " + Build.VERSION.SDK_INT + + ", signer minSdkVersion: " + minSdkVersion + + ", maxSdkVersion: " + maxSdkVersion); } ByteBuffer signatures = getLengthPrefixedSlice(signerBlock); @@ -331,7 +334,8 @@ public class ApkSignatureSchemeV3Verifier { && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) { throw new SecurityException( getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm) - + " contents digest does not match the digest specified by a preceding signer"); + + " contents digest does not match the digest specified by a " + + "preceding signer"); } ByteBuffer certificates = getLengthPrefixedSlice(signedData); @@ -379,11 +383,11 @@ public class ApkSignatureSchemeV3Verifier { private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c; - private static Pair verifyAdditionalAttributes( - ByteBuffer attrs, List certs, CertificateFactory certFactory) - throws IOException { + private static Pair + verifyAdditionalAttributes(ByteBuffer attrs, List certs, + CertificateFactory certFactory) throws IOException { X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]); - VerifiedProofOfRotation por = null; + ApkSigningBlockUtils.VerifiedProofOfRotation por = null; while (attrs.hasRemaining()) { ByteBuffer attr = getLengthPrefixedSlice(attrs); @@ -392,7 +396,7 @@ public class ApkSignatureSchemeV3Verifier { + "ID. Remaining: " + attr.remaining()); } int id = attr.getInt(); - switch(id) { + switch (id) { case PROOF_OF_ROTATION_ATTR_ID: if (por != null) { throw new SecurityException("Encountered multiple Proof-of-rotation records" @@ -404,7 +408,7 @@ public class ApkSignatureSchemeV3Verifier { try { if (por.certs.size() > 0 && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(), - certChain[0].getEncoded())) { + certChain[0].getEncoded())) { throw new SecurityException("Terminal certificate in Proof-of-rotation" + " record does not match APK signing certificate"); } @@ -422,96 +426,6 @@ public class ApkSignatureSchemeV3Verifier { return Pair.create(certChain, por); } - private static VerifiedProofOfRotation verifyProofOfRotationStruct( - ByteBuffer porBuf, - CertificateFactory certFactory) - throws SecurityException, IOException { - int levelCount = 0; - int lastSigAlgorithm = -1; - X509Certificate lastCert = null; - List certs = new ArrayList<>(); - List flagsList = new ArrayList<>(); - - // Proof-of-rotation struct: - // A uint32 version code followed by basically a singly linked list of nodes, called levels - // here, each of which have the following structure: - // * length-prefix for the entire level - // - length-prefixed signed data (if previous level exists) - // * length-prefixed X509 Certificate - // * uint32 signature algorithm ID describing how this signed data was signed - // - uint32 flags describing how to treat the cert contained in this level - // - uint32 signature algorithm ID to use to verify the signature of the next level. The - // algorithm here must match the one in the signed data section of the next level. - // - length-prefixed signature over the signed data in this level. The signature here - // is verified using the certificate from the previous level. - // The linking is provided by the certificate of each level signing the one of the next. - - try { - - // get the version code, but don't do anything with it: creator knew about all our flags - porBuf.getInt(); - HashSet certHistorySet = new HashSet<>(); - while (porBuf.hasRemaining()) { - levelCount++; - ByteBuffer level = getLengthPrefixedSlice(porBuf); - ByteBuffer signedData = getLengthPrefixedSlice(level); - int flags = level.getInt(); - int sigAlgorithm = level.getInt(); - byte[] signature = readLengthPrefixedByteArray(level); - - if (lastCert != null) { - // Use previous level cert to verify current level - Pair sigAlgParams = - getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm); - PublicKey publicKey = lastCert.getPublicKey(); - Signature sig = Signature.getInstance(sigAlgParams.first); - sig.initVerify(publicKey); - if (sigAlgParams.second != null) { - sig.setParameter(sigAlgParams.second); - } - sig.update(signedData); - if (!sig.verify(signature)) { - throw new SecurityException("Unable to verify signature of certificate #" - + levelCount + " using " + sigAlgParams.first + " when verifying" - + " Proof-of-rotation record"); - } - } - - signedData.rewind(); - byte[] encodedCert = readLengthPrefixedByteArray(signedData); - int signedSigAlgorithm = signedData.getInt(); - if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { - throw new SecurityException("Signing algorithm ID mismatch for certificate #" - + levelCount + " when verifying Proof-of-rotation record"); - } - lastCert = (X509Certificate) - certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); - lastCert = new VerbatimX509Certificate(lastCert, encodedCert); - - lastSigAlgorithm = sigAlgorithm; - if (certHistorySet.contains(lastCert)) { - throw new SecurityException("Encountered duplicate entries in " - + "Proof-of-rotation record at certificate #" + levelCount + ". All " - + "signing certificates should be unique"); - } - certHistorySet.add(lastCert); - certs.add(lastCert); - flagsList.add(flags); - } - } catch (IOException | BufferUnderflowException e) { - throw new IOException("Failed to parse Proof-of-rotation record", e); - } catch (NoSuchAlgorithmException | InvalidKeyException - | InvalidAlgorithmParameterException | SignatureException e) { - throw new SecurityException( - "Failed to verify signature over signed data for certificate #" - + levelCount + " when verifying Proof-of-rotation record", e); - } catch (CertificateException e) { - throw new SecurityException("Failed to decode certificate #" + levelCount - + " when verifying Proof-of-rotation record", e); - } - return new VerifiedProofOfRotation(certs, flagsList); - } - static byte[] getVerityRootHash(String apkPath) throws IOException, SignatureNotFoundException, SecurityException { try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { @@ -523,7 +437,7 @@ public class ApkSignatureSchemeV3Verifier { static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory) throws IOException, SignatureNotFoundException, SecurityException, DigestException, - NoSuchAlgorithmException { + NoSuchAlgorithmException { try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { SignatureInfo signatureInfo = findSignature(apk); return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo); @@ -532,7 +446,7 @@ public class ApkSignatureSchemeV3Verifier { static byte[] generateApkVerityRootHash(String apkPath) throws NoSuchAlgorithmException, DigestException, IOException, - SignatureNotFoundException { + SignatureNotFoundException { try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { SignatureInfo signatureInfo = findSignature(apk); VerifiedSigner vSigner = verify(apk, false); @@ -544,21 +458,6 @@ public class ApkSignatureSchemeV3Verifier { } } - /** - * Verified processed proof of rotation. - * - * @hide for internal use only. - */ - public static class VerifiedProofOfRotation { - public final List certs; - public final List flagsList; - - public VerifiedProofOfRotation(List certs, List flagsList) { - this.certs = certs; - this.flagsList = flagsList; - } - } - /** * Verified APK Signature Scheme v3 signer, including the proof of rotation structure. * @@ -566,14 +465,15 @@ public class ApkSignatureSchemeV3Verifier { */ public static class VerifiedSigner { public final X509Certificate[] certs; - public final VerifiedProofOfRotation por; + public final ApkSigningBlockUtils.VerifiedProofOfRotation por; public final byte[] verityRootHash; // Algorithm -> digest map of signed digests in the signature. // All these are verified if requested. public final Map contentDigests; - public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por, + public VerifiedSigner(X509Certificate[] certs, + ApkSigningBlockUtils.VerifiedProofOfRotation por, byte[] verityRootHash, Map contentDigests) { this.certs = certs; this.por = por; diff --git a/core/java/android/util/apk/ApkSigningBlockUtils.java b/core/java/android/util/apk/ApkSigningBlockUtils.java index 021f232979ef0..c97c995641d14 100644 --- a/core/java/android/util/apk/ApkSigningBlockUtils.java +++ b/core/java/android/util/apk/ApkSigningBlockUtils.java @@ -19,6 +19,7 @@ package android.util.apk; import android.util.ArrayMap; import android.util.Pair; +import java.io.ByteArrayInputStream; import java.io.FileDescriptor; import java.io.IOException; import java.io.RandomAccessFile; @@ -26,12 +27,23 @@ import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.DigestException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PSSParameterSpec; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Map; /** @@ -51,9 +63,8 @@ public final class ApkSigningBlockUtils { * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs * identifying the appropriate block to find, e.g. the APK Signature Scheme v2 * block ID. - * * @throws SignatureNotFoundException if the APK is not signed using this scheme. - * @throws IOException if an I/O error occurs while reading the APK file. + * @throws IOException if an I/O error occurs while reading the APK file. */ static SignatureInfo findSignature(RandomAccessFile apk, int blockId) throws IOException, SignatureNotFoundException { @@ -377,7 +388,7 @@ public final class ApkSigningBlockUtils { /** * Returns the ZIP End of Central Directory (EoCD) and its offset in the file. * - * @throws IOException if an I/O error occurs while reading the file. + * @throws IOException if an I/O error occurs while reading the file. * @throws SignatureNotFoundException if the EoCD could not be found. */ static Pair getEocd(RandomAccessFile apk) @@ -398,13 +409,13 @@ public final class ApkSigningBlockUtils { if (centralDirOffset > eocdOffset) { throw new SignatureNotFoundException( "ZIP Central Directory offset out of range: " + centralDirOffset - + ". ZIP End of Central Directory offset: " + eocdOffset); + + ". ZIP End of Central Directory offset: " + eocdOffset); } long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd); if (centralDirOffset + centralDirSize != eocdOffset) { throw new SignatureNotFoundException( "ZIP Central Directory is not immediately followed by End of Central" - + " Directory"); + + " Directory"); } return centralDirOffset; } @@ -687,7 +698,7 @@ public final class ApkSigningBlockUtils { static Pair findApkSigningBlock( RandomAccessFile apk, long centralDirOffset) - throws IOException, SignatureNotFoundException { + throws IOException, SignatureNotFoundException { // FORMAT: // OFFSET DATA TYPE DESCRIPTION // * @+0 bytes uint64: size in bytes (excluding this field) @@ -806,4 +817,108 @@ public final class ApkSigningBlockUtils { } } + static VerifiedProofOfRotation verifyProofOfRotationStruct( + ByteBuffer porBuf, + CertificateFactory certFactory) + throws SecurityException, IOException { + int levelCount = 0; + int lastSigAlgorithm = -1; + X509Certificate lastCert = null; + List certs = new ArrayList<>(); + List flagsList = new ArrayList<>(); + + // Proof-of-rotation struct: + // A uint32 version code followed by basically a singly linked list of nodes, called levels + // here, each of which have the following structure: + // * length-prefix for the entire level + // - length-prefixed signed data (if previous level exists) + // * length-prefixed X509 Certificate + // * uint32 signature algorithm ID describing how this signed data was signed + // - uint32 flags describing how to treat the cert contained in this level + // - uint32 signature algorithm ID to use to verify the signature of the next level. The + // algorithm here must match the one in the signed data section of the next level. + // - length-prefixed signature over the signed data in this level. The signature here + // is verified using the certificate from the previous level. + // The linking is provided by the certificate of each level signing the one of the next. + + try { + + // get the version code, but don't do anything with it: creator knew about all our flags + porBuf.getInt(); + HashSet certHistorySet = new HashSet<>(); + while (porBuf.hasRemaining()) { + levelCount++; + ByteBuffer level = getLengthPrefixedSlice(porBuf); + ByteBuffer signedData = getLengthPrefixedSlice(level); + int flags = level.getInt(); + int sigAlgorithm = level.getInt(); + byte[] signature = readLengthPrefixedByteArray(level); + + if (lastCert != null) { + // Use previous level cert to verify current level + Pair sigAlgParams = + getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm); + PublicKey publicKey = lastCert.getPublicKey(); + Signature sig = Signature.getInstance(sigAlgParams.first); + sig.initVerify(publicKey); + if (sigAlgParams.second != null) { + sig.setParameter(sigAlgParams.second); + } + sig.update(signedData); + if (!sig.verify(signature)) { + throw new SecurityException("Unable to verify signature of certificate #" + + levelCount + " using " + sigAlgParams.first + " when verifying" + + " Proof-of-rotation record"); + } + } + + signedData.rewind(); + byte[] encodedCert = readLengthPrefixedByteArray(signedData); + int signedSigAlgorithm = signedData.getInt(); + if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { + throw new SecurityException("Signing algorithm ID mismatch for certificate #" + + levelCount + " when verifying Proof-of-rotation record"); + } + lastCert = (X509Certificate) + certFactory.generateCertificate(new ByteArrayInputStream(encodedCert)); + lastCert = new VerbatimX509Certificate(lastCert, encodedCert); + + lastSigAlgorithm = sigAlgorithm; + if (certHistorySet.contains(lastCert)) { + throw new SecurityException("Encountered duplicate entries in " + + "Proof-of-rotation record at certificate #" + levelCount + ". All " + + "signing certificates should be unique"); + } + certHistorySet.add(lastCert); + certs.add(lastCert); + flagsList.add(flags); + } + } catch (IOException | BufferUnderflowException e) { + throw new IOException("Failed to parse Proof-of-rotation record", e); + } catch (NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | SignatureException e) { + throw new SecurityException( + "Failed to verify signature over signed data for certificate #" + + levelCount + " when verifying Proof-of-rotation record", e); + } catch (CertificateException e) { + throw new SecurityException("Failed to decode certificate #" + levelCount + + " when verifying Proof-of-rotation record", e); + } + return new VerifiedProofOfRotation(certs, flagsList); + } + + /** + * Verified processed proof of rotation. + * + * @hide for internal use only. + */ + public static class VerifiedProofOfRotation { + public final List certs; + public final List flagsList; + + public VerifiedProofOfRotation(List certs, List flagsList) { + this.certs = certs; + this.flagsList = flagsList; + } + } } diff --git a/core/java/android/util/apk/SourceStampVerificationResult.java b/core/java/android/util/apk/SourceStampVerificationResult.java index 2edaf623fb94e..8b9eee2f796ea 100644 --- a/core/java/android/util/apk/SourceStampVerificationResult.java +++ b/core/java/android/util/apk/SourceStampVerificationResult.java @@ -19,6 +19,8 @@ package android.util.apk; import android.annotation.Nullable; import java.security.cert.Certificate; +import java.util.Collections; +import java.util.List; /** * A class encapsulating the result from the source stamp verifier @@ -32,12 +34,15 @@ public final class SourceStampVerificationResult { private final boolean mPresent; private final boolean mVerified; private final Certificate mCertificate; + private final List mCertificateLineage; private SourceStampVerificationResult( - boolean present, boolean verified, @Nullable Certificate certificate) { + boolean present, boolean verified, @Nullable Certificate certificate, + List certificateLineage) { this.mPresent = present; this.mVerified = verified; this.mCertificate = certificate; + this.mCertificateLineage = certificateLineage; } public boolean isPresent() { @@ -52,6 +57,10 @@ public final class SourceStampVerificationResult { return mCertificate; } + public List getCertificateLineage() { + return mCertificateLineage; + } + /** * Create a non-present source stamp outcome. * @@ -59,18 +68,21 @@ public final class SourceStampVerificationResult { */ public static SourceStampVerificationResult notPresent() { return new SourceStampVerificationResult( - /* present= */ false, /* verified= */ false, /* certificate= */ null); + /* present= */ false, /* verified= */ false, /* certificate= */ + null, /* certificateLineage= */ Collections.emptyList()); } /** * Create a verified source stamp outcome. * - * @param certificate The source stamp certificate. + * @param certificate The source stamp certificate. + * @param certificateLineage The proof-of-rotation lineage for the source stamp. * @return A verified source stamp result, and the source stamp certificate. */ - public static SourceStampVerificationResult verified(Certificate certificate) { + public static SourceStampVerificationResult verified(Certificate certificate, + List certificateLineage) { return new SourceStampVerificationResult( - /* present= */ true, /* verified= */ true, certificate); + /* present= */ true, /* verified= */ true, certificate, certificateLineage); } /** @@ -80,6 +92,7 @@ public final class SourceStampVerificationResult { */ public static SourceStampVerificationResult notVerified() { return new SourceStampVerificationResult( - /* present= */ true, /* verified= */ false, /* certificate= */ null); + /* present= */ true, /* verified= */ false, /* certificate= */ + null, /* certificateLineage= */ Collections.emptyList()); } } diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java index 5fc242353d519..f9e312146ccf0 100644 --- a/core/java/android/util/apk/SourceStampVerifier.java +++ b/core/java/android/util/apk/SourceStampVerifier.java @@ -23,6 +23,7 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContent import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm; import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm; import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; +import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct; import android.util.Pair; import android.util.Slog; @@ -44,12 +45,14 @@ import java.security.PublicKey; import java.security.Signature; import java.security.SignatureException; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; 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.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -76,6 +79,7 @@ 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 = 0x6dff800d; + private static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7; private static final int VERSION_JAR_SIGNATURE_SCHEME = 1; private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2; @@ -85,11 +89,13 @@ public abstract class SourceStampVerifier { private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256"; /** Hidden constructor to prevent instantiation. */ - private SourceStampVerifier() {} + private SourceStampVerifier() { + } - /** Verifies SourceStamp present in a list of APKs. */ + /** Verifies SourceStamp present in a list of (split) APKs for the same app. */ public static SourceStampVerificationResult verify(List apkFiles) { Certificate stampCertificate = null; + List stampCertificateLineage = Collections.emptyList(); for (String apkFile : apkFiles) { SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile); if (!sourceStampVerificationResult.isPresent() @@ -97,12 +103,15 @@ public abstract class SourceStampVerifier { return sourceStampVerificationResult; } if (stampCertificate != null - && !stampCertificate.equals(sourceStampVerificationResult.getCertificate())) { + && (!stampCertificate.equals(sourceStampVerificationResult.getCertificate()) + || !stampCertificateLineage.equals( + sourceStampVerificationResult.getCertificateLineage()))) { return SourceStampVerificationResult.notVerified(); } stampCertificate = sourceStampVerificationResult.getCertificate(); + stampCertificateLineage = sourceStampVerificationResult.getCertificateLineage(); } - return SourceStampVerificationResult.verified(stampCertificate); + return SourceStampVerificationResult.verified(stampCertificate, stampCertificateLineage); } /** Verifies SourceStamp present in the provided APK. */ @@ -177,21 +186,44 @@ public abstract class SourceStampVerifier { "No signatures found for signature scheme %d", signatureSchemeDigest.getKey())); } + ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice( + signedSignatureSchemeData.get(signatureSchemeDigest.getKey())); verifySourceStampSignature( - signedSignatureSchemeData.get(signatureSchemeDigest.getKey()), + signatureSchemeDigest.getValue(), sourceStampCertificate, - signatureSchemeDigest.getValue()); + signatures); } - return SourceStampVerificationResult.verified(sourceStampCertificate); + List sourceStampCertificateLineage = Collections.emptyList(); + if (sourceStampBlockData.hasRemaining()) { + // The stamp block contains some additional attributes. + ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData); + ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData); + + byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()]; + stampAttributeData.get(stampAttributeBytes); + stampAttributeData.flip(); + + verifySourceStampSignature(stampAttributeBytes, sourceStampCertificate, + stampAttributeDataSignatures); + ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = + verifySourceStampAttributes(stampAttributeData, sourceStampCertificate); + if (verifiedProofOfRotation != null) { + sourceStampCertificateLineage = verifiedProofOfRotation.certs; + } + } + + return SourceStampVerificationResult.verified(sourceStampCertificate, + sourceStampCertificateLineage); } /** * 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 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( @@ -230,16 +262,16 @@ public abstract class SourceStampVerifier { * 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 data the digest to be verified being signed by the source stamp + * certificate. * @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. + * @param signatures the source stamp block in the APK signing block which contains + * the stamp signed digests. */ - private static void verifySourceStampSignature( - ByteBuffer signedBlockData, X509Certificate sourceStampCertificate, byte[] digest) + private static void verifySourceStampSignature(byte[] data, + X509Certificate sourceStampCertificate, ByteBuffer signatures) throws IOException { // Parse the signatures block and identify supported signatures - ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedBlockData); int signatureCount = 0; int bestSigAlgorithm = -1; byte[] bestSigAlgorithmSignatureBytes = null; @@ -285,7 +317,7 @@ public abstract class SourceStampVerifier { if (jcaSignatureAlgorithmParams != null) { sig.setParameter(jcaSignatureAlgorithmParams); } - sig.update(digest); + sig.update(data); sigVerified = sig.verify(bestSigAlgorithmSignatureBytes); } catch (InvalidKeyException | InvalidAlgorithmParameterException @@ -414,6 +446,46 @@ public abstract class SourceStampVerifier { return result.array(); } + private static ApkSigningBlockUtils.VerifiedProofOfRotation verifySourceStampAttributes( + ByteBuffer stampAttributeData, + X509Certificate sourceStampCertificate) + throws IOException { + CertificateFactory certFactory; + try { + certFactory = CertificateFactory.getInstance("X.509"); + } catch (CertificateException e) { + throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e); + } + ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData); + ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = null; + while (stampAttributes.hasRemaining()) { + ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes); + int id = attribute.getInt(); + if (id == PROOF_OF_ROTATION_ATTR_ID) { + if (verifiedProofOfRotation != null) { + throw new SecurityException("Encountered multiple Proof-of-rotation records" + + " when verifying source stamp signature"); + } + verifiedProofOfRotation = verifyProofOfRotationStruct(attribute, certFactory); + // Make sure that the last certificate in the Proof-of-rotation record matches + // the one used to sign this APK. + try { + if (verifiedProofOfRotation.certs.size() > 0 + && !Arrays.equals(verifiedProofOfRotation.certs.get( + verifiedProofOfRotation.certs.size() - 1).getEncoded(), + sourceStampCertificate.getEncoded())) { + throw new SecurityException("Terminal certificate in Proof-of-rotation" + + " record does not match source stamp certificate"); + } + } catch (CertificateEncodingException e) { + throw new SecurityException("Failed to encode certificate when comparing" + + " Proof-of-rotation record and source stamp certificate", e); + } + } + } + return verifiedProofOfRotation; + } + private static byte[] computeSha256Digest(byte[] input) { try { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-invalid.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-invalid.apk new file mode 100644 index 0000000000000..f9777c3f5ca5a Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-invalid.apk differ diff --git a/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-valid.apk b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-valid.apk new file mode 100644 index 0000000000000..955652e387b82 Binary files /dev/null and b/core/tests/coretests/assets/SourceStampVerifierTest/stamp-lineage-valid.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 81d54b57486ca..bc0bddba2f207 100644 --- a/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java +++ b/core/tests/coretests/src/android/util/apk/SourceStampVerifierTest.java @@ -17,6 +17,7 @@ package android.util.apk; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -198,6 +199,38 @@ public class SourceStampVerifierTest { assertNull(result.getCertificate()); } + @Test + public void testSourceStamp_validStampLineage() throws Exception { + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-lineage-valid.apk"); + byte[] expectedStampCertHash = getSourceStampCertificateHashFromApk(mPrimaryApk); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertTrue(result.isVerified()); + assertNotNull(result.getCertificate()); + byte[] actualStampCertHash = + MessageDigest.getInstance("SHA-256").digest(result.getCertificate().getEncoded()); + assertArrayEquals(expectedStampCertHash, actualStampCertHash); + assertEquals(2, result.getCertificateLineage().size()); + assertEquals(result.getCertificate(), + result.getCertificateLineage().get(result.getCertificateLineage().size() - 1)); + } + + @Test + public void testSourceStamp_invalidStampLineage() throws Exception { + mPrimaryApk = getApk("SourceStampVerifierTest/stamp-lineage-invalid.apk"); + + SourceStampVerificationResult result = + SourceStampVerifier.verify(mPrimaryApk.getAbsolutePath()); + + assertTrue(result.isPresent()); + assertFalse(result.isVerified()); + assertNull(result.getCertificate()); + assertTrue(result.getCertificateLineage().isEmpty()); + } + private File getApk(String apkPath) throws IOException { File apk = File.createTempFile("SourceStampApk", ".apk"); try (InputStream inputStream = mContext.getAssets().open(apkPath)) {