diff --git a/ddi/tbor/types/src/lib.rs b/ddi/tbor/types/src/lib.rs index 72c48bea9..56f4c33f6 100644 --- a/ddi/tbor/types/src/lib.rs +++ b/ddi/tbor/types/src/lib.rs @@ -89,6 +89,7 @@ mod part_info; mod part_init; mod policy; mod psk_change; +mod rsa_mod_exp; mod sd_create_peer_backup; mod sd_create_remote_backup; mod sd_reseal_remote_backup; @@ -113,6 +114,7 @@ pub use part_info::*; pub use part_init::*; pub use policy::*; pub use psk_change::*; +pub use rsa_mod_exp::*; pub use sd_create_peer_backup::*; pub use sd_create_remote_backup::*; pub use sd_reseal_remote_backup::*; diff --git a/ddi/tbor/types/src/rsa_mod_exp.rs b/ddi/tbor/types/src/rsa_mod_exp.rs new file mode 100644 index 000000000..5c405bb51 --- /dev/null +++ b/ddi/tbor/types/src/rsa_mod_exp.rs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Host-side wrapper for the TBOR `RsaModExp` command. +//! +//! `RsaModExp` is an **in-session** command (Crypto-Officer or +//! Crypto-User) that performs the RSA private-key primitive +//! `x = y^d mod n` using a caller-held **masked** RSA private key +//! (imported via [`UnwrapKey`](crate::unwrap_key) with the RSA / RSA-CRT +//! key class). It is the raw modular exponentiation underlying RSA +//! decrypt / sign — the host applies and removes any padding. There is no +//! TBOR RSA key generation; RSA keys enter the device only through +//! `UnwrapKey`. +//! +//! `op_type` is a raw 1-byte discriminant (the firmware types it as the +//! `RsaOp` open-enum; this host crate is firewalled from the firmware PAL +//! types). + +use alloc::vec::Vec; + +use crate::tbor; + +/// TBOR opcode for `RsaModExp`. +pub const TBOR_OP_RSA_MOD_EXP: u8 = 0x1A; + +/// Max masked RSA private-key envelope length (RSA-4096-CRT). +pub const RSA_MASKED_KEY_MAX_LEN: usize = 3072; +/// Max RSA modulus length (bytes) — RSA-4096. +pub const RSA_MOD_EXP_MAX_LEN: usize = 512; + +/// `RsaOp` discriminant for the RSA decrypt primitive (requires the +/// masked key's `decrypt` usage attribute). +pub const RSA_OP_DECRYPT: u8 = 1; +/// `RsaOp` discriminant for the RSA sign primitive (requires the masked +/// key's `sign` usage attribute). +pub const RSA_OP_SIGN: u8 = 2; + +/// Host-facing TBOR `RsaModExp` request. +#[tbor(opcode = TBOR_OP_RSA_MOD_EXP, session_ctrl = in_session)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TborRsaModExpReq { + /// Session id this request is bound to. + #[tbor(session_id)] + pub session_id: u16, + + /// The masked RSA private key (from `UnwrapKey`); its kind recovers the + /// modulus size and CRT form. + #[tbor(max_len = 3072)] + pub masked_key: Vec, + + /// The private-key operation, 1-byte `RsaOp` (see `RSA_OP_*`): gates on + /// the masked key's `decrypt` / `sign` usage. + pub op_type: u8, + + /// The input integer `y` in wire little-endian order, exactly the key's + /// modulus length (256 / 384 / 512 B). + #[tbor(max_len = 512)] + pub y: Vec, +} + +/// Host-facing TBOR `RsaModExp` response. +#[tbor(response)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct TborRsaModExpResp { + /// The result `x = y^d mod n` in wire little-endian order, exactly the + /// key's modulus length (256 / 384 / 512 B). + #[tbor(max_len = 512)] + pub x: Vec, +} + +#[cfg(test)] +mod tests { + use azihsm_ddi_tbor_types::TborOpReq; + + use super::*; + + #[test] + fn request_encodes_fields() { + let req = TborRsaModExpReq { + session_id: 7, + masked_key: alloc::vec![0x11u8; 400], + op_type: RSA_OP_SIGN, + y: alloc::vec![0x22u8; 256], + }; + let mut buf = [0u8; 4096]; + let frame = req.encode_request(&mut buf).expect("encode"); + assert!( + frame.contains(&RSA_OP_SIGN), + "encoded frame must carry the op-type discriminant", + ); + } +} diff --git a/ddi/tbor/types/tests/commands/mod.rs b/ddi/tbor/types/tests/commands/mod.rs index 21bb5c930..fb9c54502 100644 --- a/ddi/tbor/types/tests/commands/mod.rs +++ b/ddi/tbor/types/tests/commands/mod.rs @@ -18,6 +18,7 @@ pub mod part_final; pub mod part_info; pub mod part_init; pub mod psk_change; +pub mod rsa_mod_exp; pub mod sd_create_peer_backup; pub mod sd_create_remote_backup; pub mod sd_reseal_remote_backup; diff --git a/ddi/tbor/types/tests/commands/rsa_mod_exp.rs b/ddi/tbor/types/tests/commands/rsa_mod_exp.rs new file mode 100644 index 000000000..6dd4676bb --- /dev/null +++ b/ddi/tbor/types/tests/commands/rsa_mod_exp.rs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Integration tests for the TBOR `RsaModExp` command. +//! +//! `RsaModExp` performs the RSA private-key primitive `x = y^d mod n` +//! using a caller-held **masked** RSA private key (imported via +//! [`UnwrapKey`](super::unwrap_key) with the RSA / RSA-CRT key class). +//! These tests import a host-generated RSA key on-device (RSA-AES-wrap its +//! DER, unwrap into a masked blob), run the modular exponentiation, and +//! verify the result on the host with `azihsm_crypto` (raw, unpadded RSA) +//! — exercising the full `UnwrapKey`(Rsa) → `RsaModExp` path for both CRT +//! and non-CRT vault forms. +//! +//! The device speaks the PKA-native **little-endian** wire format for the +//! `y` input and `x` output; `azihsm_crypto` (OpenSSL) is big-endian +//! native, so the tests reverse each operand at the boundary. + +#![cfg(feature = "emu")] + +use azihsm_crypto::Encrypter; +use azihsm_crypto::ExportableKey; +use azihsm_crypto::Key; +use azihsm_crypto::KeyGenerationOp; +use azihsm_crypto::PrivateKey; +use azihsm_crypto::RsaEncryptAlgo; +use azihsm_crypto::RsaPrivateKey; +use azihsm_crypto::RsaPublicKey; +use azihsm_crypto::RsaSignAlgo; +use azihsm_crypto::Verifier; +use azihsm_ddi_tbor_types::TborRsaModExpReq; +use azihsm_ddi_tbor_types::TborStatus; +use azihsm_ddi_tbor_types::KEY_CLASS_AES; +use azihsm_ddi_tbor_types::KEY_CLASS_RSA; +use azihsm_ddi_tbor_types::KEY_CLASS_RSA_CRT; +use azihsm_ddi_tbor_types::KEY_USAGE_DECRYPT; +use azihsm_ddi_tbor_types::KEY_USAGE_ENCRYPT; +use azihsm_ddi_tbor_types::KEY_USAGE_SIGN; +use azihsm_ddi_tbor_types::KEY_USAGE_VERIFY; +use azihsm_ddi_tbor_types::RSA_OP_DECRYPT; +use azihsm_ddi_tbor_types::RSA_OP_SIGN; + +use crate::commands::sd_sealing_key_gen::finalized_co_session; +use crate::commands::unwrap_key::unwrap; +use crate::commands::unwrap_key::unwrap_with_usage; +use crate::harness::TestCtx; + +/// Reverse `bytes` into a fresh vec (wire-LE ↔ OpenSSL-BE conversion). +fn rev(bytes: &[u8]) -> Vec { + bytes.iter().rev().copied().collect() +} + +/// A non-palindrome big-endian integer of `modulus_len` bytes that stays +/// below the modulus (leading byte `0x01`, so `m < n`). The non-symmetry +/// exercises the wire little-endian operand handling. +fn test_integer(modulus_len: usize) -> Vec { + let mut m = vec![0x02u8; modulus_len]; + m[0] = 0x01; + m +} + +/// Generate a host RSA private key of `modulus_bytes` (256 / 384 / 512 for +/// RSA-2048 / 3072 / 4096), import it on-device via `UnwrapKey` under the +/// given CRT / non-CRT class, and return `(masked_key, host_public_key, +/// modulus_len)`. +fn import_rsa( + ctx: &TestCtx, + session_id: u16, + modulus_bytes: usize, + crt: bool, + usage: u8, +) -> (Vec, RsaPublicKey, usize) { + let key = RsaPrivateKey::generate(modulus_bytes).expect("generate host RSA key"); + let modulus_len = key.size(); + let der = key.to_vec().expect("RSA private DER export"); + let class = if crt { + KEY_CLASS_RSA_CRT + } else { + KEY_CLASS_RSA + }; + // The device grants exactly one usage group; `RsaModExp` Sign needs + // `sign`, Decrypt needs `decrypt`, so the caller requests the group + // matching the operation under test. + let resp = unwrap_with_usage(ctx, session_id, class, usage, &der); + assert!( + !resp.pub_key.is_empty(), + "an imported RSA key returns a re-derived public key", + ); + let pubkey = key.public_key().expect("derive host public key"); + (resp.masked_key, pubkey, modulus_len) +} + +/// Run `RsaModExp` and return the wire-LE `x` result. +fn mod_exp( + ctx: &TestCtx, + session_id: u16, + masked_key: Vec, + op_type: u8, + y_le: Vec, +) -> Vec { + ctx.tbor(&TborRsaModExpReq { + session_id, + masked_key, + op_type, + y: y_le, + }) + .expect("RsaModExp") + .x +} + +/// Import an RSA key, produce `s = m^d mod n` via `RsaModExp { Sign }`, and +/// verify on the host that `s^e mod n == m`. +fn sign_roundtrip(ctx: &TestCtx, session_id: u16, modulus_bytes: usize, crt: bool) { + let (masked_key, pubkey, modulus_len) = import_rsa( + ctx, + session_id, + modulus_bytes, + crt, + KEY_USAGE_SIGN | KEY_USAGE_VERIFY, + ); + let m = test_integer(modulus_len); + + // Device consumes wire-LE `y`, returns wire-LE `x`. + let x_le = mod_exp(ctx, session_id, masked_key, RSA_OP_SIGN, rev(&m)); + assert_eq!( + x_le.len(), + modulus_len, + "result length equals the modulus length" + ); + let signature = rev(&x_le); + + let verified = Verifier::verify(&mut RsaSignAlgo::with_no_padding(), &pubkey, &m, &signature) + .expect("raw RSA verify"); + assert!( + verified, + "RsaModExp Sign must produce a signature verifying over the message (crt={crt}, k={modulus_bytes})", + ); +} + +/// Import an RSA key, raw-encrypt a message with the host public key, and +/// confirm `RsaModExp { Decrypt }` recovers it (`c^d mod n == m`). +fn decrypt_roundtrip(ctx: &TestCtx, session_id: u16, modulus_bytes: usize, crt: bool) { + let (masked_key, pubkey, modulus_len) = import_rsa( + ctx, + session_id, + modulus_bytes, + crt, + KEY_USAGE_ENCRYPT | KEY_USAGE_DECRYPT, + ); + let m = test_integer(modulus_len); + + // c = m^e mod n (big-endian), then fed to the device as wire-LE `y`. + let ciphertext = Encrypter::encrypt_vec(&mut RsaEncryptAlgo::with_no_padding(), &pubkey, &m) + .expect("raw RSA encrypt"); + let x_le = mod_exp( + ctx, + session_id, + masked_key, + RSA_OP_DECRYPT, + rev(&ciphertext), + ); + assert_eq!( + rev(&x_le), + m, + "RsaModExp Decrypt must recover the original message" + ); +} + +#[test] +fn rsa_mod_exp_sign_roundtrip_2k_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + sign_roundtrip(&ctx, session.session_id, 256, false); +} + +#[test] +fn rsa_mod_exp_sign_roundtrip_3k_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + sign_roundtrip(&ctx, session.session_id, 384, false); +} + +#[test] +fn rsa_mod_exp_sign_roundtrip_4k_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + sign_roundtrip(&ctx, session.session_id, 512, false); +} + +#[test] +fn rsa_mod_exp_sign_roundtrip_4k_crt_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + sign_roundtrip(&ctx, session.session_id, 512, true); +} + +#[test] +fn rsa_mod_exp_decrypt_roundtrip_4k_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + decrypt_roundtrip(&ctx, session.session_id, 512, false); +} + +#[test] +fn rsa_mod_exp_sign_roundtrip_2k_crt_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + sign_roundtrip(&ctx, session.session_id, 256, true); +} + +#[test] +fn rsa_mod_exp_decrypt_roundtrip_2k_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + decrypt_roundtrip(&ctx, session.session_id, 256, false); +} + +#[test] +fn rsa_mod_exp_decrypt_roundtrip_2k_crt_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + decrypt_roundtrip(&ctx, session.session_id, 256, true); +} + +#[test] +fn rsa_mod_exp_wrong_y_len_rejected_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + let (masked_key, _pub, modulus_len) = import_rsa( + &ctx, + session.session_id, + 256, + false, + KEY_USAGE_SIGN | KEY_USAGE_VERIFY, + ); + + // A `y` one byte short of the modulus length is rejected. + ctx.expect_fw_reject( + &TborRsaModExpReq { + session_id: session.session_id, + masked_key, + op_type: RSA_OP_SIGN, + y: vec![0x01u8; modulus_len - 1], + }, + TborStatus::InvalidArg, + ); +} + +#[test] +fn rsa_mod_exp_wrong_key_class_rejected_emu() { + let ctx = TestCtx::new(); + let session = finalized_co_session(&ctx); + + // A masked AES key is not an RSA private key: `RsaModExp` must reject + // it as `InvalidKeyType` after unmasking (key-class confusion guard). + let aes = unwrap(&ctx, session.session_id, KEY_CLASS_AES, &[0x42u8; 32]); + ctx.expect_fw_reject( + &TborRsaModExpReq { + session_id: session.session_id, + masked_key: aes.masked_key, + op_type: RSA_OP_SIGN, + y: vec![0x01u8; 256], + }, + TborStatus::InvalidKeyType, + ); +} diff --git a/docs/tbor-ddi/README.md b/docs/tbor-ddi/README.md index 17775ea1b..c37e3bfb4 100644 --- a/docs/tbor-ddi/README.md +++ b/docs/tbor-ddi/README.md @@ -69,6 +69,7 @@ single `none` TOC placeholder and no typed body fields. | `0x12` | `Hmac` | InSession | [`commands/hmac.md`](./commands/hmac.md) | | `0x13` | `GetUnwrappingKey` | InSession | [`commands/get_unwrapping_key.md`](./commands/get_unwrapping_key.md) | | `0x14` | `UnwrapKey` | InSession | [`commands/unwrap_key.md`](./commands/unwrap_key.md) | +| `0x1A` | `RsaModExp` | InSession | [`commands/rsa_mod_exp.md`](./commands/rsa_mod_exp.md) | ## Default-PSK gate diff --git a/docs/tbor-ddi/commands/rsa_mod_exp.md b/docs/tbor-ddi/commands/rsa_mod_exp.md new file mode 100644 index 000000000..a993a2271 --- /dev/null +++ b/docs/tbor-ddi/commands/rsa_mod_exp.md @@ -0,0 +1,77 @@ + + +# RsaModExp (Opcode 0x1A) + +**Handler:** `fw/core/lib/src/ddi/tbor/rsa_mod_exp.rs` +**Session:** InSession + +## Description + +Performs the RSA private-key primitive `x = y^d mod n` using a +caller-held **masked** RSA private key (imported via +[`UnwrapKey`](./unwrap_key.md) with the RSA / RSA-CRT key class). + +The device unmasks the key **in place** in the request buffer (recovering +its modulus size and CRT form from the blob's key kind), checks the usage +attribute the requested operation needs, computes the modular +exponentiation, and returns the result. The recovered plaintext key is +scrubbed from the request buffer on every path. This is the raw primitive +underlying RSA decrypt / sign — the host applies and removes any padding. +This is the TBOR analogue of MBOR `RsaModExp`, keyed by a masked blob +instead of a vault id. + +There is no TBOR RSA key generation; RSA keys enter the device only +through `UnwrapKey`. + +Both the input `y` and the output `x` are in PKA-native **little-endian** +byte order; the device flips endianness internally if its primitive is +big-endian native (e.g. OpenSSL on the emulator). + +Available to **both Crypto-Officer and Crypto-User** sessions. + +## Request + +### TOC entries + +| Offset | Field | Type | Description | +|---|---|---|---| +| 4 | `session_id` | `session_id` (inline) | Session this request is bound to; cross-checked against the SQE-carried session id. | +| 8 | `masked_key` | `buffer` (164..=3072 B) | The masked RSA private key; unmasked in place. Its kind recovers the modulus size and CRT form. | +| — | `op_type` | `u8` (inline) | The [`RsaOp`] selecting the required usage attribute: `1` = Decrypt (needs `decrypt`), `2` = Sign (needs `sign`). | +| — | `y` | `buffer` (256 / 384 / 512 B) | The input integer `y` in wire little-endian order, exactly the key's modulus length. | + +### Data section + +Carries the masked key followed by the input integer. + +## Response + +### TOC entries + +| Offset | Field | Type | Description | +|---|---|---|---| +| 8 | `x` | `buffer` (256 / 384 / 512 B) | The result `x = y^d mod n` in wire little-endian order, exactly the key's modulus length. | + +### Data section + +Carries the result integer. + +## Errors + +| Error | Cause | +|---|---| +| `SessionNotFound` | `session_id` does not refer to an `Active` slot | +| `InvalidArg` | Unknown `op_type`, or `y` length ≠ the key's modulus length | +| `InvalidKeyType` | The masked blob is not an RSA private key | +| `InvalidPermissions` | The key lacks the usage the operation needs (`decrypt` for Decrypt, `sign` for Sign) | +| `MaskedKeyDecodeFailed` / `AesGcmDecryptTagDoesNotMatch` | The masked key is malformed or fails authentication (wrong scope / tampered) | +| `DefaultPskMustRotate` | The calling role's PSK is still the compiled-in default (dispatcher, pre-handler) | +| `DdiDecodeFailed` | Malformed request body | + +## See also + +- Import an RSA key: [`unwrap_key.md`](./unwrap_key.md) +- Wire schema: `fw/core/ddi/tbor/types/src/rsa_mod_exp.rs` diff --git a/fw/core/ddi/tbor/types/src/lib.rs b/fw/core/ddi/tbor/types/src/lib.rs index 8be86e435..43dce834b 100644 --- a/fw/core/ddi/tbor/types/src/lib.rs +++ b/fw/core/ddi/tbor/types/src/lib.rs @@ -46,6 +46,7 @@ pub mod part_info; pub mod part_init; pub mod policy; pub mod psk_change; +pub mod rsa_mod_exp; pub mod sd_create_peer_backup; pub mod sd_create_remote_backup; pub mod sd_reseal_remote_backup; @@ -70,6 +71,7 @@ pub use part_info::*; pub use part_init::*; pub use policy::*; pub use psk_change::*; +pub use rsa_mod_exp::*; pub use sd_create_peer_backup::*; pub use sd_create_remote_backup::*; pub use sd_reseal_remote_backup::*; diff --git a/fw/core/ddi/tbor/types/src/rsa_mod_exp.rs b/fw/core/ddi/tbor/types/src/rsa_mod_exp.rs new file mode 100644 index 000000000..b2d2bfd09 --- /dev/null +++ b/fw/core/ddi/tbor/types/src/rsa_mod_exp.rs @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `RsaModExp` wire schema. +//! +//! `RsaModExp` is an in-session command that performs the RSA private-key +//! primitive `x = y^d mod n` using a caller-held **masked** RSA private +//! key (imported via [`UnwrapKey`](crate::unwrap_key) with the RSA / +//! RSA-CRT key class). The device unmasks the key on-device (recovering +//! its modulus size from the key kind), computes the modular +//! exponentiation, and returns the result. This is the raw primitive +//! underlying RSA decrypt / sign — the host applies and removes any +//! padding. This is the TBOR analogue of MBOR `RsaModExp`, keyed by a +//! masked blob instead of a vault id. There is no TBOR RSA key +//! generation; RSA keys enter the device only through `UnwrapKey`. +//! +//! Inputs: +//! +//! * `session_id` — TOC-carried session id; cross-checked by the dispatcher. +//! * `masked_key` — the masked RSA private key (unmasked in place); its +//! kind recovers the modulus size and CRT form. +//! * `op_type` — the [`RsaOp`] selecting which usage attribute gates the +//! operation (`Decrypt` → `decrypt`, `Sign` → `sign`). +//! * `y` — the input integer in wire little-endian order, exactly the +//! key's modulus length (256 / 384 / 512 B for RSA-2048 / 3072 / 4096). +//! +//! Outputs: +//! +//! * `x` — the result `y^d mod n` in wire little-endian order, exactly the +//! key's modulus length. + +use azihsm_fw_ddi_tbor_api::tbor; +use open_enum::open_enum; + +pub use crate::unwrap_key::UNWRAP_MASKED_KEY_MAX_LEN; + +/// TBOR opcode for `RsaModExp`. +pub const TBOR_OP_RSA_MOD_EXP: u8 = 0x1A; + +/// Minimum accepted masked RSA private-key envelope length. A coarse +/// floor (an RSA-2048 masked key is far larger — its modulus alone is +/// 256 B); the authoritative gate is `unmask` (AEAD-tag verification) plus +/// the key-kind check in the handler. Pinned into the `#[tbor(buffer, +/// min_len = 164)]` literal on [`TborRsaModExpReq::masked_key`]. +pub const RSA_MASKED_KEY_MIN_LEN: usize = 164; + +/// Maximum accepted masked RSA private-key envelope length — the largest +/// masked key [`UnwrapKey`](crate::unwrap_key) can produce (RSA-4096-CRT). +/// Pinned into the `#[tbor(buffer, max_len = 3072)]` literal on +/// [`TborRsaModExpReq::masked_key`]. +pub const RSA_MASKED_KEY_MAX_LEN: usize = UNWRAP_MASKED_KEY_MAX_LEN; + +/// Maximum RSA modulus length (bytes) — RSA-4096. Pinned into the +/// `#[tbor(buffer, max_len = 512)]` literals on +/// [`TborRsaModExpReq::y`] / [`TborRsaModExpResp::x`]. +pub const RSA_MOD_EXP_MAX_LEN: usize = 512; + +// Keep the schema-literal bounds in sync with the named constants (the +// tbor derive requires integer literals in the attributes). +const _: () = assert!(RSA_MASKED_KEY_MAX_LEN == 3072); + +/// RSA private-key operation selector on the TBOR wire. +/// +/// Selects which usage attribute the masked key must carry for the +/// modular exponentiation to be permitted. The 1-byte discriminants +/// mirror the MBOR `DdiRsaOpType` values (`Decrypt = 1`, `Sign = 2`). +/// Kept as an [`open_enum`] so an unrecognized discriminant round-trips as +/// `RsaOp(x)` and is rejected on-device rather than failing to decode. +#[repr(u8)] +#[open_enum] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RsaOp { + /// RSA decrypt primitive — requires the `decrypt` usage attribute. + Decrypt = 1, + + /// RSA sign primitive — requires the `sign` usage attribute. + Sign = 2, +} + +/// `RsaModExp` request schema. +/// +/// `masked_key` is `#[tbor(mutable)]` so the handler can `unmask` it **in +/// place** in the request buffer (via `decode_mut`) — no scratch copy — and +/// compute directly from the recovered key. +#[tbor(opcode = 0x1A)] +pub struct TborRsaModExpReq<'a> { + /// CO/CU session id this request is bound to. + #[tbor(session_id)] + pub session_id: SessionId, + + /// The masked RSA private key (from `UnwrapKey`), an AEAD-GCM-256 + /// envelope of 164..=3072 B. Its kind recovers the modulus size and + /// CRT form. + #[tbor(buffer, min_len = 164, max_len = 3072, mutable)] + pub masked_key: &'a [u8], + + /// The private-key operation, 1-byte [`RsaOp`] (gates on `decrypt` / + /// `sign`). + #[tbor(U8)] + pub op_type: RsaOp, + + /// The input integer `y` in wire little-endian order, exactly the key's + /// modulus length (256 / 384 / 512 B). + #[tbor(buffer, max_len = 512)] + pub y: &'a [u8], +} + +/// `RsaModExp` response schema. +/// +/// `x` is `#[tbor(mutable)]` so the handler can reserve the slot and have +/// the PAL write `y^d mod n` straight into it (`decode_mut`). +#[tbor(response)] +pub struct TborRsaModExpResp<'a> { + /// The result `x = y^d mod n` in wire little-endian order, exactly the + /// key's modulus length (256 / 384 / 512 B). + #[tbor(buffer, max_len = 512, mutable)] + pub x: &'a [u8], +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use azihsm_fw_ddi_tbor_api::SessionId; + + use super::*; + + #[test] + fn request_round_trips_fields() { + let mut buf = [0u8; 4096]; + let masked = [0x11u8; RSA_MASKED_KEY_MIN_LEN]; + let y = [0x22u8; 256]; + let frame = TborRsaModExpReq::encode(&mut buf) + .unwrap() + .session_id(SessionId(7)) + .unwrap() + .masked_key(&masked) + .unwrap() + .op_type(RsaOp::Sign) + .unwrap() + .y(&y) + .unwrap() + .finish(); + assert_eq!(frame.op_type(), RsaOp::Sign); + assert_eq!(frame.y(), &y[..]); + } + + #[test] + fn response_round_trips_x() { + let mut buf = [0u8; 1024]; + let x = [0x33u8; RSA_MOD_EXP_MAX_LEN]; + let frame = TborRsaModExpResp::encode(&mut buf, 0, true) + .unwrap() + .x(&x) + .unwrap() + .finish(); + assert_eq!(frame.x(), &x[..]); + } + + #[test] + fn lengths_match_pinned_values() { + assert_eq!(RSA_MOD_EXP_MAX_LEN, 512); + assert_eq!(RSA_MASKED_KEY_MAX_LEN, 3072); + assert_eq!(RSA_MASKED_KEY_MIN_LEN, 164); + } +} diff --git a/fw/core/lib/src/ddi/tbor/from_pal.rs b/fw/core/lib/src/ddi/tbor/from_pal.rs index ce16224c0..3c4244c25 100644 --- a/fw/core/lib/src/ddi/tbor/from_pal.rs +++ b/fw/core/lib/src/ddi/tbor/from_pal.rs @@ -10,6 +10,9 @@ //! [`HsmVaultKeyKind::SdSealing`]) belong only in this table. use azihsm_fw_hsm_pal_traits::HsmEccCurve; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmRsaKey; use azihsm_fw_hsm_pal_traits::HsmVaultKeyKind; /// Map an ECC-private vault key kind to its NIST curve, or `None` if the @@ -25,3 +28,22 @@ pub(crate) fn ecc_curve(kind: HsmVaultKeyKind) -> Option { _ => None, } } + +/// Map an RSA-private vault key kind (plain or CRT) to its PAL modulus +/// selector, strictly. +/// +/// Only the six RSA-private kinds are accepted; any other kind — an RSA +/// public key, a non-RSA kind — maps to [`HsmError::InvalidKeyType`], so a +/// masked blob of the wrong class is rejected before use (mirrors the MBOR +/// `from_pal::rsa_key` precedent used by `RsaModExp`). +pub(crate) fn rsa_key(kind: HsmVaultKeyKind) -> HsmResult { + match kind { + HsmVaultKeyKind::Rsa2kPrivate => Ok(HsmRsaKey::Rsa2048Priv), + HsmVaultKeyKind::Rsa3kPrivate => Ok(HsmRsaKey::Rsa3072Priv), + HsmVaultKeyKind::Rsa4kPrivate => Ok(HsmRsaKey::Rsa4096Priv), + HsmVaultKeyKind::Rsa2kPrivateCrt => Ok(HsmRsaKey::Rsa2048CrtPriv), + HsmVaultKeyKind::Rsa3kPrivateCrt => Ok(HsmRsaKey::Rsa3072CrtPriv), + HsmVaultKeyKind::Rsa4kPrivateCrt => Ok(HsmRsaKey::Rsa4096CrtPriv), + _ => Err(HsmError::InvalidKeyType), + } +} diff --git a/fw/core/lib/src/ddi/tbor/mod.rs b/fw/core/lib/src/ddi/tbor/mod.rs index aa617302f..52a5a13ca 100644 --- a/fw/core/lib/src/ddi/tbor/mod.rs +++ b/fw/core/lib/src/ddi/tbor/mod.rs @@ -31,6 +31,7 @@ pub mod part_info; pub mod part_init; pub mod policy; pub(crate) mod psk_change; +pub(crate) mod rsa_mod_exp; pub(crate) mod sd_backup; pub(crate) mod sd_create_peer_backup; pub(crate) mod sd_create_remote_backup; @@ -194,6 +195,12 @@ pub(crate) mod opcode { /// return it masked under the requested scope's masking key (plus the /// re-derived public key for RSA / ECC). See [`super::unwrap_key`]. pub(crate) const UNWRAP_KEY: u8 = 0x14; + + /// `RsaModExp` — perform the RSA private-key primitive `x = y^d mod n` + /// using a caller-held masked RSA private key (imported via + /// `UnwrapKey`; unmasked on-device). The raw modular exponentiation + /// underlying RSA decrypt / sign. See [`super::rsa_mod_exp`]. + pub(crate) const RSA_MOD_EXP: u8 = 0x1A; } /// Validate that `sess_id` belongs to an active Crypto-Officer session. @@ -411,6 +418,7 @@ pub(crate) async fn dispatch<'p, P: HsmPal>( opcode::HMAC => hmac::handle(pal, io, req_buf).await, opcode::GET_UNWRAPPING_KEY => get_unwrapping_key::handle(pal, io, req_buf).await, opcode::UNWRAP_KEY => unwrap_key::handle(pal, io, req_buf, undo).await, + opcode::RSA_MOD_EXP => rsa_mod_exp::handle(pal, io, req_buf).await, _ => Err(HsmError::UnsupportedCmd), } } @@ -442,6 +450,7 @@ fn is_known_opcode(opcode: u8) -> bool { | opcode::HMAC | opcode::GET_UNWRAPPING_KEY | opcode::UNWRAP_KEY + | opcode::RSA_MOD_EXP ) } @@ -479,7 +488,8 @@ fn is_in_session(opcode: u8) -> bool { | opcode::HMAC_GENERATE_KEY | opcode::HMAC | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => true, + | opcode::UNWRAP_KEY + | opcode::RSA_MOD_EXP => true, // Default-deny: any future opcode is treated as in-session // until classified, so the default-PSK gate applies to it. _ => true, @@ -525,7 +535,8 @@ fn needs_session_id_cross_check(opcode: u8) -> bool { | opcode::HMAC_GENERATE_KEY | opcode::HMAC | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => true, + | opcode::UNWRAP_KEY + | opcode::RSA_MOD_EXP => true, _ => true, } } diff --git a/fw/core/lib/src/ddi/tbor/rsa_mod_exp.rs b/fw/core/lib/src/ddi/tbor/rsa_mod_exp.rs new file mode 100644 index 000000000..3ae5b2a44 --- /dev/null +++ b/fw/core/lib/src/ddi/tbor/rsa_mod_exp.rs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TBOR `RsaModExp` command handler. +//! +//! Within an open session, perform the RSA private-key primitive +//! `x = y^d mod n` using a caller-held **masked** RSA private key +//! (imported via [`UnwrapKey`](super::unwrap_key) with the RSA / RSA-CRT +//! key class). The key is unmasked **in place** in the request buffer (no +//! scratch copy), its modulus size / CRT form recovered from the blob's +//! key kind, and the result written straight into the reserved response +//! slot. This is the raw modular exponentiation underlying RSA decrypt / +//! sign — the host applies and removes any padding. This is the TBOR +//! analogue of MBOR `RsaModExp`, keyed by a masked blob instead of a vault +//! id. There is no TBOR RSA key generation; RSA keys enter the device +//! only through `UnwrapKey`. +//! +//! Available to both Crypto-Officer and Crypto-User sessions. + +use azihsm_fw_core_crypto_key_masking::aead::peek_metadata; +use azihsm_fw_core_crypto_key_masking::aead::unmask; +use azihsm_fw_ddi_tbor_types::RsaOp; +use azihsm_fw_ddi_tbor_types::TborRsaModExpReq; +use azihsm_fw_ddi_tbor_types::TborRsaModExpResp; +use azihsm_fw_hsm_pal_traits::DmaBuf; +use azihsm_fw_hsm_pal_traits::HsmError; +use azihsm_fw_hsm_pal_traits::HsmIo; +use azihsm_fw_hsm_pal_traits::HsmPal; +use azihsm_fw_hsm_pal_traits::HsmResult; +use azihsm_fw_hsm_pal_traits::HsmSessId; + +use super::from_pal::rsa_key; +use super::resolve_masking_key; +use super::validate_active_session; + +/// Handle a TBOR `RsaModExp` request. +/// +/// No partition lock or undo log is required: the command reads no mutable +/// partition state and **persists nothing** — it unmasks the caller's key, +/// computes `y^d mod n`, and returns the result. Takes `req_buf: &mut +/// DmaBuf` so the masked key can be unmasked in place (`decode_mut`). +pub(crate) async fn handle<'p, P: HsmPal>( + pal: &'p P, + io: &impl HsmIo, + req_buf: &mut DmaBuf, +) -> HsmResult<&'p DmaBuf> { + let req = TborRsaModExpReq::decode_mut(req_buf)?; + let sess_id = HsmSessId::from(u16::from(req.session_id)); + validate_active_session(pal, io, sess_id)?; + + // Map the wire op to the required usage attribute BEFORE unmasking, + // rejecting an unknown `op_type` up front (so a garbage op does not + // trigger an unmask). The specific attribute is checked post-unmask. + let require_sign = match RsaOp(req.op_type) { + RsaOp::Sign => true, + RsaOp::Decrypt => false, + _ => return Err(HsmError::InvalidArg), + }; + + // The scope that masked this key is recorded (cleartext, tag-bound) in + // the blob metadata; resolve its masking key before unmasking. The + // peek borrow is transient — it ends before the in-place unmask. + let scope = peek_metadata(req.masked_key)?.usage_flags().scope(); + let masking_key = resolve_masking_key(pal, io, scope, sess_id)?; + + // Unmask the private key in place, compute, and build the response + // inside a block that yields a `Result`, so **every** post-unmask path + // (success or error) falls through to the `masked_key` wipe below — the + // recovered plaintext must never survive in the request buffer. + let outcome: HsmResult<&'p DmaBuf> = async { + let view = unmask(pal, io, masking_key, req.masked_key).await?; + + // Recover the modulus size / CRT form; a non-RSA-private blob is + // rejected as `InvalidKeyType`. + let key_size = rsa_key(view.key_kind)?; + + // The permitted operation depends on the key's usage attributes: a + // decrypt primitive needs `decrypt`, a sign primitive needs `sign`. + let permitted = if require_sign { + view.key_attrs.sign() + } else { + view.key_attrs.decrypt() + }; + if !permitted { + return Err(HsmError::InvalidPermissions); + } + + // The input integer `y` must be exactly the modulus length. + let modulus_len = key_size.modulus_len(); + if req.y.len() != modulus_len { + return Err(HsmError::InvalidArg); + } + + // Reserve the modulus-sized `x` slot and have the PAL compute + // `y^d mod n` straight into it — no scratch, no copy. + let resp = pal.dma_alloc_var(io, |buf| { + let frame = TborRsaModExpResp::encode(buf, 0, false)? + .x_reserve(modulus_len)? + .finish(); + Ok(frame.as_bytes().len()) + })?; + { + let out = TborRsaModExpResp::decode_mut(resp)?; + pal.mod_exp_priv(io, key_size, view.target_key, req.y, out.x) + .await?; + } + + // Coerce the `&mut` response buffer to a shared `&DmaBuf` (preserving + // the `'p` allocator lifetime) so the async block's output matches + // the handler's `&'p DmaBuf` return. + let resp: &'p DmaBuf = resp; + Ok(resp) + } + .await; + + // Scrub the recovered plaintext key from the request buffer. + req.masked_key.zeroize(); + outcome +} diff --git a/fw/core/lib/src/op.rs b/fw/core/lib/src/op.rs index 0dd73af13..00e8193ca 100644 --- a/fw/core/lib/src/op.rs +++ b/fw/core/lib/src/op.rs @@ -272,7 +272,8 @@ impl SessionCtrl { | opcode::HMAC_GENERATE_KEY | opcode::HMAC | opcode::GET_UNWRAPPING_KEY - | opcode::UNWRAP_KEY => Self::InSession, + | opcode::UNWRAP_KEY + | opcode::RSA_MOD_EXP => Self::InSession, opcode::SESSION_CLOSE => Self::Close, _ => Self::NoSession, }