feat: support receiving Autocrypt-Gossip with _verified attribute

This commit is a preparation for
sending Autocrypt-Gossip with `_verified` attribute
instead of `Chat-Verified` header.
This commit is contained in:
link2xt
2025-08-15 22:16:07 +00:00
committed by l
parent 4033566b4a
commit 53a3e51920
7 changed files with 91 additions and 19 deletions

View File

@@ -46,6 +46,13 @@ pub struct Aheader {
pub addr: String, pub addr: String,
pub public_key: SignedPublicKey, pub public_key: SignedPublicKey,
pub prefer_encrypt: EncryptPreference, pub prefer_encrypt: EncryptPreference,
// Whether `_verified` attribute is present.
//
// `_verified` attribute is an extension to `Autocrypt-Gossip`
// header that is used to tell that the sender
// marked this key as verified.
pub verified: bool,
} }
impl fmt::Display for Aheader { impl fmt::Display for Aheader {
@@ -54,6 +61,9 @@ impl fmt::Display for Aheader {
if self.prefer_encrypt == EncryptPreference::Mutual { if self.prefer_encrypt == EncryptPreference::Mutual {
write!(fmt, " prefer-encrypt=mutual;")?; write!(fmt, " prefer-encrypt=mutual;")?;
} }
if self.verified {
write!(fmt, " _verified=1;")?;
}
// adds a whitespace every 78 characters, this allows // adds a whitespace every 78 characters, this allows
// email crate to wrap the lines according to RFC 5322 // email crate to wrap the lines according to RFC 5322
@@ -108,6 +118,8 @@ impl FromStr for Aheader {
.and_then(|raw| raw.parse().ok()) .and_then(|raw| raw.parse().ok())
.unwrap_or_default(); .unwrap_or_default();
let verified = attributes.remove("_verified").is_some();
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored // Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
// Autocrypt-Level0: unknown attribute, treat the header as invalid // Autocrypt-Level0: unknown attribute, treat the header as invalid
if attributes.keys().any(|k| !k.starts_with('_')) { if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -118,6 +130,7 @@ impl FromStr for Aheader {
addr, addr,
public_key, public_key,
prefer_encrypt, prefer_encrypt,
verified,
}) })
} }
} }
@@ -135,6 +148,7 @@ mod tests {
assert_eq!(h.addr, "me@mail.com"); assert_eq!(h.addr, "me@mail.com");
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual); assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
assert_eq!(h.verified, false);
Ok(()) Ok(())
} }
@@ -231,7 +245,8 @@ mod tests {
Aheader { Aheader {
addr: "test@example.com".to_string(), addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual prefer_encrypt: EncryptPreference::Mutual,
verified: false
} }
) )
.contains("prefer-encrypt=mutual;") .contains("prefer-encrypt=mutual;")
@@ -246,7 +261,8 @@ mod tests {
Aheader { Aheader {
addr: "test@example.com".to_string(), addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference prefer_encrypt: EncryptPreference::NoPreference,
verified: false
} }
) )
.contains("prefer-encrypt") .contains("prefer-encrypt")
@@ -259,10 +275,24 @@ mod tests {
Aheader { Aheader {
addr: "TeSt@eXaMpLe.cOm".to_string(), addr: "TeSt@eXaMpLe.cOm".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(), public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::Mutual prefer_encrypt: EncryptPreference::Mutual,
verified: false
} }
) )
.contains("test@example.com") .contains("test@example.com")
); );
assert!(
format!(
"{}",
Aheader {
addr: "test@example.com".to_string(),
public_key: SignedPublicKey::from_base64(RAWKEY).unwrap(),
prefer_encrypt: EncryptPreference::NoPreference,
verified: true
}
)
.contains("_verified")
);
} }
} }

View File

@@ -35,6 +35,7 @@ impl EncryptHelper {
addr: self.addr.clone(), addr: self.addr.clone(),
public_key: self.public_key.clone(), public_key: self.public_key.clone(),
prefer_encrypt: self.prefer_encrypt, prefer_encrypt: self.prefer_encrypt,
verified: false,
} }
} }

View File

@@ -1098,6 +1098,7 @@ impl MimeFactory {
// Autocrypt 1.1.0 specification says that // Autocrypt 1.1.0 specification says that
// `prefer-encrypt` attribute SHOULD NOT be included. // `prefer-encrypt` attribute SHOULD NOT be included.
prefer_encrypt: EncryptPreference::NoPreference, prefer_encrypt: EncryptPreference::NoPreference,
verified: false,
} }
.to_string(); .to_string();

