Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
afcb03b
feat(uno/unwrap): expose GSRAM unwrapping-key slot + derive RSA publi…
radutta99 Jul 23, 2026
b6adde6
feat(uno/unwrap): provision RSA unwrapping key from GSRAM on first use
radutta99 Jul 24, 2026
e31747d
refactor(std): provision unwrapping key via the shared PAL hook
radutta99 Jul 24, 2026
ba2ddd8
Merge remote-tracking branch 'azure-github/main' into user/radutta/ge…
radutta99 Jul 24, 2026
6b79fa8
fix(tbor): materialise unwrapping key via the provision hook
radutta99 Jul 24, 2026
2797399
fix(uno/unwrap): address review — wipe key scratch, gate fixture
radutta99 Jul 24, 2026
1cb7828
fix(unwrap): address review round 2 — fences, volatile scrub, docs
radutta99 Jul 24, 2026
3e2b2ad
fix(uno/unwrap): serialise first-use import with a per-partition guard
radutta99 Jul 24, 2026
6aa37d5
refactor(uno): implement a real partition_lock, drop the ad-hoc guard
radutta99 Jul 25, 2026
fc6f680
docs(uno/part_store): correct clear_unwrapping_key_bk ordering rationale
radutta99 Jul 25, 2026
aef11aa
feat(uno/unwrap): arm Gate 1 for the SP; drop the throwaway fixture
radutta99 Jul 25, 2026
0b32d87
refactor(unwrap): scope partition lock to first-use materialisation
radutta99 Jul 25, 2026
dcfe3c9
fix(uno/part_store): volatile access for CP<->SP mailbox bytes
radutta99 Jul 25, 2026
9ecffce
fix(uno/part_store): acquire fence after the unwrapping-key validity …
radutta99 Jul 25, 2026
823d343
fix(uno): reset unwrapping-key gate on allocation rollback
radutta99 Jul 25, 2026
8cf914e
fix(uno/part_store): drop duplicate psk/guid zeroing in clear_state
radutta99 Jul 25, 2026
0aec418
docs(uno/part_store): fix stale RsaPubKey::from_priv_pka_slice reference
radutta99 Jul 25, 2026
55dadf3
uno/unwrap: materialise unwrapping key in the property getter, no PAL…
radutta99 Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
//! expensive, so it is never done at partition enable — each PAL
//! materialises the key behind the property read instead: the std
//! (emulator) PAL generates it lazily and synchronously on first read,
//! while hardware PALs generate it in the background from partition
//! init and leave the property unset until ready. An absent id
//! therefore means generation is still pending, which this handler
//! while hardware PALs import it (staged in GSRAM by the HSP) on first
//! read and leave the property unset until it is available. An absent
//! id therefore means the key is not yet available, which this handler
//! surfaces as `PendingKeyGeneration` so the host retries. No public
//! key is cached: it is derived from the vault
//! private key on demand (matching the reference firmware).
Expand Down
6 changes: 3 additions & 3 deletions fw/core/lib/src/ddi/tbor/get_unwrapping_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
//!
//! RSA key generation is expensive, so each PAL materialises the key
//! behind the property read: the std (emulator) PAL generates it lazily on
//! first read, while hardware PALs generate it in the background from
//! partition init and leave the property unset until ready. An absent id
//! therefore means generation is still pending, surfaced as
//! first read, while hardware PALs import it (staged in GSRAM by the HSP)
//! on first read and leave the property unset until it is available. An
//! absent id therefore means the key is not yet available, surfaced as
//! `PendingKeyGeneration` so the host retries. Available to both
//! Crypto-Officer and Crypto-User sessions.

Expand Down
87 changes: 68 additions & 19 deletions fw/plat/uno/fw/crates/key_vault/src/vault.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,76 @@ impl<S: TableStorage> KeyVault<S> {
session: Option<u16>,
attrs: HsmVaultKeyAttrs,
) -> HsmResult<HsmKeyId> {
let (table, slot, start) = self.stage_key_slot(app_id, kind, session, attrs, key.len())?;

// Write key material. On failure, use evict to clean up the staged
// region (scrub + free the run).
if let Err(e) = self.write_key(gdma, io, table, start, key).await {
let entry = *self.storage.entry(table, slot)?;
self.evict(gdma, io, table, slot, entry).await?;
return Err(e);
}

Ok(make_key_id(table, slot))
}

