apkmanifest: parse fields related to relaxed rollback protection scheme

In case a Microdroid pVM wants to opt in a relaxed rollback protection
scheme it needs to have the following things defined in its manifest:
* <uses-permission USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION>
* set android.system.virtualmachine.ROLLBACK_INDEX <property>

In case only one of the two things is defined, the VM won't boot. This
is enforced by microdroid_manager (see changes to the verify.rs).

In the follow-up patch these new fields will be used to create a new
more relaxed sealing policy.

Bug: 378681279
Test: atest MicrodroidTests
Change-Id: Iabd12fd47f0eb271f021d5ad466de4f6c0669f2b
diff --git a/guest/microdroid_manager/src/verify.rs b/guest/microdroid_manager/src/verify.rs
index e5d26fc..ec8d66e 100644
--- a/guest/microdroid_manager/src/verify.rs
+++ b/guest/microdroid_manager/src/verify.rs
@@ -16,7 +16,7 @@
 use crate::payload::{get_apex_data_from_payload, to_metadata};
 use crate::MicrodroidError;
 use anyhow::{anyhow, ensure, Context, Result};
-use apkmanifest::get_manifest_info;
+use apkmanifest::{get_manifest_info, ApkManifestInfo};
 use apkverify::{extract_signed_data, verify, V4Signature};
 use glob::glob;
 use itertools::sorted;
@@ -174,6 +174,14 @@
     })
 }
 
