diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 8a6279623..33a02830c 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -89,11 +89,12 @@ pub(crate) struct MimeMessage { pub decrypting_failed: bool, /// Valid signature fingerprint if a message is an - /// Autocrypt encrypted and signed message. + /// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints + /// () if any. /// /// If a message is not encrypted or the signature is not valid, /// this is `None`. - pub signature: Option, + pub signature: Option<(Fingerprint, HashSet)>, /// The addresses for which there was a gossip header /// and their respective gossiped keys. @@ -529,12 +530,16 @@ impl MimeMessage { let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg { crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring) } else { - HashSet::new() + HashMap::new() }; let mail = mail.as_ref().map(|mail| { let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring) .unwrap_or((mail, Default::default())); + let signatures_detached = signatures_detached + .into_iter() + .map(|fp| (fp, Vec::new())) + .collect::>(); signatures.extend(signatures_detached); content }); @@ -640,6 +645,10 @@ impl MimeMessage { }; } + let signature = signatures + .into_iter() + .last() + .map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::>())); let mut parser = MimeMessage { parts: Vec::new(), headers, @@ -655,7 +664,7 @@ impl MimeMessage { decrypting_failed: mail.is_err(), // only non-empty if it was a valid autocrypt message - signature: signatures.into_iter().last(), + signature, autocrypt_fingerprint, gossiped_keys, is_forwarded: false, diff --git a/src/pgp.rs b/src/pgp.rs index 9d8c9586b..e2760d205 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -1,6 +1,6 @@ //! OpenPGP helper module using [rPGP facilities](https://github.com/rpgp/rpgp). -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::io::{BufRead, Cursor}; use anyhow::{Context as _, Result, bail}; @@ -370,19 +370,28 @@ fn check_symmetric_encryption(msg: &Message<'_>) -> std::result::Result<(), &'st /// Returns fingerprints /// of all keys from the `public_keys_for_validation` keyring that -/// have valid signatures there. +/// have valid signatures in `msg` and corresponding intended recipient fingerprints +/// () if any. /// -/// If the message is wrongly signed, HashSet will be empty. +/// If the message is wrongly signed, returns an empty map. pub fn valid_signature_fingerprints( msg: &pgp::composed::Message, public_keys_for_validation: &[SignedPublicKey], -) -> HashSet { - let mut ret_signature_fingerprints: HashSet = Default::default(); +) -> HashMap> { + let mut ret_signature_fingerprints = HashMap::new(); if msg.is_signed() { for pkey in public_keys_for_validation { - if msg.verify(&pkey.primary_key).is_ok() { + if let Ok(signature) = msg.verify(&pkey.primary_key) { let fp = pkey.dc_fingerprint(); - ret_signature_fingerprints.insert(fp); + let mut recipient_fps = Vec::new(); + if let Some(cfg) = signature.config() { + for subpkt in &cfg.hashed_subpackets { + if let SubpacketData::IntendedRecipientFingerprint(fp) = &subpkt.data { + recipient_fps.push(fp.clone().into()); + } + } + } + ret_signature_fingerprints.insert(fp, recipient_fps); } } } @@ -497,13 +506,14 @@ mod tests { use pgp::composed::Esk; use pgp::packet::PublicKeyEncryptedSessionKey; + #[expect(clippy::type_complexity)] fn pk_decrypt_and_validate<'a>( ctext: &'a [u8], private_keys_for_decryption: &'a [SignedSecretKey], public_keys_for_validation: &[SignedPublicKey], ) -> Result<( pgp::composed::Message<'static>, - HashSet, + HashMap>, Vec, )> { let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?; @@ -611,7 +621,7 @@ mod tests { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_decrypt_singed() { + async fn test_decrypt_signed() { // Check decrypting as Alice let decrypt_keyring = vec![KEYS.alice_secret.clone()]; let sig_check_keyring = vec![KEYS.alice_public.clone()]; @@ -623,6 +633,10 @@ mod tests { .unwrap(); assert_eq!(content, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); + for recipient_fps in valid_signatures.values() { + // Intended Recipient Fingerprint subpackets aren't added currently. + assert_eq!(recipient_fps.len(), 0); + } // Check decrypting as Bob let decrypt_keyring = vec![KEYS.bob_secret.clone()]; @@ -635,6 +649,9 @@ mod tests { .unwrap(); assert_eq!(content, CLEARTEXT); assert_eq!(valid_signatures.len(), 1); + for recipient_fps in valid_signatures.values() { + assert_eq!(recipient_fps.len(), 0); + } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 9ab7ae802..8a58fd6de 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -577,7 +577,7 @@ pub(crate) async fn receive_imf_inner( // For example, GitHub sends messages from `notifications@github.com`, // but uses display name of the user whose action generated the notification // as the display name. - let fingerprint = mime_parser.signature.as_ref(); + let fingerprint = mime_parser.signature.as_ref().map(|(fp, _)| fp); let (from_id, _from_id_blocked, incoming_origin) = match from_field_to_contact_id( context, &mime_parser.from, @@ -3901,7 +3901,7 @@ async fn has_verified_encryption( let signed_with_verified_key = mimeparser .signature .as_ref() - .is_some_and(|signature| *signature == fingerprint); + .is_some_and(|(signature, _)| *signature == fingerprint); if signed_with_verified_key { Ok(Verified) } else { diff --git a/src/securejoin.rs b/src/securejoin.rs index 55052791f..8bf532d69 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -784,7 +784,7 @@ fn encrypted_and_signed( mimeparser: &MimeMessage, expected_fingerprint: &Fingerprint, ) -> bool { - if let Some(signature) = mimeparser.signature.as_ref() { + if let Some((signature, _)) = mimeparser.signature.as_ref() { if signature == expected_fingerprint { true } else {