Skip to main content

auths_cli/commands/
trust.rs

1//! Trust management commands for Auths.
2//!
3//! Manage pinned identity roots for trust-on-first-use (TOFU) and explicit trust.
4
5use crate::ux::format::{JsonResponse, Output, is_json_mode};
6use anyhow::{Context, Result, anyhow};
7use auths_sdk::trust::{PinnedIdentity, PinnedIdentityStore, TrustLevel};
8use auths_verifier::PublicKeyHex;
9use chrono::{DateTime, Utc};
10use clap::{Parser, Subcommand};
11use serde::Serialize;
12
13/// Manage trusted identity roots.
14#[derive(Parser, Debug, Clone)]
15#[command(
16    name = "trust",
17    about = "Pin identities you trust for verification",
18    after_help = "Examples:
19  auths trust list          # Show all pinned trusted identities
20  auths trust pin --did did:keri:EExample --key 7f8c9d0e1a2b3c4d...
21                            # Pin an identity as trusted
22  auths trust remove did:keri:EExample
23                            # Remove a pinned identity
24  auths trust show did:keri:EExample
25                            # Show details of a trusted identity
26
27Related:
28  auths verify  — Verify signatures (uses trust store)
29  auths sign    — Create signatures
30  auths error   — Troubleshoot trust policy errors"
31)]
32pub struct TrustCommand {
33    #[command(subcommand)]
34    pub command: TrustSubcommand,
35}
36
37#[derive(Subcommand, Debug, Clone)]
38pub enum TrustSubcommand {
39    /// List all pinned identities.
40    List(TrustListCommand),
41
42    /// Manually pin an identity as trusted.
43    Pin(TrustPinCommand),
44
45    /// Remove a pinned identity.
46    Remove(TrustRemoveCommand),
47
48    /// Show details of a pinned identity.
49    Show(TrustShowCommand),
50}
51
52/// List all pinned identities.
53#[derive(Parser, Debug, Clone)]
54pub struct TrustListCommand {}
55
56/// Manually pin an identity as trusted.
57#[derive(Parser, Debug, Clone)]
58pub struct TrustPinCommand {
59    /// The DID of the identity to pin (e.g., did:keri:E...).
60    #[clap(long, required = true)]
61    pub did: String,
62
63    /// The public key in hex format (64 chars for Ed25519).
64    #[clap(long, required = true)]
65    pub key: String,
66
67    /// Identity log checkpoint for tracking key changes (optional, advanced).
68    #[clap(long)]
69    pub kel_tip: Option<String>,
70
71    /// Optional note about this identity.
72    #[clap(long)]
73    pub note: Option<String>,
74}
75
76/// Remove a pinned identity.
77#[derive(Parser, Debug, Clone)]
78pub struct TrustRemoveCommand {
79    /// The DID of the identity to remove.
80    pub did: String,
81}
82
83/// Show details of a pinned identity.
84#[derive(Parser, Debug, Clone)]
85pub struct TrustShowCommand {
86    /// The DID of the identity to show.
87    pub did: String,
88}
89
90/// JSON output for pin/remove action result.
91#[derive(Debug, Serialize)]
92struct TrustActionResult {
93    did: String,
94}
95
96/// JSON output for list command.
97#[derive(Debug, Serialize)]
98struct PinListOutput {
99    pins: Vec<PinSummary>,
100}
101
102/// Summary of a pinned identity for list output.
103#[derive(Debug, Serialize)]
104struct PinSummary {
105    did: String,
106    trust_level: String,
107    first_seen: String,
108    #[serde(skip_serializing_if = "Option::is_none")]
109    kel_sequence: Option<u128>,
110}
111
112/// JSON output for show command.
113#[derive(Debug, Serialize)]
114struct PinDetails {
115    did: String,
116    public_key_hex: PublicKeyHex,
117    trust_level: String,
118    first_seen: String,
119    origin: String,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    kel_tip_said: Option<String>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    kel_sequence: Option<u128>,
124}
125
126/// Handle trust subcommands.
127#[allow(clippy::disallowed_methods)]
128pub fn handle_trust(cmd: TrustCommand) -> Result<()> {
129    let now = Utc::now();
130    match cmd.command {
131        TrustSubcommand::List(list_cmd) => handle_list(list_cmd),
132        TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd, now),
133        TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd),
134        TrustSubcommand::Show(show_cmd) => handle_show(show_cmd),
135    }
136}
137
138fn handle_list(_cmd: TrustListCommand) -> Result<()> {
139    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
140    let pins = store.list()?;
141
142    if is_json_mode() {
143        JsonResponse::success(
144            "trust list",
145            PinListOutput {
146                pins: pins
147                    .iter()
148                    .map(|p| PinSummary {
149                        did: p.did.clone(),
150                        trust_level: format!("{:?}", p.trust_level),
151                        first_seen: p.first_seen.to_rfc3339(),
152                        kel_sequence: p.kel_sequence,
153                    })
154                    .collect(),
155            },
156        )
157        .print()?;
158    } else {
159        let out = Output::new();
160        if pins.is_empty() {
161            out.println(&out.dim("No pinned identities."));
162            out.println("");
163            out.println("Use 'auths trust pin --did <DID> --key <HEX>' to pin an identity.");
164        } else {
165            out.println(&format!("{} pinned identities:", pins.len()));
166            out.println("");
167            for pin in &pins {
168                let level = match pin.trust_level {
169                    TrustLevel::Tofu => out.dim("TOFU"),
170                    TrustLevel::Manual => out.info("Manual"),
171                    TrustLevel::OrgPolicy => out.success("OrgPolicy"),
172                };
173                out.println(&format!("  {} [{}]", pin.did, level));
174            }
175        }
176    }
177
178    Ok(())
179}
180
181fn handle_pin(cmd: TrustPinCommand, now: DateTime<Utc>) -> Result<()> {
182    let public_key_hex = PublicKeyHex::parse(&cmd.key).context("Invalid public key hex")?;
183
184    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
185
186    // Check if already pinned
187    if let Some(existing) = store.lookup(&cmd.did)? {
188        anyhow::bail!(
189            "Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
190            cmd.did,
191            existing.first_seen.format("%Y-%m-%d"),
192            cmd.did
193        );
194    }
195
196    let pin = PinnedIdentity {
197        did: cmd.did.clone(),
198        public_key_hex: public_key_hex.clone(),
199        curve: auths_crypto::did_key_decode(&cmd.did)
200            .map(|d| d.curve())
201            .unwrap_or_default(),
202        kel_tip_said: cmd.kel_tip,
203        kel_sequence: None,
204        first_seen: now,
205        origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
206        trust_level: TrustLevel::Manual,
207    };
208
209    store.pin(pin)?;
210
211    if is_json_mode() {
212        JsonResponse::success(
213            "trust pin",
214            TrustActionResult {
215                did: cmd.did.clone(),
216            },
217        )
218        .print()?;
219    } else {
220        let out = Output::new();
221        out.println(&format!(
222            "{} Pinned identity: {}",
223            out.success("OK"),
224            &cmd.did
225        ));
226    }
227
228    Ok(())
229}
230
231fn handle_remove(cmd: TrustRemoveCommand) -> Result<()> {
232    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
233
234    // Check if exists
235    if store.lookup(&cmd.did)?.is_none() {
236        anyhow::bail!(
237            "Identity {} is not pinned. Pin it first with: auths trust pin {}",
238            cmd.did,
239            cmd.did
240        );
241    }
242
243    store.remove(&cmd.did)?;
244
245    if is_json_mode() {
246        JsonResponse::success(
247            "trust remove",
248            TrustActionResult {
249                did: cmd.did.clone(),
250            },
251        )
252        .print()?;
253    } else {
254        let out = Output::new();
255        out.println(&format!(
256            "{} Removed pin for: {}",
257            out.success("OK"),
258            &cmd.did
259        ));
260    }
261
262    Ok(())
263}
264
265fn handle_show(cmd: TrustShowCommand) -> Result<()> {
266    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
267
268    let pin = store.lookup(&cmd.did)?.ok_or_else(|| {
269        anyhow!(
270            "Identity {} is not pinned. Pin it first with: auths trust pin {}",
271            cmd.did,
272            cmd.did
273        )
274    })?;
275
276    if is_json_mode() {
277        JsonResponse::success(
278            "trust show",
279            PinDetails {
280                did: pin.did.clone(),
281                public_key_hex: pin.public_key_hex.clone(),
282                trust_level: format!("{:?}", pin.trust_level),
283                first_seen: pin.first_seen.to_rfc3339(),
284                origin: pin.origin.clone(),
285                kel_tip_said: pin.kel_tip_said.clone(),
286                kel_sequence: pin.kel_sequence,
287            },
288        )
289        .print()?;
290    } else {
291        let out = Output::new();
292        out.println(&format!("DID:          {}", pin.did));
293        out.println(&format!("Public Key:   {}", pin.public_key_hex));
294        out.println(&format!("Trust Level:  {:?}", pin.trust_level));
295        out.println(&format!(
296            "First Seen:   {}",
297            pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
298        ));
299        out.println(&format!("Origin:       {}", pin.origin));
300        if let Some(ref tip) = pin.kel_tip_said {
301            out.println(&format!("Log checkpoint: {}", tip));
302        }
303        if let Some(seq) = pin.kel_sequence {
304            out.println(&format!("Log sequence:   {}", seq));
305        }
306    }
307
308    Ok(())
309}
310
311use crate::commands::executable::ExecutableCommand;
312use crate::config::CliConfig;
313
314impl ExecutableCommand for TrustCommand {
315    fn execute(&self, _ctx: &CliConfig) -> Result<()> {
316        handle_trust(self.clone())
317    }
318}