diff --git a/Cargo.lock b/Cargo.lock index 92e5967..a3c5442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2586,8 +2586,7 @@ dependencies = [ [[package]] name = "sequoia-keystore" version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e5887d0c3c5f0df1d1cab937ef7a5c398111ddfd911c714f28c904438f229c" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" dependencies = [ "anyhow", "async-generic", @@ -2603,7 +2602,7 @@ dependencies = [ "sequoia-keystore-gpg-agent", "sequoia-keystore-softkeys", "sequoia-openpgp", - "thiserror 2.0.18", + "thiserror 1.0.69", "tokio", "tokio-util", ] @@ -2611,8 +2610,7 @@ dependencies = [ [[package]] name = "sequoia-keystore-backend" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55b047f9b6412c34dc7a26a42f278205cbf1e29516feab1228edccca215f500" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" dependencies = [ "anyhow", "async-trait", @@ -2621,15 +2619,14 @@ dependencies = [ "log", "sequoia-openpgp", "tempfile", - "thiserror 2.0.18", + "thiserror 1.0.69", "tokio", ] [[package]] name = "sequoia-keystore-gpg-agent" version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ad901f1f1d88b38f5f5738d5bf2b80136ec44e94a40716193be0bbf3314df2b" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" dependencies = [ "anyhow", "async-trait", @@ -2646,8 +2643,7 @@ dependencies = [ [[package]] name = "sequoia-keystore-softkeys" version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c4944848f748ce22875020e9cff9e604d52c563aef8e19053f5abe037139e" +source = "git+https://gitlab.com/sequoia-pgp/sequoia-keystore?branch=malte%2Fsoftkey-refresh#cc77f2ea46ab1c3ca1ace0c16848a4137e67bdf7" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b3cfb6b..64465d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ log = "0.4.30" log4rs = "1.3.0" sequoia-cert-store = "0.7.3" sequoia-directories = "0.1.0" -sequoia-keystore = "0.7.2" +#sequoia-keystore = "0.7.2" +sequoia-keystore = { git = "https://gitlab.com/sequoia-pgp/sequoia-keystore", branch = "malte/softkey-refresh" } sequoia-net = "0.30.1" sequoia-openpgp = "2.3.0" sequoia-wot = "0.15.0" diff --git a/e2e-tests/common.sh b/e2e-tests/common.sh index 113b0aa..e97d84c 100644 --- a/e2e-tests/common.sh +++ b/e2e-tests/common.sh @@ -18,7 +18,7 @@ send_test_mail() { # email_address chown $USER:mail /var/mail/$USER chmod 600 /var/mail/$USER - echo "test mail" | sendmail $1 + echo -e "Subject: testmail\n\ntest mail" | sendmail $1 inotifywait -qq -t 3 -e close_write /var/mail/$USER } diff --git a/e2e-tests/tests/60_signing.sh b/e2e-tests/tests/60_signing.sh new file mode 100644 index 0000000..629369d --- /dev/null +++ b/e2e-tests/tests/60_signing.sh @@ -0,0 +1,45 @@ +#!/usr/bin/bash + +# source some functions +. common.sh + +test_header "Signing" + +# sq key generate --shared-key --without-password --email 'signing@example.com' --output /tmp/signing.pgp --rev-cert /tmp/signing.rev + +SIGN_FPR=$(sq key generate --own-key --without-password --name "Signing Key" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2) + +cd $HUSK_DIR + +$HUSK_BIN --config config/config.toml signkey set --cert $SIGN_FPR +SIGN_FPR=$($HUSK_BIN --config config/config.toml signkey show | cut -d ':' -f 2 | tr -d ' ') +echo "found signing key $SIGN_FPR" + +create_unix_user juliette +JULIETTE_FPR=$(sq key generate --own-key --without-password --name "Juliette" --email "juliette@example.com" 2>&1 | grep "Fingerprint:" | cut -d ':' -f 2) +sq pki vouch add --certifier-userid "CA" --cert $JULIETTE_FPR --all > /dev/null 2>&1 + +# send a mail, husk should encrypt it +echo "send mail to juliette@example.com" +send_test_mail juliette@example.com + +if [ -f /var/mail/juliette ] ; then + if grep -q "BEGIN PGP MESSAGE" /var/mail/juliette ; then + + FOUND_FPR=$(sq packet dump /var/mail/juliette | grep "Issuer Fingerprint:" | cut -d ':' -f 2 | tr -d ' ') + + if [ "$SIGN_FPR" != "$FOUND_FPR" ] ; then + echo "Found wrong fingerprint '$FOUND_FPR' - expected '$SIGN_FPR'" + RESULT_CODE=1 + else + RESULT_CODE=0 + fi + else + echo "no encryption detected" + exit 1 + fi +else + echo "no mail detected" + exit 1 +fi + diff --git a/src/commands/signkey.rs b/src/commands/signkey.rs index c9f971f..49a8421 100644 --- a/src/commands/signkey.rs +++ b/src/commands/signkey.rs @@ -1,6 +1,7 @@ //! signkey subcommand use anyhow::Result; +use sequoia_cert_store::Store; use sequoia_openpgp::serialize::SerializeInto; use crate::cli::cli_args::{SignkeyCommand, SignkeySubcommand}; @@ -19,16 +20,25 @@ pub async fn dispatch(cmd: SignkeyCommand, config: HuskConfigContainer) -> Resul if let Some(key) = context.signing_key { println!("Fingerprint: {}", key.fingerprint()); } else { - println!("No active signing key available."); + eprintln!("No active signing key available."); } }, SignkeySubcommand::Dump => { let context = HuskContext::new(&config.into())?; if let Some(key) = context.signing_key { - let armored = key.armored().to_vec()?; - println!("{}", String::from_utf8(armored)?); + + if let Ok(cert) = &context.cert_store.lookup_by_cert_fpr(&key.fingerprint()) { + if let Ok(cert) = cert.to_cert() { + let armored = cert.armored().to_vec()?; + println!("{}", String::from_utf8_lossy(&armored[..])); + } else { + eprintln!("Certificate {} is damaged", cert.fingerprint()); + } + } else { + eprintln!("Cannot find certificate for key {}", key.fingerprint()); + } } else { - println!("No active signing key available."); + eprintln!("No active signing key available."); } } } diff --git a/src/common/crypto.rs b/src/common/crypto.rs index c5b8dad..9610e9e 100644 --- a/src/common/crypto.rs +++ b/src/common/crypto.rs @@ -17,10 +17,11 @@ use sequoia_net::{KeyServer, wkd, dane}; use sequoia_directories::Home; use sequoia_keystore::Keystore; use sequoia_keystore::Context; +use sequoia_keystore::Key; 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::serialize::stream::{Message, Armorer, Encryptor, Signer, LiteralWriter}; use sequoia_openpgp::{Fingerprint, Cert, Packet}; use sequoia_openpgp::cert::raw::RawCertParser; use sequoia_openpgp::types::SignatureType; @@ -422,10 +423,21 @@ pub fn encrypt(context: &HuskContext<'_>, body: &[u8], recipients: &Vec anyhow::Result<()> { + message.write_all(body)?; + message.finalize()?; + Ok(()) + }); + }); Ok(Bytes::from(sink)) } else { @@ -599,7 +611,7 @@ pub fn can_sign(cert: &ValidCert) -> bool { /// Get Husks signing key -pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result> { +pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: &StandardPolicy<'_>, local_trust_root: &Cert) -> anyhow::Result> { let local_trust_root = local_trust_root.with_policy(policy, None)?; let local_trust_fpr = local_trust_root.fingerprint(); @@ -607,33 +619,31 @@ pub fn get_signing_key(sequoia_home: Home, cert_store: &CertStore<'_>, policy: & 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 + let keys = get_all_keys(sequoia_home); + for key in keys { + if let Ok(cert) = cert_store.lookup_by_cert_fpr(&key.fingerprint()) { + 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()); + result = Some(key.clone()); } + } else { // first detected key + newest = Some(time); + result = Some(key.clone()); } } } } - Ok(result) - } else { - Ok(None) } + Ok(result) } /// If `vc` is a certificate marked as a Husk signing key, return the /// creation time of the signature - else return `None`. pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Option { + log::debug!("check {} for signing key", vc.fingerprint()); for uid in vc.userids() { for sig in uid.certifications() { if sig.issuer_fingerprints().any(|i| i == local_root_fpr) @@ -641,6 +651,7 @@ pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Opt // ignore signatures without creation time if let Some(time) = sig.signature_creation_time() { + log::debug!("found"); return Some(time); } } @@ -651,7 +662,7 @@ pub fn contains_signing_key(vc: &ValidCert, local_root_fpr: &Fingerprint) -> Opt /// Get all keys from all backends // XXX: Maybe the softkeys backend is sufficient -pub fn get_all_keys(sequoia_home: Home) -> Option> { +pub fn get_all_keys(sequoia_home: Home) -> Vec { let mut result = Vec::new(); std::thread::scope(|s| { @@ -660,14 +671,14 @@ pub fn get_all_keys(sequoia_home: Home) -> Option> { .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 { + log::debug!("scanning backend {:?}", backend.id()); 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()); + result.push(k.clone()); }); } } @@ -678,7 +689,7 @@ pub fn get_all_keys(sequoia_home: Home) -> Option> { } }); }); - Some(result) + result } #[cfg(test)] @@ -821,6 +832,7 @@ jk6lh01Ej9QTGQ== signing_key: None, introducers: Vec::new(), keyservers: Vec::new(), + subject_replacement: "...".into(), mail: MailContext::new() }; @@ -836,7 +848,7 @@ jk6lh01Ej9QTGQ== softkeys.import(&signing_key)?; let keys= get_all_keys(home.clone()); - eprintln!("{:?}", keys); + for key in keys { eprintln!("{}", key.fingerprint()); } super::set_signing_key(&context, &valid_signing_cert)?; diff --git a/src/config.rs b/src/config.rs index 75c7947..1c9afac 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,13 +9,14 @@ use toml; use log4rs; use crate::types::errors::HuskError; -use crate::types::defaults::CONSOLE_LOGGING; +use crate::types::defaults::{CONSOLE_LOGGING, SUBJECT_REPLACEMENT}; #[derive(Deserialize, Debug, Clone)] pub struct HuskConfig { pub connection: String, pub sequoia_home: String, pub keyservers: Option>, + pub subject_replacement: Option, logging: Option, } @@ -68,6 +69,13 @@ impl HuskConfig { } } + pub fn get_subject_replacement(&self) -> String { + if let Some(replacement) = &self.subject_replacement { + replacement.clone() + } else { + SUBJECT_REPLACEMENT.to_string() + } + } } pub type HuskConfigContainer = Arc>; @@ -79,3 +87,23 @@ impl From for HuskConfig { } } +#[cfg(test)] +mod tests { + use crate::types::defaults; + + use super::HuskConfig; + + + #[test] + fn subject_line() { + let config = HuskConfig { + connection: "".into(), + sequoia_home: "".into(), + keyservers: None, + subject_replacement: None, + logging: None + }; + + assert_eq!(config.get_subject_replacement(), defaults::SUBJECT_REPLACEMENT); + } +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs index e071ff3..9cab8db 100644 --- a/src/daemon/mod.rs +++ b/src/daemon/mod.rs @@ -241,6 +241,11 @@ impl Daemon { } } } + // Change the subject + let _ = cx.actions.change_header( + "Subject", + 1, + Some(context.subject_replacement.clone())).await; }, Err(e) => { log::error!("{}", e); diff --git a/src/types/defaults.rs b/src/types/defaults.rs index ab955ea..e9b4d8f 100644 --- a/src/types/defaults.rs +++ b/src/types/defaults.rs @@ -1,9 +1,13 @@ //! Defaults -pub const CONFIG_FILE_LOCATION: &'static str = "/etc/husk/config.toml"; +pub const CONFIG_FILE_LOCATION: &str = "/etc/husk/config.toml"; + +/// If encrypting a mail, the subject is moved into the encrypted body +/// and replaced by `SUBJECT_REPLACEMENT` (unless configured otherwise). +pub const SUBJECT_REPLACEMENT: &str = "..."; /// Default logging configuration. -pub const CONSOLE_LOGGING: &'static str = " +pub const CONSOLE_LOGGING: &str = " [appenders.console] kind = \"console\" diff --git a/src/types/husk_context.rs b/src/types/husk_context.rs index 8e839bd..9efac1d 100644 --- a/src/types/husk_context.rs +++ b/src/types/husk_context.rs @@ -15,6 +15,7 @@ use sequoia_openpgp::policy::StandardPolicy; use sequoia_openpgp::Cert; use sequoia_cert_store::CertStore; use sequoia_directories::Home; +use sequoia_keystore::Key; use crate::config::HuskConfig; use crate::common::crypto; @@ -26,9 +27,10 @@ pub struct HuskContext<'hc> { pub policy: StandardPolicy<'hc>, pub cert_store: CertStore<'hc>, pub local_trust_root: Cert, - pub signing_key: Option, + pub signing_key: Option, pub introducers: Vec, pub keyservers: Vec, + pub subject_replacement: String, pub mail: MailContext<'hc>, } @@ -48,9 +50,13 @@ impl<'hc> HuskContext<'hc> { let signing_key = match crypto::get_signing_key(sequoia_home, &cert_store, &policy, &local_trust_root) { Ok(Some(cert)) => Some(cert), - _ => { + Ok(None) => { log::warn!("No signing key found"); None + }, + Err(e) => { + log::error!("Error while fetching signing key: {}", e); + None } }; @@ -61,6 +67,7 @@ impl<'hc> HuskContext<'hc> { signing_key, introducers, keyservers: config.get_keyservers(), + subject_replacement: config.get_subject_replacement(), mail: MailContext::new(), }) }