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`.
This commit is contained in:
Malte Meiboom
2026-05-15 12:59:24 +02:00
parent 713ff86a9e
commit 6611a43a24
8 changed files with 117 additions and 51 deletions
+10 -10
View File
@@ -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."
+3 -3
View File
@@ -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?;
},
}
-23
View File
@@ -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(())
}
-12
View File
@@ -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(())
}
+35
View File
@@ -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(())
}
@@ -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(())
+12
View File
@@ -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(())
}
+55 -1
View File
@@ -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<CertSynopsis> {
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 {