From 6611a43a24da31b47e1228de615aa008ff5974e8 Mon Sep 17 00:00:00 2001 From: Malte Meiboom Date: Fri, 15 May 2026 12:59:24 +0200 Subject: [PATCH] Add subcommand to list local certificates - Add `husk locals list` to list all certificates which would be used by husk, but are not introduced by another certificate. These certificates are added 'locally'. - Refcator the code so that `local` -> `locals`. --- src/cli/cli_args.rs | 20 +++++----- src/commands.rs | 6 +-- src/commands/local.rs | 23 ----------- src/commands/local/remove.rs | 12 ------ src/commands/locals.rs | 35 +++++++++++++++++ src/commands/{local => locals}/add.rs | 4 +- src/commands/locals/remove.rs | 12 ++++++ src/common/crypto.rs | 56 ++++++++++++++++++++++++++- 8 files changed, 117 insertions(+), 51 deletions(-) delete mode 100644 src/commands/local.rs delete mode 100644 src/commands/local/remove.rs create mode 100644 src/commands/locals.rs rename src/commands/{local => locals}/add.rs (50%) create mode 100644 src/commands/locals/remove.rs diff --git a/src/cli/cli_args.rs b/src/cli/cli_args.rs index 1247fd2..c8075b8 100644 --- a/src/cli/cli_args.rs +++ b/src/cli/cli_args.rs @@ -25,7 +25,7 @@ pub struct CliArgs { pub enum HuskSubcommands { Daemon(DaemonCommand), Introducer(IntroducerCommand), - Local(LocalCommand), + Locals(LocalsCommand), } // Daemon subcommands @@ -134,23 +134,23 @@ pub struct IntroducerRemoveCommand { disable_colored_help = true, disable_version_flag = true, )] -pub struct LocalCommand { +pub struct LocalsCommand { #[clap(subcommand)] - pub subcommand: LocalSubcommand, + pub subcommand: LocalsSubcommand, } #[derive(Debug, Subcommand)] -pub enum LocalSubcommand { - Add(LocalAddCommand), - Remove(LocalRemoveCommand), +pub enum LocalsSubcommand { + Add(LocalsAddCommand), + Remove(LocalsRemoveCommand), List } #[derive(Parser, Debug)] #[clap( - name = "local_add", + name = "locals_add", about = "Add an authenticated certificate.", )] -pub struct LocalAddCommand { +pub struct LocalsAddCommand { #[clap( long = "cert", help = "Certificate to add." @@ -160,10 +160,10 @@ pub struct LocalAddCommand { #[derive(Parser, Debug)] #[clap( - name = "local_remove", + name = "locals_remove", about = "Certificate to remove.", )] -pub struct LocalRemoveCommand { +pub struct LocalsRemoveCommand { #[clap( long = "cert", help = "Certificate to removed." diff --git a/src/commands.rs b/src/commands.rs index e5d5df6..333cafb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -8,7 +8,7 @@ use crate::config::HuskConfigContainer; pub mod daemon; pub mod introducer; -pub mod local; +pub mod locals; #[derive(thiserror::Error, Debug)] pub enum CommandError { @@ -31,8 +31,8 @@ pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> { HuskSubcommands::Introducer(subcmd) => { commands::introducer::dispatch(subcmd, config).await?; }, - HuskSubcommands::Local(subcmd) => { - commands::local::dispatch(subcmd, config).await?; + HuskSubcommands::Locals(subcmd) => { + commands::locals::dispatch(subcmd, config).await?; }, } diff --git a/src/commands/local.rs b/src/commands/local.rs deleted file mode 100644 index f5ad066..0000000 --- a/src/commands/local.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anyhow::Result; - -use crate::cli::cli_args::{LocalCommand, LocalSubcommand}; -use crate::config::HuskConfigContainer; - -pub mod add; -pub mod remove; - -pub async fn dispatch(cmd: LocalCommand, config: HuskConfigContainer) -> Result<()> { - match cmd.subcommand { - LocalSubcommand::Add(add_cmd) => { - add::dispatch(add_cmd, &config).await?; - }, - LocalSubcommand::Remove(rm_cmd) => { - remove::dispatch(rm_cmd, &config).await?; - }, - LocalSubcommand::List => { - println!("list"); - }, - } - - Ok(()) -} diff --git a/src/commands/local/remove.rs b/src/commands/local/remove.rs deleted file mode 100644 index 9f7fa27..0000000 --- a/src/commands/local/remove.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anyhow::Result; - -use crate::cli::cli_args::LocalRemoveCommand; -use crate::config::HuskConfigContainer; - -pub async fn dispatch(cmd: LocalRemoveCommand, _config: &HuskConfigContainer) -> Result<()> { - println!("remove"); - println!(" {:?}", cmd.cert); - Ok(()) -} - - diff --git a/src/commands/locals.rs b/src/commands/locals.rs new file mode 100644 index 0000000..fafd74c --- /dev/null +++ b/src/commands/locals.rs @@ -0,0 +1,35 @@ +use anyhow::Result; + +use crate::cli::cli_args::{LocalsCommand, LocalsSubcommand}; +use crate::common::crypto; +use crate::config::HuskConfigContainer; +use crate::types::husk_context::HuskContext; + +pub mod add; +pub mod remove; + +pub async fn dispatch(cmd: LocalsCommand, config: HuskConfigContainer) -> Result<()> { + match cmd.subcommand { + LocalsSubcommand::Add(add_cmd) => { + add::dispatch(add_cmd, &config).await?; + }, + LocalsSubcommand::Remove(rm_cmd) => { + remove::dispatch(rm_cmd, &config).await?; + }, + LocalsSubcommand::List => { + let context = HuskContext::new(&config.into())?; + + println!("list locals"); + for local in crypto::get_locals( + &context.cert_store, + &context.policy, + context.local_trust_root.fingerprint()) { + + println!("{}", local); + } + + }, + } + + Ok(()) +} diff --git a/src/commands/local/add.rs b/src/commands/locals/add.rs similarity index 50% rename from src/commands/local/add.rs rename to src/commands/locals/add.rs index 756abb1..bc88bae 100644 --- a/src/commands/local/add.rs +++ b/src/commands/locals/add.rs @@ -1,9 +1,9 @@ use anyhow::Result; -use crate::cli::cli_args::LocalAddCommand; +use crate::cli::cli_args::LocalsAddCommand; use crate::config::HuskConfigContainer; -pub async fn dispatch(cmd: LocalAddCommand, _config: &HuskConfigContainer) -> Result<()> { +pub async fn dispatch(cmd: LocalsAddCommand, _config: &HuskConfigContainer) -> Result<()> { println!("add"); println!(" {:?}", cmd.cert_file); Ok(()) diff --git a/src/commands/locals/remove.rs b/src/commands/locals/remove.rs new file mode 100644 index 0000000..8966055 --- /dev/null +++ b/src/commands/locals/remove.rs @@ -0,0 +1,12 @@ +use anyhow::Result; + +use crate::cli::cli_args::LocalsRemoveCommand; +use crate::config::HuskConfigContainer; + +pub async fn dispatch(cmd: LocalsRemoveCommand, _config: &HuskConfigContainer) -> Result<()> { + println!("remove"); + println!(" {:?}", cmd.cert); + Ok(()) +} + + diff --git a/src/common/crypto.rs b/src/common/crypto.rs index 57dc08a..81452bb 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -4,6 +4,7 @@ use std::io::Write; use std::sync::Arc; use std::time::Duration; use std::collections::HashMap; +use std::collections::HashSet; use std::collections::hash_map::Entry::{Occupied, Vacant}; use anyhow; @@ -20,7 +21,7 @@ use sequoia_openpgp::cert::raw::RawCertParser; use sequoia_openpgp::types::SignatureType; use sequoia_cert_store::{Store, CertStore, LazyCert, StoreUpdate}; use sequoia_cert_store::store::MergePublicCollectStats; -use sequoia_wot::{self as wot}; +use sequoia_wot::{self as wot, CertSynopsis}; use wot::store::Store as _; use wot::{Depth, Path}; @@ -309,6 +310,59 @@ pub fn get_introducers<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, result } +/// Returns all local certificates available in cert_store. +pub fn get_locals<'c>(cert_store: &CertStore<'c>, policy: &StandardPolicy, local_trust_root: Fingerprint) -> Vec { + + let mut set = HashMap::new(); + let mut introducers = HashSet::new(); + let mut targets = Vec::new(); + let local_root_fpr = local_trust_root.clone(); + + let trust_roots = vec![(local_trust_root, wot::FULLY_TRUSTED)]; + let wot_store = wot::store::CertStore::from_store(cert_store, policy, None); + let n = wot::NetworkBuilder::rooted(&wot_store, &*trust_roots) + .build(); + + n.certified_userids().iter().for_each(|(fpr, user_id)| { + let paths = n.authenticate(user_id, fpr, wot::FULLY_TRUSTED); + + paths.iter().for_each(|(path, amount)| { + if *amount == wot::FULLY_TRUSTED { + if path.residual_depth() == Depth::Limit(0) { + // A leaf, which is not an introducer itself. + targets.push(path.clone()); + } else { + // Introducer encountered. Do not add the local trust + // root to the introducers HashSet, otherwise all local + // certificates would be viewed as introduced. + if path.target().fingerprint() != local_root_fpr { + introducers.insert(path.target().fingerprint()); + } + } + } + }); + }); + + for p in targets { + let cert_synopsis = p.target().clone(); + + match set.entry(cert_synopsis.fingerprint()) { + Vacant(_) => { + // Only consider non introduced certificates. + if !p.certifications().any(|c| + introducers.contains(&c.issuer().fingerprint())) { + + set.insert(cert_synopsis.fingerprint(), cert_synopsis); + } + }, + // The target is already known, do nothing. + _ => {} + } + } + set.into_values().collect() +} + + /// Checks if a message contains the OpenPGP artifacts of an encrypted message. pub fn is_encrypted(body: &Bytes) -> bool {