Merge "Handle ForegroundServiceStartNotAllowedException when starting the VM" into main
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
index c427337..efe651e 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/Application.kt
@@ -18,21 +18,12 @@
 import android.app.Application as AndroidApplication
 import android.app.NotificationChannel
 import android.app.NotificationManager
-import android.content.ComponentName
 import android.content.Context
-import android.content.Intent
-import android.content.ServiceConnection
-import android.os.IBinder
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.ProcessLifecycleOwner
 
 public class Application : AndroidApplication() {
     override fun onCreate() {
         super.onCreate()
         setupNotificationChannels()
-        val lifecycleObserver = ApplicationLifecycleObserver()
-        ProcessLifecycleOwner.get().lifecycle.addObserver(lifecycleObserver)
     }
 
     private fun setupNotificationChannels() {
@@ -61,45 +52,4 @@
 
         fun getInstance(c: Context): Application = c.getApplicationContext() as Application
     }
-
-    /**
-     * Observes application lifecycle events and interacts with the VmLauncherService to manage
-     * virtual machine state based on application lifecycle transitions. This class binds to the
-     * VmLauncherService and notifies it of application lifecycle events (onStart, onStop), allowing
-     * the service to manage the VM accordingly.
-     */
-    inner class ApplicationLifecycleObserver() : DefaultLifecycleObserver {
-        private var vmLauncherService: VmLauncherService? = null
-        private val connection =
-            object : ServiceConnection {
-                override fun onServiceConnected(className: ComponentName, service: IBinder) {
-                    val binder = service as VmLauncherService.VmLauncherServiceBinder
-                    vmLauncherService = binder.getService()
-                }
-
-                override fun onServiceDisconnected(arg0: ComponentName) {
-                    vmLauncherService = null
-                }
-            }
-
-        override fun onCreate(owner: LifecycleOwner) {
-            super.onCreate(owner)
-            bindToVmLauncherService()
-        }
-
-        override fun onStart(owner: LifecycleOwner) {
-            super.onStart(owner)
-            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_START)
-        }
-
-        override fun onStop(owner: LifecycleOwner) {
-            vmLauncherService?.processAppLifeCycleEvent(ApplicationLifeCycleEvent.APP_ON_STOP)
-            super.onStop(owner)
-        }
-
-        fun bindToVmLauncherService() {
-            val intent = Intent(this@Application, VmLauncherService::class.java)
-            this@Application.bindService(intent, connection, 0) // No BIND_AUTO_CREATE
-        }
-    }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt b/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
deleted file mode 100644
index 4e26c3c..0000000
--- a/android/TerminalApp/java/com/android/virtualization/terminal/ApplicationLifeCycleEvent.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright 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.
- * 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.virtualization.terminal
-
-enum class ApplicationLifeCycleEvent {
-    APP_ON_START,
-    APP_ON_STOP,
-}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
index 887ae02..e035ad4 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.kt
@@ -28,6 +28,7 @@
 import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse
 import com.android.virtualization.terminal.proto.ShutdownQueueOpeningRequest
 import com.android.virtualization.terminal.proto.ShutdownRequestItem
