use crate::ux::format::{JsonResponse, Output, is_json_mode};
use anyhow::{Context, Result, anyhow};
use auths_sdk::trust::{PinnedIdentity, PinnedIdentityStore, TrustLevel};
use auths_verifier::PublicKeyHex;
use chrono::{DateTime, Utc};
use clap::{Parser, Subcommand};
use serde::Serialize;
#[derive(Parser, Debug, Clone)]
#[command(
name = "trust",
about = "Pin identities you trust for verification",
after_help = "Examples:
auths trust list # Show all pinned trusted identities
auths trust pin --did did:keri:EExample
# Pin an identity (key resolved from its local KEL)
auths trust pin --did did:keri:EExample --bundle their-bundle.json
# Pin from an exported identity bundle
auths trust remove did:keri:EExample
# Remove a pinned identity
auths trust show did:keri:EExample
# Show details of a trusted identity
Related:
auths verify — Verify signatures (uses trust store)
auths sign — Create signatures
auths error — Troubleshoot trust policy errors"
)]
pub struct TrustCommand {
#[command(subcommand)]
pub command: TrustSubcommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum TrustSubcommand {
List(TrustListCommand),
Pin(TrustPinCommand),
Remove(TrustRemoveCommand),
Show(TrustShowCommand),
}
#[derive(Parser, Debug, Clone)]
pub struct TrustListCommand {}
#[derive(Parser, Debug, Clone)]
pub struct TrustPinCommand {
#[clap(long, required = true)]
pub did: String,
#[clap(long)]
pub key: Option<String>,
#[clap(long)]
pub bundle: Option<std::path::PathBuf>,
#[clap(long)]
pub kel_tip: Option<String>,
#[clap(long)]
pub note: Option<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct TrustRemoveCommand {
pub did: String,
}
#[derive(Parser, Debug, Clone)]
pub struct TrustShowCommand {
pub did: String,
}
#[derive(Debug, Serialize)]
struct TrustActionResult {
did: String,
}
#[derive(Debug, Serialize)]
struct PinListOutput {
pins: Vec<PinSummary>,
}
#[derive(Debug, Serialize)]
struct PinSummary {
did: String,
trust_level: String,
first_seen: String,
#[serde(skip_serializing_if = "Option::is_none")]
kel_sequence: Option<u128>,
}
#[derive(Debug, Serialize)]
struct PinDetails {
did: String,
public_key_hex: PublicKeyHex,
trust_level: String,
first_seen: String,
origin: String,
#[serde(skip_serializing_if = "Option::is_none")]
kel_tip_said: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
kel_sequence: Option<u128>,
}
#[allow(clippy::disallowed_methods)]
pub fn handle_trust(cmd: TrustCommand) -> Result<()> {
let now = Utc::now();
match cmd.command {
TrustSubcommand::List(list_cmd) => handle_list(list_cmd),
TrustSubcommand::Pin(pin_cmd) => handle_pin(pin_cmd, now),
TrustSubcommand::Remove(remove_cmd) => handle_remove(remove_cmd),
TrustSubcommand::Show(show_cmd) => handle_show(show_cmd),
}
}
fn handle_list(_cmd: TrustListCommand) -> Result<()> {
let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
let pins = store.list()?;
if is_json_mode() {
JsonResponse::success(
"trust list",
PinListOutput {
pins: pins
.iter()
.map(|p| PinSummary {
did: p.did.clone(),
trust_level: format!("{:?}", p.trust_level),
first_seen: p.first_seen.to_rfc3339(),
kel_sequence: p.kel_sequence,
})
.collect(),
},
)
.print()?;
} else {
let out = Output::new();
if pins.is_empty() {
out.println(&out.dim("No pinned identities."));
out.println("");
out.println("Use 'auths trust pin --did <DID> --key <HEX>' to pin an identity.");
} else {
out.println(&format!("{} pinned identities:", pins.len()));
out.println("");
for pin in &pins {
let level = match pin.trust_level {
TrustLevel::Tofu => out.dim("TOFU"),
TrustLevel::Manual => out.info("Manual"),
TrustLevel::OrgPolicy => out.success("OrgPolicy"),
};
out.println(&format!(" {} [{}]", pin.did, level));
}
}
}
Ok(())
}
fn resolve_pin_key(cmd: &TrustPinCommand) -> Result<(PublicKeyHex, auths_crypto::CurveType)> {
if let Some(ref key_hex) = cmd.key {
let public_key_hex = PublicKeyHex::parse(key_hex).context("Invalid public key hex")?;
let curve = auths_crypto::did_key_decode(&cmd.did)
.map(|d| d.curve())
.unwrap_or_default();
return Ok((public_key_hex, curve));
}
if let Some(ref bundle_path) = cmd.bundle {
let content = std::fs::read_to_string(bundle_path)
.with_context(|| format!("Failed to read identity bundle: {bundle_path:?}"))?;
let bundle: auths_verifier::IdentityBundle = serde_json::from_str(&content)
.with_context(|| format!("Failed to parse identity bundle: {bundle_path:?}"))?;
if bundle.identity_did.as_str() != cmd.did {
anyhow::bail!(
"Bundle is for {} but --did is {}",
bundle.identity_did.as_str(),
cmd.did
);
}
return Ok((bundle.public_key_hex.clone(), bundle.curve));
}
let auths_home = auths_sdk::paths::auths_home().map_err(|e| anyhow!(e))?;
let registry = auths_sdk::storage::GitRegistryBackend::from_config_unchecked(
auths_sdk::storage::RegistryConfig::single_tenant(&auths_home),
);
let (pk, curve) = auths_sdk::keri::resolve_current_public_key(®istry, &cmd.did)
.with_context(|| {
format!(
"Could not resolve {} from the local registry. Provide --bundle <file> \
(ask the identity owner for `auths id export-bundle`) or --key <hex>.",
cmd.did
)
})?;
#[allow(clippy::disallowed_methods)] Ok((PublicKeyHex::new_unchecked(hex::encode(pk)), curve))
}
fn handle_pin(cmd: TrustPinCommand, now: DateTime<Utc>) -> Result<()> {
let (public_key_hex, curve) = resolve_pin_key(&cmd)?;
let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
if let Some(existing) = store.lookup(&cmd.did)? {
anyhow::bail!(
"Identity {} is already pinned (first seen: {}). Use 'auths trust remove {}' first.",
cmd.did,
existing.first_seen.format("%Y-%m-%d"),
cmd.did
);
}
let pin = PinnedIdentity {
did: cmd.did.clone(),
public_key_hex: public_key_hex.clone(),
curve,
kel_tip_said: cmd.kel_tip,
kel_sequence: None,
first_seen: now,
origin: cmd.note.unwrap_or_else(|| "manual".to_string()),
trust_level: TrustLevel::Manual,
};
store.pin(pin)?;
if is_json_mode() {
JsonResponse::success(
"trust pin",
TrustActionResult {
did: cmd.did.clone(),
},
)
.print()?;
} else {
let out = Output::new();
out.println(&format!(
"{} Pinned identity: {}",
out.success("OK"),
&cmd.did
));
}
Ok(())
}
fn handle_remove(cmd: TrustRemoveCommand) -> Result<()> {
let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
if store.lookup(&cmd.did)?.is_none() {
anyhow::bail!(
"Identity {} is not pinned. Pin it first with: auths trust pin {}",
cmd.did,
cmd.did
);
}
store.remove(&cmd.did)?;
if is_json_mode() {
JsonResponse::success(
"trust remove",
TrustActionResult {
did: cmd.did.clone(),
},
)
.print()?;
} else {
let out = Output::new();
out.println(&format!(
"{} Removed pin for: {}",
out.success("OK"),
&cmd.did
));
}
Ok(())
}
fn handle_show(cmd: TrustShowCommand) -> Result<()> {
let store = PinnedIdentityStore::new(PinnedIdentityStore::default_path());
let pin = store.lookup(&cmd.did)?.ok_or_else(|| {
anyhow!(
"Identity {} is not pinned. Pin it first with: auths trust pin {}",
cmd.did,
cmd.did
)
})?;
if is_json_mode() {
JsonResponse::success(
"trust show",
PinDetails {
did: pin.did.clone(),
public_key_hex: pin.public_key_hex.clone(),
trust_level: format!("{:?}", pin.trust_level),
first_seen: pin.first_seen.to_rfc3339(),
origin: pin.origin.clone(),
kel_tip_said: pin.kel_tip_said.clone(),
kel_sequence: pin.kel_sequence,
},
)
.print()?;
} else {
let out = Output::new();
out.println(&format!("DID: {}", pin.did));
out.println(&format!("Public Key: {}", pin.public_key_hex));
out.println(&format!("Trust Level: {:?}", pin.trust_level));
out.println(&format!(
"First Seen: {}",
pin.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
));
out.println(&format!("Origin: {}", pin.origin));
if let Some(ref tip) = pin.kel_tip_said {
out.println(&format!("Log checkpoint: {}", tip));
}
if let Some(seq) = pin.kel_sequence {
out.println(&format!("Log sequence: {}", seq));
}
}
Ok(())
}
use crate::commands::executable::ExecutableCommand;
use crate::config::CliConfig;
impl ExecutableCommand for TrustCommand {
fn execute(&self, _ctx: &CliConfig) -> Result<()> {
handle_trust(self.clone())
}
}