use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EpistemicStatus {
DirectObservation,
StructurallyBound,
CryptographicProof,
Opaque,
Imposed,
Declared,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RuleClass {
RawObservation,
StructuralAlignment,
CryptographicProof,
Opacity,
ImposedInterception,
GuestAgentDeclaration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Rule {
RawSniObserved,
RawHostHeaderObserved,
RawH2AuthorityObserved,
SniHostMatch,
H2AuthorityHostMatch,
SniCdnProviderMatch,
JwsSignatureVerified,
DaneTlsaBound,
SvidChainVerified,
EchDetected,
EncryptedInnerSni,
MitmHandshakeTerminated,
GuestProcSpawnObserved,
GuestProcExitObserved,
GuestFsInotifyFired,
GuestCapDenialObserved,
GuestNetConnectAttempted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum BindingStatus {
Bound,
Unknown,
ImposedBound,
NotApplicable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AuthorityInputType {
Sni,
HostHeader,
H2Authority,
TlsCertSan,
DaneTlsaRecord,
JwsPayload,
DnssecValidatedARecord,
MitmTerminatedObservation,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorityInput {
#[serde(rename = "type")]
pub input_type: AuthorityInputType,
pub value: String,
pub confidence: f64,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source_event_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AppliedRule {
pub rule: Rule,
pub class: RuleClass,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct AuthorityOutput {
pub tier: u8,
pub confidence: f64,
pub epistemic_status: EpistemicStatus,
pub binding_status: BindingStatus,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AuthorityDerivation {
pub inputs: Vec<AuthorityInput>,
pub rules_applied: Vec<AppliedRule>,
pub output: AuthorityOutput,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ValidationError {
#[error("authority derivation has no inputs")]
NoInputs,
#[error("authority derivation has no applied rules")]
NoRulesApplied,
#[error("input confidence {0} out of range [0, 1]")]
InputConfidenceOutOfRange(f64),
#[error("input confidence is NaN")]
InputConfidenceNaN,
#[error("output confidence {0} out of range [0, 1] or NaN")]
OutputConfidenceInvalid(f64),
#[error("output tier {0} out of range [0, 4]")]
TierOutOfRange(u8),
#[error("confidence inflation: output {output} exceeds max input {max_input}")]
ConfidenceInflation {
output: f64,
max_input: f64,
},
#[error("tier inflation: output tier {output} exceeds ceiling {ceiling} (limited by {limiting_class:?})")]
TierInflation {
output: u8,
ceiling: u8,
limiting_class: RuleClass,
},
#[error("epistemic status mismatch: declared {declared:?}, expected {expected:?}")]
EpistemicMismatch {
declared: EpistemicStatus,
expected: EpistemicStatus,
},
#[error("rule {rule:?} declared class {declared:?}, canonical class is {canonical:?}")]
RuleClassMismatch {
rule: Rule,
declared: RuleClass,
canonical: RuleClass,
},
#[error("binding status mismatch: declared {declared:?}, expected {expected:?}")]
BindingStatusMismatch {
declared: BindingStatus,
expected: BindingStatus,
},
#[error(
"type-class incompatible: {type_name} requires rule-classes within {permitted:?}, got {rejected:?}"
)]
TypeClassIncompatible {
type_name: &'static str,
permitted: &'static [RuleClass],
rejected: RuleClass,
},
#[error("ProvenAuthority requires a verified-signature artifact")]
ProvenAuthorityMissingArtifact,
#[error("ImposedAuthority requires the IMPOSED_INTERCEPTION rule-class in its derivation")]
ImposedAuthorityMissingInterception,
#[error("rule-class set is empty after deduplication; refusing emission")]
EmptyClassSet,
}
#[must_use]
pub const fn canonical_class_for_rule(rule: Rule) -> RuleClass {
match rule {
Rule::RawSniObserved | Rule::RawHostHeaderObserved | Rule::RawH2AuthorityObserved => {
RuleClass::RawObservation
}
Rule::SniHostMatch | Rule::H2AuthorityHostMatch | Rule::SniCdnProviderMatch => {
RuleClass::StructuralAlignment
}
Rule::JwsSignatureVerified | Rule::DaneTlsaBound | Rule::SvidChainVerified => {
RuleClass::CryptographicProof
}
Rule::EchDetected | Rule::EncryptedInnerSni => RuleClass::Opacity,
Rule::MitmHandshakeTerminated => RuleClass::ImposedInterception,
Rule::GuestProcSpawnObserved
| Rule::GuestProcExitObserved
| Rule::GuestFsInotifyFired
| Rule::GuestCapDenialObserved
| Rule::GuestNetConnectAttempted => RuleClass::GuestAgentDeclaration,
}
}
#[must_use]
pub const fn max_tier_for_class(class: RuleClass) -> u8 {
match class {
RuleClass::RawObservation => 2,
RuleClass::StructuralAlignment => 3,
RuleClass::CryptographicProof => 4,
RuleClass::Opacity => 1,
RuleClass::ImposedInterception => 4,
RuleClass::GuestAgentDeclaration => 1,
}
}
#[must_use]
pub fn epistemic_for_class_set(classes: &BTreeSet<RuleClass>) -> Option<EpistemicStatus> {
if classes.is_empty() {
return None;
}
if classes.contains(&RuleClass::ImposedInterception) {
return Some(EpistemicStatus::Imposed);
}
if classes.contains(&RuleClass::Opacity) {
return Some(EpistemicStatus::Opaque);
}
if classes.contains(&RuleClass::CryptographicProof) {
return Some(EpistemicStatus::CryptographicProof);
}
if classes.contains(&RuleClass::StructuralAlignment) {
return Some(EpistemicStatus::StructurallyBound);
}
if classes.contains(&RuleClass::RawObservation) {
return Some(EpistemicStatus::DirectObservation);
}
if classes.contains(&RuleClass::GuestAgentDeclaration) {
return Some(EpistemicStatus::Declared);
}
None
}
fn expected_binding_status(
inputs: &[AuthorityInput],
classes: &BTreeSet<RuleClass>,
) -> BindingStatus {
if classes.contains(&RuleClass::ImposedInterception) {
return BindingStatus::ImposedBound;
}
let opacity_or_imposed = classes.contains(&RuleClass::Opacity);
let alignment_or_proof = classes.contains(&RuleClass::StructuralAlignment)
|| classes.contains(&RuleClass::CryptographicProof);
if opacity_or_imposed {
return BindingStatus::Unknown;
}
if alignment_or_proof {
return BindingStatus::Bound;
}
if inputs.len() <= 1 {
BindingStatus::NotApplicable
} else {
BindingStatus::Unknown
}
}
impl AuthorityDerivation {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.inputs.is_empty() {
return Err(ValidationError::NoInputs);
}
if self.rules_applied.is_empty() {
return Err(ValidationError::NoRulesApplied);
}
for input in &self.inputs {
if input.confidence.is_nan() {
return Err(ValidationError::InputConfidenceNaN);
}
if !(0.0..=1.0).contains(&input.confidence) {
return Err(ValidationError::InputConfidenceOutOfRange(input.confidence));
}
}
if self.output.confidence.is_nan() || !(0.0..=1.0).contains(&self.output.confidence) {
return Err(ValidationError::OutputConfidenceInvalid(
self.output.confidence,
));
}
if self.output.tier > 4 {
return Err(ValidationError::TierOutOfRange(self.output.tier));
}
for ar in &self.rules_applied {
let canonical = canonical_class_for_rule(ar.rule);
if canonical != ar.class {
return Err(ValidationError::RuleClassMismatch {
rule: ar.rule,
declared: ar.class,
canonical,
});
}
}
let class_set: BTreeSet<RuleClass> = self.rules_applied.iter().map(|ar| ar.class).collect();
let expected_epistemic =
epistemic_for_class_set(&class_set).ok_or(ValidationError::EmptyClassSet)?;
if self.output.epistemic_status != expected_epistemic {
return Err(ValidationError::EpistemicMismatch {
declared: self.output.epistemic_status,
expected: expected_epistemic,
});
}
let max_input = self
.inputs
.iter()
.map(|i| i.confidence)
.fold(f64::NEG_INFINITY, f64::max);
if self.output.confidence > max_input {
return Err(ValidationError::ConfidenceInflation {
output: self.output.confidence,
max_input,
});
}
let (ceiling, limiting_class) = class_set
.iter()
.map(|c| (max_tier_for_class(*c), *c))
.min_by_key(|(t, _)| *t)
.expect("class_set non-empty (checked above)");
if self.output.tier > ceiling {
return Err(ValidationError::TierInflation {
output: self.output.tier,
ceiling,
limiting_class,
});
}
let expected_binding = expected_binding_status(&self.inputs, &class_set);
if self.output.binding_status != expected_binding {
return Err(ValidationError::BindingStatusMismatch {
declared: self.output.binding_status,
expected: expected_binding,
});
}
Ok(())
}
#[must_use]
pub fn class_set(&self) -> BTreeSet<RuleClass> {
self.rules_applied.iter().map(|ar| ar.class).collect()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ObservedAuthority {
derivation: AuthorityDerivation,
}
impl ObservedAuthority {
pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
RuleClass::RawObservation,
RuleClass::StructuralAlignment,
RuleClass::Opacity,
];
pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
derivation.validate()?;
for class in derivation.class_set() {
if !Self::PERMITTED_CLASSES.contains(&class) {
return Err(ValidationError::TypeClassIncompatible {
type_name: "ObservedAuthority",
permitted: Self::PERMITTED_CLASSES,
rejected: class,
});
}
}
Ok(Self { derivation })
}
#[must_use]
pub fn derivation(&self) -> &AuthorityDerivation {
&self.derivation
}
#[must_use]
pub fn output(&self) -> AuthorityOutput {
self.derivation.output
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProvenAuthorityArtifact {
JwsVerified {
kid: String,
},
DaneTlsaBound {
tlsa_record_id: String,
},
SvidChainVerified {
spiffe_id: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProvenAuthority {
derivation: AuthorityDerivation,
artifact: ProvenAuthorityArtifact,
}
impl ProvenAuthority {
pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
RuleClass::CryptographicProof,
RuleClass::StructuralAlignment,
RuleClass::RawObservation,
];
pub fn try_new(
derivation: AuthorityDerivation,
artifact: ProvenAuthorityArtifact,
) -> Result<Self, ValidationError> {
derivation.validate()?;
let class_set = derivation.class_set();
if !class_set.contains(&RuleClass::CryptographicProof) {
return Err(ValidationError::ProvenAuthorityMissingArtifact);
}
for class in &class_set {
if !Self::PERMITTED_CLASSES.contains(class) {
return Err(ValidationError::TypeClassIncompatible {
type_name: "ProvenAuthority",
permitted: Self::PERMITTED_CLASSES,
rejected: *class,
});
}
}
Ok(Self {
derivation,
artifact,
})
}
#[must_use]
pub fn derivation(&self) -> &AuthorityDerivation {
&self.derivation
}
#[must_use]
pub fn artifact(&self) -> &ProvenAuthorityArtifact {
&self.artifact
}
#[must_use]
pub fn output(&self) -> AuthorityOutput {
self.derivation.output
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImposedAuthority {
derivation: AuthorityDerivation,
}
impl ImposedAuthority {
pub const PERMITTED_CLASSES: &'static [RuleClass] = &[
RuleClass::ImposedInterception,
RuleClass::RawObservation,
RuleClass::StructuralAlignment,
];
pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
derivation.validate()?;
let class_set = derivation.class_set();
if !class_set.contains(&RuleClass::ImposedInterception) {
return Err(ValidationError::ImposedAuthorityMissingInterception);
}
for class in &class_set {
if !Self::PERMITTED_CLASSES.contains(class) {
return Err(ValidationError::TypeClassIncompatible {
type_name: "ImposedAuthority",
permitted: Self::PERMITTED_CLASSES,
rejected: *class,
});
}
}
Ok(Self { derivation })
}
#[must_use]
pub fn derivation(&self) -> &AuthorityDerivation {
&self.derivation
}
#[must_use]
pub fn output(&self) -> AuthorityOutput {
self.derivation.output
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DeclaredAuthority {
derivation: AuthorityDerivation,
}
impl DeclaredAuthority {
pub const PERMITTED_CLASSES: &'static [RuleClass] = &[RuleClass::GuestAgentDeclaration];
pub fn try_new(derivation: AuthorityDerivation) -> Result<Self, ValidationError> {
derivation.validate()?;
for class in derivation.class_set() {
if !Self::PERMITTED_CLASSES.contains(&class) {
return Err(ValidationError::TypeClassIncompatible {
type_name: "DeclaredAuthority",
permitted: Self::PERMITTED_CLASSES,
rejected: class,
});
}
}
Ok(Self { derivation })
}
#[must_use]
pub fn derivation(&self) -> &AuthorityDerivation {
&self.derivation
}
#[must_use]
pub fn output(&self) -> AuthorityOutput {
self.derivation.output
}
}
pub trait GuestEventBuilder {
const RULE_CLASS: RuleClass;
fn rule_class(&self) -> RuleClass {
const {
assert!(
matches!(Self::RULE_CLASS, RuleClass::GuestAgentDeclaration),
"GuestEventBuilder::RULE_CLASS must be RuleClass::GuestAgentDeclaration",
);
}
Self::RULE_CLASS
}
}