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;
12use std::path::PathBuf;
13
14#[derive(Parser, Debug, Clone)]
16#[command(
17 name = "trust",
18 about = "Pin identities you trust for verification",
19 after_help = "Examples:
20 auths trust list # Show all pinned trusted identities
21 auths trust pin --did did:keri:EExample
22 # Pin an identity (key resolved from its local KEL)
23 auths trust pin --did did:keri:EExample --bundle their-bundle.json
24 # Pin from an exported identity bundle
25 auths trust remove did:keri:EExample
26 # Remove a pinned identity
27 auths trust show did:keri:EExample
28 # Show details of a trusted identity
29
30Related:
31 auths verify — Verify signatures (uses trust store)
32 auths sign — Create signatures
33 auths error — Troubleshoot trust policy errors"
34)]
35pub struct TrustCommand {
36 #[command(subcommand)]
37 pub command: TrustSubcommand,
38}
39
40#[derive(Subcommand, Debug, Clone)]
41pub enum TrustSubcommand {
42 List(TrustListCommand),
44
45 Pin(TrustPinCommand),
47
48 Remove(TrustRemoveCommand),
50
51 Show(TrustShowCommand),
53}
54
55#[derive(Parser, Debug, Clone)]
57pub struct TrustListCommand {}
58
59#[derive(Parser, Debug, Clone)]
61pub struct TrustPinCommand {
62 #[clap(long, required = true)]
64 pub did: String,
65
66 #[clap(long)]
70 pub key: Option<String>,
71
72 #[clap(long)]
75 pub bundle: Option<std::path::PathBuf>,
76
77 #[clap(long)]
79 pub kel_tip: Option<String>,
80
81 #[clap(long)]
83 pub note: Option<String>,
84}
85
86#[derive(Parser, Debug, Clone)]
88pub struct TrustRemoveCommand {
89 pub did: String,
91}
92
93#[derive(Parser, Debug, Clone)]
95pub struct TrustShowCommand {
96 pub did: String,
98}
99
100#[derive(Debug, Serialize)]
102struct TrustActionResult {
103 did: String,
104}
105
106#[derive(Debug, Serialize)]
108struct PinListOutput {
109 pins: Vec<PinSummary>,
110}
111
112#[derive(Debug, Serialize)]
114struct PinSummary {
115 did: String,
116 trust_level: String,
117 first_seen: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 kel_sequence: Option<u128>,
120}
121
122#[derive(Debug, Serialize)]
124struct PinDetails {
125 did: String,
126 public_key_hex: PublicKeyHex,
127 trust_level: String,
128 first_seen: String,
129 origin: String,
130 #[serde(skip_serializing_if = "Option::is_none")]
131 kel_tip_said: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 kel_sequence: Option<u128>,
134}
135
136fn pinned_store(repo: Option<PathBuf>) -> Result<PinnedIdentityStore> {
146 let registry = auths_sdk::storage_layout::resolve_repo_path(repo)
147 .context("Failed to resolve the repository path for the trust store")?;
148 let default = PinnedIdentityStore::default_path();
149 let file_name = default
150 .file_name()
151 .ok_or_else(|| anyhow!("pin store path has no file name"))?;
152 Ok(PinnedIdentityStore::new(registry.join(file_name)))
153}
154
155#[allow(clippy::disallowed_methods)]
157pub fn handle_trust(cmd: TrustCommand, repo: Option<PathBuf>) -> Result<()> {
158 let store = pinned_store(repo)?;
159 let now = Utc::now();
160 match cmd.command {
161 TrustSubcommand::List(list_cmd) => handle_list(list_cmd, &store),
162 TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd, &store, now),
163 TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd, &store),
164 TrustSubcommand::Show(show_cmd) => handle_show(show_cmd, &store),
165 }
166}
167
168fn handle_list(_cmd: TrustListCommand, store: &PinnedIdentityStore) -> Result<()> {
169 let pins = store.list()?;
170
171 if is_json_mode() {
172 JsonResponse::success(
173 "trust list",
174 PinListOutput {
175 pins: pins
176 .iter()
177 .map(|p| PinSummary {
178 did: p.did.clone(),
179 trust_level: format!("{:?}", p.trust_level),
180 first_seen: p.first_seen.to_rfc3339(),
181 kel_sequence: p.kel_sequence,
182 })
183 .collect(),
184 },
185 )
186 .print()?;
187 } else {
188 let out = Output::new();
189 if pins.is_empty() {
190 out.println(&out.dim("No pinned identities."));
191 out.println("");
192 out.println("Use 'auths trust pin --did <DID> --key <HEX>' to pin an identity.");
193 } else {
194 out.println(&format!("{} pinned identities:", pins.len()));
195 out.println("");
196 for pin in &pins {
197 let level = match pin.trust_level {
198 TrustLevel::Tofu => out.dim("TOFU"),
199 TrustLevel::Manual => out.info("Manual"),
200 TrustLevel::OrgPolicy => out.success("OrgPolicy"),
201 };
202 out.println(&format!(" {} [{}]", pin.did, level));
203 }
204 }
205 }
206
207 Ok(())
208}
209
210fn resolve_local_current_key(did: &str) -> Result<Option<(PublicKeyHex, auths_crypto::CurveType)>> {
217 let auths_home = auths_sdk::paths::auths_home().map_err(|e| anyhow!(e))?;
218 let registry = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
219 auths_sdk::storage::RegistryConfig::single_tenant(&auths_home),
220 );
221 match auths_sdk::keri::resolve_current_public_key(®istry, did) {
222 Ok((pk, curve)) => {
223 #[allow(clippy::disallowed_methods)] let hex_key = PublicKeyHex::new_unchecked(hex::encode(pk));
225 Ok(Some((hex_key, curve)))
226 }
227 Err(err) if is_kel_not_found(&err) => Ok(None),
228 Err(e) => Err(anyhow!(e)).with_context(|| {
229 format!("Could not resolve the current key for {did} from the local registry")
230 }),
231 }
232}
233
234fn is_kel_not_found(err: &auths_sdk::keri::CurrentKeyError) -> bool {
244 err.to_string().contains("KEL not found for")
245}
246
247fn resolve_pin_key(cmd: &TrustPinCommand) -> Result<(PublicKeyHex, auths_crypto::CurveType)> {
257 if let Some(ref key_hex) = cmd.key {
258 let public_key_hex = PublicKeyHex::parse(key_hex).context("Invalid public key hex")?;
259 let curve = auths_crypto::did_key_decode(&cmd.did)
260 .map(|d| d.curve())
261 .unwrap_or_default();
262 if let Some((kel_key, kel_curve)) = resolve_local_current_key(&cmd.did)? {
263 if kel_key != public_key_hex {
264 anyhow::bail!(
265 "the supplied --key does not match the current key in {}'s key history \
266 (KEL); refusing to pin a key the identity does not control. Omit --key to \
267 pin the KEL-resolved key, or use --bundle.",
268 cmd.did
269 );
270 }
271 return Ok((kel_key, kel_curve));
272 }
273 return Ok((public_key_hex, curve));
274 }
275 if let Some(ref bundle_path) = cmd.bundle {
276 let content = std::fs::read_to_string(bundle_path)
277 .with_context(|| format!("Failed to read identity bundle: {bundle_path:?}"))?;
278 let bundle: auths_verifier::IdentityBundle = serde_json::from_str(&content)
279 .with_context(|| format!("Failed to parse identity bundle: {bundle_path:?}"))?;
280 if bundle.identity_did.as_str() != cmd.did {
281 anyhow::bail!(
282 "Bundle is for {} but --did is {}",
283 bundle.identity_did.as_str(),
284 cmd.did
285 );
286 }
287 return Ok((bundle.public_key_hex.clone(), bundle.curve));
288 }
289 let (key, curve) = resolve_local_current_key(&cmd.did)?.ok_or_else(|| {
290 anyhow!(
291 "Could not resolve {} from the local registry. Provide --bundle <file> \
292 (ask the identity owner for `auths id export-bundle`) or --key <hex>.",
293 cmd.did
294 )
295 })?;
296 Ok((key, curve))
297}
298
299fn handle_pin(cmd: TrustPinCommand, store: &PinnedIdentityStore, now: DateTime<Utc>) -> Result<()> {
300 let (public_key_hex, curve) = resolve_pin_key(&cmd)?;
301
302 if let Some(existing) = store.lookup(&cmd.did)? {
304 anyhow::bail!(
305 "Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
306 cmd.did,
307 existing.first_seen.format("%Y-%m-%d"),
308 cmd.did
309 );
310 }
311
312 let pin = PinnedIdentity {
313 did: cmd.did.clone(),
314 public_key_hex: public_key_hex.clone(),
315 curve,
316 kel_tip_said: cmd.kel_tip,
317 kel_sequence: None,
318 first_seen: now,
319 origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
320 trust_level: TrustLevel::Manual,
321 };
322
323 store.pin(pin)?;
324
325 if is_json_mode() {
326 JsonResponse::success(
327 "trust pin",
328 TrustActionResult {
329 did: cmd.did.clone(),
330 },
331 )
332 .print()?;
333 } else {
334 let out = Output::new();
335 out.println(&format!(
336 "{} Pinned identity: {}",
337 out.success("OK"),
338 &cmd.did
339 ));
340 }
341
342 Ok(())
343}
344
345fn handle_remove(cmd: TrustRemoveCommand, store: &PinnedIdentityStore) -> Result<()> {
346 if store.lookup(&cmd.did)?.is_none() {
348 anyhow::bail!(
349 "Identity {} is not pinned. Pin it first with: auths trust pin {}",
350 cmd.did,
351 cmd.did
352 );
353 }
354
355 store.remove(&cmd.did)?;
356
357 if is_json_mode() {
358 JsonResponse::success(
359 "trust remove",
360 TrustActionResult {
361 did: cmd.did.clone(),
362 },
363 )
364 .print()?;
365 } else {
366 let out = Output::new();
367 out.println(&format!(
368 "{} Removed pin for: {}",
369 out.success("OK"),
370 &cmd.did
371 ));
372 }
373
374 Ok(())
375}
376
377fn handle_show(cmd: TrustShowCommand, store: &PinnedIdentityStore) -> Result<()> {
378 let pin = store.lookup(&cmd.did)?.ok_or_else(|| {
379 anyhow!(
380 "Identity {} is not pinned. Pin it first with: auths trust pin {}",
381 cmd.did,
382 cmd.did
383 )
384 })?;
385
386 if is_json_mode() {
387 JsonResponse::success(
388 "trust show",
389 PinDetails {
390 did: pin.did.clone(),
391 public_key_hex: pin.public_key_hex.clone(),
392 trust_level: format!("{:?}", pin.trust_level),
393 first_seen: pin.first_seen.to_rfc3339(),
394 origin: pin.origin.clone(),
395 kel_tip_said: pin.kel_tip_said.clone(),
396 kel_sequence: pin.kel_sequence,
397 },
398 )
399 .print()?;
400 } else {
401 let out = Output::new();
402 out.println(&format!("DID: {}", pin.did));
403 out.println(&format!("Public Key: {}", pin.public_key_hex));
404 out.println(&format!("Trust Level: {:?}", pin.trust_level));
405 out.println(&format!(
406 "First Seen: {}",
407 pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
408 ));
409 out.println(&format!("Origin: {}", pin.origin));
410 if let Some(ref tip) = pin.kel_tip_said {
411 out.println(&format!("Log checkpoint: {}", tip));
412 }
413 if let Some(seq) = pin.kel_sequence {
414 out.println(&format!("Log sequence: {}", seq));
415 }
416 }
417
418 Ok(())
419}
420
421use crate::commands::executable::ExecutableCommand;
422use crate::config::CliConfig;
423
424impl ExecutableCommand for TrustCommand {
425 fn execute(&self, ctx: &CliConfig) -> Result<()> {
426 handle_trust(self.clone(), ctx.repo_path.clone())
427 }
428}