Accept APK install with v4 signature to set up fs-verity

.idsig is recognized and staged in the installer session. When .idsig is
provided, fs-verity is enabled in validateApkInstallLocked before the
first APK signature check happens.

With fs-verity enabled, ApkSignatureSchemeV4Verifier can also work (in
additional to IncFS) over fs-verity. The verifier can build fs-verity
digest from V4Signature.HashingInfo and verify the signed data is
consistent with the actual fs-verity digest. See
VerityUtils#generateFsVerityDigest.

ApkSignatureSchemeV4Verifier#extractSignature now also throws
SignatureException. When a signature size is wrong (see CTS test
PkgInstallSignatureVerificationTest#testInstallV4WithWrongSignatureBytesSize),
V4Signature.SigningInfos.fromByteArray throws an EOFException (which is
an IOException). The IOException is handled as missing signature by
rethrowing as SignatureNotFoundException. But this allows a fallback to
other v3/v2 signature check. This change distriguishes it by rethrowing a
SignatureException instead. This is not a problem during an incremental
install, because the signature size check happens earlier when the
installer commits, and it's done inside IncFS.

Bug: 277344944
Test: Force enable the (read-only) flag, since it's off in build time, then
      atest android.appsecurity.cts.PkgInstallSignatureVerificationTest
Change-Id: I6fd22fe2e04cfc58c68e690f23f63ff268938eda
diff --git a/core/java/android/os/incremental/V4Signature.java b/core/java/android/os/incremental/V4Signature.java
index 2044502..38d885f 100644
--- a/core/java/android/os/incremental/V4Signature.java
+++ b/core/java/android/os/incremental/V4Signature.java
@@ -254,7 +254,10 @@
         this.signingInfos = signingInfos;
     }
 