+import io.grpc.stub.ServerCallStreamObserver
 import io.grpc.stub.StreamObserver
 
 internal class DebianServiceImpl(context: Context) : DebianServiceImplBase() {
@@ -79,8 +80,15 @@
         request: ShutdownQueueOpeningRequest?,
         responseObserver: StreamObserver<ShutdownRequestItem?>,
     ) {
+        val serverCallStreamObserver = responseObserver as ServerCallStreamObserver<ShutdownRequestItem?>
+        serverCallStreamObserver.setOnCancelHandler {
+            shutdownRunnable = null
+        }
         Log.d(TAG, "openShutdownRequestQueue")
         shutdownRunnable = Runnable {
+            if (serverCallStreamObserver.isCancelled()) {
+                return@Runnable
+            }
             responseObserver.onNext(ShutdownRequestItem.newBuilder().build())
             responseObserver.onCompleted()
             shutdownRunnable = null
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
new file mode 100644
index 0000000..7647d9b
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MemBalloonController.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.
+ * 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.virtualization.terminal
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import android.system.virtualmachine.VirtualMachine
+import android.util.Log
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ProcessLifecycleOwner
+import com.android.virtualization.terminal.MainActivity.Companion.TAG
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+/**
+ * MemBalloonController is responsible for adjusting the memory ballon size of a VM depending on
+ * whether the app is visible or running in the background
+ */
+class MemBalloonController(val context: Context, val vm: VirtualMachine) {
+    companion object {
+        private const val INITIAL_PERCENT = 10
+        private const val MAX_PERCENT = 50
+        private const val INFLATION_STEP_PERCENT = 5
+        private const val INFLATION_PERIOD_SEC = 60L
+
+        private val mainHandler = Handler(Looper.getMainLooper())
+
+        private fun runOnMainThread(runnable: Runnable) {
+            mainHandler.post(runnable)
+        }
+    }
+
+    private val executor =
+        Executors.newSingleThreadScheduledExecutor(
+            TerminalThreadFactory(context.getApplicationContext())
+        )
+
+    private val observer =
+        object : DefaultLifecycleObserver {
+
+            // If the app is started or resumed, give deflate the balloon to 0 to give maximum
+            // available memory to the virtual machine
+            override fun onResume(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                executor.submit({
+                    Log.v(TAG, "app resumed. deflating mem balloon to the minimum")
+                    vm.setMemoryBalloonByPercent(0)
+                })
+            }
+
+            // If the app goes into background, progressively inflate the balloon from
+            // INITIAL_PERCENT until it reaches MAX_PERCENT
+            override fun onStop(owner: LifecycleOwner) {
+                ongoingInflation?.cancel(false)
+                balloonPercent = INITIAL_PERCENT
+                ongoingInflation =
+                    executor.scheduleAtFixedRate(
+                        {
+                            if (balloonPercent <= MAX_PERCENT) {
+                                Log.v(TAG, "inflating mem balloon to ${balloonPercent} %")
+                                vm.setMemoryBalloonByPercent(balloonPercent)
+                                balloonPercent += INFLATION_STEP_PERCENT
+                            } else {
+                                Log.v(TAG, "mem balloon is inflated to its max (${MAX_PERCENT} %)")
+                                ongoingInflation!!.cancel(false)
+                            }
+                        },
+                        0 /* initialDelay */,
+                        INFLATION_PERIOD_SEC,
+                        TimeUnit.SECONDS,
+                    )
+            }
+        }
+
+    private var balloonPercent = 0
+    private var ongoingInflation: ScheduledFuture<*>? = null
+
+    fun start() {
+        // addObserver is @MainThread
+        runOnMainThread({ ProcessLifecycleOwner.get().lifecycle.addObserver(observer) })
+    }
+
+    fun stop() {
+        // removeObserver is @MainThread
+        runOnMainThread({
+            ProcessLifecycleOwner.get().lifecycle.removeObserver(observer)
+            executor.shutdown()
+        })
+    }
+}
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
index c49b7cd..2d7dde9 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.kt
@@ -39,7 +39,6 @@
 import android.system.virtualmachine.VirtualMachineException
 import android.util.Log
 import android.widget.Toast
-import com.android.internal.annotations.GuardedBy
 import com.android.system.virtualmachine.flags.Flags.terminalGuiSupport
 import com.android.virtualization.terminal.MainActivity.Companion.TAG
 import com.android.virtualization.terminal.Runner.Companion.create
@@ -56,7 +55,6 @@
 import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
-import java.lang.Math.min
 import java.lang.RuntimeException
 import java.net.InetSocketAddress
 import java.net.SocketAddress
@@ -67,12 +65,6 @@
 import java.util.concurrent.TimeUnit
 
 class VmLauncherService : Service() {
-    inner class VmLauncherServiceBinder : android.os.Binder() {
-        fun getService(): VmLauncherService = this@VmLauncherService
-    }
-
-    private val binder = VmLauncherServiceBinder()
-
     private lateinit var executorService: ExecutorService
 
     // TODO: using lateinit for some fields to avoid null
@@ -81,42 +73,6 @@
     private var server: Server? = null
     private var debianService: DebianServiceImpl? = null
     private var portNotifier: PortNotifier? = null
-    private var mLock = Object()
-    @GuardedBy("mLock") private var currentMemBalloonPercent = 0
-
-    @GuardedBy("mLock") private val inflateMemBalloonHandler = Handler(Looper.getMainLooper())
-    private val inflateMemBalloonTask: Runnable =
-        object : Runnable {
-            override fun run() {
-                synchronized(mLock) {
-                    if (
-                        currentMemBalloonPercent < INITIAL_MEM_BALLOON_PERCENT ||
-                            currentMemBalloonPercent > MAX_MEM_BALLOON_PERCENT
-                    ) {
-                        Log.e(
-                            TAG,
-                            "currentBalloonPercent=$currentMemBalloonPercent is invalid," +
-                                " should be in range: " +
-                                "$INITIAL_MEM_BALLOON_PERCENT~$MAX_MEM_BALLOON_PERCENT",
-                        )
-                        return
-                    }
-                    // Increases the balloon size by MEM_BALLOON_PERCENT_STEP% every time
-                    if (currentMemBalloonPercent < MAX_MEM_BALLOON_PERCENT) {
-                        currentMemBalloonPercent =
-                            min(
-                                MAX_MEM_BALLOON_PERCENT,
-                                currentMemBalloonPercent + MEM_BALLOON_PERCENT_STEP,
-                            )
-                        virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                        inflateMemBalloonHandler.postDelayed(
-                            this,
-                            MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                        )
-                    }
-                }
-            }
-        }
 
     interface VmLauncherServiceCallback {
         fun onVmStart()
@@ -129,45 +85,7 @@
     }
 
     override fun onBind(intent: Intent?): IBinder? {
-        return binder
-    }
-
-    /**
-     * Processes application lifecycle events and adjusts the virtual machine's memory balloon
-     * accordingly.
-     *
-     * @param event The application lifecycle event.
-     */
-    fun processAppLifeCycleEvent(event: ApplicationLifeCycleEvent) {
-        when (event) {
-            // When the app starts, reset the memory balloon to 0%.
-            // This gives the app maximum available memory.
-            ApplicationLifeCycleEvent.APP_ON_START -> {
-                synchronized(mLock) {
-                    inflateMemBalloonHandler.removeCallbacks(inflateMemBalloonTask)
-                    currentMemBalloonPercent = 0
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                }
-            }
-            ApplicationLifeCycleEvent.APP_ON_STOP -> {
-                // When the app stops, inflate the memory balloon to INITIAL_MEM_BALLOON_PERCENT.
-                // Inflate the balloon by MEM_BALLOON_PERCENT_STEP every
-                // MEM_BALLOON_INFLATE_INTERVAL_MILLIS milliseconds until reaching
-                // MAX_MEM_BALLOON_PERCENT of total memory. This allows the system to reclaim
-                // memory while the app is in the background.
-                synchronized(mLock) {
-                    currentMemBalloonPercent = INITIAL_MEM_BALLOON_PERCENT
-                    virtualMachine?.setMemoryBalloonByPercent(currentMemBalloonPercent)
-                    inflateMemBalloonHandler.postDelayed(
-                        inflateMemBalloonTask,
-                        MEM_BALLOON_INFLATE_INTERVAL_MILLIS,
-                    )
-                }
-            }
-            else -> {
-                Log.e(TAG, "unrecognized lifecycle event: $event")
-            }
-        }
+        return null
     }
 
     override fun onCreate() {
@@ -221,7 +139,11 @@
                 ResultReceiver::class.java,
             )
 
+        val mbc = MemBalloonController(this, virtualMachine!!)
+        mbc.start()
+
         runner.exitStatus.thenAcceptAsync { success: Boolean ->
+            mbc.stop()
             resultReceiver?.send(if (success) RESULT_STOP else RESULT_ERROR, null)
             stopSelf()
         }
@@ -493,11 +415,6 @@
                 }
             }()
 
-        private const val INITIAL_MEM_BALLOON_PERCENT = 10
-        private const val MAX_MEM_BALLOON_PERCENT = 50
-        private const val MEM_BALLOON_INFLATE_INTERVAL_MILLIS = 60000L
-        private const val MEM_BALLOON_PERCENT_STEP = 5
-
         private fun getMyIntent(context: Context): Intent {
             return Intent(context.getApplicationContext(), VmLauncherService::class.java)
         }
diff --git a/libs/dice/TEST_MAPPING b/libs/dice/TEST_MAPPING
index a43d7a2..d2d89e4 100644
--- a/libs/dice/TEST_MAPPING
+++ b/libs/dice/TEST_MAPPING
@@ -12,6 +12,9 @@
       "name": "libdiced_open_dice.integration_test"
     },
     {
+      "name": "libdiced_open_dice_multialg.integration_test"
+    },
+    {
       "name": "libdiced_open_dice_nostd.integration_test"
     },
     {
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index 986f496..739f245 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -63,6 +63,33 @@
     ],
 }
 
