Skip to content
Open
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
292 changes: 288 additions & 4 deletions ddi/tbor/types/tests/commands/get_cert_chain_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,21 @@
//! path (same underlying cert store), and confirm an invalid slot is
//! rejected.

use azihsm_ddi_tbor_types::codec::DecodeError;
use azihsm_ddi_tbor_types::codec::ResponseEncoder;
use azihsm_ddi_tbor_types::codec::MAX_TOC_ENTRIES;
use azihsm_ddi_tbor_types::codec::PROTOCOL_VERSION;
use azihsm_ddi_tbor_types::SessionType;
use azihsm_ddi_tbor_types::TborGetCertChainInfoReq;
use azihsm_ddi_tbor_types::TborGetCertChainInfoResp;
use azihsm_ddi_tbor_types::TborGetCertReq;
use azihsm_ddi_tbor_types::TborResp;
use azihsm_ddi_tbor_types::TborStatus;
use azihsm_ddi_tbor_types::CERT_THUMBPRINT_LEN;

use crate::harness::TestCtx;

const CO: u8 = 0;
#[test]
fn round_trip() {
let ctx = TestCtx::new();
Expand Down Expand Up @@ -79,10 +88,285 @@ fn matches_mbor_path() {
);
}

/// The std/emu PAL only provisions chain slot 0; any other slot is
/// rejected with `InvalidArg`.
/// The reported count defines the complete set of valid certificate
/// indices for the same slot: every advertised index is readable and the
/// first index beyond the count is rejected.
#[test]
fn invalid_slot_rejected() {
fn reported_count_defines_certificate_bounds() {
let ctx = TestCtx::new();
ctx.expect_fw_reject(&TborGetCertChainInfoReq::new(1), TborStatus::InvalidArg);
let info = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo");

assert!(info.num_certs > 0, "chain must be non-empty");
for cert_id in 0..info.num_certs {
let cert = ctx
.tbor(&TborGetCertReq::new(0, cert_id))
.unwrap_or_else(|e| panic!("advertised certificate {cert_id} is unreadable: {e:?}"));
assert!(
!cert.certificate.is_empty(),
"advertised certificate {cert_id} must contain DER bytes",
);
}

ctx.expect_fw_reject(
&TborGetCertReq::new(0, info.num_certs),
TborStatus::InvalidArg,
);
}

/// Reading certificates from the chain must not mutate the information
/// subsequently returned by `GetCertChainInfo`.
#[test]
fn certificate_reads_do_not_change_chain_info() {
let ctx = TestCtx::new();

let before = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo before certificate reads");

for cert_id in 0..before.num_certs {
ctx.tbor(&TborGetCertReq::new(0, cert_id))
.unwrap_or_else(|e| panic!("failed to read certificate {cert_id}: {e:?}"));
}

let after = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo after certificate reads");

assert_eq!(
before, after,
"certificate reads must not mutate chain metadata",
);
}

/// Every advertised certificate is stable across repeated reads.
#[test]
fn certificates_are_stable_across_reads() {
let ctx = TestCtx::new();
let info = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo");

assert!(info.num_certs > 0, "chain must be non-empty");

for cert_id in 0..info.num_certs {
let first = ctx
.tbor(&TborGetCertReq::new(0, cert_id))
.unwrap_or_else(|e| panic!("first read of certificate {cert_id} failed: {e:?}"));

let second = ctx
.tbor(&TborGetCertReq::new(0, cert_id))
.unwrap_or_else(|e| panic!("second read of certificate {cert_id} failed: {e:?}"));

assert_eq!(
first.certificate, second.certificate,
"certificate {cert_id} must be stable across reads",
);
}
}

/// Distinct indices in a multi-certificate chain must not alias the same
/// certificate bytes.
#[test]
fn certificate_indices_do_not_alias() {
let ctx = TestCtx::new();
let info = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo");

let mut certificates = Vec::new();

for cert_id in 0..info.num_certs {
let cert = ctx
.tbor(&TborGetCertReq::new(0, cert_id))
.unwrap_or_else(|e| panic!("failed to read certificate {cert_id}: {e:?}"));

for (previous_id, previous) in certificates.iter().enumerate() {
assert_ne!(
&cert.certificate, previous,
"certificate {cert_id} aliases certificate {previous_id}",
);
}

certificates.push(cert.certificate);
}
}

/// A rejected request for another slot must not disturb the valid
/// provisioned chain in slot 0.
#[cfg(feature = "emu")]
#[test]
fn invalid_slot_does_not_affect_valid_slot() {
let ctx = TestCtx::new();

let before = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo before invalid request");

ctx.expect_fw_reject(
&TborGetCertChainInfoReq::new(u8::MAX),
TborStatus::InvalidArg,
);

let after = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo after invalid request");

assert_eq!(
before, after,
"invalid slot request must not affect the valid certificate chain",
);
}

/// `GetCertChainInfo` remains stable across unrelated session activity.
#[test]
fn stable_across_session_activity() {
let ctx = TestCtx::new();

let before = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo before session activity");

let session = ctx
.open_session(CO, SessionType::Authenticated)
.expect("open CO authenticated session");

session.close().expect("close CO authenticated session");

let after = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo after session activity");

assert_eq!(
before, after,
"session activity must not change certificate-chain info",
);
}

/// `GetCertChainInfo` remains callable while an unrelated session is active.
#[test]
fn callable_while_session_active() {
let ctx = TestCtx::new();

let before = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo before opening session");

let session = ctx
.open_session(CO, SessionType::Authenticated)
.expect("open CO authenticated session");

let during = ctx
.tbor(&TborGetCertChainInfoReq::new(0))
.expect("GetCertChainInfo while session is active");

assert_eq!(
before, during,
"active session must not affect out-of-session certificate-chain info",
);

session.close().expect("close CO authenticated session");
}

/// Representative unsupported slot IDs are rejected with `InvalidArg`.
#[cfg(feature = "emu")]
#[test]
fn unsupported_slot_boundaries_rejected() {
let ctx = TestCtx::new();

for slot_id in [1, 2, 127, 254, u8::MAX] {
ctx.expect_fw_reject(
&TborGetCertChainInfoReq::new(slot_id),
TborStatus::InvalidArg,
);
}
}

/// A response missing the thumbprint TOC entry is rejected as truncated.
#[test]
fn truncated_response_rejected() {
let mut buf = [0u8; 128];

// GetCertChainInfo expects:
// 1. num_certs
// 2. thumbprint
// Encode only num_certs.
let bytes = ResponseEncoder::new(&mut buf, PROTOCOL_VERSION, 0, false)
.uint8(4)
.expect("encode num_certs")
.finish()
.expect("finish truncated response");

let err = TborGetCertChainInfoResp::decode_response(bytes)
.expect_err("missing thumbprint must be rejected");

assert_eq!(err, DecodeError::MessageTruncated);
}

/// A response with the maximum supported TOC count still decodes the known prefix.
#[test]
fn max_toc_response_decodes_known_fields() {
let mut buf = [0u8; 512];
let thumbprint = [0xA5u8; CERT_THUMBPRINT_LEN];

let mut encoder = ResponseEncoder::new(&mut buf, PROTOCOL_VERSION, 0, false)
.uint8(4)
.expect("encode num_certs")
.buffer(&thumbprint)
.expect("encode thumbprint");

// Fill the remaining TOC slots with unknown future fields.
for _ in 2..MAX_TOC_ENTRIES {
encoder = encoder.uint8(0xFF).expect("encode trailing TOC entry");
}

let bytes = encoder.finish().expect("finish max-TOC response");

let resp = TborGetCertChainInfoResp::decode_response(bytes)
.expect("maximum TOC response must decode known prefix");

assert_eq!(resp.num_certs, 4);
assert_eq!(resp.thumbprint, thumbprint);
}

/// A response with the wrong TOC type for `num_certs` is rejected.
#[test]
fn wrong_num_certs_type_rejected() {
let mut buf = [0u8; 512];
let thumbprint = [0xA5u8; CERT_THUMBPRINT_LEN];

// num_certs expects Uint8; deliberately encode it as Uint16.
let bytes = ResponseEncoder::new(&mut buf, PROTOCOL_VERSION, 0, false)
.uint16(4)
.expect("encode wrong num_certs type")
.buffer(&thumbprint)
.expect("encode thumbprint")
.finish()
.expect("finish response");

let err = TborGetCertChainInfoResp::decode_response(bytes)
.expect_err("wrong num_certs TOC type must be rejected");

assert_eq!(err, DecodeError::UnexpectedTocType);
}

/// A response with the wrong TOC type for the thumbprint is rejected.
#[test]
fn wrong_thumbprint_type_rejected() {
let mut buf = [0u8; 128];

// thumbprint expects Buffer; deliberately encode it as Uint8.
let bytes = ResponseEncoder::new(&mut buf, PROTOCOL_VERSION, 0, false)
.uint8(4)
.expect("encode num_certs")
.uint8(0xA5)
.expect("encode wrong thumbprint type")
.finish()
.expect("finish response");

let err = TborGetCertChainInfoResp::decode_response(bytes)
.expect_err("wrong thumbprint TOC type must be rejected");

assert_eq!(err, DecodeError::UnexpectedTocType);
}
Loading