+fn validate_manifest_info(info: &ApkManifestInfo) -> Result<()> {
+    ensure!(
+        info.has_relaxed_rollback_protection_permission == info.rollback_index.is_some(),
+        MicrodroidError::PayloadVerificationFailed(String::from("to opt in relaxed rollback protection scheme manifest must request android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION permission and set the android.system.virtualmachine.ROLLBACK_INDEX property"))
+    );
+    Ok(())
+}
+
 fn get_data_from_apk(
     apk_path: &str,
     root_hash: Box<[u8]>,
@@ -188,6 +196,8 @@
         .map_err(|e| warn!("Failed to read manifest info from APK: {e:?}"))
         .unwrap_or_default();
 
+    validate_manifest_info(&manifest_info)?;
+
     Ok(ApkData {
         root_hash: root_hash.into(),
         cert_hash,
diff --git a/libs/apkmanifest/native/apkmanifest.cpp b/libs/apkmanifest/native/apkmanifest.cpp
index ab0ba72..f424bd8 100644
--- a/libs/apkmanifest/native/apkmanifest.cpp
+++ b/libs/apkmanifest/native/apkmanifest.cpp
@@ -28,6 +28,7 @@
 
 #include <cstdlib>
 #include <limits>
+#include <optional>
 #include <string>
 #include <string_view>
 
@@ -49,6 +50,8 @@
     std::string package;
     uint32_t version_code;
     uint32_t version_code_major;
+    std::optional<uint32_t> rollback_index;
+    bool has_relaxed_rollback_protection_permission;
 };
 
 namespace {
@@ -58,6 +61,15 @@
 constexpr u16string_view PACKAGE_ATTRIBUTE_NAME{u"package"};
 constexpr u16string_view VERSION_CODE_ATTRIBUTE_NAME{u"versionCode"};
 constexpr u16string_view VERSION_CODE_MAJOR_ATTRIBUTE_NAME{u"versionCodeMajor"};
+constexpr u16string_view USES_PERMISSION_TAG_NAME{u"uses-permission"};
+// This name is awkward, but i don't have a better idea ¯\_(ツ)_/¯.
+constexpr u16string_view NAME_ATTRIBUTE_NAME{u"name"};
+constexpr u16string_view VALUE_ATTRIBUTE_NAME{u"value"};
+constexpr u16string_view PROPERTY_TAG_NAME{u"property"};
+constexpr u16string_view ROLLBACK_INDEX_PROPERTY_NAME{
+        u"android.system.virtualmachine.ROLLBACK_INDEX"};
+constexpr u16string_view USE_RELAXED_ROLLBACK_PROTECTION_PERMISSION_NAME{
+        u"android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION"};
 
 // Read through the XML parse tree up to the <manifest> element.
 Result<void> findManifestElement(ResXMLTree& tree) {
@@ -135,11 +147,99 @@
     }
 }
 
-// Parse the binary manifest and extract the information we care about.
-// Everything we're interested in should be an attribute on the <manifest> tag.
-// We don't care what order they come in, absent attributes will be treated as
-// the default value, and any unknown attributes (including ones not in the
-// expected namespace) will be ignored.
+// Returns true if the given perm_tag contains the
+// `USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION` permission.
+bool isRelaxedRollbackProtectionPermission(const ResXMLTree& perm_tag) {
+    size_t count = perm_tag.getAttributeCount();
+
+    for (size_t i = 0; i < count; i++) {
+        size_t len = 0;
+        const char16_t* chars = perm_tag.getAttributeNamespace(i, &len);
+        auto namespaceUrl = chars ? u16string_view(chars, len) : u16string_view();
+
+        chars = perm_tag.getAttributeName(i, &len);
+        auto attributeName = chars ? u16string_view(chars, len) : u16string_view();
+
+        if (namespaceUrl != ANDROID_NAMESPACE_URL) {
+            continue;
+        }
+
+        if (attributeName != NAME_ATTRIBUTE_NAME) {
+            continue;
+        }
+
+        chars = perm_tag.getAttributeStringValue(i, &len);
+        if (!chars) {
+            LOG(WARNING) << "expected name attribute to be non-empty";
+            continue;
+        }
+
+        // What a name!
+        auto nameName = u16string_view(chars, len);
+        if (nameName == USE_RELAXED_ROLLBACK_PROTECTION_PERMISSION_NAME) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
+// Returns value of the `android.system.virtualmachine.ROLLBACK_INDEX` property or std::nullopt if
+// given prop_tag doesn't represent the rollback_index property.
+std::optional<uint32_t> getRollbackIndexValue(const ResXMLTree& prop_tag) {
+    size_t count = prop_tag.getAttributeCount();
+    bool is_rollback_index_prop = false;
+
+    // Note: in theory the `android:value` attribute can come before `android:name` one, so we need
+    // to iterate over all attributes twice.
+    for (size_t it = 0; it < 2 * count; it++) {
+        size_t i = it >= count ? it - count : it;
+
+        size_t len = 0;
+        const char16_t* chars = prop_tag.getAttributeNamespace(i, &len);
+        auto namespaceUrl = chars ? u16string_view(chars, len) : u16string_view();
+
+        chars = prop_tag.getAttributeName(i, &len);
+        auto attributeName = chars ? u16string_view(chars, len) : u16string_view();
+
+        if (namespaceUrl != ANDROID_NAMESPACE_URL) {
+            continue;
+        }
+
+        if (attributeName == NAME_ATTRIBUTE_NAME) {
+            chars = prop_tag.getAttributeStringValue(i, &len);
+            if (!chars) {
+                LOG(WARNING) << "expected name attribute to be non-empty";
+                continue;
+            }
+
+            // What a name!
+            auto nameName = u16string_view(chars, len);
+            if (nameName != ROLLBACK_INDEX_PROPERTY_NAME) {
+                return std::nullopt;
+            }
+            is_rollback_index_prop = true;
+        } else if (attributeName == VALUE_ATTRIBUTE_NAME) {
+            if (!is_rollback_index_prop) {
+                // We don't know yet if this is the right property. Skip for now.
+                continue;
+            }
+            auto value = getU32Attribute(prop_tag, i);
+            if (!value.ok()) {
+                LOG(ERROR) << "Failed to parse value of the rollback index : " << value.error();
+                return std::nullopt;
+            }
+            return std::make_optional(std::move(*value));
+        }
+    }
+
+    return std::nullopt;
+}
+
+// Parse the binary manifest and extract the information we care about. Everything we're interested
+// in should be an attribute on the <manifest> tag. We don't care what order they come in, absent
+// attributes will be treated as the default value, and any unknown attributes (including ones not
+// in the expected namespace) will be ignored.
 Result<unique_ptr<ApkManifestInfo>> parseManifest(const void* manifest, size_t size) {
     ResXMLTree tree;
     auto status = tree.setTo(manifest, size);
@@ -181,8 +281,66 @@
         }
     }
 
+    info->has_relaxed_rollback_protection_permission = false;
+
+    // Now we need to parse the rest of the manifest to check if it contains the
+    // `USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION` permission and the
+    // `android.system.virtualmachine.ROLLBACK_INDEX` property.
+    for (;;) {
+        ResXMLParser::event_code_t event = tree.next();
+        switch (event) {
+            case ResXMLParser::END_DOCUMENT:
+                return info;
+            case ResXMLParser::BAD_DOCUMENT:
+                return Error() << "Failed to parse XML: " << statusToString(tree.getError());
+            case ResXMLParser::START_TAG: {
+                size_t len = 0;
+                const char16_t* chars = tree.getElementName(&len);
+                if (!chars) {
+                    return Error() << "Missing tag name";
+                }
+                auto tag_name = u16string_view(chars, len);
+                if (tag_name != USES_PERMISSION_TAG_NAME && tag_name != PROPERTY_TAG_NAME) {
+                    // We are only interested in <uses-permission> and <property> tags.
+                    break;
+                }
+
+                if (tag_name == USES_PERMISSION_TAG_NAME) {
+                    if (isRelaxedRollbackProtectionPermission(tree)) {
+                        info->has_relaxed_rollback_protection_permission = true;
+                    }
+                } else if (tag_name == PROPERTY_TAG_NAME) {
+                    auto rollback_index = getRollbackIndexValue(tree);
+                    if (rollback_index.has_value()) {
+                        LOG(INFO) << "found rollback_index : " << *rollback_index;
+                        if (info->rollback_index.has_value()) {
+                            LOG(WARNING)
+                                    << "found duplicate rollback index, overriding previous value";
+                        }
+                        info->rollback_index.emplace(*rollback_index);
+                    }
+                } else {
+                    break;
+                }
+
+                break;
+            }
+            case ResXMLParser::START_NAMESPACE:
+                break;
+            case ResXMLParser::END_NAMESPACE:
+                break;
+            case ResXMLParser::END_TAG:
+                break;
+            default: {
+                LOG(ERROR) << "found unexpected event : " << event;
+                continue;
+            }
+        }
+    }
+
     return info;
 }
+
 } // namespace
 
 const ApkManifestInfo* extractManifestInfo(const void* manifest, size_t size) {
@@ -205,3 +363,11 @@
 uint64_t getVersionCode(const ApkManifestInfo* info) {
     return info->version_code | (static_cast<uint64_t>(info->version_code_major) << 32);
 }
+
+const uint32_t* getRollbackIndex(const ApkManifestInfo* info) {
+    return info->rollback_index.has_value() ? &info->rollback_index.value() : nullptr;
+}
+
+bool hasRelaxedRollbackProtectionPermission(const ApkManifestInfo* info) {
+    return info->has_relaxed_rollback_protection_permission;
+}
diff --git a/libs/apkmanifest/native/apkmanifest.hpp b/libs/apkmanifest/native/apkmanifest.hpp
index 352912e..f86d5d9 100644
--- a/libs/apkmanifest/native/apkmanifest.hpp
+++ b/libs/apkmanifest/native/apkmanifest.hpp
@@ -41,4 +41,13 @@
 
 // Given a valid ApkManifestInfo pointer, return the version code of the APK.
 uint64_t getVersionCode(const ApkManifestInfo* info);
+
+// Given a valid ApkManifestInfo pointer, return the value of the rollback
+// index. If rollback index is not defined in the AndroidManifest.xml returns
+// nullptr.
+const uint32_t* getRollbackIndex(const ApkManifestInfo* info);
+
+// Give a valid ApkManifestInfo pointer, return the value of the
+// has_relaxed_rollback_protection_permission.
+bool hasRelaxedRollbackProtectionPermission(const ApkManifestInfo* info);
 }
diff --git a/libs/apkmanifest/src/apkmanifest.rs b/libs/apkmanifest/src/apkmanifest.rs
index b92aa74..9ba899d 100644
--- a/libs/apkmanifest/src/apkmanifest.rs
+++ b/libs/apkmanifest/src/apkmanifest.rs
@@ -22,7 +22,10 @@
 //! things).
 
 use anyhow::{bail, Context, Result};
-use apkmanifest_bindgen::{extractManifestInfo, freeManifestInfo, getPackageName, getVersionCode};
+use apkmanifest_bindgen::{
+    extractManifestInfo, freeManifestInfo, getPackageName, getRollbackIndex, getVersionCode,
+    hasRelaxedRollbackProtectionPermission,
+};
 use std::ffi::CStr;
 use std::fs::File;
 use std::path::Path;
@@ -34,6 +37,12 @@
     pub package: String,
     /// The version code of the app.
     pub version_code: u64,
+    /// Rollback index of the app used in the sealing dice policy.
+    /// This is only set if the apk manifest has USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION
+    /// permission.
+    pub rollback_index: Option<u32>,
+    /// Whether manifest has USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION permission.
+    pub has_relaxed_rollback_protection_permission: bool,
 }
 
 const ANDROID_MANIFEST: &str = "AndroidManifest.xml";
