Skip to content

feat(uno): GetUnwrappingKey bring-up — provision RSA unwrapping key from GSRAM - #601

Merged
vsonims merged 18 commits into
mainfrom
user/radutta/get-unwrapping-key
Jul 30, 2026
Merged

feat(uno): GetUnwrappingKey bring-up — provision RSA unwrapping key from GSRAM#601
vsonims merged 18 commits into
mainfrom
user/radutta/get-unwrapping-key

Conversation

@radutta99

@radutta99 radutta99 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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

  • New PAL hook HsmPartitionManager::provision_unwrapping_key(io) (default no-op). The shared GetUnwrappingKey handler takes the partition lock and calls it before reading the key id, so materialisation is serialised against concurrent first use.
  • Two-gate CP↔SP handshake (partition persistent-store slot; offsets pinned by compile-time asserts to the reference layout):
    • Gate 1 unwrapping_key_required (offset 22, was reserved1): CP-only flag the SP reads. The uno PAL arms it on SetResource when mask != 0 (mirrors the reference part_init); disarms it on partition deallocation.
    • Gate 2 unwrapping_key_bk_valid (offset 23, u8): the SP stages the 516-byte PKA-LE key (d‖n‖e) into unwrapping_key_bk (offset 24) and marks this non-zero.
  • uno provisioning: imports the SP-staged key into the vault (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 later SetResource re-arms and the SP re-stages.
  • std/emulator: the same hook generates the key (moved out of the part_prop_get_u16 getter side-effect), so both PALs provision uniformly.
  • Partition lock: the uno PAL now implements a real per-partition async mutex (previously a no-op), matching the std PAL; the shared handler's partition_lock genuinely serialises the await-crossing import.
  • rsa_priv_pub_key (uno): derives the wire public key n_le‖e_le from the vault blob.

Validation

  • Hardware (Manticore EVB): get_unwrapping_key DDI suite 7/7 — public-key derivation, masked envelope, multi-threaded stress, stable-across-calls.
  • Emulator (mock/std PAL): unwrap suite 36/36, incl. multi_threaded_stress.
  • fmt + taplo + clippy -D warnings clean.

Follow-up (not in this PR)

  • Adopt the full 3-state UnwrappingKeyValidity enum + one-shot PCT (pairwise consistency test) on the freshly imported key, to match mcr-hsm's FIPS behavior.
  • RsaUnwrap needs the uno RSA private-key mod-exp brought up on hardware — tracked separately.

…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).
Copilot AI review requested due to automatic review settings July 24, 2026 21:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from get_unwrapping_key under 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_key by deriving n_le‖e_le directly 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.

Comment thread fw/plat/uno/fw/pal/src/part.rs Outdated
Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs Outdated
Comment thread fw/plat/uno/fw/pal/src/part.rs Outdated
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).
Copilot AI review requested due to automatic review settings July 24, 2026 22:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_buf contains the raw RSA private key copied out of GSRAM, but it is never securely wiped. UnoScopedAlloc only rewinds watermarks on drop, so the key bytes can linger in DMA scratch. Wipe key_buf with DmaBuf::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_bk claims 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). Use DmaBuf::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_LE so the real private key bytes are not compiled into production artifacts.
];

Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs Outdated
Comment thread fw/plat/std/pal/src/part.rs Outdated
Comment thread fw/pal/traits/src/part.rs Outdated
Comment thread fw/plat/uno/fw/pal/src/unwrapping_key_fixture.rs Outdated
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.
Copilot AI review requested due to automatic review settings July 24, 2026 22:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 imports zeroize::Zeroizing; wrapping the buffer in Zeroizing provides a guaranteed wipe on drop without needing an explicit fill(0).
        })();
        priv_buf.fill(0);
        result

Comment thread fw/plat/uno/fw/pal/src/part.rs Outdated
- 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).
Copilot AI review requested due to automatic review settings July 24, 2026 22:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?;

Comment thread fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs Outdated
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.
Copilot AI review requested due to automatic review settings July 24, 2026 23:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

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.
Copilot AI review requested due to automatic review settings July 25, 2026 00:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs Outdated
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.
Copilot AI review requested due to automatic review settings July 25, 2026 00:33
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.
Copilot AI review requested due to automatic review settings July 25, 2026 06:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs Outdated
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.
Copilot AI review requested due to automatic review settings July 25, 2026 06:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs Outdated
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).
Copilot AI review requested due to automatic review settings July 25, 2026 06:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Comment thread fw/pal/traits/src/part.rs Outdated
Comment thread fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs Outdated
Comment thread fw/core/lib/src/ddi/tbor/get_unwrapping_key.rs Outdated
Comment thread fw/plat/std/pal/src/part.rs Outdated
Comment thread fw/plat/uno/fw/drivers/part_store/src/part_store.rs
… 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).
Copilot AI review requested due to automatic review settings July 29, 2026 23:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_sync is new public behavior that is now relied on for Uno unwrapping-key import, but the key-vault unit tests only exercise the async create path (no create_sync coverage). Adding tests for create_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_slot immediately sets kind and disabled = false on the entry, and entry() 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 via evict).
    /// 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.

@radutta99
radutta99 requested a review from vsonims July 29, 2026 23:45
@vsonims
vsonims enabled auto-merge July 30, 2026 00:31
@vsonims
vsonims added this pull request to the merge queue Jul 30, 2026
Merged via the queue into main with commit 49d84b6 Jul 30, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants