chio_kernel/boot.rs
1//! Kernel boot path: gate the PQ signing key load on a self-quote.
2//!
3//! The PQ signing key load is gated on a verified self-quote that binds the
4//! `expect_report_data` and hybrid-signing paths. Boot order:
5//!
6//! 1. The kernel comes up with its classical Ed25519 keypair already
7//! materialized. No PQ key has been touched.
8//! 2. The kernel requests its own TEE quote from the surrounding
9//! `chio-tee` container and feeds the bytes into a
10//! verifier-side [`KernelSelfQuoteVerifier`]. The quote
11//! MUST commit to `expect_report_data(kernel_classical_pk,
12//! receipt_root_genesis)` where `receipt_root_genesis` is the all-zero
13//! 32-byte sentinel `[0u8; 32]` representing the empty receipt-tree root
14//! at boot.
15//! 3. Only after the verifier returns success does the kernel derive the
16//! [`MlDsa65Backend`](chio_core_types::pq::MlDsa65Backend) from the
17//! operator-supplied seed and compose the
18//! [`HybridBackend`](chio_core_types::pq::HybridBackend). Any failure in
19//! step 2 leaves the kernel running classical-only and the boot path
20//! returns an error: under `crypto_floor=allow_hybrid` or
21//! `crypto_floor=pq_required` the operator MUST refuse to start signing.
22//!
23//! ## Why a port trait rather than a hard `chio-attest-verify` dep
24//!
25//! `chio-kernel` does not depend on `chio-attest-verify` and this module
26//! preserves that boundary. The verifier crate today carries TDX/SEV-SNP/
27//! Nitro backends each pulled in behind a cargo feature; the kernel does
28//! not need any of them at boot time. The port trait below is small enough
29//! to implement against `chio-attest-verify`'s `QuoteVerifier` by a
30//! one-line shim in the operator binary that wires the two crates together,
31//! and small enough to implement against an in-memory mock for the
32//! integration test.
33//!
34//! ## Trust-boundary discipline
35//!
36//! - Fail-closed: every error path returns
37//! [`KernelBootError`] without producing a [`HybridBackend`].
38//! - The kernel signing key (the classical Ed25519 key plus the PQ
39//! composition) is never returned partially. Callers that ask for a
40//! hybrid backend either get one fully gated by a verified self-quote
41//! or they get an error.
42//! - `receipt_root_genesis` is the [`RECEIPT_ROOT_GENESIS`] constant.
43//! Production callers MUST NOT thread their current receipt root in;
44//! the genesis quote binds to the empty tree so the chain has a fixed
45//! cryptographic anchor independent of the first receipt's contents.
46
47use chio_core::crypto::{Keypair, PublicKey, SigningBackend};
48
49use crate::receipt_support::{
50 kernel_signing_backend, KernelCryptoFloor, KernelSigningBackendError,
51};
52
53/// Receipt-root sentinel used for the kernel self-quote.
54///
55/// The first signed receipt advances the root, but the genesis-quote
56/// binds to the empty tree so the chain has a fixed cryptographic anchor
57/// independent of the first receipt's contents (per the self-quote
58/// boot-gate contract).
59pub const RECEIPT_ROOT_GENESIS: [u8; 32] = [0u8; 32];
60
61/// Outcome reported by a [`KernelSelfQuoteVerifier`].
62///
63/// The verifier carries the actual collateral chain validation and TCB
64/// freshness check; the kernel boot path only inspects the boolean
65/// success flag and the diagnostic message. A successful verification
66/// MUST mean that:
67///
68/// - the quote envelope parsed and matched the configured TEE family,
69/// - the collateral chained to its trusted root,
70/// - the TCB status was acceptable per the verifier's policy,
71/// - and the 64-byte `report_data` slot byte-matched
72/// `expect_report_data(kernel_classical_pk, RECEIPT_ROOT_GENESIS)`.
73///
74/// Any failure produces [`KernelSelfQuoteOutcome::rejected`]. Verifiers
75/// MUST NOT report success on partial verification.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct KernelSelfQuoteOutcome {
78 /// True when the quote verified end-to-end.
79 pub verified: bool,
80 /// Verifier-side diagnostic propagated into [`KernelBootError`] when
81 /// `verified` is `false`. Empty on success.
82 pub diagnostic: String,
83}
84
85impl KernelSelfQuoteOutcome {
86 /// Verifier-friendly constructor for a successful verification.
87 #[must_use]
88 pub fn accepted() -> Self {
89 Self {
90 verified: true,
91 diagnostic: String::new(),
92 }
93 }
94
95 /// Verifier-friendly constructor for a failed verification.
96 #[must_use]
97 pub fn rejected(diagnostic: impl Into<String>) -> Self {
98 Self {
99 verified: false,
100 diagnostic: diagnostic.into(),
101 }
102 }
103}
104
105/// Port the kernel boot path consults to verify its own TEE quote.
106///
107/// Implementations live in operator binaries (which compose
108/// `chio-attest-verify` backends) or in
109/// integration tests (which inject a mock with a captured expected
110/// `report_data`). The port trait deliberately does not return the full
111/// `VerifiedQuote` shape: the kernel boot path only needs a yes/no
112/// answer plus a diagnostic message, and keeping the interface narrow
113/// preserves the no-`chio-attest-verify`-dep boundary on `chio-kernel`.
114pub trait KernelSelfQuoteVerifier: Send + Sync {
115 /// Verify a kernel self-quote against the supplied classical
116 /// public key and the genesis receipt root.
117 ///
118 /// `quote_bytes` is the raw on-the-wire envelope produced by the
119 /// surrounding TEE container (chio-tee). The verifier MUST consult
120 /// `expected_classical_pk` and `RECEIPT_ROOT_GENESIS` when
121 /// reconstructing the expected `report_data`; any other binding is
122 /// a verifier bug.
123 fn verify_self_quote(
124 &self,
125 quote_bytes: &[u8],
126 expected_classical_pk: &PublicKey,
127 ) -> KernelSelfQuoteOutcome;
128}
129
130/// Errors the boot path raises while gating the PQ key load.
131///
132/// Distinct from [`KernelSigningBackendError`] so the operator log can
133/// distinguish "the policy floor was misconfigured" from "the self-quote
134/// did not bind". Both still fail the kernel start.
135#[derive(Debug, thiserror::Error)]
136pub enum KernelBootError {
137 /// The supplied self-quote did not verify or did not bind to the
138 /// classical kernel public key. The kernel MUST NOT proceed to load
139 /// the PQ signing key under any non-classical floor.
140 #[error("kernel self-quote did not bind to classical kernel public key: {diagnostic}")]
141 SelfQuoteRejected {
142 /// Verifier-supplied diagnostic message.
143 diagnostic: String,
144 },
145
146 /// The boot path tried to construct a hybrid backend but the PQ
147 /// seed or floor was misconfigured. Wraps the existing
148 /// [`KernelSigningBackendError`] so callers can pattern-match if
149 /// they need to.
150 #[error(transparent)]
151 SigningBackend(#[from] KernelSigningBackendError),
152}
153
154/// Load the kernel signing backend, gating the PQ half on a verified
155/// self-quote.
156///
157/// Boot logic:
158///
159/// - Under [`KernelCryptoFloor::AllowClassical`] the self-quote step is
160/// skipped: there is no PQ key to gate, and a classical-only deployment
161/// may not even ship a TEE container. The function returns the
162/// classical-only backend produced by [`kernel_signing_backend`].
163/// - Under [`KernelCryptoFloor::AllowHybrid`] or
164/// [`KernelCryptoFloor::PqRequired`] the function consults the
165/// verifier port FIRST. Only on
166/// [`KernelSelfQuoteOutcome::verified`] does it construct the hybrid
167/// backend. A rejected outcome returns
168/// [`KernelBootError::SelfQuoteRejected`] without ever materializing
169/// the [`MlDsa65Backend`](chio_core_types::pq::MlDsa65Backend).
170///
171/// `quote_bytes` is the on-the-wire self-quote produced by the TEE
172/// container. For tests, an empty slice combined with a mock verifier
173/// is fine; for production the bytes come from `chio-tee`.
174///
175/// # Errors
176///
177/// - [`KernelBootError::SelfQuoteRejected`] when the verifier rejects
178/// the self-quote and the floor mandates a PQ key.
179/// - [`KernelBootError::SigningBackend`] when the floor or PQ seed is
180/// misconfigured (forwarded from
181/// [`KernelSigningBackendError::HybridFloorRequiresPqKey`] or
182/// [`KernelSigningBackendError::PqKeyImportFailed`]).
183pub fn load_kernel_signing_backend_after_self_quote(
184 crypto_floor: KernelCryptoFloor,
185 classical_keypair: Keypair,
186 pq_seed: Option<&[u8; 32]>,
187 quote_bytes: &[u8],
188 verifier: &dyn KernelSelfQuoteVerifier,
189) -> Result<Box<dyn SigningBackend>, KernelBootError> {
190 if !crypto_floor.allows_hybrid() {
191 // Classical-only deployment: no PQ key to gate. Defer to the
192 // existing helper so the classical path stays byte-identical to
193 // deployments that have not opted in.
194 return Ok(kernel_signing_backend(
195 crypto_floor,
196 classical_keypair,
197 pq_seed,
198 )?);
199 }
200
201 // Non-classical floor: ALWAYS consult the verifier before deriving
202 // any PQ material. The classical public key is the gating identity
203 // every backend must commit into report_data.
204 let classical_pk = classical_keypair.public_key();
205 let outcome = verifier.verify_self_quote(quote_bytes, &classical_pk);
206 if !outcome.verified {
207 return Err(KernelBootError::SelfQuoteRejected {
208 diagnostic: outcome.diagnostic,
209 });
210 }
211
212 // Verifier said yes: materialize the hybrid backend through the
213 // shared boot helper so the resulting backend is bit-identical to
214 // the one the classical-only path already wires through.
215 Ok(kernel_signing_backend(
216 crypto_floor,
217 classical_keypair,
218 pq_seed,
219 )?)
220}