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