-    private static V4Signature readFrom(InputStream stream) throws IOException {
+    /**
+     * Constructs a V4Signature from an InputStream.
+     */
+    public static V4Signature readFrom(InputStream stream) throws IOException {
         final int version = readIntLE(stream);
         int maxSize = INCFS_MAX_SIGNATURE_SIZE;
         final byte[] hashingInfo = readBytes(stream, maxSize);
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
index 6b26155..5210271 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
@@ -27,9 +27,14 @@
 import android.util.ArrayMap;
 import android.util.Pair;
 
+import com.android.internal.security.VerityUtils;
+
 import java.io.ByteArrayInputStream;
+import java.io.EOFException;
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.security.DigestException;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.KeyFactory;
@@ -60,7 +65,7 @@
      * certificates associated with each signer.
      */
     public static VerifiedSigner extractCertificates(String apkFile)
-            throws SignatureNotFoundException, SecurityException {
+            throws SignatureNotFoundException, SignatureException, SecurityException {
         Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> pair = extractSignature(apkFile);
         return verify(apkFile, pair.first, pair.second, APK_SIGNATURE_SCHEME_DEFAULT);
     }
@@ -69,15 +74,37 @@
      * Extracts APK Signature Scheme v4 signature of the provided APK.
      */
     public static Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> extractSignature(
-            String apkFile) throws SignatureNotFoundException {
-        final File apk = new File(apkFile);
-        final byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature(
-                apk.getAbsolutePath());
-        if (signatureBytes == null || signatureBytes.length == 0) {
-            throw new SignatureNotFoundException("Failed to obtain signature bytes from IncFS.");
-        }
+            String apkFile) throws SignatureNotFoundException, SignatureException {
         try {
-            final V4Signature signature = V4Signature.readFrom(signatureBytes);
+            final File apk = new File(apkFile);
+            boolean needsConsistencyCheck;
+
+            // 1. Try IncFS first. IncFS verifies the file according to the integrity metadata
+            // (including the root hash of Merkle tree) it keeps track of with signature check. No
+            // further consistentcy check is needed.
+            byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature(
+                    apk.getAbsolutePath());
+            V4Signature signature;
+            if (signatureBytes != null && signatureBytes.length > 0) {
+                needsConsistencyCheck = false;
+                signature = V4Signature.readFrom(signatureBytes);
+            } else if (android.security.Flags.extendVbChainToUpdatedApk()) {
+                // 2. Try fs-verity next. fs-verity checks against the Merkle tree, but the
+                // v4 signature file (including a raw root hash) is managed separately. We need to
+                // ensure the signed data from the file is consistent with the actual file.
+                needsConsistencyCheck = true;
+
+                final File idsig = new File(apk.getAbsolutePath() + V4Signature.EXT);
+                try (var fis = new FileInputStream(idsig.getAbsolutePath())) {
+                    signature = V4Signature.readFrom(fis);
+                } catch (IOException e) {
+                    throw new SignatureNotFoundException(
+                            "Failed to obtain signature bytes from .idsig");
+                }
+            } else {
+                throw new SignatureNotFoundException(
+                        "Failed to obtain signature bytes from IncFS.");
+            }
             if (!signature.isVersionSupported()) {
                 throw new SecurityException(
                         "v4 signature version " + signature.version + " is not supported");
@@ -86,9 +113,26 @@
                     signature.hashingInfo);
             final V4Signature.SigningInfos signingInfos = V4Signature.SigningInfos.fromByteArray(
                     signature.signingInfos);
+
+            if (needsConsistencyCheck) {
+                final byte[] actualDigest = VerityUtils.getFsverityDigest(apk.getAbsolutePath());
+                if (actualDigest == null) {
+                    throw new SecurityException("The APK does not have fs-verity");
+                }
+                final byte[] computedDigest =
+                        VerityUtils.generateFsVerityDigest(apk.length(), hashingInfo);
+                if (!Arrays.equals(computedDigest, actualDigest)) {
+                    throw new SignatureException("Actual digest does not match the v4 signature");
+                }
+            }
+
             return Pair.create(hashingInfo, signingInfos);
+        } catch (EOFException e) {
+            throw new SignatureException("V4 signature is invalid.", e);
         } catch (IOException e) {
             throw new SignatureNotFoundException("Failed to read V4 signature.", e);
+        } catch (DigestException | NoSuchAlgorithmException e) {
+            throw new SecurityException("Failed to calculate the digest", e);
         }
     }
 
@@ -107,7 +151,7 @@
                 signingInfo);
         final Pair<Certificate, byte[]> result = verifySigner(signingInfo, signedData);
 
-        // Populate digests enforced by IncFS driver.
+        // Populate digests enforced by IncFS driver and fs-verity.
         Map<Integer, byte[]> contentDigests = new ArrayMap<>();
         contentDigests.put(convertToContentDigestType(hashingInfo.hashAlgorithm),
                 hashingInfo.rawRootHash);
@@ -217,7 +261,7 @@
         public final byte[] apkDigest;
 
         // Algorithm -> digest map of signed digests in the signature.
-        // These are continuously enforced by the IncFS driver.
+        // These are continuously enforced by the IncFS driver and fs-verity.
         public final Map<Integer, byte[]> contentDigests;
 
         public VerifiedSigner(Certificate[] certs, byte[] apkDigest,
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 74a9d16..7f7ea8b 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -17,8 +17,10 @@
 package com.android.internal.security;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.os.Build;
 import android.os.SystemProperties;
+import android.os.incremental.V4Signature;
 import android.system.Os;
 import android.system.OsConstants;
 import android.util.Slog;
@@ -40,6 +42,9 @@
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
@@ -192,9 +197,9 @@
      *
      * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#file-digest-computation">
      *      File digest computation in Linux kernel documentation</a>
-     * @return Bytes of fs-verity digest
+     * @return Bytes of fs-verity digest, or null if the file does not have fs-verity enabled
      */
-    public static byte[] getFsverityDigest(@NonNull String filePath) {
+    public static @Nullable byte[] getFsverityDigest(@NonNull String filePath) {
         byte[] result = new byte[HASH_SIZE_BYTES];
         int retval = measureFsverityNative(filePath, result);
         if (retval < 0) {
@@ -206,6 +211,34 @@
         return result;
     }
 
+    /**
+     * Generates an fs-verity digest from a V4Signature.HashingInfo and the file's size.
+     */
+    public static @NonNull byte[] generateFsVerityDigest(long fileSize,
+            @NonNull V4Signature.HashingInfo hashingInfo)
+            throws DigestException, NoSuchAlgorithmException {
+        if (hashingInfo.rawRootHash == null || hashingInfo.rawRootHash.length != 32) {
+            throw new IllegalArgumentException("Expect a 32-byte rootHash for SHA256");
+        }
+        if (hashingInfo.log2BlockSize != 12) {
+            throw new IllegalArgumentException(
+                    "Unsupported log2BlockSize: " + hashingInfo.log2BlockSize);
+        }
+
+        var buffer = ByteBuffer.allocate(256);  // sizeof(fsverity_descriptor)
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        buffer.put((byte) 1);                   // version
+        buffer.put((byte) 1);                   // Merkle tree hash algorithm, 1 for SHA256
+        buffer.put(hashingInfo.log2BlockSize);  // log2(block-size), only log2(4096) is supported
+        buffer.put((byte) 0);                   // size of salt in bytes; 0 if none
+        buffer.putInt(0);                       // reserved, must be 0
+        buffer.putLong(fileSize);               // size of file the Merkle tree is built over
+        buffer.put(hashingInfo.rawRootHash);    // Merkle tree root hash
+        // The rest are zeros, including the latter half of root hash unused for SHA256.
+
+        return MessageDigest.getInstance("SHA-256").digest(buffer.array());
+    }
+
     /** @hide */
     @VisibleForTesting
     public static byte[] toFormattedDigest(byte[] digest) {
diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
index a7c986d..ec858ee2 100644
--- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
+++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java
@@ -513,6 +513,7 @@
                 List<String> apkFiles =
                         filesList
                                 .map(path -> path.toAbsolutePath().toString())
+                                .filter(str -> str.endsWith(".apk"))
                                 .collect(Collectors.toList());
                 sourceStampVerificationResult = SourceStampVerifier.verify(apkFiles);
             } catch (IOException e) {
diff --git a/services/core/java/com/android/server/pm/ApkChecksums.java b/services/core/java/com/android/server/pm/ApkChecksums.java
index 5b93244..50ed3b1 100644
--- a/services/core/java/com/android/server/pm/ApkChecksums.java
+++ b/services/core/java/com/android/server/pm/ApkChecksums.java
@@ -655,7 +655,7 @@
             }
         } catch (SignatureNotFoundException e) {
             // Nothing
-        } catch (SecurityException e) {
+        } catch (SignatureException | SecurityException e) {
             Slog.e(TAG, "V4 signature error", e);
         }
         return null;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 2e9da09..3364b67 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -137,6 +137,7 @@
 import android.os.incremental.IncrementalManager;
 import android.os.incremental.PerUidReadTimeouts;
 import android.os.incremental.StorageHealthCheckParams;
+import android.os.incremental.V4Signature;
 import android.os.storage.StorageManager;
 import android.provider.DeviceConfig;
 import android.provider.Settings.Global;
@@ -801,6 +802,7 @@
             // entries like "lost+found".
             if (file.isDirectory()) return false;
             if (file.getName().endsWith(REMOVE_MARKER_EXTENSION)) return false;
+            if (file.getName().endsWith(V4Signature.EXT)) return false;
             if (isAppMetadata(file)) return false;
             if (DexMetadataHelper.isDexMetadataFile(file)) return false;
             if (VerityUtils.isFsveritySignatureFile(file)) return false;
@@ -1506,6 +1508,21 @@
     }
 
     @GuardedBy("mLock")
+    private void enableFsVerityToAddedApksWithIdsig() throws PackageManagerException {
+        try {
+            List<File> files = getAddedApksLocked();
+            for (var file : files) {
+                if (new File(file.getPath() + V4Signature.EXT).exists()) {
+                    VerityUtils.setUpFsverity(file.getPath());
+                }
+            }
+        } catch (IOException e) {
+            throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
+                    "Failed to enable fs-verity to verify with idsig: " + e);
+        }
+    }
+
+    @GuardedBy("mLock")
     private List<ApkLite> getAddedApkLitesLocked() throws PackageManagerException {
         if (!isArchivedInstallation()) {
             List<File> files = getAddedApksLocked();
@@ -3294,6 +3311,14 @@
             }
         }
 
+        // Needs to happen before the first v4 signature verification, which happens in
+        // getAddedApkLitesLocked.
+        if (android.security.Flags.extendVbChainToUpdatedApk()) {
+            if (!isIncrementalInstallation()) {
+                enableFsVerityToAddedApksWithIdsig();
+            }
+        }
+
         final List<ApkLite> addedFiles = getAddedApkLitesLocked();
         if (addedFiles.isEmpty()
                 && (removeSplitList.size() == 0 || getStagedAppMetadataFile() != null)) {
@@ -3657,6 +3682,16 @@
     }
 
     @GuardedBy("mLock")
+    private void maybeStageV4SignatureLocked(File origFile, File targetFile)
+            throws PackageManagerException {
+        final File originalSignature = new File(origFile.getPath() + V4Signature.EXT);
+        if (originalSignature.exists()) {
+            final File stagedSignature = new File(targetFile.getPath() + V4Signature.EXT);
+            stageFileLocked(originalSignature, stagedSignature);
+        }
+    }
+
+    @GuardedBy("mLock")
     private void maybeStageDexMetadataLocked(File origFile, File targetFile)
             throws PackageManagerException {
         final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(origFile);
@@ -3783,6 +3818,10 @@
         // Stage APK's fs-verity signature if present.
         maybeStageFsveritySignatureLocked(origFile, targetFile,
                 isFsVerityRequiredForApk(origFile, targetFile));
+        // Stage APK's v4 signature if present.
+        if (android.security.Flags.extendVbChainToUpdatedApk()) {
+            maybeStageV4SignatureLocked(origFile, targetFile);
+        }
         // Stage dex metadata (.dm) and corresponding fs-verity signature if present.
         maybeStageDexMetadataLocked(origFile, targetFile);
         // Stage checksums (.digests) if present.
@@ -3800,10 +3839,22 @@
     }
 
     @GuardedBy("mLock")
+    private void maybeInheritV4SignatureLocked(File origFile) {
+        // Inherit the v4 signature file if present.
+        final File v4SignatureFile = new File(origFile.getPath() + V4Signature.EXT);
+        if (v4SignatureFile.exists()) {
+            mResolvedInheritedFiles.add(v4SignatureFile);
+        }
+    }
+
+    @GuardedBy("mLock")
     private void inheritFileLocked(File origFile) {
         mResolvedInheritedFiles.add(origFile);
 
         maybeInheritFsveritySignatureLocked(origFile);
+        if (android.security.Flags.extendVbChainToUpdatedApk()) {
+            maybeInheritV4SignatureLocked(origFile);
+        }
 
         // Inherit the dex metadata if present.
         final File dexMetadataFile =