Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions ddi/mbor/types/tests/azihsm_ddi_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ mod integration {
pub mod get_session_encryption_key;
pub mod get_session_encryption_key_smoke;
pub mod get_unwrapping_key;
pub mod get_unwrapping_key_smoke;
pub mod hkdf_smoke;
pub mod hmac;
pub mod hmac_smoke;
Expand Down
107 changes: 107 additions & 0 deletions ddi/mbor/types/tests/integration/get_unwrapping_key_smoke.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! GetUnwrappingKey smoke tests.
//!
//! - Happy path: with an open session, the command returns an RSA-2048
//! public key and a vault key id. We intentionally do *not* assert
//! on `masked_key` — the firmware emits an empty placeholder until
//! vault-key masking is wired up — nor on the exact `key_id` value,
//! which is an opaque handle (the sim backend legitimately assigns
//! `0`).
//! - Stability: two calls on the same partition return the same key id
//! and public-key bytes — the key is partition-cached and lazily
//! generated on first use.
//! - Without a session: rejected with `FileHandleSessionIdDoesNotMatch`
//! by the host-side dev validator before the request reaches firmware.

#![cfg(test)]

use azihsm_ddi::*;
use azihsm_ddi_mbor_types::*;
use test_with_tracing::test;

use super::common::*;

#[test]
fn test_get_unwrapping_key_smoke() {
ddi_dev_test(
common_setup,
common_cleanup,
|dev, _ddi, _path, session_id| {
let resp = helper_get_unwrapping_key(
dev,
Some(session_id),
Some(DdiApiRev { major: 1, minor: 0 }),
)
.expect("get_unwrapping_key should succeed");

assert_eq!(resp.hdr.op, DdiOp::GetUnwrappingKey);
assert_eq!(resp.hdr.status, DdiStatus::Success);
assert_eq!(resp.hdr.sess_id, Some(session_id));

assert!(
!resp.data.pub_key.der.is_empty(),
"unwrap pub_key must be non-empty"
);
assert_eq!(
resp.data.pub_key.key_kind,
DdiKeyType::Rsa2kPublic,
"unwrap key must be RSA-2048"
);
},
Comment thread
jaygmsft marked this conversation as resolved.
);
}

#[test]
fn test_get_unwrapping_key_stable_across_calls_smoke() {
ddi_dev_test(
common_setup,
common_cleanup,
|dev, _ddi, _path, session_id| {
let r1 = helper_get_unwrapping_key(
dev,
Some(session_id),
Some(DdiApiRev { major: 1, minor: 0 }),
)
.expect("first get_unwrapping_key");
let r2 = helper_get_unwrapping_key(
dev,
Some(session_id),
Some(DdiApiRev { major: 1, minor: 0 }),
)
.expect("second get_unwrapping_key");

assert_eq!(
r1.data.key_id, r2.data.key_id,
"repeat call must return the same key id"
);
assert_eq!(
r1.data.pub_key.der.as_slice(),
r2.data.pub_key.der.as_slice(),
"repeat call must return the same pub_key bytes"
);
},
);
}

#[test]
fn test_get_unwrapping_key_no_session_smoke() {
ddi_dev_test(
common_setup,
common_cleanup,
|dev, _ddi, _path, _session_id| {
let err = helper_get_unwrapping_key(dev, None, Some(DdiApiRev { major: 1, minor: 0 }))
.expect_err("must be rejected without a session");

assert!(
matches!(
err,
DdiError::DdiStatus(DdiStatus::FileHandleSessionIdDoesNotMatch)
),
"expected FileHandleSessionIdDoesNotMatch, got {:?}",
err
);
},
);
}
2 changes: 1 addition & 1 deletion fw/core/ddi/mbor/types/src/get_unwrapping_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct DdiGetUnwrappingKeyReq {}
pub struct DdiGetUnwrappingKeyResp<'a> {
#[ddi(id = 1)]
pub key_id: u16,
#[ddi(id = 2)]
#[ddi(id = 2, frame)]
pub pub_key: DdiPublicKey<'a>,
#[ddi(id = 3, max_len = 1024)]
pub masked_key: &'a [u8],
Expand Down
78 changes: 78 additions & 0 deletions fw/core/lib/src/ddi/mbor/get_unwrapping_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! DDI GetUnwrappingKey command handler.
//!
//! Within an open session, return the partition's RSA-2048 *unwrapping*
//! key — the public key (raw wire `n_le ‖ e_le`) plus its vault key id,
//! used by the host to RSA-AES key-wrap a payload for
//! [`RsaUnwrap`](super::DdiOp::RsaUnwrap).
//!
//! The unwrapping key is materialised by the PAL and cached for the
//! partition's current enabled incarnation — it is dropped (and its
//! vault slot deleted) when the partition is disabled or freed, and
//! regenerated on the next request after a re-enable. RSA key
//! generation is expensive, so it is never done at partition enable:
//! the std (emulator) PAL generates it lazily on the first call, while
//! hardware PALs generate it in the background from partition init and
//! report `PendingKeyGeneration` until it is ready, prompting the host
//! to retry.

use azihsm_fw_ddi_mbor_types::get_unwrapping_key::DdiGetUnwrappingKeyReq;
use azihsm_fw_ddi_mbor_types::get_unwrapping_key::DdiGetUnwrappingKeyResp;
use azihsm_fw_ddi_mbor_types::DdiPublicKeyFrameParams;

use super::*;

/// Handle `DdiGetUnwrappingKeyCmd`.
pub(crate) async fn get_unwrapping_key<'p, P: HsmPal>(
pal: &'p P,
io: &impl HsmIo,
decoder: &mut DdiDecoder<'_>,
hdr: &DdiReqHdr,
) -> HsmResult<&'p DmaBuf> {
let _body: DdiGetUnwrappingKeyReq = decoder.decode_data()?;
let sess_id = hdr.sess_id.ok_or(HsmError::SessionExpected)?;

