use std::{
net::SocketAddr,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use clap::Args;
use onomancy_core::{collections::Map, time::UnixSeconds};
use onomancy_dnssec::{
certificate::Certificate,
dns_name::DnsName,
freshness::Freshness,
statement::{rotation::RotationStatement, successor::SuccessorStatement},
validator::{Validator, WalkError},
};
use onomancy_hickory::provider::FetchChainError;
use onomancy_keyhive::authority::KeyhiveAuthority;
use onomancy_protocol::verifier::{
state::{
VerifierState,
decisions::Decisions,
diff::{Event, EventKind},
store::item::Item,
},
verdict::{self, Rejection},
};
use crate::{
say,
store_dir::{self, StoreDirError},
};
#[derive(Debug, Args)]
pub(crate) struct Resolve {
#[arg(long)]
hostname: String,
#[arg(long)]
resolver: Option<SocketAddr>,
#[arg(long)]
cert: Option<PathBuf>,
#[arg(long)]
chain_out: Option<PathBuf>,
#[arg(long)]
store: Option<PathBuf>,
#[arg(long, requires = "store")]
ingest: Vec<PathBuf>,
}
impl Resolve {
pub(crate) fn run(&self) -> Result<(), ResolveError> {
let hostname = DnsName::parse_display(&self.hostname)?;
if let Some(store_path) = &self.store {
return stateful_pass(
self.resolver,
store_path,
&hostname,
self.cert.as_deref(),
&self.ingest,
);
}
let now = UnixSeconds::from(now_seconds());
let validator = Validator::iana();
let provider = crate::provider(self.resolver);
let chain = crate::block_on(provider.fetch_chain(&hostname))??;
say(&format!("chain: {} links fetched", chain.links().len()));
if let Some(chain_out) = &self.chain_out {
let mut framed = Vec::new();
chain.write_framed(&mut framed);
std::fs::write(chain_out, framed)?;
say(&format!("chain written: {}", chain_out.display()));
}
let proof = validator.validate_detailed(&hostname, &chain)?;
let grade = match proof.window.grade(now) {
onomancy_dnssec::freshness::Grade::Fresh => "fresh \u{2713}",
onomancy_dnssec::freshness::Grade::Stale => "stale \u{26a0}",
onomancy_dnssec::freshness::Grade::NotYetBegun => "not yet begun (deferred)",
};
say(&format!("DNSSEC: valid, window {grade}"));
for record in &proof.records {
say(&format!("zone says: {record}"));
}
let Some(cert_path) = &self.cert else {
return Ok(());
};
let bytes = std::fs::read(cert_path)?;
let authority = KeyhiveAuthority;
let verdict = verdict::verify(&bytes, &hostname, now, &validator, &authority)?;
let freshness = match verdict.freshness {
Freshness::Fresh => "fresh \u{2713}",
Freshness::Stale => "stale \u{26a0}",
};
let generation = match verdict.generation_check {
verdict::GenerationCheck::OnPath => "on delegation path",
verdict::GenerationCheck::Provisional => {
"provisional ⚠ (stale evidence; re-checked when fresher evidence arrives)"
}
};
say(&format!("verdict: {freshness}"));
say(&format!(" document: {}", verdict.document));
say(&format!(" serial: {}", verdict.serial));
say(&format!(" generation: {generation}"));
Ok(())
}
}
pub(crate) fn stateful_pass(
resolver: Option<SocketAddr>,
store_path: &Path,
hostname: &DnsName,
cert: Option<&Path>,
ingest: &[PathBuf],
) -> Result<(), ResolveError> {
let now = UnixSeconds::from(now_seconds());
let validator = Validator::iana();
let authority = KeyhiveAuthority;
let pins: Map<DnsName, Vec<onomancy_core::anchor::doc::DocAnchor>> = Map::default();
let decisions = Decisions::default();
let mut store = store_dir::load(store_path)?;
let before = VerifierState::compute(&store, now, &decisions, &pins, &validator, &authority);
let mut new_items: Vec<Item> = ingest
.iter()
.map(|path| decode_unit(path))
.collect::<Result<_, _>>()?;
let provider = crate::provider(resolver);
match crate::block_on(provider.fetch_chain(hostname))? {
Ok(chain) => {
say(&format!("chain: {} links fetched", chain.links().len()));
new_items.push(Item::ChainRefresh {
hostname: hostname.clone(),
chain,
});
}
Err(failure) => say(&format!(
"chain: fetch failed ({failure}) — judging held evidence only"
)),
}
if let Some(cert_path) = cert {
new_items.push(Item::Record(Certificate::decode(&std::fs::read(
cert_path,
)?)?));
}
for item in new_items {
if let Some(path) = store_dir::persist(store_path, &item)? {
say(&format!("store: holding {}", path.display()));
}
store.insert(item);
}
let after = VerifierState::compute(&store, now, &decisions, &pins, &validator, &authority);
let events = after.diff(&before);
if events.is_empty() {
say("no change: the evidence supports what it supported before");
}
for event in &events {
say(&describe(event));
}
summarize(hostname, &after);
Ok(())
}
fn decode_unit(path: &Path) -> Result<Item, ResolveError> {
let bytes = std::fs::read(path)?;
match path.extension().and_then(|e| e.to_str()) {
Some("onc") => Ok(Item::Record(Certificate::decode(&bytes)?)),
Some("onr") => Ok(Item::Rotation(RotationStatement::decode(&bytes)?)),
Some("ons") => Ok(Item::Successor(SuccessorStatement::decode(&bytes)?)),
_ => Err(ResolveError::UnknownUnit(path.to_path_buf())),
}
}
fn describe(event: &Event) -> String {
let marker = if event.kind.may_prompt() { "⚡" } else { "·" };
let hostname = &event.hostname;
let change = match &event.kind {
EventKind::BindingChanged { from, to } => format!(
"binding changed: {} → {}",
from.as_ref()
.map_or_else(|| "none".into(), ToString::to_string),
to.as_ref()
.map_or_else(|| "none".into(), ToString::to_string),
),
EventKind::ContestedCleared => "contested cleared".into(),
EventKind::ContestedEntered => "CONTESTED: competing evidence of equal rank".into(),
EventKind::DivergenceCleared(divergence) => format!("divergence cleared: {divergence:?}"),
EventKind::DivergenceSurfaced(divergence) => format!("divergence: {divergence:?}"),
EventKind::GradeChanged { from, to } => format!("grade: {from:?} → {to:?}"),
EventKind::LineageForkSurfaced(fork) => format!("LINEAGE FORK (equivocation): {fork:?}"),
EventKind::LosingAcceptanceCleared(document) => {
format!("losing acceptance cleared: {document}")
}
EventKind::LosingAcceptanceSurfaced(document) => {
format!("acceptance outranked by receipts: {document}")
}
EventKind::PendingCleared(document) => format!("pending cleared: {document}"),
EventKind::PendingSurfaced(document) => format!("pending (stale challenger): {document}"),
EventKind::SerialRegression { from, to } => format!("RATCHET RESET: serial {from} → {to}"),
EventKind::SuccessionForkSurfaced(fork) => format!("SUCCESSION FORK: {fork:?}"),
};
format!("{marker} {hostname}: {change}")
}
fn summarize(hostname: &DnsName, state: &VerifierState) {
let Some(host) = state.bindings.get(hostname) else {
say(&format!("{hostname}: no evidence held"));
return;
};
match &host.accepted {
Some(accepted) => say(&format!(
"{hostname}: accepted → {} ({:?}, continuity {:?})",
accepted.document, accepted.grade, accepted.continuity,
)),
None if host.contested => say(&format!("{hostname}: CONTESTED — no accepted binding")),
None => say(&format!("{hostname}: no accepted binding")),
}
if !host.pending.is_empty() {
say(&format!(" pending: {}", host.pending.len()));
}
if !host.forks.is_empty() || !host.succession_forks.is_empty() {
say(&format!(
" forks: {} lineage, {} succession",
host.forks.len(),
host.succession_forks.len(),
));
}
}
fn now_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs())
}
#[derive(Debug, thiserror::Error)]
pub(crate) enum ResolveError {
#[error(transparent)]
Fetch(#[from] FetchChainError),
#[error("hostname: {0}")]
Hostname(#[from] onomancy_dnssec::dns_name::ParseDnsNameError),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("certificate rejected: {0}")]
Rejected(#[from] Rejection),
#[error("certificate: {0}")]
Certificate(#[from] onomancy_dnssec::certificate::DecodeCertificateError),
#[error("rotation statement: {0}")]
Rotation(#[from] onomancy_dnssec::statement::rotation::DecodeRotationError),
#[error("store: {0}")]
Store(#[from] StoreDirError),
#[error("successor statement: {0}")]
Successor(#[from] onomancy_dnssec::statement::successor::DecodeSuccessorError),
#[error("not a unit file (expected .onc/.onr/.ons): {0}")]
UnknownUnit(PathBuf),
#[error("chain invalid: {0}")]
Walk(#[from] WalkError),
}