feat(tbor): implement RSA-AES key unwrap (GetUnwrappingKey + UnwrapKey)#583
feat(tbor): implement RSA-AES key unwrap (GetUnwrappingKey + UnwrapKey)#583vsonims wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds the TBOR RSA‑AES key import command pair (GetUnwrappingKey / UnwrapKey) to allow a host to wrap key material to a partition and re-import it as a masked (unmask-on-use) blob, plus supporting plumbing (shared session/masking helpers, HashAlgo wire enum) and tests/docs. It also updates the SessionEx schedule to use a 32‑byte AES‑GCM masking key (TBOR) while keeping legacy MBOR’s 80‑byte CBC masking key schedule.
Changes:
- Introduces TBOR opcodes
0x13(GetUnwrappingKey) and0x14(UnwrapKey) end-to-end: wire types, dispatcher routing, handlers, host wrappers, and docs. - Adds shared TBOR helper(s) for active-session validation and resolving per-scope masking keys; moves/introduces
HashAlgoin sharedkey_props. - Extends key decode to support HMAC key material (32/48/64 B →
_HmacSha*) and adds TBOR derive support for “reserve + fill later” for mutable buffer fields.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| fw/plat/uno/fw/pal/src/session.rs | Updates SessionEx blob sizing/validation to use 32-byte masking key for TBOR. |
| fw/plat/std/pal/src/session.rs | Updates SessionEx blob sizing and makes session masking-key extraction length-aware (legacy vs SessionEx). |
| fw/pal/traits/src/lib.rs | Changes SESSION_MASKING_KEY_LEN to 32 and clarifies it’s for TBOR SessionEx masking. |
| fw/core/lib/src/op.rs | Classifies new TBOR opcodes as InSession for session-flag validation. |
| fw/core/lib/src/ddi/tbor/unwrap_key.rs | New TBOR UnwrapKey handler: unwrap, decode, scope-mask, and respond (incl. pubkey for RSA/ECC). |
| fw/core/lib/src/ddi/tbor/session_open_finish.rs | Updates SessionEx schedule documentation to 32-byte AES-GCM masking key. |
| fw/core/lib/src/ddi/tbor/mod.rs | Adds opcode constants, dispatch routing, active-session validation helper, and masking-key resolver helper. |
| fw/core/lib/src/ddi/tbor/get_unwrapping_key.rs | New TBOR GetUnwrappingKey handler: derives and returns partition RSA unwrapping public key. |
| fw/core/lib/src/ddi/mod.rs | Adds a shared hmac_hash helper at the command level for both codecs. |
| fw/core/lib/src/ddi/mbor/hmac.rs | Switches to shared crate::ddi::hmac_hash. |
| fw/core/lib/src/ddi/mbor/from_pal.rs | Removes the duplicated MBOR-only hmac_hash mapping helper. |
| fw/core/key-decode/src/lib.rs | Adds HMAC as a decode class and wires it into decode(). |
| fw/core/key-decode/src/hmac.rs | New HMAC decode path (length-based classification). |
| fw/core/ddi/tbor/types/src/unwrap_key.rs | New TBOR wire schema for UnwrapKey. |
| fw/core/ddi/tbor/types/src/lib.rs | Exposes new TBOR type modules (get_unwrapping_key, unwrap_key). |
| fw/core/ddi/tbor/types/src/key_props.rs | Expands key-props to include shared HashAlgo wire enum. |
| fw/core/ddi/tbor/types/src/get_unwrapping_key.rs | New TBOR wire schema for GetUnwrappingKey. |
| fw/core/ddi/tbor/derive/src/schema.rs | Documents semantics of mutable fields and the reserve+fill pattern. |
| fw/core/ddi/tbor/derive/src/codegen_enc.rs | Implements encoder generation for <field>_reserve(len) for mutable buffers. |
| docs/tbor-ddi/README.md | Adds 0x13/0x14 to the TBOR command table. |
| docs/tbor-ddi/commands/unwrap_key.md | New documentation page for UnwrapKey. |
| docs/tbor-ddi/commands/get_unwrapping_key.md | New documentation page for GetUnwrappingKey. |
| ddi/tbor/types/tests/commands/unwrap_key.rs | New emu integration tests for UnwrapKey (AES + HMAC). |
| ddi/tbor/types/tests/commands/mod.rs | Registers new TBOR command tests. |
| ddi/tbor/types/tests/commands/get_unwrapping_key.rs | New emu integration tests for GetUnwrappingKey (length/non-zero/stability). |
| ddi/tbor/types/src/unwrap_key.rs | New host-side TBOR request/response wrappers for UnwrapKey. |
| ddi/tbor/types/src/lib.rs | Re-exports new host-side command wrappers. |
| ddi/tbor/types/src/get_unwrapping_key.rs | New host-side TBOR request/response wrappers for GetUnwrappingKey. |
| //! `GetUnwrappingKey` is an in-session command that returns the | ||
| //! partition's RSA-2048 **unwrapping** public key, which the host uses to | ||
| //! RSA-AES key-wrap a payload for [`UnwrapKey`](crate::hmac). The | ||
| //! unwrapping key is a device-provisioned partition-internal key; only its | ||
| //! public half is returned (the private half never leaves the device and |
| assert_eq!(frame.masked_key(), &masked[..]); | ||
| assert_eq!(frame.pub_key(), &pub_key[..]); | ||
| } | ||
| } |
| match scope { | ||
| HsmKeyScope::Session => pal.session_masking_key(io, sess_id), | ||
| _ => { | ||
| let mk_key_id = masking_key_id_for_scope(pal, io, scope)?; | ||
| pal.vault_key(io, mk_key_id) |
|
Update: this PR now also introduces encoder "reserve + fill" support in the TBOR derive. For any |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
fw/core/ddi/tbor/types/src/get_unwrapping_key.rs:9
- The rustdoc link for
UnwrapKeypoints tocrate::hmac, which is unrelated and breaks navigation for readers of the wire schema docs.
//! `GetUnwrappingKey` is an in-session command that returns the
//! partition's RSA-2048 **unwrapping** public key, which the host uses to
//! RSA-AES key-wrap a payload for [`UnwrapKey`](crate::hmac). The
//! unwrapping key is a device-provisioned partition-internal key; only its
fw/core/ddi/tbor/types/src/unwrap_key.rs:59
- The constants are described as being "pinned" into
#[tbor(... max_len = ...)]literals, but there are no compile-time assertions to prevent them from drifting out of sync (a pattern used in other TBOR type schemas).
pub const UNWRAP_PUB_KEY_MAX_LEN: usize = 520;
fw/core/lib/src/ddi/tbor/mod.rs:229
resolve_masking_keyusespal.session_masking_key()forKeyScope::Session, but the Uno PAL currently returnsUnsupportedCmdfromsession_masking_key(fw/plat/uno/fw/pal/src/session.rs:333-335). This makesSession-scoped masking fail on Uno/hardware even though the docs and handler comments indicate it should work for any Active session.
) -> HsmResult<&'p DmaBuf> {
match scope {
HsmKeyScope::Session => pal.session_masking_key(io, sess_id),
_ => {
let mk_key_id = masking_key_id_for_scope(pal, io, scope)?;
pal.vault_key(io, mk_key_id)
}
| if self.data_offset + len > azihsm_fw_ddi_tbor::MAX_DATA_SIZE { | ||
| return Err(azihsm_fw_hsm_pal_traits::HsmError::TborDataTooLarge); | ||
| } | ||
| self.data_offset += len; | ||
| let word = azihsm_fw_ddi_tbor::toc::build_toc_offset_len(#toc_type_id, len, off); |
| blob under the requested scope's masking key. This is the TBOR analogue | ||
| of MBOR `RsaUnwrap`, but it re-masks the recovered key (unmask-on-use) | ||
| instead of vaulting it under a key handle. Nothing is persisted on the | ||
| device, so the command records no rollback on the undo log. | ||
|
|
Add the TBOR Hash command (opcode 0x1B): within an open session, compute a SHA-256 / 384 / 512 digest of a host-supplied message and return it. A pure hashing utility — no key, no scope, no partition state — the TBOR analogue of MBOR ShaDigest. Named generically (Hash rather than ShaDigest) to leave room for future XOF / SHAKE variants. Uses the reserve-then-fill pattern: the response frame is encoded with the digest slot reserved, then the PAL hashes straight into it (natural big-endian output) — no intermediate buffer, no copy. Reuses the shared HashAlgo enum from key_props. Wire schemas (fw + host mirror); dispatch / classifier / op.rs wiring (in-session, session-id cross-checked, default-PSK gated). Emu tests verify device digests byte-for-byte against host-computed SHA (azihsm_crypto) for all three algorithms over short / empty / long messages, plus an unknown-algo reject. Per-command docs added. Based on tbor/unwrap-key (#583) for the shared HashAlgo enum; uses opcode 0x1B to avoid collision with the sibling AES / ECC / RSA crypto branches (0x15..0x1A). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
The TBOR SessionEx (CU/CO) per-session `masking_key` was carried as an 80 B `aes32 ‖ hmac48` key shaped for the legacy `key_masking::cbc` MBOR masked-key system, even though TBOR masked keys are (and will be) wrapped with `key_masking::aead` (AES-256-GCM), which needs only a 32 B key. The extra 48 B were dead weight in every CU/CO session blob and blurred the key's purpose. Reshape the SessionEx masking key to a native 32 B AES-256-GCM key: * `SESSION_MASKING_KEY_LEN` 80 → 32 (CU/CO only). The legacy MBOR `Session` blob keeps its 80 B cbc key via the std/uno-PAL-local `SESSION_MASKING_KEY_SIZE`, so MBOR's cbc masked-key system is unaffected. * std + uno PAL: CU/CO blob shrinks (CU 120→72 B, CO 216→168 B); the std `session_masking_key` reader now sizes the returned key by blob kind — 32 B for SessionEx blobs, 80 B for the legacy Session blob — so MBOR still reads its full 80 B key. `session_promote` validation follows the const. * `session_open_finish` derives the 32 B key (auto via the const); the `bmk_session` envelope is variable-length (`BMK_SESSION_MAX_LEN`), so the shorter payload needs no schema change. Foundational for the upcoming TBOR HMAC/crypto masked-key commands, which resolve a scope's 32 B AEAD masking key uniformly (Session → this key). Validation: TBOR emu 108/108 (CU + CO sessions, close, reopen, SD roundtrips); MBOR emu 225/225 (masked_key, unmask_key, aes_gen, live-migration/reopen — legacy 80 B path intact); std + uno fmt + clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
Relocate the `HsmVaultKeyKind -> HsmHashAlgo` HMAC mapping out of the MBOR-only `mbor::from_pal` module up to the codec-neutral `ddi` module (alongside `recover_bk_boot`), so the upcoming TBOR HMAC commands can reuse it without depending on `mbor`. Pure refactor: MBOR's `Hmac` handler now calls `crate::ddi::hmac_hash`; behavior is unchanged. Validation: MBOR HMAC emu 36/36 (fixed + var-len HMAC, all SHA variants, sign-permission, KBKDF-derived, masked-key HMAC); fmt + clippy clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
Add the TBOR key-import pair that lets a host RSA-AES-wrap a key to a partition and re-import it as a masked (unmask-on-use) blob — the TBOR analogue of MBOR `RsaUnwrap`, but re-masking the recovered key under a scope's masking key instead of vaulting it under a key handle. GetUnwrappingKey (opcode 0x13): return the partition's RSA-2048 unwrapping public key (`n_le(256) ‖ e_le(4)`). The private half never leaves the device; `UnwrapKey` resolves it internally by the partition's `RSA_UNWRAPPING_KEY_ID` property. The key is materialised lazily (std PAL) or in the background from partition init (hardware); an absent key surfaces as `PendingKeyGeneration` so the host retries. UnwrapKey (opcode 0x14): within an open CO/CU session, unwrap a host-supplied `RSA-OAEP(KEK) ‖ AES-KWP(key)` blob with the unwrapping key and return the recovered key masked (AEAD-GCM-256) under the requested scope's masking key. Supports the AES, RSA (plain/CRT), ECC, and HMAC key classes via the shared `azihsm_fw_hsm_key_unwrap` / `azihsm_fw_hsm_key_decode` crates; the decode crate gains an HMAC path (32/48/64 B -> _HmacShaN). The re-derived wire public key is returned for the asymmetric classes. As the first general crypto command in the TBOR stack, this introduces the shared session/masking helpers `validate_active_session` and `resolve_masking_key(scope, sess_id)`, and moves the `HashAlgo` wire enum into the shared `key_props` module. Encoder "reserve + fill" support: the TBOR derive now emits a `<field>_reserve(len)` setter for `#[tbor(mutable)]` buffer / sealed_key fields, which reserves the slot without copying so the response can be built with reserved slots and then filled in place via `decode_mut` (zero-copy, mirroring MBOR's `reserve` / `from_layout`). GetUnwrappingKey uses it to have the PAL derive the wire public key straight into the response — no scratch buffer, no copy. Wires both opcodes through the fw dispatcher, the `is_known_opcode` / `is_in_session` / `needs_session_id_cross_check` classifiers, and `op::SessionCtrl::from_tbor_opcode`. Adds fw + host wire schemas, emu round-trip tests (GetUnwrappingKey; UnwrapKey AES + HMAC key-class), and command docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258 Mask RSA-4096 keys within the fixed per-IO DMA budget: the recovered key is vaulted transiently (created inside an allocation scope that frees the multi-KB unwrap/decode scratch, masked from vault storage — which lives outside the per-IO DMA arena — then deleted, registered in the undo log for abort-safe cleanup), mirroring MBOR RsaUnwrap. The masked blob and wire public key are derived straight into reserved response slots (no scratch buffers), and aead::mask no longer stages a redundant plaintext copy. This keeps RSA-3072/4096 (CRT and non-CRT) UnwrapKey within 8 KB; new emu tests cover RSA-4096 CRT and non-CRT.
Add the TBOR RsaModExp command (opcode 0x1A): the RSA private-key primitive x = y^d mod n using a caller-held masked RSA private key (imported via UnwrapKey with the RSA / RSA-CRT key class). The device unmasks the key in place (recovering its modulus size and CRT form from the blob's key kind), checks the op's usage attribute (Decrypt -> decrypt, Sign -> sign), computes the modular exponentiation into the reserved response slot, and scrubs the recovered key on every path. The raw primitive underlying RSA decrypt / sign — the host applies and removes any padding. TBOR analogue of MBOR RsaModExp; there is no TBOR RSA key generation, so RSA keys enter only through UnwrapKey. Wire schemas (fw + host mirror) with a 1-byte RsaOp selector; dispatch / classifier / op.rs wiring; a strict from_pal::rsa_key mapping (rejects non-RSA-private kinds as InvalidKeyType). Emu tests cover host-verified RSA sign/decrypt roundtrips for 2K/3K/4K and 4K-CRT through the full UnwrapKey(Rsa) -> RsaModExp path, plus wrong-y-length and wrong-key-class rejects. Per-command docs added. Based on tbor/unwrap-key (#583), which provides the DMA-budget-efficient UnwrapKey that lets RSA-3072/4096 keys be imported within 8 KB. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
Add the TBOR Hash command (opcode 0x1B): within an open session, compute a SHA-256 / 384 / 512 digest of a host-supplied message and return it. A pure hashing utility — no key, no scope, no partition state — the TBOR analogue of MBOR ShaDigest. Named generically (Hash rather than ShaDigest) to leave room for future XOF / SHAKE variants. Uses the reserve-then-fill pattern: the response frame is encoded with the digest slot reserved, then the PAL hashes straight into it (natural big-endian output) — no intermediate buffer, no copy. Reuses the shared HashAlgo enum from key_props. Wire schemas (fw + host mirror); dispatch / classifier / op.rs wiring (in-session, session-id cross-checked, default-PSK gated). Emu tests verify device digests byte-for-byte against host-computed SHA (azihsm_crypto) for all three algorithms over short / empty / long messages, plus an unknown-algo reject. Per-command docs added. Based on tbor/unwrap-key (#583) for the shared HashAlgo enum; uses opcode 0x1B to avoid collision with the sibling AES / ECC / RSA crypto branches (0x15..0x1A). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0b09e50a-a9be-4bae-b347-d42dc775a258
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 29 out of 29 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (3)
fw/core/ddi/tbor/types/src/get_unwrapping_key.rs:9
- Rustdoc link target is incorrect:
crate::hmacdoesn’t exist in this module and will produce a broken intra-doc link. This should point to theunwrap_keyschema/module.
//! `GetUnwrappingKey` is an in-session command that returns the
//! partition's RSA-2048 **unwrapping** public key, which the host uses to
//! RSA-AES key-wrap a payload for [`UnwrapKey`](crate::hmac). The
//! unwrapping key is a device-provisioned partition-internal key; only its
fw/core/ddi/tbor/types/src/unwrap_key.rs:59
- The max-len constants are described as “pinned” to
#[tbor(... max_len = ...)]literals, but there are noconst _: () = assert!(...)tripwires to keep them in sync. Other TBOR schemas use these asserts to prevent silent drift (e.g.get_unwrapping_key.rs).
/// Max recovered public-key length (RSA-4096 `n_le(512) ‖ e_le(4)`).
/// Pinned into the `#[tbor(buffer, max_len = 520)]` literal on
/// [`TborUnwrapKeyResp::pub_key`].
pub const UNWRAP_PUB_KEY_MAX_LEN: usize = 520;
fw/core/lib/src/ddi/tbor/mod.rs:239
resolve_masking_keyassumesHsmKeyScope::Sessioncan always be satisfied viapal.session_masking_key(...), but on the Uno PAL this method currently returnsUnsupportedCmd(seefw/plat/uno/fw/pal/src/session.rs:333-335). This meansSession-scoped masking (documented as supported) will fail on hardware unless the Uno PAL implementssession_masking_keyforSessionExblobs (32-byte AES-GCM key).
/// Resolve the 32-byte AES-256-GCM masking key material for `scope`,
/// returning a borrow of the on-device key.
///
/// `Session` scope resolves to the per-session masking key
/// ([`HsmSessionManager::session_masking_key`](azihsm_fw_hsm_pal_traits::HsmSessionManager::session_masking_key));
/// every other scope resolves via [`masking_key_id_for_scope`] and reads
/// the vault key. All scopes yield a 32-byte key suitable for the
/// `key_masking::aead` (AES-GCM-256) masked-key system.
fn resolve_masking_key<'p, P: HsmPal>(
pal: &'p P,
io: &impl HsmIo,
scope: HsmKeyScope,
sess_id: HsmSessId,
) -> HsmResult<&'p DmaBuf> {
match scope {
HsmKeyScope::Session => pal.session_masking_key(io, sess_id),
_ => {
let mk_key_id = masking_key_id_for_scope(pal, io, scope)?;
pal.vault_key(io, mk_key_id)
}
}
| let key_id = pal | ||
| .alloc_scoped_async(io, async |_scope| -> HsmResult<HsmKeyId> { | ||
| let material = unwrap_key( | ||
| pal, | ||
| io, | ||
| UnwrapParams { | ||
| unwrap_key_id, | ||
| oaep_hash, | ||
| wrapped_blob, | ||
| }, | ||
| ) | ||
| .await?; | ||
| let decoded = decode(pal, io, material, dclass).await?; | ||
| // Transient, partition-scoped (`None` session): deleted below. | ||
| pal.vault_key_create(io, decoded.material, decoded.kind, None, attrs) | ||
| .await | ||
| }) | ||
| .await?; |
| mask( | ||
| pal, | ||
| io, | ||
| alloc, | ||
| AeadAlg::AesGcm256, | ||
| masking_key, | ||
| ¶ms, | ||
| priv_blob, | ||
| Some(out.masked_key), | ||
| ) | ||
| .await | ||
| .map(|_| ()) |
Summary
Implements the TBOR key-import pair that lets a host RSA-AES-wrap a
key to a partition and re-import it as a masked (unmask-on-use) blob
— the TBOR analogue of MBOR
RsaUnwrap, but re-masking the recoveredkey under a scope's masking key instead of vaulting it under a key
handle.
GetUnwrappingKey(opcode0x13)Returns the partition's RSA-2048 unwrapping public key in HSM wire
format (
n_le(256) ‖ e_le(4)). The private half never leaves thedevice;
UnwrapKeyresolves it internally by the partition'sRSA_UNWRAPPING_KEY_IDproperty. Materialised lazily (std/emulator PAL)or generated in the background from partition init (hardware); an absent
key surfaces as
PendingKeyGenerationso the host retries. Both CO/CU.UnwrapKey(opcode0x14)Within an open CO/CU session, unwrap a host-supplied
RSA-OAEP(KEK) ‖ AES-KWP(key)blob with the partition unwrapping key andreturn the recovered key masked (AEAD-GCM-256) under the requested
scope's masking key. Supports the AES, RSA (plain/CRT), ECC, and HMAC
key classes via the shared
azihsm_fw_hsm_key_unwrap/azihsm_fw_hsm_key_decodecrates; the decode crate gains an HMAC path(32/48/64 B →
_HmacShaN). The re-derived wire public key is returnedfor the asymmetric (RSA/ECC) classes; symmetric classes return an empty
pub_key. Persists nothing. Both CO/CU.Shared helpers introduced here
validate_active_sessionandresolve_masking_key(scope, sess_id)(Session → per-session key; Ephemeral/Local/SecurityDomain → scope
vault key).
HashAlgowire enum, moved into the sharedkey_propsmodule (itselects the OAEP hash here and, in feat(tbor): implement HmacGenerateKey + Hmac (masked-key HMAC crypto) #580, the HMAC SHA variant).
Tests / validation
GetUnwrappingKey;UnwrapKeyAES (masked, nopubkey) and HMAC key-class (recovered kind =
_HmacSha256). The fullunwrap→MAC round-trip lives in feat(tbor): implement HmacGenerateKey + Hmac (masked-key HMAC crypto) #580, which builds on this command.
clippyclean;nightly
fmt+copyrightclean; Uno (thumbv7em) build passes.docs/tbor-ddi/commands/{get_unwrapping_key,unwrap_key}.md+README rows (
0x13,0x14).Endianness note (host wrap)
GetUnwrappingKeyreturns the modulus/exponent little-endian, and thedevice expects the OAEP ciphertext in wire-LE (it flips to big-endian
internally for OpenSSL). The emu test's host-side wrap reverses the
pubkey components (LE→BE for
from_hsm_bytes) and the RSA-OAEPciphertext (BE→LE) accordingly.