From 74890297814d78b4558b264faee392e41b19b8e8 Mon Sep 17 00:00:00 2001 From: Vishal Soni Date: Wed, 1 Jul 2026 03:56:30 +0000 Subject: [PATCH] feat(tbor): implement SdSealingKeyGen (masked sealing key) Implement the firmware TBOR SdSealingKeyGen command (previously schema-only). Within an active Crypto-Officer session it generates a fresh P-384 ECC sealing keypair for ECDH key agreement and returns the MASKED private key plus the public key. Maps to manticore CreateSDSealingKey -> ..., MaskedSDSealingKey. The private key is not stored on the device: it is masked (AEAD-GCM-256) under the masking key associated with the requested KeyScope, and the masked blob is returned so the caller re-imports it (unmask-on-use) when the key is later needed. Because nothing is persisted, the command records no rollback on the undo log. Handler (fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs): - Gate: Crypto-Officer-only (InvalidPermissions); session Active; partition Initialized (the scope masking keys are provisioned by PartFinal). - Scope -> masking key: Ephemeral -> PartitionEphemeralMaskingKey, Local -> PartitionLocalMaskingKey. Session and SecurityDomain (and any other) are rejected with the new UnsupportedKeyScope error until their masking keys exist (session-key masking / CreateSD's SDKMK). - Generate the P-384 keypair (KeyAgreement PCT), mask the private scalar under the scope's key, and return (masked_key 180B, pub_key 96B). The SdSealing vault kind is retained as the masked blob's metadata key_kind; it is no longer stored. Schema + dispatch: - Response masked_key (180B AEAD envelope) replaces key_handle in both the firmware and host crates; add MASKED_SEALING_KEY_LEN. - Add HsmError::UnsupportedKeyScope (0x08700102) + TborStatus mirror. - Wire SD_SEALING_KEY_GEN (0x09) into the opcode table, dispatcher, is_known_opcode / is_in_session / session-id cross-check classifiers, and SessionCtrl; the handler takes no undo log (nothing to roll back). - Add the SdSealing vault key kind (P-384, 48 B) across the PAL traits enum, std PAL, and Uno key_vault (metadata only, never stored). Tests + docs: - emu tests (ddi/tbor/types): Ephemeral + Local roundtrip (180B masked key + 96B pubkey, distinct per call), unsupported-scope reject, before-finalize reject, CU reject, default-PSK reject. - Document the masked-key output, scope-based masking, and error set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ddi/tbor/types/src/sd_sealing_key_gen.rs | 36 ++- ddi/tbor/types/src/status.rs | 5 + ddi/tbor/types/tests/commands/mod.rs | 1 + .../tests/commands/sd_sealing_key_gen.rs | 205 +++++++++++++ docs/tbor-ddi/README.md | 2 +- docs/tbor-ddi/commands/sd_sealing_key_gen.md | 51 +++- .../ddi/tbor/types/src/sd_sealing_key_gen.rs | 77 +++-- fw/core/lib/src/ddi/tbor/mod.rs | 29 +- .../lib/src/ddi/tbor/sd_sealing_key_gen.rs | 272 ++++++++++++++++++ fw/core/lib/src/op.rs | 3 +- fw/pal/traits/src/error.rs | 10 + fw/pal/traits/src/vault.rs | 19 +- fw/plat/std/pal/src/drivers/vault.rs | 2 + fw/plat/std/pal/src/ecc.rs | 41 ++- fw/plat/uno/fw/crates/key_vault/src/kind.rs | 6 +- fw/plat/uno/fw/pal/src/crypto/ecc.rs | 17 +- 16 files changed, 718 insertions(+), 58 deletions(-) create mode 100644 ddi/tbor/types/tests/commands/sd_sealing_key_gen.rs create mode 100644 fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs diff --git a/ddi/tbor/types/src/sd_sealing_key_gen.rs b/ddi/tbor/types/src/sd_sealing_key_gen.rs index 66e02e054..dd51edb3b 100644 --- a/ddi/tbor/types/src/sd_sealing_key_gen.rs +++ b/ddi/tbor/types/src/sd_sealing_key_gen.rs @@ -3,22 +3,34 @@ //! Host-side wrapper for the TBOR `SdSealingKeyGen` command. //! -//! `SdSealingKeyGen` is an **in-session** command that generates a new -//! security-domain sealing key in the partition's vault and returns its -//! key handle. +//! `SdSealingKeyGen` is an **in-session** Crypto-Officer command that +//! generates a new security-domain sealing key (ECC-P384) and returns +//! the **masked** private key plus its public key. The private key is +//! not stored on the device; the caller holds the masked blob and +//! re-imports it (unmask-on-use) when the key is later needed. //! //! The request carries the requested key `scope` (lifecycle / visibility //! domain) as its 1-byte discriminant. The firmware-side schema //! (`azihsm_fw_ddi_tbor_types::sd_sealing_key_gen`) types it as the //! `KeyScope` open-enum (mirror of the PAL `HsmKeyScope`); this host //! crate is firewalled from the firmware PAL types, so it carries the -//! same byte as a raw `u8`. +//! same byte as a raw `u8`. The private key is masked under the masking +//! key associated with the scope. use crate::tbor; /// TBOR opcode for `SdSealingKeyGen`. pub const TBOR_OP_SD_SEALING_KEY_GEN: u8 = 0x09; +/// Wire length of the returned sealing public key: a raw P-384 point +/// (`x ‖ y`, 48 + 48 bytes). +pub const SD_SEALING_PUB_KEY_LEN: usize = 96; + +/// Wire length of the masked sealing private key: an AEAD-GCM-256 +/// masked-key envelope (`header(8) ‖ iv(12) ‖ aad(96) ‖ pt(48) ‖ +/// tag(16)`) over the 48-byte raw P-384 private scalar. +pub const MASKED_SEALING_KEY_LEN: usize = 8 + 12 + 96 + 48 + 16; + /// Host-facing TBOR `SdSealingKeyGen` request. #[tbor(opcode = TBOR_OP_SD_SEALING_KEY_GEN, session_ctrl = in_session)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -35,13 +47,17 @@ pub struct TborSdSealingKeyGenReq { /// Host-facing TBOR `SdSealingKeyGen` response. #[tbor(response)] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TborSdSealingKeyGenResp { - /// Vault id (`HsmKeyId`) of the newly generated sealing key. Carried - /// as a `KeyId` (inline 16-bit, TOC entry type 1); represented here - /// as the raw `u16`. - #[tbor(key_id)] - pub key_handle: u16, + /// The new sealing key's ECC-P384 private half, masked + /// (AEAD-GCM-256) under the requested scope's masking key. Always + /// exactly [`MASKED_SEALING_KEY_LEN`] (180 B); not stored on-device. + pub masked_key: [u8; MASKED_SEALING_KEY_LEN], + + /// Raw P-384 public key (`x ‖ y` affine coordinates, 96 bytes, + /// little-endian per coordinate) of the new sealing key. Not a SEC1 + /// point encoding (no `0x04` prefix). + pub pub_key: [u8; SD_SEALING_PUB_KEY_LEN], } #[cfg(test)] diff --git a/ddi/tbor/types/src/status.rs b/ddi/tbor/types/src/status.rs index 4dabde05d..b7a5ad2d9 100644 --- a/ddi/tbor/types/src/status.rs +++ b/ddi/tbor/types/src/status.rs @@ -276,6 +276,11 @@ pub enum TborStatus { UmsKeyAlreadySet = 0x087000FD, UmsKeyNotSet = 0x087000FE, + /// A command was asked to operate on a `HsmKeyScope` whose backing + /// key material does not yet exist (mirror of + /// `HsmError::UnsupportedKeyScope`). + UnsupportedKeyScope = 0x08700102, + /// The SQE's out-of-band descriptor-array length (`oob_len`) is /// malformed (mirror of `HsmError::IoChannelInvalidOobLen`). IoChannelInvalidOobLen = 0x08700104, diff --git a/ddi/tbor/types/tests/commands/mod.rs b/ddi/tbor/types/tests/commands/mod.rs index a695b7cbc..6c79d2f17 100644 --- a/ddi/tbor/types/tests/commands/mod.rs +++ b/ddi/tbor/types/tests/commands/mod.rs @@ -14,5 +14,6 @@ pub mod part_final; pub mod part_info; pub mod part_init; pub mod psk_change; +pub mod sd_sealing_key_gen; pub mod session_close; pub mod unexpected_toc_type; diff --git a/ddi/tbor/types/tests/commands/sd_sealing_key_gen.rs b/ddi/tbor/types/tests/commands/sd_sealing_key_gen.rs new file mode 100644 index 000000000..4f98c07de --- /dev/null +++ b/ddi/tbor/types/tests/commands/sd_sealing_key_gen.rs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Integration tests for the TBOR `SdSealingKeyGen` command. +//! +//! Cross-test isolation comes from `open_dev`'s factory reset; no +//! per-test cleanup is required (see [`crate::harness::fixture`]). +//! +//! The command generates a P-384 sealing keypair and returns the +//! **masked** private key (masked under the requested scope's masking +//! key) plus the public key — nothing is stored on the device. The +//! Ephemeral/Local masking keys are provisioned by `PartFinal`, so the +//! happy-path tests first drive `PartInit → PartFinal`. +//! +//! Coverage: +//! * Happy path (Ephemeral + Local) — returns a non-zero 180-byte masked +//! key + 96-byte public key; a second call yields a distinct keypair. +//! * Unsupported scope (Session + SecurityDomain) → `UnsupportedKeyScope`. +//! * Before finalize (partition not `Initialized`) → `InvalidArg`. +//! * Crypto-User session → `InvalidPermissions`. +//! * Default-PSK gate → `DefaultPskMustRotate` (dispatcher, pre-handler). + +#![cfg(feature = "emu")] + +use azihsm_ddi_tbor_types::SessionType; +use azihsm_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_ddi_tbor_types::PSK_LEN; +use azihsm_ddi_tbor_types::SD_SEALING_PUB_KEY_LEN; + +use crate::commands::part_init::bootstrap_rotated_co; +use crate::commands::part_init::mach_seed; +use crate::commands::part_init::part_policy_with_pota; +use crate::commands::part_init::pota_thumbprint; +use crate::commands::part_init::CO; +use crate::commands::part_init::ROTATED_CO_PSK; +use crate::harness::x509_fixture::make_pta_chain; +use crate::harness::x509_fixture::pta_pub_from_csr; +use crate::harness::x509_fixture::CaKey; +use crate::harness::SessionHandshake; +use crate::harness::SessionOpenInitOptions; +use crate::harness::TestCtx; + +/// `KeyScope` discriminants (wire mirror of the firmware `HsmKeyScope`). +const SCOPE_SESSION: u8 = 0b001; +const SCOPE_EPHEMERAL: u8 = 0b010; +const SCOPE_LOCAL: u8 = 0b011; +const SCOPE_SECURITY_DOMAIN: u8 = 0b100; + +/// Crypto-User PSK id. +const CU: u8 = 1; + +/// Non-default 32-byte CU PSK, used to clear the default-PSK gate so the +/// CU-role reject path — not the default-PSK gate — is exercised. +const ROTATED_CU_PSK: [u8; PSK_LEN] = [ + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, +]; + +/// Bring a partition to `Initialized` on a rotated CO session: +/// bootstrap → `PartInit` → `PartFinal`. Post-condition: the +/// Ephemeral/Local masking keys exist, so `SdSealingKeyGen` can mask +/// under them. Returns the live CO session. +/// +/// `PartFinal` validates the supplied PTA certificate chain against the +/// POTA trust anchor bound into the policy, so this mints a POTA CA, binds +/// its public key into the policy, and issues a POTA-anchored PTA chain +/// from the `PartInit` CSR (mirrors the `part_final` happy-path setup). +fn finalized_co_session(ctx: &TestCtx) -> SessionHandshake { + let session = bootstrap_rotated_co(ctx, &ROTATED_CO_PSK); + + let pota = CaKey::generate(); + let policy = part_policy_with_pota(&pota.raw_pub()); + let init = ctx + .part_init(&session, &mach_seed(), &policy, &pota_thumbprint()) + .expect("PartInit"); + let chain = make_pta_chain(&pota, &pta_pub_from_csr(&init.pta_csr)); + ctx.part_final(&session, &policy, &[], &chain.der_items()) + .expect("PartFinal"); + session +} + +/// Happy path for a supported `scope`: the masked key + public key are +/// full/non-zero, and a second call yields a distinct keypair. +fn roundtrip_for_scope(scope: u8) { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + + let req = TborSdSealingKeyGenReq { + session_id: session.session_id, + scope, + }; + let resp = ctx.tbor(&req).expect("SdSealingKeyGen roundtrip"); + + // Masked private key: exactly the pinned length, non-zero. + assert_eq!(resp.masked_key.len(), MASKED_SEALING_KEY_LEN); + assert!( + resp.masked_key.iter().any(|&b| b != 0), + "masked_key must not be all-zero", + ); + // Public key: a full, non-zero P-384 point. + assert_eq!(resp.pub_key.len(), SD_SEALING_PUB_KEY_LEN); + assert!( + resp.pub_key.iter().any(|&b| b != 0), + "pub_key must not be all-zero", + ); + + // Each call generates fresh randomness → a distinct keypair. + let resp2 = ctx.tbor(&req).expect("second SdSealingKeyGen"); + assert_ne!( + resp.masked_key, resp2.masked_key, + "each generation must yield a distinct masked key", + ); + assert_ne!( + resp.pub_key, resp2.pub_key, + "each generation must yield a distinct public key", + ); +} + +#[test] +fn sd_sealing_key_gen_ephemeral_roundtrip_emu() { + roundtrip_for_scope(SCOPE_EPHEMERAL); +} + +#[test] +fn sd_sealing_key_gen_local_roundtrip_emu() { + roundtrip_for_scope(SCOPE_LOCAL); +} + +#[test] +fn sd_sealing_key_gen_rejects_unsupported_scope_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + + // Session and SecurityDomain masking keys are not yet provisioned + // (session-key masking / CreateSD's SDKMK), so both must be rejected + // with the dedicated UnsupportedKeyScope error. + for scope in [SCOPE_SESSION, SCOPE_SECURITY_DOMAIN] { + let req = TborSdSealingKeyGenReq { + session_id: session.session_id, + scope, + }; + ctx.expect_fw_reject(&req, TborStatus::UnsupportedKeyScope); + } +} + +#[test] +fn sd_sealing_key_gen_rejects_before_finalize_emu() { + let ctx = TestCtx::new(); + // Rotated CO session but no PartInit/PartFinal → the partition is not + // Initialized, so the scope's masking key does not exist yet. + let session = bootstrap_rotated_co(&ctx, &ROTATED_CO_PSK); + + let req = TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: SCOPE_EPHEMERAL, + }; + ctx.expect_fw_reject(&req, TborStatus::InvalidArg); +} + +#[test] +fn sd_sealing_key_gen_rejected_on_cu_session_emu() { + let ctx = TestCtx::new(); + + // Rotate the CU PSK out of the default so the dispatcher's default-PSK + // gate does not fire first; then reopen a CU session under the rotated + // PSK. CU sessions are pinned to `SessionType::PlainText` (CO-only is + // `Authenticated`). + let bootstrap = ctx.open_session(CU, SessionType::PlainText); + ctx.psk_change(bootstrap.handshake(), &ROTATED_CU_PSK) + .expect("rotate CU PSK"); + bootstrap.close().expect("close bootstrap CU session"); + + let opts = SessionOpenInitOptions::new(CU, SessionType::PlainText).with_psk(&ROTATED_CU_PSK); + let pending = ctx + .session_open_init_with_options(opts) + .expect("CU session_open_init under rotated PSK"); + let session = ctx + .session_open_finish(pending) + .expect("CU session_open_finish under rotated PSK"); + + // SdSealingKeyGen is Crypto-Officer-only: the handler's role gate + // (checked before the scope/state gates) rejects a CU session. + let req = TborSdSealingKeyGenReq { + session_id: session.session_id, + scope: SCOPE_EPHEMERAL, + }; + ctx.expect_fw_reject(&req, TborStatus::InvalidPermissions); +} + +#[test] +fn sd_sealing_key_gen_rejected_on_default_psk_emu() { + let ctx = TestCtx::new(); + // Open a CO session WITHOUT rotating the PSK (still the public + // default) — the dispatcher's default-PSK gate must reject the command + // before the handler runs. + let session = ctx.open_session(CO, SessionType::Authenticated); + + let req = TborSdSealingKeyGenReq { + session_id: session.session_id(), + scope: SCOPE_EPHEMERAL, + }; + ctx.expect_fw_reject(&req, TborStatus::DefaultPskMustRotate); +} diff --git a/docs/tbor-ddi/README.md b/docs/tbor-ddi/README.md index 2c588c718..f98187c65 100644 --- a/docs/tbor-ddi/README.md +++ b/docs/tbor-ddi/README.md @@ -57,7 +57,7 @@ single `none` TOC placeholder and no typed body fields. | `0x06` | `PskChange` | InSession | [`commands/psk_change.md`](./commands/psk_change.md) | | `0x07` | `PartInit` | InSession | [`commands/part_init.md`](./commands/part_init.md) | | `0x08` | `PartFinal` | InSession | [`commands/part_final.md`](./commands/part_final.md) | -| `0x09` | `SdSealingKeyGen` (schema-only) | InSession | [`commands/sd_sealing_key_gen.md`](./commands/sd_sealing_key_gen.md) | +| `0x09` | `SdSealingKeyGen` | InSession | [`commands/sd_sealing_key_gen.md`](./commands/sd_sealing_key_gen.md) | | `0x0A` | `SdCreateRemoteBackup` (schema-only) | InSession | [`commands/sd_create_remote_backup.md`](./commands/sd_create_remote_backup.md) | | `0x0B` | `SdResealBackup` (schema-only) | InSession | [`commands/sd_reseal_backup.md`](./commands/sd_reseal_backup.md) | | `0x0C` | `SdRestoreRemoteBackup` (schema-only) | InSession | [`commands/sd_restore_remote_backup.md`](./commands/sd_restore_remote_backup.md) | diff --git a/docs/tbor-ddi/commands/sd_sealing_key_gen.md b/docs/tbor-ddi/commands/sd_sealing_key_gen.md index b5d022e3c..a12d6535c 100644 --- a/docs/tbor-ddi/commands/sd_sealing_key_gen.md +++ b/docs/tbor-ddi/commands/sd_sealing_key_gen.md @@ -5,15 +5,38 @@ Licensed under the MIT License. # SdSealingKeyGen (Opcode 0x09) -**Handler:** _Not yet landed — wire schema only._ +**Handler:** `fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs` **Session:** InSession ## Description -Generates a new security-domain sealing key in the active session's -partition vault and returns its key handle. The request carries the -requested key `scope` (lifecycle / visibility domain) as its 1-byte -`KeyScope` discriminant — a wire mirror of the firmware `HsmKeyScope`. +Generates a new security-domain sealing key and returns the **masked** +private key together with the public key. The sealing key is a **P-384 +ECC keypair for ECDH key agreement** (ECIES-style seal / unseal). + +The private key is **not** stored on the device. It is masked +(AEAD-GCM-256) under the masking key associated with the requested +`scope` and the masked blob is returned to the caller, which re-imports +it (unmask-on-use) when the key is later needed. Because nothing is +persisted, the command records no rollback on the undo log. + +The request carries the requested key `scope` (lifecycle / visibility +domain) as its 1-byte `KeyScope` discriminant — a wire mirror of the +firmware `HsmKeyScope`. Scope → masking key: + +- `Ephemeral` → the partition `PartitionEphemeralMaskingKey`. +- `Local` → the partition `PartitionLocalMaskingKey`. + +Both masking keys are provisioned by `PartFinal`, so the partition must +be in the `Initialized` lifecycle state. The `Session` and +`SecurityDomain` scopes (and any other) are rejected with +`UnsupportedKeyScope` until their masking keys exist (session-key +masking / `CreateSD`'s `SDKMK`). The masked key's metadata records the +sealing key as `derive`-only, `local`, `private`, and never-extractable, +plus the requested scope. + +This command is **Crypto-Officer-only**: a Crypto-User session is +rejected with `InvalidPermissions`. ## Request @@ -24,8 +47,8 @@ Wire layout: 4-byte header, followed by the TOC entries, then the | Offset | Field | Type | Description | |---|---|---|---| -| 4 | `session_id` | `session_id` (inline) | CO/CU session this request is bound to; cross-checked against the SQE-carried session id. | -| 8 | `scope` | `uint8` (inline) | Requested key scope (`KeyScope` discriminant): `0` = Unspecified, `1` = Session, `2` = Ephemeral, `3` = Local, `4` = SecurityDomain, `5` = Internal. Unknown values round-trip as `KeyScope(x)`. | +| 4 | `session_id` | `session_id` (inline) | CO session this request is bound to; cross-checked against the SQE-carried session id. | +| 8 | `scope` | `uint8` (inline) | Requested key scope (`KeyScope` discriminant): `0` = Unspecified, `1` = Session, `2` = Ephemeral, `3` = Local, `4` = SecurityDomain, `5` = Internal. Only `Ephemeral` and `Local` are supported; others return `UnsupportedKeyScope`. | ### Data section @@ -33,24 +56,28 @@ _Empty — both fields are carried inline within their TOC entries._ ## Response -Wire layout: 8-byte header, followed by the TOC entry, then the -(empty) data section. +Wire layout: 8-byte header, followed by the TOC entries, then the data +section carrying the masked key and public key. ### TOC entries | Offset | Field | Type | Description | |---|---|---|---| -| 8 | `key_handle` | `key_id` (inline) | Vault id (`HsmKeyId`) of the newly generated sealing key, carried as a `KeyId` (TOC entry type 1). | +| 8 | `masked_key` | `buffer` (180 B) | The sealing key's ECC-P384 private half, masked (AEAD-GCM-256) under the scope's masking key: `header(8) ‖ iv(12) ‖ aad(96) ‖ pt(48) ‖ tag(16)`. Not stored on-device. | +| 12 | `pub_key` | `buffer` (96 B) | Raw P-384 public key: `x ‖ y` affine coordinates (48 + 48 bytes, little-endian per coordinate) of the new sealing key. Not a SEC1 point encoding (no `0x04` prefix). | ### Data section -_Empty — `key_handle` is carried inline within its TOC entry._ +Carries the 180-byte `masked_key` followed by the 96-byte `pub_key`. ## Errors | Error | Cause | |---|---| -| `SessionNotFound` | `session_id` does not refer to an allocated slot | +| `InvalidPermissions` | The calling session is a Crypto User (this command is Crypto-Officer-only) | +| `SessionNotFound` | `session_id` does not refer to an allocated slot, or the slot is not `Active` | +| `InvalidArg` | The partition is not `Initialized` (run `PartFinal` first) | +| `UnsupportedKeyScope` | The requested scope (`Session`, `SecurityDomain`, or other) has no masking key yet | | `DdiDecodeFailed` | Malformed request body | ## See also diff --git a/fw/core/ddi/tbor/types/src/sd_sealing_key_gen.rs b/fw/core/ddi/tbor/types/src/sd_sealing_key_gen.rs index 0f2c4f04b..9696a38c2 100644 --- a/fw/core/ddi/tbor/types/src/sd_sealing_key_gen.rs +++ b/fw/core/ddi/tbor/types/src/sd_sealing_key_gen.rs @@ -4,8 +4,9 @@ //! TBOR `SdSealingKeyGen` wire schema. //! //! `SdSealingKeyGen` is an in-session command that generates a new -//! security-domain sealing key in the partition's vault and returns its -//! [`KeyId`](azihsm_fw_ddi_tbor_api::KeyId) handle. +//! security-domain sealing key and returns it as a masked private-key +//! blob plus the public key. The private key is not stored on the +//! device (see Outputs below). //! //! Inputs: //! @@ -17,11 +18,18 @@ //! discriminant. Mirrors the firmware //! [`HsmKeyScope`](azihsm_fw_hsm_pal_traits::HsmKeyScope). //! -//! Output: +//! Outputs: //! -//! * `key_handle` — the new key's vault id -//! ([`HsmKeyId`](azihsm_fw_hsm_pal_traits::HsmKeyId) value), carried -//! as a [`KeyId`](azihsm_fw_ddi_tbor_api::KeyId) (TOC entry type 1). +//! * `masked_key` — the new sealing key's ECC-P384 **private** half, +//! masked (AEAD-GCM-256) under the requested scope's masking key, as a +//! fixed [`MASKED_SEALING_KEY_LEN`] (180 B) envelope. The private key +//! is **not** stored on the device: the caller holds the masked blob +//! and re-imports it (unmask-on-use) when the key is later needed. +//! * `pub_key` — the raw P-384 public key of the new sealing key: the +//! `x ‖ y` affine coordinates (96 bytes, little-endian per coordinate) +//! as emitted by the PAL. This is the bare coordinate pair, **not** a +//! SEC1 point encoding (no `0x04` prefix). The caller uses it as the +//! ECDH peer for ECIES-style seal / unseal. use azihsm_fw_ddi_tbor_api::tbor; @@ -30,6 +38,20 @@ use crate::key_props::KeyScope; /// TBOR opcode for `SdSealingKeyGen`. pub const TBOR_OP_SD_SEALING_KEY_GEN: u8 = 0x09; +/// Wire length of the returned sealing public key: a raw P-384 point +/// (`x ‖ y`, 48 + 48 bytes). Pinned into the `#[tbor(buffer, len = +/// 96)]` literal on [`TborSdSealingKeyGenResp::pub_key`] (see the +/// `pub_key_len_matches_pinned_value` test). +pub const SD_SEALING_PUB_KEY_LEN: usize = 96; + +/// Wire length of the masked sealing private key: an AEAD-GCM-256 +/// masked-key envelope (`header(8) ‖ iv(12) ‖ aad(96) ‖ pt(48) ‖ +/// tag(16)`) whose plaintext is the 48-byte raw P-384 private scalar and +/// whose AAD is the 96-byte `MaskedKeyMetadata`. Pinned into the +/// `#[tbor(buffer, len = 180)]` literal on +/// [`TborSdSealingKeyGenResp::masked_key`]. +pub const MASKED_SEALING_KEY_LEN: usize = 8 + 12 + 96 + 48 + 16; + /// `SdSealingKeyGen` request schema. /// /// Generates a security-domain sealing key under the active session's @@ -49,19 +71,25 @@ pub struct TborSdSealingKeyGenReq { /// `SdSealingKeyGen` response schema. #[tbor(response)] -pub struct TborSdSealingKeyGenResp { - /// Vault id (`HsmKeyId`) of the newly generated sealing key. Carried - /// as a [`KeyId`](azihsm_fw_ddi_tbor_api::KeyId) (inline 16-bit, TOC - /// entry type 1). - #[tbor(key_id)] - pub key_handle: KeyId, +pub struct TborSdSealingKeyGenResp<'a> { + /// The new sealing key's ECC-P384 private half, masked (AEAD-GCM-256) + /// under the requested scope's masking key. Always exactly + /// [`MASKED_SEALING_KEY_LEN`] (180 B). The private key is not stored + /// on the device; the caller re-imports this blob when needed. + #[tbor(buffer, len = 180)] + pub masked_key: &'a [u8], + + /// Raw P-384 public key (`x ‖ y` affine coordinates, 96 bytes, + /// little-endian per coordinate) of the new sealing key, as emitted + /// by the PAL. Not a SEC1 point encoding (no `0x04` prefix). + #[tbor(buffer, len = 96)] + pub pub_key: &'a [u8], } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] - use azihsm_fw_ddi_tbor_api::KeyId; use azihsm_fw_ddi_tbor_api::SessionId; use super::*; @@ -82,13 +110,28 @@ mod tests { } #[test] - fn response_round_trips_key_handle() { - let mut buf = [0u8; 256]; + fn response_round_trips_masked_key_and_pub_key() { + let mut buf = [0u8; 512]; + let masked = [0xCDu8; MASKED_SEALING_KEY_LEN]; + let pub_key = [0xABu8; SD_SEALING_PUB_KEY_LEN]; let frame = TborSdSealingKeyGenResp::encode(&mut buf, 0, true) .unwrap() - .key_handle(KeyId(0x1234)) + .masked_key(&masked) + .unwrap() + .pub_key(&pub_key) .unwrap() .finish(); - assert_eq!(frame.key_handle(), KeyId(0x1234)); + assert_eq!(frame.masked_key(), &masked[..]); + assert_eq!(frame.pub_key(), &pub_key[..]); + } + + #[test] + fn response_lengths_match_pinned_values() { + // The `#[tbor(buffer, len = N)]` attributes must remain numeric + // literals; pin them against the exported consts. + const _: () = assert!(96 == SD_SEALING_PUB_KEY_LEN); + const _: () = assert!(180 == MASKED_SEALING_KEY_LEN); + assert_eq!(SD_SEALING_PUB_KEY_LEN, 96); + assert_eq!(MASKED_SEALING_KEY_LEN, 180); } } diff --git a/fw/core/lib/src/ddi/tbor/mod.rs b/fw/core/lib/src/ddi/tbor/mod.rs index 7aa884a61..68f772860 100644 --- a/fw/core/lib/src/ddi/tbor/mod.rs +++ b/fw/core/lib/src/ddi/tbor/mod.rs @@ -26,6 +26,7 @@ pub mod part_info; pub mod part_init; pub mod policy; pub(crate) mod psk_change; +pub(crate) mod sd_sealing_key_gen; pub(crate) mod session_close; pub(crate) mod session_open_finish; pub(crate) mod session_open_init; @@ -92,6 +93,13 @@ pub(crate) mod opcode { /// key). TBOR analogue of MBOR `GetDeviceInfo` + the Manticore /// `GetPartID` primitive. pub(crate) const PART_INFO: u8 = 0x02; + + /// `SdSealingKeyGen` — generate a security-domain sealing key (a + /// P-384 ECC keypair for ECDH) under the active session's partition; + /// return the private half **masked** under the requested scope's + /// masking key (nothing is persisted on-device) plus the public key. + /// See [`super::sd_sealing_key_gen`]. + pub(crate) const SD_SEALING_KEY_GEN: u8 = 0x09; } /// Dispatch a parsed TBOR request to its handler. @@ -174,6 +182,7 @@ pub(crate) async fn dispatch<'p, P: HsmPal>( opcode::PART_INIT => part_init::handle(pal, io, req_buf).await, opcode::PART_FINAL => part_final::handle(pal, io, req_buf, oob).await, opcode::PART_INFO => part_info::handle(pal, io, req_buf), + opcode::SD_SEALING_KEY_GEN => sd_sealing_key_gen::handle(pal, io, req_buf).await, _ => Err(HsmError::UnsupportedCmd), } } @@ -193,6 +202,7 @@ fn is_known_opcode(opcode: u8) -> bool { | opcode::PART_INIT | opcode::PART_FINAL | opcode::PART_INFO + | opcode::SD_SEALING_KEY_GEN ) } @@ -215,7 +225,11 @@ fn is_in_session(opcode: u8) -> bool { | opcode::SESSION_OPEN_INIT | opcode::SESSION_OPEN_FINISH | opcode::PART_INFO => false, - opcode::SESSION_CLOSE | opcode::PSK_CHANGE | opcode::PART_INIT | opcode::PART_FINAL => true, + opcode::SESSION_CLOSE + | opcode::PSK_CHANGE + | opcode::PART_INIT + | opcode::PART_FINAL + | opcode::SD_SEALING_KEY_GEN => true, // Default-deny: any future opcode is treated as in-session // until classified, so the default-PSK gate applies to it. _ => true, @@ -249,7 +263,8 @@ fn needs_session_id_cross_check(opcode: u8) -> bool { | opcode::SESSION_CLOSE | opcode::PSK_CHANGE | opcode::PART_INIT - | opcode::PART_FINAL => true, + | opcode::PART_FINAL + | opcode::SD_SEALING_KEY_GEN => true, _ => true, } } @@ -332,6 +347,10 @@ mod tests { opcode::SESSION_OPEN_FINISH, opcode::SESSION_CLOSE, opcode::PSK_CHANGE, + opcode::PART_INIT, + opcode::PART_INFO, + opcode::PART_FINAL, + opcode::SD_SEALING_KEY_GEN, ] { assert!(is_known_opcode(op), "{op:#04x} should be known"); } @@ -356,6 +375,11 @@ mod tests { assert!(is_in_session(opcode::PSK_CHANGE)); } + #[test] + fn sd_sealing_key_gen_is_in_session() { + assert!(is_in_session(opcode::SD_SEALING_KEY_GEN)); + } + #[test] fn unknown_opcode_defaults_to_in_session() { // Default-deny: an unknown future opcode is treated as @@ -373,6 +397,7 @@ mod tests { opcode::API_REV, opcode::SESSION_OPEN_INIT, opcode::SESSION_OPEN_FINISH, + opcode::SD_SEALING_KEY_GEN, SYNTHETIC_FUTURE_OPCODE, 0x00, 0xFF, diff --git a/fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs b/fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs new file mode 100644 index 000000000..065879f3b --- /dev/null +++ b/fw/core/lib/src/ddi/tbor/sd_sealing_key_gen.rs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `SdSealingKeyGen` handler. +//! +//! Generates a fresh security-domain sealing key — a P-384 ECC keypair +//! for ECDH key agreement (ECIES-style seal / unseal) — under the active +//! session's partition, and returns the **masked** private key plus the +//! public key. +//! +//! The private key is **not** stored on the device. Instead it is masked +//! (AEAD-GCM-256) under the masking key associated with the requested +//! [`scope`](HsmKeyScope) and the masked blob is returned to the caller, +//! which re-imports it (unmask-on-use) when the key is later needed. +//! Because nothing is persisted, the command records no rollback on the +//! undo log. +//! +//! Scope → masking key: +//! +//! * [`Ephemeral`](HsmKeyScope::Ephemeral) → the partition +//! [`PartitionEphemeralMaskingKey`](HsmVaultKeyKind::PartitionEphemeralMaskingKey). +//! * [`Local`](HsmKeyScope::Local) → the partition +//! [`PartitionLocalMaskingKey`](HsmVaultKeyKind::PartitionLocalMaskingKey). +//! +//! Both masking keys are provisioned by `PartFinal`, so this command +//! requires a partition in the [`Initialized`](PartState::Initialized) +//! lifecycle state. The [`Session`](HsmKeyScope::Session) and +//! [`SecurityDomain`](HsmKeyScope::SecurityDomain) scopes (and any other) +//! are rejected with [`HsmError::UnsupportedKeyScope`] until their +//! masking keys exist (session-key masking / `CreateSD`'s `SDKMK`). +//! +//! This command is **Crypto-Officer-only**: a Crypto-User session is +//! rejected with [`HsmError::InvalidPermissions`]. + +use azihsm_fw_core_crypto_key_masking::aead::mask; +use azihsm_fw_core_crypto_key_masking::aead::AeadAlg; +use azihsm_fw_core_crypto_key_masking::aead::MaskParams; +use azihsm_fw_ddi_tbor_types::TborSdSealingKeyGenReq; +use azihsm_fw_ddi_tbor_types::TborSdSealingKeyGenResp; +use azihsm_fw_ddi_tbor_types::MASKED_SEALING_KEY_LEN; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmEccCurve; +use azihsm_fw_hsm_pal_traits::HsmEccPct; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmIo; +use azihsm_fw_hsm_pal_traits::HsmKeyId; +use azihsm_fw_hsm_pal_traits::HsmKeyScope; +use azihsm_fw_hsm_pal_traits::HsmPal; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmScopedAlloc; +use azihsm_fw_hsm_pal_traits::HsmSessId; +use azihsm_fw_hsm_pal_traits::HsmSessionState; +use azihsm_fw_hsm_pal_traits::HsmVaultKeyAttrs; +use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; +use azihsm_fw_hsm_pal_traits::PartState; +use azihsm_fw_hsm_pal_traits::SessionRole; + +use crate::part_state; + +/// NIST curve for security-domain sealing keys. Fixed at P-384 to match +/// the SD / session / establish-cred key material elsewhere in the +/// firmware. +const SD_SEALING_CURVE: HsmEccCurve = HsmEccCurve::P384; + +/// Envelope key-label recorded in the masked blob's `MaskedKeyMetadata`. +const SEALING_KEY_LABEL: &[u8] = b"SDSealingKey"; + +fn validate_crypto_officer_active_session( + pal: &P, + io: &impl HsmIo, + sess_id: HsmSessId, +) -> HsmResult<()> { + // SdSealingKeyGen is Crypto-Officer-only. + if sess_id.role() != SessionRole::CryptoOfficer { + return Err(HsmError::InvalidPermissions); + } + + // The dispatcher validates only SQE/body consistency; the slot must + // still be checked for liveness at the handler boundary. + if !matches!(pal.session_state(io, sess_id), HsmSessionState::Active) { + return Err(HsmError::SessionNotFound); + } + + Ok(()) +} + +/// Attributes recorded in the masked blob's metadata (restored on +/// re-import). Per the SD-sealing-key contract the only usage attribute +/// is `derive`; `local`/`private`/`never_extractable` are HSM-internal +/// flags, and `scope` records the lifecycle / visibility domain. +fn sealing_key_attrs(scope: HsmKeyScope) -> HsmVaultKeyAttrs { + HsmVaultKeyAttrs::new() + .with_local(true) + .with_private(true) + .with_never_extractable(true) + .with_derive(true) + .with_scope(scope) +} + +/// Resolve the vault id of the masking key associated with `scope`. +/// +/// Only [`Ephemeral`](HsmKeyScope::Ephemeral) and +/// [`Local`](HsmKeyScope::Local) are supported for now (their masking +/// keys are provisioned by `PartFinal`). Every other scope — +/// [`Session`](HsmKeyScope::Session), +/// [`SecurityDomain`](HsmKeyScope::SecurityDomain), or an unrecognized +/// discriminant — is rejected with [`HsmError::UnsupportedKeyScope`]. +fn masking_key_id_for_scope( + pal: &P, + io: &impl HsmIo, + scope: HsmKeyScope, +) -> HsmResult { + match scope { + HsmKeyScope::Ephemeral => part_state::part_ephemeral_mk_key_id(pal, io), + HsmKeyScope::Local => part_state::part_local_mk_key_id(pal, io), + _ => Err(HsmError::UnsupportedKeyScope), + } +} + +async fn generate_sealing_keypair<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, +) -> HsmResult<(&'p mut DmaBuf, &'p mut DmaBuf)> { + // Generate the P-384 keypair for ECDH. The buffers must outlive the + // scoped keygen-scratch allocator, so allocate them between the two + // query/use calls rather than inside one scope. + let (priv_key_size, pub_key_size) = pal + .alloc_scoped_async(io, async |a| { + pal.ecc_gen_keypair(io, a, SD_SEALING_CURVE, None, HsmEccPct::KeyAgreement) + .await + }) + .await?; + let priv_key = pal.dma_alloc(io, priv_key_size)?; + let pub_key = pal.dma_alloc(io, pub_key_size)?; + let gen_res = pal + .alloc_scoped_async(io, async |a| -> HsmResult<_> { + pal.ecc_gen_keypair( + io, + a, + SD_SEALING_CURVE, + Some((&mut *priv_key, &mut *pub_key)), + HsmEccPct::KeyAgreement, + ) + .await + }) + .await; + let (priv_key_len, pub_key_len) = match gen_res { + Ok(lens) => lens, + Err(e) => { + // Keygen may have partially written the private scalar into the + // already-allocated buffer. Scope rewind does not clear DMA + // memory, so wipe it before it returns to the per-IO pool. + priv_key.zeroize(); + return Err(e); + } + }; + + Ok((&mut priv_key[..priv_key_len], &mut pub_key[..pub_key_len])) +} + +async fn mask_sealing_private_key<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + mk_key_id: HsmKeyId, + attrs: HsmVaultKeyAttrs, + svn: u64, + owner: u16, + priv_key: &DmaBuf, +) -> HsmResult<&'p mut DmaBuf> { + let masked_key = pal.dma_alloc(io, MASKED_SEALING_KEY_LEN)?; + + // The masked blob lives in an IO-scoped buffer allocated before the + // masking scope so it survives the scratch allocator's reset. + pal.alloc_scoped_async(io, async |alloc| -> HsmResult<()> { + let masking_key = pal.vault_key(io, mk_key_id)?; + let key_label = alloc.dma_alloc(SEALING_KEY_LABEL.len())?; + key_label.copy_from_slice(SEALING_KEY_LABEL); + let params = MaskParams { + key_kind: HsmVaultKeyKind::SdSealing, + key_attrs: attrs, + svn, + owner_seed_id: owner, + key_label, + }; + mask( + pal, + io, + alloc, + AeadAlg::AesGcm256, + masking_key, + ¶ms, + priv_key, + Some(masked_key), + ) + .await?; + Ok(()) + }) + .await?; + + Ok(masked_key) +} + +fn encode_sealing_response<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + masked_key: &DmaBuf, + pub_key: &DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let resp = pal.dma_alloc_var(io, |buf| { + let frame = TborSdSealingKeyGenResp::encode(buf, 0, false)? + .masked_key(masked_key)? + .pub_key(pub_key)? + .finish(); + Ok(frame.as_bytes().len()) + })?; + Ok(resp) +} + +/// Handle a TBOR `SdSealingKeyGen` request. +/// +/// No partition lock or undo log is required: the command **persists +/// nothing** — it reads partition state and returns a freshly generated, +/// masked keypair. It makes no observable state change, so a +/// concurrently-dispatched command (IOs run in a task pool and interleave +/// at await points) can neither observe it half-done nor require its +/// rollback on failure. The raw private scalar is wiped from the per-IO +/// DMA pool once it has been masked. +pub(crate) async fn handle<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + req_buf: &DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let req = TborSdSealingKeyGenReq::decode(req_buf)?; + let sess_id = HsmSessId::from(u16::from(req.session_id())); + + // Losslessly map the wire `KeyScope` onto the PAL `HsmKeyScope` + // (byte-identical 3-bit discriminants; unknown values round-trip). + let scope = HsmKeyScope(req.scope().0); + validate_crypto_officer_active_session(pal, io, sess_id)?; + + // The scope's masking key is provisioned by `PartFinal`, so the + // partition must be finalized (`Initialized`). + if part_state::part_state(pal, io)? != PartState::Initialized { + return Err(HsmError::InvalidArg); + } + + // Resolve (and validate) the masking key before generating anything, + // so an unsupported scope fails cheaply. + let mk_key_id = masking_key_id_for_scope(pal, io, scope)?; + + // Platform identity that binds the masked blob (anti-rollback on + // re-import): SVN (BKS1 lineage) and owner-seed id (BKS2 lineage). + let svn = part_state::part_mfgr_svn(pal); + let owner = u16::try_from(part_state::part_owner_svn(pal)).map_err(|_| HsmError::InvalidArg)?; + let attrs = sealing_key_attrs(scope); + let (priv_key, pub_key) = generate_sealing_keypair(pal, io).await?; + let masked_res = + mask_sealing_private_key(pal, io, mk_key_id, attrs, svn, owner, &priv_key[..]).await; + + // The raw private scalar is no longer needed once masking has been + // attempted. Scope rewind does not clear DMA memory, so wipe it + // explicitly (volatile per-byte writes) on every path — success or + // masking failure — to keep it from lingering in, and leaking through, + // a later per-IO allocation. + priv_key.zeroize(); + let masked_key = masked_res?; + + // Encode the response: the masked private key plus the public key. + // The PAL already emitted the public key in wire format (little-endian), + // so copy it through unchanged. + encode_sealing_response(pal, io, masked_key, pub_key) +} diff --git a/fw/core/lib/src/op.rs b/fw/core/lib/src/op.rs index dbcd89266..bfe3e1b7a 100644 --- a/fw/core/lib/src/op.rs +++ b/fw/core/lib/src/op.rs @@ -260,7 +260,8 @@ impl SessionCtrl { opcode::SESSION_OPEN_FINISH | opcode::PSK_CHANGE | opcode::PART_INIT - | opcode::PART_FINAL => Self::InSession, + | opcode::PART_FINAL + | opcode::SD_SEALING_KEY_GEN => Self::InSession, opcode::SESSION_CLOSE => Self::Close, _ => Self::NoSession, } diff --git a/fw/pal/traits/src/error.rs b/fw/pal/traits/src/error.rs index 4d49522f6..aaa8c7530 100644 --- a/fw/pal/traits/src/error.rs +++ b/fw/pal/traits/src/error.rs @@ -350,6 +350,16 @@ pub enum HsmError { /// carries the requested selector (SVN / owner id). SeedNotFound = 0x08700100, + /// A command was asked to operate on a [`HsmKeyScope`](crate::HsmKeyScope) + /// whose backing key material does not (yet) exist on the partition. + /// Returned by `SdSealingKeyGen` for the `Session` and + /// `SecurityDomain` scopes: their masking keys are provisioned by + /// commands that are not yet implemented (session-key masking / + /// `CreateSD`'s `SDKMK`). Distinct from + /// [`InvalidArg`](Self::InvalidArg) (a malformed request) — the + /// request is well-formed, the scope is simply unsupported for now. + UnsupportedKeyScope = 0x08700102, + /// The SQE's out-of-band descriptor-array length (`oob_len`) is /// malformed: not a whole number of 16-byte SGL descriptors, or /// larger than a single descriptor page. diff --git a/fw/pal/traits/src/vault.rs b/fw/pal/traits/src/vault.rs index d69a42fab..842da544f 100644 --- a/fw/pal/traits/src/vault.rs +++ b/fw/pal/traits/src/vault.rs @@ -45,7 +45,7 @@ use super::*; /// Types of keys that can be managed by the HSM key vault. /// /// Each variant corresponds to a specific cryptographic algorithm and -/// key size. The discriminant values (`0..34`) match the firmware's +/// key size. The discriminant values (`0..=41`) match the firmware's /// `EntryKind` enum so that key type information is wire-compatible /// across the DDI protocol. /// @@ -66,6 +66,10 @@ use super::*; /// | 28–30 | HMAC fixed-length | `_HmacSha256`, `_HmacSha384`, `_HmacSha512` | /// | 31 | Masking key | `MaskingKey` | /// | 32–34 | HMAC variable-length | `VarLenHmacSha256` .. `VarLenHmacSha512` | +/// | 35–36 | TBOR session blobs | `SessionExPending`, `SessionEx` | +/// | 37–38 | Partition incarnation keys | `PartitionTrustAnchor`, `UniquePartitionSecret` | +/// | 39–40 | PartFinal masking keys | `PartitionLocalMaskingKey`, `PartitionEphemeralMaskingKey` | +/// | 41 | Security-domain sealing key | `SdSealing` | #[repr(u8)] #[open_enum] #[derive(Clone, Copy, Debug)] @@ -200,6 +204,19 @@ pub enum HsmVaultKeyKind { /// implicitly revoked (along with everything it masked) on the next /// partition reset. PartitionEphemeralMaskingKey = 40, + + /// Security-domain sealing key — ECC-P384 private key. + /// + /// Generated on device by the TBOR `SdSealingKeyGen` handler for ECDH + /// key agreement (ECIES-style seal / unseal). The private half is + /// **not** stored on-device: it is returned to the caller masked under + /// the requested scope's masking key, and this kind is recorded only + /// as the masked blob's `key_kind` metadata. Tagged with the caller's + /// requested [`HsmKeyScope`] (currently `Ephemeral` or `Local`; + /// `SecurityDomain` is not yet supported) via its attribute `scope` + /// field. Represented as the 48-byte raw private scalar, mirroring + /// [`PartitionTrustAnchor`](Self::PartitionTrustAnchor). + SdSealing = 41, } /// Key scope: the lifecycle / visibility domain a vault key belongs diff --git a/fw/plat/std/pal/src/drivers/vault.rs b/fw/plat/std/pal/src/drivers/vault.rs index fffea5c68..68a30d1a1 100644 --- a/fw/plat/std/pal/src/drivers/vault.rs +++ b/fw/plat/std/pal/src/drivers/vault.rs @@ -102,6 +102,7 @@ pub fn fw_key_size(kind: HsmVaultKeyKind) -> Option { // PartFinal AES-256-GCM masking keys (v2 AEAD envelope). HsmVaultKeyKind::PartitionLocalMaskingKey | HsmVaultKeyKind::PartitionEphemeralMaskingKey => 32, + HsmVaultKeyKind::SdSealing => 48, // SessionEx is length-discriminated by session type // (PlainText=120, Authenticated=216); reported as variable // length, same handling as VarLenHmac*. @@ -585,6 +586,7 @@ mod tests { (HsmVaultKeyKind::UniquePartitionSecret, 48), (HsmVaultKeyKind::PartitionLocalMaskingKey, 32), (HsmVaultKeyKind::PartitionEphemeralMaskingKey, 32), + (HsmVaultKeyKind::SdSealing, 48), ]; for &(kind, expected) in cases { assert_eq!( diff --git a/fw/plat/std/pal/src/ecc.rs b/fw/plat/std/pal/src/ecc.rs index 921938332..d2dc0dc34 100644 --- a/fw/plat/std/pal/src/ecc.rs +++ b/fw/plat/std/pal/src/ecc.rs @@ -107,12 +107,22 @@ impl HsmEcc for StdHsmPal { let (pk, pub_key) = self.ecc.gen_keypair(to_ecc_curve(curve)).await?; self.ecc.pub_coords(&pub_key, false, scratch_pub).await?; - pk.to_hsm_bytes(&mut scratch_priv[..priv_len]) - .map_err(|_| HsmError::EccExportError)?; + // From here the private scalar is in `scratch_priv`; wipe it on + // every exit (scope rewind does not clear DMA memory). + if pk.to_hsm_bytes(&mut scratch_priv[..priv_len]).is_err() { + scratch_priv.zeroize(); + return Err(HsmError::EccExportError); + } priv_out[..priv_len].copy_from_slice(&scratch_priv[..priv_len]); pub_out[..wire_pub_len].copy_from_slice(scratch_pub); + // Scrub the private-scalar half of the scratch before returning: + // scope rewind does not clear DMA memory, so the freshly generated + // scalar would otherwise linger in — and leak through — a later + // per-IO allocation. (The pub half is not secret.) + scratch_priv.zeroize(); + Ok((priv_len, wire_pub_len)) } @@ -146,16 +156,25 @@ impl HsmEcc for StdHsmPal { let pk = EccPrivateKey::from_okm_a2_1(to_ecc_curve(curve), okm) .map_err(|_| HsmError::EccGenerateError)?; - pk.to_hsm_bytes(&mut scratch_priv[..priv_len]) - .map_err(|_| HsmError::EccExportError)?; - let pub_key = pk - .public_key() - .map_err(|_| HsmError::EccGetCoordinatesError)?; - self.ecc.pub_coords(&pub_key, false, scratch_pub).await?; - - priv_out[..priv_len].copy_from_slice(&scratch_priv[..priv_len]); - pub_out[..wire_pub_len].copy_from_slice(scratch_pub); + // Serialize the scalar into `scratch_priv`, derive the public + // coordinates, and copy both out. Once the scalar is in DMA + // scratch, every exit must wipe it (scope rewind does not clear DMA + // memory), so run the fallible tail and scrub unconditionally after. + let fill = async { + pk.to_hsm_bytes(&mut scratch_priv[..priv_len]) + .map_err(|_| HsmError::EccExportError)?; + let pub_key = pk + .public_key() + .map_err(|_| HsmError::EccGetCoordinatesError)?; + self.ecc.pub_coords(&pub_key, false, scratch_pub).await?; + priv_out[..priv_len].copy_from_slice(&scratch_priv[..priv_len]); + pub_out[..wire_pub_len].copy_from_slice(scratch_pub); + Ok::<(), HsmError>(()) + } + .await; + scratch_priv.zeroize(); + fill?; Ok((priv_len, wire_pub_len)) } diff --git a/fw/plat/uno/fw/crates/key_vault/src/kind.rs b/fw/plat/uno/fw/crates/key_vault/src/kind.rs index 2db719b8a..1e7d5e861 100644 --- a/fw/plat/uno/fw/crates/key_vault/src/kind.rs +++ b/fw/plat/uno/fw/crates/key_vault/src/kind.rs @@ -82,7 +82,7 @@ impl KeyLen { /// and var-HMAC min/max. `SessionEx` is length-discriminated by session /// type (PlainText=120, Authenticated=216) and modelled as variable; /// `SessionExPending` holds the in-flight TBOR Pending blob (up to 256). -static KIND_LEN: [KeyLen; 41] = [ +static KIND_LEN: [KeyLen; 42] = [ /* 0 Free */ KeyLen::Invalid, /* 1 Rsa2kPublic */ KeyLen::Fixed(260), /* 2 Rsa3kPublic */ KeyLen::Fixed(388), @@ -124,6 +124,7 @@ static KIND_LEN: [KeyLen; 41] = [ /* 38 UniquePartitionSecret */ KeyLen::Fixed(48), /* 39 PartitionLocalMaskingKey */ KeyLen::Fixed(32), /* 40 PartitionEphemeralMaskingKey */ KeyLen::Fixed(32), + /* 41 SdSealing */ KeyLen::Fixed(48), ]; /// Resolves the length contract for `kind` in O(1). @@ -188,6 +189,7 @@ mod tests { (HsmVaultKeyKind::UniquePartitionSecret, 48), (HsmVaultKeyKind::PartitionLocalMaskingKey, 32), (HsmVaultKeyKind::PartitionEphemeralMaskingKey, 32), + (HsmVaultKeyKind::SdSealing, 48), ]; for (kind, len) in table { assert_eq!(key_len(kind), Ok(KeyLen::Fixed(len)), "{kind:?}"); @@ -201,7 +203,7 @@ mod tests { #[test] fn unknown_discriminant_is_invalid_key_type() { - // 200 is well outside the named 0..=40 range. + // 200 is well outside the named 0..=41 range. assert_eq!(key_len(HsmVaultKeyKind(200)), Err(HsmError::InvalidKeyType)); } diff --git a/fw/plat/uno/fw/pal/src/crypto/ecc.rs b/fw/plat/uno/fw/pal/src/crypto/ecc.rs index fb7ffef94..5bfdd1a39 100644 --- a/fw/plat/uno/fw/pal/src/crypto/ecc.rs +++ b/fw/plat/uno/fw/pal/src/crypto/ecc.rs @@ -160,12 +160,27 @@ impl HsmEcc for UnoHsmPal { } let scratch = alloc.dma_alloc(pub_len + priv_len)?; - let total_len = self.pka.ecc_gen_keypair(pka_curve, scratch).await?; + let total_len = match self.pka.ecc_gen_keypair(pka_curve, scratch).await { + Ok(n) => n, + Err(e) => { + // Keygen may have written partial key material into + // `scratch`; wipe the whole buffer before it returns to the + // per-IO pool (scope rewind does not clear DMA memory). + scratch.zeroize(); + return Err(e); + } + }; let (pub_key, priv_key) = scratch[..total_len].split_at(pub_len); priv_out[..priv_len].copy_from_slice(priv_key); pub_out[..pub_len].copy_from_slice(pub_key); + // Scrub the private-scalar half of the scratch before returning: + // scope rewind does not clear DMA memory, so the freshly generated + // scalar would otherwise linger in — and leak through — a later + // per-IO allocation. (The pub half is not secret.) + scratch[pub_len..total_len].zeroize(); + Ok((priv_len, pub_len)) }