Skip to main content

auths_cli/commands/
key_state.rs

1//! `auths key-state` — emit or ingest a KERI-conformant key-state notice (`ksn`).
2//!
3//! A key-state notice lets a thin client trust an identity's current key-state
4//! without replaying its whole KEL. This command speaks the **KERI wire shape**
5//! (`KeyStateRecord`: `{vn,i,s,p,d,f,dt,et,kt,k,nt,n,bt,b,c,ee,di}`), so a record
6//! auths emits reads in keripy/keriox and a record those peers publish ingests
7//! here. The crypto/wire definition lives in `auths-keri::ksn`; this is a thin
8//! CLI adapter over it.
9
10use std::path::{Path, PathBuf};
11
12use anyhow::{Result, anyhow};
13use auths_keri::{KeyStateRecord, TrustedKel, parse_kel_json};
14use auths_utils::path::expand_tilde;
15use clap::Parser;
16
17use crate::config::CliConfig;
18
19/// Emit or ingest a KERI-conformant key-state notice (`ksn`).
20#[derive(Parser, Debug, Clone)]
21#[command(
22    about = "Emit or ingest a KERI key-state notice (ksn) — interoperable with keripy/keriox",
23    visible_alias = "ksn",
24    after_help = "Examples:
25  auths key-state --from-kel kel.json   # emit a KERI ksn a peer can read
26  auths key-state --ingest ksn.json     # consume a keripy/keriox ksn"
27)]
28pub struct KeyStateCommand {
29    /// Replay this KEL file and emit its current key-state as a KERI `ksn` record
30    /// (the shape a keripy/keriox peer can read).
31    #[clap(long, value_name = "KEL.json")]
32    pub from_kel: Option<PathBuf>,
33
34    /// Ingest a KERI `ksn` record from this file (the shape a keripy/keriox peer
35    /// publishes) and print the resolved key-state.
36    #[clap(long, value_name = "KSN.json", conflicts_with = "from_kel")]
37    pub ingest: Option<PathBuf>,
38
39    /// Reject an ingested notice whose sequence is below this (lowercase-hex)
40    /// value — a verifier already holding a newer state refuses a stale or
41    /// replayed view. Fails closed with a distinct reason; only valid with
42    /// `--ingest`.
43    #[clap(long, value_name = "HEX_SEQ", requires = "ingest")]
44    pub reject_stale_below: Option<String>,
45
46    /// Timestamp (RFC 3339) to stamp an emitted notice with. Defaults to the
47    /// epoch so emission stays deterministic; pass the real `now` to publish.
48    #[clap(long, default_value = "1970-01-01T00:00:00+00:00")]
49    pub dt: String,
50}
51
52impl KeyStateCommand {
53    /// Run the command (emit or ingest).
54    pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
55        match (&self.from_kel, &self.ingest) {
56            (Some(kel_path), None) => self.emit(kel_path),
57            (None, Some(ksn_path)) => self.ingest_record(ksn_path),
58            (Some(_), Some(_)) => {
59                unreachable!("clap conflicts_with guarantees mutual exclusion")
60            }
61            (None, None) => Err(anyhow!(
62                "key-state needs --from-kel <KEL.json> (emit) or --ingest <KSN.json> (consume)"
63            )),
64        }
65    }
66
67    /// Replay a KEL file and print its current key-state as a KERI `ksn` record.
68    fn emit(&self, kel_path: &Path) -> Result<()> {
69        let path = expand_tilde(kel_path)?;
70        let json = std::fs::read_to_string(&path)
71            .map_err(|e| anyhow!("read KEL {}: {e}", path.display()))?;
72        let events = parse_kel_json(&json).map_err(|e| anyhow!("parse KEL: {e}"))?;
73        // A KEL file the operator hands us is a local, self-owned artifact — the
74        // reviewable trust assertion that structural replay requires.
75        let state = TrustedKel::from_trusted_source(&events)
76            .replay()
77            .map_err(|e| anyhow!("replay KEL: {e}"))?;
78        let record = KeyStateRecord::from_kel(&events, &state, self.dt.clone())
79            .ok_or_else(|| anyhow!("KEL is empty — no key-state to notice"))?;
80        println!("{}", serde_json::to_string_pretty(&record)?);
81        Ok(())
82    }
83
84    /// Ingest a KERI `ksn` record and print the resolved auths key-state.
85    fn ingest_record(&self, ksn_path: &Path) -> Result<()> {
86        let path = expand_tilde(ksn_path)?;
87        let json = std::fs::read_to_string(&path)
88            .map_err(|e| anyhow!("read ksn {}: {e}", path.display()))?;
89        let record: KeyStateRecord =
90            serde_json::from_str(&json).map_err(|e| anyhow!("parse KERI ksn: {e}"))?;
91
92        // Fail closed on a stale view if the verifier names the newest state it
93        // already trusts — a rewind below it is a stale or replayed notice.
94        if let Some(hex_seq) = &self.reject_stale_below {
95            let last_seen =
96                u128::from_str_radix(hex_seq.trim_start_matches("0x"), 16).map_err(|_| {
97                    anyhow!("--reject-stale-below must be lowercase hex, got {hex_seq:?}")
98                })?;
99            record.check_not_stale(last_seen).map_err(|e| {
100                anyhow!("rejected: stale key-state notice — {e}; a verifier holding a newer state refuses to rewind")
101            })?;
102        }
103
104        let state = record.into_key_state();
105        println!("{}", serde_json::to_string_pretty(&state)?);
106        Ok(())
107    }
108}