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()