@@ -66,5 +75,24 @@
     // Safety: It is always safe to call this with a valid native_info, which we have.
     let version_code = unsafe { getVersionCode(native_info) };
 
-    Ok(ApkManifestInfo { package, version_code })
+    // Safety: It is always safe to call this with a valid native_info, which we have.
+    let rollback_index = unsafe {
+        let rollback_index = getRollbackIndex(native_info);
+        if rollback_index.is_null() {
+            None
+        } else {
+            Some(*rollback_index)
+        }
+    };
+
+    // Safety: It is always safe to call this with a valid native_info, which we have.
+    let has_relaxed_rollback_protection_permission =
+        unsafe { hasRelaxedRollbackProtectionPermission(native_info) };
+
+    Ok(ApkManifestInfo {
+        package,
+        version_code,
+        rollback_index,
+        has_relaxed_rollback_protection_permission,
+    })
 }
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 806592d..0ffab5d 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -64,6 +64,8 @@
 DATA = [
     ":MicrodroidTestAppUpdated",
     ":MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5",
+    ":MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission",
+    ":MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index",
     ":MicrodroidVmShareApp",
     ":test_microdroid_vendor_image",
     ":test_microdroid_vendor_image_unsigned",
@@ -74,6 +76,29 @@
     defaults: ["MicrodroidTestAppsDefaults"],
     manifest: "AndroidManifestV5_relaxed_rollback_protection.xml",
     jni_libs: [
+        "MicrodroidTestNativeLib",
+        "MicrodroidTestNativeLibWithLibIcu",
+    ],
+    min_sdk_version: "33",
+}
+
+android_test_helper_app {
+    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index",
+    defaults: ["MicrodroidTestAppsDefaults"],
+    manifest: "AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml",
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+        "MicrodroidTestNativeLibWithLibIcu",
+    ],
+    min_sdk_version: "33",
+}
+
+android_test_helper_app {
+    name: "MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission",
+    defaults: ["MicrodroidTestAppsDefaults"],
+    manifest: "AndroidManifestV5_relaxed_rollback_protection_no_permission.xml",
+    jni_libs: [
+        "MicrodroidTestNativeLib",
         "MicrodroidTestNativeLibWithLibIcu",
     ],
     min_sdk_version: "33",
diff --git a/tests/testapk/AndroidManifestV5.xml b/tests/testapk/AndroidManifestV5.xml
index 2ef1b6b..f80edac 100644
--- a/tests/testapk/AndroidManifestV5.xml
+++ b/tests/testapk/AndroidManifestV5.xml
@@ -18,7 +18,6 @@
       android:versionCode="5">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
-    <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
     <queries>
diff --git a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection.xml b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection.xml
index 619d158..16c585a 100644
--- a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection.xml
+++ b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2021 The Android Open Source Project
+<!-- Copyright (C) 2025 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.
@@ -15,11 +15,13 @@
 -->
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.android.microdroid.test_relaxed_rollback_protection_scheme"
-      android:versionCode="5">
+      android:versionCode="5" >
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
-    <application />
+    <application>
+        <property android:name="android.system.virtualmachine.ROLLBACK_INDEX" android:value="1" />
+    </application>
 </manifest>
diff --git a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml
new file mode 100644
index 0000000..91de2a0
--- /dev/null
+++ b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_permission.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_relaxed_rollback_protection_scheme"
+      android:versionCode="5" >
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application>
+        <property android:name="android.system.virtualmachine.ROLLBACK_INDEX" android:value="1" />
+    </application>
+</manifest>
diff --git a/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml
new file mode 100644
index 0000000..3d6d734
--- /dev/null
+++ b/tests/testapk/AndroidManifestV5_relaxed_rollback_protection_no_rollback_index.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_relaxed_rollback_protection_scheme"
+      android:versionCode="5" >
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
+    <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application />
+</manifest>
diff --git a/tests/testapk/AndroidManifestV6.xml b/tests/testapk/AndroidManifestV6.xml
index 7dd0663..2115ef7 100644
--- a/tests/testapk/AndroidManifestV6.xml
+++ b/tests/testapk/AndroidManifestV6.xml
@@ -18,11 +18,11 @@
       android:versionCode="6">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
-    <uses-permission android:name="android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
     <queries>
         <package android:name="com.android.microdroid.vmshare_app" />
+        <package android:name="com.android.microdroid.test_relaxed_rollback_protection_scheme" />
     </queries>
     <application />
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/testapk/AndroidTestTemplate.xml b/tests/testapk/AndroidTestTemplate.xml
index 6cdf984..5ed7a07 100644
--- a/tests/testapk/AndroidTestTemplate.xml
+++ b/tests/testapk/AndroidTestTemplate.xml
@@ -21,7 +21,6 @@
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="test-file-name" value="{MODULE}.apk" />
         <option name="test-file-name" value="MicrodroidVmShareApp.apk" />
-        <option name="test-file-name" value="MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk" />
     </target_preparer>
     <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
         <option name="run-command" value="mkdir -p /data/local/tmp/cts/microdroid" />
@@ -31,6 +30,9 @@
         <option name="cleanup" value="true" />
         <option name="push" value="test_microdroid_vendor_image.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image.img" />
         <option name="push" value="test_microdroid_vendor_image_unsigned.img->/data/local/tmp/cts/microdroid/test_microdroid_vendor_image_unsigned.img" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission.apk" />
+        <option name="push" value="MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index.apk->/data/local/tmp/cts/microdroid/MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index.apk" />
     </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.microdroid.test" />
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index b492684..521ee9a 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -59,6 +59,7 @@
 import android.system.virtualmachine.VirtualMachineDescriptor;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
@@ -130,8 +131,8 @@
     private static final String VM_ATTESTATION_MESSAGE = "Hello RKP from AVF!";
     private static final int ENCRYPTED_STORAGE_BYTES = 4_000_000;
 
-    private static final String USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION_PERMISSION =
-            "android.permission.USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION";
+    private static final String RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME =
+            "com.android.microdroid.test_relaxed_rollback_protection_scheme";
 
     @Rule public Timeout globalTimeout = Timeout.seconds(300);
 
@@ -166,19 +167,18 @@
             // Tests that rely on the state of the permission should explicitly grant or revoke it.
             revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         }