View File

@@ -1,7 +1,7 @@
//! # MIME message parsing module. //! # MIME message parsing module.
use std::cmp::min; use std::cmp::min;
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::Path; use std::path::Path;
use std::str; use std::str;
use std::str::FromStr; use std::str::FromStr;
@@ -36,6 +36,17 @@ use crate::tools::{
}; };
use crate::{chatlist_events, location, stock_str, tools}; use crate::{chatlist_events, location, stock_str, tools};
/// Public key extracted from `Autocrypt-Gossip`
/// header with associated information.
#[derive(Debug)]
pub struct GossipedKey {
/// Public key extracted from `keydata` attribute.
pub public_key: SignedPublicKey,
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
pub verified: bool,
}
/// A parsed MIME message. /// A parsed MIME message.
/// ///
/// This represents the relevant information of a parsed MIME message /// This represents the relevant information of a parsed MIME message
@@ -85,7 +96,7 @@ pub(crate) struct MimeMessage {
/// The addresses for which there was a gossip header /// The addresses for which there was a gossip header
/// and their respective gossiped keys. /// and their respective gossiped keys.
pub gossiped_keys: HashMap<String, SignedPublicKey>, pub gossiped_keys: BTreeMap<String, GossipedKey>,
/// Fingerprint of the key in the Autocrypt header. /// Fingerprint of the key in the Autocrypt header.
/// ///
@@ -1963,9 +1974,9 @@ async fn parse_gossip_headers(
from: &str, from: &str,
recipients: &[SingleInfo], recipients: &[SingleInfo],
gossip_headers: Vec<String>, gossip_headers: Vec<String>,
) -> Result<HashMap<String, SignedPublicKey>> { ) -> Result<BTreeMap<String, GossipedKey>> {
// XXX split the parsing from the modification part // XXX split the parsing from the modification part
let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default(); let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
for value in &gossip_headers { for value in &gossip_headers {
let header = match value.parse::<Aheader>() { let header = match value.parse::<Aheader>() {
@@ -2007,7 +2018,12 @@ async fn parse_gossip_headers(
) )
.await?; .await?;
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key); let gossiped_key = GossipedKey {
public_key: header.public_key,
verified: header.verified,
};
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
} }
Ok(gossiped_keys) Ok(gossiped_keys)

View File

@@ -1,6 +1,6 @@
//! Internet Message Format reception pipeline. //! Internet Message Format reception pipeline.
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::iter; use std::iter;
use std::sync::LazyLock; use std::sync::LazyLock;
@@ -28,14 +28,14 @@ use crate::events::EventType;
use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::headerdef::{HeaderDef, HeaderDefMap};
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table}; use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
use crate::key::self_fingerprint_opt; use crate::key::self_fingerprint_opt;
use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::key::{DcKey, Fingerprint};
use crate::log::LogExt; use crate::log::LogExt;
use crate::log::{info, warn}; use crate::log::{info, warn};
use crate::logged_debug_assert; use crate::logged_debug_assert;
use crate::message::{ use crate::message::{
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists, self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
}; };
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids}; use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
use crate::param::{Param, Params}; use crate::param::{Param, Params};
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub}; use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
use crate::reaction::{Reaction, set_msg_reaction}; use crate::reaction::{Reaction, set_msg_reaction};
@@ -835,7 +835,7 @@ pub(crate) async fn receive_imf_inner(
context context
.sql .sql
.transaction(move |transaction| { .transaction(move |transaction| {
let fingerprint = gossiped_key.dc_fingerprint().hex(); let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
transaction.execute( transaction.execute(
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp) "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
VALUES (?, ?, ?) VALUES (?, ?, ?)
@@ -2917,7 +2917,7 @@ async fn apply_group_changes(
// highest `add_timestamp` to disambiguate. // highest `add_timestamp` to disambiguate.
// The result of the error is that info message // The result of the error is that info message
// may contain display name of the wrong contact. // may contain display name of the wrong contact.
let fingerprint = key.dc_fingerprint().hex(); let fingerprint = key.public_key.dc_fingerprint().hex();
if let Some(contact_id) = if let Some(contact_id) =
lookup_key_contact_by_fingerprint(context, &fingerprint).await? lookup_key_contact_by_fingerprint(context, &fingerprint).await?
{ {
@@ -3659,10 +3659,28 @@ async fn mark_recipients_as_verified(
to_ids: &[Option<ContactId>], to_ids: &[Option<ContactId>],
mimeparser: &MimeMessage, mimeparser: &MimeMessage,
) -> Result<()> { ) -> Result<()> {
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
for gossiped_key in mimeparser
.gossiped_keys
.values()
.filter(|gossiped_key| gossiped_key.verified)
{
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
continue;
};
if to_id == ContactId::SELF || to_id == from_id {
continue;
}
mark_contact_id_as_verified(context, to_id, verifier_id).await?;
ChatId::set_protection_for_contact(context, to_id, mimeparser.timestamp_sent).await?;
}
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
return Ok(()); return Ok(());
} }
let verifier_id = Some(from_id).filter(|&id| id != ContactId::SELF);
for to_id in to_ids.iter().filter_map(|&x| x) { for to_id in to_ids.iter().filter_map(|&x| x) {
if to_id == ContactId::SELF || to_id == from_id { if to_id == ContactId::SELF || to_id == from_id {
continue; continue;
@@ -3755,7 +3773,7 @@ async fn add_or_lookup_contacts_by_address_list(
async fn add_or_lookup_key_contacts( async fn add_or_lookup_key_contacts(
context: &Context, context: &Context,
address_list: &[SingleInfo], address_list: &[SingleInfo],
gossiped_keys: &HashMap<String, SignedPublicKey>, gossiped_keys: &BTreeMap<String, GossipedKey>,
fingerprints: &[Fingerprint], fingerprints: &[Fingerprint],
origin: Origin, origin: Origin,
) -> Result<Vec<Option<ContactId>>> { ) -> Result<Vec<Option<ContactId>>> {
@@ -3771,7 +3789,7 @@ async fn add_or_lookup_key_contacts(
// Iterator has not ran out of fingerprints yet. // Iterator has not ran out of fingerprints yet.
fp.hex() fp.hex()
} else if let Some(key) = gossiped_keys.get(addr) { } else if let Some(key) = gossiped_keys.get(addr) {
key.dc_fingerprint().hex() key.public_key.dc_fingerprint().hex()
} else if context.is_self_addr(addr).await? { } else if context.is_self_addr(addr).await? {
contact_ids.push(Some(ContactId::SELF)); contact_ids.push(Some(ContactId::SELF));
continue; continue;

View File

@@ -272,7 +272,9 @@ pub(crate) async fn handle_securejoin_handshake(
let mut self_found = false; let mut self_found = false;
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint(); let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
for (addr, key) in &mime_message.gossiped_keys { for (addr, key) in &mime_message.gossiped_keys {
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? { if key.public_key.dc_fingerprint() == self_fingerprint
&& context.is_self_addr(addr).await?
{
self_found = true; self_found = true;
break; break;
} }
@@ -542,7 +544,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);
}; };
if key.dc_fingerprint() != contact_fingerprint { if key.public_key.dc_fingerprint() != contact_fingerprint {
// Fingerprint does not match, ignore. // Fingerprint does not match, ignore.
warn!(context, "Fingerprint does not match."); warn!(context, "Fingerprint does not match.");
return Ok(HandshakeMessage::Ignore); return Ok(HandshakeMessage::Ignore);

View File

@@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat};
use crate::chatlist::Chatlist; use crate::chatlist::Chatlist;
use crate::constants::Chattype; use crate::constants::Chattype;
use crate::key::self_fingerprint; use crate::key::self_fingerprint;
use crate::mimeparser::GossipedKey;
use crate::receive_imf::receive_imf; use crate::receive_imf::receive_imf;
use crate::stock_str::{self, messages_e2e_encrypted}; use crate::stock_str::{self, messages_e2e_encrypted};
use crate::test_utils::{ use crate::test_utils::{
@@ -185,7 +186,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
); );
if case == SetupContactCase::WrongAliceGossip { if case == SetupContactCase::WrongAliceGossip {
let wrong_pubkey = load_self_public_key(&bob).await.unwrap(); let wrong_pubkey = GossipedKey {
public_key: load_self_public_key(&bob).await.unwrap(),
verified: false,
};
let alice_pubkey = msg let alice_pubkey = msg
.gossiped_keys .gossiped_keys
.insert(alice_addr.to_string(), wrong_pubkey) .insert(alice_addr.to_string(), wrong_pubkey)