Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 760f1e4f authored by Victor Hsieh's avatar Victor Hsieh
Browse files

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
parent 8c6d8c87
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -254,7 +254,10 @@ public class V4Signature {
        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);
+55 −11
Original line number Diff line number Diff line
@@ -27,9 +27,14 @@ import android.os.incremental.V4Signature;
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 @@ public class ApkSignatureSchemeV4Verifier {
     * 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 @@ public class ApkSignatureSchemeV4Verifier {
     * Extracts APK Signature Scheme v4 signature of the provided APK.
     */
    public static Pair<V4Signature.HashingInfo, V4Signature.SigningInfos> extractSignature(
            String apkFile) throws SignatureNotFoundException {
            String apkFile) throws SignatureNotFoundException, SignatureException {
        try {
            final File apk = new File(apkFile);
        final byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature(
            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());
        if (signatureBytes == null || signatureBytes.length == 0) {
            throw new SignatureNotFoundException("Failed to obtain signature bytes from IncFS.");
            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.");
            }
        try {
            final V4Signature signature = V4Signature.readFrom(signatureBytes);
            if (!signature.isVersionSupported()) {
                throw new SecurityException(
                        "v4 signature version " + signature.version + " is not supported");
@@ -86,9 +113,26 @@ public class ApkSignatureSchemeV4Verifier {
                    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 @@ public class ApkSignatureSchemeV4Verifier {
                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 class ApkSignatureSchemeV4Verifier {
        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,
+35 −2
Original line number Diff line number Diff line
@@ -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.io.InputStream;
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 @@ public abstract class VerityUtils {
     *
     * @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 @@ public abstract class VerityUtils {
        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) {
+1 −0
Original line number Diff line number Diff line
@@ -513,6 +513,7 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub {
                List<String> apkFiles =
                        filesList
                                .map(path -> path.toAbsolutePath().toString())
                                .filter(str -> str.endsWith(".apk"))
                                .collect(Collectors.toList());
                sourceStampVerificationResult = SourceStampVerifier.verify(apkFiles);
            } catch (IOException e) {
+1 −1
Original line number Diff line number Diff line
@@ -655,7 +655,7 @@ public class ApkChecksums {
            }
        } catch (SignatureNotFoundException e) {
            // Nothing
        } catch (SecurityException e) {
        } catch (SignatureException | SecurityException e) {
            Slog.e(TAG, "V4 signature error", e);
        }
        return null;
Loading