Merge "Check signature of updated font during boot"

This commit is contained in:
Treehugger Robot
2022-12-07 18:26:33 +00:00
committed by Gerrit Code Review
8 changed files with 201 additions and 28 deletions

View File

@@ -198,6 +198,15 @@ public class FontManager {
*/ */
public static final int RESULT_ERROR_INVALID_XML = -10007; 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) { private FontManager(@NonNull IFontManager iFontManager) {
mIFontManager = iFontManager; mIFontManager = iFontManager;
} }

View File

@@ -5775,4 +5775,8 @@
<string-array name="config_serviceStateLocationAllowedPackages"> <string-array name="config_serviceStateLocationAllowedPackages">
<item>"com.android.phone"</item> <item>"com.android.phone"</item>
</string-array> </string-array>
<!-- List of certificate to be used for font fs-verity integrity verification -->
<string-array translatable="false" name="config_fontManagerServiceCerts">
</string-array>
</resources> </resources>

View File

@@ -2273,6 +2273,7 @@
<java-symbol type="id" name="media_actions" /> <java-symbol type="id" name="media_actions" />
<java-symbol type="dimen" name="config_mediaMetadataBitmapMaxSize" /> <java-symbol type="dimen" name="config_mediaMetadataBitmapMaxSize" />
<java-symbol type="array" name="config_fontManagerServiceCerts" />
<!-- From SystemUI --> <!-- From SystemUI -->
<java-symbol type="anim" name="push_down_in" /> <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.FontManager;
import android.graphics.fonts.FontUpdateRequest; import android.graphics.fonts.FontUpdateRequest;
import android.graphics.fonts.SystemFonts; import android.graphics.fonts.SystemFonts;
import android.os.Build;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver; import android.os.ResultReceiver;
import android.os.SharedMemory; import android.os.SharedMemory;
@@ -35,8 +36,10 @@ import android.text.FontConfig;
import android.util.AndroidException; import android.util.AndroidException;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.IndentingPrintWriter; import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Slog; import android.util.Slog;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.GuardedBy;
import com.android.internal.graphics.fonts.IFontManager; import com.android.internal.graphics.fonts.IFontManager;
import com.android.internal.security.VerityUtils; import com.android.internal.security.VerityUtils;
@@ -47,7 +50,9 @@ import com.android.server.SystemService;
import java.io.File; import java.io.File;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.DirectByteBuffer; import java.nio.DirectByteBuffer;
@@ -154,9 +159,30 @@ public final class FontManagerService extends IFontManager.Stub {
} }
private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil { private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
private final String[] mDerCertPaths;
FsverityUtilImpl(String[] derCertPaths) {
mDerCertPaths = derCertPaths;
}
@Override @Override
public boolean hasFsverity(String filePath) { public boolean isFromTrustedProvider(String fontPath, byte[] pkcs7Signature) {
return VerityUtils.hasFsverity(filePath); 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 @Override
@@ -174,11 +200,15 @@ public final class FontManagerService extends IFontManager.Stub {
@NonNull @NonNull
private final Context mContext; private final Context mContext;
private final boolean mIsSafeMode;
private final Object mUpdatableFontDirLock = new Object(); private final Object mUpdatableFontDirLock = new Object();
private String mDebugCertFilePath = null;
@GuardedBy("mUpdatableFontDirLock") @GuardedBy("mUpdatableFontDirLock")
@Nullable @Nullable
private final UpdatableFontDir mUpdatableFontDir; private UpdatableFontDir mUpdatableFontDir;
// mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock. // mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock.
// mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock. // mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock.
@@ -194,22 +224,43 @@ public final class FontManagerService extends IFontManager.Stub {
UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
} }
mContext = context; mContext = context;
mUpdatableFontDir = createUpdatableFontDir(safeMode); mIsSafeMode = safeMode;
initialize(); initialize();
} }
@Nullable @Nullable
private static UpdatableFontDir createUpdatableFontDir(boolean safeMode) { private UpdatableFontDir createUpdatableFontDir() {
// Never read updatable font files in safe mode. // 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 apk verity is supported, fs-verity should be available.
if (!VerityUtils.isFsVeritySupported()) return null; 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(), 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() { private void initialize() {
synchronized (mUpdatableFontDirLock) { synchronized (mUpdatableFontDirLock) {
mUpdatableFontDir = createUpdatableFontDir();
if (mUpdatableFontDir == null) { if (mUpdatableFontDir == null) {
setSerializedFontMap(serializeSystemServerFontMap()); setSerializedFontMap(serializeSystemServerFontMap());
return; return;
@@ -232,12 +283,12 @@ public final class FontManagerService extends IFontManager.Stub {
/* package */ void update(int baseVersion, List<FontUpdateRequest> requests) /* package */ void update(int baseVersion, List<FontUpdateRequest> requests)
throws SystemFontException { throws SystemFontException {
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) { if (mUpdatableFontDir == null) {
throw new SystemFontException( throw new SystemFontException(
FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED, FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
"The font updater is disabled."); "The font updater is disabled.");
} }
synchronized (mUpdatableFontDirLock) {
// baseVersion == -1 only happens from shell command. This is filtered and treated as // baseVersion == -1 only happens from shell command. This is filtered and treated as
// error from SystemApi call. // error from SystemApi call.
if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) { if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) {
@@ -272,10 +323,10 @@ public final class FontManagerService extends IFontManager.Stub {
} }
/* package */ Map<String, File> getFontFileMap() { /* package */ Map<String, File> getFontFileMap() {
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) { if (mUpdatableFontDir == null) {
return Collections.emptyMap(); return Collections.emptyMap();
} }
synchronized (mUpdatableFontDirLock) {
return mUpdatableFontDir.getPostScriptMap(); return mUpdatableFontDir.getPostScriptMap();
} }
} }
@@ -301,10 +352,10 @@ public final class FontManagerService extends IFontManager.Stub {
* Returns an active system font configuration. * Returns an active system font configuration.
*/ */
public @NonNull FontConfig getSystemFontConfig() { public @NonNull FontConfig getSystemFontConfig() {
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) { if (mUpdatableFontDir == null) {
return SystemFonts.getSystemPreinstalledFontConfig(); return SystemFonts.getSystemPreinstalledFontConfig();
} }
synchronized (mUpdatableFontDirLock) {
return mUpdatableFontDir.getSystemFontConfig(); return mUpdatableFontDir.getSystemFontConfig();
} }
} }

View File

@@ -28,6 +28,7 @@ import android.graphics.fonts.FontUpdateRequest;
import android.graphics.fonts.FontVariationAxis; import android.graphics.fonts.FontVariationAxis;
import android.graphics.fonts.SystemFonts; import android.graphics.fonts.SystemFonts;
import android.os.Binder; import android.os.Binder;
import android.os.Build;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.Process; import android.os.Process;
import android.os.ShellCommand; import android.os.ShellCommand;
@@ -103,6 +104,10 @@ public class FontManagerShellCommand extends ShellCommand {
w.println("update-family [family definition XML path]"); w.println("update-family [family definition XML path]");
w.println(" Update font families with the new definitions."); w.println(" Update font families with the new definitions.");
w.println(); 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("clear");
w.println(" Remove all installed font files and reset to the initial state."); w.println(" Remove all installed font files and reset to the initial state.");
w.println(); w.println();
@@ -322,6 +327,33 @@ public class FontManagerShellCommand extends ShellCommand {
return 0; 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 { private int update(ShellCommand shell) throws SystemFontException {
String fontPath = shell.getNextArg(); String fontPath = shell.getNextArg();
if (fontPath == null) { if (fontPath == null) {
@@ -494,6 +526,8 @@ public class FontManagerShellCommand extends ShellCommand {
return restart(shell); return restart(shell);
case "status": case "status":
return status(shell); return status(shell);
case "install-debug-cert":
return installCert(shell);
default: default:
return shell.handleDefaultCommands(cmd); return shell.handleDefaultCommands(cmd);
} }

View File

@@ -40,6 +40,8 @@ import java.io.FileDescriptor;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@@ -59,6 +61,8 @@ final class UpdatableFontDir {
private static final String TAG = "UpdatableFontDir"; private static final String TAG = "UpdatableFontDir";
private static final String RANDOM_DIR_PREFIX = "~~"; 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 to mock font file access in tests. */
interface FontFileParser { interface FontFileParser {
String getPostScriptName(File file) throws IOException; String getPostScriptName(File file) throws IOException;
@@ -72,7 +76,7 @@ final class UpdatableFontDir {
/** Interface to mock fs-verity in tests. */ /** Interface to mock fs-verity in tests. */
interface FsverityUtil { interface FsverityUtil {
boolean hasFsverity(String path); boolean isFromTrustedProvider(String path, byte[] pkcs7Signature);
void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException; void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;
@@ -188,12 +192,35 @@ final class UpdatableFontDir {
FileUtils.deleteContentsAndDir(dir); FileUtils.deleteContentsAndDir(dir);
continue; 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(); File[] files = dir.listFiles();
if (files == null || files.length != 1) { if (files == null || files.length != 2) {
Slog.e(TAG, "Unexpected files in dir: " + dir); Slog.e(TAG, "Unexpected files in dir: " + dir);
return; 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) { if (fontConfig == null) {
fontConfig = getSystemFontConfig(); fontConfig = getSystemFontConfig();
} }
@@ -359,9 +386,25 @@ final class UpdatableFontDir {
} catch (ErrnoException e) { } catch (ErrnoException e) {
throw new SystemFontException( throw new SystemFontException(
FontManager.RESULT_ERROR_FAILED_TO_WRITE_FONT_FILE, 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 to create Typeface and treat as failure something goes wrong.
try { try {
@@ -478,8 +521,9 @@ final class UpdatableFontDir {
* is higher than the currently used font. * is higher than the currently used font.
*/ */
@NonNull @NonNull
private FontFileInfo validateFontFile(File file) throws SystemFontException { private FontFileInfo validateFontFile(File file, byte[] pkcs7Signature)
if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) { throws SystemFontException {
if (!mFsverityUtil.isFromTrustedProvider(file.getAbsolutePath(), pkcs7Signature)) {
throw new SystemFontException( throw new SystemFontException(
FontManager.RESULT_ERROR_VERIFICATION_FAILURE, FontManager.RESULT_ERROR_VERIFICATION_FAILURE,
"Font validation failed. Fs-verity is not enabled: " + file); "Font validation failed. Fs-verity is not enabled: " + file);

View File

@@ -108,7 +108,7 @@ public final class UpdatableFontDirTest {
} }
@Override @Override
public boolean hasFsverity(String path) { public boolean isFromTrustedProvider(String path, byte[] signature) {
return mHasFsverityPaths.contains(path); return mHasFsverityPaths.contains(path);
} }
@@ -290,6 +290,32 @@ public final class UpdatableFontDirTest {
assertThat(dir.getFontFamilyMap()).isEmpty(); 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 @Test
public void construct_olderThanPreinstalledFont() throws Exception { public void construct_olderThanPreinstalledFont() throws Exception {
Function<Map<String, File>, FontConfig> configSupplier = (map) -> { Function<Map<String, File>, FontConfig> configSupplier = (map) -> {
@@ -782,8 +808,8 @@ public final class UpdatableFontDirTest {
UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() { UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() {
@Override @Override
public boolean hasFsverity(String path) { public boolean isFromTrustedProvider(String path, byte[] signature) {
return mFakeFsverityUtil.hasFsverity(path); return mFakeFsverityUtil.isFromTrustedProvider(path, signature);
} }
@Override @Override

View File

@@ -373,6 +373,10 @@ public class UpdatableSystemFontTest {
try (InputStream is = new FileInputStream(certPath)) { try (InputStream is = new FileInputStream(certPath)) {
result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is); 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. // Assert that there are no errors.
assertThat(result.second).isEmpty(); assertThat(result.second).isEmpty();
String keyId = result.first.trim(); String keyId = result.first.trim();