Skip to content
Open
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
16 changes: 16 additions & 0 deletions api/lib/src/algo/sealing/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ impl HsmSecretKey for HsmSealingKey {}

impl HsmDerivationKey for HsmSealingKey {}

impl HsmKeyReportOp for HsmSealingKey {
type Error = HsmError;

/// Attests this non-resident sealing key via TBOR `KeyReport`,
/// routing on its masked-key envelope since there is no device
/// handle to reference.
fn generate_key_report(
&self,
report_data: &[u8],
report: Option<&mut [u8]>,
) -> Result<usize, Self::Error> {
let masked_key = self.masked_key_vec()?;
ddi::masked_key_report(&self.session(), &masked_key, report_data, report)
}
}

#[derive(Default)]
pub struct HsmSealingKeyGenAlgo {}

Expand Down
64 changes: 64 additions & 0 deletions api/lib/src/ddi/descriptor_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Out-of-band SGL descriptor packing shared by TBOR commands that ship
//! DER certificate chains / reports out of band (`PartFinal`, the SD
//! backup family).
//!
//! Each item ships as its own OOB SGL Data Block; the firmware locates it
//! by the descriptor's `index` (its position in the shared `oob` item
//! list) and reads `length` bytes from it.

use azihsm_ddi_tbor_types::*;

use super::*;

/// Appends a certificate chain's DER bytes to `oob`, returning the
/// matching `(index, length)` descriptors. Rejects an empty chain, a
/// chain longer than `max_certs`, an empty (zero-length) cert, or a cert
/// whose length overflows the 16-bit descriptor field.
pub(crate) fn push_cert_chain<'a>(
chain: &'a [HsmCert<'a>],
oob: &mut Vec<&'a [u8]>,
max_certs: usize,
) -> HsmResult<Vec<CertDescriptor>> {
// Firmware evidence verification rejects an empty chain as InvalidArg;
// fail fast rather than round-trip a guaranteed rejection.
if chain.is_empty() || chain.len() > max_certs {
return Err(HsmError::InvalidArgument);
}
let mut descriptors = Vec::with_capacity(chain.len());
for cert in chain {
let der = cert.cert;
if der.is_empty() || der.len() > u16::MAX as usize {
return Err(HsmError::InvalidArgument);
}
// Descriptor index = position in the shared OOB list (must fit u8).
let index = u8::try_from(oob.len()).map_err(|_| HsmError::InvalidArgument)?;
descriptors.push(CertDescriptor {
index,
length: tbor_int::U16::new(der.len() as u16),
});
oob.push(der);
}
Ok(descriptors)
}

