Skip to main content

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;