underhill_attestation/runtime_sealing/tcb_floor.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Conservative comparison of trusted local report snapshots, never VMGS data.
5//!
6//! # Reading the flow
7//!
8//! An **observation** is the SVN and comparison domain extracted from one trusted
9//! local report. A **floor** is the last accepted observation: later runtime
10//! operations must meet or exceed every ordered component of that floor.
11//! **Ratchet** means replacing the floor with an accepted observation, never
12//! rolling it back if subsequent sealing or persistence fails.
13//!
14//! There are three entry paths:
15//! - **Boot:** `BootTcbFloor::observe` consumes an already acquired report (no
16//! hardware I/O). It initializes or advances the prospective floor. A bad
17//! observation permanently disables export for that boot, without failing boot
18//! unsealing. The caller also requires successful boot sealing before enrollment.
19//! - **Runtime:** `observe_and_ratchet` calls `Snapshot::observe` to fetch a fresh
20//! report, then calls `check_successor` to validate it against the floor. Only
21//! after that check succeeds does it replace the floor and return the accepted
22//! SVN for sealing or candidate verification. A rejected observation leaves the
23//! existing floor intact so a later attempt can retry.
24//! - **Servicing:** `RuntimeTcbFloor::restore` validates an accepted floor carried
25//! by an authenticated, VM-bound, fresh servicing channel, without hardware I/O.
26//! Replacing a resident floor additionally requires `check_restored_successor`.
27//!
28//! `check_successor` is a pure compatibility/minimum check: it neither fetches a
29//! report nor changes state. "Successor" includes an equal TCB, not only an
30//! upgrade. It does not authenticate a protector or appraise vendor TCB status.
31//!
32//! SNP layouts follow `TCB_VERSION` and `ATTESTATION_REPORT` in the
33//! [SEV-SNP Firmware ABI specification (AMD 56860)](https://docs.amd.com/v/u/en-US/56860_PUB_SEV_SNP).
34//! Raw packed integer ordering is not a component-wise security ordering, and
35//! Turin moves components relative to Milan/Genoa.
36
37pub(super) mod saved_state;
38
39use self::saved_state::SavedRuntimeTcbFloor;
40use self::saved_state::SavedRuntimeTcbFloorSvn;
41use super::Error;
42use super::ErrorInner;
43use super::create_protector_with_svn;
44use super::protector_matches;
45use super::sealing_context;
46use super::svn_matches_tee;
47use super::validated_policy;
48use crate::vmgs::parse_hardware_key_protector;
49use cvm_tracing::CVM_ALLOWED;
50use openhcl_attestation_protocol::igvm_attest::get::runtime_claims::AttestationVmConfig;
51use tee_call::GetAttestationReportResult;
52use tee_call::KeyDerivationSvn;
53use tee_call::REPORT_DATA_SIZE;
54use tee_call::TeeCall;
55use x86defs::snp::SNP_CPUID_FAMILY_MILAN_GENOA;
56use x86defs::snp::SNP_CPUID_FAMILY_TURIN;
57use x86defs::snp::SNP_CPUID_MODELS_MILAN_GENOA;
58use x86defs::snp::SNP_CPUID_MODELS_TURIN_90_AF;
59use x86defs::snp::SNP_CPUID_MODELS_TURIN_C0_CF;
60use x86defs::snp::SnpReport;
61use x86defs::snp::SnpTcbVersionLegacy;
62use x86defs::snp::SnpTcbVersionTurin;
63use x86defs::tdx::TeeTcbSvn;
64use zerocopy::FromBytes;
65
66// OpenHCL policy, not an AMD CPU-generation mapping: only report versions
67// 3 through 5 currently support component ordering here. V2 lacks CPUID fields;
68// v6 adds extended TCB semantics this floor does not model. Other versions
69// remain equality-only, even when the CPU model has a known TCB layout.
70const SNP_REPORT_VERSIONS_WITH_COMPONENT_ORDERING: core::ops::RangeInclusive<u32> = 3..=5;
71
72/// A resident TCB floor originating exclusively from trusted local hardware.
73///
74/// Obtain once from platform initialization (reusing boot reports), or use
75/// [`Self::new`] **before accepting worker events**, then
76/// retain the same instance across attempts and errors. Blocking hardware jobs
77/// must hold the caller's shared mutex for each complete method call. Never
78/// recreate the floor on retry or initialize it from a VMGS protector header.
79/// Trusted servicing may transfer it with [`Self::save`] and [`Self::restore`].
80/// The caller must provide an authenticated, VM-bound, fresh servicing channel;
81/// the DTO is not proof of a hardware observation. Never deserialize a floor
82/// from VMGS or host-controlled metadata. This is not a persisted anti-rollback
83/// counter and does not protect an arbitrary restart from rollback. It has no
84/// `Clone`, `Copy`, or `Default` implementation; restore validates the typed DTO
85/// without observing hardware or bootstrapping a replacement floor.
86///
87/// SNP ordering is component-wise only for report versions 3, 4, and 5 within
88/// the exact same version/family/model. Milan/Genoa (family 0x19, models
89/// 0x00..=0x1f) and Turin (family 0x1a, models 0x90..=0xaf and 0xc0..=0xcf)
90/// use distinct layouts;
91/// reserved TCB bytes must remain equal. All other SNP domains require exact
92/// SVN equality. Version 2 has no trusted CPUID and is also equality-only.
93/// TDX uses a structural minimum-TCB policy: CPU SVN bytes must individually
94/// meet the floor. TEE SVN byte 1 must match, including legacy identity 0.
95/// For TD-preserving modules byte 0 is module ISVSVN and bytes 2..16 are platform
96/// components; these bytes must individually meet the floor in both layouts.
97/// This is not quote appraisal
98/// or an Intel TCB status decision, which requires authenticated TCB Info.
99#[derive(Debug)]
100pub struct RuntimeTcbFloor {
101 snapshot: Snapshot,
102}
103
104/// Collect a runtime floor from reports already obtained during boot. No I/O.
105///
106/// Keep one collector across all unlock retries, including failed SKR attempts.
107/// Only comparable, nondecreasing observations ratchet the accepted floor.
108/// ANY malformed, incompatible, or lowered observation permanently disables
109/// export for this boot, even if a later report is good. This is deliberately
110/// stricter than runtime retry handling: boot must never export an ambiguous
111/// initial floor or silently bootstrap later after a worker event.
112///
113/// Collection never affects boot unlocking, including cached lower-SVN unseal
114/// policies, SKR fallback, and retry/skip-hardware-unsealing decisions.
115/// A valid collection alone does not authorize runtime enrollment: the caller
116/// must also require successful sealing and persistence of the current active
117/// DEK in the successful unlock attempt.
118pub(crate) struct BootTcbFloor {
119 enabled: bool,
120 floor: Option<RuntimeTcbFloor>,
121 invalid: bool,
122}
123
124impl BootTcbFloor {
125 pub(crate) fn new(tee: Option<&dyn TeeCall>, config: &AttestationVmConfig) -> Self {
126 Self {
127 enabled: tee.is_some_and(|tee| sealing_context(tee, config).is_ok()),
128 floor: None,
129 invalid: false,
130 }
131 }
132
133 /// Record an existing boot report; this method makes no hardware calls.
134 ///
135 /// The first valid observation initializes the prospective floor. Subsequent
136 /// ones must pass `check_successor` before replacing it. A parsing or policy
137 /// error latches `invalid`: later reports cannot re-enable export in this
138 /// boot. Disabled or already-invalid collectors ignore further observations.
139 /// This deliberately returns no error to the boot-unlock flow.
140 ///
141 /// Only pass the result directly from the trusted local `TeeCall`, before
142 /// host callouts or key unwrap can fail. Never pass host or VMGS report bytes.
143 pub(crate) fn observe(&mut self, tee: &dyn TeeCall, report: &GetAttestationReportResult) {
144 if !self.enabled || self.invalid {
145 return;
146 }
147 let observed = Snapshot::from_report(tee, report).and_then(|observed| {
148 if let Some(floor) = &self.floor {
149 floor.snapshot.check_successor(&observed)?;
150 }
151 Ok(observed)
152 });
153 match observed {
154 Ok(snapshot) => self.floor = Some(RuntimeTcbFloor { snapshot }),
155 Err(err) => {
156 self.invalid = true;
157 tracelimit::warn_ratelimited!(
158 CVM_ALLOWED,
159 error = &err as &dyn std::error::Error,
160 "Boot report cannot establish runtime TCB floor; runtime hardware resealing disabled"
161 );
162 }
163 }
164 }
165
166 /// Transfer the collected floor if at least one valid report was observed
167 /// and collection was never invalidated. This alone does not prove sealing
168 /// succeeded; the boot caller separately gates runtime enrollment on that.
169 pub(crate) fn finish(self) -> Option<RuntimeTcbFloor> {
170 if self.enabled && !self.invalid {
171 self.floor
172 } else {
173 None
174 }
175 }
176}
177
178/// Compact metadata from one local report, not a persisted VMGS snapshot.
179/// Used both for a new observation and for the currently accepted floor.
180#[derive(Debug)]
181struct Snapshot {
182 svn: KeyDerivationSvn,
183 snp_domain: Option<SnpDomain>,
184}
185
186/// The interpretation domain of an SNP report's raw TCB bytes.
187///
188/// SVN bytes alone cannot tell us which security components they describe.
189/// AMD 56860 rev. 1.59, section 2.3, tables 4/5 define different TCB_VERSION
190/// layouts for pre-Turin and Turin CPUs: e.g. byte 0 is bootloader on the
191/// former but FMC on the latter. CPU family/model selects that layout, not
192/// report version. The report version separately gates the semantics we support
193/// (including CPUID availability); future reports can add unmodeled TCB fields.
194/// See the [AMD firmware ABI specification].
195///
196/// Require exact domain equality before even accepting equal raw SVNs. This
197/// avoids comparing unrelated components across CPUs or report formats. It is
198/// deliberately stricter than merely having the same layout: no cross-domain
199/// ordering is defined here. Unknown domains allow unchanged SVNs only; their
200/// metadata must not be discarded or interpreted using a guessed layout.
201///
202/// [AMD firmware ABI specification]: https://docs.amd.com/v/u/en-US/56860_PUB_SEV_SNP
203#[derive(Debug, PartialEq, Eq)]
204struct SnpDomain {
205 version: u32,
206 // Unknown v3+ layouts retain these bytes only as opaque discriminators;
207 // they never authorize component ordering. V2 does not have CPUID fields.
208 cpuid: Option<[u8; 2]>,
209}
210
211#[derive(Debug, PartialEq, Eq)]
212enum SnpTcbLayout {
213 Legacy,
214 Turin,
215}
216
217impl SnpDomain {
218 /// Select a supported layout, separately from comparing its components.
219 /// Family alone is insufficient: each family includes other CPU models.
220 fn tcb_layout(&self) -> Option<SnpTcbLayout> {
221 if !SNP_REPORT_VERSIONS_WITH_COMPONENT_ORDERING.contains(&self.version) {
222 return None;
223 }
224 let [family, model] = self.cpuid?;
225 match family {
226 SNP_CPUID_FAMILY_MILAN_GENOA if SNP_CPUID_MODELS_MILAN_GENOA.contains(&model) => {
227 Some(SnpTcbLayout::Legacy)
228 }
229 SNP_CPUID_FAMILY_TURIN
230 if SNP_CPUID_MODELS_TURIN_90_AF.contains(&model)
231 || SNP_CPUID_MODELS_TURIN_C0_CF.contains(&model) =>
232 {
233 Some(SnpTcbLayout::Turin)
234 }
235 _ => None,
236 }
237 }
238}
239
240impl RuntimeTcbFloor {
241 /// Save only accepted SVN/domain metadata, never secrets or derived keys.
242 /// The result is only for an authenticated, VM-bound, fresh servicing channel.
243 pub fn save(&self) -> SavedRuntimeTcbFloor {
244 let svn = match self.snapshot.svn {
245 KeyDerivationSvn::Snp { tcb_version } => {
246 let domain = self
247 .snapshot
248 .snp_domain
249 .as_ref()
250 .expect("SNP floor has a comparison domain");
251 SavedRuntimeTcbFloorSvn::Snp {
252 tcb_version,
253 report_version: domain.version,
254 cpuid: domain.cpuid.map(|cpuid| cpuid.to_vec()),
255 }
256 }
257 KeyDerivationSvn::Tdx {
258 tee_tcb_svn,
259 cpu_svn,
260 } => SavedRuntimeTcbFloorSvn::Tdx {
261 tee_tcb_svn: tee_tcb_svn.to_vec(),
262 cpu_svn: cpu_svn.to_vec(),
263 },
264 };
265 SavedRuntimeTcbFloor {
266 version: 1,
267 svn: Some(svn),
268 }
269 }
270
271 /// Validate state from an authenticated, VM-bound, fresh servicing channel.
272 /// The caller establishes that trust: the DTO itself is not a proof, and must
273 /// never come from VMGS, protector headers, or other host-controlled metadata.
274 /// No report, key derivation, VMGS access, or bootstrap occurs here. Only the
275 /// configured policy and TEE capabilities are inspected, not the live TCB.
276 /// Unknown SNP domains retain their existing equality-only interpretation.
277 ///
278 /// When replacing an existing resident floor, first call its
279 /// [`Self::check_restored_successor`] while holding the shared state lock.
280 /// Do not replace the resident floor if either validation step fails.
281 pub fn restore(
282 state: SavedRuntimeTcbFloor,
283 tee: &dyn TeeCall,
284 config: &AttestationVmConfig,
285 ) -> Result<Self, Error> {
286 sealing_context(tee, config)?;
287 let invalid = || Error(ErrorInner::InvalidSavedTcbFloor);
288 if state.version != 1 {
289 return Err(invalid());
290 }
291 let (svn, snp_domain) = match state.svn.ok_or_else(invalid)? {
292 SavedRuntimeTcbFloorSvn::Snp {
293 tcb_version,
294 report_version,
295 cpuid,
296 } => {
297 let cpuid = match (report_version >= 3, cpuid) {
298 (false, None) => None,
299 (true, Some(cpuid)) => Some(cpuid.try_into().map_err(|_| invalid())?),
300 _ => return Err(invalid()),
301 };
302 (
303 KeyDerivationSvn::Snp { tcb_version },
304 Some(SnpDomain {
305 version: report_version,
306 cpuid,
307 }),
308 )
309 }
310 SavedRuntimeTcbFloorSvn::Tdx {
311 tee_tcb_svn,
312 cpu_svn,
313 } => (
314 KeyDerivationSvn::Tdx {
315 tee_tcb_svn: tee_tcb_svn.try_into().map_err(|_| invalid())?,
316 cpu_svn: cpu_svn.try_into().map_err(|_| invalid())?,
317 },
318 None,
319 ),
320 };
321 if !svn_matches_tee(svn, tee.tee_type()) {
322 return Err(invalid());
323 }
324 Ok(Self {
325 snapshot: Snapshot { svn, snp_domain },
326 })
327 }
328
329 /// Pure check that replacing this resident floor cannot decrease it or
330 /// change its comparison domain. Equal floors are allowed. Neither instance
331 /// is modified, including on failure. The caller must hold its shared state
332 /// lock across this check and replacement; no hardware calls are made.
333 pub fn check_restored_successor(&self, restored: &Self) -> Result<(), Error> {
334 self.snapshot.check_successor(&restored.snapshot)
335 }
336
337 /// Initialize from a fresh trusted local report, without deriving any keys.
338 /// The `tee` must be the trusted local hardware interface, not report bytes
339 /// supplied by the root or by a persisted protector.
340 pub fn new(tee: &dyn TeeCall, config: &AttestationVmConfig) -> Result<Self, Error> {
341 sealing_context(tee, config)?;
342 Ok(Self {
343 snapshot: Snapshot::observe(tee)?,
344 })
345 }
346
347 /// Validate and ratchet from a fresh report, then seal the active DEK using
348 /// that same report's SVN. Derivation/sealing errors cannot undo the ratchet.
349 /// The caller performs candidate pre-write verification separately.
350 pub fn create_protector(
351 &mut self,
352 tee: &dyn TeeCall,
353 config: &AttestationVmConfig,
354 dek: &[u8; 32],
355 ) -> Result<Vec<u8>, Error> {
356 let (hardware, mix_measurement) = sealing_context(tee, config)?;
357 let svn = self.observe_and_ratchet(tee)?;
358 create_protector_with_svn(hardware, config, svn, mix_measurement, dek)
359 }
360
361 /// Validate and ratchet from a fresh report before inspecting the candidate.
362 /// Return `Ok(false)` if its header SVN differs from the observed SVN, even
363 /// if hardware could still derive a key for that older candidate. Otherwise
364 /// freshly derive hardware keys and authenticate/unseal via the existing
365 /// stateless verifier. Neither mismatches nor errors undo a ratchet.
366 ///
367 /// This does not make the report and derivation atomic with migration; the
368 /// caller still needs pre-write and post-flush verification and retry logic.
369 pub fn verify_protector(
370 &mut self,
371 tee: &dyn TeeCall,
372 config: &AttestationVmConfig,
373 protector: &[u8],
374 dek: &[u8; 32],
375 ) -> Result<bool, Error> {
376 sealing_context(tee, config)?;
377 let svn = self.observe_and_ratchet(tee)?;
378 let Ok(candidate) = parse_hardware_key_protector(protector) else {
379 return Ok(false);
380 };
381 let Some(policy) = validated_policy(&candidate) else {
382 return Ok(false);
383 };
384 if !svn_equal(policy.svn, svn) {
385 return Ok(false);
386 }
387 protector_matches(tee, config, protector, dek)
388 }
389
390 /// Fetch, check, and commit a runtime observation, in that order.
391 ///
392 /// Report acquisition/parsing or `check_successor` failure leaves the floor
393 /// unchanged. On success, return the SVN of the newly accepted floor. The
394 /// update intentionally precedes derivation, protector verification, and
395 /// disk I/O: none of those later failures may undo a trusted TCB increase.
396 ///
397 /// For example, accepting components (2, 4) after (2, 3) retains (2, 4)
398 /// even if the ensuing write fails; a retry at (2, 3) must then be rejected.
399 /// These are component vectors, not packed integers or lexicographic values.
400 fn observe_and_ratchet(&mut self, tee: &dyn TeeCall) -> Result<KeyDerivationSvn, Error> {
401 let observed = Snapshot::observe(tee)?;
402 self.snapshot.check_successor(&observed)?;
403 // Commit before any key derivation, crypto, or untrusted header parsing.
404 self.snapshot = observed;
405 Ok(self.snapshot.svn)
406 }
407}
408
409impl Snapshot {
410 /// Fetch one fresh local report and extract its SVN/comparison domain.
411 /// Unlike `BootTcbFloor::observe`, this performs hardware I/O. It does not
412 /// compare against or mutate a floor, derive keys, or inspect VMGS.
413 fn observe(tee: &dyn TeeCall) -> Result<Self, Error> {
414 let report = tee
415 .get_attestation_report(&[0; REPORT_DATA_SIZE])
416 .map_err(|err| Error(ErrorInner::Report(err)))?;
417 Self::from_report(tee, &report)
418 }
419
420 /// Extract a snapshot without I/O or comparison against an earlier TCB.
421 /// Check the SVN's TEE variant and, for SNP, the report layout and agreement
422 /// between its raw reported TCB and the adapter's extracted SVN.
423 ///
424 /// Trust comes from the local hardware interface, not this parser: this is
425 /// not signature verification of an arbitrary report. Never pass report
426 /// bytes supplied by the host or a VMGS header.
427 fn from_report(tee: &dyn TeeCall, report: &GetAttestationReportResult) -> Result<Self, Error> {
428 let svn = report
429 .key_derivation_svn
430 .ok_or(Error(ErrorInner::MissingKeyDerivationSvn))?;
431 if !svn_matches_tee(svn, tee.tee_type()) {
432 return Err(Error(ErrorInner::ReportSvnMismatch));
433 }
434 let snp_domain = match svn {
435 KeyDerivationSvn::Snp { tcb_version } => {
436 // Read an owned value: the byte buffer need not satisfy the
437 // report's alignment. Preserve acceptance of trailing bytes.
438 let (snp_report, _) = SnpReport::read_from_prefix(&report.report)
439 .map_err(|_| Error(ErrorInner::MalformedReport))?;
440 let version = snp_report.version;
441 if snp_report.reported_tcb != tcb_version {
442 return Err(Error(ErrorInner::ReportTcbMismatch));
443 }
444 let cpuid = if version >= 3 {
445 Some([snp_report.cpuid_fam_id, snp_report.cpuid_mod_id])
446 } else {
447 None
448 };
449 Some(SnpDomain { version, cpuid })
450 }
451 KeyDerivationSvn::Tdx { .. } => None,
452 };
453 Ok(Self { svn, snp_domain })
454 }
455
456 /// Can `observed` replace `self` as the minimum accepted runtime TCB?
457 /// `self` is the existing floor; `observed` is a candidate local observation.
458 /// Neither argument is modified, and this method performs no hardware I/O.
459 ///
460 /// - Require the same SNP comparison domain (report version/family/model).
461 /// - Within a compatible domain, identical SVN values are accepted, even
462 /// when there is no supported component-ordering rule.
463 /// - For changed SNP SVNs, require a supported layout, equal reserved bytes,
464 /// and non-decreasing named components.
465 /// - For TDX, require the same module identity and reserved bytes, with
466 /// non-decreasing minor SVN, SE_SVN, and CPU SVN components. Different
467 /// TEE types are incompatible.
468 ///
469 /// A higher component cannot compensate for a lower one. Known component
470 /// regressions fail with `TcbLowered`; incompatible domains, identities,
471 /// reserved bytes, or unsupported ordering fail with `TcbIncompatible`.
472 /// Acceptance is only a minimum-TCB decision, not proof that a candidate
473 /// protector can be unsealed or that the TCB has a particular vendor status.
474 fn check_successor(&self, observed: &Self) -> Result<(), Error> {
475 if self.snp_domain != observed.snp_domain {
476 return Err(Error(ErrorInner::TcbIncompatible));
477 }
478 if svn_equal(self.svn, observed.svn) {
479 return Ok(());
480 }
481 match (self.svn, observed.svn, self.snp_domain.as_ref()) {
482 (
483 KeyDerivationSvn::Snp { tcb_version: floor },
484 KeyDerivationSvn::Snp { tcb_version: next },
485 Some(domain),
486 ) => {
487 // AMD 56860 rev. 1.59 section 2.3, tables 4 and 5. The
488 // report version gates supported semantics; family/model
489 // chooses the encoding. Preserve raw SVN for key derivation.
490 let floor = floor.to_le_bytes();
491 let next = next.to_le_bytes();
492 let layout = domain
493 .tcb_layout()
494 .ok_or(Error(ErrorInner::TcbIncompatible))?;
495 let meets_floor = match layout {
496 SnpTcbLayout::Legacy => {
497 let floor = SnpTcbVersionLegacy::read_from_bytes(&floor)
498 .map_err(|_| Error(ErrorInner::MalformedReport))?;
499 let next = SnpTcbVersionLegacy::read_from_bytes(&next)
500 .map_err(|_| Error(ErrorInner::MalformedReport))?;
501 if next.reserved != floor.reserved {
502 return Err(Error(ErrorInner::TcbIncompatible));
503 }
504 next.bootloader >= floor.bootloader
505 && next.tee >= floor.tee
506 && next.snp >= floor.snp
507 && next.microcode >= floor.microcode
508 }
509 SnpTcbLayout::Turin => {
510 let floor = SnpTcbVersionTurin::read_from_bytes(&floor)
511 .map_err(|_| Error(ErrorInner::MalformedReport))?;
512 let next = SnpTcbVersionTurin::read_from_bytes(&next)
513 .map_err(|_| Error(ErrorInner::MalformedReport))?;
514 if next.reserved != floor.reserved {
515 return Err(Error(ErrorInner::TcbIncompatible));
516 }
517 next.fmc >= floor.fmc
518 && next.bootloader >= floor.bootloader
519 && next.tee >= floor.tee
520 && next.snp >= floor.snp
521 && next.microcode >= floor.microcode
522 }
523 };
524 if !meets_floor {
525 return Err(Error(ErrorInner::TcbLowered));
526 }
527 Ok(())
528 }
529 (
530 KeyDerivationSvn::Tdx {
531 tee_tcb_svn: floor_tee,
532 cpu_svn: floor_cpu,
533 },
534 KeyDerivationSvn::Tdx {
535 tee_tcb_svn: next_tee,
536 cpu_svn: next_cpu,
537 },
538 None,
539 ) => {
540 // Parse the ABI layout without changing the raw derivation SVN.
541 let floor_tee = TeeTcbSvn::read_from_bytes(&floor_tee)
542 .map_err(|_| Error(ErrorInner::MalformedReport))?;
543 let next_tee = TeeTcbSvn::read_from_bytes(&next_tee)
544 .map_err(|_| Error(ErrorInner::MalformedReport))?;
545 // Major SVN selects the module identity, not an ordered SVN.
546 // This rejects both legacy/TD-preserving transitions and
547 // transitions between distinct TD-preserving identities. A
548 // change in reserved metadata has no supported ordering.
549 if next_tee.tdx_module_svn_major != floor_tee.tdx_module_svn_major
550 || next_tee._reserved != floor_tee._reserved
551 {
552 return Err(Error(ErrorInner::TcbIncompatible));
553 }
554 // CPU SVN is kept as 16 bytes: apply the local per-position
555 // floor rule documented on components_meet, not Rust array Ord
556 // (lexicographic) or a packed-integer comparison. TEE SVN has
557 // its own named-field rules and must not use that helper.
558 if !components_meet(&next_cpu, &floor_cpu)
559 || next_tee.tdx_module_svn_minor < floor_tee.tdx_module_svn_minor
560 || next_tee.seam_last_patch_svn < floor_tee.seam_last_patch_svn
561 {
562 return Err(Error(ErrorInner::TcbLowered));
563 }
564 Ok(())
565 }
566 // Mixed TEE snapshots have no ordering contract.
567 _ => Err(Error(ErrorInner::TcbIncompatible)),
568 }
569 }
570}
571
572/// Inclusive component-wise minimum: every unsigned byte must meet the same
573/// position in the floor. For CPU SVN, N is 16; there is no endian conversion,
574/// sorting, or carry between components. With other bytes equal, (2, 4) meets
575/// (2, 3), but (3, 2) does not: growth cannot compensate for a regression.
576///
577/// Official context: Intel PCS [Get TDX TCB Info V4], step 3.a, requires all
578/// 16 PCK certificate TCB component SVNs to meet their corresponding TCB Info
579/// values; [Appendix A] describes the 16 components and `tcbType` comparison
580/// metadata. This motivates component-wise rather than scalar ordering.
581///
582/// This function applies OpenHCL's local no-decrease policy to two raw CPU SVN
583/// observations. It does NOT implement PCS appraisal, map raw bytes to PCK
584/// component identities, or establish equivalence across platform families.
585/// Do not infer `UpToDate`/revocation status from it: that requires authenticated
586/// platform-specific collateral, including FMSPC and TCB Info.
587///
588/// [Get TDX TCB Info V4]: https://api.portal.trustedservices.intel.com/content/documentation.html#pcs-tcb-info-tdx-v4
589/// [Appendix A]: https://api.portal.trustedservices.intel.com/content/documentation.html#pcs-tcb-info-model-v3
590fn components_meet<const N: usize>(actual: &[u8; N], minimum: &[u8; N]) -> bool {
591 actual
592 .iter()
593 .zip(minimum)
594 .all(|(actual, minimum)| actual >= minimum)
595}
596
597// Keep equality local rather than imposing a public ordering/equality contract
598// on TeeCall's types. These values are SVN metadata, not secret key material.
599fn svn_equal(left: KeyDerivationSvn, right: KeyDerivationSvn) -> bool {
600 match (left, right) {
601 (
602 KeyDerivationSvn::Snp { tcb_version: left },
603 KeyDerivationSvn::Snp { tcb_version: right },
604 ) => left == right,
605 (
606 KeyDerivationSvn::Tdx {
607 tee_tcb_svn: left_tee,
608 cpu_svn: left_cpu,
609 },
610 KeyDerivationSvn::Tdx {
611 tee_tcb_svn: right_tee,
612 cpu_svn: right_cpu,
613 },
614 ) => left_tee == right_tee && left_cpu == right_cpu,
615 _ => false,
616 }
617}
618
619#[cfg(test)]
620mod tests;