Implement 'introducer remove'

- Implement the subcommand `remove` for `introducer`.
- Remove the introducer by adding a certification with trust level 1 and
  trust depth 0.
- Move `crypt.rs` into `common/` and add a generic authenticaton
  function to be used by `introducer add` and `... remove`.
This commit is contained in:
Malte Meiboom
2026-05-06 15:38:33 +02:00
parent 9a6de637ea
commit e98f5f7576
4 changed files with 137 additions and 85 deletions
+104
View File
@@ -7,15 +7,19 @@ use std::collections::HashMap;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use anyhow;
use sequoia_openpgp::cert::ValidCert;
use tokio::task::JoinSet;
use bytes::Bytes;
use sequoia_net::{KeyServer, wkd, dane};
use sequoia_openpgp::packet::prelude::SignatureBuilder;
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::serialize::stream::{Message, Armorer, Encryptor, LiteralWriter};
use sequoia_openpgp::{Fingerprint, Cert, Packet};
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 wot::store::Store as _;
use wot::{Depth, Path};
@@ -24,6 +28,8 @@ use crate::types::errors::HuskError;
use crate::types::husk_context::HuskContext;
use crate::types::introducer::Introducer;
use crate::types::recipient::Recipient;
use crate::mail;
use crate::common::escape;
/// Returns the local trust root from the cert_store. If the trust root
/// cannot be found, it returns an error.
@@ -368,6 +374,104 @@ pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipie
}
}
/// Authenticate a certificate.
///
/// If `activate` is true, the certificate becomes an introducer for
/// `domains`. Passing `None` or an empty list in this case is an error.
/// If `activate` is false, create a certifiaction with minimal trust_roots
/// amount and no depth. In this case `domains` is ignored.
pub fn authenticate(context: &HuskContext, cert: &ValidCert, activate: bool, domains: Option<Vec<String>>)
-> anyhow::Result<()> {
let policy = &context.policy;
let root_signing_key = match context.local_trust_root.with_policy(policy, None)?
.keys()
.supported()
.alive()
.revoked(false)
.for_certification()
.next() {
Some(k) => k.key(),
None => {
return Err(HuskError::DamangedLocalTrustRoot(
"missing certification capability".to_string())
.into());
}
};
let mut root_signer = root_signing_key
.clone()
.parts_into_secret()?
.into_keypair()?;
// Create the SignatureBuilder with a default preconfiguration.
let mut builder =
SignatureBuilder::new(SignatureType::GenericCertification)
.set_signature_creation_time(std::time::SystemTime::now())?
.set_exportable_certification(false)?
.set_revocable(true)?;
// set no expiration date
// Add domains
if activate {
// set the trust level and depth for an active introducer.
builder = builder.set_trust_signature(1, 120)?;
if let Some(domains) = domains {
for domain in domains {
if !mail::is_domain(domain.as_str()) {
return Err(HuskError::InvalidDomainName(domain).into());
}
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
escape::regex_ctrl_escape(domain.as_str())?
))?;
}
} else {
// An introducer without domains would be unlimited.
return Err(HuskError::UnlimitedIntroducer.into());
}
} else {
// deactive the introducer
builder = builder.set_trust_signature(0, 1)?;
}
// For a certification, user ids are needed. Build a list of
// valid (non self revoked) user ids.
let user_ids: Vec<_> = cert.userids()
.filter(|u| {
u.self_revocations().count() == 0
})
.collect();
if user_ids.is_empty() {
return Err(HuskError::NoUserIDs(cert.fingerprint().to_hex()).into());
}
// now certify
let signatures: Vec<_> = user_ids.into_iter()
.filter_map(|u| {
let b = builder.clone();
b.sign_userid_binding(
&mut root_signer,
cert.primary_key().key(),
u.userid()).ok()
})
.collect();
let (new_cert, _) = cert.cert().clone().insert_packets(signatures)?;
// and (re)import
let new_cert = Arc::new(new_cert.into());
let stats = MergePublicCollectStats::new();
let cert_store = &context.cert_store;
cert_store.update_by(new_cert, &stats)?;
log::debug!("{:?}", stats);
// XXX: do something with the created stats
Ok(())
}
#[cfg(test)]
pub mod tests {