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
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(TrustListCommand),
43
44 Pin(TrustPinCommand),
46
47 Remove(TrustRemoveCommand),
49
50 Show(TrustShowCommand),
52}
53
54#[derive(Parser, Debug, Clone)]
56pub struct TrustListCommand {}
57
58#[derive(Parser, Debug, Clone)]
60pub struct TrustPinCommand {
61 #[clap(long, required = true)]
63 pub did: String,
64
65 #[clap(long)]
69 pub key: Option<String>,
70
71 #[clap(long)]
74 pub bundle: Option<std::path::PathBuf>,
75
76 #[clap(long)]
78 pub kel_tip: Option<String>,
79
80 #[clap(long)]
82 pub note: Option<String>,
83}
84
85#[derive(Parser, Debug, Clone)]
87pub struct TrustRemoveCommand {
88 pub did: String,
90}
91
92#[derive(Parser, Debug, Clone)]
94pub struct TrustShowCommand {
95 pub did: String,
97}
98
99#[derive(Debug, Serialize)]
101struct TrustActionResult {
102 did: String,
103}
104
105#[derive(Debug, Serialize)]
107struct PinListOutput {
108 pins: Vec<PinSummary>,
109}
110
111#[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#[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#[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
190fn 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(®istry, &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)] 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 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 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}