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
21                            # Pin an identity (key resolved from its local KEL)
22  auths trust pin --did did:keri:EExample --bundle their-bundle.json
23                            # Pin from an exported identity bundle
24  auths trust remove did:keri:EExample
25                            # Remove a pinned identity
26  auths trust show did:keri:EExample
27                            # Show details of a trusted identity
28
29Related:
30  auths verify  — Verify signatures (uses trust store)
31  auths sign    — Create signatures
32  auths error   — Troubleshoot trust policy errors"
33)]
34pub struct TrustCommand {
35    #[command(subcommand)]
36    pub command: TrustSubcommand,
37}
38
39#[derive(Subcommand, Debug, Clone)]
40pub enum TrustSubcommand {
41    /// List all pinned identities.
42    List(TrustListCommand),
43
44    /// Manually pin an identity as trusted.
45    Pin(TrustPinCommand),
46
47    /// Remove a pinned identity.
48    Remove(TrustRemoveCommand),
49
50    /// Show details of a pinned identity.
51    Show(TrustShowCommand),
52}
53
54/// List all pinned identities.
55#[derive(Parser, Debug, Clone)]
56pub struct TrustListCommand {}
57
58/// Manually pin an identity as trusted.
59#[derive(Parser, Debug, Clone)]
60pub struct TrustPinCommand {
61    /// The DID of the identity to pin (e.g., did:keri:E...).
62    #[clap(long, required = true)]
63    pub did: String,
64
65    /// The public key in hex format. Omit it to resolve the current key from
66    /// the identity's locally-replayed KEL (air-gapped ceremony is the only
67    /// case that needs the explicit hex).
68    #[clap(long)]
69    pub key: Option<String>,
70
71    /// Path to an identity bundle JSON to resolve the key from (alternative to
72    /// --key and to local KEL resolution).
73    #[clap(long)]
74    pub bundle: Option<std::path::PathBuf>,
75
76    /// Identity log checkpoint for tracking key changes (optional, advanced).
77    #[clap(long)]
78    pub kel_tip: Option<String>,
79
80    /// Optional note about this identity.
81    #[clap(long)]
82    pub note: Option<String>,
83}
84
85/// Remove a pinned identity.
86#[derive(Parser, Debug, Clone)]
87pub struct TrustRemoveCommand {
88    /// The DID of the identity to remove.
89    pub did: String,
90}
91
92/// Show details of a pinned identity.
93#[derive(Parser, Debug, Clone)]
94pub struct TrustShowCommand {
95    /// The DID of the identity to show.
96    pub did: String,
97}
98
99/// JSON output for pin/remove action result.
100#[derive(Debug, Serialize)]
101struct TrustActionResult {
102    did: String,
103}
104
105/// JSON output for list command.
106#[derive(Debug, Serialize)]
107struct PinListOutput {
108    pins: Vec<PinSummary>,
109}
110
111/// Summary of a pinned identity for list output.
112#[derive(Debug, Serialize)]
113struct PinSummary {
114    did: String,
115    trust_level: String,
116    first_seen: String,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    kel_sequence: Option<u128>,
119}
120
121/// JSON output for show command.
122#[derive(Debug, Serialize)]
123struct PinDetails {
124    did: String,
125    public_key_hex: PublicKeyHex,
126    trust_level: String,
127    first_seen: String,
128    origin: String,
129    #[serde(skip_serializing_if = "Option::is_none")]
130    kel_tip_said: Option<String>,
131    #[serde(skip_serializing_if = "Option::is_none")]
132    kel_sequence: Option<u128>,
133}
134
135/// Handle trust subcommands.
136#[allow(clippy::disallowed_methods)]
137pub fn handle_trust(cmd: TrustCommand) -> Result<()> {
138    let now = Utc::now();
139    match cmd.command {
140        TrustSubcommand::List(list_cmd) => handle_list(list_cmd),
141        TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd, now),
142        TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd),
143        TrustSubcommand::Show(show_cmd) => handle_show(show_cmd),
144    }
145}
146
147fn handle_list(_cmd: TrustListCommand) -> Result<()> {
148    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
149    let pins = store.list()?;
150
151    if is_json_mode() {
152        JsonResponse::success(
153            "trust list",
154            PinListOutput {
155                pins: pins
156                    .iter()
157                    .map(|p| PinSummary {
158                        did: p.did.clone(),
159                        trust_level: format!("{:?}", p.trust_level),
160                        first_seen: p.first_seen.to_rfc3339(),
161                        kel_sequence: p.kel_sequence,
162                    })
163                    .collect(),
164            },
165        )
166        .print()?;
167    } else {
168        let out = Output::new();
169        if pins.is_empty() {
170            out.println(&out.dim("No pinned identities."));
171            out.println("");
172            out.println("Use 'auths trust pin --did <DID> --key <HEX>' to pin an identity.");
173        } else {
174            out.println(&format!("{} pinned identities:", pins.len()));
175            out.println("");
176            for pin in &pins {
177                let level = match pin.trust_level {
178                    TrustLevel::Tofu => out.dim("TOFU"),
179                    TrustLevel::Manual => out.info("Manual"),
180                    TrustLevel::OrgPolicy => out.success("OrgPolicy"),
181                };
182                out.println(&format!("  {} [{}]", pin.did, level));
183            }
184        }
185    }
186
187    Ok(())
188}
189
190/// Resolve the key material for a pin: explicit `--key` hex, a `--bundle`
191/// file, or the identity's locally-replayed KEL — in that order. Humans never
192/// have to produce raw hex on the happy path.
193fn resolve_pin_key(cmd: &TrustPinCommand) -> Result<(PublicKeyHex, auths_crypto::CurveType)> {
194    if let Some(ref key_hex) = cmd.key {
195        let public_key_hex = PublicKeyHex::parse(key_hex).context("Invalid public key hex")?;
196        let curve = auths_crypto::did_key_decode(&cmd.did)
197            .map(|d| d.curve())
198            .unwrap_or_default();
199        return Ok((public_key_hex, curve));
200    }
201    if let Some(ref bundle_path) = cmd.bundle {
202        let content = std::fs::read_to_string(bundle_path)
203            .with_context(|| format!("Failed to read identity bundle: {bundle_path:?}"))?;
204        let bundle: auths_verifier::IdentityBundle = serde_json::from_str(&content)
205            .with_context(|| format!("Failed to parse identity bundle: {bundle_path:?}"))?;
206        if bundle.identity_did.as_str() != cmd.did {
207            anyhow::bail!(
208                "Bundle is for {} but --did is {}",
209                bundle.identity_did.as_str(),
210                cmd.did
211            );
212        }
213        return Ok((bundle.public_key_hex.clone(), bundle.curve));
214    }
215    let auths_home = auths_sdk::paths::auths_home().map_err(|e| anyhow!(e))?;
216    let registry = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
217        auths_sdk::storage::RegistryConfig::single_tenant(&auths_home),
218    );
219    let (pk, curve) = auths_sdk::keri::resolve_current_public_key(&registry, &cmd.did)
220        .with_context(|| {
221            format!(
222                "Could not resolve {} from the local registry. Provide --bundle <file> \
223                 (ask the identity owner for `auths id export-bundle`) or --key <hex>.",
224                cmd.did
225            )
226        })?;
227    #[allow(clippy::disallowed_methods)] // INVARIANT: hex::encode always produces valid hex
228    Ok((PublicKeyHex::new_unchecked(hex::encode(pk)), curve))
229}
230
231fn handle_pin(cmd: TrustPinCommand, now: DateTime<Utc>) -> Result<()> {
232    let (public_key_hex, curve) = resolve_pin_key(&cmd)?;
233
234    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
235
236    // Check if already pinned
237    if let Some(existing) = store.lookup(&cmd.did)? {
238        anyhow::bail!(
239            "Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
240            cmd.did,
241            existing.first_seen.format("%Y-%m-%d"),
242            cmd.did
243        );
244    }
245
246    let pin = PinnedIdentity {
247        did: cmd.did.clone(),
248        public_key_hex: public_key_hex.clone(),
249        curve,
250        kel_tip_said: cmd.kel_tip,
251        kel_sequence: None,
252        first_seen: now,
253        origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
254        trust_level: TrustLevel::Manual,
255    };
256
257    store.pin(pin)?;
258
259    if is_json_mode() {
260        JsonResponse::success(
261            "trust pin",
262            TrustActionResult {
263                did: cmd.did.clone(),
264            },
265        )
266        .print()?;
267    } else {
268        let out = Output::new();
269        out.println(&format!(
270            "{} Pinned identity: {}",
271            out.success("OK"),
272            &cmd.did
273        ));
274    }
275
276    Ok(())
277}
278
279fn handle_remove(cmd: TrustRemoveCommand) -> Result<()> {
280    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
281
282    // Check if exists
283    if store.lookup(&cmd.did)?.is_none() {
284        anyhow::bail!(
285            "Identity {} is not pinned. Pin it first with: auths trust pin {}",
286            cmd.did,
287            cmd.did
288        );
289    }
290
291    store.remove(&cmd.did)?;
292
293    if is_json_mode() {
294        JsonResponse::success(
295            "trust remove",
296            TrustActionResult {
297                did: cmd.did.clone(),
298            },
299        )
300        .print()?;
301    } else {
302        let out = Output::new();
303        out.println(&format!(
304            "{} Removed pin for: {}",
305            out.success("OK"),
306            &cmd.did
307        ));
308    }
309
310    Ok(())
311}
312
313fn handle_show(cmd: TrustShowCommand) -> Result<()> {
314    let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
315
316    let pin = store.lookup(&cmd.did)?.ok_or_else(|| {
317        anyhow!(
318            "Identity {} is not pinned. Pin it first with: auths trust pin {}",
319            cmd.did,
320            cmd.did
321        )
322    })?;
323
324    if is_json_mode() {
325        JsonResponse::success(
326            "trust show",
327            PinDetails {
328                did: pin.did.clone(),
329                public_key_hex: pin.public_key_hex.clone(),
330                trust_level: format!("{:?}", pin.trust_level),
331                first_seen: pin.first_seen.to_rfc3339(),
332                origin: pin.origin.clone(),
333                kel_tip_said: pin.kel_tip_said.clone(),
334                kel_sequence: pin.kel_sequence,
335            },
336        )
337        .print()?;
338    } else {
339        let out = Output::new();
340        out.println(&format!("DID:          {}", pin.did));
341        out.println(&format!("Public Key:   {}", pin.public_key_hex));
342        out.println(&format!("Trust Level:  {:?}", pin.trust_level));
343        out.println(&format!(
344            "First Seen:   {}",
345            pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
346        ));
347        out.println(&format!("Origin:       {}", pin.origin));
348        if let Some(ref tip) = pin.kel_tip_said {
349            out.println(&format!("Log checkpoint: {}", tip));
350        }
351        if let Some(seq) = pin.kel_sequence {
352            out.println(&format!("Log sequence:   {}", seq));
353        }
354    }
355
356    Ok(())
357}
358
359use crate::commands::executable::ExecutableCommand;
360use crate::config::CliConfig;
361
362impl ExecutableCommand for TrustCommand {
363    fn execute(&self, _ctx: &CliConfig) -> Result<()> {
364        handle_trust(self.clone())
365    }
366}