1use 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#[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(TrustListCommand),
41
42 Pin(TrustPinCommand),
44
45 Remove(TrustRemoveCommand),
47
48 Show(TrustShowCommand),
50}
51
52#[derive(Parser, Debug, Clone)]
54pub struct TrustListCommand {}
55
56#[derive(Parser, Debug, Clone)]
58pub struct TrustPinCommand {
59 #[clap(long, required = true)]
61 pub did: String,
62
63 #[clap(long, required = true)]
65 pub key: String,
66
67 #[clap(long)]
69 pub kel_tip: Option<String>,
70
71 #[clap(long)]
73 pub note: Option<String>,
74}
75
76#[derive(Parser, Debug, Clone)]
78pub struct TrustRemoveCommand {
79 pub did: String,
81}
82
83#[derive(Parser, Debug, Clone)]
85pub struct TrustShowCommand {
86 pub did: String,
88}
89
90#[derive(Debug, Serialize)]
92struct TrustActionResult {
93 did: String,
94}
95
96#[derive(Debug, Serialize)]
98struct PinListOutput {
99 pins: Vec<PinSummary>,
100}
101
102#[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#[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#[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 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 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}