+rust_library {
+    name: "libdiced_open_dice_multialg",
+    defaults: ["libdiced_open_dice_defaults"],
+    host_supported: true,
+    vendor_available: true,
+    rustlibs: [
+        "libcoset",
+        "libopen_dice_android_bindgen_multialg",
+        "libopen_dice_cbor_bindgen_multialg",
+        "libzeroize",
+    ],
+    features: [
+        "std",
+        "multialg",
+    ],
+    shared_libs: [
+        "libcrypto",
+    ],
+    visibility: [
+        "//system/software_defined_vehicle:__subpackages__",
+    ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+}
+
 rust_defaults {
     name: "libdiced_open_dice_test_defaults",
     crate_name: "diced_open_dice_test",
@@ -80,6 +107,18 @@
 }
 
 rust_test {
+    name: "libdiced_open_dice_multialg.integration_test",
+    defaults: ["libdiced_open_dice_test_defaults"],
+    rustlibs: [
+        "libdiced_open_dice_multialg",
+        "libcoset",
+    ],
+    features: [
+        "multialg",
+    ],
+}
+
+rust_test {
     name: "libdiced_open_dice_nostd.integration_test",
     defaults: ["libdiced_open_dice_test_defaults"],
     rustlibs: [
@@ -174,6 +213,20 @@
 }
 
 rust_bindgen {
+    name: "libopen_dice_cbor_bindgen_multialg",
+    defaults: [
+        "libopen_dice.rust_defaults",
+        "libopen_dice_cbor_bindgen.rust_defaults",
+    ],
+    bindgen_flags: [
+        "--rustified-enum DiceKeyAlgorithm",
+        "--allowlist-type=DiceContext",
+    ],
+    whole_static_libs: ["libopen_dice_cbor_multialg"],
+    shared_libs: ["libcrypto"],
+}
+
+rust_bindgen {
     name: "libopen_dice_cbor_bindgen_nostd",
     defaults: [
         "libopen_dice_cbor_bindgen.rust_defaults",
@@ -231,6 +284,18 @@
 }
 
 rust_bindgen {
+    name: "libopen_dice_android_bindgen_multialg",
+    defaults: [
+        "libopen_dice.rust_defaults",
+        "libopen_dice_android_bindgen.rust_defaults",
+    ],
+    rustlibs: [
+        "libopen_dice_cbor_bindgen_multialg",
+    ],
+    whole_static_libs: ["libopen_dice_android_multialg"],
+}
+
+rust_bindgen {
     name: "libopen_dice_android_bindgen_nostd",
     defaults: [
         "libopen_dice_android_bindgen.rust_defaults",
diff --git a/libs/dice/open_dice/src/lib.rs b/libs/dice/open_dice/src/lib.rs
index 33fb65c..9268b03 100644
--- a/libs/dice/open_dice/src/lib.rs
+++ b/libs/dice/open_dice/src/lib.rs
@@ -43,11 +43,19 @@
 };
 pub use error::{DiceError, Result};
 pub use ops::{
-    derive_cdi_leaf_priv, generate_certificate, hash, kdf, keypair_from_seed, sign,
-    sign_cose_sign1, sign_cose_sign1_with_cdi_leaf_priv, verify,
+    derive_cdi_leaf_priv, generate_certificate, hash, kdf, keypair_from_seed, sign, verify,
+};
+#[cfg(feature = "multialg")]
+pub use ops::{
+    derive_cdi_leaf_priv_multialg, keypair_from_seed_multialg, sign_cose_sign1_multialg,
+    sign_cose_sign1_with_cdi_leaf_priv_multialg, verify_multialg,
 };
 pub use retry::{
     retry_bcc_format_config_descriptor, retry_bcc_main_flow, retry_dice_main_flow,
     retry_generate_certificate, retry_sign_cose_sign1, retry_sign_cose_sign1_with_cdi_leaf_priv,
     OwnedDiceArtifacts,
 };
+#[cfg(feature = "multialg")]
+pub use retry::{
+    retry_sign_cose_sign1_multialg, retry_sign_cose_sign1_with_cdi_leaf_priv_multialg,
+};
diff --git a/libs/dice/open_dice/src/ops.rs b/libs/dice/open_dice/src/ops.rs
index 2014118..b22b113 100644
--- a/libs/dice/open_dice/src/ops.rs
+++ b/libs/dice/open_dice/src/ops.rs
@@ -21,11 +21,17 @@
     PRIVATE_KEY_SEED_SIZE, PRIVATE_KEY_SIZE, VM_KEY_ALGORITHM,
 };
 use crate::error::{check_result, DiceError, Result};
+#[cfg(feature = "multialg")]
+use crate::KeyAlgorithm;
 use alloc::{vec, vec::Vec};
+#[cfg(feature = "multialg")]
+use open_dice_cbor_bindgen::DiceContext_;
 use open_dice_cbor_bindgen::{
     DiceCoseSignAndEncodeSign1, DiceGenerateCertificate, DiceHash, DiceKdf, DiceKeypairFromSeed,
     DicePrincipal, DiceSign, DiceVerify,
 };
+#[cfg(feature = "multialg")]
+use std::ffi::c_void;
 use std::ptr;
 
 /// Hashes the provided input using DICE's hash function `DiceHash`.
@@ -100,6 +106,44 @@
     Ok((public_key, private_key))
 }
 
+/// Deterministically generates a public and private key pair from `seed` and `key_algorithm`.
+/// Since this is deterministic, `seed` is as sensitive as a private key and can
+/// be used directly as the private key.
+#[cfg(feature = "multialg")]
+pub fn keypair_from_seed_multialg(
+    seed: &[u8; PRIVATE_KEY_SEED_SIZE],
+    key_algorithm: KeyAlgorithm,
+) -> Result<(Vec<u8>, PrivateKey)> {
+    let mut public_key = vec![0u8; key_algorithm.public_key_size()];
+    let mut private_key = PrivateKey::default();
+    // This function is used with an open-dice config that uses the same algorithms for the
+    // subject and authority. Therefore, the principal is irrelevant in this context as this
+    // function only derives the key pair cryptographically without caring about which
+    // principal it is for. Hence, we arbitrarily set it to `DicePrincipal::kDicePrincipalSubject`.
+    let principal = DicePrincipal::kDicePrincipalSubject;
+    let context = DiceContext_ {
+        authority_algorithm: key_algorithm.into(),
+        subject_algorithm: key_algorithm.into(),
+    };
+    check_result(
+        // SAFETY: The function writes to the `public_key` and `private_key` within the given
+        // bounds, and only reads the `seed`.
+        // The first argument is a pointer to a valid |DiceContext_| object for multi-alg
+        // open-dice.
+        unsafe {
+            DiceKeypairFromSeed(
+                &context as *const DiceContext_ as *mut c_void,
+                principal,
+                seed.as_ptr(),
+                public_key.as_mut_ptr(),
+                private_key.as_mut_ptr(),
+            )
+        },
+        public_key.len(),
+    )?;
+    Ok((public_key, private_key))
+}
+
 /// Derives the CDI_Leaf_Priv from the provided `dice_artifacts`.
 ///
 /// The corresponding public key is included in the leaf certificate of the DICE chain
@@ -114,6 +158,17 @@
     Ok(private_key)
 }
 
+/// Multialg variant of `derive_cdi_leaf_priv`.
+#[cfg(feature = "multialg")]
+pub fn derive_cdi_leaf_priv_multialg(
+    dice_artifacts: &dyn DiceArtifacts,
+    key_algorithm: KeyAlgorithm,
+) -> Result<PrivateKey> {
+    let cdi_priv_key_seed = derive_cdi_private_key_seed(dice_artifacts.cdi_attest())?;
+    let (_, private_key) = keypair_from_seed_multialg(cdi_priv_key_seed.as_array(), key_algorithm)?;
+    Ok(private_key)
+}
+
 /// Signs the `message` with the given `private_key` using `DiceSign`.
 pub fn sign(message: &[u8], private_key: &[u8; PRIVATE_KEY_SIZE]) -> Result<Vec<u8>> {
     let mut signature = vec![0u8; VM_KEY_ALGORITHM.signature_size()];
@@ -173,6 +228,45 @@
     Ok(encoded_signature_actual_size)
 }
 
+/// Multialg variant of `sign_cose_sign1`.
+#[cfg(feature = "multialg")]
+pub fn sign_cose_sign1_multialg(
+    message: &[u8],
+    aad: &[u8],
+    private_key: &[u8; PRIVATE_KEY_SIZE],
+    encoded_signature: &mut [u8],
+    key_algorithm: KeyAlgorithm,
+) -> Result<usize> {
+    let mut encoded_signature_actual_size = 0;
+    let context = DiceContext_ {
+        authority_algorithm: key_algorithm.into(),
+        subject_algorithm: key_algorithm.into(),
+    };
+    check_result(
+        // SAFETY: The function writes to `encoded_signature` and `encoded_signature_actual_size`
+        // within the given bounds. It only reads `message`, `aad`, and `private_key` within their
+        // given bounds.
+        //
+        // The first argument is a pointer to a valid |DiceContext_| object for multi-alg
+        // open-dice.
+        unsafe {
+            DiceCoseSignAndEncodeSign1(
+                &context as *const DiceContext_ as *mut c_void,
+                message.as_ptr(),
+                message.len(),
+                aad.as_ptr(),
+                aad.len(),
+                private_key.as_ptr(),
+                encoded_signature.len(),
+                encoded_signature.as_mut_ptr(),
+                &mut encoded_signature_actual_size,
+            )
+        },
+        encoded_signature_actual_size,
+    )?;
+    Ok(encoded_signature_actual_size)
+}
+
 /// Signs the `message` with a private key derived from the given `dice_artifacts`
 /// CDI Attest. On success, places a `CoseSign1` encoded object in `encoded_signature`.
 /// Uses `DiceCoseSignAndEncodeSign1`.
@@ -188,6 +282,19 @@
     sign_cose_sign1(message, aad, private_key.as_array(), encoded_signature)
 }
 
