cellos_core/authority/validator.rs
1//! Typed authority validator — the three variants and their validation logic.
2//!
3//! See parent module [`crate::authority`] for the doctrine narrative and
4//! status notes. This file holds the mechanical truth.
5//!
6//! # Mechanical separation
7//!
8//! [`ObservedAuthority`], [`ProvenAuthority`], and [`ImposedAuthority`] are
9//! three **distinct, non-convertible** structs. There are no `From`/`Into`
10//! impls between them, no shared trait, no common base type. Merging is a
11//! compile error.
12//!
13//! Each variant carries its own validated [`AuthorityDerivation`] (the ADG
14//! sub-object). Construction goes through the type's `try_new` method which
15//! invokes [`AuthorityDerivation::validate`] plus type-class compatibility
16//! checks. Once constructed, fields are immutable (no public setters; ADG
17//! and output are exposed read-only).
18
19use std::collections::BTreeSet;
20
21use serde::{Deserialize, Serialize};
22
23// ---------------------------------------------------------------------------
24// Enums shared across the three typed-authority variants
25// ---------------------------------------------------------------------------
26
27/// Capability-tier and confidence semantics from
28/// [docs/authority-model.md §5b](../../../../docs/authority-model.md).
29///
30/// `STATISTICALLY_BOUNDED` is reserved per §5a but not yet emitted by any
31/// rule-class; it does not appear in the canonical mapping.
32///
33/// `DECLARED` is the [ADR-0006] `DeclaredAuthority` variant — guest-agent
34/// declarations forwarded over the host-bound vsock channel. It is the
35/// **floor** of the precedence ladder: any co-occurring class outranks it.
36///
37/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
40pub enum EpistemicStatus {
41 /// Raw signal seen on the wire; no structural cross-check applied.
42 DirectObservation,
43 /// Two or more signals align in the way the protocol mandates.
44 StructurallyBound,
45 /// Verified via signature or chain-of-trust the workload cannot forge.
46 CryptographicProof,
47 /// A structural signal we expected to see was deliberately encrypted/withheld.
48 Opaque,
49 /// Authority asserted via in-line interception (Stack 3 / M2 quarantine only).
50 Imposed,
51 /// Guest-agent declaration forwarded over a host-bound vsock channel
52 /// (ADR-0006). Strictly weaker than [`Self::DirectObservation`] — any
53 /// co-occurring class outranks it in the canonical mapping.
54 Declared,
55}
56
57/// Rule-class drives [`EpistemicStatus`]; this is the canonical enum from
58/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
60#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
61pub enum RuleClass {
62 /// Lone-signal sighting. Tier ceiling 2.
63 RawObservation,
64 /// Two protocol-mandated signals align. Tier ceiling 3.
65 StructuralAlignment,
66 /// Cryptographic verification (JWS, DANE-TLSA, SVID). Tier ceiling 4.
67 CryptographicProof,
68 /// Expected signal deliberately withheld (ECH, encrypted inner SNI).
69 /// Tier ceiling 1.
70 Opacity,
71 /// In-line termination observed plaintext post-MITM. Tier ceiling 4
72 /// (with named structural residuals; emission must carry
73 /// `non_authoritative_enforcement: true`).
74 ImposedInterception,
75 /// Guest-agent declaration forwarded over the per-cell vsock channel
76 /// (ADR-0006). Tier ceiling 1; floor of the precedence ladder. Backs
77 /// [`DeclaredAuthority`] exclusively.
78 GuestAgentDeclaration,
79}
80
81/// Named rules per
82/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
83/// Adding a new rule requires updating that document AND
84/// [`canonical_class_for_rule`].
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
87pub enum Rule {
88 /// SNI parsed from ClientHello, no cross-check.
89 RawSniObserved,
90 /// Host header parsed, no cross-check.
91 RawHostHeaderObserved,
92 /// h2 `:authority` parsed.
93 RawH2AuthorityObserved,
94 /// SNI value byte-equals Host value (post-IDN, post-port-strip).
95 SniHostMatch,
96 /// h2 `:authority` matches HTTP/1.x Host on the same flow.
97 H2AuthorityHostMatch,
98 /// SNI matches a declared `cdnAuthority.providers[].hostnamePattern`.
99 SniCdnProviderMatch,
100 /// Workload-signed authority claim verified against cell-ephemeral key.
101 JwsSignatureVerified,
102 /// Upstream cert chain matches DNSSEC-validated TLSA record.
103 DaneTlsaBound,
104 /// SPIFFE SVID chain verified (future).
105 SvidChainVerified,
106 /// `encrypted_client_hello` extension present.
107 EchDetected,
108 /// Successor to ECH_DETECTED when more state available.
109 EncryptedInnerSni,
110 /// Stack 3 / M2 only.
111 MitmHandshakeTerminated,
112 /// In-VM agent declared a process spawn (ADR-0006).
113 GuestProcSpawnObserved,
114 /// In-VM agent declared a process exit (ADR-0006).
115 GuestProcExitObserved,
116 /// In-VM agent declared an inotify watch firing (ADR-0006).
117 GuestFsInotifyFired,
118 /// In-VM agent declared a capability denial (ADR-0006).
119 GuestCapDenialObserved,
120 /// In-VM agent declared a `connect()` attempt (ADR-0006).
121 GuestNetConnectAttempted,
122}
123
124/// Binding status per
125/// [docs/authority-model.md §6](../../../../docs/authority-model.md).
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
127#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
128pub enum BindingStatus {
129 /// At least one structural-alignment or cryptographic-proof rule fired
130 /// AND no opacity/imposed rule fired.
131 Bound,
132 /// Any opacity rule fired, OR no alignment/proof rule fired against a
133 /// multi-input set.
134 Unknown,
135 /// Imposed-interception rule fired (Stack 3 only).
136 ImposedBound,
137 /// Single-input emission where binding is undefined.
138 NotApplicable,
139}
140
141/// Canonical input types — a strict subset of those listed in the ADG doc.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
143#[serde(rename_all = "snake_case")]
144pub enum AuthorityInputType {
145 /// Bytes from ClientHello SNI extension.
146 Sni,
147 /// Bytes from HTTP/1.x `Host:` header.
148 HostHeader,
149 /// Bytes from h2 HEADERS `:authority` pseudo-header.
150 H2Authority,
151 /// Subject Alternative Name from a verified upstream cert.
152 TlsCertSan,
153 /// DNSSEC-validated TLSA record content.
154 DaneTlsaRecord,
155 /// Verified body of a workload-signed JWS authority claim.
156 JwsPayload,
157 /// DNSSEC-validated A/AAAA answer (links to upstream Phase 3h emission).
158 DnssecValidatedARecord,
159 /// Stack 3 / `IMPOSED` only; bytes seen post-termination.
160 MitmTerminatedObservation,
161}
162
163// ---------------------------------------------------------------------------
164// ADG sub-object types
165// ---------------------------------------------------------------------------
166
167/// One evidence atom consumed by a derivation. Mirrors the ADG `inputs[]`
168/// schema; see [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
169#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct AuthorityInput {
171 /// Canonical input type.
172 #[serde(rename = "type")]
173 pub input_type: AuthorityInputType,
174 /// Observed value (redacted per `RedactingEventSink` policy if applicable).
175 pub value: String,
176 /// Coverage score for this input alone, ∈ \[0, 1\].
177 pub confidence: f64,
178 /// Optional UUID — when this input itself was the output of a prior
179 /// emission, links the chain.
180 #[serde(skip_serializing_if = "Option::is_none", default)]
181 pub source_event_id: Option<String>,
182}
183
184/// One rule that fired during derivation.
185#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
186pub struct AppliedRule {
187 /// Named rule.
188 pub rule: Rule,
189 /// Class — must match [`canonical_class_for_rule`] for `rule`.
190 pub class: RuleClass,
191}
192
193/// The derived `(tier, confidence, epistemic_status, binding_status)` tuple.
194#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
195pub struct AuthorityOutput {
196 /// Capability tier, ∈ \[0, 4\].
197 pub tier: u8,
198 /// Coverage-derived confidence, ∈ \[0, 1\].
199 pub confidence: f64,
200 /// Deterministically derived from the rule-class set.
201 pub epistemic_status: EpistemicStatus,
202 /// Per [`BindingStatus`] semantics.
203 pub binding_status: BindingStatus,
204}
205
206/// The complete ADG sub-object. Serializable for downstream events.
207///
208/// Construction does not validate; callers must invoke [`Self::validate`] (or
209/// go through one of the typed-authority `try_new` constructors which call
210/// `validate` for them).
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212pub struct AuthorityDerivation {
213 /// Inputs consumed by the derivation. Must be non-empty.
214 pub inputs: Vec<AuthorityInput>,
215 /// Rules applied to those inputs. Must be non-empty.
216 pub rules_applied: Vec<AppliedRule>,
217 /// The derivation's output tuple.
218 pub output: AuthorityOutput,
219}
220
221// ---------------------------------------------------------------------------
222// Validation errors
223// ---------------------------------------------------------------------------
224
225/// Errors the validator can return. Each variant names the specific §9
226/// invariant it violates so debug logs and tests can distinguish them.
227#[derive(Debug, Clone, PartialEq, thiserror::Error)]
228pub enum ValidationError {
229 /// `inputs` is empty (Property 4 — ADG presence).
230 #[error("authority derivation has no inputs")]
231 NoInputs,
232 /// `rules_applied` is empty (Property 4 — ADG presence).
233 #[error("authority derivation has no applied rules")]
234 NoRulesApplied,
235 /// An input's `confidence` is outside \[0, 1\].
236 #[error("input confidence {0} out of range [0, 1]")]
237 InputConfidenceOutOfRange(f64),
238 /// An input's `confidence` is NaN.
239 #[error("input confidence is NaN")]
240 InputConfidenceNaN,
241 /// `output.confidence` is outside \[0, 1\] or NaN.
242 #[error("output confidence {0} out of range [0, 1] or NaN")]
243 OutputConfidenceInvalid(f64),
244 /// `output.tier` is outside \[0, 4\].
245 #[error("output tier {0} out of range [0, 4]")]
246 TierOutOfRange(u8),
247 /// Property 1 — `output.confidence > max(inputs.confidence)`.
248 #[error("confidence inflation: output {output} exceeds max input {max_input}")]
249 ConfidenceInflation {
250 /// Declared output confidence.
251 output: f64,
252 /// Maximum input confidence observed.
253 max_input: f64,
254 },
255 /// Property 2 — `output.tier > min(class_tier_ceiling)`.
256 #[error("tier inflation: output tier {output} exceeds ceiling {ceiling} (limited by {limiting_class:?})")]
257 TierInflation {
258 /// Declared output tier.
259 output: u8,
260 /// Maximum tier permitted by the rule-class set.
261 ceiling: u8,
262 /// The class whose ceiling was violated.
263 limiting_class: RuleClass,
264 },
265 /// Property 3 — `output.epistemic_status` is not the deterministic image
266 /// of the rule-class set.
267 #[error("epistemic status mismatch: declared {declared:?}, expected {expected:?}")]
268 EpistemicMismatch {
269 /// Declared `epistemic_status`.
270 declared: EpistemicStatus,
271 /// Status the canonical mapping demands.
272 expected: EpistemicStatus,
273 },
274 /// Property 5 — a rule's declared class does not match the canonical
275 /// `rule -> class` mapping.
276 #[error("rule {rule:?} declared class {declared:?}, canonical class is {canonical:?}")]
277 RuleClassMismatch {
278 /// Rule whose class was declared.
279 rule: Rule,
280 /// Class the caller declared.
281 declared: RuleClass,
282 /// Class the canonical mapping demands.
283 canonical: RuleClass,
284 },
285 /// `binding_status` does not match the rules' implications.
286 #[error("binding status mismatch: declared {declared:?}, expected {expected:?}")]
287 BindingStatusMismatch {
288 /// Declared `binding_status`.
289 declared: BindingStatus,
290 /// Status the canonical mapping demands.
291 expected: BindingStatus,
292 },
293 /// Type-class compatibility violation — the typed authority's permitted
294 /// rule-class envelope was exceeded.
295 #[error(
296 "type-class incompatible: {type_name} requires rule-classes within {permitted:?}, got {rejected:?}"
297 )]
298 TypeClassIncompatible {
299 /// `"ObservedAuthority"` / `"ProvenAuthority"` / `"ImposedAuthority"`.
300 type_name: &'static str,
301 /// The permitted set for this type.
302 permitted: &'static [RuleClass],
303 /// The class that triggered the rejection.
304 rejected: RuleClass,
305 },
306 /// `ProvenAuthority` was constructed without a verified-signature artifact.
307 #[error("ProvenAuthority requires a verified-signature artifact")]
308 ProvenAuthorityMissingArtifact,
309 /// `ImposedAuthority` requires the IMPOSED_INTERCEPTION rule-class to
310 /// have fired in its derivation.
311 #[error("ImposedAuthority requires the IMPOSED_INTERCEPTION rule-class in its derivation")]
312 ImposedAuthorityMissingInterception,
313 /// The empty-class-set case from the ADG mapping table.
314 #[error("rule-class set is empty after deduplication; refusing emission")]
315 EmptyClassSet,
316}
317
318// ---------------------------------------------------------------------------
319// Canonical mappings
320// ---------------------------------------------------------------------------
321
322/// The canonical rule-name → rule-class mapping. Mirrors the table in
323/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md).
324///
325/// **This function is the source of truth for Property 5 (rule-class
326/// consistency).** Adding a new rule requires:
327///
328/// 1. Adding the variant to [`Rule`].
329/// 2. Adding the entry here.
330/// 3. Updating the ADG doc.
331/// 4. Updating any tests that enumerate `Rule` exhaustively.
332///
333/// The compiler's exhaustiveness check catches step 2 if step 1 lands first.
334#[must_use]
335pub const fn canonical_class_for_rule(rule: Rule) -> RuleClass {
336 match rule {
337 Rule::RawSniObserved | Rule::RawHostHeaderObserved | Rule::RawH2AuthorityObserved => {
338 RuleClass::RawObservation
339 }
340 Rule::SniHostMatch | Rule::H2AuthorityHostMatch | Rule::SniCdnProviderMatch => {
341 RuleClass::StructuralAlignment
342 }
343 Rule::JwsSignatureVerified | Rule::DaneTlsaBound | Rule::SvidChainVerified => {
344 RuleClass::CryptographicProof
345 }
346 Rule::EchDetected | Rule::EncryptedInnerSni => RuleClass::Opacity,
347 Rule::MitmHandshakeTerminated => RuleClass::ImposedInterception,
348 Rule::GuestProcSpawnObserved
349 | Rule::GuestProcExitObserved
350 | Rule::GuestFsInotifyFired
351 | Rule::GuestCapDenialObserved
352 | Rule::GuestNetConnectAttempted => RuleClass::GuestAgentDeclaration,
353 }
354}
355
356/// Tier ceiling per rule-class — the canonical mapping. The ADG doc and
357/// [docs/authority-model.md §5b](../../../../docs/authority-model.md) pin
358/// these numbers; do not change them without an ADR amendment.
359#[must_use]
360pub const fn max_tier_for_class(class: RuleClass) -> u8 {
361 match class {
362 RuleClass::RawObservation => 2,
363 RuleClass::StructuralAlignment => 3,
364 RuleClass::CryptographicProof => 4,
365 RuleClass::Opacity => 1,
366 RuleClass::ImposedInterception => 4,
367 RuleClass::GuestAgentDeclaration => 1,
368 }
369}
370
371/// The deterministic class-set → epistemic-status mapping per
372/// [docs/authority-derivation-graph.md](../../../../docs/authority-derivation-graph.md):
373///
374/// ```text
375/// {ImposedInterception, ...} → Imposed
376/// {Opacity, ...except Imposed} → Opaque
377/// {CryptographicProof, ...except above} → CryptographicProof
378/// {StructuralAlignment, ...except above} → StructurallyBound
379/// {RawObservation, ...except above} → DirectObservation
380/// {GuestAgentDeclaration only} → Declared (ADR-0006)
381/// empty → returns None
382/// ```
383///
384/// Per [ADR-0006] §2 the precedence ladder is
385/// `Imposed > Opaque > CryptographicProof > StructurallyBound > DirectObservation > Declared`
386/// — `Declared` is the floor, so any host-side class co-occurring with
387/// `GuestAgentDeclaration` outranks it.
388///
389/// Returns `None` for the empty set; callers translate to
390/// [`ValidationError::EmptyClassSet`].
391///
392/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
393#[must_use]
394pub fn epistemic_for_class_set(classes: &BTreeSet<RuleClass>) -> Option<EpistemicStatus> {
395 if classes.is_empty() {
396 return None;
397 }
398 if classes.contains(&RuleClass::ImposedInterception) {
399 return Some(EpistemicStatus::Imposed);
400 }
401 if classes.contains(&RuleClass::Opacity) {
402 return Some(EpistemicStatus::Opaque);
403 }
404 if classes.contains(&RuleClass::CryptographicProof) {
405 return Some(EpistemicStatus::CryptographicProof);
406 }
407 if classes.contains(&RuleClass::StructuralAlignment) {
408 return Some(EpistemicStatus::StructurallyBound);
409 }
410 if classes.contains(&RuleClass::RawObservation) {
411 return Some(EpistemicStatus::DirectObservation);
412 }
413 // Floor of the precedence ladder — only GuestAgentDeclaration left.
414 if classes.contains(&RuleClass::GuestAgentDeclaration) {
415 return Some(EpistemicStatus::Declared);
416 }
417 // Unreachable given enum exhaustiveness above, but kept defensive.
418 None
419}
420
421/// Compute the expected [`BindingStatus`] given the inputs and rule-class
422/// set, per [`BindingStatus`] doc-comments.
423fn expected_binding_status(
424 inputs: &[AuthorityInput],
425 classes: &BTreeSet<RuleClass>,
426) -> BindingStatus {
427 if classes.contains(&RuleClass::ImposedInterception) {
428 return BindingStatus::ImposedBound;
429 }
430 let opacity_or_imposed = classes.contains(&RuleClass::Opacity);
431 let alignment_or_proof = classes.contains(&RuleClass::StructuralAlignment)
432 || classes.contains(&RuleClass::CryptographicProof);
433 if opacity_or_imposed {
434 return BindingStatus::Unknown;
435 }
436 if alignment_or_proof {
437 return BindingStatus::Bound;
438 }
439 if inputs.len() <= 1 {
440 // single-input derivation; binding is undefined.
441 BindingStatus::NotApplicable
442 } else {
443 // multi-input set with no alignment/proof — explicit UNKNOWN per ADG §4.
444 BindingStatus::Unknown
445 }
446}
447
448impl AuthorityDerivation {
449 /// Run all six §9 invariants over this derivation. Returns `Ok(())` on
450 /// success or the first violation as a [`ValidationError`].
451 ///
452 /// This is a pure function — no I/O, no allocations beyond the
453 /// rule-class set deduplication.
454 pub fn validate(&self) -> Result<(), ValidationError> {
455 // Property 4 — ADG presence
456 if self.inputs.is_empty() {
457 return Err(ValidationError::NoInputs);
458 }
459 if self.rules_applied.is_empty() {
460 return Err(ValidationError::NoRulesApplied);
461 }
462
463 // Range checks on numeric outputs and inputs
464 for input in &self.inputs {
465 if input.confidence.is_nan() {
466 return Err(ValidationError::InputConfidenceNaN);
467 }
468 if !(0.0..=1.0).contains(&input.confidence) {
469 return Err(ValidationError::InputConfidenceOutOfRange(input.confidence));
470 }
471 }
472 if self.output.confidence.is_nan() || !(0.0..=1.0).contains(&self.output.confidence) {
473 return Err(ValidationError::OutputConfidenceInvalid(
474 self.output.confidence,
475 ));
476 }
477 if self.output.tier > 4 {
478 return Err(ValidationError::TierOutOfRange(self.output.tier));
479 }
480
481 // Property 5 — rule-name → class consistency
482 for ar in &self.rules_applied {
483 let canonical = canonical_class_for_rule(ar.rule);
484 if canonical != ar.class {
485 return Err(ValidationError::RuleClassMismatch {
486 rule: ar.rule,
487 declared: ar.class,
488 canonical,
489 });
490 }
491 }
492
493 let class_set: BTreeSet<RuleClass> = self.rules_applied.iter().map(|ar| ar.class).collect();
494
495 // Property 3 — epistemic determinism
496 let expected_epistemic =
497 epistemic_for_class_set(&class_set).ok_or(ValidationError::EmptyClassSet)?;
498 if self.output.epistemic_status != expected_epistemic {
499 return Err(ValidationError::EpistemicMismatch {
500 declared: self.output.epistemic_status,
501 expected: expected_epistemic,
502 });
503 }
504
505 // Property 1 — confidence non-inflation
506 let max_input = self
507 .inputs
508 .iter()
509 .map(|i| i.confidence)
510 .fold(f64::NEG_INFINITY, f64::max);
511 if self.output.confidence > max_input {
512 return Err(ValidationError::ConfidenceInflation {
513 output: self.output.confidence,
514 max_input,
515 });
516 }
517
518 // Property 2 — tier ceiling
519 let (ceiling, limiting_class) = class_set
520 .iter()
521 .map(|c| (max_tier_for_class(*c), *c))
522 .min_by_key(|(t, _)| *t)
523 .expect("class_set non-empty (checked above)");
524 if self.output.tier > ceiling {
525 return Err(ValidationError::TierInflation {
526 output: self.output.tier,
527 ceiling,
528 limiting_class,
529 });
530 }
531
532 // Binding status determinism
533 let expected_binding = expected_binding_status(&self.inputs, &class_set);
534 if self.output.binding_status != expected_binding {
535 return Err(ValidationError::BindingStatusMismatch {
536 declared: self.output.binding_status,
537 expected: expected_binding,
538 });
539 }
540
541 Ok(())
542 }
543
544 /// Returns the deduplicated rule-class set this derivation applied.
545 /// Exposed for tests and downstream consumers (e.g. taudit) that want to
546 /// reason about class composition without re-walking `rules_applied`.
547 #[must_use]
548 pub fn class_set(&self) -> BTreeSet<RuleClass> {
549 self.rules_applied.iter().map(|ar| ar.class).collect()
550 }
551}
552
553// ---------------------------------------------------------------------------
554// Typed authority — the three variants
555// ---------------------------------------------------------------------------
556
557/// Authority derived from CellOS's own observation of bytes inside the
558/// cell's network namespace.
559///
560/// **Permitted rule-classes:** `RawObservation`, `StructuralAlignment`,
561/// `Opacity`. Tier ceiling: 2 (or up to 3 via `StructuralAlignment`, capped
562/// at 2 by the type-class envelope per Authority Model §14.1).
563///
564/// **Forbidden rule-classes:** `CryptographicProof` (no signature was
565/// verified), `ImposedInterception` (no termination occurred),
566/// `GuestAgentDeclaration` (reserved for `DeclaredAuthority`).
567///
568/// **Type-system discipline:** No `From`/`Into` impls to or from any other
569/// authority variant. The only public constructor is [`Self::try_new`].
570#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
571pub struct ObservedAuthority {
572 derivation: AuthorityDerivation,
573}
574
575impl ObservedAuthority {
576 /// Permitted rule-classes for this variant. Public so tests and
577 /// downstream consumers can introspect the envelope.
578 pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
579 RuleClass::RawObservation,
580 RuleClass::StructuralAlignment,
581 RuleClass::Opacity,
582 ];
583
584 /// Construct an `ObservedAuthority` from a validated derivation.
585 ///
586 /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
587 /// check (no `CryptographicProof`, no `ImposedInterception`, no
588 /// `GuestAgentDeclaration`).
589 ///
590 /// # Errors
591 ///
592 /// Returns the first [`ValidationError`] encountered.
593 pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
594 derivation.validate()?;
595 for class in derivation.class_set() {
596 if !Self::PERMITTED_CLASSES.contains(&class) {
597 return Err(ValidationError::TypeClassIncompatible {
598 type_name: "ObservedAuthority",
599 permitted: Self::PERMITTED_CLASSES,
600 rejected: class,
601 });
602 }
603 }
604 Ok(Self { derivation })
605 }
606
607 /// Read-only access to the validated derivation.
608 #[must_use]
609 pub fn derivation(&self) -> &AuthorityDerivation {
610 &self.derivation
611 }
612
613 /// Convenience accessor for the output tuple.
614 #[must_use]
615 pub fn output(&self) -> AuthorityOutput {
616 self.derivation.output
617 }
618}
619
620/// Verified-signature artifact backing a [`ProvenAuthority`]. Storing the
621/// artifact alongside the derivation prevents `ProvenAuthority` from being
622/// constructed from a derivation that *claims* `CryptographicProof` without
623/// actually carrying the proof material.
624///
625/// The verifier path that produced this artifact is upstream of
626/// `cellos-core` (e.g., supervisor's JWS verifier, DNSSEC validator); this
627/// crate holds no signing/verification keys per D11.
628#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
629pub enum ProvenAuthorityArtifact {
630 /// JWS authority claim verified against a workload-bound key.
631 JwsVerified {
632 /// `kid` of the key that verified the signature.
633 kid: String,
634 },
635 /// Upstream cert chain matched a DNSSEC-validated TLSA record.
636 DaneTlsaBound {
637 /// TLSA record selector / matching-type the chain matched.
638 tlsa_record_id: String,
639 },
640 /// SPIFFE SVID chain verified.
641 SvidChainVerified {
642 /// SPIFFE ID this chain bound to.
643 spiffe_id: String,
644 },
645}
646
647/// Authority derived from cryptographic verification of a claim CellOS did
648/// not author.
649///
650/// **Permitted rule-classes:** `CryptographicProof` (REQUIRED — at least one
651/// rule from this class must fire). `StructuralAlignment` and
652/// `RawObservation` may co-occur (e.g. SNI parsed alongside a verified JWS),
653/// but `Opacity` and `ImposedInterception` are forbidden.
654///
655/// **Forbidden:** `Opacity` (would invalidate the proof claim);
656/// `ImposedInterception` (different stack); `GuestAgentDeclaration`.
657///
658/// **Constructor discipline:** [`Self::try_new`] requires a
659/// [`ProvenAuthorityArtifact`]. Without the artifact, construction fails —
660/// preventing a non-cryptographic emission from minting a `ProvenAuthority`
661/// even if its derivation declares `CryptographicProof`.
662#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
663pub struct ProvenAuthority {
664 derivation: AuthorityDerivation,
665 artifact: ProvenAuthorityArtifact,
666}
667
668impl ProvenAuthority {
669 /// Permitted rule-classes for this variant.
670 pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
671 RuleClass::CryptographicProof,
672 RuleClass::StructuralAlignment,
673 RuleClass::RawObservation,
674 ];
675
676 /// Construct a `ProvenAuthority`.
677 ///
678 /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
679 /// check, and additionally requires:
680 ///
681 /// 1. At least one rule with class `CryptographicProof` (otherwise we
682 /// have nothing to prove).
683 /// 2. A non-trivial [`ProvenAuthorityArtifact`].
684 ///
685 /// # Errors
686 ///
687 /// Returns the first [`ValidationError`] encountered.
688 pub fn try_new(
689 derivation: AuthorityDerivation,
690 artifact: ProvenAuthorityArtifact,
691 ) -> Result<Self, ValidationError> {
692 derivation.validate()?;
693 let class_set = derivation.class_set();
694 if !class_set.contains(&RuleClass::CryptographicProof) {
695 return Err(ValidationError::ProvenAuthorityMissingArtifact);
696 }
697 for class in &class_set {
698 if !Self::PERMITTED_CLASSES.contains(class) {
699 return Err(ValidationError::TypeClassIncompatible {
700 type_name: "ProvenAuthority",
701 permitted: Self::PERMITTED_CLASSES,
702 rejected: *class,
703 });
704 }
705 }
706 Ok(Self {
707 derivation,
708 artifact,
709 })
710 }
711
712 /// Read-only access to the derivation.
713 #[must_use]
714 pub fn derivation(&self) -> &AuthorityDerivation {
715 &self.derivation
716 }
717
718 /// Read-only access to the verification artifact.
719 #[must_use]
720 pub fn artifact(&self) -> &ProvenAuthorityArtifact {
721 &self.artifact
722 }
723
724 /// Convenience accessor for the output tuple.
725 #[must_use]
726 pub fn output(&self) -> AuthorityOutput {
727 self.derivation.output
728 }
729}
730
731/// Authority asserted by CellOS having terminated TLS in-line and observed
732/// plaintext that the workload would not have shown without termination.
733///
734/// **Sources:** Stack 3 / M2 only — lives architecturally in the quarantined
735/// `cellos-tls-term` crate (post-1.0). The type lives in `cellos-core` so
736/// downstream code can refer to it without depending on the quarantined
737/// crate, but no `cellos-core` code path constructs one today.
738///
739/// **Permitted rule-classes:** `ImposedInterception` (REQUIRED).
740/// Co-occurrence with other classes is permitted only insofar as the ADG
741/// mapping demands `Imposed` epistemic status anyway.
742///
743/// **Doctrine:** `ImposedAuthority` MUST NOT be emitted on the
744/// `cell.observability.v1.l7_authority_evidence` event type. It uses
745/// `cell.observability.v1.l7_imposed_enforcement` exclusively, with the
746/// `non_authoritative_enforcement: true` flag. This module enforces the
747/// type-system half of that contract; the event-builder half lands in B3.
748#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
749pub struct ImposedAuthority {
750 derivation: AuthorityDerivation,
751}
752
753impl ImposedAuthority {
754 /// Permitted rule-classes for this variant. `ImposedInterception` is
755 /// REQUIRED to appear; co-occurrence with other classes is permitted but
756 /// the deterministic mapping forces output epistemic status to `Imposed`.
757 pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
758 RuleClass::ImposedInterception,
759 RuleClass::RawObservation,
760 RuleClass::StructuralAlignment,
761 ];
762
763 /// Construct an `ImposedAuthority`.
764 ///
765 /// Runs [`AuthorityDerivation::validate`] plus the type-class envelope
766 /// check, and additionally requires the `ImposedInterception`
767 /// rule-class to be present.
768 ///
769 /// # Errors
770 ///
771 /// Returns the first [`ValidationError`] encountered.
772 pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
773 derivation.validate()?;
774 let class_set = derivation.class_set();
775 if !class_set.contains(&RuleClass::ImposedInterception) {
776 return Err(ValidationError::ImposedAuthorityMissingInterception);
777 }
778 for class in &class_set {
779 if !Self::PERMITTED_CLASSES.contains(class) {
780 return Err(ValidationError::TypeClassIncompatible {
781 type_name: "ImposedAuthority",
782 permitted: Self::PERMITTED_CLASSES,
783 rejected: *class,
784 });
785 }
786 }
787 Ok(Self { derivation })
788 }
789
790 /// Read-only access to the derivation.
791 #[must_use]
792 pub fn derivation(&self) -> &AuthorityDerivation {
793 &self.derivation
794 }
795
796 /// Convenience accessor for the output tuple.
797 #[must_use]
798 pub fn output(&self) -> AuthorityOutput {
799 self.derivation.output
800 }
801}
802
803/// Authority asserted by a guest-side declaration (in-VM `cellos-telemetry`
804/// agent over the per-cell vsock channel) per [ADR-0006].
805///
806/// **Permitted rule-classes:** [`RuleClass::GuestAgentDeclaration`] only —
807/// the envelope is mono-class per ADR-0006 §3. Any other class violates the
808/// type-system gate that prevents guest declarations from being silently
809/// upgraded to host-side epistemic statuses.
810///
811/// **Tier ceiling:** 1. **Epistemic status:** [`EpistemicStatus::Declared`].
812/// **Binding status:** [`BindingStatus::NotApplicable`] for single-input
813/// derivations; multi-input declarations remain [`BindingStatus::Unknown`]
814/// because no host-side alignment/proof rule can fire alongside.
815///
816/// **Type-system discipline:** No `From`/`Into` impls to or from any other
817/// authority variant. Construction goes only through [`Self::try_new`].
818///
819/// [ADR-0006]: ../../../../docs/adr/0006-in-vm-observability-runner-evidence.md
820#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
821pub struct DeclaredAuthority {
822 derivation: AuthorityDerivation,
823}
824
825impl DeclaredAuthority {
826 /// Permitted rule-classes for this variant. Mono-class envelope per
827 /// ADR-0006 §3 — `GuestAgentDeclaration` is the only legal class.
828 pub const PERMITTED_CLASSES: &'static [RuleClass] = &[RuleClass::GuestAgentDeclaration];
829
830 /// Construct a `DeclaredAuthority`.
831 ///
832 /// Runs [`AuthorityDerivation::validate`] plus the mono-class envelope
833 /// check (any class other than `GuestAgentDeclaration` is rejected with
834 /// [`ValidationError::TypeClassIncompatible`]).
835 ///
836 /// # Errors
837 ///
838 /// Returns the first [`ValidationError`] encountered.
839 pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
840 derivation.validate()?;
841 for class in derivation.class_set() {
842 if !Self::PERMITTED_CLASSES.contains(&class) {
843 return Err(ValidationError::TypeClassIncompatible {
844 type_name: "DeclaredAuthority",
845 permitted: Self::PERMITTED_CLASSES,
846 rejected: class,
847 });
848 }
849 }
850 Ok(Self { derivation })
851 }
852
853 /// Read-only access to the validated derivation.
854 #[must_use]
855 pub fn derivation(&self) -> &AuthorityDerivation {
856 &self.derivation
857 }
858
859 /// Convenience accessor for the output tuple.
860 #[must_use]
861 pub fn output(&self) -> AuthorityOutput {
862 self.derivation.output
863 }
864}
865
866/// Compile-fail gate for guest event-builders (ADR-0006 / O2 doctrine
867/// red-line). Implementors declare their canonical [`RuleClass`] as an
868/// associated constant; a `const`-asserted check in [`Self::rule_class`]
869/// makes any class other than [`RuleClass::GuestAgentDeclaration`] a
870/// **compilation error** at the call site.
871///
872/// This is the type-system half of the F-tier admission gate: a guest
873/// event-builder cannot accidentally route through `RawObservation`
874/// (Tier 2 / `DirectObservation`) and silently launder a guest declaration
875/// into a host-side epistemic status.
876///
877/// # Compile-fail example
878///
879/// Pointing `RULE_CLASS` at a non-guest class fails to compile when
880/// `rule_class()` is invoked:
881///
882/// ```compile_fail
883/// use cellos_core::authority::{GuestEventBuilder, RuleClass};
884///
885/// struct BadBuilder;
886/// impl GuestEventBuilder for BadBuilder {
887/// const RULE_CLASS: RuleClass = RuleClass::RawObservation;
888/// }
889///
890/// let _ = BadBuilder.rule_class();
891/// ```
892pub trait GuestEventBuilder {
893 /// The canonical [`RuleClass`] this builder emits. **MUST** be
894 /// [`RuleClass::GuestAgentDeclaration`].
895 const RULE_CLASS: RuleClass;
896
897 /// Returns [`Self::RULE_CLASS`]. The default implementation is a
898 /// `const`-asserted compile-time check that the class is
899 /// `GuestAgentDeclaration`; overriding is allowed but the assertion
900 /// still fires on any path that calls the trait method.
901 fn rule_class(&self) -> RuleClass {
902 const {
903 assert!(
904 matches!(Self::RULE_CLASS, RuleClass::GuestAgentDeclaration),
905 "GuestEventBuilder::RULE_CLASS must be RuleClass::GuestAgentDeclaration",
906 );
907 }
908 Self::RULE_CLASS
909 }
910}
911
912// ---------------------------------------------------------------------------
913// Doctrine red-line: D9 — no Into/From between types.
914// ---------------------------------------------------------------------------
915//
916// The doctrine gate D9 (mechanical separation, Authority Model §14 +
917// ADR-0006 §1) requires that none of the four typed-authority variants
918// (`ObservedAuthority`, `ProvenAuthority`, `ImposedAuthority`,
919// `DeclaredAuthority`) can be converted into another. Rust's coherence
920// rules enforce this trivially: we simply do NOT implement
921// `From<ObservedAuthority> for DeclaredAuthority` and so on.
922//
923// `tests::no_cross_type_conversion_compiles` (in the sibling `tests` module)
924// does the soft confirmation: trait-object-style `is::<TypeA>()` checks on
925// stored values plus an `assert_not_impl_all!`-style negative trait test.
926// A genuine compile-fail test would require `trybuild`, which is not in our
927// dev-dep set; the §14 contract is upheld by review discipline plus the
928// run-time test that asserts the four types are distinct via `TypeId`.