Start with signing outgoing mails

- Add a scheme on how to discover Husk signing keys. Siging keys mut be
  certified by the `local trust root` with a signature having a notation
  with a name equal to `signing_keys@husk-projet.org`.
- Fetch the key from the keystore.
- Expand `authenticate()` so that notation can be added to the
  signature.
- Add a test to create a signing key and fetch it from the keystore.
This commit is contained in:
Malte Meiboom
2026-06-10 15:00:33 +02:00
parent 44d00b262e
commit 1f89d40cd3
7 changed files with 621 additions and 10 deletions
+1
View File
@@ -37,6 +37,7 @@ pub async fn dispatch(cmd: LocalsAddCommand, config: HuskConfigContainer) -> Res
&vc,
crypto::Role::Local,
true,
None,
None
)
}
+1
View File
@@ -41,6 +41,7 @@ pub async fn dispatch(cmd: LocalsRemoveCommand, config: HuskConfigContainer) ->
cert,
crypto::Role::Local,
false,
None,
None)
} else {
Err(CommandError::CertNotUsable(fpr).into())
+193 -1
View File
@@ -3,15 +3,20 @@
use std::io::Write;
use std::sync::Arc;
use std::time::Duration;
use std::time::SystemTime;
use std::collections::HashMap;
use std::collections::HashSet;
use std::collections::hash_map::Entry::{Occupied, Vacant};
use anyhow;
use sequoia_openpgp::cert::ValidCert;
use sequoia_openpgp::packet::signature::subpacket::NotationDataFlags;
use tokio::task::JoinSet;
use bytes::Bytes;
use sequoia_net::{KeyServer, wkd, dane};
use sequoia_directories::Home;
use sequoia_keystore::Keystore;
use sequoia_keystore::Context;
use sequoia_openpgp::packet::prelude::SignatureBuilder;
use sequoia_openpgp::parse::{PacketParser, PacketParserResult, Parse};
use sequoia_openpgp::policy::StandardPolicy;
@@ -437,7 +442,7 @@ pub enum Role {
/// `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, role: Role, activate: bool, domains: Option<Vec<String>>)
pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activate: bool, domains: Option<Vec<String>>, notation: Option<(String, String)>)
-> anyhow::Result<()> {
let policy = &context.policy;
@@ -502,6 +507,14 @@ pub fn authenticate(context: &HuskContext, cert: &ValidCert, role: Role, activat
builder = builder.set_trust_signature(0, 1)?;
}
if let Some((name, value)) = notation {
builder = builder.set_notation(
name,
value,
NotationDataFlags::empty().set_human_readable(),
false)?;
}
// For a certification, user ids are needed. Build a list of
// valid (non self revoked) user ids.
let user_ids: Vec<_> = cert.userids()
@@ -559,9 +572,107 @@ pub fn can_encrypt(cert: &ValidCert) -> bool {
.count() > 0
}
/// Signing keys used by Husk must have a certification with a
/// notation of the name `HUSK_SIG_NOTATION`.
pub const HUSK_SIG_NOTATION: &str = "signing_key@husk-project.org";
/// Get Husks signing key
pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result<Option<Cert>> {
let local_trust_root = local_trust_root.with_policy(policy, None)?;
let local_trust_fpr = local_trust_root.fingerprint();
let mut newest = None;
let mut result = None;
if let Some(keys) = get_all_keys(sequoia_home) {
for key in keys {
if let Ok(cert) = cert_store.lookup_by_cert_fpr(&key) {
if let Ok(vc) = cert.with_policy(policy, None) {
if let Some(time) = contains_signing_key(&vc, &local_trust_fpr) {
if let Some(othertime) = newest {
if time > othertime {
newest = Some(time);
result = Some(vc.cert().clone());
}
} else { // first detected key
newest = Some(time);
result = Some(vc.cert().clone());
}
}
}
}
}
Ok(result)
} else {
Ok(None)
}
}
/// If `vc` is a certificate marked as a Husk signing key
/// key, return the creation time of the signature - else return `None`.
pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option<SystemTime> {
for uid in vc.userids().into_iter() {
for sig in uid.certifications() {
if sig.issuer_fingerprints().any(|i| i == local_root_fpr)
&& sig.notation(HUSK_SIG_NOTATION).count() > 0 {
// ignore signatures without creation time
if let Some(time) = sig.signature_creation_time() {
return Some(time);
}
}
}
}
None
}
/// Get all keys from all backends
pub fn get_all_keys(sequoia_home: Home) -> Option<Vec<Fingerprint>> {
let mut result = Vec::new();
std::thread::scope(|s| {
s.spawn(|| {
if let Ok(keystore_base) = Context::configure()
.home(sequoia_home.data_dir(sequoia_directories::Component::Keystore))
.build() {
if let Ok(mut keystore) = Keystore::connect(&keystore_base) {
if let Ok(mut backends) = keystore.backends() {
for backend in &mut backends {
if let Ok(devices) = backend.devices() {
for mut device in devices {
if let Ok(keys) = device.keys() {
keys.iter().for_each(|k| {
result.push(k.fingerprint());
});
}
}
}
}
}
}
}
});
});
Some(result)
}
#[cfg(test)]
pub mod tests {
use sequoia_cert_store::store::CertD;
use sequoia_cert_store::{CertStore, LazyCert, StoreUpdate};
use sequoia_keystore::Context;
use sequoia_keystore::Keystore;
use sequoia_directories::Home;
use sequoia_directories::Component;
use sequoia_openpgp::{cert::CertBuilder, policy::StandardPolicy};
use std::sync::Arc;
use crate::common::crypto::{HUSK_SIG_NOTATION, get_all_keys};
use crate::types::{husk_context::HuskContext, mail_context::MailContext};
use super::{authenticate, get_signing_key};
#[test]
pub fn test_is_encrypted() {
@@ -644,4 +755,85 @@ jk6lh01Ej9QTGQ==
}
#[test]
fn test_signing_key() -> anyhow::Result<()> {
let temp_dir = tempfile::tempdir()?;
let temp_path = temp_dir.path().to_path_buf();
let home = Home::new(temp_path)?;
// build the keystore
let keystore_home = home.data_dir(Component::Keystore);
let c = Context::configure()
.home(keystore_home)
.build()?;
let mut ks = Keystore::connect(&c)?;
// get the softkeys backend
let mut softkeys = None;
for mut backend in ks.backends()?.into_iter() {
if backend.id().expect("backend id") == "softkeys" {
softkeys = Some(backend);
}
}
assert!(softkeys.is_some());
let mut softkeys = softkeys.unwrap();
// build the certstore
let cert_store_base = home.data_dir(sequoia_directories::Component::CertD);
std::fs::create_dir_all(&cert_store_base)?;
let _ = CertD::open(&cert_store_base)?;
let store = CertStore::open(cert_store_base)?;
// build the trust root
let (root, _) = CertBuilder::general_purpose(Some("local root"))
.add_signing_subkey()
.generate()?;
// create a skeleton HuskContext for later use.
let context = HuskContext {
cert_store: store,
policy: StandardPolicy::new(),
local_trust_root: root,
signing_key: None,
introducers: Vec::new(),
keyservers: Vec::new(),
mail: MailContext::new()
};
// store the local trust root
context.cert_store.update(Arc::new(LazyCert::from(context.local_trust_root.clone())))?;
// the siging key
let (signing_key, _) = CertBuilder::general_purpose(Some("signing key"))
.add_signing_subkey()
.generate()?;
let valid_signing_cert = signing_key.with_policy(&context.policy, None)?;
let signing_fpr = signing_key.fingerprint();
softkeys.import(&signing_key)?;
let keys= get_all_keys(home.clone());
eprintln!("{:?}", keys);
authenticate(&context,
&valid_signing_cert,
super::Role::Local,
true,
None,
Some((HUSK_SIG_NOTATION.to_string(), "dummy".to_string())))?;
// get the Husk signing key
let key = get_signing_key(home,
&context.cert_store,
&context.policy,
&context.local_trust_root)?;
assert!(key.is_some());
let fpr = key.unwrap().fingerprint();
assert_eq!(signing_fpr, fpr);
Ok(())
}
}
+10
View File
@@ -26,6 +26,7 @@ pub struct HuskContext<'hc> {
pub policy: StandardPolicy<'hc>,
pub cert_store: CertStore<'hc>,
pub local_trust_root: Cert,
pub signing_key: Option<Cert>,
pub introducers: Vec<Introducer>,
pub keyservers: Vec<String>,
pub mail: MailContext<'hc>,
@@ -45,10 +46,19 @@ impl<'hc> HuskContext<'hc> {
let local_trust_root = crypto::get_local_trust_root(&cert_store)?;
let introducers = crypto::get_introducers(&cert_store, &policy, local_trust_root.fingerprint());
let signing_key = match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) {
Ok(Some(cert)) => Some(cert),
_ => {
log::warn!("No signing key found");
None
}
};
Ok(HuskContext {
policy,
cert_store,
local_trust_root,
signing_key,
introducers,
keyservers: config.get_keyservers(),
mail: MailContext::new(),
+3 -1
View File
@@ -83,7 +83,8 @@ impl Introducer {
cert,
crypto::Role::Introducer,
true,
Some(domains))
Some(domains),
None)
}
/// Remove an introducer.
@@ -101,6 +102,7 @@ impl Introducer {
cert,
crypto::Role::Introducer,
false,
None,
None)
}
}