Commit the previously-untracked standard directories and catalog them in the
root README alongside proof:
- aac/ — communication cargo class: portable boards + a ten-level
offline-first cascade (Apache-2.0 code / CC-BY-4.0 spec).
- map/ — geospatial cargo class: range-requestable signed tilesets +
an F0–F4 fidelity cascade (Apache-2.0 code / CC-BY-4.0 spec).
- sandbox/— isolated, metered, attested execution across the heterogeneous
compute fabric (Apache-2.0 code / CC-BY-4.0 spec).
- jouleclaw-otel-authz, jouleclaw-trust — new jouleclaw reference crates.
Root README now lists aac, map, sandbox, and proof in the catalog, the tree,
and the contribution guide.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
241 lines
8.3 KiB
Rust
241 lines
8.3 KiB
Rust
//! TEE report policy and verification (SPEC §7.6).
|
|
//!
|
|
//! A relying party verifies a report in two parts: the **signature** (the report
|
|
//! is genuinely signed by the VCEK it trusts) and the **policy** (the report's
|
|
//! measurement, TCB, debug state, and report-data binding meet requirements).
|
|
//! Both are hardware-independent and fully exercised here; the trust in the VCEK
|
|
//! key is what distinguishes a hardware-rooted report from a software one
|
|
//! (see [`crate::Grounding`]).
|
|
|
|
use crate::report::{signed_body, SnpReport};
|
|
use crate::sig::verify_body;
|
|
use crate::TeeError;
|
|
use p384::ecdsa::VerifyingKey;
|
|
use sandbox_attest::{TeeEvidence, TeeKind};
|
|
|
|
/// The policy a verified report must satisfy.
|
|
#[derive(Clone, Debug)]
|
|
pub struct TeePolicy {
|
|
/// The kinds of TEE accepted (this crate models `SevSnp`).
|
|
pub allowed_kinds: Vec<TeeKind>,
|
|
/// The required launch measurement, if pinned.
|
|
pub expected_measurement: Option<[u8; 48]>,
|
|
/// The minimum acceptable platform TCB.
|
|
pub min_tcb: u64,
|
|
/// Require the guest policy to disable debugging.
|
|
pub require_debug_disabled: bool,
|
|
/// The required report-data binding (e.g. a hash of the run identity), if pinned.
|
|
pub expected_report_data: Option<[u8; 64]>,
|
|
}
|
|
|
|
impl TeePolicy {
|
|
/// A permissive SEV-SNP policy: any measurement, debug must be disabled.
|
|
pub fn sev_snp_baseline() -> Self {
|
|
TeePolicy {
|
|
allowed_kinds: vec![TeeKind::SevSnp],
|
|
expected_measurement: None,
|
|
min_tcb: 0,
|
|
require_debug_disabled: true,
|
|
expected_report_data: None,
|
|
}
|
|
}
|
|
|
|
/// A permissive TDX policy: any MR_TD, TD debug must be disabled.
|
|
pub fn tdx_baseline() -> Self {
|
|
TeePolicy {
|
|
allowed_kinds: vec![TeeKind::Tdx],
|
|
expected_measurement: None,
|
|
min_tcb: 0,
|
|
require_debug_disabled: true,
|
|
expected_report_data: None,
|
|
}
|
|
}
|
|
|
|
/// Pin the launch measurement.
|
|
pub fn with_measurement(mut self, m: [u8; 48]) -> Self {
|
|
self.expected_measurement = Some(m);
|
|
self
|
|
}
|
|
|
|
/// Pin the report-data binding.
|
|
pub fn with_report_data(mut self, d: [u8; 64]) -> Self {
|
|
self.expected_report_data = Some(d);
|
|
self
|
|
}
|
|
}
|
|
|
|
/// A report that has passed signature and policy verification.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct VerifiedReport {
|
|
pub kind: TeeKind,
|
|
pub measurement: [u8; 48],
|
|
pub current_tcb: u64,
|
|
pub report_data: [u8; 64],
|
|
}
|
|
|
|
impl VerifiedReport {
|
|
/// Project to the attestation's [`TeeEvidence`] summary (SPEC §7.1, §7.6).
|
|
pub fn to_evidence(&self) -> TeeEvidence {
|
|
TeeEvidence {
|
|
kind: self.kind,
|
|
measurement_hex: hex::encode(self.measurement),
|
|
tcb: format!("{:#018x}", self.current_tcb),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Verify a SEV-SNP report: signature against `vcek`, then policy. Returns the
|
|
/// verified report on success, or the first violated check.
|
|
pub fn verify_snp(
|
|
report_bytes: &[u8],
|
|
policy: &TeePolicy,
|
|
vcek: &VerifyingKey,
|
|
) -> Result<VerifiedReport, TeeError> {
|
|
if !policy.allowed_kinds.contains(&TeeKind::SevSnp) {
|
|
return Err(TeeError::Policy("SevSnp not an allowed kind".into()));
|
|
}
|
|
let report = SnpReport::parse(report_bytes)?;
|
|
|
|
// 0. Version: this verifier models the v2 field layout (and v3, whose
|
|
// checked fields share those offsets). Reject a version it does not model
|
|
// rather than silently misinterpret fields at the wrong offsets.
|
|
const MIN_MODELED_VERSION: u32 = 2;
|
|
if report.version < MIN_MODELED_VERSION {
|
|
return Err(TeeError::Policy(format!(
|
|
"unsupported SEV-SNP report version {} (verifier models v{MIN_MODELED_VERSION}+)",
|
|
report.version
|
|
)));
|
|
}
|
|
|
|
// 1. Signature over the signed region.
|
|
verify_body(vcek, signed_body(report_bytes)?, &report.signature)?;
|
|
|
|
// 2. Policy.
|
|
if policy.require_debug_disabled && !report.debug_disabled() {
|
|
return Err(TeeError::Policy("guest debug is enabled".into()));
|
|
}
|
|
if let Some(m) = policy.expected_measurement {
|
|
if m != report.measurement {
|
|
return Err(TeeError::Policy("launch measurement mismatch".into()));
|
|
}
|
|
}
|
|
if report.current_tcb < policy.min_tcb {
|
|
return Err(TeeError::Policy("platform TCB below minimum".into()));
|
|
}
|
|
if let Some(rd) = policy.expected_report_data {
|
|
if rd != report.report_data {
|
|
return Err(TeeError::Policy("report-data binding mismatch".into()));
|
|
}
|
|
}
|
|
|
|
Ok(VerifiedReport {
|
|
kind: TeeKind::SevSnp,
|
|
measurement: report.measurement,
|
|
current_tcb: report.current_tcb,
|
|
report_data: report.report_data,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sig::{dev_signing_key, sign_body};
|
|
use crate::report::{signed_body as sb, SnpReport};
|
|
|
|
fn signed_report(measurement: [u8; 48], policy_bits: u64, report_data: [u8; 64]) -> Vec<u8> {
|
|
let sk = dev_signing_key();
|
|
let mut report = SnpReport {
|
|
version: 2,
|
|
guest_svn: 1,
|
|
policy: policy_bits,
|
|
current_tcb: 5,
|
|
report_data,
|
|
measurement,
|
|
host_data: [0; 32],
|
|
chip_id: [0; 64],
|
|
signature: crate::sig::P384Sig {
|
|
r: [0; 48],
|
|
s: [0; 48],
|
|
},
|
|
};
|
|
// Sign the body, then re-embed the signature.
|
|
let bytes0 = report.to_bytes();
|
|
let sig = sign_body(&sk, sb(&bytes0).unwrap());
|
|
report.signature = sig;
|
|
report.to_bytes().to_vec()
|
|
}
|
|
|
|
#[test]
|
|
fn a_valid_report_verifies() {
|
|
let vk = *dev_signing_key().verifying_key();
|
|
let bytes = signed_report([9; 48], 0, [7; 64]);
|
|
let policy = TeePolicy::sev_snp_baseline()
|
|
.with_measurement([9; 48])
|
|
.with_report_data([7; 64]);
|
|
let v = verify_snp(&bytes, &policy, &vk).unwrap();
|
|
assert_eq!(v.kind, TeeKind::SevSnp);
|
|
assert_eq!(v.measurement, [9; 48]);
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_measurement_is_rejected() {
|
|
let vk = *dev_signing_key().verifying_key();
|
|
let bytes = signed_report([9; 48], 0, [7; 64]);
|
|
let policy = TeePolicy::sev_snp_baseline().with_measurement([1; 48]);
|
|
assert!(verify_snp(&bytes, &policy, &vk).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn enabled_debug_is_rejected() {
|
|
let vk = *dev_signing_key().verifying_key();
|
|
let bytes = signed_report([9; 48], crate::report::POLICY_DEBUG, [7; 64]);
|
|
let policy = TeePolicy::sev_snp_baseline();
|
|
assert!(verify_snp(&bytes, &policy, &vk).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn tampered_report_fails_signature() {
|
|
let vk = *dev_signing_key().verifying_key();
|
|
let mut bytes = signed_report([9; 48], 0, [7; 64]);
|
|
bytes[0x090] ^= 1; // flip a measurement bit ⇒ signature breaks
|
|
let policy = TeePolicy::sev_snp_baseline();
|
|
assert!(matches!(
|
|
verify_snp(&bytes, &policy, &vk),
|
|
Err(TeeError::BadSignature)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn evidence_summary_is_produced() {
|
|
let vk = *dev_signing_key().verifying_key();
|
|
let bytes = signed_report([0xAB; 48], 0, [0; 64]);
|
|
let v = verify_snp(&bytes, &TeePolicy::sev_snp_baseline(), &vk).unwrap();
|
|
let ev = v.to_evidence();
|
|
assert_eq!(ev.kind, TeeKind::SevSnp);
|
|
assert!(ev.measurement_hex.starts_with("abab"));
|
|
}
|
|
|
|
#[test]
|
|
fn unmodeled_report_version_is_rejected() {
|
|
// A v1 report (different field layout) must be rejected, not parsed at
|
|
// v2 offsets. Sign the body so the rejection is on version, not sig.
|
|
let sk = dev_signing_key();
|
|
let vk = *sk.verifying_key();
|
|
let mut report = SnpReport {
|
|
version: 1,
|
|
guest_svn: 1,
|
|
policy: 0,
|
|
current_tcb: 5,
|
|
report_data: [0; 64],
|
|
measurement: [9; 48],
|
|
host_data: [0; 32],
|
|
chip_id: [0; 64],
|
|
signature: crate::sig::P384Sig { r: [0; 48], s: [0; 48] },
|
|
};
|
|
let bytes0 = report.to_bytes();
|
|
report.signature = sign_body(&sk, sb(&bytes0).unwrap());
|
|
let bytes = report.to_bytes().to_vec();
|
|
let err = verify_snp(&bytes, &TeePolicy::sev_snp_baseline(), &vk).unwrap_err();
|
|
assert!(format!("{err}").contains("version"));
|
|
}
|
|
}
|