VM: Introduce writePayloadRpData/readPayloadRpData
VM payload require an api to allow storing n bytes' data with
confidentialty & tamper evidence integrity guarantees.
Microdroid Manager implements this using the vm_secret module, which
uses the payload's DICE chain to store/get secret from Secretkeeper.
Additionally introduce a test that uses these api.
Test: #rollbackProtectedDataOfPayload
Bug: 378911776
Change-Id: Id39f5c6c626531029bf33ef5d28dc237881e40e6
diff --git a/guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
index b7a539b..30c4299 100644
--- a/guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
+++ b/guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
@@ -78,6 +78,20 @@
byte[] getVmInstanceSecret(in byte[] identifier, int size);
/**
+ * Write `data`, on behalf of the client, to Secretkeeper.
+ * This is confidential to the pVM and protected via appropriate DICE policy
+ * on the payload's DICE chain.
+ */
+ void writePayloadRpData(in byte[32] data);
+
+ /**
+ * Read payload's `data` written on behalf of the payload in Secretkeeper.
+ * The returned value can be null either due to no value written or because
+ * Android maliciously deleted the value - Secretkeeper deletion are not authenticated.
+ */
+ @nullable byte[32] readPayloadRpData();
+
+ /**
* Gets the DICE attestation chain for the VM.
*
* The DICE chain must not be made available to all VMs as it contains privacy breaking
diff --git a/guest/microdroid_manager/src/vm_payload_service.rs b/guest/microdroid_manager/src/vm_payload_service.rs
index 7f4317b..c4a9111 100644
--- a/guest/microdroid_manager/src/vm_payload_service.rs
+++ b/guest/microdroid_manager/src/vm_payload_service.rs
@@ -97,6 +97,25 @@
certificateChain: cert_chain,
})
}
+
+ fn readPayloadRpData(&self) -> binder::Result<Option<[u8; 32]>> {
+ let data = self
+ .secret
+ .read_payload_data_rp()
+ .context("Failed to read payload's rollback protected data")
+ .with_log()
+ .or_service_specific_exception(-1)?;
+ Ok(data)
+ }
+
+ fn writePayloadRpData(&self, data: &[u8; 32]) -> binder::Result<()> {
+ self.secret
+ .write_payload_data_rp(data)
+ .context("Failed to write payload's rollback protected data")
+ .with_log()
+ .or_service_specific_exception(-1)?;
+ Ok(())
+ }
}
impl Interface for VmPayloadService {}
diff --git a/guest/microdroid_manager/src/vm_secret.rs b/guest/microdroid_manager/src/vm_secret.rs
index 5cc90ff..04d6817 100644
--- a/guest/microdroid_manager/src/vm_secret.rs
+++ b/guest/microdroid_manager/src/vm_secret.rs
@@ -71,13 +71,20 @@
// with downgraded images will not have access to VM's secret.
// V2 secrets require hardware support - Secretkeeper HAL, which (among other things)
// is backed by tamper-evident storage, providing rollback protection to these secrets.
- V2 { dice_artifacts: OwnedDiceArtifactsWithExplicitKey, skp_secret: ZVec },
+ V2 {
+ instance_id: [u8; ID_SIZE],
+ dice_artifacts: OwnedDiceArtifactsWithExplicitKey,
+ skp_secret: ZVec,
+ secretkeeper_session: SkVmSession,
+ },
// V1 secrets are not protected against rollback of boot images.
// They are reliable only if rollback of images was prevented by verified boot ie,
// each stage (including pvmfw/Microdroid/Microdroid Manager) prevents downgrade of next
// stage. These are now legacy secrets & used only when Secretkeeper HAL is not supported
// by device.
- V1 { dice_artifacts: OwnedDiceArtifacts },
+ V1 {
+ dice_artifacts: OwnedDiceArtifacts,
+ },
}
// For supporting V2 secrets, guest expects the public key to be present in the Linux device tree.
@@ -100,24 +107,26 @@
let explicit_dice = OwnedDiceArtifactsWithExplicitKey::from_owned_artifacts(dice_artifacts)
.context("Failed to get Dice artifacts in explicit key format")?;
- let session = SkVmSession::new(vm_service, &explicit_dice)?;
let id = super::get_instance_id()?.ok_or(anyhow!("Missing instance_id"))?;
let explicit_dice_chain = explicit_dice
.explicit_key_dice_chain()
.ok_or(anyhow!("Missing explicit dice chain, this is unusual"))?;
let policy = sealing_policy(explicit_dice_chain)
.map_err(|e| anyhow!("Failed to build a sealing_policy: {e}"))?;
+ let session = SkVmSession::new(vm_service, &explicit_dice, policy)?;
let mut skp_secret = Zeroizing::new([0u8; SECRET_SIZE]);
- if let Some(secret) = session.get_secret(id, Some(policy.clone()))? {
+ if let Some(secret) = session.get_secret(id)? {
*skp_secret = secret
} else {
log::warn!("No entry found in Secretkeeper for this VM instance, creating new secret.");
*skp_secret = rand::random();
- session.store_secret(id, skp_secret.clone(), policy)?;
+ session.store_secret(id, skp_secret.clone())?;
}
Ok(Self::V2 {
+ instance_id: id,
dice_artifacts: explicit_dice,
skp_secret: ZVec::try_from(skp_secret.to_vec())?,
+ secretkeeper_session: session,
})
}
@@ -130,7 +139,7 @@
fn get_vm_secret(&self, salt: &[u8], identifier: &[u8], key: &mut [u8]) -> Result<()> {
match self {
- Self::V2 { dice_artifacts, skp_secret } => {
+ Self::V2 { dice_artifacts, skp_secret, .. } => {
let mut hasher = sha::Sha256::new();
hasher.update(dice_artifacts.cdi_seal());
hasher.update(skp_secret);
@@ -152,6 +161,23 @@
pub fn derive_encryptedstore_key(&self, key: &mut [u8]) -> Result<()> {
self.get_vm_secret(SALT_ENCRYPTED_STORE, ENCRYPTEDSTORE_KEY_IDENTIFIER.as_bytes(), key)
}
+
+ pub fn read_payload_data_rp(&self) -> Result<Option<[u8; SECRET_SIZE]>> {
+ let Self::V2 { instance_id, secretkeeper_session, .. } = self else {
+ return Err(anyhow!("Rollback protected data is not available with V1 secrets"));
+ };
+ let payload_id = sha::sha512(instance_id);
+ secretkeeper_session.get_secret(payload_id)
+ }
+
+ pub fn write_payload_data_rp(&self, data: &[u8; SECRET_SIZE]) -> Result<()> {
+ let data = Zeroizing::new(*data);
+ let Self::V2 { instance_id, secretkeeper_session, .. } = self else {
+ return Err(anyhow!("Rollback protected data is not available with V1 secrets"));
+ };
+ let payload_id = sha::sha512(instance_id);
+ secretkeeper_session.store_secret(payload_id, data)
+ }
}
// Construct a sealing policy on the dice chain. VMs uses the following set of constraint for
@@ -227,31 +253,35 @@
}
// The secure session between VM & Secretkeeper
-struct SkVmSession(Arc<Mutex<SkSession>>);
+pub(crate) struct SkVmSession {
+ session: Arc<Mutex<SkSession>>,
+ sealing_policy: Vec<u8>,
+}
+
+// TODO(b/378911776): This get_secret/store_secret fails on expired session.
+// Introduce retry after refreshing the session
impl SkVmSession {
fn new(
vm_service: &Strong<dyn IVirtualMachineService>,
dice: &OwnedDiceArtifactsWithExplicitKey,
+ sealing_policy: Vec<u8>,
) -> Result<Self> {
let secretkeeper_proxy = get_secretkeeper_service(vm_service)?;
- let secure_session =
- SkSession::new(secretkeeper_proxy, dice, Some(get_secretkeeper_identity()?))?;
- let secure_session = Arc::new(Mutex::new(secure_session));
- Ok(Self(secure_session))
+ let session = SkSession::new(secretkeeper_proxy, dice, Some(get_secretkeeper_identity()?))?;
+ let session = Arc::new(Mutex::new(session));
+ Ok(Self { session, sealing_policy })
}
- fn store_secret(
- &self,
- id: [u8; ID_SIZE],
- secret: Zeroizing<[u8; SECRET_SIZE]>,
- sealing_policy: Vec<u8>,
- ) -> Result<()> {
- let store_request =
- StoreSecretRequest { id: Id(id), secret: Secret(*secret), sealing_policy };
+ fn store_secret(&self, id: [u8; ID_SIZE], secret: Zeroizing<[u8; SECRET_SIZE]>) -> Result<()> {
+ let store_request = StoreSecretRequest {
+ id: Id(id),
+ secret: Secret(*secret),
+ sealing_policy: self.sealing_policy.clone(),
+ };
log::info!("Secretkeeper operation: {:?}", store_request);
let store_request = store_request.serialize_to_packet().to_vec().map_err(anyhow_err)?;
- let session = &mut *self.0.lock().unwrap();
+ let session = &mut *self.session.lock().unwrap();
let store_response = session.secret_management_request(&store_request)?;
let store_response = ResponsePacket::from_slice(&store_response).map_err(anyhow_err)?;
let response_type = store_response.response_type().map_err(anyhow_err)?;
@@ -263,15 +293,14 @@
Ok(())
}
- fn get_secret(
- &self,
- id: [u8; ID_SIZE],
- updated_sealing_policy: Option<Vec<u8>>,
- ) -> Result<Option<[u8; SECRET_SIZE]>> {
- let get_request = GetSecretRequest { id: Id(id), updated_sealing_policy };
+ fn get_secret(&self, id: [u8; ID_SIZE]) -> Result<Option<[u8; SECRET_SIZE]>> {
+ let get_request = GetSecretRequest {
+ id: Id(id),
+ updated_sealing_policy: Some(self.sealing_policy.clone()),
+ };
log::info!("Secretkeeper operation: {:?}", get_request);
let get_request = get_request.serialize_to_packet().to_vec().map_err(anyhow_err)?;
- let session = &mut *self.0.lock().unwrap();
+ let session = &mut *self.session.lock().unwrap();
let get_response = session.secret_management_request(&get_request)?;
let get_response = ResponsePacket::from_slice(&get_response).map_err(anyhow_err)?;
let response_type = get_response.response_type().map_err(anyhow_err)?;
diff --git a/libs/libvm_payload/Android.bp b/libs/libvm_payload/Android.bp
index bb91737..1ebbe39 100644
--- a/libs/libvm_payload/Android.bp
+++ b/libs/libvm_payload/Android.bp
@@ -34,6 +34,7 @@
bindgen_flags: [
"--default-enum-style rust",
"--allowlist-type=AVmAttestationStatus",
+ "--allowlist-type=AVmAccessRollbackProtectedSecretStatus",
],
visibility: [":__subpackages__"],
}
diff --git a/libs/libvm_payload/include/vm_payload.h b/libs/libvm_payload/include/vm_payload.h
index 5e15607..43fba82 100644
--- a/libs/libvm_payload/include/vm_payload.h
+++ b/libs/libvm_payload/include/vm_payload.h
@@ -52,6 +52,22 @@
} AVmAttestationStatus;
/**
+ * Introduced in API 36.
+ * Status type used to indicate error while accessing RollbackProtectedSecret.
+ */
+typedef enum AVmAccessRollbackProtectedSecretStatus : int32_t {
+ /**
+ * Relevant Entry not found. This can happen either due to no value was ever written or because
+ * Android maliciously deleted the value (deletions may not be authenticated).
+ */
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ENTRY_NOT_FOUND = -1,
+ /** Requested access size is not supported by the implementation */
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_BAD_SIZE = -2,
+ /** Access failed, this could be due to lacking support from Hardware */
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ACCESS_FAILED = -3,
+} AVmAccessRollbackProtectedSecretStatus;
+
+/**
* Notifies the host that the payload is ready.
*
* If the host app has set a `VirtualMachineCallback` for the VM, its
@@ -259,5 +275,33 @@
size_t AVmAttestationResult_getCertificateAt(const AVmAttestationResult* _Nonnull result,
size_t index, void* _Nullable data, size_t size)
__INTRODUCED_IN(__ANDROID_API_V__);
+/**
+ * Writes up to n bytes from buffer starting at `buf`, on behalf of the payload, to rollback
+ * detectable storage. The number of bytes written may be less than n if, for example, the
+ * underlying storage has size constraints. This stored data is confidential to the pVM and
+ * protected via appropriate DICE policy on the payload's DICE chain.
+ *
+ * \param buf A pointer to data to be written. This should have the size of at least n bytes.
+ * \param n The maximum number of bytes to be filled in `buf`.
+ *
+ * \return On success, the number of bytes written is returned. On error, appropriate
+ * AVmAccessRollbackProtectedSecretStatus (negative number) is returned.
+ */
+
+int32_t AVmPayload_writeRollbackProtectedSecret(const void* _Nonnull buf, size_t n)
+ __INTRODUCED_IN(36);
+/**
+ * Read up to n bytes of payload's data in rollback detectable storage into `buf`.
+ *
+ * \param buf A pointer to buffer where the requested data is written. This should have the size of
+ * at least n bytes.
+ * \param n The maximum number of bytes to be read.
+ *
+ * \return On success, the number of bytes that would have been written to `buf` if n was
+ * sufficiently large. On error, appropriate AVmAccessRollbackProtectedSecretStatus(a negative
+ * number) is returned.
+ */
+int32_t AVmPayload_readRollbackProtectedSecret(void* _Nullable buf, size_t n) __INTRODUCED_IN(36);
+;
__END_DECLS
diff --git a/libs/libvm_payload/libvm_payload.map.txt b/libs/libvm_payload/libvm_payload.map.txt
index 3daad00..0c6b56d 100644
--- a/libs/libvm_payload/libvm_payload.map.txt
+++ b/libs/libvm_payload/libvm_payload.map.txt
@@ -15,6 +15,8 @@
AVmAttestationStatus_toString; # systemapi introduced=VanillaIceCream
AVmAttestationResult_getCertificateCount; # systemapi introduced=VanillaIceCream
AVmAttestationResult_getCertificateAt; # systemapi introduced=VanillaIceCream
+ AVmPayload_writeRollbackProtectedSecret; # systemapi introduced=36
+ AVmPayload_readRollbackProtectedSecret; # systemapi introduced=36
local:
*;
};
diff --git a/libs/libvm_payload/src/lib.rs b/libs/libvm_payload/src/lib.rs
index eb81752..29b0cbd 100644
--- a/libs/libvm_payload/src/lib.rs
+++ b/libs/libvm_payload/src/lib.rs
@@ -16,14 +16,14 @@
use android_system_virtualization_payload::aidl::android::system::virtualization::payload:: IVmPayloadService::{
IVmPayloadService, ENCRYPTEDSTORE_MOUNTPOINT, VM_APK_CONTENTS_PATH,
- VM_PAYLOAD_SERVICE_SOCKET_NAME, AttestationResult::AttestationResult,
+ VM_PAYLOAD_SERVICE_SOCKET_NAME, AttestationResult::AttestationResult
};
use anyhow::{bail, ensure, Context, Result};
use binder::{
unstable_api::{new_spibinder, AIBinder},
Strong, ExceptionCode,
};
-use log::{error, info, LevelFilter};
+use log::{error, info, LevelFilter, debug};
use rpcbinder::{RpcServer, RpcSession};
use openssl::{ec::EcKey, sha::sha256, ecdsa::EcdsaSig};
use std::convert::Infallible;
@@ -38,9 +38,12 @@
Mutex,
};
use vm_payload_status_bindgen::AVmAttestationStatus;
+use vm_payload_status_bindgen::AVmAccessRollbackProtectedSecretStatus::{AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ENTRY_NOT_FOUND, AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ACCESS_FAILED, AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_BAD_SIZE};
+use std::cmp::min;
/// Maximum size of an ECDSA signature for EC P-256 key is 72 bytes.
const MAX_ECDSA_P256_SIGNATURE_SIZE: usize = 72;
+const RP_DATA_SIZE: usize = 32;
static VM_APK_CONTENTS_PATH_C: LazyLock<CString> =
LazyLock::new(|| CString::new(VM_APK_CONTENTS_PATH).expect("CString::new failed"));
@@ -566,3 +569,86 @@
ptr::null()
}
}
+
+/// Writes up to n bytes from buffer starting at `buf`, on behalf of the payload, to rollback
+/// detectable storage and return the number of bytes written or appropriate (negative) status.
+/// For this implementation, the backing storage is Secretkeeper HAL, which allows storing & reading
+/// of 32 bytes secret!
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `buf` must be [valid] for reads of n bytes.
+///
+/// [valid]: ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmPayload_writeRollbackProtectedSecret(buf: *const u8, n: usize) -> i32 {
+ initialize_logging();
+ if n < RP_DATA_SIZE {
+ error!(
+ "Requested writing {} bytes, while Secretkeeper supports only {} bytes",
+ n, RP_DATA_SIZE
+ );
+ return AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_BAD_SIZE as i32;
+ }
+ // Safety: See the requirements on `buf` above and we just checked that n >= RP_DATA_SIZE.
+ let buf = unsafe { std::slice::from_raw_parts(buf, RP_DATA_SIZE) };
+ match try_writing_payload_rollback_protected_data(buf.try_into().unwrap()) {
+ Ok(()) => RP_DATA_SIZE as i32,
+ Err(e) => {
+ error!("Failed to write rollback protected data: {e:?}");
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ACCESS_FAILED as i32
+ }
+ }
+}
+
+/// Read up to n bytes of payload's data in rollback detectable storage into `buf`.
+/// For this implementation, the backing storage is Secretkeeper HAL, which allows storing & reading
+/// of 32 bytes secret!
+///
+/// # Safety
+///
+/// Behavior is undefined if any of the following conditions are violated:
+///
+/// * `buf` must be [valid] for writes of n bytes.
+///
+/// [valid]: ptr#safety
+#[no_mangle]
+pub unsafe extern "C" fn AVmPayload_readRollbackProtectedSecret(buf: *mut u8, n: usize) -> i32 {
+ initialize_logging();
+ match try_read_rollback_protected_data() {
+ Err(e) => {
+ error!("Failed to read rollback protected data: {e:?}");
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ACCESS_FAILED as i32
+ }
+ Ok(stored_data) => {
+ if let Some(stored_data) = stored_data {
+ // SAFETY: See the requirements on `buf` above; `stored_data` is known to have
+ // length `RP_DATA_SIZE`, and cannot overlap `data` because we just allocated
+ // it.
+ unsafe {
+ ptr::copy_nonoverlapping(stored_data.as_ptr(), buf, min(n, RP_DATA_SIZE));
+ }
+ RP_DATA_SIZE as i32
+ } else {
+ debug!("No relevant entry found in Secretkeeper");
+ AVMACCESSROLLBACKPROTECTEDSECRETSTATUS_ENTRY_NOT_FOUND as i32
+ }
+ }
+ }
+}
+
+fn try_writing_payload_rollback_protected_data(data: &[u8; RP_DATA_SIZE]) -> Result<()> {
+ get_vm_payload_service()?
+ .writePayloadRpData(data)
+ .context("Failed to write payload rollback protected data")?;
+ Ok(())
+}
+
+fn try_read_rollback_protected_data() -> Result<Option<[u8; RP_DATA_SIZE]>> {
+ let rp = get_vm_payload_service()?
+ .readPayloadRpData()
+ .context("Failed to read rollback protected data")?;
+ Ok(rp)
+}
diff --git a/libs/libvm_payload/wrapper/lib.rs b/libs/libvm_payload/wrapper/lib.rs
index b9ce6c8..133b14e 100644
--- a/libs/libvm_payload/wrapper/lib.rs
+++ b/libs/libvm_payload/wrapper/lib.rs
@@ -31,7 +31,9 @@
use std::ptr;
use vm_payload_bindgen::{
AIBinder, AVmPayload_getApkContentsPath, AVmPayload_getEncryptedStoragePath,
- AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
+ AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady,
+ AVmPayload_readRollbackProtectedSecret, AVmPayload_runVsockRpcServer,
+ AVmPayload_writeRollbackProtectedSecret,
};
/// The functions declared here are restricted to VMs created with a config file;
@@ -194,3 +196,15 @@
)
}
}
+
+/// Read payload's `data` written on behalf of the payload in Secretkeeper.
+pub fn read_rollback_protected_secret(data: &mut [u8]) -> i32 {
+ // SAFETY: The function only reads from`[data]` within its bounds.
+ unsafe { AVmPayload_readRollbackProtectedSecret(data.as_ptr() as *mut c_void, data.len()) }
+}
+
+/// Write `data`, on behalf of the payload, to Secretkeeper.
+pub fn write_rollback_protected_secret(data: &[u8]) -> i32 {
+ // SAFETY: The function only writes to `[data]` within its bounds.
+ unsafe { AVmPayload_writeRollbackProtectedSecret(data.as_ptr() as *const c_void, data.len()) }
+}
diff --git a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
index 6a3bc1b..73e5c5c 100644
--- a/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
+++ b/tests/aidl/com/android/microdroid/testservice/ITestService.aidl
@@ -80,6 +80,19 @@
String readLineFromConsole();
/**
+ * Read payload's rollback protected data. The `AVmAccessRollbackProtectedSecretStatus` is
+ * wrapped as service_specific error in case of failure. This is _only_ used for testing.
+ */
+ byte[32] insecurelyReadPayloadRpData();
+
+ /**
+ * Request VM to write payload's rollback protected data. The
+ * `AVmAccessRollbackProtectedSecretStatus` is wrapped as service_specific error in case of
+ * failure. This is _only_ used for testing.
+ */
+ void insecurelyWritePayloadRpData(in byte[32] data);
+
+ /**
* Request the service to exit, triggering the termination of the VM. This may cause any
* requests in flight to fail.
*/
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 2986bdc..3dbc242 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -272,9 +272,11 @@
}
protected void assumeNoUpdatableVmSupport() throws VirtualMachineException {
- assume().withMessage("Secretkeeper not supported")
- .that(getVirtualMachineManager().isUpdatableVmSupported())
- .isFalse();
+ assume().withMessage("Secretkeeper not supported").that(isUpdatableVmSupported()).isFalse();
+ }
+
+ protected boolean isUpdatableVmSupported() throws VirtualMachineException {
+ return getVirtualMachineManager().isUpdatableVmSupported();
}
public abstract static class VmEventListener implements VirtualMachineCallback {
@@ -588,6 +590,7 @@
public String mConsoleInput;
public byte[] mInstanceSecret;
public int mPageSize;
+ public byte[] mPayloadRpData;
public void assertNoException() {
if (mException != null) {
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 97a5e78..35ee7dd 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1826,6 +1826,79 @@
assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING);
}
+ private boolean deviceCapableOfProtectedVm() {
+ int capabilities = getVirtualMachineManager().getCapabilities();
+ if ((capabilities & CAPABILITY_PROTECTED_VM) != 0) {
+ return true;
+ }
+ return false;
+ }
+
+ private void ensureUpdatableVmSupported() throws Exception {
+ if (getVendorApiLevel() >= 202504 && deviceCapableOfProtectedVm()) {
+ assertTrue(
+ "Missing Updatable VM support, have you declared Secretkeeper interface?",
+ isUpdatableVmSupported());
+ } else {
+ assumeTrue("Device does not support Updatable VM", isUpdatableVmSupported());
+ }
+ }
+
+ @Test
+ public void rollbackProtectedDataOfPayload() throws Exception {
+ assumeSupportedDevice();
+ // Rollback protected data is only possible if Updatable VMs is supported -
+ // which implies Secretkeeper support.
+ ensureUpdatableVmSupported();
+ byte[] value1 = new byte[32];
+ Arrays.fill(value1, (byte) 0xcc);
+ byte[] value2 = new byte[32];
+ Arrays.fill(value2, (byte) 0xdd);
+
+ VirtualMachineConfig config =
+ newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
+ .setMemoryBytes(minMemoryRequired())
+ .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+ .setDebugLevel(DEBUG_LEVEL_FULL)
+ .build();
+ VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config);
+ TestResults testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mPayloadRpData = ts.insecurelyReadPayloadRpData();
+ });
+ // ainsecurelyReadPayloadRpData()` must've failed since no data was ever written!
+ assertWithMessage("The read (unexpectedly) succeeded!")
+ .that(testResults.mException)
+ .isNotNull();
+
+ // Re-run the same VM & write/read th RP data & verify it what we just wrote!
+ testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ ts.insecurelyWritePayloadRpData(value1);
+ tr.mPayloadRpData = ts.insecurelyReadPayloadRpData();
+ ts.insecurelyWritePayloadRpData(value2);
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mPayloadRpData).isEqualTo(value1);
+
+ // Re-run the same VM again
+ testResults =
+ runVmTestService(
+ TAG,
+ vm,
+ (ts, tr) -> {
+ tr.mPayloadRpData = ts.insecurelyReadPayloadRpData();
+ });
+ testResults.assertNoException();
+ assertThat(testResults.mPayloadRpData).isEqualTo(value2);
+ }
+
@Test
@CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
public void canReadFileFromAssets_debugFull() throws Exception {
diff --git a/tests/testapk/src/native/testbinary.cpp b/tests/testapk/src/native/testbinary.cpp
index 632f648..a1739f9 100644
--- a/tests/testapk/src/native/testbinary.cpp
+++ b/tests/testapk/src/native/testbinary.cpp
@@ -344,6 +344,23 @@
return ScopedAStatus::ok();
}
+ ScopedAStatus insecurelyReadPayloadRpData(std::array<uint8_t, 32>* out) override {
+ int32_t ret = AVmPayload_readRollbackProtectedSecret(out->data(), 32);
+ if (ret != 32) {
+ return ScopedAStatus::fromServiceSpecificError(ret);
+ }
+ return ScopedAStatus::ok();
+ }
+
+ ScopedAStatus insecurelyWritePayloadRpData(
+ const std::array<uint8_t, 32>& inputData) override {
+ int32_t ret = AVmPayload_writeRollbackProtectedSecret(inputData.data(), 32);
+ if (ret != 32) {
+ return ScopedAStatus::fromServiceSpecificError(ret);
+ }
+ return ScopedAStatus::ok();
+ }
+
ScopedAStatus quit() override { exit(0); }
};
auto testService = ndk::SharedRefBase::make<TestService>();
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
index e479342..2b2fa28 100644
--- a/tests/testapk/src/native/testbinary.rs
+++ b/tests/testapk/src/native/testbinary.rs
@@ -132,6 +132,12 @@
fn readLineFromConsole(&self) -> BinderResult<String> {
unimplemented()
}
+ fn insecurelyReadPayloadRpData(&self) -> BinderResult<[u8; 32]> {
+ unimplemented()
+ }
+ fn insecurelyWritePayloadRpData(&self, _: &[u8; 32]) -> BinderResult<()> {
+ unimplemented()
+ }
}
fn unimplemented<T>() -> BinderResult<T> {
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 13b0c51..4465c5e 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -276,5 +276,15 @@
public void quit() throws RemoteException {
throw new UnsupportedOperationException("Not supported");
}
+
+ @Override
+ public byte[] insecurelyReadPayloadRpData() {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ public void insecurelyWritePayloadRpData(byte[] data) {
+ throw new UnsupportedOperationException("Not supported");
+ }
}
}