Skip to main content

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}