Skip to main content

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`.