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:
Generated
+2
-1
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
@@ -987,6 +987,7 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
name = "husk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"clap",
|
||||
|
||||
@@ -11,6 +11,7 @@ categories = ["cryptography", "email"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[dependencies]
|
||||
aho-corasick = "1.1.4"
|
||||
anyhow = "1.0.100"
|
||||
bytes = "1.11.1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use sequoia_openpgp::Fingerprint;
|
||||
|
||||
use crate::commands;
|
||||
use crate::cli::cli_args::{CliArgs, HuskSubcommands};
|
||||
use crate::config::HuskConfigContainer;
|
||||
@@ -8,6 +10,14 @@ pub mod daemon;
|
||||
pub mod introducer;
|
||||
pub mod local;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum CommandError {
|
||||
#[error("Certificate not found: {0}")]
|
||||
CertNotFound(Fingerprint),
|
||||
#[error("List of domains is empty")]
|
||||
DomainListEmpty,
|
||||
}
|
||||
|
||||
pub async fn dispatch(cli: CliArgs, config: HuskConfigContainer) -> Result<()> {
|
||||
|
||||
match cli.subcommand {
|
||||
|
||||
@@ -11,7 +11,7 @@ pub mod remove;
|
||||
pub async fn dispatch(cmd: IntroducerCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
match cmd.subcommand {
|
||||
IntroducerSubcommand::Add(add_cmd) => {
|
||||
add::dispatch(add_cmd, &config).await?;
|
||||
add::dispatch(add_cmd, config).await?;
|
||||
},
|
||||
IntroducerSubcommand::Remove(rm_cmd) => {
|
||||
remove::dispatch(rm_cmd, &config).await?;
|
||||
|
||||
@@ -1,12 +1,40 @@
|
||||
use anyhow::Result;
|
||||
use sequoia_cert_store::Store;
|
||||
use sequoia_openpgp::cert::ValidCert;
|
||||
use sequoia_openpgp::{Fingerprint, KeyHandle};
|
||||
|
||||
use crate::cli::cli_args::IntroducerAddCommand;
|
||||
use crate::commands::CommandError;
|
||||
use crate::config::HuskConfigContainer;
|
||||
use crate::types::husk_context::HuskContext;
|
||||
use crate::types::introducer::Introducer;
|
||||
|
||||
pub async fn dispatch(cmd: IntroducerAddCommand, _config: &HuskConfigContainer) -> Result<()> {
|
||||
pub async fn dispatch(cmd: IntroducerAddCommand, config: HuskConfigContainer) -> Result<()> {
|
||||
println!("add");
|
||||
println!(" {:?}", cmd.cert);
|
||||
println!(" {:?}", cmd.domains);
|
||||
|
||||
let context = HuskContext::new(&config.into())?;
|
||||
let policy = &context.policy;
|
||||
let cert_store = &context.cert_store;
|
||||
|
||||
// Get the certificate
|
||||
let fpr = Fingerprint::from_hex(&cmd.cert)?;
|
||||
let certs = cert_store.lookup_by_cert(&KeyHandle::try_from(&fpr)?)
|
||||
.map_err(|_| CommandError::CertNotFound(fpr.clone()))?;
|
||||
|
||||
let certs: Vec<ValidCert> = certs.iter()
|
||||
.filter_map(|c|
|
||||
c.with_policy(policy, None).ok()
|
||||
)
|
||||
.collect();
|
||||
|
||||
if let Some(cert) = certs.first() {
|
||||
Introducer::create(&context, cert, cmd.domains)?;
|
||||
} else {
|
||||
return Err(CommandError::CertNotFound(fpr).into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+33
-2
@@ -4,9 +4,9 @@ use aho_corasick::AhoCorasick;
|
||||
use anyhow::Result;
|
||||
|
||||
const REGEX_CONTROL_CHARS: &[&str]
|
||||
= &[ ".", "|", "(", ")", "*", "+", "?", "^", "$", "[", "]", "\\" ];
|
||||
= &[ "\\", ".", "|", "(", ")", "*", "+", "?", "^", "$", "[", "]" ];
|
||||
const REGEX_ESCAPED_CHARS: &[&str]
|
||||
= &[ "\\.", "\\|", "\\(", "\\)", "\\*", "\\+", "\\?", "\\^", "\\$", "\\[", "\\]", "\\\\" ];
|
||||
= &[ "\\\\", "\\.", "\\|", "\\(", "\\)", "\\*", "\\+", "\\?", "\\^", "\\$", "\\[", "\\]" ];
|
||||
|
||||
pub fn regex_ctrl_escape(value: &str) -> Result<String> {
|
||||
let r = AhoCorasick::new(REGEX_CONTROL_CHARS)?;
|
||||
@@ -18,6 +18,24 @@ pub fn regex_ctrl_unescape(value: &str) -> Result<String> {
|
||||
Ok(r.replace_all(value, REGEX_CONTROL_CHARS))
|
||||
}
|
||||
|
||||
pub fn regex_domain_escape(value: &str) -> Result<String> {
|
||||
regex_ctrl_escape(format!("<[^>]+[@.]{}>$", value).as_str())
|
||||
}
|
||||
|
||||
pub fn regex_domain_unescape(value: &str) -> Result<String> {
|
||||
let value = regex_ctrl_unescape(value)?;
|
||||
|
||||
if let Some(stripped) = value.strip_prefix("<[^>]+[@.]") {
|
||||
if let Some(inner) = stripped.strip_suffix(">$") {
|
||||
Ok(inner.into())
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
} else {
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
pub mod tests {
|
||||
@@ -34,4 +52,17 @@ pub mod tests {
|
||||
assert_eq!(original, unescaped);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_escape_test() -> Result<()> {
|
||||
|
||||
let domain = "example.com";
|
||||
let escaped_domain = regex_domain_escape(domain)?;
|
||||
let unescaped_domain = regex_domain_unescape(&escaped_domain)?;
|
||||
|
||||
assert_eq!("<\\[\\^>\\]\\+\\[@\\.\\]example\\.com>\\$", escaped_domain);
|
||||
assert_eq!(unescaped_domain, domain);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +342,6 @@ pub fn encrypt(context: &HuskContext<'_>, body: &Bytes, recipients: &Vec<Recipie
|
||||
|
||||
let mut keys = Vec::new();
|
||||
|
||||
// XXX: handle the case where a certificate misses an encryption subkey
|
||||
for rcpt in recipients {
|
||||
if rcpt.can_encrypt() {
|
||||
for cert in &rcpt.certs {
|
||||
|
||||
+16
@@ -20,3 +20,19 @@ pub fn to_email(rcpt: &CString) -> Option<String> {
|
||||
else { None }
|
||||
} else { None }
|
||||
}
|
||||
|
||||
/// Checks if the parameter has a valid email format.
|
||||
pub fn is_email(email: &str) -> bool {
|
||||
let uid = UserID::from(email);
|
||||
|
||||
if let Ok(Some(e)) = uid.email() {
|
||||
e == email
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the parameter has a valid domain format.
|
||||
pub fn is_domain(domain: &str) -> bool {
|
||||
is_email(format!("test@{}", domain).as_str())
|
||||
}
|
||||
|
||||
+121
-2
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user