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 =