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#[derive(Clone, Debug)]
27pub(crate) struct NativeSourceEvidence {
28 pub(crate) code: ContentId,
30 pub(crate) dependencies: ContentId,
32 pub(crate) receipt: LiveCheckerReceipt,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct ClosedWasmEvidence {
39 pub module: ContentId,
41 pub imports: DeterministicImportManifest,
43 pub runtime: QualifiedRuntime,
45 pub admission: ContentId,
47 pub import_manifest_complete: bool,
49 pub start_behavior_checked: bool,
51 pub budgets_enforced: bool,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum QualificationError {
58 NativeCodeMismatch,
60 WrongCheckerScope,
62 WrongCheckerSubject,
65 InsufficientEvidenceGrade,
67 CheckerUnavailable(String),
69 Checker(String),
71 IncompleteImportManifest,
73 ImportManifestMismatch,
75 ForbiddenImport(String),
77 StartBehaviorUnchecked,
79 RuntimeSemanticsUnqualified,
81 RuntimeBudgetsUnenforced,
83 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#[derive(Clone, Copy, Debug, Default)]
104pub(crate) struct ProjectorQualificationVerifier;
105
106impl ProjectorQualificationVerifier {
107 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 #[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
197fn native_source_review_scope() -> Result<CheckScopeId, QualificationError> {
202 CheckScopeId::from_text("projection/native-source-review-v1").map_err(checker_error)
203}
204
205pub(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
301fn 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#[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 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}