Merge changes I695409f2,Ie87efba3,I737f0582

* changes:
  Delete dead code
  Enable fs-verity asynchronously on package install
  Move fs-verity signature check from kernel to install
This commit is contained in:
Victor Hsieh
2022-11-22 00:48:49 +00:00
committed by Android (Google) Code Review
4 changed files with 158 additions and 95 deletions

View File

@@ -174,6 +174,7 @@ import com.android.server.pm.pkg.component.ParsedPermission;
import com.android.server.pm.pkg.component.ParsedPermissionGroup;
import com.android.server.pm.pkg.parsing.ParsingPackageUtils;
import com.android.server.rollback.RollbackManagerInternal;
import com.android.server.security.FileIntegrityService;
import com.android.server.utils.WatchedArrayMap;
import com.android.server.utils.WatchedLongSparseArray;
@@ -1836,6 +1837,7 @@ final class InstallPackageHelper {
}
}
var fis = FileIntegrityService.getService();
for (Map.Entry<String, String> entry : fsverityCandidates.entrySet()) {
try {
final String filePath = entry.getKey();
@@ -1843,13 +1845,31 @@ final class InstallPackageHelper {
continue;
}
// Set up fs-verity with optional signature.
final String signaturePath = entry.getValue();
String optionalSignaturePath = null;
if (new File(signaturePath).exists()) {
optionalSignaturePath = signaturePath;
// If signature is provided, enable fs-verity first so that the file can be
// measured for signature check below.
VerityUtils.setUpFsverity(filePath, (byte[]) null);
if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) {
throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
"fs-verity signature does not verify against a known key");
}
} else {
// Without signature, we don't need to access the digest right away and can
// enable fs-verity in background (since this is a blocking call).
new Thread("fsverity-setup") {
@Override public void run() {
try {
VerityUtils.setUpFsverity(filePath, (byte[]) null);
} catch (IOException e) {
// There's nothing we can do if the setup failed. Since fs-verity is
// optional, just ignore the error for now.
Slog.e(TAG, "Failed to enable fs-verity to " + filePath);
}
}
}.start();
}
VerityUtils.setUpFsverity(filePath, optionalSignaturePath);
} catch (IOException e) {
throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
"Failed to enable fs-verity: " + e);

View File