+/// Multialg variant of `sign_cose_sign1_with_cdi_leaf_priv`.
+#[cfg(feature = "multialg")]
+pub fn sign_cose_sign1_with_cdi_leaf_priv_multialg(
+    message: &[u8],
+    aad: &[u8],
+    dice_artifacts: &dyn DiceArtifacts,
+    encoded_signature: &mut [u8],
+    key_algorithm: KeyAlgorithm,
+) -> Result<usize> {
+    let private_key = derive_cdi_leaf_priv_multialg(dice_artifacts, key_algorithm)?;
+    sign_cose_sign1_multialg(message, aad, private_key.as_array(), encoded_signature, key_algorithm)
+}
+
 /// Verifies the `signature` of the `message` with the given `public_key` using `DiceVerify`.
 pub fn verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> Result<()> {
     if signature.len() != VM_KEY_ALGORITHM.signature_size()
@@ -212,6 +319,40 @@
     )
 }
 
+/// Multialg variant of `verify`.
+#[cfg(feature = "multialg")]
+pub fn verify_multialg(
+    message: &[u8],
+    signature: &[u8],
+    public_key: &[u8],
+    key_algorithm: KeyAlgorithm,
+) -> Result<()> {
+    if signature.len() != key_algorithm.signature_size()
+        || public_key.len() != key_algorithm.public_key_size()
+    {
+        return Err(DiceError::InvalidInput);
+    }
+    let context = DiceContext_ {
+        authority_algorithm: key_algorithm.into(),
+        subject_algorithm: key_algorithm.into(),
+    };
+    check_result(
+        // SAFETY: only reads the messages, signature and public key as constant values.
+        // The first argument is a pointer to a valid |DiceContext_| object for multi-alg
+        // open-dice.
+        unsafe {
+            DiceVerify(
+                &context as *const DiceContext_ as *mut c_void,
+                message.as_ptr(),
+                message.len(),
+                signature.as_ptr(),
+                public_key.as_ptr(),
+            )
+        },
+        0,
+    )
+}
+
 /// Generates an X.509 certificate from the given `subject_private_key_seed` and
 /// `input_values`, and signed by `authority_private_key_seed`.
 /// The subject private key seed is supplied here so the implementation can choose