/// Synchronously stores a new key by CPU copy, returning its
/// [`HsmKeyId`].
///
/// Identical to [`create`](Self::create) but copies the key material with
/// the CPU (`copy_from_slice`) instead of routing large keys through the
/// GDMA engine, so it needs no [`HsmGdmaController`] and — crucially — has
/// no `.await`. This lets a synchronous, non-yielding caller (e.g. lazy
/// materialisation inside a `part_prop_get`) create a key atomically with
/// respect to the cooperative scheduler. The vault blob is CPU-writable
/// (the attribute blob is always CPU-copied), so the only cost versus
/// `create` is a slower copy for large keys; intended for small, fixed-size
/// internal keys such as the RSA-2048 unwrapping key. Because a
/// `copy_from_slice` cannot fail after staging, there is no key-write
/// rollback path.
///
/// # Errors
///
/// Same size/space errors as [`create`](Self::create).
pub fn create_sync(
&mut self,
app_id: u8,
key: &DmaBuf,
kind: HsmVaultKeyKind,
session: Option<u16>,
attrs: HsmVaultKeyAttrs,
) -> HsmResult<HsmKeyId> {
let (table, slot, start) = self.stage_key_slot(app_id, kind, session, attrs, key.len())?;

// Synchronous CPU copy of the key material (no GDMA, no `.await`).
let len = key.len();
let key_off = usize::from(start) * BLOCK_ALIGNMENT + ATTRIBUTES_BLOB_SIZE;
self.storage.blob_mut(table)?[key_off..key_off + len].copy_from_slice(key);

Ok(make_key_id(table, slot))
}