// Ensure the partition's RSA-2048 unwrapping key exists (generated
// lazily by the std PAL, or in the background on hardware) and get
// its vault key id. On hardware this may return
// `PendingKeyGeneration` while generation is in flight, which the
// host retries.
let key_id = pal.part_ensure_unwrapping_key(io).await?;

// Query the wire length of the unwrapping public key derived from
// the vault-stored private key — no separate public key is cached
// (matches the reference firmware). The actual serialization writes
// straight into the reserved response slot below.
let priv_key = pal.vault_key(io, key_id)?;
let pub_len = pal.rsa_priv_pub_key(io, priv_key, None)?;

Comment thread
jaygmsft marked this conversation as resolved.
// `masked_key` is the host's opaque re-import blob; firmware-side
// masking is pending the corresponding unmask path — emit an empty
// placeholder for now so the response is wire-valid.
let (resp, layout) = pal.dma_alloc_var_with(io, |buf| {
let mut encoder = super::encode_resp_hdr(
&super::success_hdr_sess(hdr, DdiOp::GetUnwrappingKey, sess_id),
buf,
)?;
let layout = DdiGetUnwrappingKeyResp::reserve(
&mut encoder,
u16::from(key_id),
DdiPublicKeyFrameParams {
raw_len: pub_len,
key_kind: DdiKeyType::Rsa2kPublic,
},
0, /* masked_key length — empty placeholder */
)?;
Ok((encoder.position(), layout))
})?;

// Serialize the wire-format public key directly into the frame's
// reserved slot — the PAL converts its vault representation into the
// wire form (incl. any big-endian↔little-endian flip).
let frame = DdiGetUnwrappingKeyResp::from_layout(resp, &layout);
pal.rsa_priv_pub_key(io, priv_key, Some(frame.pub_key.raw))?;

