feat(uno): GetUnwrappingKey bring-up — provision RSA unwrapping key from GSRAM - #601
Conversation
…c key Foundational pieces for GetUnwrappingKey via the direct-GSRAM protocol (HSP publishes the RSA-2048 unwrapping key into the per-partition persistent store; CP1 reads it): - part_store: add accessors for the HSP-published unwrapping-key backup slot (already modeled at offsets +23/+24): `unwrapping_key_bk_valid()`, `unwrapping_key_bk() -> &DmaBuf` (516-byte PKA-LE d||n||e), and `clear_unwrapping_key_bk()` (zeroizes the payload before clearing the valid flag so the HSP never observes a valid flag over stale bytes). - rsa PAL: implement `rsa_priv_pub_key`. The Uno vault RSA private key is the PKA little-endian `d(k) || n(k) || e(4)` layout, so the wire public key `n_le || e_le` is the trailing `priv_key[k..]` slice verbatim (no endianness flip, no PKA op); returns `modulus_len + 4`.
Wire GetUnwrappingKey end-to-end on uno. The shared handler now takes the partition lock and calls a new async HsmPartitionManager::provision_unwrapping_key hook (default no-op; the std/emulator PAL keeps its lazy self-generation) before reading the key id. The uno override imports the 516-byte PKA-LE key from its GSRAM backup slot into the vault (vault_key_create Rsa2kPrivate), records the id, and clears the slot; the cached id makes re-entry a cache hit. Until the HSP ephemeral-key monitor publishes real keys, seed the slot with a hardcoded RSA-2048 test key (unwrapping_key_fixture, throwaway) via a new part_store set_unwrapping_key_bk producer accessor. HW-validated: get_unwrapping_key_smoke passes 7/7 on the Manticore EVB (public key derivation + masked envelope + stable-across-calls). RsaUnwrap private-key decrypt is still WIP.
Move the emulator's lazy RSA-2048 unwrapping-key generation out of the part_prop_get_u16 property getter and into the shared HsmPartitionManager::provision_unwrapping_key hook that the GetUnwrappingKey handler already calls (under the partition lock) before reading the key id. Both PALs now materialise the key through one uniform entry point — the emulator generates it, hardware imports it from GSRAM — instead of the getter carrying a surprising keygen side effect. Removes the getter-side special case and the redundant inherent method; behaviour is unchanged (the key still materialises on first GetUnwrappingKey and never reports PendingKeyGeneration on emu). Validated: get_unwrapping_key smoke 7/7 on the mock/std-PAL path (including multi_threaded_stress).
There was a problem hiding this comment.
Pull request overview
Brings up GetUnwrappingKey end-to-end by adding a PAL hook to provision/materialize the per-partition RSA-2048 unwrapping key on first use, wiring Uno to import from a GSRAM backup slot into the vault, and updating the handler to call the hook under the partition lock before reading the key id.
Changes:
- Add
HsmPartitionManager::provision_unwrapping_key(io)(default no-op) and call it fromget_unwrapping_keyunder the partition lock. - Uno PAL: import
d‖n‖e(PKA-LE) from per-partition GSRAM slot into the vault, cache the key id, and clear the slot; add a temporary hardcoded bring-up fixture. - Uno RSA: implement
rsa_priv_pub_keyby derivingn_le‖e_ledirectly from the vault private-key blob layout.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| fw/plat/uno/fw/pal/src/unwrapping_key_fixture.rs | Adds a temporary hardcoded RSA-2048 fixture key for bring-up seeding. |
| fw/plat/uno/fw/pal/src/part.rs | Implements Uno provision_unwrapping_key: reads GSRAM slot, imports into vault, caches id, clears slot; includes bring-up seeding path. |
| fw/plat/uno/fw/pal/src/lib.rs | Wires the new fixture module into the Uno PAL crate. |
| fw/plat/uno/fw/pal/src/crypto/rsa.rs | Implements rsa_priv_pub_key for Uno by slicing d‖n‖e into the wire n_le‖e_le. |
| fw/plat/uno/fw/drivers/part_store/src/part_store.rs | Adds GSRAM slot accessors for unwrapping-key backup + valid flag + clear/set operations. |
| fw/plat/std/pal/src/part.rs | Moves emulator key provisioning into the new provision_unwrapping_key hook; removes the old property-read side effect. |
| fw/pal/traits/src/part.rs | Adds the new async provision_unwrapping_key hook to the PAL partition manager trait. |
| fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs | Takes the partition lock and calls provision_unwrapping_key before reading RSA_UNWRAPPING_KEY_ID. |
The TBOR GetUnwrappingKey handler read the RSA_UNWRAPPING_KEY_ID property expecting the std PAL's getter-side lazy generation, which the uniform provisioning refactor removed. Call HsmPartitionManager::provision_unwrapping_key under the partition lock before the read (mirroring the MBOR handler) so the key materialises on first use on both the emulator and hardware. UnwrapKey is unchanged — it documents that GetUnwrappingKey must be called first. Fixes the TBOR get_unwrapping_key / unwrap_key emu tests (10/10 pass).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (4)
fw/plat/uno/fw/pal/src/part.rs:552
- The throwaway unwrapping-key fixture is always seeded when the GSRAM slot is invalid. If this path makes it into production, the HSM would provision a hardcoded private key without the HSP ever publishing one, which is a serious security risk. Gate the seeding to debug builds (or remove entirely) and otherwise leave the key-id absent so the handler surfaces PendingKeyGeneration.
part.set_unwrapping_key_bk(
&crate::unwrapping_key_fixture::UNWRAPPING_KEY_TEST_PKA_LE,
)?;
fw/plat/uno/fw/pal/src/part.rs:577
key_bufcontains the raw RSA private key copied out of GSRAM, but it is never securely wiped.UnoScopedAlloconly rewinds watermarks on drop, so the key bytes can linger in DMA scratch. Wipekey_bufwithDmaBuf::zeroize()on both success and error after the vault import completes.
let kid = crate::vault::vault(&admin_io)
.create(
self,
&admin_io,
u8::from(pid),
fw/plat/uno/fw/drivers/part_store/src/part_store.rs:1155
clear_unwrapping_key_bkclaims to "zeroize" the key bytes before clearing the valid flag, but it uses a normal assignment. For secret material this can be optimized away, and without a fence the compiler may reorder the flag store ahead of the wipe (breaking the producer/consumer handshake). UseDmaBuf::zeroize()(volatile per-byte) and fence before clearing the flag.
pub fn clear_unwrapping_key_bk(mut self) {
let slot = self.slot_mut();
slot.unwrapping_key_bk = [0u8; UNWRAPPING_KEY_BK_LEN];
slot.unwrapping_key_bk_valid = false;
}
fw/plat/uno/fw/pal/src/unwrapping_key_fixture.rs:45
- Provide a release-build stub for
UNWRAPPING_KEY_TEST_PKA_LEso the real private key bytes are not compiled into production artifacts.
];
Address PR review on the unwrapping-key provisioning: - Wipe the raw RSA private key from DMA scratch with DmaBuf::zeroize() after the vault import, on success or failure. The bump allocator only rewinds a watermark on drop, so the key would otherwise linger in the DMA region. - clear_unwrapping_key_bk now volatile-wipes the 516-byte GSRAM payload via DmaBuf::zeroize() (+ fence) instead of a plain array assignment, matching the repo secure-wipe pattern so the wipe cannot be optimised away. - Gate the hardcoded RSA-2048 bring-up fixture behind a new, off-by-default `unwrapping-key-fixture` Cargo feature so the private key never ships in a production image. When the feature is off and no key has been published, provision_unwrapping_key returns Ok(()) so GetUnwrappingKey surfaces PendingKeyGeneration and the host retries.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
fw/plat/std/pal/src/part.rs:533
priv_buf.fill(0)is not a reliable secret wipe: the compiler may elide it as a dead store, leaving the generated RSA private key material in process memory. This file already importszeroize::Zeroizing; wrapping the buffer inZeroizingprovides a guaranteed wipe on drop without needing an explicitfill(0).
})();
priv_buf.fill(0);
result
- set_unwrapping_key_bk: add a compiler_fence between copying the key payload and setting unwrapping_key_bk_valid = true so the publish ordering (payload before flag) cannot be reordered by the compiler. - std PAL provision_unwrapping_key: replace priv_buf.fill(0) with a volatile scrub helper (per-byte write_volatile + compiler_fence) so the generated RSA private key bytes are actually cleared and not elided as dead writes. - HsmPartitionManager::provision_unwrapping_key trait doc: correct the stale "emulator generates lazily behind the property read" note — PALs now override this hook (uno imports the HSP-published GSRAM key; std generates + stores).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs:50
- This comment still cites the emulator generating the key "behind the property read", but in this PR the emulator generation happens in
provision_unwrapping_key()(called just below) and the property read is purely a lookup. Updating this avoids future confusion about where the side effects live.
// its id in the property read below. Take the partition lock first so
// the multi-step import is serialised against a concurrent first use;
// the hook is a no-op where the key is materialised by another route
// (e.g. the emulator generates it lazily behind the property read).
let _lock = pal.partition_lock(io).await?;
The uno partition_lock is a no-op (the PAL relies on handlers committing partition writes synchronously). provision_unwrapping_key is the exception: it awaits the GDMA vault import between checking unwrapping_key_id and committing it, so two concurrent first-use GetUnwrappingKey requests for the same partition could both observe None and import the key twice — leaking a vault entry with a last-writer-wins id. Add a runtime per-partition once-guard (AtomicBool CAS + RAII release) around the import so it runs at most once at a time per partition. A losing caller returns Ok(()) leaving the id absent, so GetUnwrappingKey surfaces PendingKeyGeneration and the host retries once the winner commits. The guard is runtime-only and does not touch the persistent PartStore slot layout. Also correct the MBOR get_unwrapping_key module/handler docs: provisioning now runs in provision_unwrapping_key under the partition lock (before the property read), not lazily behind the property getter.
Replace the uno partition_lock no-op with a real per-partition async mutex, mirroring the std reference PAL. The seven shared DDI handlers already take `pal.partition_lock(io).await?` expecting real serialisation (std provides it); the uno no-op silently ignored that intent, which is why the first-use unwrapping-key import needed a bespoke once-guard. - lock.rs: one embassy_sync Mutex per partition in a `static`, over a CriticalSectionRawMutex (Sync, so it can live in a static; the app already enables cortex-m/critical-section-single-core). The critical section is held only while updating the mutex's own state on lock/unlock, so handlers may still `.await` while holding the guard. - part.rs: remove the AtomicBool once-guard (static + ImportGuard) now that provision_unwrapping_key runs under the caller's real partition lock; the check-import-commit sequence is serialised per partition despite the GDMA await, and a second concurrent first use now blocks and observes the committed key rather than getting PendingKeyGeneration. The lock table is runtime-only and does not touch the persistent PartStore slot layout.
The doc claimed wipe-before-flag-clear stops the HSP observing valid==true over stale bytes, which is backwards. The actual guarantee is that the producer (HSP) can never observe valid==false while stale key bytes remain, so a refill write cannot race — and be clobbered by — the wipe. Reword to match the real handshake; zeroize's trailing compiler_fence keeps the flag store from being reordered ahead of the wipe.
set_resource arms Gate 1 (unwrapping_key_required) before provisioning, but the rollback path on a provisioning failure only reset identity/mask/state, leaving Gate 1 armed (and any SP-staged payload) for a partition rolled back to Unallocated — so the SP could keep thinking a key is required. Extract the unwrapping-key teardown (disarm Gate 1, volatile-wipe payload, clear validity) into a shared PartStore::clear_unwrapping_key, used by both clear_state(Disable) and rollback_alloc.
The clear_unwrapping_key refactor left a duplicate zeroing of psk_co, psk_cu, and vm_launch_guid in the clear_state(Disable) branch. Remove the redundant second assignment.
That symbol is from mcr-hsm and does not exist in this repo. Describe the 516-byte payload layout directly (d||n||e PKA-LE; the wire public key is the trailing n||e, derived by rsa_priv_pub_key).
… API Review feedback: drop the provision_unwrapping_key PAL trait method and materialise the RSA-2048 unwrapping key lazily and synchronously inside each PAL's part_prop_get_u16(RSA_UNWRAPPING_KEY_ID). The uno PAL imports the HSP-staged GSRAM key into the vault via a new synchronous KeyVault::create_sync (CPU copy, no GDMA, no await), so the getter needs no new PAL API, no partition lock, and never exposes key bytes as a property. std keeps its existing lazy provisioning (unchanged from main). key_vault: add create_sync and factor the shared slot/block allocation and attribute staging into stage_key_slot, so create and create_sync differ only in how the key material is written (async GDMA vs sync CPU copy). To keep the change minimal, revert the pieces that only existed to support the removed trait hook: the real per-partition partition_lock (back to the main no-op), the std provision refactor, and the trait method itself. HW: get_unwrapping_key 7/7 on the Manticore EVB (incl. multi-threaded stress).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
fw/plat/uno/fw/crates/key_vault/src/vault.rs:121
create_syncis new public behavior that is now relied on for Uno unwrapping-key import, but the key-vault unit tests only exercise the asynccreatepath (nocreate_synccoverage). Adding tests forcreate_sync(success path + invalid-size/space errors) would help prevent regressions, especially since it bypasses the GDMA write/evict rollback path.
pub fn create_sync(
&mut self,
app_id: u8,
key: &DmaBuf,
kind: HsmVaultKeyKind,
session: Option<u16>,
attrs: HsmVaultKeyAttrs,
) -> HsmResult<HsmKeyId> {
fw/plat/uno/fw/crates/key_vault/src/vault.rs:139
- The doc comment says “The entry is not live for lookups until the key write completes”, but
stage_key_slotimmediately setskindanddisabled = falseon the entry, andentry()treats any non-free, non-disabled entry as live. This is misleading; the actual guarantee is that the key id is not returned to callers until after the key material write (or it’s rolled back viaevict).
/// The block run is reserved directly in the table's bitmap (in place — no
/// working copy). The entry is not live for lookups until the key write
/// completes.
Summary
Brings up GetUnwrappingKey end-to-end on the uno HSM firmware using the real HSP↔CP GSRAM protocol (matching mcr-hsm PR #200287). The partition RSA-2048 unwrapping key is staged by the SP into GSRAM, imported into the vault on first use, and returned (public key + masked-key envelope) to the host.
What changed
HsmPartitionManager::provision_unwrapping_key(io)(default no-op). The sharedGetUnwrappingKeyhandler takes the partition lock and calls it before reading the key id, so materialisation is serialised against concurrent first use.unwrapping_key_required(offset 22, wasreserved1): CP-only flag the SP reads. The uno PAL arms it onSetResourcewhenmask != 0(mirrors the referencepart_init); disarms it on partition deallocation.unwrapping_key_bk_valid(offset 23,u8): the SP stages the 516-byte PKA-LE key (d‖n‖e) intounwrapping_key_bk(offset 24) and marks this non-zero.vault_key_create(Rsa2kPrivate)) and records the id. The key persists in the GSRAM slot (it is not consumed on import) — on partition erase the vault key + id are wiped but the SP slot survives, so the next first-use re-imports. On full deallocation (clear_state(Disable)) Gate 1 is disarmed and the payload volatile-wiped; a laterSetResourcere-arms and the SP re-stages.part_prop_get_u16getter side-effect), so both PALs provision uniformly.partition_lockgenuinely serialises the await-crossing import.rsa_priv_pub_key(uno): derives the wire public keyn_le‖e_lefrom the vault blob.Validation
get_unwrapping_keyDDI suite 7/7 — public-key derivation, masked envelope, multi-threaded stress, stable-across-calls.multi_threaded_stress.-D warningsclean.Follow-up (not in this PR)
UnwrappingKeyValidityenum + one-shot PCT (pairwise consistency test) on the freshly imported key, to match mcr-hsm's FIPS behavior.RsaUnwrapneeds the uno RSA private-key mod-exp brought up on hardware — tracked separately.