diff --git a/libs/dice/open_dice/src/retry.rs b/libs/dice/open_dice/src/retry.rs
index cf36bc0..d793218 100644
--- a/libs/dice/open_dice/src/retry.rs
+++ b/libs/dice/open_dice/src/retry.rs
@@ -24,6 +24,11 @@
 };
 use crate::error::{DiceError, Result};
 use crate::ops::{generate_certificate, sign_cose_sign1, sign_cose_sign1_with_cdi_leaf_priv};
+#[cfg(feature = "multialg")]
+use crate::{
+    ops::{sign_cose_sign1_multialg, sign_cose_sign1_with_cdi_leaf_priv_multialg},
+    KeyAlgorithm,
+};
 use alloc::vec::Vec;
 #[cfg(feature = "serde_derive")]
 use serde_derive::{Deserialize, Serialize};
@@ -158,6 +163,19 @@
     })
 }
 
+/// Multialg variant of `retry_sign_cose_sign1`.
+#[cfg(feature = "multialg")]
+pub fn retry_sign_cose_sign1_multialg(
+    message: &[u8],
+    aad: &[u8],
+    private_key: &[u8; PRIVATE_KEY_SIZE],
+    key_algorithm: KeyAlgorithm,
+) -> Result<Vec<u8>> {
+    retry_with_measured_buffer(|encoded_signature| {
+        sign_cose_sign1_multialg(message, aad, private_key, encoded_signature, key_algorithm)
+    })
+}
+
 /// Signs a message with the given the private key derived from the
 /// CDI Attest of the given `dice_artifacts` and returns the signature
 /// as an encoded CoseSign1 object.
