Integration test for updatable system font.

This test:
(1) signs a font file with the test key.
(2) side-loads the test cert to the device under test.
(3) verifies that the signed font file can be installed.

The device must be rootable for doing step (2).

Bug: 176939176
Test: atest UpdatableSystemFontTest
Change-Id: I7a9b614aa3c77589c3495b663cb76056ba657006
diff --git a/Android.bp b/Android.bp
index d170913..a99f3e5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1294,6 +1294,18 @@
     ],
 }
 
+python_binary_host {
+    name: "update_font_metadata",
+    defaults: ["base_default"],
+    main: "tools/fonts/update_font_metadata.py",
+    srcs: [
+        "tools/fonts/update_font_metadata.py",
+    ],
+    libs: [
+        "fontTools",
+    ],
+}
+
 filegroup {
     name: "framework-media-annotation-srcs",
     srcs: [
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 d2111e7..2029f39 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java
@@ -169,8 +169,9 @@
             sb.append(c++);
             sb.append("]: lang=\"");
             sb.append(family.getLocaleList().toLanguageTags());
+            sb.append("\"");
             if (family.getVariant() != FontConfig.FontFamily.VARIANT_DEFAULT) {
-                sb.append("\", variant=");
+                sb.append(", variant=");
                 switch (family.getVariant()) {
                     case FontConfig.FontFamily.VARIANT_COMPACT:
                         sb.append("Compact");
diff --git a/tests/ApkVerityTest/Android.bp b/tests/ApkVerityTest/Android.bp
index 02c75ed..39dc9c2 100644
--- a/tests/ApkVerityTest/Android.bp
+++ b/tests/ApkVerityTest/Android.bp
@@ -16,6 +16,7 @@
     name: "ApkVerityTest",
     srcs: ["src/**/*.java"],
     libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"],
+    static_libs: ["frameworks-base-hostutils"],
     test_suites: ["general-tests", "vts"],
     target_required: [
         "block_device_writer_module",
diff --git a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
index 629b6c7..d0eb9be 100644
--- a/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
+++ b/tests/ApkVerityTest/src/com/android/apkverity/ApkVerityTest.java
@@ -21,10 +21,10 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.junit.Assume.assumeTrue;
 
 import android.platform.test.annotations.RootPermissionTest;
 
+import com.android.fsverity.AddFsVerityCertRule;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.log.LogUtil.CLog;
@@ -35,6 +35,7 @@
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -85,40 +86,25 @@
     private static final String DAMAGING_EXECUTABLE = "/data/local/tmp/block_device_writer";
     private static final String CERT_PATH = "/data/local/tmp/ApkVerityTestCert.der";
 
-    private static final String APK_VERITY_STANDARD_MODE = "2";
-
     /** Only 4K page is supported by fs-verity currently. */
     private static final int FSVERITY_PAGE_SIZE = 4096;
 
+    @Rule
+    public final AddFsVerityCertRule mAddFsVerityCertRule =
+            new AddFsVerityCertRule(this, CERT_PATH);
+
     private ITestDevice mDevice;
-    private String mKeyId;
 
     @Before
     public void setUp() throws DeviceNotAvailableException {
         mDevice = getDevice();
 
-        String apkVerityMode = mDevice.getProperty("ro.apk_verity.mode");
-        assumeTrue(mDevice.getLaunchApiLevel() >= 30
-                || APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
-
-        mKeyId = expectRemoteCommandToSucceed(
-                "mini-keyctl padd asymmetric fsv_test .fs-verity < " + CERT_PATH).trim();
-        if (!mKeyId.matches("^\\d+$")) {
-            String keyId = mKeyId;
-            mKeyId = null;
-            fail("Key ID is not decimal: " + keyId);
-        }
-
         uninstallPackage(TARGET_PACKAGE);
     }
 
     @After
     public void tearDown() throws DeviceNotAvailableException {
         uninstallPackage(TARGET_PACKAGE);
-
-        if (mKeyId != null) {
-            expectRemoteCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
-        }
     }
 
     @Test
diff --git a/tests/UpdatableSystemFontTest/Android.bp b/tests/UpdatableSystemFontTest/Android.bp
new file mode 100644
index 0000000..d809fe8
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/Android.bp
@@ -0,0 +1,30 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_test_host {
+    name: "UpdatableSystemFontTest",
+    srcs: ["src/**/*.java"],
+    libs: ["tradefed", "compatibility-tradefed", "compatibility-host-util"],
+    static_libs: ["frameworks-base-hostutils"],
+    test_suites: ["general-tests", "vts"],
+    data: [
+        ":NotoColorEmojiTtf",
+        ":UpdatableSystemFontTestCertDer",
+        ":UpdatableSystemFontTestNotoColorEmojiTtfFsvSig",
+        ":UpdatableSystemFontTestNotoColorEmojiV1Ttf",
+        ":UpdatableSystemFontTestNotoColorEmojiV1TtfFsvSig",
+        ":UpdatableSystemFontTestNotoColorEmojiV2Ttf",
+        ":UpdatableSystemFontTestNotoColorEmojiV2TtfFsvSig",
+    ],
+}
diff --git a/tests/UpdatableSystemFontTest/AndroidTest.xml b/tests/UpdatableSystemFontTest/AndroidTest.xml
new file mode 100644
index 0000000..efe5d70
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Updatable system font integration/regression test">
+    <option name="test-suite-tag" value="apct" />
+
+    <!-- This test requires root to side load fs-verity cert. -->
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="UpdatableSystemFontTestCert.der->/data/local/tmp/UpdatableSystemFontTestCert.der" />
+        <option name="push" value="NotoColorEmoji.ttf->/data/local/tmp/NotoColorEmoji.ttf" />
+        <option name="push" value="UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig" />
+        <option name="push" value="UpdatableSystemFontTestNotoColorEmojiV1.ttf->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV1.ttf" />
+        <option name="push" value="UpdatableSystemFontTestNotoColorEmojiV1.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV1.ttf.fsv_sig" />
+        <option name="push" value="UpdatableSystemFontTestNotoColorEmojiV2.ttf->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV2.ttf" />
+        <option name="push" value="UpdatableSystemFontTestNotoColorEmojiV2.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV2.ttf.fsv_sig" />
+    </target_preparer>
+
+    <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
+        <option name="jar" value="UpdatableSystemFontTest.jar" />
+    </test>
+</configuration>
diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
new file mode 100644
index 0000000..6d161a5
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.updatablesystemfont;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.RootPermissionTest;
+
+import com.android.fsverity.AddFsVerityCertRule;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tests if fonts can be updated by 'cmd font'.
+ */
+@RootPermissionTest
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
+
+    private static final String CERT_PATH = "/data/local/tmp/UpdatableSystemFontTestCert.der";
+
+    private static final Pattern PATTERN_FONT = Pattern.compile("path = ([^, \n]*)");
+    private static final String NOTO_COLOR_EMOJI_TTF = "NotoColorEmoji.ttf";
+    private static final String TEST_NOTO_COLOR_EMOJI_V1_TTF =
+            "/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV1.ttf";
+    private static final String TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG =
+            "/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV1.ttf.fsv_sig";
+    private static final String TEST_NOTO_COLOR_EMOJI_V2_TTF =
+            "/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV2.ttf";
+    private static final String TEST_NOTO_COLOR_EMOJI_V2_TTF_FSV_SIG =
+            "/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiV2.ttf.fsv_sig";
+    private static final String ORIGINAL_NOTO_COLOR_EMOJI_TTF =
+            "/data/local/tmp/NotoColorEmoji.ttf";
+    private static final String ORIGINAL_NOTO_COLOR_EMOJI_TTF_FSV_SIG =
+            "/data/local/tmp/UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig";
+
+    @Rule
+    public final AddFsVerityCertRule mAddFsverityCertRule =
+            new AddFsVerityCertRule(this, CERT_PATH);
+
+    @Before
+    public void setUp() throws Exception {
+        expectRemoteCommandToSucceed("cmd font clear");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        expectRemoteCommandToSucceed("cmd font clear");
+    }
+
+    @Test
+    public void updateFont() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath).startsWith("/data/fonts/files/");
+    }
+
+    @Test
+    public void updateFont_twice() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V2_TTF, TEST_NOTO_COLOR_EMOJI_V2_TTF_FSV_SIG));
+        String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath2).startsWith("/data/fonts/files/");
+        assertThat(fontPath2).isNotEqualTo(fontPath);
+    }
+
+    @Test
+    public void updatedFont_dataFileIsImmutableAndReadable() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+        String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
+        assertThat(fontPath).startsWith("/data");
+
+        expectRemoteCommandToFail("echo -n '' >> " + fontPath);
+        expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null");
+    }
+
+    @Test
+    public void updateFont_invalidCert() throws Exception {
+        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V2_TTF_FSV_SIG));
+    }
+
+    @Test
+    public void updateFont_downgradeFromSystem() throws Exception {
+        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+                ORIGINAL_NOTO_COLOR_EMOJI_TTF, ORIGINAL_NOTO_COLOR_EMOJI_TTF_FSV_SIG));
+    }
+
+    @Test
+    public void updateFont_downgradeFromData() throws Exception {
+        expectRemoteCommandToSucceed(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V2_TTF, TEST_NOTO_COLOR_EMOJI_V2_TTF_FSV_SIG));
+        expectRemoteCommandToFail(String.format("cmd font update %s %s",
+                TEST_NOTO_COLOR_EMOJI_V1_TTF, TEST_NOTO_COLOR_EMOJI_V1_TTF_FSV_SIG));
+    }
+
+    private String getFontPath(String fontFileName) throws Exception {
+        // TODO: add a dedicated command for testing.
+        String lines = expectRemoteCommandToSucceed("cmd font dump");
+        for (String line : lines.split("\n")) {
+            Matcher m = PATTERN_FONT.matcher(line);
+            if (m.find() && m.group(1).endsWith(fontFileName)) {
+                return m.group(1);
+            }
+        }
+        CLog.e("Font not found: " + fontFileName);
+        return null;
+    }
+
+    private String expectRemoteCommandToSucceed(String cmd) throws Exception {
+        CommandResult result = getDevice().executeShellV2Command(cmd);
+        assertWithMessage("`" + cmd + "` failed: " + result.getStderr())
+                .that(result.getStatus())
+                .isEqualTo(CommandStatus.SUCCESS);
+        return result.getStdout();
+    }
+
+    private void expectRemoteCommandToFail(String cmd) throws Exception {
+        CommandResult result = getDevice().executeShellV2Command(cmd);
+        assertWithMessage("Unexpected success from `" + cmd + "`: " + result.getStderr())
+                .that(result.getStatus())
+                .isNotEqualTo(CommandStatus.SUCCESS);
+    }
+}
diff --git a/tests/UpdatableSystemFontTest/testdata/Android.bp b/tests/UpdatableSystemFontTest/testdata/Android.bp
new file mode 100644
index 0000000..1296699
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/testdata/Android.bp
@@ -0,0 +1,82 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+filegroup {
+    name: "UpdatableSystemFontTestKeyPem",
+    srcs: ["UpdatableSystemFontTestKey.pem"],
+}
+
+filegroup {
+    name: "UpdatableSystemFontTestCertPem",
+    srcs: ["UpdatableSystemFontTestCert.pem"],
+}
+
+filegroup {
+    name: "UpdatableSystemFontTestCertDer",
+    srcs: ["UpdatableSystemFontTestCert.der"],
+}
+
+genrule_defaults {
+    name: "updatable_system_font_increment_font_revision_default",
+    tools: ["update_font_metadata"],
+    cmd: "$(location update_font_metadata) " +
+        "--input=$(in) " +
+        "--output=$(out) " +
+        "--revision=+1",
+}
+
+genrule {
+    name: "UpdatableSystemFontTestNotoColorEmojiV1Ttf",
+    defaults: ["updatable_system_font_increment_font_revision_default"],
+    srcs: [":NotoColorEmojiTtf"],
+    out: ["UpdatableSystemFontTestNotoColorEmojiV1.ttf"],
+}
+
+genrule {
+    name: "UpdatableSystemFontTestNotoColorEmojiV2Ttf",
+    defaults: ["updatable_system_font_increment_font_revision_default"],
+    srcs: [":UpdatableSystemFontTestNotoColorEmojiV1Ttf"],
+    out: ["UpdatableSystemFontTestNotoColorEmojiV2.ttf"],
+}
+
+genrule_defaults {
+    name: "updatable_system_font_sig_gen_default",
+    tools: ["fsverity"],
+    tool_files: [":UpdatableSystemFontTestKeyPem", ":UpdatableSystemFontTestCertPem"],
+    cmd: "$(location fsverity) sign $(in) $(out) " +
+        "--key=$(location :UpdatableSystemFontTestKeyPem) " +
+        "--cert=$(location :UpdatableSystemFontTestCertPem) " +
+        "> /dev/null",
+}
+
+genrule {
+    name: "UpdatableSystemFontTestNotoColorEmojiTtfFsvSig",
+    defaults: ["updatable_system_font_sig_gen_default"],
+    srcs: [":NotoColorEmojiTtf"],
+    out: ["UpdatableSystemFontTestNotoColorEmoji.ttf.fsv_sig"],
+}
+
+genrule {
+    name: "UpdatableSystemFontTestNotoColorEmojiV1TtfFsvSig",
+    defaults: ["updatable_system_font_sig_gen_default"],
+    srcs: [":UpdatableSystemFontTestNotoColorEmojiV1Ttf"],
+    out: ["UpdatableSystemFontTestNotoColorEmojiV1.ttf.fsv_sig"],
+}
+
+genrule {
+    name: "UpdatableSystemFontTestNotoColorEmojiV2TtfFsvSig",
+    defaults: ["updatable_system_font_sig_gen_default"],
+    srcs: [":UpdatableSystemFontTestNotoColorEmojiV2Ttf"],
+    out: ["UpdatableSystemFontTestNotoColorEmojiV2.ttf.fsv_sig"],
+}
diff --git a/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.der b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.der
new file mode 100644
index 0000000..f7aa15f
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.der
Binary files differ
diff --git a/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.pem b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.pem
new file mode 100644
index 0000000..0cd1f66
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestCert.pem
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIIFOTCCAyGgAwIBAgIUFaI1D5NtwkCVM3G4bFZ6sQSb598wDQYJKoZIhvcNAQEL
+BQAwLDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRAwDgYDVQQKDAdBbmRyb2lk
+MB4XDTIxMDIwMTA3MzAyNFoXDTIxMDMwMzA3MzAyNFowLDELMAkGA1UEBhMCVVMx
+CzAJBgNVBAgMAkNBMRAwDgYDVQQKDAdBbmRyb2lkMIICIjANBgkqhkiG9w0BAQEF
+AAOCAg8AMIICCgKCAgEA0U1zptc41E65ooeBPD33Mjgp6cYPydyj2Acq80Xy7lP7
+d6/2t6w7nNNl2x3n8dAhOl3de3IxTp6JI2SdoRb7obfcp+hWJoo/cxnHpr3q/u4R
+KED0rmWaOVHpGbajSTFZgN+cTbTKJbgtXm/H65x1QfO18ep/vj5fRiu1xPpJqDv/
+xuvuko2U3eC2+NayxzCWXVFrKPLx8GvzSQ3Utaug17vs7/5GqkRJgq3lk4DvmjNA
+vY8YA4RAkII1sSaceAWFEG6ztENLu2kjcxAI9qHxxBwQZit/NtFVlFGqSN3MEYjS
+M9Fz04RsUxF672QJpAgwCJDZ41rdB3hkHvOUK9PcepBsHdZq9cQ+E64+TX+jsJLu
+VouViKlYr6WYjvhfqZeRhwbj7CoEZ2DyEZKrl27fgWaidUT5LGEQLVxg90ymbimI
+6UwXRUwmRBQJBdRO4RGvngtqxRuamyjAKDDHx5YccXCX4FWLUypyQlz0asojvbJZ
+Og7DFa1qsRdGrGIRoQJ8pYnAjBJfSudr1l0mR7fZSfZc0W9ZmuROWx9Ip7aJWQnQ
+8JLtbNPuFLD2qbmg9Y1lcXJp1FvI9FcM8JsBqZNEANQwwsdTaa8gw+3W6J2SXKQP
+H+yZI/fJWWWRFADtqmpxtvXK9K+Cy1HQmg7D0IIxVPp8rrbz6TGrg+R3/N9FBWMC
+AwEAAaNTMFEwHQYDVR0OBBYEFDS48o2UAstoOyLMcgamHKrZdl3cMB8GA1UdIwQY
+MBaAFDS48o2UAstoOyLMcgamHKrZdl3cMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
+hvcNAQELBQADggIBAF7taBYAe20tWZu0pY9d4Z8il4LoJcRrKF4YiA02UizErgCF
+h4iECy6+pcu7DJUfvCh3dCWE7CDG+OnfUWTwEHVG9n8XI/ydetBUG76PZwTadI7B
+gzJ1y7/vWqJo5U6ki+sXNmq3hkgNsNZgza3LpdovkWJYeRdffM6m/bimzwYx9id8
+5mKw2PcbVZcb25r+0dCoLVJsqqCoRjdYUy/MKPutWG2bPzmaIv8KsKFN+mzlwhJH
+lpJ/LR+3NoaHrOCFG7CW/2Ihe501vmdQ2m/VKosyk0igw8WmTsY6xMbw2t77yKkD
+hnJr1NbhKeEV9gAB2BFX8nRWI7NTgp8fG78YLVz1UcbIHmYLgFoc3ezyma+CoR86
+ER20lKd4+TNnz4RtaPdZlBa0Ba3bsMtEneqlrHvcPrZ5tgGsQR9+cy3ZtTZ/LUQX
++Xuj/EoJXuuB3hkhg52zawN5n7WUe8efWHcv1jHqeIj0phcgbZ6u4fFBPsYjzDKe
+VuYHXglNOchmoBQwEaJI/TCiEgI8dcSJXSquLAXrtznVnxzT46ZMEt5LaW1/1NLx
+q//yoPdolCI0lpunh5jvIZJpUl5XMjxVSyaveQDNVqJkITWzWqIxAT5yTLtkCNlW
+c1XyzeHkpMItiJtBruExmnaTmNjlVKsXP8wQFOYbDGgXY5iHIMbgovptRyH/
+-----END CERTIFICATE-----
diff --git a/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestKey.pem b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestKey.pem
new file mode 100644
index 0000000..09bb104
--- /dev/null
+++ b/tests/UpdatableSystemFontTest/testdata/UpdatableSystemFontTestKey.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDRTXOm1zjUTrmi
+h4E8PfcyOCnpxg/J3KPYByrzRfLuU/t3r/a3rDuc02XbHefx0CE6Xd17cjFOnokj
+ZJ2hFvuht9yn6FYmij9zGcemver+7hEoQPSuZZo5UekZtqNJMVmA35xNtMoluC1e
+b8frnHVB87Xx6n++Pl9GK7XE+kmoO//G6+6SjZTd4Lb41rLHMJZdUWso8vHwa/NJ
+DdS1q6DXu+zv/kaqREmCreWTgO+aM0C9jxgDhECQgjWxJpx4BYUQbrO0Q0u7aSNz
+EAj2ofHEHBBmK3820VWUUapI3cwRiNIz0XPThGxTEXrvZAmkCDAIkNnjWt0HeGQe
+85Qr09x6kGwd1mr1xD4Trj5Nf6Owku5Wi5WIqVivpZiO+F+pl5GHBuPsKgRnYPIR
+kquXbt+BZqJ1RPksYRAtXGD3TKZuKYjpTBdFTCZEFAkF1E7hEa+eC2rFG5qbKMAo
+MMfHlhxxcJfgVYtTKnJCXPRqyiO9slk6DsMVrWqxF0asYhGhAnylicCMEl9K52vW
+XSZHt9lJ9lzRb1ma5E5bH0intolZCdDwku1s0+4UsPapuaD1jWVxcmnUW8j0Vwzw
+mwGpk0QA1DDCx1NpryDD7dbonZJcpA8f7Jkj98lZZZEUAO2qanG29cr0r4LLUdCa
+DsPQgjFU+nyutvPpMauD5Hf830UFYwIDAQABAoICAQCTVTcFCdl6MdSg4UwK0P/S
+fRCb/A0fJs67Agis6N9h/wI0NUyx7G6mLXU0si+U29KYGH0RKcgltJmKrYf8XoZR
+R3DvTTBfvs99QXd2G5hxTboMIPVcUi8nDE7PB+6XVkLP4hhP5uSpeqWNJZiQdTlh
+bKH2IgE8NQGyDpDMkPcKkvmw2GG/DiTtrwJ91fxRFRWzqN2LHMFMYWEHWtIR9Der
+xSC7q72om5s3fxvtIkUHwe5fwXvA9fbRAqezBR/9qL0LXTHowbpsuUz38SCuJD9g
+sfSlRxcsyly4pGf/FQpSiYKWcWlcSopKSzLDkyLqMc1GKlkGnu6aFJg95W63D1LS
+OaOXuYShHxLkqyhT8uQGRqDCu3E2ivb6fMxAPzJxxs3JrZvumNsqyxbp+HVF0idj
+NijMN/8Kb4KmNHG9I3SHG61tQFYDtxoMMNiHzq3fafBJnVcf6iThQdE5pGLN2OdF
+3rcSTeHI2HxhTrXtuiHmWXNk9aZ2TOhrssNZZjDkFL/KYh6G8guy+tn66YWy77VC
+id+6PBzYXTXsUauo4NWW1rLUfzT/y93IwVGpoXs7GHUUduZ6Q3PxsMTMF9IjBvqR
+JvfP84CUfGXoebVJmWyGhtW6N5ParvQxbonDitA0TPZcRyX3N8yqIGO/mb8MWA9M
+7s/xMZuksOpw5LSCoTAW6QKCAQEA9Z1WM/5XT+5HkwPBvS5aRz0SqMqPxAkZ0dNz
+O06zpXv6e2IPy40UzFWCIkyq3vWKQ5bqU1fnejUdmjvtnP+KhH6fxnQCgiunnrDq
+j1Sk2Y4gb1KyZY/C8IejOexM2qX7sfDTLI8XEvxJxVFCNmvYfnv4AX5QD8VOsjrg
+bvodLAgDSo2FVDP+mkpW7zAIoRV02l6QdZA+YcG940eqxB9sPR3/1KUHe9wUTTbJ
+FV5ahEPuXCyvRJkZ/rD5CPPZoQHfjKHxDlu7yfoatzCSYj10R+RInfqOFGVY4D/C
+2csXjymwTN4CFUnJcP410YhPFn5ekmc/E3xqPKgIDQhQ2+oivwKCAQEA2icN1iEo
+YuBJwB3pX3jrwk+1bpUXASueWhyAhSeNMrTrJ/lSgEAy9FBxJNnK1PjkABRnFhS+
+uxbC2hQdfAkNDS21PQOk6hOhebUVuBdfmKY1CL+P9y4Af0fjV1rgRGHihnZB5lKU
+1R/wFb8c4QgPwduiqDoZ5QP3dgxpZ4R3SCzuoyVelUSlgyWPoslg+yPddcnV8bTf
+BUlEOOyXgVudkSFlRpWZ+/ZAkLTj8rnrtKp8/+GtTQvJvPJHPfuSsDhI+EQVWqab
+HelMhxvIi9xOyN/OCc3Ex6JlEVa+X8EyNhQsV2sAKnld92hdEozW2uxRniIkxvL4
+CuBp/p3fWxEaXQKCAQEAw2j7RYCMnNZZ8Zhiko4HW3g2mT4XpYMMHMlbe4sBGJ8L
+yRBaurqzGmLJl1ph8+NsrpuqMMbWLn+F3sjhIjCZVxKbMbvopwHuaS4eYAya30/Z
+dFhaAL2g/dccQSBEgQzftFGC4YeydvNsCeW9hSjGZNNinGWPcwyqsNhw6Tpq7TYu
+0CjKNBTt8nlEsyYHJ4m3n2jvC+nIB+Spm+LP9Rt+9R0iBl+KFbwiFtCIqUyZPXQC
+dylCBJS+fsj0SXAg7J1d6ziIXcEUJfyrNqYZQLneAriYIcBPO+DqFfgEoVyYkNk9
+H9rd02wSLajCzsLhEWdW/KnSIEGzEDErvpqoIl8kZwKCAQEAjNO3T+sZyjKmCXqF
+xBcogsi4BAoEzsGcuOk7Yjn1Ia2/PI/r3VUUT7l6QOLD2JZPgWmqXovH0LjR0rw3
+iHHDViWSoS+wD1fa3tmyiqO0F7P7+ojHZDbzJTeAIE1PB3X1KP5AbnITGD5E25UD
+DJYKrgeeSmEvhDL6Vd+PT78ozZQL/Y/LLishebcOsXS0wYsWlMpV7XHoot34R5Mb
+/urond7kJRvASvJeHcxYdsHk0j1Y8kp6eIlKk0oICZBU0qOTH4m8C0gQTM/lkjay
+UO9IgM5RkOyfwowoGHhZ7zClvFlrgodVlRXCPkvGAYqfzLXPvnimKzSAQW07n53E
+qWIyFQKCAQAWRNG6hPCOzkNxMJ/RK7dwxZW6b4a1L3PMXTT/xrBKRIS1WNjbpkYO
+/FLIufOqJT6FQN2obM5uso3TI+R7MwH8DnTSnDDy0Hvs3CdHdtn2tapZOViF/UVv
+uCQa+/jMVKFCZ8k7pPFMIG6tB6WBA5MmJrW+8s0ouxLbRF5rZyzqOeyPdVBYYYDb
+68nGNA6GAtTQs9h7xV2tsQ1bXNP+6gqG3BgZYo+76xKITddT6s9aaC++LVBnPOdq
+LHV7gUvoBkVLjIp4L3Fb/DGMCcOVMCxmFlRBn+RBlV7slehvgq+Ywz2GHWLr+O/z
+V2NAtwvCfZE2Do/4f2mpHnamhS6AvrDe
+-----END PRIVATE KEY-----
diff --git a/tests/utils/hostutils/src/com/android/fsverity/AddFsVerityCertRule.java b/tests/utils/hostutils/src/com/android/fsverity/AddFsVerityCertRule.java
new file mode 100644
index 0000000..5ab4dc6
--- /dev/null
+++ b/tests/utils/hostutils/src/com/android/fsverity/AddFsVerityCertRule.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.fsverity;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+
+import org.junit.rules.ExternalResource;
+
+public final class AddFsVerityCertRule extends ExternalResource {
+
+    private static final String APK_VERITY_STANDARD_MODE = "2";
+
+    private final BaseHostJUnit4Test mHost;
+    private final String mCertPath;
+    private String mKeyId;
+
+    public AddFsVerityCertRule(BaseHostJUnit4Test host, String certPath) {
+        mHost = host;
+        mCertPath = certPath;
+    }
+
+    @Override
+    protected void before() throws Throwable {
+        ITestDevice device = mHost.getDevice();
+        String apkVerityMode = device.getProperty("ro.apk_verity.mode");
+        assumeTrue(device.getLaunchApiLevel() >= 30
+                || APK_VERITY_STANDARD_MODE.equals(apkVerityMode));
+
+        String keyId = executeCommand(
+                "mini-keyctl padd asymmetric fsv_test .fs-verity < " + mCertPath).trim();
+        assertThat(keyId).matches("^\\d+$");
+        mKeyId = keyId;
+    }
+
+    @Override
+    protected void after() {
+        if (mKeyId == null) return;
+        try {
+            executeCommand("mini-keyctl unlink " + mKeyId + " .fs-verity");
+        } catch (DeviceNotAvailableException e) {
+            LogUtil.CLog.e(e);
+        }
+        mKeyId = null;
+    }
+
+    private String executeCommand(String cmd) throws DeviceNotAvailableException {
+        CommandResult result = mHost.getDevice().executeShellV2Command(cmd);
+        assertWithMessage("`" + cmd + "` failed: " + result.getStderr())
+                .that(result.getStatus())
+                .isEqualTo(CommandStatus.SUCCESS);
+        return result.getStdout();
+    }
+}
diff --git a/tools/fonts/update_font_metadata.py b/tools/fonts/update_font_metadata.py
new file mode 100755
index 0000000..c07a98a
--- /dev/null
+++ b/tools/fonts/update_font_metadata.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+import argparse
+
+from fontTools import ttLib
+
+
+def update_font_revision(font, revisionSpec):
+    if revisionSpec.startswith('+'):
+      font['head'].fontRevision += float(revisionSpec[1:])
+    else:
+      font['head'].fontRevision = float(revisionSpec)
+
+
+def main():
+    args_parser = argparse.ArgumentParser(description='Update font file metadata')
+    args_parser.add_argument('--input', help='Input otf/ttf font file.')
+    args_parser.add_argument('--output', help='Output file for updated font file.')
+    args_parser.add_argument('--revision', help='Updated font revision. Use + to update revision based on the current revision')
+    args = args_parser.parse_args()
+
+    font = ttLib.TTFont(args.input)
+    update_font_revision(font, args.revision)
+    font.save(args.output)
+
+if __name__ == "__main__":
+    main()