Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ddi/tbor/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::*;
Expand Down
92 changes: 92 additions & 0 deletions ddi/tbor/types/src/rsa_mod_exp.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,

/// 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<u8>,
}

/// 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<u8>,
}

#[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",
);
}
}
1 change: 1 addition & 0 deletions ddi/tbor/types/tests/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
266 changes: 266 additions & 0 deletions ddi/tbor/types/tests/commands/rsa_mod_exp.rs
Original file line number Diff line number Diff line change
@@ -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<u8> {
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<u8> {
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<u8>, 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<u8>,
op_type: u8,
y_le: Vec<u8>,
) -> Vec<u8> {
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,
);
}
1 change: 1 addition & 0 deletions docs/tbor-ddi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading