Merge "Check signature of updated font during boot"

This commit is contained in:
TreeHugger Robot
2022-11-06 23:59:40 +00:00
committed by Android (Google) Code Review
8 changed files with 202 additions and 28 deletions

View File

@@ -198,6 +198,15 @@ public class FontManager {
*/
public static final int RESULT_ERROR_INVALID_XML = -10007;
/**
* Indicates a failure due to invalid debug certificate file.
*
* This error code is only used with the shell command interaction.
*
* @hide
*/
public static final int RESULT_ERROR_INVALID_DEBUG_CERTIFICATE = -10008;
private FontManager(@NonNull IFontManager iFontManager) {
mIFontManager = iFontManager;
}

View File

@@ -5951,4 +5951,9 @@
<!-- Whether the lock screen is allowed to run its own live wallpaper,
different from the home screen wallpaper. -->
<bool name="config_independentLockscreenLiveWallpaper">false</bool>
<!-- List of certificate to be used for font fs-verity integrity verification -->
<string-array translatable="false" name="config_fontManagerServiceCerts">
</string-array>
</resources>

View File

@@ -2309,6 +2309,7 @@
<java-symbol type="id" name="media_actions" />
<java-symbol type="dimen" name="config_mediaMetadataBitmapMaxSize" />
<java-symbol type="array" name="config_fontManagerServiceCerts" />
<!-- From SystemUI -->
<java-symbol type="anim" name="push_down_in" />

View File

@@ -26,6 +26,7 @@ import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontManager;
import android.graphics.fonts.FontUpdateRequest;
import android.graphics.fonts.SystemFonts;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver;
import android.os.SharedMemory;
@@ -35,8 +36,10 @@ import android.text.FontConfig;
import android.util.AndroidException;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.graphics.fonts.IFontManager;
import com.android.internal.security.VerityUtils;
@@ -47,7 +50,9 @@ import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.DirectByteBuffer;
@@ -155,9 +160,30 @@ public final class FontManagerService extends IFontManager.Stub {
}
private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
private final String[] mDerCertPaths;
FsverityUtilImpl(String[] derCertPaths) {
mDerCertPaths = derCertPaths;
}
@Override
public boolean hasFsverity(String filePath) {
return VerityUtils.hasFsverity(filePath);
public boolean isFromTrustedProvider(String fontPath, byte[] pkcs7Signature) {
final byte[] digest = VerityUtils.getFsverityDigest(fontPath);
if (digest == null) {
Log.w(TAG, "Failed to get fs-verity digest for " + fontPath);
return false;
}
for (String certPath : mDerCertPaths) {
try (InputStream is = new FileInputStream(certPath)) {
if (VerityUtils.verifyPkcs7DetachedSignature(pkcs7Signature, digest, is)) {
return true;
}
} catch (IOException e) {
Log.w(TAG, "Failed to read certificate file: " + certPath);
}
}
return false;
}
@Override
@@ -175,11 +201,15 @@ public final class FontManagerService extends IFontManager.Stub {
@NonNull
private final Context mContext;
private final boolean mIsSafeMode;
private final Object mUpdatableFontDirLock = new Object();
private String mDebugCertFilePath = null;
@GuardedBy("mUpdatableFontDirLock")
@Nullable
private final UpdatableFontDir mUpdatableFontDir;
private UpdatableFontDir mUpdatableFontDir;
// mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock.
// mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock.
@@ -195,22 +225,43 @@ public final class FontManagerService extends IFontManager.Stub {
UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
}
mContext = context;
mUpdatableFontDir = createUpdatableFontDir(safeMode);
mIsSafeMode = safeMode;
initialize();
}
@Nullable
private static UpdatableFontDir createUpdatableFontDir(boolean safeMode) {
private UpdatableFontDir createUpdatableFontDir() {
// Never read updatable font files in safe mode.
if (safeMode) return null;
if (mIsSafeMode) return null;
// If apk verity is supported, fs-verity should be available.
if (!VerityUtils.isFsVeritySupported()) return null;
String[] certs = mContext.getResources().getStringArray(
R.array.config_fontManagerServiceCerts);
if (mDebugCertFilePath != null && (Build.IS_USERDEBUG || Build.IS_ENG)) {
String[] tmp = new String[certs.length + 1];
System.arraycopy(certs, 0, tmp, 0, certs.length);
tmp[certs.length] = mDebugCertFilePath;
certs = tmp;
}
return new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser(),
new FsverityUtilImpl(), new File(CONFIG_XML_FILE));
new FsverityUtilImpl(certs), new File(CONFIG_XML_FILE));
}
/**
* Add debug certificate to the cert list. This must be called only on userdebug/eng
* build.
* @param debugCertPath a debug certificate file path
*/
public void addDebugCertificate(@Nullable String debugCertPath) {
mDebugCertFilePath = debugCertPath;
}
private void initialize() {
synchronized (mUpdatableFontDirLock) {
mUpdatableFontDir = createUpdatableFontDir();
if (mUpdatableFontDir == null) {
setSerializedFontMap(serializeSystemServerFontMap());
return;
@@ -233,12 +284,12 @@ public final class FontManagerService extends IFontManager.Stub {
/* package */ void update(int baseVersion, List<FontUpdateRequest> requests)
throws SystemFontException {
if (mUpdatableFontDir == null) {
throw new SystemFontException(
FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
"The font updater is disabled.");
}
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) {
throw new SystemFontException(
FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
"The font updater is disabled.");
}
// baseVersion == -1 only happens from shell command. This is filtered and treated as
// error from SystemApi call.
if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) {
@@ -273,10 +324,10 @@ public final class FontManagerService extends IFontManager.Stub {
}
/* package */ Map<String, File> getFontFileMap() {
if (mUpdatableFontDir == null) {
return Collections.emptyMap();
}
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) {
return Collections.emptyMap();
}
return mUpdatableFontDir.getPostScriptMap();
}
}
@@ -302,10 +353,10 @@ public final class FontManagerService extends IFontManager.Stub {
* Returns an active system font configuration.
*/
public @NonNull FontConfig getSystemFontConfig() {
if (mUpdatableFontDir == null) {
return SystemFonts.getSystemPreinstalledFontConfig();
}
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) {
return SystemFonts.getSystemPreinstalledFontConfig();
}
return mUpdatableFontDir.getSystemFontConfig();
}
}

View File

@@ -28,6 +28,7 @@ import android.graphics.fonts.FontUpdateRequest;
import android.graphics.fonts.FontVariationAxis;
import android.graphics.fonts.SystemFonts;
import android.os.Binder;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.ShellCommand;
@@ -103,6 +104,10 @@ public class FontManagerShellCommand extends ShellCommand {
w.println("update-family [family definition XML path]");
w.println(" Update font families with the new definitions.");
w.println();
w.println("install-debug-cert [cert file path]");
w.println(" Install debug certificate file. This command can be used only on userdebug");
w.println(" or eng device with root user.");
w.println();
w.println("clear");
w.println(" Remove all installed font files and reset to the initial state.");
w.println();
@@ -322,6 +327,33 @@ public class FontManagerShellCommand extends ShellCommand {
return 0;
}
private int installCert(ShellCommand shell) throws SystemFontException {
if (!(Build.IS_USERDEBUG || Build.IS_ENG)) {
throw new SecurityException("Only userdebug/eng device can add debug certificate");
}
if (Binder.getCallingUid() != Process.ROOT_UID) {
throw new SecurityException("Only root can add debug certificate");
}
String certPath = shell.getNextArg();
if (certPath == null) {
throw new SystemFontException(
FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
"Cert file path argument is required.");
}
File file = new File(certPath);
if (!file.isFile()) {
throw new SystemFontException(
FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
"Cert file (" + file + ") is not found");
}
mService.addDebugCertificate(certPath);
mService.restart();
shell.getOutPrintWriter().println("Success");
return 0;
}
private int update(ShellCommand shell) throws SystemFontException {
String fontPath = shell.getNextArg();
if (fontPath == null) {
@@ -494,6 +526,8 @@ public class FontManagerShellCommand extends ShellCommand {
return restart(shell);
case "status":
return status(shell);
case "install-debug-cert":
return installCert(shell);
default:
return shell.handleDefaultCommands(cmd);
}

View File

@@ -40,6 +40,8 @@ import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
@@ -59,6 +61,8 @@ final class UpdatableFontDir {
private static final String TAG = "UpdatableFontDir";
private static final String RANDOM_DIR_PREFIX = "~~";
private static final String FONT_SIGNATURE_FILE = "font.fsv_sig";
/** Interface to mock font file access in tests. */
interface FontFileParser {
String getPostScriptName(File file) throws IOException;
@@ -72,7 +76,7 @@ final class UpdatableFontDir {
/** Interface to mock fs-verity in tests. */
interface FsverityUtil {
boolean hasFsverity(String path);
boolean isFromTrustedProvider(String path, byte[] pkcs7Signature);
void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;
@@ -188,12 +192,35 @@ final class UpdatableFontDir {
FileUtils.deleteContentsAndDir(dir);
continue;
}
File signatureFile = new File(dir, FONT_SIGNATURE_FILE);
if (!signatureFile.exists()) {
Slog.i(TAG, "The signature file is missing.");
FileUtils.deleteContentsAndDir(dir);
continue;
}
byte[] signature;
try {
signature = Files.readAllBytes(Paths.get(signatureFile.getAbsolutePath()));
} catch (IOException e) {
Slog.e(TAG, "Failed to read signature file.");
return;
}
File[] files = dir.listFiles();
if (files == null || files.length != 1) {
if (files == null || files.length != 2) {
Slog.e(TAG, "Unexpected files in dir: " + dir);
return;
}
FontFileInfo fontFileInfo = validateFontFile(files[0]);
File fontFile;
if (files[0].equals(signatureFile)) {
fontFile = files[1];
} else {
fontFile = files[0];
}
FontFileInfo fontFileInfo = validateFontFile(fontFile, signature);
if (fontConfig == null) {
fontConfig = getSystemFontConfig();
}
@@ -359,9 +386,25 @@ final class UpdatableFontDir {
} catch (ErrnoException e) {
throw new SystemFontException(
FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
"Failed to change mode to 711", e);
"Failed to change font file mode to 644", e);
}
FontFileInfo fontFileInfo = validateFontFile(newFontFile);
File signatureFile = new File(newDir, FONT_SIGNATURE_FILE);
try (FileOutputStream out = new FileOutputStream(signatureFile)) {
out.write(pkcs7Signature);
} catch (IOException e) {
// TODO: Do we need new error code for signature write failure?
throw new SystemFontException(
FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
"Failed to write font signature file to storage.", e);
}
try {
Os.chmod(signatureFile.getAbsolutePath(), 0600);
} catch (ErrnoException e) {
throw new SystemFontException(
FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE,
"Failed to change the signature file mode to 600", e);
}
FontFileInfo fontFileInfo = validateFontFile(newFontFile, pkcs7Signature);
// Try to create Typeface and treat as failure something goes wrong.
try {
@@ -478,8 +521,9 @@ final class UpdatableFontDir {
* is higher than the currently used font.
*/
@NonNull
private FontFileInfo validateFontFile(File file) throws SystemFontException {
if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) {
private FontFileInfo validateFontFile(File file, byte[] pkcs7Signature)
throws SystemFontException {
if (!mFsverityUtil.isFromTrustedProvider(file.getAbsolutePath(), pkcs7Signature)) {
throw new SystemFontException(
FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
"Font validation failed. Fs-verity is not enabled: " + file);

View File

@@ -108,7 +108,7 @@ public final class UpdatableFontDirTest {
}
@Override
public boolean hasFsverity(String path) {
public boolean isFromTrustedProvider(String path, byte[] signature) {
return mHasFsverityPaths.contains(path);
}
@@ -290,6 +290,32 @@ public final class UpdatableFontDirTest {
assertThat(dir.getFontFamilyMap()).isEmpty();
}
@Test
public void construct_missingSignatureFile() throws Exception {
UpdatableFontDir dirForPreparation = new UpdatableFontDir(
mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
dirForPreparation.loadFontFileMap();
dirForPreparation.update(Arrays.asList(
newFontUpdateRequest("foo.ttf,1,foo", GOOD_SIGNATURE)));
assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
// Remove signature file next to the font file.
File fontDir = dirForPreparation.getPostScriptMap().get("foo");
File sigFile = new File(fontDir.getParentFile(), "font.fsv_sig");
assertThat(sigFile.exists()).isTrue();
sigFile.delete();
UpdatableFontDir dir = new UpdatableFontDir(
mUpdatableFontFilesDir, mParser, mFakeFsverityUtil,
mConfigFile, mCurrentTimeSupplier, mConfigSupplier);
dir.loadFontFileMap();
// The font file should be removed and should not be loaded.
assertThat(dir.getPostScriptMap()).isEmpty();
assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
assertThat(dir.getFontFamilyMap()).isEmpty();
}
@Test
public void construct_olderThanPreinstalledFont() throws Exception {
Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
@@ -782,8 +808,8 @@ public final class UpdatableFontDirTest {
UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() {
@Override
public boolean hasFsverity(String path) {
return mFakeFsverityUtil.hasFsverity(path);
public boolean isFromTrustedProvider(String path, byte[] signature) {
return mFakeFsverityUtil.isFromTrustedProvider(path, signature);
}
@Override

View File

@@ -373,6 +373,10 @@ public class UpdatableSystemFontTest {
try (InputStream is = new FileInputStream(certPath)) {
result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
}
// /data/local/tmp is not readable by system server. Copy a cert file to /data/fonts
final String copiedCert = "/data/fonts/debug_cert.der";
runShellCommand("cp " + certPath + " " + copiedCert, null);
runShellCommand("cmd font install-debug-cert " + copiedCert, null);
// Assert that there are no errors.
assertThat(result.second).isEmpty();
String keyId = result.first.trim();