Add introducer add command

- Create a command to add an introducer (with domains).
- Add a module for escaping regular expressions used in certifications
- Add some helper functions.
This commit is contained in:
Malte Meiboom
2026-05-04 22:55:02 +02:00
parent 5f50624478
commit bf53af50ec
9 changed files with 213 additions and 8 deletions
+121 -2
View File
@@ -4,10 +4,38 @@
// introducers
//
use std::sync::Arc;
use std::collections::HashSet;
use anyhow::Result;
use sequoia_cert_store::StoreUpdate;
use sequoia_cert_store::store::MergePublicCollectStats;
use sequoia_openpgp::packet::prelude::SignatureBuilder;
use sequoia_wot as wot;
use wot::{CertSynopsis, Certification};
use sequoia_openpgp::policy::StandardPolicy;
use sequoia_openpgp::{cert::ValidCert, policy::StandardPolicy};
use sequoia_openpgp::Cert;
use sequoia_openpgp::Fingerprint;
use sequoia_openpgp::types::SignatureType;
use crate::mail;
use crate::types::husk_context::HuskContext;
use crate::common::escape;
#[derive(thiserror::Error, Debug)]
pub enum IntroducerError {
#[error("Unsuitable certificate")]
UnsuitableCertificate,
#[error("Unlimited introducer")]
UnlimitedIntroducer,
#[error("Invalid domain name: '{0}'")]
InvalidDomainName(String),
#[error("Certificate {0} contains no usable user id")]
NoUserIDs(Fingerprint),
#[error("Local trust root is damanged")]
RootDamaged,
}
#[derive(Debug, Clone)]
pub struct Introducer {
@@ -54,6 +82,97 @@ impl Introducer {
result
}
pub fn create(context: &HuskContext, cert: &ValidCert, domains: Vec<String>)
-> Result<()> {
// Check if the local trust root certificate should be made an
// introducer, which would be an error.
if cert.fingerprint() == context.local_trust_root.fingerprint() {
return Err(IntroducerError::UnsuitableCertificate.into());
}
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(IntroducerError::RootDamaged.into());
}
};
let mut root_signer = root_signing_key
.clone()
.parts_into_secret()?
.into_keypair()?;
// Check if domain list is not empty.
// An empty list would create an unlimited introducer.
if domains.is_empty() {
return Err(IntroducerError::UnlimitedIntroducer.into());
}
// Create the SignatureBuilder with a default preconfiguration.
let mut builder =
SignatureBuilder::new(SignatureType::GenericCertification)
.set_signature_creation_time(std::time::SystemTime::now())?
.set_trust_signature(1, 120)?
.set_exportable_certification(false)?
.set_revocable(true)?;
// set no expiration date
// Add domains
for domain in domains {
if !mail::is_domain(domain.as_str()) {
return Err(IntroducerError::InvalidDomainName(domain).into());
}
builder = builder.add_regular_expression(format!("<[^>]+[@.]{}>$",
escape::regex_ctrl_escape(domain.as_str())?
))?;
}
// 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(IntroducerError::NoUserIDs(cert.fingerprint()).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(())
}
}
use std::fmt::{Display, Formatter};
@@ -65,7 +184,7 @@ impl Display for Introducer {
.collect::<Vec<String>>()
.join(", ");
writeln!(f, "Introducer: {}", self.cert.fingerprint())?;
write!(f, " UserIDs: {}", uids)?;
writeln!(f, " UserIDs: {}", uids)?;
Ok(())
}
}