diff --git a/core/java/android/graphics/fonts/FontManager.java b/core/java/android/graphics/fonts/FontManager.java
index 24480e954a645..beb7f36142c71 100644
--- a/core/java/android/graphics/fonts/FontManager.java
+++ b/core/java/android/graphics/fonts/FontManager.java
@@ -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;
}
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f244ce412d5e7..83d6816d174d6 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5775,4 +5775,8 @@
- "com.android.phone"
+
+
+
+
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 596d89af99a64..bc2191e4bf206 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2273,6 +2273,7 @@
+
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index ae9c64b480068..ad27c45df6d46 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -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;
@@ -154,9 +159,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
@@ -174,11 +200,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.
@@ -194,22 +224,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;
@@ -232,12 +283,12 @@ public final class FontManagerService extends IFontManager.Stub {
/* package */ void update(int baseVersion, List 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) {
@@ -272,10 +323,10 @@ public final class FontManagerService extends IFontManager.Stub {
}
/* package */ Map getFontFileMap() {
- if (mUpdatableFontDir == null) {
- return Collections.emptyMap();
- }
synchronized (mUpdatableFontDirLock) {
+ if (mUpdatableFontDir == null) {
+ return Collections.emptyMap();
+ }
return mUpdatableFontDir.getPostScriptMap();
}
}
@@ -301,10 +352,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();
}
}
diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
index 3fecef703613b..94783449b0d56 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
@@ -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);
}
diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
index 743b4d90dd738..457d5b7afe846 100644
--- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
+++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java
@@ -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);
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index f9b8373cc79e2..9672085b8f3ae 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -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