Skip to content

Commit e180d7f

Browse files
committed
feat: verify contacts via Autocrypt-Gossip
This mechanism replaces `Chat-Verified` header. New parameter `_verified=1` in `Autocrypt-Gossip` header marks that the sender has the gossiped key verified. Using `_verified=1` instead of `_verified` because it is less likely to cause troubles with existing Autocrypt header parsers. This is also how https://www.rfc-editor.org/rfc/rfc2045 defines parameter syntax.
1 parent a955cb5 commit e180d7f

File tree

9 files changed

+205
-107
lines changed

9 files changed

+205
-107
lines changed

src/aheader.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ pub struct Aheader {
4848
pub addr: String,
4949
pub public_key: SignedPublicKey,
5050
pub prefer_encrypt: EncryptPreference,
51+
52+
// Whether `_verified` attribute is present.
53+
//
54+
// `_verified` attribute is an extension to `Autocrypt-Gossip`
55+
// header that is used to tell that the sender
56+
// marked this key as verified.
57+
pub is_verified: bool,
5158
}
5259

5360
impl Aheader {
@@ -56,11 +63,13 @@ impl Aheader {
5663
addr: String,
5764
public_key: SignedPublicKey,
5865
prefer_encrypt: EncryptPreference,
66+
is_verified: bool,
5967
) -> Self {
6068
Aheader {
6169
addr,
6270
public_key,
6371
prefer_encrypt,
72+
is_verified,
6473
}
6574
}
6675
}
@@ -71,6 +80,9 @@ impl fmt::Display for Aheader {
7180
if self.prefer_encrypt == EncryptPreference::Mutual {
7281
write!(fmt, " prefer-encrypt=mutual;")?;
7382
}
83+
if self.is_verified {
84+
write!(fmt, " _verified=1;")?;
85+
}
7486

7587
// adds a whitespace every 78 characters, this allows
7688
// email crate to wrap the lines according to RFC 5322
@@ -125,6 +137,8 @@ impl FromStr for Aheader {
125137
.and_then(|raw| raw.parse().ok())
126138
.unwrap_or_default();
127139

140+
let is_verified = attributes.remove("_verified").is_some();
141+
128142
// Autocrypt-Level0: unknown attributes starting with an underscore can be safely ignored
129143
// Autocrypt-Level0: unknown attribute, treat the header as invalid
130144
if attributes.keys().any(|k| !k.starts_with('_')) {
@@ -135,6 +149,7 @@ impl FromStr for Aheader {
135149
addr,
136150
public_key,
137151
prefer_encrypt,
152+
is_verified,
138153
})
139154
}
140155
}
@@ -152,6 +167,7 @@ mod tests {
152167

153168
assert_eq!(h.addr, "[email protected]");
154169
assert_eq!(h.prefer_encrypt, EncryptPreference::Mutual);
170+
assert_eq!(h.is_verified, false);
155171
Ok(())
156172
}
157173

@@ -248,7 +264,8 @@ mod tests {
248264
Aheader::new(
249265
"[email protected]".to_string(),
250266
SignedPublicKey::from_base64(RAWKEY).unwrap(),
251-
EncryptPreference::Mutual
267+
EncryptPreference::Mutual,
268+
false
252269
)
253270
)
254271
.contains("prefer-encrypt=mutual;")
@@ -263,7 +280,8 @@ mod tests {
263280
Aheader::new(
264281
"[email protected]".to_string(),
265282
SignedPublicKey::from_base64(RAWKEY).unwrap(),
266-
EncryptPreference::NoPreference
283+
EncryptPreference::NoPreference,
284+
false
267285
)
268286
)
269287
.contains("prefer-encrypt")
@@ -276,10 +294,24 @@ mod tests {
276294
Aheader::new(
277295
"[email protected]".to_string(),
278296
SignedPublicKey::from_base64(RAWKEY).unwrap(),
279-
EncryptPreference::Mutual
297+
EncryptPreference::Mutual,
298+
false
280299
)
281300
)
282301
.contains("[email protected]")
283302
);
303+
304+
assert!(
305+
format!(
306+
"{}",
307+
Aheader::new(
308+
"[email protected]".to_string(),
309+
SignedPublicKey::from_base64(RAWKEY).unwrap(),
310+
EncryptPreference::NoPreference,
311+
true
312+
)
313+
)
314+
.contains("_verified")
315+
);
284316
}
285317
}

