Skip to main content

auths_cli/commands/
kel.rs

1//! `auths kel validate` — read-only structural check of a Key Event Log.
2//!
3//! Replays a KEL and reports the first defect that would make it fail at sign or
4//! verify time: a stale or mismatched SAID encoding, or a broken chain link.
5//! Exits nonzero when the log is unsound, so a corrupt KEL is caught up front
6//! instead of silently breaking signing later.
7
8use anyhow::{Result, anyhow};
9use clap::{Parser, Subcommand};
10use std::path::PathBuf;
11
12use auths_keri::{Event, TrustedKel, parse_kel_json, verify_event_said};
13use auths_sdk::keri::KelResolverChain;
14use auths_sdk::storage::{GitRegistryBackend, RegistryConfig};
15
16use crate::config::CliConfig;
17use crate::ux::format::is_json_mode;
18
19/// Inspect a Key Event Log (read-only).
20#[derive(Parser, Debug, Clone)]
21#[command(
22    about = "Inspect a Key Event Log (read-only)",
23    after_help = "Examples:
24  auths kel validate                    # validate your own identity's local KEL
25  auths kel validate --did did:keri:... # validate another identity's local KEL
26  auths kel validate --kel kel.json     # validate a KEL JSON file"
27)]
28pub struct KelCommand {
29    #[command(subcommand)]
30    pub command: KelSubcommand,
31}
32
33/// KEL subcommands.
34#[derive(Subcommand, Debug, Clone)]
35pub enum KelSubcommand {
36    /// Structurally validate a KEL: SAID encodings and chain linkage.
37    ///
38    /// Exits nonzero on a stale encoding or a broken chain so a corrupt log is
39    /// caught before it fails silently at sign time.
40    Validate(KelValidateArgs),
41}
42
43/// Arguments for `auths kel validate`.
44#[derive(Parser, Debug, Clone)]
45pub struct KelValidateArgs {
46    /// Validate this KEL JSON file instead of a stored identity's KEL.
47    #[arg(long, value_name = "KEL.json")]
48    pub kel: Option<PathBuf>,
49
50    /// Validate this identity's local KEL (defaults to your own identity).
51    #[arg(long, value_name = "did:keri:...", conflicts_with = "kel")]
52    pub did: Option<String>,
53}
54
55impl KelCommand {
56    /// Run the command.
57    pub fn execute(&self, ctx: &CliConfig) -> Result<()> {
58        match &self.command {
59            KelSubcommand::Validate(args) => validate(args, ctx),
60        }
61    }
62}
63
64/// Validate a KEL and report the first structural defect, if any.
65fn validate(args: &KelValidateArgs, ctx: &CliConfig) -> Result<()> {
66    let (source, events) = load_events(args, ctx)?;
67    if events.is_empty() {
68        return Err(anyhow!("{source} contains no key events"));
69    }
70
71    // Stale or mismatched encodings: each event's SAID must match its content
72    // hash. A stale encoding (wrong version string / re-serialized shape) breaks
73    // this before any chain check.
74    for (idx, event) in events.iter().enumerate() {
75        if let Err(e) = verify_event_said(event) {
76            return Err(anyhow!(
77                "{source}: stale or invalid encoding at event {idx}: {e}"
78            ));
79        }
80    }
81
82    // Broken chains: structural replay checks sequence monotonicity, prev-event
83    // linkage, and the pre-rotation commitment across the whole log.
84    if let Err(e) = TrustedKel::from_trusted_source(&events).replay() {
85        return Err(anyhow!("{source}: broken chain: {e}"));
86    }
87
88    report_ok(&source, events.len());
89    Ok(())
90}
91
92/// Print the success line in the active output mode.
93fn report_ok(source: &str, count: usize) {
94    if is_json_mode() {
95        let out = serde_json::json!({
96            "valid": true,
97            "source": source,
98            "events": count,
99        });
100        println!("{out}");
101    } else {
102        println!("OK: {source} — {count} event(s), SAIDs and chain linkage valid");
103    }
104}
105
106/// Resolve the KEL events to validate, plus a human label for the source.
107fn load_events(args: &KelValidateArgs, ctx: &CliConfig) -> Result<(String, Vec<Event>)> {
108    if let Some(path) = &args.kel {
109        let json = std::fs::read_to_string(path)
110            .map_err(|e| anyhow!("cannot read {}: {e}", path.display()))?;
111        let events = parse_kel_json(&json).map_err(|e| anyhow!("cannot parse KEL JSON: {e}"))?;
112        return Ok((path.display().to_string(), events));
113    }
114
115    let auths_home = auths_sdk::storage_layout::resolve_repo_path(ctx.repo_path.clone())?;
116    let did = match &args.did {
117        Some(d) => d.clone(),
118        None => {
119            let sdk_ctx =
120                crate::factories::storage::build_auths_context(&auths_home, &ctx.env_config, None)?;
121            auths_sdk::workflows::commit_trust::local_self_root(&sdk_ctx).ok_or_else(|| {
122                anyhow!("no local identity found — run `auths init` first, or pass --kel/--did")
123            })?
124        }
125    };
126    let registry =
127        GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(&auths_home));
128    let events = KelResolverChain::local(&registry)
129        .resolve_kel(&did)
130        .map_err(|e| anyhow!("cannot resolve local KEL for {did}: {e}"))?;
131    Ok((did, events))
132}