mirror of
https://github.com/chatmail/core.git
synced 2026-04-29 11:26:29 +03:00
feat: merge OpenPGP certificates and distribute relays in them
We put all relay addresses as a notation subpacket in the direct key signature to distribute the relay addresses.
This commit is contained in:
@@ -315,11 +315,10 @@ def test_transport_limit(acfactory) -> None:
|
||||
account.add_transport_from_qr(qr)
|
||||
|
||||
|
||||
def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
def test_message_info_imap_urls(acfactory) -> None:
|
||||
"""Test that message info contains IMAP URLs of where the message was received."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
log.section("Alice adds ac1 clone removes second transport")
|
||||
qr = acfactory.get_account_qr()
|
||||
for i in range(3):
|
||||
alice.add_transport_from_qr(qr)
|
||||
@@ -327,9 +326,6 @@ def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
for _ in range(i + 1):
|
||||
alice.bring_online()
|
||||
|
||||
new_alice_addr = alice.list_transports()[2]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
|
||||
# Enable multi-device mode so messages are not deleted immediately.
|
||||
alice.set_config("bcc_self", "1")
|
||||
|
||||
@@ -337,12 +333,51 @@ def test_message_info_imap_urls(acfactory, log) -> None:
|
||||
# This is where he will send the message.
|
||||
bob_chat = bob.create_chat(alice)
|
||||
|
||||
# Alice changes the transport again.
|
||||
alice.set_config("configured_addr", alice.list_transports()[3]["addr"])
|
||||
# Alice switches to another transport and removes the rest of the transports.
|
||||
new_alice_addr = alice.list_transports()[1]["addr"]
|
||||
alice.set_config("configured_addr", new_alice_addr)
|
||||
removed_addrs = []
|
||||
for transport in alice.list_transports():
|
||||
if transport["addr"] != new_alice_addr:
|
||||
alice.delete_transport(transport["addr"])
|
||||
removed_addrs.append(transport["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
|
||||
msg = alice.wait_for_incoming_msg()
|
||||
for alice_transport in alice.list_transports():
|
||||
addr = alice_transport["addr"]
|
||||
assert (addr == new_alice_addr) == (addr in msg.get_info())
|
||||
msg_info = msg.get_info()
|
||||
assert new_alice_addr in msg_info
|
||||
for removed_addr in removed_addrs:
|
||||
assert removed_addr not in msg_info
|
||||
assert f"{new_alice_addr}/INBOX" in msg_info
|
||||
|
||||
|
||||
def test_remove_primary_transport(acfactory) -> None:
|
||||
"""Test that after removing the primary relay, Alice can still receive messages."""
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
qr = acfactory.get_account_qr()
|
||||
|
||||
alice.add_transport_from_qr(qr)
|
||||
alice.bring_online()
|
||||
|
||||
bob_chat = bob.create_chat(alice)
|
||||
alice.create_chat(bob)
|
||||
|
||||
# Alice changes the transport.
|
||||
[transport1, transport2] = alice.list_transports()
|
||||
alice.set_config("configured_addr", transport2["addr"])
|
||||
|
||||
bob_chat.send_text("Hello!")
|
||||
msg1 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg1.text == "Hello!"
|
||||
|
||||
# Alice deletes the first transport.
|
||||
alice.delete_transport(transport1["addr"])
|
||||
alice.stop_io()
|
||||
alice.start_io()
|
||||
|
||||
bob_chat.send_text("Hello again!")
|
||||
msg2 = alice.wait_for_incoming_msg().get_snapshot()
|
||||
assert msg2.text == "Hello again!"
|
||||
|
||||
10
src/chat.rs
10
src/chat.rs
@@ -42,6 +42,7 @@ use crate::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use crate::mimefactory::{MimeFactory, RenderedEmail};
|
||||
use crate::mimeparser::SystemMessage;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::addresses_from_public_key;
|
||||
use crate::receive_imf::ReceivedMsg;
|
||||
use crate::smtp::{self, send_msg_to_smtp};
|
||||
use crate::stock_str;
|
||||
@@ -1174,8 +1175,13 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
|
||||
let fingerprint = contact
|
||||
.fingerprint()
|
||||
.context("Contact does not have a fingerprint in encrypted chat")?;
|
||||
if contact.public_key(context).await?.is_some() {
|
||||
ret += &format!("\n{addr}\n{fingerprint}\n");
|
||||
if let Some(public_key) = contact.public_key(context).await? {
|
||||
if let Some(relay_addrs) = addresses_from_public_key(&public_key) {
|
||||
let relays = relay_addrs.join(",");
|
||||
ret += &format!("\n{addr}({relays})\n{fingerprint}\n");
|
||||
} else {
|
||||
ret += &format!("\n{addr}\n{fingerprint}\n");
|
||||
}
|
||||
} else {
|
||||
ret += &format!("\n{addr}\n(key missing)\n{fingerprint}\n");
|
||||
}
|
||||
|
||||
@@ -3856,6 +3856,7 @@ async fn test_only_broadcast_owner_can_send_2() -> Result<()> {
|
||||
tcm.section("Now, Alice's fingerprint changes");
|
||||
|
||||
alice.sql.execute("DELETE FROM keypairs", ()).await?;
|
||||
*alice.self_public_key.lock().await = None;
|
||||
alice
|
||||
.sql
|
||||
.execute("DELETE FROM config WHERE keyname='key_id'", ())
|
||||
@@ -4046,7 +4047,7 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"Messages are end-to-end encrypted.\n\
|
||||
\n\
|
||||
bob@example.net\n\
|
||||
bob@example.net(bob@example.net)\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
@@ -4056,11 +4057,11 @@ async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
chat_id.get_encryption_info(alice).await?,
|
||||
"Messages are end-to-end encrypted.\n\
|
||||
\n\
|
||||
fiona@example.net\n\
|
||||
fiona@example.net(fiona@example.net)\n\
|
||||
C8BA 50BF 4AC1 2FAF 38D7\n\
|
||||
F657 DDFC 8E9F 3C79 9195\n\
|
||||
\n\
|
||||
bob@example.net\n\
|
||||
bob@example.net(bob@example.net)\n\
|
||||
CCCB 5AA9 F6E1 141C 9431\n\
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
);
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::log::{LogExt, warn};
|
||||
use crate::message::MessageState;
|
||||
use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::{addresses_from_public_key, merge_openpgp_certificates};
|
||||
use crate::sync::{self, Sync::*};
|
||||
use crate::tools::{SystemTime, duration_to_str, get_abs_path, normalize_text, time, to_lowercase};
|
||||
use crate::{chat, chatlist_events, ensure_and_debug_assert_ne, stock_str};
|
||||
@@ -314,6 +315,67 @@ pub async fn make_vcard(context: &Context, contacts: &[ContactId]) -> Result<Str
|
||||
.to_string())
|
||||
}
|
||||
|
||||
/// Imports public key into the public key store.
|
||||
///
|
||||
/// They key may come from Autocrypt header,
|
||||
/// Autocrypt-Gossip header or a vCard.
|
||||
///
|
||||
/// If the key with the same fingerprint already exists,
|
||||
/// it is updated by merging the new key.
|
||||
pub(crate) async fn import_public_key(
|
||||
context: &Context,
|
||||
public_key: &SignedPublicKey,
|
||||
) -> Result<()> {
|
||||
public_key
|
||||
.verify_bindings()
|
||||
.context("Attempt to import broken public key")?;
|
||||
|
||||
let fingerprint = public_key.dc_fingerprint().hex();
|
||||
|
||||
let merged_public_key;
|
||||
let merged_public_key_ref = if let Some(public_key_bytes) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT public_key
|
||||
FROM public_keys
|
||||
WHERE fingerprint=?",
|
||||
(&fingerprint,),
|
||||
|row| {
|
||||
let bytes: Vec<u8> = row.get(0)?;
|
||||
Ok(bytes)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
let old_public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
merged_public_key = merge_openpgp_certificates(public_key.clone(), old_public_key)
|
||||
.context("Failed to merge public keys")?;
|
||||
&merged_public_key
|
||||
} else {
|
||||
public_key
|
||||
};
|
||||
|
||||
let inserted = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO UPDATE SET public_key=excluded.public_key
|
||||
WHERE public_key!=excluded.public_key",
|
||||
(&fingerprint, merged_public_key_ref.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
if inserted > 0 {
|
||||
info!(
|
||||
context,
|
||||
"Saved key with fingerprint {fingerprint} from the Autocrypt header"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Imports contacts from the given vCard.
|
||||
///
|
||||
/// Returns the ids of successfully processed contacts in the order they appear in `vcard`,
|
||||
@@ -352,23 +414,14 @@ async fn import_vcard_contact(context: &Context, contact: &VcardContact) -> Resu
|
||||
.ok()
|
||||
});
|
||||
|
||||
let fingerprint;
|
||||
if let Some(public_key) = key {
|
||||
fingerprint = public_key.dc_fingerprint().hex();
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
let fingerprint = if let Some(public_key) = key {
|
||||
import_public_key(context, &public_key)
|
||||
.await
|
||||
.context("Failed to import public key from vCard")?;
|
||||
public_key.dc_fingerprint().hex()
|
||||
} else {
|
||||
fingerprint = String::new();
|
||||
}
|
||||
String::new()
|
||||
};
|
||||
|
||||
let (id, modified) =
|
||||
match Contact::add_or_lookup_ex(context, &contact.authname, &addr, &fingerprint, origin)
|
||||
@@ -1384,6 +1437,16 @@ WHERE addr=?
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(public_key) = contact.public_key(context).await?
|
||||
&& let Some(relay_addrs) = addresses_from_public_key(&public_key)
|
||||
{
|
||||
ret += "\n\nRelays:";
|
||||
for relay in &relay_addrs {
|
||||
ret += "\n";
|
||||
ret += relay;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
|
||||
@@ -841,7 +841,10 @@ Me (alice@example.org):
|
||||
|
||||
bob@example.net (bob@example.net):
|
||||
CCCB 5AA9 F6E1 141C 9431
|
||||
65F1 DB18 B18C BCF7 0487"
|
||||
65F1 DB18 B18C BCF7 0487
|
||||
|
||||
Relays:
|
||||
bob@example.net"
|
||||
);
|
||||
let contact = Contact::get_by_id(alice, contact_bob_id).await?;
|
||||
assert!(contact.e2ee_avail(alice).await?);
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{Result, bail, ensure};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use pgp::composed::SignedPublicKey;
|
||||
use ratelimit::Ratelimit;
|
||||
use tokio::sync::{Mutex, Notify, RwLock};
|
||||
|
||||
@@ -233,8 +234,6 @@ pub struct InnerContext {
|
||||
/// This is a global mutex-like state for operations which should be modal in the
|
||||
/// clients.
|
||||
running_state: RwLock<RunningState>,
|
||||
/// Mutex to avoid generating the key for the user more than once.
|
||||
pub(crate) generating_key_mutex: Mutex<()>,
|
||||
/// Mutex to enforce only a single running oauth2 is running.
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messages being sent.
|
||||
@@ -317,6 +316,13 @@ pub struct InnerContext {
|
||||
/// the standard library's OnceLock is enough, and it's a lot smaller in memory.
|
||||
pub(crate) self_fingerprint: OnceLock<String>,
|
||||
|
||||
/// OpenPGP certificate aka Transferrable Public Key.
|
||||
///
|
||||
/// It is generated on first use from the secret key stored in the database.
|
||||
///
|
||||
/// Mutex is also held while generating the key to avoid generating the key twice.
|
||||
pub(crate) self_public_key: Mutex<Option<SignedPublicKey>>,
|
||||
|
||||
/// `Connectivity` values for mailboxes, unordered. Used to compute the aggregate connectivity,
|
||||
/// see [`Context::get_connectivity()`].
|
||||
pub(crate) connectivities: parking_lot::Mutex<Vec<ConnectivityStore>>,
|
||||
@@ -486,7 +492,6 @@ impl Context {
|
||||
running_state: RwLock::new(Default::default()),
|
||||
sql: Sql::new(dbfile),
|
||||
smeared_timestamp: SmearedTimestamp::new(),
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
housekeeping_mutex: Mutex::new(()),
|
||||
@@ -508,6 +513,7 @@ impl Context {
|
||||
tls_session_store: TlsSessionStore::new(),
|
||||
iroh: Arc::new(RwLock::new(None)),
|
||||
self_fingerprint: OnceLock::new(),
|
||||
self_public_key: Mutex::new(None),
|
||||
connectivities: parking_lot::Mutex::new(Vec::new()),
|
||||
pre_encrypt_mime_hook: None.into(),
|
||||
};
|
||||
|
||||
188
src/key.rs
188
src/key.rs
@@ -7,10 +7,18 @@ use std::io::Cursor;
|
||||
use anyhow::{Context as _, Result, bail, ensure};
|
||||
use base64::Engine as _;
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use pgp::composed::Deserializable;
|
||||
use pgp::composed::{Deserializable, SignedKeyDetails};
|
||||
pub use pgp::composed::{SignedPublicKey, SignedSecretKey};
|
||||
use pgp::crypto::aead::AeadAlgorithm;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{
|
||||
Features, KeyFlags, Notation, PacketTrait as _, SignatureConfig, SignatureType, Subpacket,
|
||||
SubpacketData,
|
||||
};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::KeyDetails;
|
||||
use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion};
|
||||
use rand_old::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::context::Context;
|
||||
@@ -114,10 +122,149 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
fn is_private() -> bool;
|
||||
}
|
||||
|
||||
/// Converts secret key to public key.
|
||||
pub(crate) fn secret_key_to_public_key(
|
||||
context: &Context,
|
||||
mut signed_secret_key: SignedSecretKey,
|
||||
timestamp: u32,
|
||||
addr: &str,
|
||||
relay_addrs: &str,
|
||||
) -> Result<SignedPublicKey> {
|
||||
info!(context, "Converting secret key to public key.");
|
||||
let timestamp = pgp::types::Timestamp::from_secs(timestamp);
|
||||
|
||||
// Subpackets that we want to share between DKS and User ID signature.
|
||||
let common_subpackets = || -> Result<Vec<Subpacket>> {
|
||||
let keyflags = {
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_certify(true);
|
||||
keyflags.set_sign(true);
|
||||
keyflags
|
||||
};
|
||||
let features = {
|
||||
let mut features = Features::default();
|
||||
features.set_seipd_v1(true);
|
||||
features.set_seipd_v2(true);
|
||||
features
|
||||
};
|
||||
|
||||
Ok(vec![
|
||||
Subpacket::regular(SubpacketData::SignatureCreationTime(timestamp))?,
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
signed_secret_key.fingerprint(),
|
||||
))?,
|
||||
Subpacket::regular(SubpacketData::KeyFlags(keyflags))?,
|
||||
Subpacket::regular(SubpacketData::Features(features))?,
|
||||
Subpacket::regular(SubpacketData::PreferredSymmetricAlgorithms(smallvec![
|
||||
SymmetricKeyAlgorithm::AES256,
|
||||
SymmetricKeyAlgorithm::AES192,
|
||||
SymmetricKeyAlgorithm::AES128
|
||||
]))?,
|
||||
Subpacket::regular(SubpacketData::PreferredHashAlgorithms(smallvec![
|
||||
HashAlgorithm::Sha256,
|
||||
HashAlgorithm::Sha384,
|
||||
HashAlgorithm::Sha512,
|
||||
HashAlgorithm::Sha224,
|
||||
]))?,
|
||||
Subpacket::regular(SubpacketData::PreferredCompressionAlgorithms(smallvec![
|
||||
CompressionAlgorithm::ZLIB,
|
||||
CompressionAlgorithm::ZIP,
|
||||
]))?,
|
||||
Subpacket::regular(SubpacketData::PreferredAeadAlgorithms(smallvec![(
|
||||
SymmetricKeyAlgorithm::AES256,
|
||||
AeadAlgorithm::Ocb
|
||||
)]))?,
|
||||
Subpacket::regular(SubpacketData::IsPrimary(true))?,
|
||||
])
|
||||
};
|
||||
|
||||
// RFC 4880 required that Transferrable Public Key (aka OpenPGP Certificate)
|
||||
// contains at least one User ID:
|
||||
// <https://www.rfc-editor.org/rfc/rfc4880#section-11.1>
|
||||
// RFC 9580 does not require User ID even for V4 certificates anymore:
|
||||
// <https://www.rfc-editor.org/rfc/rfc9580.html#name-openpgp-version-4-certifica>
|
||||
//
|
||||
// We do not use and do not expect User ID in any keys,
|
||||
// but nevertheless include User ID in V4 keys for compatibility with clients that follow RFC 4880.
|
||||
// RFC 9580 also recommends including User ID into V4 keys:
|
||||
// <https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.10-8>
|
||||
//
|
||||
// We do not support keys older than V4 and are not going
|
||||
// to include User ID in newer V6 keys as all clients that support V6
|
||||
// should support keys without User ID.
|
||||
let users = if signed_secret_key.version() == KeyVersion::V4 {
|
||||
let user_id = format!("<{addr}>");
|
||||
|
||||
let mut rng = thread_rng();
|
||||
// Self-signature is a "positive certification",
|
||||
// see <https://www.ietf.org/archive/id/draft-gallagher-openpgp-signatures-02.html#name-certification-signature-typ>.
|
||||
let mut user_id_signature_config = SignatureConfig::from_key(
|
||||
&mut rng,
|
||||
&signed_secret_key.primary_key,
|
||||
SignatureType::CertPositive,
|
||||
)?;
|
||||
user_id_signature_config.hashed_subpackets = common_subpackets()?;
|
||||
user_id_signature_config.unhashed_subpackets = vec![Subpacket::regular(
|
||||
SubpacketData::IssuerKeyId(signed_secret_key.legacy_key_id()),
|
||||
)?];
|
||||
let user_id_packet =
|
||||
pgp::packet::UserId::from_str(pgp::types::PacketHeaderVersion::New, &user_id)?;
|
||||
let signature = user_id_signature_config.sign_certification(
|
||||
&signed_secret_key.primary_key,
|
||||
&signed_secret_key.primary_key.public_key(),
|
||||
&pgp::types::Password::empty(),
|
||||
user_id_packet.tag(),
|
||||
&user_id_packet,
|
||||
)?;
|
||||
vec![user_id_packet.into_signed(signature)]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let direct_signatures = {
|
||||
let mut rng = thread_rng();
|
||||
let mut direct_key_signature_config = SignatureConfig::from_key(
|
||||
&mut rng,
|
||||
&signed_secret_key.primary_key,
|
||||
SignatureType::Key,
|
||||
)?;
|
||||
direct_key_signature_config.hashed_subpackets = common_subpackets()?;
|
||||
let notation = Notation {
|
||||
readable: true,
|
||||
name: "relays@chatmail.at".into(),
|
||||
value: relay_addrs.to_string().into(),
|
||||
};
|
||||
direct_key_signature_config
|
||||
.hashed_subpackets
|
||||
.push(Subpacket::regular(SubpacketData::Notation(notation))?);
|
||||
let direct_key_signature = direct_key_signature_config.sign_key(
|
||||
&signed_secret_key.primary_key,
|
||||
&pgp::types::Password::empty(),
|
||||
signed_secret_key.primary_key.public_key(),
|
||||
)?;
|
||||
vec![direct_key_signature]
|
||||
};
|
||||
|
||||
signed_secret_key.details = SignedKeyDetails {
|
||||
revocation_signatures: vec![],
|
||||
direct_signatures,
|
||||
users,
|
||||
user_attributes: vec![],
|
||||
};
|
||||
|
||||
Ok(signed_secret_key.to_public_key())
|
||||
}
|
||||
|
||||
/// Attempts to load own public key.
|
||||
///
|
||||
/// Returns `None` if no key is generated yet.
|
||||
/// Returns `None` if no secret key is generated yet.
|
||||
pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option<SignedPublicKey>> {
|
||||
let mut lock = context.self_public_key.lock().await;
|
||||
|
||||
if let Some(ref public_key) = *lock {
|
||||
return Ok(Some(public_key.clone()));
|
||||
}
|
||||
|
||||
let Some(secret_key_bytes) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
@@ -135,7 +282,25 @@ pub(crate) async fn load_self_public_key_opt(context: &Context) -> Result<Option
|
||||
return Ok(None);
|
||||
};
|
||||
let signed_secret_key = SignedSecretKey::from_slice(&secret_key_bytes)?;
|
||||
let signed_public_key = signed_secret_key.to_public_key();
|
||||
let timestamp = context
|
||||
.sql
|
||||
.query_get_value::<u32>(
|
||||
"SELECT MAX(timestamp)
|
||||
FROM (SELECT add_timestamp AS timestamp
|
||||
FROM transports
|
||||
UNION ALL
|
||||
SELECT remove_timestamp AS timestamp
|
||||
FROM removed_transports)",
|
||||
(),
|
||||
)
|
||||
.await?
|
||||
.context("No transports configured")?;
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let all_addrs = context.get_all_self_addrs().await?.join(",");
|
||||
let signed_public_key =
|
||||
secret_key_to_public_key(context, signed_secret_key, timestamp, &addr, &all_addrs)?;
|
||||
*lock = Some(signed_public_key.clone());
|
||||
|
||||
Ok(Some(signed_public_key))
|
||||
}
|
||||
|
||||
@@ -146,8 +311,11 @@ pub(crate) async fn load_self_public_key(context: &Context) -> Result<SignedPubl
|
||||
match load_self_public_key_opt(context).await? {
|
||||
Some(public_key) => Ok(public_key),
|
||||
None => {
|
||||
let signed_secret_key = generate_keypair(context).await?;
|
||||
Ok(signed_secret_key.to_public_key())
|
||||
generate_keypair(context).await?;
|
||||
let public_key = load_self_public_key_opt(context)
|
||||
.await?
|
||||
.context("Secret key generated, but public key cannot be created")?;
|
||||
Ok(public_key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,7 +455,7 @@ impl DcKey for SignedSecretKey {
|
||||
async fn generate_keypair(context: &Context) -> Result<SignedSecretKey> {
|
||||
let addr = context.get_primary_self_addr().await?;
|
||||
let addr = EmailAddress::new(&addr)?;
|
||||
let _guard = context.generating_key_mutex.lock().await;
|
||||
let _public_key_guard = context.self_public_key.lock().await;
|
||||
|
||||
// Check if the key appeared while we were waiting on the lock.
|
||||
match load_keypair(context).await? {
|
||||
@@ -343,6 +511,12 @@ pub(crate) async fn store_self_keypair(
|
||||
context: &Context,
|
||||
signed_secret_key: &SignedSecretKey,
|
||||
) -> Result<()> {
|
||||
// This public key is stored in the database
|
||||
// only for backwards compatibility.
|
||||
//
|
||||
// It should not be used e.g. in Autocrypt headers or vCards.
|
||||
// Use `secret_key_to_public_key()` function instead,
|
||||
// which adds relay list to the signature.
|
||||
let signed_public_key = signed_secret_key.to_public_key();
|
||||
let mut config_cache_lock = context.sql.config_cache.write().await;
|
||||
let new_key_id = context
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{SystemMessage, is_hidden};
|
||||
use crate::param::Param;
|
||||
use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
|
||||
use crate::pgp::SeipdVersion;
|
||||
use crate::pgp::{SeipdVersion, addresses_from_public_key};
|
||||
use crate::simplify::escape_message_footer_marks;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -274,10 +274,13 @@ impl MimeFactory {
|
||||
.await?
|
||||
.context("Can't send member addition/removal: missing key")?;
|
||||
|
||||
recipients.push(addr.clone());
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
|
||||
let relays =
|
||||
addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
|
||||
recipients.extend(relays);
|
||||
to.push((authname, addr.clone()));
|
||||
|
||||
let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
|
||||
encryption_pubkeys = Some(vec![(addr, public_key)]);
|
||||
} else {
|
||||
let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
|
||||
@@ -354,9 +357,23 @@ impl MimeFactory {
|
||||
false => "".to_string(),
|
||||
};
|
||||
if add_timestamp >= remove_timestamp {
|
||||
let relays = if let Some(public_key) = public_key_opt {
|
||||
let addrs = addresses_from_public_key(&public_key);
|
||||
keys.push((addr.clone(), public_key));
|
||||
addrs
|
||||
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}.unwrap_or_else(|| vec![addr.clone()]);
|
||||
|
||||
if !recipients_contain_addr(&to, &addr) {
|
||||
if id != ContactId::SELF {
|
||||
recipients.push(addr.clone());
|
||||
recipients.extend(relays);
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
to.push((name, addr.clone()));
|
||||
@@ -374,35 +391,31 @@ impl MimeFactory {
|
||||
}
|
||||
}
|
||||
recipient_ids.insert(id);
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
}
|
||||
}
|
||||
} else if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
|
||||
// Row is a tombstone,
|
||||
// member is not actually part of the group.
|
||||
if !recipients_contain_addr(&past_members, &addr) {
|
||||
if let Some(email_to_remove) = email_to_remove
|
||||
&& email_to_remove == addr {
|
||||
// This is a "member removed" message,
|
||||
// we need to notify removed member
|
||||
// that it was removed.
|
||||
if id != ContactId::SELF {
|
||||
recipients.push(addr.clone());
|
||||
}
|
||||
|
||||
if let Some(public_key) = public_key_opt {
|
||||
keys.push((addr.clone(), public_key))
|
||||
let relays = if let Some(public_key) = public_key_opt {
|
||||
let addrs = addresses_from_public_key(&public_key);
|
||||
keys.push((addr.clone(), public_key));
|
||||
addrs
|
||||
} else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
|
||||
missing_key_addresses.insert(addr.clone());
|
||||
if is_encrypted {
|
||||
warn!(context, "Missing key for {addr}");
|
||||
}
|
||||
None
|
||||
} else {
|
||||
None
|
||||
}.unwrap_or_else(|| vec![addr.clone()]);
|
||||
|
||||
// This is a "member removed" message,
|
||||
// we need to notify removed member
|
||||
// that it was removed.
|
||||
if id != ContactId::SELF {
|
||||
recipients.extend(relays);
|
||||
}
|
||||
}
|
||||
if !undisclosed_recipients {
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::blob::BlobObject;
|
||||
use crate::chat::ChatId;
|
||||
use crate::config::Config;
|
||||
use crate::constants;
|
||||
use crate::contact::ContactId;
|
||||
use crate::contact::{ContactId, import_public_key};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{self, validate_detached_signature};
|
||||
use crate::dehtml::dehtml;
|
||||
@@ -458,22 +458,9 @@ impl MimeMessage {
|
||||
|
||||
let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
|
||||
let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
|
||||
let inserted = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, autocrypt_header.public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
if inserted > 0 {
|
||||
info!(
|
||||
context,
|
||||
"Saved key with fingerprint {fingerprint} from the Autocrypt header"
|
||||
);
|
||||
}
|
||||
import_public_key(context, &autocrypt_header.public_key)
|
||||
.await
|
||||
.context("Failed to import public key from the Autocrypt header")?;
|
||||
Some(fingerprint)
|
||||
} else {
|
||||
None
|
||||
@@ -1659,24 +1646,12 @@ impl MimeMessage {
|
||||
}
|
||||
Ok(key) => key,
|
||||
};
|
||||
if let Err(err) = key.verify_bindings() {
|
||||
warn!(context, "Attached PGP key verification failed: {err:#}.");
|
||||
if let Err(err) = import_public_key(context, &key).await {
|
||||
warn!(context, "Attached PGP key import failed: {err:#}.");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let fingerprint = key.dc_fingerprint().hex();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(context, "Imported PGP key {fingerprint} from attachment.");
|
||||
info!(context, "Imported PGP key from attachment.");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -2185,17 +2160,9 @@ async fn parse_gossip_headers(
|
||||
continue;
|
||||
}
|
||||
|
||||
let fingerprint = header.public_key.dc_fingerprint().hex();
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO public_keys (fingerprint, public_key)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (fingerprint)
|
||||
DO NOTHING",
|
||||
(&fingerprint, header.public_key.to_bytes()),
|
||||
)
|
||||
.await?;
|
||||
import_public_key(context, &header.public_key)
|
||||
.await
|
||||
.context("Failed to import Autocrypt-Gossip key")?;
|
||||
|
||||
let gossiped_key = GossipedKey {
|
||||
public_key: header.public_key,
|
||||
|
||||
194
src/pgp.rs
194
src/pgp.rs
@@ -3,23 +3,25 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::io::{BufRead, Cursor};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_contact_tools::EmailAddress;
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
use deltachat_contact_tools::{EmailAddress, may_be_valid_addr};
|
||||
use pgp::armor::BlockType;
|
||||
use pgp::composed::{
|
||||
ArmorOptions, Deserializable, DetachedSignature, EncryptionCaps, KeyType as PgpKeyType,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
|
||||
SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
Message, MessageBuilder, SecretKeyParamsBuilder, SignedKeyDetails, SignedPublicKey,
|
||||
SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder, SubpacketConfig,
|
||||
};
|
||||
use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
|
||||
use pgp::crypto::ecc_curve::ECCCurve;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, SignatureConfig, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, KeyDetails, KeyVersion, Password, SigningKey as _, StringToKey,
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
};
|
||||
use rand_old::{Rng as _, thread_rng};
|
||||
use sha2::Sha256;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
@@ -420,6 +422,166 @@ pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
|
||||
.await?
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
/// at most one User ID with exactly one signature.
|
||||
///
|
||||
/// See <https://openpgp.dev/book/adv/certificates.html#merging>
|
||||
/// and <https://openpgp.dev/book/adv/certificates.html#certificate-minimization>.
|
||||
///
|
||||
/// `new_certificate` does not necessarily contain newer data.
|
||||
/// It may come not directly from the key owner,
|
||||
/// e.g. via protected Autocrypt header or protected attachment
|
||||
/// in a signed message, but from Autocrypt-Gossip header or a vCard.
|
||||
/// Gossiped key may be older than the one we have
|
||||
/// or even have some packets maliciously dropped
|
||||
/// (for example, all encryption subkeys dropped)
|
||||
/// or restored from some older version of the certificate.
|
||||
pub fn merge_openpgp_certificates(
|
||||
old_certificate: SignedPublicKey,
|
||||
new_certificate: SignedPublicKey,
|
||||
) -> Result<SignedPublicKey> {
|
||||
old_certificate
|
||||
.verify_bindings()
|
||||
.context("First key cannot be verified")?;
|
||||
new_certificate
|
||||
.verify_bindings()
|
||||
.context("Second key cannot be verified")?;
|
||||
|
||||
// Decompose certificates.
|
||||
let SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
details: old_details,
|
||||
public_subkeys: old_public_subkeys,
|
||||
} = old_certificate;
|
||||
let SignedPublicKey {
|
||||
primary_key: new_primary_key,
|
||||
details: new_details,
|
||||
public_subkeys: _new_public_subkeys,
|
||||
} = new_certificate;
|
||||
|
||||
// Public keys may be serialized differently, e.g. using old and new packet type,
|
||||
// so we compare imprints instead of comparing the keys
|
||||
// directly with `old_primary_key == new_primary_key`.
|
||||
// Imprints, like fingerprints, are calculated over normalized packets.
|
||||
// On error we print fingerprints as this is what is used in the database
|
||||
// and what most tools show.
|
||||
let old_imprint = old_primary_key.imprint::<Sha256>()?;
|
||||
let new_imprint = new_primary_key.imprint::<Sha256>()?;
|
||||
ensure!(
|
||||
old_imprint == new_imprint,
|
||||
"Cannot merge certificates with different primary keys {} and {}",
|
||||
old_primary_key.fingerprint(),
|
||||
new_primary_key.fingerprint()
|
||||
);
|
||||
|
||||
// Decompose old and the new key details.
|
||||
//
|
||||
// Revocation signatures are currently ignored so we do not store them.
|
||||
//
|
||||
// User attributes are thrown away on purpose,
|
||||
// the only defined in RFC 9580 attribute is the Image Attribute
|
||||
// (<https://www.rfc-editor.org/rfc/rfc9580.html#section-5.12.1>
|
||||
// which we do not use and do not want to gossip.
|
||||
let SignedKeyDetails {
|
||||
revocation_signatures: _old_revocation_signatures,
|
||||
direct_signatures: old_direct_signatures,
|
||||
users: old_users,
|
||||
user_attributes: _old_user_attributes,
|
||||
} = old_details;
|
||||
let SignedKeyDetails {
|
||||
revocation_signatures: _new_revocation_signatures,
|
||||
direct_signatures: new_direct_signatures,
|
||||
users: new_users,
|
||||
user_attributes: _new_user_attributes,
|
||||
} = new_details;
|
||||
|
||||
// Select at most one direct key signature, the newest one.
|
||||
let best_direct_key_signature: Option<Signature> = old_direct_signatures
|
||||
.into_iter()
|
||||
.chain(new_direct_signatures)
|
||||
.filter(|x: &Signature| x.verify_key(&old_primary_key).is_ok())
|
||||
.max_by_key(|x: &Signature|
|
||||
// Converting to seconds because `Ord` is not derived for `Timestamp`:
|
||||
// <https://github.com/rpgp/rpgp/issues/737>
|
||||
x.created().map_or(0, |ts| ts.as_secs()));
|
||||
let direct_signatures: Vec<Signature> = best_direct_key_signature.into_iter().collect();
|
||||
|
||||
// Select at most one User ID.
|
||||
//
|
||||
// We prefer User IDs marked as primary,
|
||||
// but will select non-primary otherwise
|
||||
// because sometimes keys have no primary User ID,
|
||||
// such as Alice's key in `test-data/key/alice-secret.asc`.
|
||||
let best_user: Option<SignedUser> = old_users
|
||||
.into_iter()
|
||||
.chain(new_users.clone())
|
||||
.filter_map(|SignedUser { id, signatures }| {
|
||||
// Select the best signature for each User ID.
|
||||
// If User ID has no valid signatures, it is filtered out.
|
||||
let best_user_signature: Option<Signature> = signatures
|
||||
.into_iter()
|
||||
.filter(|signature: &Signature| {
|
||||
signature
|
||||
.verify_certification(&old_primary_key, pgp::types::Tag::UserId, &id)
|
||||
.is_ok()
|
||||
})
|
||||
.max_by_key(|signature: &Signature| {
|
||||
signature.created().map_or(0, |ts| ts.as_secs())
|
||||
});
|
||||
best_user_signature.map(|signature| (id, signature))
|
||||
})
|
||||
.max_by_key(|(_id, signature)| signature.created().map_or(0, |ts| ts.as_secs()))
|
||||
.map(|(id, signature)| SignedUser {
|
||||
id,
|
||||
signatures: vec![signature],
|
||||
});
|
||||
let users: Vec<SignedUser> = best_user.into_iter().collect();
|
||||
|
||||
let public_subkeys = old_public_subkeys;
|
||||
|
||||
Ok(SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
details: SignedKeyDetails {
|
||||
revocation_signatures: vec![],
|
||||
direct_signatures,
|
||||
users,
|
||||
user_attributes: vec![],
|
||||
},
|
||||
public_subkeys,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns relays addresses from the public key signature.
|
||||
///
|
||||
/// Not more than 3 relays are returned for each key.
|
||||
pub(crate) fn addresses_from_public_key(public_key: &SignedPublicKey) -> Option<Vec<String>> {
|
||||
for signature in &public_key.details.direct_signatures {
|
||||
// The signature should be verified already when importing the key,
|
||||
// but we double-check here.
|
||||
let signature_is_valid = signature.verify_key(&public_key.primary_key).is_ok();
|
||||
debug_assert!(signature_is_valid);
|
||||
if signature_is_valid {
|
||||
for notation in signature.notations() {
|
||||
if notation.name == "relays@chatmail.at"
|
||||
&& let Ok(value) = str::from_utf8(¬ation.value)
|
||||
{
|
||||
return Some(
|
||||
value
|
||||
.split(",")
|
||||
.map(|s| s.to_string())
|
||||
.filter(|s| may_be_valid_addr(s))
|
||||
.take(3)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::LazyLock;
|
||||
@@ -818,4 +980,24 @@ mod tests {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_openpgp_certificates() {
|
||||
let alice = alice_keypair().to_public_key();
|
||||
let bob = bob_keypair().to_public_key();
|
||||
|
||||
// Merging certificate with itself does not change it.
|
||||
assert_eq!(
|
||||
merge_openpgp_certificates(alice.clone(), alice.clone()).unwrap(),
|
||||
alice
|
||||
);
|
||||
assert_eq!(
|
||||
merge_openpgp_certificates(bob.clone(), bob.clone()).unwrap(),
|
||||
bob
|
||||
);
|
||||
|
||||
// Cannot merge certificates with different primary key.
|
||||
assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err());
|
||||
assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -861,6 +861,10 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
|
||||
if transport_changed {
|
||||
info!(context, "Primary transport changed to {from_addr:?}.");
|
||||
context.sql.uncache_raw_config("configured_addr").await;
|
||||
|
||||
// Regenerate User ID in V4 keys.
|
||||
context.self_public_key.lock().await.take();
|
||||
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -669,6 +669,9 @@ pub(crate) async fn save_transport(
|
||||
pub(crate) async fn send_sync_transports(context: &Context) -> Result<()> {
|
||||
info!(context, "Sending transport synchronization message.");
|
||||
|
||||
// Regenerate public key to include all transports.
|
||||
context.self_public_key.lock().await.take();
|
||||
|
||||
// Synchronize all transport configurations.
|
||||
//
|
||||
// Transport with ID 1 is never synchronized
|
||||
@@ -761,6 +764,7 @@ pub(crate) async fn sync_transports(
|
||||
.await?;
|
||||
|
||||
if modified {
|
||||
context.self_public_key.lock().await.take();
|
||||
tokio::task::spawn(restart_io_if_running_boxed(context.clone()));
|
||||
context.emit_event(EventType::TransportsModified);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user