open-standards/sandbox/sandbox-rs/crates/sandbox-tee/src/policy.rs
Transaction Science ba1212361f Add aac, map, sandbox standards; catalog the standards family
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]>
2026-06-07 17:04:36 -04:00

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"));
}
}