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 23dd1b407b00d..a7149cb8be641 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5945,4 +5945,9 @@ false + + + + + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 476d36d0c2078..18d67f11cea9a 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2309,6 +2309,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 326d720927ab5..84c4e9f2dbf24 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; @@ -153,9 +158,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 @@ -173,11 +199,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. @@ -193,22 +223,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; @@ -231,12 +282,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) { @@ -271,10 +322,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(); } } @@ -300,10 +351,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 88145bd9fdd9d..4cd0d6e42121a 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, 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 diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java index cbe13d9aa1492..650686ff3b85d 100644 --- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java +++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java @@ -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();