@@ -23,27 +23,37 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.UserHandle;
import android.security.IFileIntegrityService;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.security.VerityUtils;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
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.util.ArrayList;
import java.util.Collection;
/**
* A {@link SystemService} that provides file integrity related operations.
@@ -52,9 +62,19 @@ import java.util.Collection;
public class FileIntegrityService extends SystemService {
private static final String TAG = "FileIntegrityService";
/** The maximum size of signature file. This is just to avoid potential abuse. */
private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
private static CertificateFactory sCertFactory;
private Collection<X509Certificate> mTrustedCertificates = new ArrayList<X509Certificate>();
@GuardedBy("mTrustedCertificates")
private final ArrayList<X509Certificate> mTrustedCertificates =
new ArrayList<X509Certificate>();
/** Gets the instance of the service */
public static FileIntegrityService getService() {
return LocalServices.getService(FileIntegrityService.class);
}
private final IBinder mService = new IFileIntegrityService.Stub() {
@Override
@@ -75,13 +95,23 @@ public class FileIntegrityService extends SystemService {
Slog.w(TAG, "Received a null certificate");
return false;
}
return mTrustedCertificates.contains(toCertificate(certificateBytes));
synchronized (mTrustedCertificates) {
return mTrustedCertificates.contains(toCertificate(certificateBytes));
}
} catch (CertificateException e) {
Slog.e(TAG, "Failed to convert the certificate: " + e);
return false;
}
}
@Override
public void onShellCommand(FileDescriptor in, FileDescriptor out,
FileDescriptor err, String[] args, ShellCallback callback,
ResultReceiver resultReceiver) {
new FileIntegrityServiceShellCommand()
.exec(this, in, out, err, args, callback, resultReceiver);
}
private void checkCallerPermission(String packageName) {
final int callingUid = Binder.getCallingUid();
final int callingUserId = UserHandle.getUserId(callingUid);
@@ -116,6 +146,7 @@ public class FileIntegrityService extends SystemService {
} catch (CertificateException e) {
Slog.wtf(TAG, "Cannot get an instance of X.509 certificate factory");
}
LocalServices.addService(FileIntegrityService.class, this);
}
@Override
@@ -124,6 +155,34 @@ public class FileIntegrityService extends SystemService {
publishBinderService(Context.FILE_INTEGRITY_SERVICE, mService);
}
/**
* Returns whether the signature over the file's fs-verity digest can be verified by one of the
* known certiticates.
*/
public boolean verifyPkcs7DetachedSignature(String signaturePath, String filePath)
throws IOException {
if (Files.size(Paths.get(signaturePath)) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
throw new SecurityException("Signature file is unexpectedly large: "
+ signaturePath);
}
byte[] signatureBytes = Files.readAllBytes(Paths.get(signaturePath));
byte[] digest = VerityUtils.getFsverityDigest(filePath);
synchronized (mTrustedCertificates) {
for (var cert : mTrustedCertificates) {
try {
byte[] derEncoded = cert.getEncoded();
if (VerityUtils.verifyPkcs7DetachedSignature(signatureBytes, digest,
new ByteArrayInputStream(derEncoded))) {
return true;
}
} catch (CertificateEncodingException e) {
Slog.w(TAG, "Ignoring ill-formed certificate: " + e);
}
}
}
return false;
}
private void loadAllCertificates() {
// A better alternative to load certificates would be to read from .fs-verity kernel
// keyring, which fsverity_init loads to during earlier boot time from the same sources
@@ -148,10 +207,6 @@ public class FileIntegrityService extends SystemService {
for (File cert : files) {
byte[] certificateBytes = Files.readAllBytes(cert.toPath());
if (certificateBytes == null) {
Slog.w(TAG, "The certificate file is empty, ignoring " + cert);
continue;
}
collectCertificate(certificateBytes);
}
} catch (IOException e) {
@@ -165,7 +220,9 @@ public class FileIntegrityService extends SystemService {
*/
private void collectCertificate(@NonNull byte[] bytes) {
try {
mTrustedCertificates.add(toCertificate(bytes));
synchronized (mTrustedCertificates) {
mTrustedCertificates.add(toCertificate(bytes));
}
} catch (CertificateException e) {
Slog.e(TAG, "Invalid certificate, ignored: " + e);
}
@@ -184,4 +241,71 @@ public class FileIntegrityService extends SystemService {
}
return (X509Certificate) certificate;
}
private class FileIntegrityServiceShellCommand extends ShellCommand {
@Override
public int onCommand(String cmd) {
if (!Build.IS_DEBUGGABLE) {
return -1;
}
if (cmd == null) {
return handleDefaultCommands(cmd);
}
final PrintWriter pw = getOutPrintWriter();
switch (cmd) {
case "append-cert":
String nextArg = getNextArg();
if (nextArg == null) {
pw.println("Invalid argument");
pw.println("");
onHelp();
return -1;
}
ParcelFileDescriptor pfd = openFileForSystem(nextArg, "r");
if (pfd == null) {
pw.println("Cannot open the file");
return -1;
}
InputStream is = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
try {
collectCertificate(is.readAllBytes());
} catch (IOException e) {
pw.println("Failed to add certificate: " + e);
return -1;
}
pw.println("Certificate is added successfully");
return 0;
case "remove-last-cert":
synchronized (mTrustedCertificates) {
if (mTrustedCertificates.size() == 0) {
pw.println("Certificate list is already empty");
return -1;
}
mTrustedCertificates.remove(mTrustedCertificates.size() - 1);
}
pw.println("Certificate is removed successfully");
return 0;
default:
pw.println("Unknown action");
pw.println("");
onHelp();
}
return -1;
}
@Override
public void onHelp() {
final PrintWriter pw = getOutPrintWriter();
pw.println("File integrity service commands:");
pw.println(" help");
pw.println(" Print this help text.");
pw.println(" append-cert path/to/cert.der");
pw.println(" Add the DER-encoded certificate (only in debug builds)");
pw.println(" remove-last-cert");
pw.println(" Remove the last certificate in the key list (only in debug builds)");
pw.println("");
}
}
}

View File

@@ -25,7 +25,6 @@ import static org.junit.Assert.fail;
import android.platform.test.annotations.RootPermissionTest;
import com.android.blockdevicewriter.BlockDeviceWriter;
import com.android.fsverity.AddFsVerityCertRule;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
@@ -36,7 +35,6 @@ import com.android.tradefed.util.CommandStatus;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -90,10 +88,6 @@ public class ApkVerityTest extends BaseHostJUnit4Test {
/** Only 4K page is supported by fs-verity currently. */
private static final int FSVERITY_PAGE_SIZE = 4096;
@Rule
public final AddFsVerityCertRule mAddFsVerityCertRule =
new AddFsVerityCertRule(this, CERT_PATH);
private ITestDevice mDevice;
private boolean mDmRequireFsVerity;
@@ -103,11 +97,13 @@ public class ApkVerityTest extends BaseHostJUnit4Test {
mDmRequireFsVerity = "true".equals(
mDevice.getProperty("pm.dexopt.dm.require_fsverity"));
expectRemoteCommandToSucceed("cmd file_integrity append-cert " + CERT_PATH);
uninstallPackage(TARGET_PACKAGE);
}
@After
public void tearDown() throws DeviceNotAvailableException {
expectRemoteCommandToSucceed("cmd file_integrity remove-last-cert");
uninstallPackage(TARGET_PACKAGE);
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.fsverity;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import org.junit.rules.ExternalResource;
public final class AddFsVerityCertRule extends ExternalResource {
private static final String APK_VERITY_STANDARD_MODE = "2";
private final BaseHostJUnit4Test mHost;
private final String mCertPath;
private String mKeyId;
public AddFsVerityCertRule(BaseHostJUnit4Test host, String certPath) {
mHost = host;
mCertPath = certPath;
}
@Override
protected void before() throws Throwable {
ITestDevice device = mHost.getDevice();
String apkVerityMode = device.getProperty("ro.apk_verity.mode");
assumeTrue(device.getLaunchApiLevel() >= 30
|| APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
String keyId = executeCommand(
"mini-keyctl padd asymmetric fsv_test .fs-verity < " + mCertPath).trim();
assertThat(keyId).matches("^\\d+$");
mKeyId = keyId;
}
@Override
protected void after() {
if (mKeyId == null) return;
try {
executeCommand("mini-keyctl unlink " + mKeyId + " .fs-verity");
} catch (DeviceNotAvailableException e) {
LogUtil.CLog.e(e);
}
mKeyId = null;
}
private String executeCommand(String cmd) throws DeviceNotAvailableException {
CommandResult result = mHost.getDevice().executeShellV2Command(cmd);
assertWithMessage("`" + cmd + "` failed: " + result.getStderr())
.that(result.getStatus())
.isEqualTo(CommandStatus.SUCCESS);
return result.getStdout();
}
}