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
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
90 changes: 90 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,90 @@
// 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 id lives in the partition's
//! [`RSA_UNWRAPPING_KEY_ID`](crate::part_state::part_unwrapping_key_id)
//! property; this handler simply reads it. RSA key generation is
//! expensive, so it is never done at partition enable — each PAL
//! materialises the key behind the property read instead: the std
//! (emulator) PAL generates it lazily and synchronously on first read,
//! while hardware PALs generate it in the background from partition
//! init and leave the property unset until ready. An absent id
//! therefore means generation is still pending, which this handler
//! surfaces as `PendingKeyGeneration` so the host retries. No public
//! key is cached: it is derived from the vault
//! private key on demand (matching the reference firmware).

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)?;

// Read the partition's RSA-2048 unwrapping key id from its property.
// The PAL materialises the key behind this read: the std PAL
// generates it synchronously on first read (so it never reports the
// id as absent), while hardware PALs generate it in the background
// and leave the property unset until ready. An absent id therefore
// means generation is still pending — surface it as such so the host
// retries.
let key_id = match crate::part_state::part_unwrapping_key_id(pal, io) {
Ok(id) => id,
Err(HsmError::PartPropNotFound) => return Err(HsmError::PendingKeyGeneration),
Err(e) => return Err(e),
};

// 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);
let actual_pub_len = pal.rsa_priv_pub_key(io, priv_key, Some(frame.pub_key.raw))?;
if actual_pub_len != pub_len {
return Err(HsmError::InvalidArg);
}

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
40 changes: 39 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,44 @@ 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.
/// - `Err(HsmError::RsaInvalidKeyLength)` — `pub_out` is too small
/// or the derived public-key components do not fit the wire
/// format.
/// - `Err(HsmError::RsaGenerateError)` — deriving or reading the
/// public key failed for another reason.
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
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