Comment thread
jaygmsft marked this conversation as resolved.
Ok(resp)
}
3 changes: 3 additions & 0 deletions fw/core/lib/src/ddi/mbor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub(crate) mod get_device_info;
pub(crate) mod get_establish_cred_encryption_key;
pub(crate) mod get_sealed_bk3;
pub(crate) mod get_session_encryption_key;
pub(crate) mod get_unwrapping_key;
pub(crate) mod hkdf_derive;
pub(crate) mod hmac;
pub(crate) mod init_bk3;
Expand Down Expand Up @@ -48,6 +49,7 @@ pub(crate) use get_device_info::*;
pub(crate) use get_establish_cred_encryption_key::*;
pub(crate) use get_sealed_bk3::*;
pub(crate) use get_session_encryption_key::*;
pub(crate) use get_unwrapping_key::*;
pub(crate) use hkdf_derive::*;
pub(crate) use hmac::*;
pub(crate) use init_bk3::*;
Expand Down Expand Up @@ -124,6 +126,7 @@ pub(crate) async fn dispatch<'p, P: HsmPal>(
get_establish_cred_encryption_key(pal, io, decoder, hdr).await
}
DdiOp::GetSessionEncryptionKey => get_session_encryption_key(pal, io, decoder, hdr).await,
DdiOp::GetUnwrappingKey => get_unwrapping_key(pal, io, decoder, hdr).await,
DdiOp::GetSealedBk3 => get_sealed_bk3(pal, io, decoder, hdr),
DdiOp::SetSealedBk3 => set_sealed_bk3(pal, io, decoder, hdr),
DdiOp::InitBk3 => init_bk3(pal, io, decoder, hdr).await,
Expand Down
35 changes: 34 additions & 1 deletion fw/pal/traits/src/crypto/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ pub trait HsmRsa {
/// - `Err(HsmError::InvalidArg)` — buffer-size mismatch.
/// - `Err(HsmError)` — PKA / RNG failure or PCT failed (the key
/// pair is rejected).
async fn ras_gen_keypair(
async fn rsa_gen_keypair(
&self,
io: &impl HsmIo,
key_size: HsmRsaKey,
Expand Down Expand Up @@ -257,6 +257,39 @@ pub trait HsmRsa {
y: &mut DmaBuf,
) -> Result<(), HsmError>;

/// Derive the raw wire-format public key (`n_le || e_le`) from a
/// vault-stored RSA private key.
///
/// Used to recover the public key of a vault-stored private key
/// (e.g. the partition unwrapping key) without persisting a
/// separate copy.
///
/// `priv_key` is in the PAL's own vault representation — raw key
/// material on real PKA hardware, DER in the std/OpenSSL PAL — and
/// this method converts it into the wire form: the little-endian
/// modulus followed by a fixed 4-byte little-endian exponent (the
/// raw layout the host's `DdiDerPublicKey` post-decode turns into
/// DER). The PAL owns the whole vault-format → wire-format
/// conversion, including any big-endian↔little-endian flip (real
/// PKA is little-endian native; an OpenSSL backend holds big-endian
/// components and reverses them).
///
/// Follows the query/alloc/use convention: pass `pub_out = None` to
/// query the wire length the caller must allocate, then
/// `pub_out = Some(buf)` to serialize.
///
/// # Returns
/// - `Ok(len)` — wire byte length (`modulus_len + 4`); in `Some`
/// mode those bytes are written into `pub_out`.
/// - `Err(HsmError::InvalidArg)` — `priv_key` is not valid vault
/// format or `pub_out` is too small.
Comment thread
jaygmsft marked this conversation as resolved.
Outdated
fn rsa_priv_pub_key(
&self,
io: &impl HsmIo,
priv_key: &DmaBuf,
pub_out: Option<&mut DmaBuf>,
) -> HsmResult<usize>;

/// PKCS#1 v1.5 encrypt (EME-PKCS1-v1_5).
///
/// Pads `message` with random non-zero bytes per RFC 8017 §7.2.1
Expand Down
30 changes: 30 additions & 0 deletions fw/pal/traits/src/part.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,36 @@ pub trait HsmPartitionManager {
/// [`HsmResult<()>`] — `Ok(())` on success. Idempotent on an
/// already-absent slot (also returns `Ok(())`).
fn part_prop_clear(&self, io: &impl HsmIo, id: PartPropId) -> HsmResult<()>;

/// Obtain the partition's RSA-2048 unwrapping key, returning its
/// vault key id once the key is available.
///
/// The key is materialised once per enabled partition incarnation:
/// the private key lives in the vault (with `internal`/`local`/
/// `unwrap` attributes) and its id in
/// [`RSA_UNWRAPPING_KEY_ID`](PartPropId::RSA_UNWRAPPING_KEY_ID).
/// It is dropped when the partition is disabled or freed and
/// regenerated on the next request after a re-enable. No public key
/// is cached — callers derive it on demand from the vault private
/// key via
/// [`HsmRsa::rsa_priv_pub_key`](crate::HsmRsa::rsa_priv_pub_key),
/// matching the reference firmware.
///
/// RSA key generation is expensive, so PALs never generate it at
/// partition enable. Implementations differ in *when* they
/// generate:
/// - The std (emulator) PAL generates lazily on the first call and
/// returns the id synchronously.
/// - Hardware PALs kick generation off in the background at
/// partition init; while it is still in flight they return
/// [`HsmError::PendingKeyGeneration`] so the host retries until
/// the key is ready.
///
/// # Errors
/// - [`HsmError::PendingKeyGeneration`] — generation is still in
/// progress; the host should retry.
/// - Propagated PKA / vault failures.
async fn part_ensure_unwrapping_key(&self, io: &impl HsmIo) -> HsmResult<HsmKeyId>;
Comment thread
jaygmsft marked this conversation as resolved.
Outdated
}

/// Length of the per-partition `BK_BOOT` boot-key material in bytes.
Expand Down
24 changes: 1 addition & 23 deletions fw/plat/std/pal/src/drivers/ecc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,9 @@ use azihsm_crypto::SignOp;
use azihsm_crypto::VerifyOp;
use azihsm_fw_hsm_pal_traits::*;

use super::reverse_copy;
use crate::worker::WorkerPool;

// On-wire-format helpers
//
// The std PAL accepts inputs and produces outputs in the wire-LE
// convention that real PKA hardware emits natively (`x_le || y_le`
// for public keys, `r_le || s_le` for signatures, with P-521
// components padded from 66 to 68 bytes per word). The single
// `reverse_copy` helper centralizes the BE↔LE byte reversal so the
// `_le`-suffixed driver methods can stay free of byte-shuffling
// boilerplate.

/// Reverse-copy `src` into `dst[..src.len()]`. Used for BE↔LE
/// component swaps; the direction is symmetric. Trailing pad bytes
/// in `dst` (e.g. the 2-byte pad after a 66-byte P-521 coordinate
/// inside a 68-byte word) are left untouched — callers that need
/// them zeroed should pre-`fill(0)` the full `dst` slot.
fn reverse_copy(dst: &mut [u8], src: &[u8]) {
let len = src.len();
debug_assert!(dst.len() >= len);
for (d, s) in dst[..len].iter_mut().zip(src.iter().rev()) {
*d = *s;
}
}

/// Std ECC driver — software ECC via OpenSSL with async worker dispatch.
pub struct StdEcc {
pool: WorkerPool,
Expand Down
18 changes: 18 additions & 0 deletions fw/plat/std/pal/src/drivers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,21 @@ pub mod oic;
pub mod rsa;
pub mod session;
pub mod vault;

/// Reverse-copy `src` into `dst[..src.len()]`.
///
/// Centralizes the big-endian↔little-endian byte reversal shared by the
/// crypto drivers: their wire convention is little-endian (what real PKA
/// hardware emits natively), while the OpenSSL backend produces
/// big-endian components. The reversal is symmetric, so the same helper
/// serves both directions. Trailing pad bytes in `dst` beyond
/// `src.len()` (e.g. the 2-byte pad after a 66-byte P-521 coordinate
/// inside a 68-byte word) are left untouched — callers that need them
/// zeroed should pre-`fill(0)` the full `dst` slot.
pub(crate) fn reverse_copy(dst: &mut [u8], src: &[u8]) {
let len = src.len();
debug_assert!(dst.len() >= len);
for (d, s) in dst[..len].iter_mut().zip(src.iter().rev()) {
*d = *s;
}
}
Loading
Loading