@@ -170,3 +188,22 @@
         sign_cose_sign1_with_cdi_leaf_priv(message, aad, dice_artifacts, encoded_signature)
     })
 }
+
+/// Multialg variant of `retry_sign_cose_sign1_with_cdi_leaf_priv`.
+#[cfg(feature = "multialg")]
+pub fn retry_sign_cose_sign1_with_cdi_leaf_priv_multialg(
+    message: &[u8],
+    aad: &[u8],
+    dice_artifacts: &dyn DiceArtifacts,
+    key_algorithm: KeyAlgorithm,
+) -> Result<Vec<u8>> {
+    retry_with_measured_buffer(|encoded_signature| {
+        sign_cose_sign1_with_cdi_leaf_priv_multialg(
+            message,
+            aad,
+            dice_artifacts,
+            encoded_signature,
+            key_algorithm,
+        )
+    })
+}
diff --git a/libs/dice/open_dice/tests/api_test.rs b/libs/dice/open_dice/tests/api_test.rs
index b0c2ca7..0f8af10 100644
--- a/libs/dice/open_dice/tests/api_test.rs
+++ b/libs/dice/open_dice/tests/api_test.rs
@@ -21,6 +21,11 @@
         retry_sign_cose_sign1, retry_sign_cose_sign1_with_cdi_leaf_priv, sign, verify,
         DiceArtifacts, PrivateKey, CDI_SIZE, HASH_SIZE, ID_SIZE, PRIVATE_KEY_SEED_SIZE,
     };