/// Appends a COSE_Sign1 report DER to `oob`, returning its descriptor.
/// Rejects an empty report (firmware verification rejects `report_len ==
/// 0` as InvalidArg) or one exceeding the 16-bit length.
pub(crate) fn push_report<'a>(
report: &'a [u8],
oob: &mut Vec<&'a [u8]>,
) -> HsmResult<ReportDescriptor> {
if report.is_empty() || report.len() > u16::MAX as usize {
return Err(HsmError::InvalidArgument);
}
let index = u8::try_from(oob.len()).map_err(|_| HsmError::InvalidArgument)?;
let descriptor = ReportDescriptor {
index,
length: tbor_int::U16::new(report.len() as u16),
};
oob.push(report);
Ok(descriptor)
}
24 changes: 24 additions & 0 deletions api/lib/src/ddi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod aes;
mod aes_xts_key;
mod descriptor_utils;
mod dev;
mod ecc;
mod hkdf;
Expand All @@ -13,6 +14,9 @@ mod masked_key;
mod partition;
mod partition_ex;
mod rsa;
mod sd_create_remote_backup;
mod sd_evidence;
mod sd_reseal_remote_backup;
mod sd_sealing_key_gen;
mod session;
mod session_ex;
Expand All @@ -23,14 +27,24 @@ pub(crate) use aes_xts_key::*;
use azihsm_ddi::*;
use azihsm_ddi_mbor_codec::*;
use azihsm_ddi_mbor_types::*;
/// Maximum number of certificates in one SD-evidence certificate chain.
pub use azihsm_ddi_tbor_types::EVIDENCE_CHAIN_MAX_CERTS;
/// Size, in bytes, of the `part_final` `local_mk_backup` envelope.
pub use azihsm_ddi_tbor_types::LOCAL_MK_BACKUP_LEN;
/// Exact length of the local (masked) security-domain backup.
pub use azihsm_ddi_tbor_types::MASKED_SD_LEN;
/// Maximum number of certificates in a `part_final` PTA chain.
pub use azihsm_ddi_tbor_types::MAX_CERTS;
/// Exact length of the remote partition-owner-key backup (`SdCreate`/`SdReseal`).
pub use azihsm_ddi_tbor_types::POK_REMOTE_BACKUP_LEN;
/// Maximum size, in bytes, of the `part_init` `pta_csr` buffer.
pub use azihsm_ddi_tbor_types::PTA_CSR_MAX_LEN;
/// Maximum size, in bytes, of the `part_init` `pta_report` buffer.
pub use azihsm_ddi_tbor_types::PTA_REPORT_MAX_LEN;
/// Exact length of the security-domain masking-key backup envelope.
pub use azihsm_ddi_tbor_types::SD_MK_BACKUP_LEN;
use azihsm_ddi_tbor_types::TborStatus;
pub(crate) use descriptor_utils::*;
pub(crate) use dev::*;
pub(crate) use ecc::*;
pub(crate) use hkdf::*;
Expand All @@ -41,6 +55,9 @@ pub(crate) use masked_key::*;
pub(crate) use partition::*;
pub(crate) use partition_ex::*;
pub(crate) use rsa::*;
pub(crate) use sd_create_remote_backup::*;
pub(crate) use sd_evidence::*;
pub(crate) use sd_reseal_remote_backup::*;
pub(crate) use sd_sealing_key_gen::*;
pub(crate) use session::*;
pub(crate) use session_ex::*;
Expand Down Expand Up @@ -103,6 +120,13 @@ impl From<DdiError> for HsmError {
DdiError::DdiStatus(DdiStatus::CannotDeleteInternalKeys) => {
HsmError::CannotDeleteInternalKeys
}
DdiError::TborStatus(TborStatus::SdAlreadyInitialized) => {
HsmError::SdAlreadyInitialized
}
Comment thread
rajesh-gali marked this conversation as resolved.
// Map the firmware's contract-level `InvalidArg` to the same
// `InvalidArgument` the host guards return, so callers see a
// consistent argument-rejection error across transports.
DdiError::TborStatus(TborStatus::InvalidArg) => HsmError::InvalidArgument,
_ => {
tracing::error!(?err, hsm_error = ?HsmError::DdiCmdFailure, "Unmapped DDI error");
HsmError::DdiCmdFailure
Expand Down
67 changes: 44 additions & 23 deletions api/lib/src/ddi/partition_ex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,37 +234,16 @@ pub(crate) fn part_final_ex(
if part_policy.len() != PART_POLICY_LEN {
return Err(HsmError::InvalidArgument);
}
if pta_cert_chain.is_empty() || pta_cert_chain.len() > MAX_CERTS {
return Err(HsmError::InvalidArgument);
}
// The firmware treats a non-empty `prev_local_mk_backup` as a
// fixed-size envelope of exactly `LOCAL_MK_BACKUP_LEN` bytes, so
// reject any other present length up front (deterministic guard).
if prev_local_mk_backup.is_some_and(|b| b.len() != LOCAL_MK_BACKUP_LEN) {
return Err(HsmError::InvalidArgument);
}

// Each DER cert ships as its own out-of-band SGL Data Block; the
// firmware locates each one by the descriptor's `index` (its position
// in the OOB item list) and reads `length` bytes from it.
// Each DER cert ships as its own out-of-band SGL Data Block.
let mut oob: Vec<&[u8]> = Vec::with_capacity(pta_cert_chain.len());
let mut cert_descriptors = Vec::with_capacity(pta_cert_chain.len());
for (i, desc) in pta_cert_chain.iter().enumerate() {
let cert = desc.cert;
let length = cert.len();
// An empty cert is not valid DER and would yield a zero-length
// descriptor; reject it up front alongside the other
// deterministic host-side guards.
if length == 0 || length > u16::MAX as usize {
return Err(HsmError::InvalidArgument);
}
// `i` is bounded by the `MAX_CERTS` check above, so it fits `u8`.
cert_descriptors.push(CertDescriptor {
index: i as u8,
length: tbor_int::U16::new(length as u16),
});
oob.push(cert);
}
let cert_descriptors = push_cert_chain(pta_cert_chain, &mut oob, MAX_CERTS)?;

let mut req = TborPartFinalReq {
session_id,
Expand Down Expand Up @@ -293,6 +272,48 @@ pub(crate) fn part_final_ex(
Ok(HsmPartFinalExResult::from(resp))
}

/// Partition identity returned by a `PartInfo` query: the stable PID plus
/// the raw ECC-P384 identity public key.
///
/// DDI-internal carrier only. Public callers reach these fields through
/// the [`HsmPartition::pid`] / [`HsmPartition::ex_pub_key`] getters, so the
/// compound type never surfaces in the public API.
pub(crate) struct HsmPartInfo {
/// 16-byte partition identity (PID).
pub pid: Vec<u8>,
/// Raw ECC-P384 identity public-key coordinates (`x ‖ y`, 96 B).
pub pid_pub_key: Vec<u8>,
}

/// Converts the DDI/wire `PartInfo` response into the DDI-internal
/// [`HsmPartInfo`] with owned bytes, keeping the wire response type
/// confined to the DDI layer.
impl From<TborPartInfoResp> for HsmPartInfo {
fn from(resp: TborPartInfoResp) -> Self {
Self {
pid: resp.pid.to_vec(),
pid_pub_key: resp.pid_pub_key.to_vec(),
}
}
}

/// Issue `PartInfo` on the partition, returning its identity (PID) and
/// raw identity public key.
///
/// This is a partition-level query and carries no session id.
///
/// # Errors
///
/// Surfaces DDI/device failures from the round-trip.
pub(crate) fn part_info(partition: &HsmPartition) -> HsmResult<HsmPartInfo> {
let inner = partition.inner().read();
let dev = inner.dev();
let mut cookie = None;
dev.exec_op_tbor(&TborPartInfoReq::new(), None, &mut cookie)
.map(HsmPartInfo::from)
.map_err(HsmError::from)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
87 changes: 87 additions & 0 deletions api/lib/src/ddi/sd_create_remote_backup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! `SdCreateRemoteBackup` (opcode `0x0A`) over the TBOR transport at the
//! DDI layer.
//!
//! Creates a new security domain from a caller-supplied unified
//! `PartPolicy`, returning the remote partition-owner-key backup together
//! with the device-local partition-owner-key and security-domain
//! masking-key backups. Runs **inside an already-open session**; the
//! request carries the active session id, which the firmware dispatcher
//! cross-checks against the SQE-carried session id. Attestation evidence
//! travels out of band (see [`super::sd_evidence`]).

use azihsm_ddi_tbor_types::*;

use super::*;

/// Converts the wire `SdCreateRemoteBackup` response into the owned
/// API-layer [`HsmSdRemoteBackupResult`].
impl From<TborSdCreateRemoteBackupResp> for HsmSdRemoteBackupResult {
fn from(resp: TborSdCreateRemoteBackupResp) -> Self {
Self {
pok_remote_backup: resp.pok_remote_backup.to_vec(),
pok_local_backup: resp.pok_local_backup.to_vec(),
sd_mk_backup: resp.sd_mk_backup.to_vec(),
}
}
}

/// Issue `SdCreateRemoteBackup` (opcode `0x0A`) on the active session.
///
/// Creates a new security domain from the caller-supplied unified
/// `policy`, using the sender's masked sealing key and the receiver's
/// attestation evidence, and returns the remote backup together with the
/// device-local backups.
///
/// # Arguments
///
/// * `partition` - The HSM partition handle.
/// * `session_id` - The active session id this request binds to.
/// * `masked_sealing_key` - The sender's masked SD-sealing key (from
/// `SdSealingKeyGen`), exactly [`MASKED_SEALING_KEY_LEN`] bytes.
/// * `receiver_evidence` - Receiver attestation evidence (cert chains and
/// report), transmitted out of band.
/// * `policy` - Unified [`PartPolicy`] image ([`PART_POLICY_LEN`] bytes).
///
/// # Errors
///
/// Returns [`HsmError::InvalidArgument`] for a wrong-length
/// `masked_sealing_key` or a malformed `policy`, and surfaces DDI/device
/// failures from the round-trip.
pub(crate) fn sd_create_remote_backup_ex(
partition: &HsmPartition,
session_id: u16,
masked_sealing_key: &[u8],
receiver_evidence: &HsmSdEvidence<'_>,
policy: &[u8],
) -> HsmResult<HsmSdRemoteBackupResult> {
// Exact-length array conversion enforces MASKED_SEALING_KEY_LEN.
let masked_sealing_key: [u8; MASKED_SEALING_KEY_LEN] = masked_sealing_key
.try_into()
.map_err(|_| HsmError::InvalidArgument)?;
let policy = decode_policy(policy)?;

// Flatten the receiver evidence into descriptors + shared OOB items.
let mut oob: Vec<&[u8]> = Vec::new();
let receiver = push_evidence(receiver_evidence, &mut oob)?;

let req = TborSdCreateRemoteBackupReq {
session_id,
masked_sealing_key,
receiver_mfgr_cert_chain: receiver.mfgr,
receiver_owner_cert_chain: receiver.owner,
receiver_part_owner_cert_chain: receiver.part_owner,
receiver_report: receiver.report,
policy,
};

let inner = partition.inner().read();
let dev = inner.dev();
let mut cookie = None;
let oob_items = (!oob.is_empty()).then_some(oob.as_slice());
dev.exec_op_tbor(&req, oob_items, &mut cookie)
.map(HsmSdRemoteBackupResult::from)
.map_err(HsmError::from)
}
58 changes: 58 additions & 0 deletions api/lib/src/ddi/sd_evidence.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Shared helpers for the security-domain backup commands
//! (`SdCreateRemoteBackup`, `SdResealRemoteBackup`).
//!
//! # Out-of-band evidence
//!
//! Bulk attestation evidence (DER cert chains and a COSE_Sign1 report)
//! travels out of band: each [`HsmSdEvidence`] is flattened into
//! `(index, length)` descriptors (via [`super::descriptor_utils`]) and its
//! DER bytes
//! are shipped as `oob_items`, mirroring the `PartFinal` cert-chain
//! transport.

use azihsm_ddi_tbor_types::*;

use super::*;

/// Decodes a caller-supplied unified `PartPolicy` image
/// ([`PART_POLICY_LEN`] bytes), failing fast with
/// [`HsmError::InvalidArgument`] on a wrong length or malformed image.
pub(crate) fn decode_policy(policy: &[u8]) -> HsmResult<PartPolicy> {
if policy.len() != PART_POLICY_LEN {
return Err(HsmError::InvalidArgument);
}
<PartPolicy as zerocopy::TryFromBytes>::try_read_from_bytes(policy)
.map_err(|_| HsmError::InvalidArgument)
}

/// Wire descriptors for one attestation party: the three cert-chain
/// lists plus the report descriptor. Produced by [`push_evidence`].
pub(crate) struct EvidenceDescriptors {
pub(crate) mfgr: Vec<CertDescriptor>,
pub(crate) owner: Vec<CertDescriptor>,
pub(crate) part_owner: Vec<CertDescriptor>,
pub(crate) report: ReportDescriptor,
}

/// Flattens one [`HsmSdEvidence`] party into its wire descriptors,
/// appending all referenced DER bytes (the three cert chains, then the
/// report) to the shared `oob` list so their descriptor indices are
/// contiguous.
pub(crate) fn push_evidence<'a>(
evidence: &HsmSdEvidence<'a>,
oob: &mut Vec<&'a [u8]>,
) -> HsmResult<EvidenceDescriptors> {
Ok(EvidenceDescriptors {
mfgr: push_cert_chain(evidence.mfgr_cert_chain, oob, EVIDENCE_CHAIN_MAX_CERTS)?,
owner: push_cert_chain(evidence.owner_cert_chain, oob, EVIDENCE_CHAIN_MAX_CERTS)?,
part_owner: push_cert_chain(
evidence.part_owner_cert_chain,
oob,
EVIDENCE_CHAIN_MAX_CERTS,
)?,
report: push_report(evidence.report, oob)?,
})
}
Loading
Loading