src/e2ee.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ impl EncryptHelper {
3737
pub fn get_aheader(&self) -> Aheader {
3838
let pk = self.public_key.clone();
3939
let addr = self.addr.to_string();
40-
Aheader::new(addr, pk, self.prefer_encrypt)
40+
let verified = false;
41+
Aheader::new(addr, pk, self.prefer_encrypt, verified)
4142
}
4243

4344
/// Tries to encrypt the passed in `mail`.

src/mimefactory.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,17 @@ impl MimeFactory {
10881088
.is_none_or(|ts| now >= ts + gossip_period || now < ts)
10891089
};
10901090

1091+
let verifier_id: Option<u32> = context
1092+
.sql
1093+
.query_get_value(
1094+
"SELECT verifier FROM contacts WHERE fingerprint=?",
1095+
(&fingerprint,),
1096+
)
1097+
.await?;
1098+
1099+
let is_verified =
1100+
verifier_id.is_some_and(|verifier_id| verifier_id != 0);
1101+
10911102
if !should_do_gossip {
10921103
continue;
10931104
}
@@ -1098,6 +1109,7 @@ impl MimeFactory {
10981109
// Autocrypt 1.1.0 specification says that
10991110
// `prefer-encrypt` attribute SHOULD NOT be included.
11001111
EncryptPreference::NoPreference,
1112+
is_verified,
11011113
)
11021114
.to_string();
11031115

src/mimeparser.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! # MIME message parsing module.
22
33
use std::cmp::min;
4-
use std::collections::{HashMap, HashSet};
4+
use std::collections::{BTreeMap, HashMap, HashSet};
55
use std::path::Path;
66
use std::str;
77
use std::str::FromStr;
@@ -36,6 +36,17 @@ use crate::tools::{
3636
};
3737
use crate::{chatlist_events, location, stock_str, tools};
3838

39+
/// Public key extracted from `Autocrypt-Gossip`
40+
/// header with associated information.
41+
#[derive(Debug)]
42+
pub struct GossipedKey {
43+
/// Public key extracted from `keydata` attribute.
44+
pub public_key: SignedPublicKey,
45+
46+
/// True if `Autocrypt-Gossip` has a `_verified` attribute.
47+
pub is_verified: bool,
48+
}
49+
3950
/// A parsed MIME message.
4051
///
4152
/// This represents the relevant information of a parsed MIME message
@@ -85,7 +96,7 @@ pub(crate) struct MimeMessage {
8596

8697
/// The addresses for which there was a gossip header
8798
/// and their respective gossiped keys.
88-
pub gossiped_keys: HashMap<String, SignedPublicKey>,
99+
pub gossiped_keys: BTreeMap<String, GossipedKey>,
89100

90101
/// Fingerprint of the key in the Autocrypt header.
91102
///
@@ -1904,9 +1915,9 @@ async fn parse_gossip_headers(
19041915
from: &str,
19051916
recipients: &[SingleInfo],
19061917
gossip_headers: Vec<String>,
1907-
) -> Result<HashMap<String, SignedPublicKey>> {
1918+
) -> Result<BTreeMap<String, GossipedKey>> {
19081919
// XXX split the parsing from the modification part
1909-
let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
1920+
let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
19101921

19111922
for value in &gossip_headers {
19121923
let header = match value.parse::<Aheader>() {
@@ -1948,7 +1959,12 @@ async fn parse_gossip_headers(
19481959
)
19491960
.await?;
19501961

1951-
gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
1962+
let gossiped_key = GossipedKey {
1963+
public_key: header.public_key,
1964+
1965+
is_verified: header.is_verified,
1966+
};
1967+
gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
19521968
}
19531969

19541970
Ok(gossiped_keys)

src/receive_imf.rs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Internet Message Format reception pipeline.
22
3-
use std::collections::{HashMap, HashSet};
3+
use std::collections::{BTreeMap, HashSet};
44
use std::iter;
55
use std::sync::LazyLock;
66

@@ -28,14 +28,14 @@ use crate::events::EventType;
2828
use crate::headerdef::{HeaderDef, HeaderDefMap};
2929
use crate::imap::{GENERATED_PREFIX, markseen_on_imap_table};
3030
use crate::key::self_fingerprint_opt;
31-
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
31+
use crate::key::{DcKey, Fingerprint};
3232
use crate::log::LogExt;
3333
use crate::log::{info, warn};
3434
use crate::logged_debug_assert;
3535
use crate::message::{
3636
self, Message, MessageState, MessengerMessage, MsgId, Viewtype, rfc724_mid_exists,
3737
};
38-
use crate::mimeparser::{AvatarAction, MimeMessage, SystemMessage, parse_message_ids};
38+
use crate::mimeparser::{AvatarAction, GossipedKey, MimeMessage, SystemMessage, parse_message_ids};
3939
use crate::param::{Param, Params};
4040
use crate::peer_channels::{add_gossip_peer_from_header, insert_topic_stub};
4141
use crate::reaction::{Reaction, set_msg_reaction};
@@ -743,7 +743,7 @@ pub(crate) async fn receive_imf_inner(
743743
let verified_encryption = has_verified_encryption(context, &mime_parser, from_id).await?;
744744

745745
if verified_encryption == VerifiedEncryption::Verified {
746-
mark_recipients_as_verified(context, from_id, &to_ids, &mime_parser).await?;
746+
mark_recipients_as_verified(context, from_id, &mime_parser).await?;
747747
}
748748

749749
let received_msg = if let Some(received_msg) = received_msg {
@@ -836,7 +836,7 @@ pub(crate) async fn receive_imf_inner(
836836
context
837837
.sql
838838
.transaction(move |transaction| {
839-
let fingerprint = gossiped_key.dc_fingerprint().hex();
839+
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
840840
transaction.execute(
841841
"INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
842842
VALUES (?, ?, ?)
@@ -2923,7 +2923,7 @@ async fn apply_group_changes(
29232923
// highest `add_timestamp` to disambiguate.
29242924
// The result of the error is that info message
29252925
// may contain display name of the wrong contact.
2926-
let fingerprint = key.dc_fingerprint().hex();
2926+
let fingerprint = key.public_key.dc_fingerprint().hex();
29272927
if let Some(contact_id) =
29282928
lookup_key_contact_by_fingerprint(context, &fingerprint).await?
29292929
{
@@ -3662,13 +3662,18 @@ async fn has_verified_encryption(
36623662
async fn mark_recipients_as_verified(
36633663
context: &Context,
36643664
from_id: ContactId,
3665-
to_ids: &[Option<ContactId>],
36663665
mimeparser: &MimeMessage,
36673666
) -> Result<()> {
3668-
if mimeparser.get_header(HeaderDef::ChatVerified).is_none() {
3669-
return Ok(());
3670-
}
3671-
for to_id in to_ids.iter().filter_map(|&x| x) {
3667+
for gossiped_key in mimeparser
3668+
.gossiped_keys
3669+
.values()
3670+
.filter(|gossiped_key| gossiped_key.is_verified)
3671+
{
3672+
let fingerprint = gossiped_key.public_key.dc_fingerprint().hex();
3673+
let Some(to_id) = lookup_key_contact_by_fingerprint(context, &fingerprint).await? else {
3674+
continue;
3675+
};
3676+
36723677
if to_id == ContactId::SELF || to_id == from_id {
36733678
continue;
36743679
}
@@ -3760,7 +3765,7 @@ async fn add_or_lookup_contacts_by_address_list(
37603765
async fn add_or_lookup_key_contacts(
37613766
context: &Context,
37623767
address_list: &[SingleInfo],
3763-
gossiped_keys: &HashMap<String, SignedPublicKey>,
3768+
gossiped_keys: &BTreeMap<String, GossipedKey>,
37643769
fingerprints: &[Fingerprint],
37653770
origin: Origin,
37663771
) -> Result<Vec<Option<ContactId>>> {
@@ -3776,7 +3781,7 @@ async fn add_or_lookup_key_contacts(
37763781
// Iterator has not ran out of fingerprints yet.
37773782
fp.hex()
37783783
} else if let Some(key) = gossiped_keys.get(addr) {
3779-
key.dc_fingerprint().hex()
3784+
key.public_key.dc_fingerprint().hex()
37803785
} else if context.is_self_addr(addr).await? {
37813786
contact_ids.push(Some(ContactId::SELF));
37823787
continue;

src/receive_imf/receive_imf_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5117,6 +5117,8 @@ async fn test_unverified_member_msg() -> Result<()> {
51175117
.contains("Re-download the message or see 'Info' for more details")
51185118
);
51195119

5120+
// Shift the time by 1 week to trigger gossiping.
5121+
SystemTime::shift(Duration::from_secs(3600 * 24 * 7));
51205122
let alice_sent_msg = alice
51215123
.send_text(alice_chat_id, "Hi all, it's Alice introducing Fiona")
51225124
.await;

src/securejoin.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,9 @@ pub(crate) async fn handle_securejoin_handshake(
272272
let mut self_found = false;
273273
let self_fingerprint = load_self_public_key(context).await?.dc_fingerprint();
274274
for (addr, key) in &mime_message.gossiped_keys {
275-
if key.dc_fingerprint() == self_fingerprint && context.is_self_addr(addr).await? {
275+
if key.public_key.dc_fingerprint() == self_fingerprint
276+
&& context.is_self_addr(addr).await?
277+
{
276278
self_found = true;
277279
break;
278280
}
@@ -542,7 +544,7 @@ pub(crate) async fn observe_securejoin_on_other_device(
542544
return Ok(HandshakeMessage::Ignore);
543545
};
544546

545-
if key.dc_fingerprint() != contact_fingerprint {
547+
if key.public_key.dc_fingerprint() != contact_fingerprint {
546548
// Fingerprint does not match, ignore.
547549
warn!(context, "Fingerprint does not match.");
548550
return Ok(HandshakeMessage::Ignore);

src/securejoin/securejoin_tests.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::chat::{CantSendReason, remove_contact_from_chat};
55
use crate::chatlist::Chatlist;
66
use crate::constants::Chattype;
77
use crate::key::self_fingerprint;
8+
use crate::mimeparser::GossipedKey;
89
use crate::receive_imf::receive_imf;
910
use crate::stock_str::{self, messages_e2e_encrypted};
1011
use crate::test_utils::{
@@ -185,7 +186,10 @@ async fn test_setup_contact_ex(case: SetupContactCase) {
185186
);
186187

187188
if case == SetupContactCase::WrongAliceGossip {
188-
let wrong_pubkey = load_self_public_key(&bob).await.unwrap();
189+
let wrong_pubkey = GossipedKey {
190+
public_key: load_self_public_key(&bob).await.unwrap(),
191+
is_verified: false,
192+
};
189193
let alice_pubkey = msg
190194
.gossiped_keys
191195
.insert(alice_addr.to_string(), wrong_pubkey)

0 commit comments

Comments
 (0)