-        revokePermission(USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION_PERMISSION);
     }
 
     @After
     public void tearDown() {
         revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        revokePermission(USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION_PERMISSION);
+        // Some tests might install additional apks, so we need to clean them up here.
+        uninstallApp(RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME);
     }
+
     private static final String EXAMPLE_STRING = "Literally any string!! :)";
 
     private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app";
-    private static final String RELAXED_ROLLBACK_PROTECTION_SCHEME_PACKAGE_NAME =
-            "com.android.microdroid.test_relaxed_rollback_protection_scheme";
 
     private void createAndConnectToVmHelper(int cpuTopology, boolean shouldUseHugepages)
             throws Exception {
@@ -2754,37 +2754,111 @@
         assertThat(testResults.mPageSize).isEqualTo(expectedPageSize);
     }
 
+    // This test requires MicrodroidTestApp to have USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION
+    // permission. This means that the permission needs to be declared in the AndroidManifest.xml of
+    // the MicrodroidTestApp.apk. Which in turns leads microdroid_manager to enable the relaxed
+    // rollback protection scheme, which we don't want to be enabled for most of the tests here.
+    // For now comment out this test. It will be un-commented (and probably moved to a separate test
+    // apk) in a follow-up patch.
+    // TODO(ioffe): bring this test back!
+    /*
+        @Test
+        public void libIcuIsLoadable() throws Exception {
+            assumeSupportedDevice();
+            // This test relies on the test apk having USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION
+            // permission.
+            grantPermission(USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION_PERMISSION);
+
+            // This test requires additional test apk.
+            installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_correct_V5.apk");
+
+            Context otherAppCtx =
+                    getContext()
+                            .createPackageContext(RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME, 0);
+
+            VirtualMachineConfig config =
+                    new VirtualMachineConfig.Builder(otherAppCtx)
+                            .setDebugLevel(DEBUG_LEVEL_FULL)
+                            .setPayloadBinaryName("MicrodroidTestNativeLibWithLibIcu.so")
+                            .setProtectedVm(isProtectedVm())
+                            .setOs(os())
+                            .build();
+
+            VirtualMachine vm = forceCreateNewVirtualMachine("test_libicu_is_loadable", config);
+
+            TestResults testResults =
+                    runVmTestService(
+                            TAG,
+                            vm,
+                            (ts, tr) -> {
+                                ts.checkLibIcuIsAccessible();
+                            });
+
+            // checkLibIcuIsAccessible will throw an exception if something goes wrong.
+            assertThat(testResults.mException).isNull();
+        }
+    */
+
     @Test
