Skip to main content

base64_ng/v2/assurance/
context.rs

1//! Generation-bound assurance context and tokens.
2
3use core::{
4    cell::UnsafeCell,
5    marker::PhantomData,
6    sync::atomic::{AtomicUsize, Ordering},
7};
8
9use crate::runtime::{CtGatePosture, WipePosture};
10
11use super::provider::ProtectedMemoryProvider;
12
13/// Snapshot of the four independent context generations.
14#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
15pub struct AssuranceGenerations {
16    /// Ordinary backend-health generation.
17    pub ordinary_backend: usize,
18    /// Secret scalar-algorithm policy generation.
19    pub secret_algorithm: usize,
20    /// Wipe primitive and barrier generation.
21    pub wipe_barrier: usize,
22    /// Speculation-posture generation.
23    pub speculation: usize,
24}
25
26/// Marker for dependency-free best-effort secret policy.
27#[derive(Debug)]
28pub struct BestEffort {
29    _private: (),
30}
31
32/// Marker for deployment-attested high-assurance policy.
33#[derive(Debug)]
34pub struct Attested {
35    _private: (),
36}
37
38mod sealed {
39    pub trait Level {
40        const ATTESTED: bool;
41    }
42}
43
44impl sealed::Level for BestEffort {
45    const ATTESTED: bool = false;
46}
47
48impl sealed::Level for Attested {
49    const ATTESTED: bool = true;
50}
51
52/// Assurance-level behavior used by protected owners.
53pub trait AssuranceLevel: sealed::Level {}
54
55impl AssuranceLevel for BestEffort {}
56impl AssuranceLevel for Attested {}
57
58/// Current target identity bound by platform evidence.
59#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
60#[non_exhaustive]
61pub enum TargetAttestation {
62    /// x86 with the crate's reviewed native barriers.
63    X86,
64    /// x86-64 with the crate's reviewed native barriers.
65    X86_64,
66    /// `AArch64` with deployment-attested CSDB effectiveness.
67    Aarch64Csdb,
68    /// A reviewed external embedded target/provider combination.
69    ReviewedEmbedded,
70}
71
72impl TargetAttestation {
73    const fn matches_current_target(self) -> bool {
74        match self {
75            Self::X86 => cfg!(target_arch = "x86"),
76            Self::X86_64 => cfg!(target_arch = "x86_64"),
77            Self::Aarch64Csdb => {
78                cfg!(all(
79                    target_arch = "aarch64",
80                    base64_ng_aarch64_csdb_attested
81                ))
82            }
83            Self::ReviewedEmbedded => true,
84        }
85    }
86}
87
88/// Exact wipe procedure named by platform evidence.
89#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
90#[non_exhaustive]
91pub enum WipeAttestation {
92    /// Byte-wise volatile overwrite plus the crate's selected barrier.
93    VolatileBytesAndSelectedBarrier,
94}
95
96/// Unsafe-provider evidence used to mint an attested token.
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub struct AttestationEvidence {
99    target: TargetAttestation,
100    wipe: WipeAttestation,
101    wipe_posture: WipePosture,
102    speculation_posture: CtGatePosture,
103    provider_identity: usize,
104    provider_generation: usize,
105}
106
107impl AttestationEvidence {
108    /// Constructs reviewed platform evidence.
109    ///
110    /// # Safety
111    ///
112    /// The caller must have independently established that every field is
113    /// true for the exact deployed target, provider instance, wipe primitive,
114    /// barrier sequence, and generation. Evidence must not outlive or be
115    /// replayed across a provider-instance reset.
116    #[must_use]
117    #[allow(unsafe_code)]
118    pub const unsafe fn new(
119        target: TargetAttestation,
120        wipe: WipeAttestation,
121        wipe_posture: WipePosture,
122        speculation_posture: CtGatePosture,
123        provider_identity: usize,
124        provider_generation: usize,
125    ) -> Self {
126        Self {
127            target,
128            wipe,
129            wipe_posture,
130            speculation_posture,
131            provider_identity,
132            provider_generation,
133        }
134    }
135
136    pub(crate) const fn provider_identity(self) -> usize {
137        self.provider_identity
138    }
139
140    pub(crate) const fn provider_generation(self) -> usize {
141        self.provider_generation
142    }
143
144    pub(crate) const fn wipe_posture(self) -> WipePosture {
145        self.wipe_posture
146    }
147
148    pub(crate) const fn speculation_posture(self) -> CtGatePosture {
149        self.speculation_posture
150    }
151}
152
153/// Reviewed platform evidence source.
154///
155/// # Safety
156///
157/// Implementors must return evidence obtained from the actual deployment and
158/// exact provider instance. The implementation must not unwind and must not
159/// treat build flags, target names, or requested policy as hardware evidence.
160#[allow(unsafe_code)]
161pub unsafe trait PlatformAttestation {
162    /// Returns current evidence or a redacted failure.
163    fn attest(&self) -> Result<AttestationEvidence, AssuranceError>;
164}
165
166/// Runtime assurance failure.
167#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
168#[non_exhaustive]
169pub enum AssuranceError {
170    /// A relevant context generation changed.
171    StaleGeneration,
172    /// The explicit high-assurance build policy is absent.
173    HighAssuranceBuildRequired,
174    /// Evidence names another target or provider instance.
175    MismatchedAttestation,
176    /// Wipe or speculation posture is insufficient.
177    InsufficientPosture,
178    /// Provider health or generation is stale.
179    ProviderUnavailable,
180}
181
182impl core::fmt::Display for AssuranceError {
183    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
184        formatter.write_str(match self {
185            Self::StaleGeneration => "assurance generation is stale",
186            Self::HighAssuranceBuildRequired => "high-assurance build policy is required",
187            Self::MismatchedAttestation => "platform attestation does not match",
188            Self::InsufficientPosture => "platform posture is insufficient",
189            Self::ProviderUnavailable => "protected-memory provider is unavailable",
190        })
191    }
192}
193
194#[cfg(feature = "std")]
195impl std::error::Error for AssuranceError {}
196
197/// Mutable runtime assurance authority.
198///
199/// Generation invalidation is monotonic. Allocation protection is deliberately
200/// absent from this context and must be proven separately for each protected
201/// allocation.
202pub struct AssuranceContext {
203    ordinary_backend: AtomicUsize,
204    secret_algorithm: AtomicUsize,
205    wipe_barrier: AtomicUsize,
206    speculation: AtomicUsize,
207}
208
209impl AssuranceContext {
210    /// Creates a fresh context with no platform attestation.
211    #[must_use]
212    pub const fn new() -> Self {
213        Self {
214            ordinary_backend: AtomicUsize::new(1),
215            secret_algorithm: AtomicUsize::new(1),
216            wipe_barrier: AtomicUsize::new(crate::cleanup::WIPE_PRIMITIVE_REVISION),
217            speculation: AtomicUsize::new(1),
218        }
219    }
220
221    /// Returns all current generation counters.
222    #[must_use]
223    pub fn generations(&self) -> AssuranceGenerations {
224        AssuranceGenerations {
225            ordinary_backend: self.ordinary_backend.load(Ordering::Acquire),
226            secret_algorithm: self.secret_algorithm.load(Ordering::Acquire),
227            wipe_barrier: self.wipe_barrier.load(Ordering::Acquire),
228            speculation: self.speculation.load(Ordering::Acquire),
229        }
230    }
231
232    /// Mints a context-borrowing best-effort token.
233    #[must_use]
234    pub fn best_effort_token(&self) -> AssuranceToken<'_, BestEffort> {
235        AssuranceToken::new(self, self.generations(), None)
236    }
237
238    /// Mints an attested token after checking exact provider and target evidence.
239    pub fn attested_token<P>(
240        &self,
241        provider: &P,
242    ) -> Result<AssuranceToken<'_, Attested>, AssuranceError>
243    where
244        P: PlatformAttestation + ProtectedMemoryProvider,
245    {
246        if !cfg!(base64_ng_require_high_assurance) {
247            return Err(AssuranceError::HighAssuranceBuildRequired);
248        }
249        let evidence = provider.attest()?;
250        if !evidence.target.matches_current_target()
251            || evidence.provider_identity != provider.provider_identity()
252            || evidence.provider_generation != provider.provider_generation()
253        {
254            return Err(AssuranceError::MismatchedAttestation);
255        }
256        if evidence.wipe != WipeAttestation::VolatileBytesAndSelectedBarrier
257            || !wipe_posture_is_attestable(evidence.wipe_posture)
258            || !speculation_posture_is_attestable(evidence.speculation_posture)
259        {
260            return Err(AssuranceError::InsufficientPosture);
261        }
262        let generations = self.generations();
263        Ok(AssuranceToken::new(self, generations, Some(evidence)))
264    }
265
266    /// Invalidates only ordinary backend-health evidence.
267    pub fn invalidate_ordinary_backend(&self) {
268        advance(&self.ordinary_backend);
269    }
270
271    /// Invalidates secret scalar-algorithm evidence.
272    pub fn invalidate_secret_algorithm(&self) {
273        advance(&self.secret_algorithm);
274    }
275
276    /// Invalidates wipe primitive and barrier evidence.
277    pub fn invalidate_wipe_barrier(&self) {
278        advance(&self.wipe_barrier);
279    }
280
281    /// Invalidates speculation-posture evidence.
282    pub fn invalidate_speculation(&self) {
283        advance(&self.speculation);
284    }
285}
286
287impl Default for AssuranceContext {
288    fn default() -> Self {
289        Self::new()
290    }
291}
292
293/// Non-forgeable, context-borrowing assurance token.
294///
295/// Tokens are deliberately neither `Copy` nor `Clone`. The marker also makes
296/// them `!Sync`, `!UnwindSafe`, and `!RefUnwindSafe`; these auto traits are API
297/// friction only, never a cleanup security boundary.
298pub struct AssuranceToken<'context, Level: AssuranceLevel> {
299    context: &'context AssuranceContext,
300    generations: AssuranceGenerations,
301    evidence: Option<AttestationEvidence>,
302    _level: PhantomData<Level>,
303    _not_sync_or_unwind_safe: PhantomData<(UnsafeCell<()>, &'context mut dyn FnMut())>,
304}
305
306impl<'context, Level: AssuranceLevel> AssuranceToken<'context, Level> {
307    fn new(
308        context: &'context AssuranceContext,
309        generations: AssuranceGenerations,
310        evidence: Option<AttestationEvidence>,
311    ) -> Self {
312        Self {
313            context,
314            generations,
315            evidence,
316            _level: PhantomData,
317            _not_sync_or_unwind_safe: PhantomData,
318        }
319    }
320
321    /// Returns the captured generation snapshot.
322    #[must_use]
323    pub const fn generations(&self) -> AssuranceGenerations {
324        self.generations
325    }
326
327    /// Revalidates only generations relevant to secret computation.
328    pub fn revalidate(&self) -> Result<(), AssuranceError> {
329        let current = self.context.generations();
330        if current.secret_algorithm == 0
331            || current.wipe_barrier == 0
332            || (Level::ATTESTED && current.speculation == 0)
333            || current.secret_algorithm != self.generations.secret_algorithm
334            || current.wipe_barrier != self.generations.wipe_barrier
335            || (Level::ATTESTED && current.speculation != self.generations.speculation)
336        {
337            return Err(AssuranceError::StaleGeneration);
338        }
339        Ok(())
340    }
341
342    pub(crate) const fn context(&self) -> &'context AssuranceContext {
343        self.context
344    }
345
346    pub(crate) const fn evidence(&self) -> Option<AttestationEvidence> {
347        self.evidence
348    }
349
350    pub(crate) const fn requires_attestation() -> bool {
351        Level::ATTESTED
352    }
353}
354
355impl AssuranceContext {
356    pub(crate) fn revalidate_snapshot<Level: AssuranceLevel>(
357        &self,
358        generations: AssuranceGenerations,
359    ) -> Result<(), AssuranceError> {
360        let current = self.generations();
361        if current.secret_algorithm == 0
362            || current.wipe_barrier == 0
363            || (Level::ATTESTED && current.speculation == 0)
364            || current.secret_algorithm != generations.secret_algorithm
365            || current.wipe_barrier != generations.wipe_barrier
366            || (Level::ATTESTED && current.speculation != generations.speculation)
367        {
368            Err(AssuranceError::StaleGeneration)
369        } else {
370            Ok(())
371        }
372    }
373
374    pub(crate) fn revalidate_wipe_snapshot<Level: AssuranceLevel>(
375        &self,
376        generations: AssuranceGenerations,
377    ) -> Result<(), AssuranceError> {
378        let current = self.generations();
379        if current.wipe_barrier == 0
380            || (Level::ATTESTED && current.speculation == 0)
381            || current.wipe_barrier != generations.wipe_barrier
382            || (Level::ATTESTED && current.speculation != generations.speculation)
383        {
384            Err(AssuranceError::StaleGeneration)
385        } else {
386            Ok(())
387        }
388    }
389}
390
391impl<Level: AssuranceLevel> core::fmt::Debug for AssuranceToken<'_, Level> {
392    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
393        formatter
394            .debug_struct("AssuranceToken")
395            .field(
396                "level",
397                &if Level::ATTESTED {
398                    "attested"
399                } else {
400                    "best-effort"
401                },
402            )
403            .field("generations", &self.generations)
404            .finish_non_exhaustive()
405    }
406}
407
408// Rust 1.97 renamed this operation; retain the 1.90 MSRV spelling until the
409// workspace MSRV can use `try_update`.
410#[allow(deprecated)]
411fn advance(generation: &AtomicUsize) {
412    let _ = generation.fetch_update(Ordering::AcqRel, Ordering::Acquire, |value| {
413        Some(if value == 0 {
414            0
415        } else {
416            value.checked_add(1).unwrap_or(0)
417        })
418    });
419}
420
421const fn wipe_posture_is_attestable(posture: WipePosture) -> bool {
422    matches!(posture, WipePosture::HardwareFence)
423}
424
425const fn speculation_posture_is_attestable(posture: CtGatePosture) -> bool {
426    matches!(
427        posture,
428        CtGatePosture::HardwareSpeculationBarrier
429            | CtGatePosture::HardwareSpeculationBarrierBuildAsserted
430    )
431}