Merge "Check signature of updated font during boot"
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5775,4 +5775,8 @@
|
||||
<string-array name="config_serviceStateLocationAllowedPackages">
|
||||
<item>"com.android.phone"</item>
|
||||
</string-array>
|
||||
|
||||
<!-- List of certificate to be used for font fs-verity integrity verification -->
|
||||
<string-array translatable="false" name="config_fontManagerServiceCerts">
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -2273,6 +2273,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" />
|
||||
|
||||
@@ -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<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) {
|
||||
@@ -272,10 +323,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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user