-    public void libIcuIsLoadable() throws Exception {
+    public void relaxedRollbackProtectionScheme_apkDoesNotHavePermission_bootFails()
+            throws Exception {
         assumeSupportedDevice();
 
-        // This test relies on the test apk having USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION
-        // permission.
-        grantPermission(USE_RELAXED_MICRODROID_ROLLBACK_PROTECTION_PERMISSION);
+        // This test requires additional test apk.
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_no_permission.apk");
 
         Context otherAppCtx =
                 getContext()
-                        .createPackageContext(RELAXED_ROLLBACK_PROTECTION_SCHEME_PACKAGE_NAME, 0);
+                        .createPackageContext(
+                                RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME, 0);
+
         VirtualMachineConfig config =
                 new VirtualMachineConfig.Builder(otherAppCtx)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
-                        .setPayloadBinaryName("MicrodroidTestNativeLibWithLibIcu.so")
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
                         .setProtectedVm(isProtectedVm())
                         .setOs(os())
                         .build();
 
-        VirtualMachine vm = forceCreateNewVirtualMachine("test_libicu_is_loadable", config);
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine(
+                        "test_relaxed_rollback_protection_scheme_no_permission", config);
+        BootResult bootResult =
+                tryBootVm(TAG, "test_relaxed_rollback_protection_scheme_no_permission");
+        assertThat(bootResult.deathReason)
+                .isEqualTo(
+                        VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED);
+    }
 
-        TestResults testResults =
-                runVmTestService(
-                        TAG,
-                        vm,
-                        (ts, tr) -> {
-                            ts.checkLibIcuIsAccessible();
-                        });
+    @Test
+    public void relaxedRollbackProtectionScheme_apkDoesNotHaveRollbackIndex_bootFails()
+            throws Exception {
+        assumeSupportedDevice();
 
-        // checkLibIcuIsAccessible will throw an exception if something goes wrong.
-        assertThat(testResults.mException).isNull();
+        // This test requires additional test apk.
+        installApp("MicrodroidTestHelperAppRelaxedRollbackProtection_no_rollback_index.apk");
+
+        Context otherAppCtx =
+                getContext()
+                        .createPackageContext(
+                                RELAXED_ROLLBACK_PROTECTION_SCHEME_TEST_PACKAGE_NAME, 0);
+
+        VirtualMachineConfig config =
+                new VirtualMachineConfig.Builder(otherAppCtx)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setPayloadBinaryName("MicrodroidTestNativeLib.so")
+                        .setProtectedVm(isProtectedVm())
+                        .setOs(os())
+                        .build();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine(
+                        "test_relaxed_rollback_protection_scheme_no_rollback_index", config);
+        BootResult bootResult =
+                tryBootVm(TAG, "test_relaxed_rollback_protection_scheme_no_rollback_index");
+        assertThat(bootResult.deathReason)
+                .isEqualTo(
+                        VirtualMachineCallback.STOP_REASON_MICRODROID_PAYLOAD_VERIFICATION_FAILED);
     }
 
     private static class VmShareServiceConnection implements ServiceConnection {
@@ -2881,4 +2955,40 @@
         Exception e = assertThrows(VirtualMachineException.class, runnable);
         assertThat(e).hasMessageThat().contains(expectedContents);
     }
+
+    private void installApp(String apkName) throws Exception {
+        String apkFile = new File("/data/local/tmp/cts/microdroid/", apkName).getAbsolutePath();
+        UiAutomation uai = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        Log.i(TAG, "Installing apk " + apkFile);
+        // We read the output of the shell command not only to see if it succeeds, but also to make
+        // sure that the installation finishes. This avoids a race condition when test tries to
+        // create a context of the installed package before the installation finished.
+        try (ParcelFileDescriptor pfd = uai.executeShellCommand("pm install " + apkFile)) {
+            try (InputStream is = new FileInputStream(pfd.getFileDescriptor())) {
+                try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        Log.i(TAG, line);
+                    }
+                }
+            }
+        }
+    }
+
+    private void uninstallApp(String packageName) {
+        Log.i(TAG, "Uninstalling package " + packageName);
+        UiAutomation uai = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+        try (ParcelFileDescriptor pfd = uai.executeShellCommand("pm uninstall " + packageName)) {
+            try (InputStream is = new FileInputStream(pfd.getFileDescriptor())) {
+                try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
+                    String line;
+                    while ((line = br.readLine()) != null) {
+                        Log.i(TAG, line);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to uninstall " + packageName, e);
+        }
+    }
 }