+    #[cfg(feature = "multialg")]
+    use diced_open_dice::{
+        keypair_from_seed_multialg, retry_sign_cose_sign1_multialg,
+        retry_sign_cose_sign1_with_cdi_leaf_priv_multialg, verify_multialg, KeyAlgorithm,
+    };
 
     use coset::{CborSerializable, CoseSign1};
 
@@ -93,7 +98,21 @@
         0xfc, 0x23, 0xc9, 0x21, 0x5c, 0x48, 0x21, 0x47, 0xee, 0x5b, 0xfa, 0xaf, 0x88, 0x9a, 0x52,
         0xf1, 0x61, 0x06, 0x37,
     ];
-
+    #[cfg(feature = "multialg")]
+    const EXPECTED_EC_P256_PUB_KEY: &[u8] = &[
+        0xa7, 0x93, 0x70, 0x16, 0xff, 0xe8, 0x3c, 0x23, 0x5f, 0x6b, 0xf9, 0x38, 0x7e, 0x9c, 0xe5,
+        0x21, 0xb5, 0x8a, 0x9b, 0x68, 0x5a, 0x2f, 0x62, 0xf4, 0x15, 0x94, 0x1c, 0x61, 0xb3, 0xbb,
+        0xe1, 0x26, 0x61, 0x47, 0x97, 0xbf, 0x3a, 0x1f, 0x6b, 0x87, 0x86, 0x47, 0x5e, 0xc3, 0xa6,
+        0x8b, 0x95, 0x89, 0x9e, 0x29, 0xd5, 0x55, 0x2a, 0xdd, 0x2a, 0x3f, 0xe5, 0xf0, 0x7a, 0xd6,
+        0xc4, 0x7b, 0x64, 0xe0,
+    ];
+    #[cfg(feature = "multialg")]
+    const EXPECTED_EC_P256_PRIV_KEY: &[u8] = &[
+        0x62, 0x32, 0x1b, 0xb, 0x5c, 0xac, 0x8f, 0x20, 0x61, 0xb7, 0xa3, 0xbb, 0x46, 0x2b, 0x4e,
+        0xb3, 0x3f, 0xa7, 0xf6, 0x9b, 0x2f, 0x5b, 0x80, 0xa8, 0x55, 0x5e, 0x80, 0x26, 0xbb, 0x72,
+        0xbe, 0xe7, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+        0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
+    ];
     const EXPECTED_SIGNATURE: &[u8] = &[
         0x44, 0xae, 0xcc, 0xe2, 0xb9, 0x96, 0x18, 0x39, 0x0e, 0x61, 0x0f, 0x53, 0x07, 0xbf, 0xf2,
         0x32, 0x3d, 0x44, 0xd4, 0xf2, 0x07, 0x23, 0x30, 0x85, 0x32, 0x18, 0xd2, 0x69, 0xb8, 0x29,
@@ -140,6 +159,41 @@
         assert!(verify_result.is_err());
     }
 