/// Finds a table the partition owns with a free entry slot and block run,
/// stages the entry metadata and attribute blob for a key of `key_bytes`
/// bytes, and returns `(table, slot, block_start)`. The caller writes the
/// key material (and, on the fallible GDMA path, rolls 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.
fn stage_key_slot(
&mut self,
app_id: u8,
kind: HsmVaultKeyKind,
session: Option<u16>,
attrs: HsmVaultKeyAttrs,
key_bytes: usize,
) -> HsmResult<(usize, usize, u16)> {
let spec = key_len(kind)?;
let persisted_len = spec.check(key.len())?;
let persisted_len = spec.check(key_bytes)?;
let total = storage_bytes(persisted_len);
let need = blocks_for(total);

// Find a table the partition owns with both a free entry slot and
// a free block run.
let mut saw_defrag = false;
for table in 0..self.storage.table_count() {
if !self.storage.is_valid_table(table) {
Expand All @@ -97,11 +160,6 @@ impl<S: TableStorage> KeyVault<S> {
continue;
};

// Reserve the run directly in the table's bitmap (in place — no
// working copy is materialized). The reservation is committed
// before the `.await` so nothing bitmap-sized crosses it; on a
// failed key write we roll back: scrub the staged region and
// free the run.
let start = match crate::block::alloc(self.storage.bitmap_mut(table)?, need) {
Ok(s) => s,
Err(HsmError::DefragmentationNeeded) => {
Expand All @@ -112,9 +170,7 @@ impl<S: TableStorage> KeyVault<S> {
Err(e) => return Err(e),
};

// Stage the entry's fields directly in storage. The slot is
// not yet live for lookups until the key write succeeds (a
// failed write rolls it back via `evict`).
// Stage the entry's fields directly in storage.
{
let entry = self.storage.entry_mut(table, slot)?;
entry.set_block_offset(start);
Expand All @@ -133,14 +189,7 @@ impl<S: TableStorage> KeyVault<S> {
}
self.write_attrs(table, start, attrs_blob)?;

// Write key material. On failure, use evict to clean up the staged region.
if let Err(e) = self.write_key(gdma, io, table, start, key).await {
let entry = *self.storage.entry(table, slot)?;
self.evict(gdma, io, table, slot, entry).await?;
return Err(e);
}

return Ok(make_key_id(table, slot));
return Ok((table, slot, start));
}

Err(if saw_defrag {
Expand Down
93 changes: 91 additions & 2 deletions fw/plat/uno/fw/drivers/part_store/src/part_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,16 @@ struct Storage {
version: u8,
flags: [u8; 3],
session_table: [u8; SESSION_TABLE_LEN],
reserved1: u8,
unwrapping_key_bk_valid: bool,
/// Gate-1 staging flag (offset 22, was `reserved1`): CP writes, SP reads.
/// `false` = not armed, `true` = armed. When armed (set on `SetResource`
/// with a non-zero mask), the SP stages the RSA-2048 unwrapping key into
/// `unwrapping_key_bk` and marks `unwrapping_key_bk_valid` non-zero.
unwrapping_key_required: bool,
Comment thread
radutta99 marked this conversation as resolved.
/// Unwrapping-key slot state, written by the SP. `0` = empty; non-zero =
/// occupied (the SP uses a 3-state `UnwrappingKeyValidity` enum —
/// Empty / PendingPct / PctPassed — so this is a `u8`, not a `bool`;
/// reading a value > 1 as a Rust `bool` would be UB).
unwrapping_key_bk_valid: u8,
unwrapping_key_bk: [u8; UNWRAPPING_KEY_BK_LEN],
pin_policy: PinPolicy,
vm_launch_guid: [u8; GUID_LEN],
Expand Down Expand Up @@ -489,6 +497,11 @@ impl Partition {
self.clear_sealed_bk3();
self.set_bk3_initialized(false);
self.set_sd_initialized(false);
// Disarm Gate 1 and wipe the SP-published unwrapping key: the slot
// belongs to a partition that is being deallocated. A later
// `SetResource` re-arms Gate 1, prompting the SP to stage a fresh
// key.
self.clear_unwrapping_key();
let slot = self.slot_mut();
slot.psk_co = [0u8; PSK_LEN];
slot.psk_cu = [0u8; PSK_LEN];
Expand Down Expand Up @@ -1117,6 +1130,82 @@ impl Partition {
self.slot_mut().unwrapping_key_id = write_handle(key);
}

// ── RSA unwrapping-key backup (SP-published) — two-gate CP↔SP handshake ──
//
// Gate 1: the CP arms `unwrapping_key_required` (via `set_unwrapping_key_required`,
// on `SetResource` with a non-zero mask). Gate 2: the SP, seeing Gate 1
// armed, stages a ready-to-use 516-byte PKA-LE RSA-2048 private key into
// `unwrapping_key_bk` and marks `unwrapping_key_bk_valid` non-zero. The HSM
// then imports it into its vault (recording `unwrapping_key_id`). The key
// persists in the slot (it is not consumed on import); it is wiped only on
// partition deallocation. The 516-byte payload is the RSA-2048 private key
// in PKA little-endian order, `d(256) ‖ n(256) ‖ e(4)`; the wire public key
// is the trailing `n ‖ e` (derived on demand by `rsa_priv_pub_key`).

/// Arms/disarms Gate 1 (`unwrapping_key_required`): the CP-only flag the SP
/// reads to decide whether to stage an unwrapping key for this partition.
#[inline(never)]
pub fn set_unwrapping_key_required(mut self, required: bool) {
// Volatile: this is a CP→SP shared-memory mailbox byte. A plain store
// could be reordered or elided by the compiler, breaking the handshake.
// SAFETY: valid, aligned, writable byte in this partition's GSRAM slot.
unsafe {
core::ptr::write_volatile(&mut self.slot_mut().unwrapping_key_required, required);
}
}

/// Whether Gate 1 (`unwrapping_key_required`) is currently armed.
#[inline(never)]
pub fn unwrapping_key_required(self) -> bool {
// Volatile read of the CP↔SP mailbox byte.
// SAFETY: valid, aligned, readable byte in this partition's GSRAM slot.
unsafe { core::ptr::read_volatile(&self.slot().unwrapping_key_required) }
}

/// Whether the SP has published a valid RSA-2048 unwrapping key into this
/// partition's GSRAM slot. The SP writes a `UnwrappingKeyValidity` `u8`
/// (`0` = empty; non-zero = occupied), so occupancy is `!= 0`.
#[inline(never)]
pub fn unwrapping_key_bk_valid(self) -> bool {
// Volatile read: the SP writes this mailbox byte from another processor,
// so a cached/hoisted load could miss the publish.
// SAFETY: valid, aligned, readable byte in this partition's GSRAM slot.
let valid = unsafe { core::ptr::read_volatile(&self.slot().unwrapping_key_bk_valid) } != 0;
// Acquire fence pairing the SP's release before it sets this byte: the
// producer writes the 516-byte `unwrapping_key_bk` payload *before*
// marking this valid, so a consumer that observes it set must not have
// its subsequent payload loads (in `unwrapping_key_bk`) hoisted ahead of
// this observation.
core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::Acquire);
valid
}

/// Borrows the 516-byte PKA-LE RSA-2048 unwrapping-key backup published by
/// the HSP. Only meaningful when
/// [`unwrapping_key_bk_valid`](Self::unwrapping_key_bk_valid) is true.
#[inline(never)]
pub fn unwrapping_key_bk(self) -> &'static DmaBuf {
// SAFETY: `unwrapping_key_bk` is an align-1 packed field; GSRAM bytes
// branded as DMA-accessible; valid for 'static.
unsafe { DmaBuf::from_raw(&self.slot().unwrapping_key_bk) }
}

/// Resets the SP↔CP unwrapping-key slot on partition teardown: disarms
/// Gate 1, volatile-wipes the 516-byte payload (so the private key can't be
/// recovered from GSRAM), and clears the validity byte. A later
/// `SetResource` re-arms Gate 1 and the SP re-stages a fresh key.
#[inline(never)]
pub fn clear_unwrapping_key(mut self) {
let slot = self.slot_mut();
// The gate + validity are CP↔SP mailbox bytes → volatile writes; the
// payload is volatile-wiped. SAFETY: valid, aligned bytes in this slot.
unsafe {
core::ptr::write_volatile(&mut slot.unwrapping_key_required, false);
DmaBuf::from_raw_mut(&mut slot.unwrapping_key_bk).zeroize();
core::ptr::write_volatile(&mut slot.unwrapping_key_bk_valid, 0);
}
}

/// Reads the partition-local masking key (`PartLocalMK`) handle.
#[inline(never)]
pub fn local_mk_key_id(self) -> Option<HsmKeyId> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ const _: () = assert!(STRIDE == STORE_SIZE);
// via DMA, which requires 4-byte alignment (see `Storage` field ordering).
const _: () = assert!(core::mem::offset_of!(Storage, ec_pub_key) % 4 == 0);
const _: () = assert!(core::mem::offset_of!(Storage, se_pub_key) % 4 == 0);

// Pin the SP<->CP gate-byte offsets so the SP's ephemeral-key monitor and this
// slot agree (must match the reference `HsmPartPersistentStore`): Gate 1 at 22,
// the validity byte at 23, and the 516-byte key payload at 24.
const _: () = assert!(core::mem::offset_of!(Storage, unwrapping_key_required) == 22);
const _: () = assert!(core::mem::offset_of!(Storage, unwrapping_key_bk_valid) == 23);
const _: () = assert!(core::mem::offset_of!(Storage, unwrapping_key_bk) == 24);
27 changes: 22 additions & 5 deletions fw/plat/uno/fw/pal/src/crypto/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,29 @@ impl HsmRsa for UnoHsmPal {
fn rsa_priv_pub_key(
&self,
_io: &impl HsmIo,
_priv_key: &DmaBuf,
_pub_out: Option<&mut DmaBuf>,
priv_key: &DmaBuf,
pub_out: Option<&mut DmaBuf>,
) -> HsmResult<usize> {
// TODO: convert the raw vault-format RSA private key into the
// wire public key on Uno PKA (GetUnwrappingKey / RsaUnwrap).
Err(HsmError::UnsupportedCmd)
// The Uno vault RSA private key is the PKA little-endian layout
// `d(k) ‖ n(k) ‖ e(4)` (k = modulus length), the same 516-byte form the
// HSP publishes for RSA-2048, so `priv_key.len() == 2*k + 4`. The wire
// public key is `n_le ‖ e_le` (`k + 4` bytes) — already little-endian,
// so it is exactly the trailing `priv_key[k..]` slice with no
// endianness flip and no PKA operation.
const EXP_WIRE_LEN: usize = 4;
let total = priv_key.len();
if total <= EXP_WIRE_LEN || !(total - EXP_WIRE_LEN).is_multiple_of(2) {
return Err(HsmError::InvalidArg);
}
let modulus_len = (total - EXP_WIRE_LEN) / 2;
let wire_len = modulus_len + EXP_WIRE_LEN;
if let Some(out) = pub_out {
if out.len() < wire_len {
return Err(HsmError::RsaInvalidKeyLength);
}
out[..wire_len].copy_from_slice(&priv_key[modulus_len..total]);
}
Ok(wire_len)
}

fn rsa_priv_der_to_vault(
Expand Down
Loading
Loading