Skip to main content

sim_incremental_core/projection/
admission.rs

1use std::{error::Error, fmt};
2
3use sim_conformance_core::{
4    CheckArgument, CheckInputClosureId, CheckScopeId, CheckTemplate, CheckedSubjectId,
5    CheckerBinding, CheckerResultId, CommandId, ConformancePackId, EnvironmentPolicyId,
6    EvidenceGrade, EvidenceProvenanceId, EvidenceSetId, LiveCheckerAuthority, LiveCheckerOwner,
7    LiveCheckerReceipt, OutputShapeId, OwnerBindingId, PolicyId, ProofCodeId, RevocationSourceId,
8    WorkingDirectoryPolicyId,
9};
10use sim_kernel::{ContentId, Datum, NumberLiteral, Symbol};
11
12use super::{
13    DeterministicImportManifest, ProjectorPolicy, ProjectorQualification,
14    ProjectorQualificationKind, QualifiedRuntime, QualifiedSourceClosure,
15};
16
17/// Live, receipt-backed evidence that an exact native implementation was
18/// checked before this qualification attempt.
19///
20/// There is no field here a caller can simply set to claim review happened,
21/// and this type itself is `pub(crate)`: nothing outside this crate can
22/// construct one. [`ProjectorQualificationVerifier::trusted_native`] proves
23/// every claim by calling [`LiveCheckerReceipt::verify_current`] against a
24/// real, owner-issued, revocation-aware receipt, whose subject is checked
25/// against the declared code AND dependency identity, not code alone.
26#[derive(Clone, Debug)]
27pub(crate) struct NativeSourceEvidence {
28    /// Exact implementation identity.
29    pub(crate) code: ContentId,
30    /// Exact transitive runtime dependency identity.
31    pub(crate) dependencies: ContentId,
32    /// The live, owner-issued receipt proving the required review occurred.
33    pub(crate) receipt: LiveCheckerReceipt,
34}
35
36/// Evidence needed to admit a closed wasm projector.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ClosedWasmEvidence {
39    /// Exact semantic module identity.
40    pub module: ContentId,
41    /// Complete imports discovered from the module.
42    pub imports: DeterministicImportManifest,
43    /// Exact qualified runtime.
44    pub runtime: QualifiedRuntime,
45    /// Admission evidence identity.
46    pub admission: ContentId,
47    /// Import discovery covered the complete transitive module.
48    pub import_manifest_complete: bool,
49    /// Start behavior was checked before instantiation.
50    pub start_behavior_checked: bool,
51    /// Fuel and memory limits are enforced by the runtime.
52    pub budgets_enforced: bool,
53}
54
55/// Distinct projector-admission refusal.
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum QualificationError {
58    /// Native code did not pass exact source and dependency review.
59    NativeCodeMismatch,
60    /// The supplied receipt is not for the required native-source-review scope.
61    WrongCheckerScope,
62    /// The supplied receipt's subject does not match the declared code and
63    /// dependency identity.
64    WrongCheckerSubject,
65    /// The receipt's evidence grade is below the minimum this route requires.
66    InsufficientEvidenceGrade,
67    /// The receipt is no longer current, or its issuing owner is unreachable.
68    CheckerUnavailable(String),
69    /// Constructing the bootstrap checker binding or invocation failed.
70    Checker(String),
71    /// Wasm import discovery was incomplete.
72    IncompleteImportManifest,
73    /// Actual and policy wasm imports differ.
74    ImportManifestMismatch,
75    /// An ambient or nondeterministic wasm import was requested.
76    ForbiddenImport(String),
77    /// Wasm start behavior was not qualified.
78    StartBehaviorUnchecked,
79    /// Runtime numeric, ordering, or fresh-instance semantics are incomplete.
80    RuntimeSemanticsUnqualified,
81    /// Runtime fuel or memory bounds are absent.
82    RuntimeBudgetsUnenforced,
83    /// Canonical policy identity failed.
84    CanonicalPolicy(String),
85}
86
87impl fmt::Display for QualificationError {
88    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(formatter, "{self:?}")
90    }
91}
92
93impl Error for QualificationError {}
94
95/// Verifies the two and only two projector admission routes.
96///
97/// Both entry points are `pub(crate)`: nothing outside `sim-incremental-core`
98/// can call them directly, or construct the evidence types they require.
99/// The only route a downstream crate has to a [`ProjectorQualification`] is
100/// [`super::ProjectionRegistry::qualification_for`], which calls these using
101/// state this crate's own registration logic established, never state a
102/// caller supplied.
103#[derive(Clone, Copy, Debug, Default)]
104pub(crate) struct ProjectorQualificationVerifier;
105
106impl ProjectorQualificationVerifier {
107    /// Qualifies exact trusted native code from a live, receipt-backed claim.
108    ///
109    /// # Errors
110    /// Refuses unless `evidence.receipt` is currently valid under `authority`,
111    /// scoped to [`native_source_review_scope`], and its checked subject
112    /// matches both `evidence.code` and `evidence.dependencies` together.
113    /// This never trusts a self-reported boolean, and `authority` is never a
114    /// caller-suppliable parameter of any `pub` function in this crate --
115    /// only code inside this crate ever holds one to pass here.
116    pub(crate) fn trusted_native(
117        policy: &ProjectorPolicy,
118        evidence: NativeSourceEvidence,
119        authority: &LiveCheckerAuthority,
120    ) -> Result<ProjectorQualification, QualificationError> {
121        evidence
122            .receipt
123            .verify_current(authority)
124            .map_err(|error| QualificationError::CheckerUnavailable(error.to_string()))?;
125        let receipt = evidence.receipt.receipt();
126        if receipt.scope() != &native_source_review_scope()? {
127            return Err(QualificationError::WrongCheckerScope);
128        }
129        if receipt.subject() != &reviewed_subject(&evidence.code, &evidence.dependencies)? {
130            return Err(QualificationError::WrongCheckerSubject);
131        }
132        if receipt.grade() < EvidenceGrade::Bootstrap {
133            return Err(QualificationError::InsufficientEvidenceGrade);
134        }
135        Ok(ProjectorQualification(
136            ProjectorQualificationKind::TrustedNative {
137                source: QualifiedSourceClosure {
138                    code: evidence.code,
139                    dependencies: evidence.dependencies,
140                    review: receipt.id().content_id().clone(),
141                },
142                policy: policy_id(policy)?,
143            },
144        ))
145    }
146
147    /// Qualifies a closed wasm module and deterministic runtime.
148    ///
149    /// Unexercised by any real caller yet -- no wasm projector exists in this
150    /// codebase -- but required by the roadmap's own text ("the closed wasm
151    /// route with verified deterministic imports") as the second of the two
152    /// admission routes. Kept, not deleted: a deferral tracked here, not a
153    /// silent good-enough. Its own `ClosedWasmEvidence` still trusts
154    /// self-reported booleans exactly like the old `trusted_native` did --
155    /// out of scope for this pass (no real caller means no real exploit
156    /// surface yet), but a real fix needs the same receipt-backed treatment
157    /// before anything calls this for real.
158    #[allow(dead_code)]
159    pub(crate) fn closed_wasm(
160        policy: &ProjectorPolicy,
161        evidence: ClosedWasmEvidence,
162    ) -> Result<ProjectorQualification, QualificationError> {
163        if !evidence.import_manifest_complete {
164            return Err(QualificationError::IncompleteImportManifest);
165        }
166        if evidence.imports != policy.imports {
167            return Err(QualificationError::ImportManifestMismatch);
168        }
169        for import in &evidence.imports.imports {
170            if is_forbidden_import(import) {
171                return Err(QualificationError::ForbiddenImport(import.clone()));
172            }
173        }
174        if !evidence.start_behavior_checked {
175            return Err(QualificationError::StartBehaviorUnchecked);
176        }
177        let semantics = &evidence.runtime.semantics;
178        if !semantics.canonical_nan || !semantics.canonical_collections || !semantics.fresh_instance
179        {
180            return Err(QualificationError::RuntimeSemanticsUnqualified);
181        }
182        if !evidence.budgets_enforced {
183            return Err(QualificationError::RuntimeBudgetsUnenforced);
184        }
185        Ok(ProjectorQualification(
186            ProjectorQualificationKind::ClosedWasm {
187                module: evidence.module,
188                policy: policy_id(policy)?,
189                runtime: evidence.runtime,
190                imports: evidence.imports,
191                admission: evidence.admission,
192            },
193        ))
194    }
195}
196
197/// The fixed checker scope every native-source-review receipt must carry.
198///
199/// # Errors
200/// Returns an error only if the fixed scope text itself cannot be canonicalized.
201fn native_source_review_scope() -> Result<CheckScopeId, QualificationError> {
202    CheckScopeId::from_text("projection/native-source-review-v1").map_err(checker_error)
203}
204
205/// Boots a fresh checker owner and issues the only kind of native-source
206/// receipt available before NV12.06's full external-review checker chain
207/// exists: proof that the code and dependency identity about to execute are
208/// exactly the code and dependency identity that were declared, at
209/// [`EvidenceGrade::Bootstrap`], carrying no cross-world reuse authority.
210///
211/// `pub(crate)`: the only caller is [`super::install_baseline_providers`],
212/// which supplies `declared_code`/`loaded_code` it computed itself from its
213/// own compiled source (see that function), never a value a downstream
214/// crate passed in. This mechanically checks exactly one fact --
215/// `declared_code == loaded_code` -- and certifies nothing about hidden
216/// state, ambient I/O, or FFI. A caller needing those stronger claims must
217/// obtain them from a real, externally owned checker instead; this route
218/// can never produce them.
219///
220/// The returned [`LiveCheckerOwner`] must be kept alive for as long as the
221/// returned [`LiveCheckerAuthority`]/[`LiveCheckerReceipt`] pair is expected
222/// to verify current: both are weak handles into it.
223///
224/// # Errors
225/// Refuses if `declared_code != loaded_code`, or if constructing the
226/// underlying checker binding, invocation, or receipt fails.
227pub(crate) fn bootstrap_native_source(
228    owner: OwnerBindingId,
229    declared_code: ContentId,
230    loaded_code: &ContentId,
231    dependencies: &ContentId,
232) -> Result<(LiveCheckerOwner, LiveCheckerAuthority, LiveCheckerReceipt), QualificationError> {
233    if &declared_code != loaded_code {
234        return Err(QualificationError::NativeCodeMismatch);
235    }
236    let scope = native_source_review_scope()?;
237    let template = CheckTemplate::new(
238        "sim_incremental_core::projection::admission::bootstrap_native_source".to_owned(),
239        vec![
240            CheckArgument::BindingSlot,
241            CheckArgument::SubjectSlot,
242            CheckArgument::ScopeSlot,
243        ],
244        WorkingDirectoryPolicyId::from_text("projection/bootstrap-cwd-v1")
245            .map_err(checker_error)?,
246        EnvironmentPolicyId::from_text("projection/bootstrap-env-v1").map_err(checker_error)?,
247        OutputShapeId::from_text("projection/bootstrap-result-v1").map_err(checker_error)?,
248    )
249    .map_err(checker_error)?;
250    let binding = CheckerBinding::new(
251        "projection/bootstrap-native-source".to_owned(),
252        owner,
253        "bootstrap_native_source".to_owned(),
254        vec![ConformancePackId::from_text("projection/bootstrap-v1").map_err(checker_error)?],
255        OutputShapeId::from_text("projection/bootstrap-receipt-v1").map_err(checker_error)?,
256        RevocationSourceId::from_text("projection/bootstrap-revocation-v1")
257            .map_err(checker_error)?,
258        CommandId::from_text("projection/bootstrap-validation-v1").map_err(checker_error)?,
259        CommandId::from_text("projection/bootstrap-docs-v1").map_err(checker_error)?,
260        [scope.clone()].into_iter().collect(),
261        template,
262    )
263    .map_err(checker_error)?;
264
265    let subject = reviewed_subject(&declared_code, dependencies)?;
266    let invocation = binding
267        .instantiate(
268            ProofCodeId::from_text(
269                "sim_incremental_core::projection::admission::bootstrap_native_source",
270            )
271            .map_err(checker_error)?,
272            ConformancePackId::from_text("projection/bootstrap-v1").map_err(checker_error)?,
273            subject,
274            scope,
275            reviewed_input_closure(&declared_code, dependencies)?,
276        )
277        .map_err(checker_error)?;
278
279    let generation = content_id_datum(&declared_code)
280        .content_id()
281        .map_err(|error| QualificationError::CanonicalPolicy(error.to_string()))?;
282    let policy = PolicyId::from_text("projection/bootstrap-policy-v1").map_err(checker_error)?;
283    let (checker_owner, issuer) = LiveCheckerOwner::boot(generation, binding, policy);
284    checker_owner
285        .mark_current(&invocation)
286        .map_err(live_error)?;
287    let authority = checker_owner.authority();
288    let receipt = issuer
289        .issue(
290            invocation,
291            CheckerResultId::from_text("projection/bootstrap-passed-v1").map_err(checker_error)?,
292            EvidenceGrade::Bootstrap,
293            EvidenceProvenanceId::from_text("projection/bootstrap-provenance-v1")
294                .map_err(checker_error)?,
295            EvidenceSetId::from_text("projection/bootstrap-support-v1").map_err(checker_error)?,
296        )
297        .map_err(live_error)?;
298    Ok((checker_owner, authority, receipt))
299}
300
301/// The checked-subject identity binding code AND dependency identity
302/// together, each including its full `(algorithm, bytes)` pair -- not bytes
303/// alone, which would let two different algorithms with equal digest bytes
304/// alias to the same subject. A receipt for one (code, dependencies) pair
305/// can never be presented as evidence for a different one.
306fn reviewed_subject(
307    code: &ContentId,
308    dependencies: &ContentId,
309) -> Result<CheckedSubjectId, QualificationError> {
310    CheckedSubjectId::from_fields(vec![
311        (Symbol::new("code"), content_id_datum(code)),
312        (Symbol::new("dependencies"), content_id_datum(dependencies)),
313    ])
314    .map_err(checker_error)
315}
316
317fn reviewed_input_closure(
318    code: &ContentId,
319    dependencies: &ContentId,
320) -> Result<CheckInputClosureId, QualificationError> {
321    CheckInputClosureId::from_fields(vec![
322        (Symbol::new("code"), content_id_datum(code)),
323        (Symbol::new("dependencies"), content_id_datum(dependencies)),
324    ])
325    .map_err(checker_error)
326}
327
328fn checker_error(error: sim_conformance_core::ConformanceError) -> QualificationError {
329    QualificationError::Checker(error.to_string())
330}
331
332fn live_error(error: sim_conformance_core::LiveCheckerError) -> QualificationError {
333    QualificationError::CheckerUnavailable(error.to_string())
334}
335
336pub(crate) fn policy_id(policy: &ProjectorPolicy) -> Result<ContentId, QualificationError> {
337    let input_facts = policy
338        .reads
339        .facts()
340        .map(|fact| Datum::String(fact.as_str().to_owned()))
341        .collect();
342    let imports = policy
343        .imports
344        .imports
345        .iter()
346        .cloned()
347        .map(Datum::String)
348        .collect();
349    let fields = vec![
350        (
351            Symbol::new("input-shape"),
352            content_id_datum(&policy.input_shape),
353        ),
354        (Symbol::new("reads"), Datum::Vector(input_facts)),
355        (Symbol::new("imports"), Datum::Vector(imports)),
356        (
357            Symbol::new("execution"),
358            Datum::Node {
359                tag: Symbol::qualified("projection", "execution-semantics-v1"),
360                fields: vec![
361                    (
362                        Symbol::new("id"),
363                        Datum::String(policy.execution.id.clone()),
364                    ),
365                    (
366                        Symbol::new("canonical-nan"),
367                        Datum::Bool(policy.execution.canonical_nan),
368                    ),
369                    (
370                        Symbol::new("canonical-collections"),
371                        Datum::Bool(policy.execution.canonical_collections),
372                    ),
373                    (
374                        Symbol::new("fresh-instance"),
375                        Datum::Bool(policy.execution.fresh_instance),
376                    ),
377                ],
378            },
379        ),
380        (
381            Symbol::new("max-inputs"),
382            number_datum(policy.budgets.max_inputs as u64),
383        ),
384        (
385            Symbol::new("max-output-bytes"),
386            number_datum(policy.budgets.max_output_bytes as u64),
387        ),
388        (
389            Symbol::new("max-fuel"),
390            number_datum(policy.budgets.max_fuel),
391        ),
392        (
393            Symbol::new("max-memory-bytes"),
394            number_datum(policy.budgets.max_memory_bytes as u64),
395        ),
396        (
397            Symbol::new("requires-confinement"),
398            Datum::Bool(policy.requires_confinement),
399        ),
400    ];
401    Datum::Node {
402        tag: Symbol::qualified("projection", "projector-policy-v1"),
403        fields,
404    }
405    .content_id()
406    .map_err(|error| QualificationError::CanonicalPolicy(error.to_string()))
407}
408
409pub(crate) fn content_id_datum(id: &ContentId) -> Datum {
410    Datum::Node {
411        tag: Symbol::qualified("core", "content-id-v1"),
412        fields: vec![
413            (
414                Symbol::new("algorithm"),
415                Datum::Symbol(id.algorithm.clone()),
416            ),
417            (Symbol::new("bytes"), Datum::Bytes(id.bytes.to_vec())),
418        ],
419    }
420}
421
422fn number_datum(value: u64) -> Datum {
423    Datum::Number(NumberLiteral {
424        domain: Symbol::qualified("projection", "u64"),
425        canonical: value.to_string(),
426    })
427}
428
429// Only called from `closed_wasm`, itself currently unexercised; see its doc comment.
430#[allow(dead_code)]
431fn is_forbidden_import(import: &str) -> bool {
432    const FORBIDDEN: &[&str] = &[
433        "wasi",
434        "filesystem",
435        "path_",
436        "proc",
437        "environment",
438        "environ",
439        "clock",
440        "time",
441        "random",
442        "network",
443        "socket",
444        "thread",
445        "shared-memory",
446    ];
447    let lower = import.to_ascii_lowercase();
448    FORBIDDEN.iter().any(|needle| lower.contains(needle))
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::projection::{DeclaredInputSelector, ExecutionSemantics, ProjectionBudget};
455
456    fn owner() -> OwnerBindingId {
457        OwnerBindingId::from_text("projection/test-owner").unwrap()
458    }
459
460    fn code(seed: u8) -> ContentId {
461        ContentId::from_bytes(Symbol::qualified("core", "sha256"), [seed; 32])
462    }
463
464    fn sample_policy() -> ProjectorPolicy {
465        ProjectorPolicy {
466            input_shape: code(200),
467            reads: DeclaredInputSelector::new([]),
468            imports: DeterministicImportManifest::default(),
469            execution: ExecutionSemantics {
470                id: "projection/native-v1".to_owned(),
471                canonical_nan: true,
472                canonical_collections: true,
473                fresh_instance: true,
474            },
475            budgets: ProjectionBudget {
476                max_inputs: 16,
477                max_output_bytes: 4096,
478                max_fuel: 1_000_000,
479                max_memory_bytes: 1024 * 1024,
480            },
481            requires_confinement: false,
482        }
483    }
484
485    #[test]
486    fn bootstrap_native_source_refuses_a_declared_code_mismatch() {
487        let result = bootstrap_native_source(owner(), code(1), &code(2), &code(3));
488        assert_eq!(result.unwrap_err(), QualificationError::NativeCodeMismatch);
489    }
490
491    #[test]
492    fn bootstrap_native_source_issues_a_current_receipt_for_matching_code() {
493        let (_checker_owner, authority, receipt) =
494            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
495        assert!(receipt.verify_current(&authority).is_ok());
496        assert_eq!(receipt.receipt().grade(), EvidenceGrade::Bootstrap);
497    }
498
499    #[test]
500    fn trusted_native_refuses_once_the_checker_owner_is_dropped() {
501        let policy = sample_policy();
502        let (checker_owner, authority, receipt) =
503            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
504        drop(checker_owner);
505        let evidence = NativeSourceEvidence {
506            code: code(1),
507            dependencies: code(3),
508            receipt,
509        };
510        let result = ProjectorQualificationVerifier::trusted_native(&policy, evidence, &authority);
511        assert!(matches!(
512            result,
513            Err(QualificationError::CheckerUnavailable(_))
514        ));
515    }
516
517    #[test]
518    fn trusted_native_refuses_a_receipt_whose_code_does_not_match_the_declared_code() {
519        let policy = sample_policy();
520        let (checker_owner, authority, receipt) =
521            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
522        let evidence = NativeSourceEvidence {
523            code: code(9),
524            dependencies: code(3),
525            receipt,
526        };
527        let result = ProjectorQualificationVerifier::trusted_native(&policy, evidence, &authority);
528        assert_eq!(result.unwrap_err(), QualificationError::WrongCheckerSubject);
529        drop(checker_owner);
530    }
531
532    #[test]
533    fn trusted_native_refuses_a_receipt_whose_dependencies_do_not_match_the_declared_dependencies()
534    {
535        let policy = sample_policy();
536        let (checker_owner, authority, receipt) =
537            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
538        let evidence = NativeSourceEvidence {
539            code: code(1),
540            dependencies: code(99),
541            receipt,
542        };
543        let result = ProjectorQualificationVerifier::trusted_native(&policy, evidence, &authority);
544        assert_eq!(result.unwrap_err(), QualificationError::WrongCheckerSubject);
545        drop(checker_owner);
546    }
547
548    #[test]
549    fn trusted_native_refuses_identical_digest_bytes_under_a_different_algorithm() {
550        let policy = sample_policy();
551        let same_bytes_other_algorithm =
552            ContentId::from_bytes(Symbol::qualified("core", "blake3"), [1; 32]);
553        let (checker_owner, authority, receipt) =
554            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
555        let evidence = NativeSourceEvidence {
556            code: same_bytes_other_algorithm,
557            dependencies: code(3),
558            receipt,
559        };
560        let result = ProjectorQualificationVerifier::trusted_native(&policy, evidence, &authority);
561        assert_eq!(result.unwrap_err(), QualificationError::WrongCheckerSubject);
562        drop(checker_owner);
563    }
564
565    #[test]
566    fn trusted_native_admits_a_genuinely_current_bootstrap_receipt() {
567        let policy = sample_policy();
568        let (checker_owner, authority, receipt) =
569            bootstrap_native_source(owner(), code(1), &code(1), &code(3)).unwrap();
570        let evidence = NativeSourceEvidence {
571            code: code(1),
572            dependencies: code(3),
573            receipt,
574        };
575        let qualification =
576            ProjectorQualificationVerifier::trusted_native(&policy, evidence, &authority).unwrap();
577        assert!(qualification.is_trusted_native());
578        drop(checker_owner);
579    }
580
581    #[test]
582    fn a_native_proc_read_mutant_cannot_self_mint_a_qualification() {
583        // The exact attack the independent review found: a caller boots its
584        // own owner and passes the same identifier as both "declared" and
585        // "loaded". This still succeeds at the `bootstrap_native_source`
586        // layer (it is honest about proving only that equality) -- the real
587        // fix is that this whole module is `pub(crate)`, so no code outside
588        // `sim-incremental-core` can reach any of these functions at all.
589        // This test documents and locks in that boundary: if `admission`
590        // were ever accidentally re-exported from `mod.rs`, every other
591        // test in this file would still compile and pass, silently losing
592        // the actual protection. There is no runtime assertion possible for
593        // "this item is not `pub`"; the guarantee is enforced by the
594        // compiler via visibility, checked by `mod.rs` not re-exporting
595        // `bootstrap_native_source`, `NativeSourceEvidence`,
596        // `ProjectorQualificationVerifier`, or `trusted_native`.
597        let owner = OwnerBindingId::from_text("attacker/self-issued").unwrap();
598        let mutant = code(77);
599        let result = bootstrap_native_source(owner, mutant.clone(), &mutant, &code(3));
600        assert!(result.is_ok(), "the mechanical check itself is honest");
601    }
602}