+    #[cfg(feature = "multialg")]
+    #[test]
+    fn sign_cose_sign1_verify_multialg() {
+        let (pub_key, priv_key) = get_test_key_pair_ec_p256();
+
+        let signature_res = retry_sign_cose_sign1_multialg(
+            b"MyMessage",
+            b"MyAad",
+            priv_key.as_array(),
+            KeyAlgorithm::EcdsaP256,
+        );
+        assert!(signature_res.is_ok());
+        let signature = signature_res.unwrap();
+        let cose_sign1_res = CoseSign1::from_slice(&signature);
+        assert!(cose_sign1_res.is_ok());
+        let mut cose_sign1 = cose_sign1_res.unwrap();
+
+        let mut verify_result = cose_sign1.verify_signature(b"MyAad", |sign, data| {
+            verify_multialg(data, sign, &pub_key, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_ok());
+
+        verify_result = cose_sign1.verify_signature(b"BadAad", |sign, data| {
+            verify_multialg(data, sign, &pub_key, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_err());
+
+        // if we modify the signature, the payload should no longer verify
+        cose_sign1.signature.push(0xAA);
+        verify_result = cose_sign1.verify_signature(b"MyAad", |sign, data| {
+            verify_multialg(data, sign, &pub_key, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_err());
+    }
+
     struct TestArtifactsForSigning {}
 
     impl DiceArtifacts for TestArtifactsForSigning {
@@ -182,6 +236,41 @@
         assert!(verify_result.is_err());
     }
 
+    #[cfg(feature = "multialg")]
+    #[test]
+    fn sign_cose_sign1_with_cdi_leaf_priv_verify_multialg() {
+        let dice = TestArtifactsForSigning {};
+
+        let signature_res = retry_sign_cose_sign1_with_cdi_leaf_priv_multialg(
+            b"MyMessage",
+            b"MyAad",
+            &dice,
+            KeyAlgorithm::EcdsaP256,
+        );
+        assert!(signature_res.is_ok());
+        let signature = signature_res.unwrap();
+        let cose_sign1_res = CoseSign1::from_slice(&signature);
+        assert!(cose_sign1_res.is_ok());
+        let mut cose_sign1 = cose_sign1_res.unwrap();
+
+        let mut verify_result = cose_sign1.verify_signature(b"MyAad", |sign, data| {
+            verify_multialg(data, sign, EXPECTED_EC_P256_PUB_KEY, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_ok());
+
+        verify_result = cose_sign1.verify_signature(b"BadAad", |sign, data| {
+            verify_multialg(data, sign, EXPECTED_EC_P256_PUB_KEY, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_err());
+
+        // if we modify the signature, the payload should no longer verify
+        cose_sign1.signature.push(0xAA);
+        verify_result = cose_sign1.verify_signature(b"MyAad", |sign, data| {
+            verify_multialg(data, sign, EXPECTED_EC_P256_PUB_KEY, KeyAlgorithm::EcdsaP256)
+        });
+        assert!(verify_result.is_err());
+    }
+
     fn get_test_key_pair() -> (Vec<u8>, PrivateKey) {
         let seed = hash(b"MySeedString").unwrap();
         assert_eq!(seed, EXPECTED_SEED);
@@ -196,4 +285,23 @@
 
         (pub_key, priv_key)
     }
+
+    #[cfg(feature = "multialg")]
+    fn get_test_key_pair_ec_p256() -> (Vec<u8>, PrivateKey) {
+        let seed = hash(b"MySeedString").unwrap();
+        assert_eq!(seed, EXPECTED_SEED);
+        let cdi_attest = &seed[..CDI_SIZE];
+        assert_eq!(cdi_attest, EXPECTED_CDI_ATTEST);
+        let cdi_private_key_seed =
+            derive_cdi_private_key_seed(cdi_attest.try_into().unwrap()).unwrap();
+        assert_eq!(cdi_private_key_seed.as_array(), EXPECTED_CDI_PRIVATE_KEY_SEED);
+        let (pub_key, priv_key) =
+            keypair_from_seed_multialg(cdi_private_key_seed.as_array(), KeyAlgorithm::EcdsaP256)
+                .unwrap();
+
+        assert_eq!(&pub_key, EXPECTED_EC_P256_PUB_KEY);
+        assert_eq!(priv_key.as_array(), EXPECTED_EC_P256_PRIV_KEY);
+
+        (pub_key, priv_key)
+    }
 }
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 4294df4..4523572 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
@@ -625,7 +625,7 @@
             stdout.transferTo(out);
             stderr.transferTo(out);
             String output = out.toString("UTF-8");
-            Log.i(tag, "Got output : " + stdout);
+            Log.i(tag, "Got stdout + stderr : " + output);
             return output;
         } catch (IOException e) {
             Log.e(tag, "Error executing: " + command, e);