mirror of
https://github.com/chatmail/core.git
synced 2026-07-23 03:13:06 +03:00
Compare commits
2 Commits
link2xt/au
...
hpk/outer-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
285e8b08d8 | ||
|
|
0e6d8c4d4e |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -1378,7 +1378,6 @@ dependencies = [
|
||||
"futures",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"hkdf",
|
||||
"http-body-util",
|
||||
"humansize",
|
||||
"hyper",
|
||||
|
||||
@@ -61,7 +61,6 @@ fd-lock = "4"
|
||||
futures-lite = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
hex = "0.4.0"
|
||||
hkdf = { version = "0.12", default-features = false }
|
||||
http-body-util = "0.1.3"
|
||||
humansize = "2"
|
||||
hyper = "1"
|
||||
|
||||
@@ -478,9 +478,14 @@ pub enum Config {
|
||||
#[strum(props(default = "1"))]
|
||||
ForceEncryption,
|
||||
|
||||
/// Generate Autocrypt 2 instead of Autocrypt 1 key.
|
||||
#[strum(props(default = "1"))]
|
||||
Autocrypt2,
|
||||
/// If the From address in the encrypted part differs from the outer one,
|
||||
/// keep the outer address as the contact address and trash the message
|
||||
/// unless the outer address is a relay address of the sender's key.
|
||||
///
|
||||
/// Only the outer From is enforced by the sender's relay,
|
||||
/// so only it can be used as an identity.
|
||||
#[strum(props(default = "0"))]
|
||||
EnforceOuterFromKeyAlignment,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -1038,8 +1038,10 @@ impl Context {
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"autocrypt2",
|
||||
self.get_config_bool(Config::Autocrypt2).await?.to_string(),
|
||||
"enforce_outer_from_key_alignment",
|
||||
self.get_config_bool(Config::EnforceOuterFromKeyAlignment)
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
|
||||
let elapsed = time_elapsed(&self.creation_time);
|
||||
|
||||
@@ -25,10 +25,9 @@ impl EncryptHelper {
|
||||
}
|
||||
|
||||
pub fn get_aheader(&self) -> Aheader {
|
||||
let public_key = pgp::minimize_autocrypt_certificate(&self.public_key);
|
||||
Aheader {
|
||||
addr: self.addr.clone(),
|
||||
public_key,
|
||||
public_key: self.public_key.clone(),
|
||||
prefer_encrypt: EncryptPreference::Mutual,
|
||||
verified: false,
|
||||
}
|
||||
|
||||
21
src/key.rs
21
src/key.rs
@@ -17,12 +17,10 @@ use pgp::packet::{
|
||||
SubpacketData,
|
||||
};
|
||||
use pgp::ser::Serialize;
|
||||
use pgp::types::Timestamp as PgpTimestamp;
|
||||
use pgp::types::{CompressionAlgorithm, KeyDetails, KeyVersion};
|
||||
use rand_old::thread_rng;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::events::EventType;
|
||||
use crate::log::LogExt;
|
||||
@@ -126,11 +124,13 @@ pub trait DcKey: Serialize + Deserializable + Clone {
|
||||
|
||||
/// 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.
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn secret_key_to_public_key(
|
||||
};
|
||||
|
||||
Ok(vec.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::btree_map::Entry as BTreeMapEntry;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::{Context as _, Result, ensure};
|
||||
@@ -16,7 +14,7 @@ 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::{Signature, SignatureType, Subpacket, SubpacketData};
|
||||
use pgp::packet::{Signature, Subpacket, SubpacketData};
|
||||
use pgp::types::{
|
||||
CompressionAlgorithm, Imprint, KeyDetails, KeyVersion, Password, SignedUser, SigningKey as _,
|
||||
StringToKey,
|
||||
@@ -27,8 +25,6 @@ use tokio::runtime::Handle;
|
||||
|
||||
use crate::key::{DcKey, Fingerprint};
|
||||
|
||||
pub(crate) mod autocrypt2;
|
||||
|
||||
/// Preferred symmetric encryption algorithm.
|
||||
const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
|
||||
|
||||
@@ -87,63 +83,13 @@ pub(crate) fn create_keypair(addr: EmailAddress) -> Result<SignedSecretKey> {
|
||||
|
||||
/// Selects a subkey of the public key to use for encryption.
|
||||
///
|
||||
/// The key is selected according to
|
||||
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.3-4>.
|
||||
/// If multiple keys are available, the one that will expire sooner is selected.
|
||||
///
|
||||
/// Returns `None` if the public key cannot be used for encryption.
|
||||
fn select_pk_for_encryption(now: u32, key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
///
|
||||
/// TODO: take key flags and expiration dates into account
|
||||
fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
|
||||
key.public_subkeys
|
||||
.iter()
|
||||
.filter(|subkey| subkey.algorithm().can_encrypt())
|
||||
.filter_map(|subkey| {
|
||||
// TODO: deal with multiple signatures.
|
||||
let signature = subkey.signatures.first()?;
|
||||
|
||||
let key_flags = signature.key_flags();
|
||||
if !key_flags.encrypt_comms() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(expiration_duration) = signature
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0)
|
||||
&& now
|
||||
> subkey
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration_duration.as_secs())
|
||||
{
|
||||
// Key is expired.
|
||||
return None;
|
||||
}
|
||||
Some((subkey, signature))
|
||||
})
|
||||
.min_by(|(subkey1, signature1), (subkey2, signature2)| {
|
||||
match (
|
||||
signature1
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
signature2
|
||||
.key_expiration_time()
|
||||
.filter(|duration| duration.as_secs() != 0),
|
||||
) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Greater,
|
||||
(Some(_), None) => Ordering::Less,
|
||||
(Some(expiration1), Some(expiration2)) => (subkey1
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration1.as_secs()))
|
||||
.cmp(
|
||||
&(subkey2
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.saturating_add(expiration2.as_secs())),
|
||||
),
|
||||
}
|
||||
})
|
||||
.map(|(subkey, _signature)| subkey)
|
||||
.find(|subkey| subkey.algorithm().can_encrypt())
|
||||
}
|
||||
|
||||
/// Version of SEIPD packet to use.
|
||||
@@ -173,11 +119,10 @@ pub async fn pk_encrypt(
|
||||
Handle::current()
|
||||
.spawn_blocking(move || {
|
||||
let mut rng = thread_rng();
|
||||
let now = pgp::types::Timestamp::now();
|
||||
|
||||
let pkeys = public_keys_for_encryption
|
||||
.iter()
|
||||
.filter_map(|key| select_pk_for_encryption(now.as_secs(), key));
|
||||
.filter_map(select_pk_for_encryption);
|
||||
let subpkts = {
|
||||
let mut hashed = Vec::with_capacity(1 + public_keys_for_encryption.len() + 1);
|
||||
hashed.push(Subpacket::critical(SubpacketData::SignatureCreationTime(
|
||||
@@ -346,161 +291,6 @@ pub fn symm_encrypt_message(
|
||||
Ok(encoded_msg)
|
||||
}
|
||||
|
||||
/// Minimizes the signatures of a subkey.
|
||||
///
|
||||
/// Keeps at most one subkey binding signature
|
||||
/// and at most one revocation signature,
|
||||
/// preferring the newest signatures.
|
||||
///
|
||||
/// Subkey binding signature is kept
|
||||
/// even if the revocation signature exists
|
||||
/// because according to
|
||||
/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-openpgp-version-6-certifica>
|
||||
/// "Every subkey MUST have at least one Subkey Binding signature."
|
||||
/// Distributing subkey with only a revocation signature
|
||||
/// is not allowed according to the standard,
|
||||
/// so we keep a subkey binding signature next to it
|
||||
/// for interoperability.
|
||||
///
|
||||
/// This function does not check if the signatures are valid.
|
||||
/// Such properties should be validated when importing OpenPGP certificates.
|
||||
fn minimize_subpacket_signatures(signatures: Vec<Signature>) -> Vec<Signature> {
|
||||
let mut newest_revocation_signature: Option<Signature> = None;
|
||||
let mut newest_binding_signature: Option<Signature> = None;
|
||||
for signature in signatures {
|
||||
let Some(config) = signature.config() else {
|
||||
// Skip unknown signatures.
|
||||
continue;
|
||||
};
|
||||
match config.typ {
|
||||
SignatureType::SubkeyBinding => {
|
||||
if newest_binding_signature
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.created() < signature.created())
|
||||
{
|
||||
newest_binding_signature = Some(signature)
|
||||
}
|
||||
}
|
||||
SignatureType::SubkeyRevocation => {
|
||||
if newest_revocation_signature
|
||||
.as_ref()
|
||||
.is_none_or(|s| s.created() < signature.created())
|
||||
{
|
||||
newest_revocation_signature = Some(signature)
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
newest_revocation_signature
|
||||
.into_iter()
|
||||
.chain(newest_binding_signature)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Minimizes OpenPGP certificate for Autocrypt and Autocrypt-Gossip headers.
|
||||
pub fn minimize_autocrypt_certificate(certificate: &SignedPublicKey) -> SignedPublicKey {
|
||||
let primary_key = certificate.primary_key.clone();
|
||||
let details = certificate.details.clone();
|
||||
|
||||
// Select the newest non-expiring subkey and the newest expiring subkey.
|
||||
let fallback_subkey = certificate
|
||||
.public_subkeys
|
||||
.iter()
|
||||
.filter(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.is_some_and(|signature| signature.key_expiration_time().is_none())
|
||||
})
|
||||
.max_by_key(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
|
||||
});
|
||||
let rotating_subkey = certificate
|
||||
.public_subkeys
|
||||
.iter()
|
||||
.filter(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.is_some_and(|signature| signature.key_expiration_time().is_some())
|
||||
})
|
||||
.max_by_key(|subkey| {
|
||||
subkey
|
||||
.signatures
|
||||
.iter()
|
||||
.find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})
|
||||
.map(|signature| signature.created().unwrap_or(subkey.created_at()))
|
||||
});
|
||||
let public_subkeys: Vec<_> = fallback_subkey
|
||||
.into_iter()
|
||||
.chain(rotating_subkey)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// We do not want to ever gossip more than two subkeys
|
||||
// to save the traffic.
|
||||
debug_assert!(public_subkeys.len() <= 2);
|
||||
|
||||
SignedPublicKey {
|
||||
primary_key,
|
||||
details,
|
||||
public_subkeys,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges two OpenPGP subkeys.
|
||||
fn merge_openpgp_subkey(old_subkey: &mut SignedPublicSubKey, new_subkey: SignedPublicSubKey) {
|
||||
debug_assert_eq!(old_subkey.fingerprint(), new_subkey.fingerprint());
|
||||
old_subkey.signatures = minimize_subpacket_signatures(
|
||||
std::mem::take(&mut old_subkey.signatures)
|
||||
.into_iter()
|
||||
.chain(new_subkey.signatures)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Merges OpenPGP subkey vectors.
|
||||
pub fn merge_openpgp_subkeys(
|
||||
subkeys: impl IntoIterator<Item = SignedPublicSubKey>,
|
||||
) -> Result<Vec<SignedPublicSubKey>> {
|
||||
let mut merged_subkeys: BTreeMap<_, SignedPublicSubKey> = BTreeMap::new();
|
||||
for subkey in subkeys {
|
||||
let imprint = subkey.imprint::<Sha256>()?;
|
||||
match merged_subkeys.entry(imprint) {
|
||||
BTreeMapEntry::Vacant(entry) => {
|
||||
entry.insert(subkey);
|
||||
}
|
||||
BTreeMapEntry::Occupied(entry) => {
|
||||
merge_openpgp_subkey(entry.into_mut(), subkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(merged_subkeys.into_values().collect())
|
||||
}
|
||||
|
||||
/// Merges and minimizes OpenPGP certificates.
|
||||
///
|
||||
/// Keeps at most one direct key signature and
|
||||
@@ -537,7 +327,7 @@ pub fn merge_openpgp_certificates(
|
||||
let SignedPublicKey {
|
||||
primary_key: new_primary_key,
|
||||
details: new_details,
|
||||
public_subkeys: new_public_subkeys,
|
||||
public_subkeys: _new_public_subkeys,
|
||||
} = new_certificate;
|
||||
|
||||
// Public keys may be serialized differently, e.g. using old and new packet type,
|
||||
@@ -613,55 +403,7 @@ pub fn merge_openpgp_certificates(
|
||||
});
|
||||
let users: Vec<SignedUser> = best_user.into_iter().collect();
|
||||
|
||||
let (fallback_subkeys, mut rotating_subkeys): (Vec<_>, Vec<_>) =
|
||||
merge_openpgp_subkeys(old_public_subkeys.into_iter().chain(new_public_subkeys))?
|
||||
.into_iter()
|
||||
.filter_map(|subkey| {
|
||||
// Select the newest subkey binding signature.
|
||||
//
|
||||
// There is at most one subkey binding signature at this point
|
||||
// because older subkey binding signatures are removed during merging.
|
||||
let signature = subkey.signatures.iter().find(|signature| {
|
||||
signature
|
||||
.config()
|
||||
.is_some_and(|config| config.typ == SignatureType::SubkeyBinding)
|
||||
})?;
|
||||
|
||||
let created_at_secs = signature.created().unwrap_or(subkey.created_at()).as_secs();
|
||||
let expires_at_secs: Option<u32> = signature
|
||||
.key_expiration_time()
|
||||
.map(|duration| duration.as_secs())
|
||||
.filter(|duration_secs| *duration_secs != 0)
|
||||
.map(|duration_secs| {
|
||||
subkey.created_at().as_secs().saturating_add(duration_secs)
|
||||
});
|
||||
|
||||
Some((subkey, created_at_secs, expires_at_secs))
|
||||
})
|
||||
.partition(|(_subkey, _created_at_secs, expires_at_secs)| expires_at_secs.is_none());
|
||||
let fallback_subkey: Option<SignedPublicSubKey> = fallback_subkeys
|
||||
.into_iter()
|
||||
.max_by_key(|(_subkey, created_at_secs, _)| *created_at_secs)
|
||||
.map(|(subkey, _, _)| subkey);
|
||||
|
||||
rotating_subkeys
|
||||
.sort_by_key(|(_subkey, created_at_secs, _)| std::cmp::Reverse(*created_at_secs));
|
||||
|
||||
// Put the fallback subkey first so it is gossiped first.
|
||||
//
|
||||
// We want to always gossip non-expiring key first
|
||||
// for older versions that always encrypted to the first subkey.
|
||||
//
|
||||
// Keep 10 newest rotating subkeys to avoid storing indefinitely growing number of subkeys locally.
|
||||
let public_subkeys = fallback_subkey
|
||||
.into_iter()
|
||||
.chain(
|
||||
rotating_subkeys
|
||||
.into_iter()
|
||||
.take(10)
|
||||
.map(|(subkey, _, _)| subkey),
|
||||
)
|
||||
.collect();
|
||||
let public_subkeys = old_public_subkeys;
|
||||
|
||||
Ok(SignedPublicKey {
|
||||
primary_key: old_primary_key,
|
||||
|
||||
@@ -1,588 +0,0 @@
|
||||
//! Autocrypt2 implementation.
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use anyhow::ensure;
|
||||
use anyhow::format_err;
|
||||
use hkdf::Hkdf;
|
||||
use pgp::composed::SignedKeyDetails;
|
||||
use pgp::composed::SignedSecretKey;
|
||||
use pgp::composed::SignedSecretSubKey;
|
||||
use pgp::crypto::aead::AeadAlgorithm;
|
||||
use pgp::crypto::ed25519;
|
||||
use pgp::crypto::hash::HashAlgorithm;
|
||||
use pgp::crypto::ml_kem768_x25519;
|
||||
use pgp::crypto::public_key::PublicKeyAlgorithm;
|
||||
use pgp::crypto::sym::SymmetricKeyAlgorithm;
|
||||
use pgp::packet::Features;
|
||||
use pgp::packet::KeyFlags;
|
||||
use pgp::packet::PacketTrait as _;
|
||||
use pgp::packet::PubKeyInner;
|
||||
use pgp::packet::PublicKey;
|
||||
use pgp::packet::PublicSubkey;
|
||||
use pgp::packet::SecretKey;
|
||||
use pgp::packet::SecretSubkey;
|
||||
use pgp::packet::SignatureConfig;
|
||||
use pgp::packet::SignatureType;
|
||||
use pgp::packet::Subpacket;
|
||||
use pgp::packet::SubpacketData;
|
||||
use pgp::ser::Serialize as _;
|
||||
use pgp::types::Duration as PgpDuration;
|
||||
use pgp::types::Ed25519PublicParams;
|
||||
use pgp::types::KeyDetails;
|
||||
use pgp::types::KeyVersion;
|
||||
use pgp::types::MlKem768X25519PublicParams;
|
||||
use pgp::types::Password;
|
||||
use pgp::types::PlainSecretParams;
|
||||
use pgp::types::PublicParams;
|
||||
use pgp::types::SecretParams;
|
||||
use pgp::types::Timestamp;
|
||||
use rand_old::thread_rng;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha512;
|
||||
|
||||
/// Creates an Autocrypt 2 TSK.
|
||||
///
|
||||
/// <https://datatracker.ietf.org/doc/draft-autocrypt-openpgp-v2-cert/>
|
||||
pub(crate) fn create_autocrypt2_keypair(now: Timestamp) -> Result<SignedSecretKey> {
|
||||
let mut rng = thread_rng();
|
||||
|
||||
// Fake zero timestamp for primary key and fallback key creation.
|
||||
// We do not want to leak the key creation date to contacts.
|
||||
// This is not to be used for rotating subkey timestamps.
|
||||
let zero_timestamp = Timestamp::from_secs(0);
|
||||
|
||||
let public_key_algorithm = PublicKeyAlgorithm::Ed25519;
|
||||
|
||||
let primary_key_packet = {
|
||||
let ed25519_secret = ed25519::SecretKey::generate(&mut rng, ed25519::Mode::Ed25519);
|
||||
let public_params = PublicParams::Ed25519(Ed25519PublicParams::from(&ed25519_secret));
|
||||
let secret_params = SecretParams::Plain(PlainSecretParams::Ed25519(ed25519_secret));
|
||||
|
||||
let pubkey_inner = PubKeyInner::new(
|
||||
KeyVersion::V6,
|
||||
public_key_algorithm,
|
||||
zero_timestamp,
|
||||
None,
|
||||
public_params,
|
||||
)?;
|
||||
let pubkey = PublicKey::from_inner(pubkey_inner)?;
|
||||
SecretKey::new(pubkey, secret_params)?
|
||||
};
|
||||
|
||||
let details = {
|
||||
let mut signature_config =
|
||||
SignatureConfig::from_key(&mut rng, &primary_key_packet, SignatureType::Key)?;
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_certify(true);
|
||||
keyflags.set_sign(true);
|
||||
|
||||
let mut features = Features::default();
|
||||
features.set_seipd_v1(true);
|
||||
features.set_seipd_v2(true);
|
||||
|
||||
signature_config.hashed_subpackets = vec![
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(now))?,
|
||||
Subpacket::regular(SubpacketData::KeyFlags(keyflags))?,
|
||||
Subpacket::regular(SubpacketData::Features(features))?,
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
primary_key_packet.fingerprint(),
|
||||
))?,
|
||||
Subpacket::regular(SubpacketData::PreferredAeadAlgorithms(smallvec![(
|
||||
SymmetricKeyAlgorithm::AES256,
|
||||
AeadAlgorithm::Ocb
|
||||
)]))?,
|
||||
];
|
||||
|
||||
let signature = signature_config.sign_key(
|
||||
&primary_key_packet,
|
||||
&Password::empty(),
|
||||
&primary_key_packet.public_key(),
|
||||
)?;
|
||||
|
||||
SignedKeyDetails {
|
||||
revocation_signatures: vec![],
|
||||
direct_signatures: vec![signature],
|
||||
users: vec![],
|
||||
user_attributes: vec![],
|
||||
}
|
||||
};
|
||||
|
||||
let fallback_subkey_packet = {
|
||||
let ml_kem_secret = ml_kem768_x25519::SecretKey::generate(&mut rng);
|
||||
let public_params =
|
||||
PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(&ml_kem_secret));
|
||||
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(ml_kem_secret));
|
||||
|
||||
let pubkey_inner = PubKeyInner::new(
|
||||
KeyVersion::V6,
|
||||
PublicKeyAlgorithm::MlKem768X25519,
|
||||
zero_timestamp,
|
||||
None,
|
||||
public_params,
|
||||
)?;
|
||||
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
|
||||
SecretSubkey::new(public_subkey, secret_params)?
|
||||
};
|
||||
|
||||
let signed_fallback_subkey = {
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_encrypt_storage(true);
|
||||
keyflags.set_encrypt_comms(true);
|
||||
|
||||
let mut signature_config = SignatureConfig::v6(
|
||||
&mut rng,
|
||||
SignatureType::SubkeyBinding,
|
||||
public_key_algorithm,
|
||||
HashAlgorithm::Sha256,
|
||||
)?;
|
||||
signature_config.hashed_subpackets = vec![
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(zero_timestamp))?,
|
||||
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
primary_key_packet.fingerprint(),
|
||||
))?,
|
||||
];
|
||||
let signature = signature_config.sign_subkey_binding(
|
||||
&primary_key_packet,
|
||||
primary_key_packet.public_key(),
|
||||
&Password::empty(),
|
||||
fallback_subkey_packet.public_key(),
|
||||
)?;
|
||||
SignedSecretSubKey {
|
||||
key: fallback_subkey_packet,
|
||||
signatures: vec![signature],
|
||||
}
|
||||
};
|
||||
|
||||
let rotating_subkey_packet = {
|
||||
let ml_kem_secret = ml_kem768_x25519::SecretKey::generate(&mut rng);
|
||||
let public_params =
|
||||
PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(&ml_kem_secret));
|
||||
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(ml_kem_secret));
|
||||
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_encrypt_comms(true);
|
||||
|
||||
let pubkey_inner = PubKeyInner::new(
|
||||
KeyVersion::V6,
|
||||
PublicKeyAlgorithm::MlKem768X25519,
|
||||
now,
|
||||
None,
|
||||
public_params,
|
||||
)?;
|
||||
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
|
||||
SecretSubkey::new(public_subkey, secret_params)?
|
||||
};
|
||||
|
||||
let signed_rotating_subkey = {
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_encrypt_comms(true);
|
||||
|
||||
let mut signature_config = SignatureConfig::v6(
|
||||
&mut rng,
|
||||
SignatureType::SubkeyBinding,
|
||||
public_key_algorithm,
|
||||
HashAlgorithm::Sha256,
|
||||
)?;
|
||||
|
||||
// Expiration duration is 10 days according to
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.6.2.2.1>
|
||||
let expiration_duration = PgpDuration::from_secs(864000);
|
||||
signature_config.hashed_subpackets = vec![
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(now))?,
|
||||
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
|
||||
// XXX: marking expiration as critical
|
||||
// even though reference implementation does not:
|
||||
// <https://codeberg.org/autocrypt2/autocrypt-v2-cert/issues/53>
|
||||
Subpacket::critical(SubpacketData::KeyExpirationTime(expiration_duration))?,
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
primary_key_packet.fingerprint(),
|
||||
))?,
|
||||
];
|
||||
let signature = signature_config.sign_subkey_binding(
|
||||
&primary_key_packet,
|
||||
primary_key_packet.public_key(),
|
||||
&Password::empty(),
|
||||
rotating_subkey_packet.public_key(),
|
||||
)?;
|
||||
SignedSecretSubKey {
|
||||
key: rotating_subkey_packet,
|
||||
signatures: vec![signature],
|
||||
}
|
||||
};
|
||||
|
||||
let secret_key = SignedSecretKey {
|
||||
primary_key: primary_key_packet,
|
||||
details,
|
||||
public_subkeys: Vec::new(),
|
||||
secret_subkeys: vec![signed_fallback_subkey, signed_rotating_subkey],
|
||||
};
|
||||
|
||||
secret_key
|
||||
.verify_bindings()
|
||||
.context("Invalid Autocrypt2 key generated")?;
|
||||
|
||||
Ok(secret_key)
|
||||
}
|
||||
|
||||
/// Returns true if TSK is an Autocrypt 2 TSK.
|
||||
///
|
||||
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#name-identification-by-tsk-struc>
|
||||
fn is_autocrypt2_tsk(tsk: &SignedSecretKey) -> bool {
|
||||
if tsk.primary_key.version() != KeyVersion::V6
|
||||
|| tsk.primary_key.algorithm() != PublicKeyAlgorithm::Ed25519
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Direct key signature.
|
||||
let [direct_key_signature] = &tsk.details.direct_signatures[..] else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(features) = direct_key_signature.features() else {
|
||||
return false;
|
||||
};
|
||||
// SEIPDv2 feature is required according to
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.4.1>
|
||||
if !features.seipd_v2() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Primary key must have certification (0x01) and signing (0x02) flags.
|
||||
let dks_key_flags = direct_key_signature.key_flags();
|
||||
if !dks_key_flags.certify() || !dks_key_flags.sign() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// No expiration:
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.6.1>
|
||||
// No key expiration (<https://www.rfc-editor.org/rfc/rfc9580.html#name-key-expiration-time>)
|
||||
// and no signature expiration (<https://docs.rs/pgp/latest/pgp/packet/struct.Signature.html#method.signature_expiration_time>).
|
||||
//
|
||||
// XXX: spec should say explicitly that both key expiration and signature expiration should not be there
|
||||
if direct_key_signature
|
||||
.key_expiration_time()
|
||||
.is_some_and(|duration| duration.as_secs() != 0)
|
||||
|| direct_key_signature
|
||||
.signature_expiration_time()
|
||||
.is_some_and(|duration| duration.as_secs() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if !(tsk.details.revocation_signatures.is_empty()
|
||||
&& tsk.details.users.is_empty()
|
||||
&& tsk.details.user_attributes.is_empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if !tsk.public_subkeys.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: check all rotating subkeys
|
||||
// Subkeys may overlap, as long as subkey is not expired, it does not need to be deleted.
|
||||
let [ref fallback_subkey, .., ref rotating_subkey] = tsk.secret_subkeys[..] else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let [ref fallback_subkey_signature] = fallback_subkey.signatures[..] else {
|
||||
return false;
|
||||
};
|
||||
let fallback_subkey_flags = fallback_subkey_signature.key_flags();
|
||||
if !fallback_subkey_flags.encrypt_comms() || !fallback_subkey_flags.encrypt_storage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if fallback_subkey_signature
|
||||
.key_expiration_time()
|
||||
.is_some_and(|duration| duration.as_secs() != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let [ref rotating_subkey_signature] = rotating_subkey.signatures[..] else {
|
||||
return false;
|
||||
};
|
||||
let rotating_subkey_flags = rotating_subkey_signature.key_flags();
|
||||
// Rotating subkey can be used to encrypt communications, but not storage:
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.6.2.3.1>
|
||||
if !rotating_subkey_flags.encrypt_comms() || rotating_subkey_flags.encrypt_storage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if rotating_subkey_signature
|
||||
.key_expiration_time()
|
||||
.is_none_or(|duration| duration.as_secs() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn normalize_x25519_scalar(m: &mut [u8]) {
|
||||
// From decodeScalar25519 in <https://www.rfc-editor.org/info/rfc7748/#section-5>
|
||||
m[0] &= 248;
|
||||
m[31] &= 127;
|
||||
m[31] |= 64;
|
||||
}
|
||||
|
||||
/// Generates new rotating subkey from a previous one.
|
||||
///
|
||||
/// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1>
|
||||
fn ratchet(mut tsk: SignedSecretKey) -> Result<SignedSecretKey> {
|
||||
// Extract the last rotating subkey.
|
||||
// Other rotating subkeys do not matter.
|
||||
// This corresponds to
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1-6.2.1>
|
||||
let [ref _fallback_subkey, .., ref rotating_subkey] = tsk.secret_subkeys[..] else {
|
||||
bail!("Cannot extract last rotating subkey");
|
||||
};
|
||||
let [ref rotating_subkey_signature] = rotating_subkey.signatures[..] else {
|
||||
bail!("Rotating subkey must have exactly one signature");
|
||||
};
|
||||
let rotating_subkey_flags = rotating_subkey_signature.key_flags();
|
||||
// We do not search for the latest-expiring subkey
|
||||
// with the ability to encrypt communications.
|
||||
// It must be the last one by convention.
|
||||
// TODO: write TSK structure explicitly in the specification.
|
||||
let max_rd: u32 = rotating_subkey_signature
|
||||
.key_expiration_time()
|
||||
.context("Last subkey is not expiring")?
|
||||
.as_secs();
|
||||
let min_rd: u32 = max_rd / 2;
|
||||
ensure!(
|
||||
rotating_subkey_flags.encrypt_comms(),
|
||||
"Last rotating subkey cannot be used to encrypt communications"
|
||||
);
|
||||
|
||||
let start: u32 = rotating_subkey
|
||||
.created_at()
|
||||
.as_secs()
|
||||
.checked_add(min_rd)
|
||||
.context("Overflow while adding min_rd")?;
|
||||
let mut salt = Vec::from(start.to_be_bytes());
|
||||
rotating_subkey
|
||||
.public_key()
|
||||
.to_writer_with_header(&mut salt)
|
||||
.context("Failed to serialize rotating subkey")?;
|
||||
debug_assert_eq!(
|
||||
salt.len(),
|
||||
4 + rotating_subkey.public_key().write_len_with_header()
|
||||
);
|
||||
|
||||
let SecretParams::Plain(PlainSecretParams::MlKem768X25519(old_ml_kem768_x25519_secret_key)) =
|
||||
rotating_subkey.secret_params()
|
||||
else {
|
||||
bail!("Cannot extract ML-KEM-768 + X25519 secret key");
|
||||
};
|
||||
let mut ikm = Vec::with_capacity(old_ml_kem768_x25519_secret_key.write_len());
|
||||
old_ml_kem768_x25519_secret_key
|
||||
.to_writer(&mut ikm)
|
||||
.context("Failed to serialize IKM")?;
|
||||
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-4.1.1-6.6.1>
|
||||
normalize_x25519_scalar(&mut ikm);
|
||||
debug_assert_eq!(ikm.len(), 96);
|
||||
|
||||
let info = {
|
||||
let mut info = b"Autocrypt_v2_ratchet".to_vec();
|
||||
tsk.primary_key
|
||||
.public_key()
|
||||
.to_writer_with_header(&mut info)
|
||||
.context("Failed to serialize primary key")?;
|
||||
info.extend_from_slice(&max_rd.to_be_bytes());
|
||||
info
|
||||
};
|
||||
|
||||
let hkdf = Hkdf::<Sha512>::new(Some(&salt), &ikm);
|
||||
let mut ks = [0u8; 160];
|
||||
hkdf.expand(&info, &mut ks)
|
||||
.map_err(|_err: hkdf::InvalidLength| {
|
||||
format_err!("HKDF-Expand failed because of invalid output length")
|
||||
})?;
|
||||
|
||||
let new_ml_kem768_x25519_secret_key = {
|
||||
let mut new_x25519 = [0u8; 32];
|
||||
let mut new_ml_kem = [0u8; 64];
|
||||
new_x25519.copy_from_slice(&ks[64..96]);
|
||||
new_ml_kem.copy_from_slice(&ks[96..160]);
|
||||
normalize_x25519_scalar(&mut new_x25519[..]);
|
||||
|
||||
ml_kem768_x25519::SecretKey::try_from_bytes(new_x25519, new_ml_kem)?
|
||||
};
|
||||
let new_rotating_subkey = {
|
||||
let public_params = PublicParams::MlKem768X25519(MlKem768X25519PublicParams::from(
|
||||
&new_ml_kem768_x25519_secret_key,
|
||||
));
|
||||
let secret_params = SecretParams::Plain(PlainSecretParams::MlKem768X25519(
|
||||
new_ml_kem768_x25519_secret_key,
|
||||
));
|
||||
|
||||
let pubkey_inner = PubKeyInner::new(
|
||||
KeyVersion::V6,
|
||||
PublicKeyAlgorithm::MlKem768X25519,
|
||||
Timestamp::from_secs(start),
|
||||
None,
|
||||
public_params,
|
||||
)?;
|
||||
let public_subkey = PublicSubkey::from_inner(pubkey_inner)?;
|
||||
SecretSubkey::new(public_subkey, secret_params)?
|
||||
};
|
||||
|
||||
let new_signed_rotating_subkey = {
|
||||
let mut keyflags = KeyFlags::default();
|
||||
keyflags.set_encrypt_comms(true);
|
||||
|
||||
let digest = Sha512::digest(&ks[0..64]);
|
||||
let bssalt = digest[0..16].to_vec();
|
||||
|
||||
let mut signature_config = SignatureConfig::v6_with_salt(
|
||||
SignatureType::SubkeyBinding,
|
||||
tsk.primary_key.algorithm(),
|
||||
HashAlgorithm::Sha256,
|
||||
bssalt,
|
||||
);
|
||||
|
||||
// FIXME
|
||||
let expiration_duration = PgpDuration::from_secs(864000);
|
||||
signature_config.hashed_subpackets = vec![
|
||||
Subpacket::critical(SubpacketData::SignatureCreationTime(Timestamp::from_secs(
|
||||
start,
|
||||
)))?,
|
||||
Subpacket::critical(SubpacketData::KeyFlags(keyflags))?,
|
||||
// XXX: marking expiration as critical
|
||||
// even though reference implementation does not:
|
||||
// <https://codeberg.org/autocrypt2/autocrypt-v2-cert/issues/53>
|
||||
Subpacket::critical(SubpacketData::KeyExpirationTime(expiration_duration))?,
|
||||
Subpacket::regular(SubpacketData::IssuerFingerprint(
|
||||
tsk.primary_key.public_key().fingerprint(),
|
||||
))?,
|
||||
];
|
||||
let signature = signature_config.sign_subkey_binding(
|
||||
&tsk.primary_key,
|
||||
tsk.primary_key.public_key(),
|
||||
&Password::empty(),
|
||||
new_rotating_subkey.public_key(),
|
||||
)?;
|
||||
SignedSecretSubKey {
|
||||
key: new_rotating_subkey,
|
||||
signatures: vec![signature],
|
||||
}
|
||||
};
|
||||
|
||||
tsk.secret_subkeys.push(new_signed_rotating_subkey);
|
||||
Ok(tsk)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::key;
|
||||
use crate::pgp::DcKey;
|
||||
use crate::test_utils;
|
||||
|
||||
/// Tests creating Autocrypt 2 TSK and detecting it.
|
||||
#[test]
|
||||
fn test_create_autocrypt2_keypair() {
|
||||
let now = Timestamp::now();
|
||||
let keypair = create_autocrypt2_keypair(now).unwrap();
|
||||
assert!(is_autocrypt2_tsk(&keypair));
|
||||
|
||||
// Test that Autocrypt 2 TSK can be serialized and deserialized.
|
||||
let secret_key_bytes = DcKey::to_bytes(&keypair);
|
||||
let signed_secret_key = SignedSecretKey::from_slice(&secret_key_bytes)
|
||||
.expect("Cannot deserialize Autocrypt2 TSK");
|
||||
|
||||
assert!(is_autocrypt2_tsk(&signed_secret_key));
|
||||
}
|
||||
|
||||
/// Tests that the key does not leak creation timestamp.
|
||||
#[test]
|
||||
fn test_tsk_timestamps() {
|
||||
let now = Timestamp::now();
|
||||
let tsk = create_autocrypt2_keypair(now).unwrap();
|
||||
|
||||
// Primary key creation timestamp is zero.
|
||||
assert_eq!(tsk.primary_key.created_at().as_secs(), 0);
|
||||
|
||||
// Primary key direct key signature timestamp is zero.
|
||||
let [ref direct_signature] = tsk.details.direct_signatures[..] else {
|
||||
panic!("Autocrypt 2 TSK must have exactly one direct key signature");
|
||||
};
|
||||
|
||||
// Direct key signature is a real key creation timestamp
|
||||
// and should not be zero.
|
||||
// <https://www.ietf.org/archive/id/draft-autocrypt-openpgp-v2-cert-02.html#section-2.2-2.2.2.1.1>
|
||||
// This timestamp from TSK should not leak into the public key however
|
||||
// as we recreate the signature every time relay list is changed:
|
||||
let created_timestamp = direct_signature.created().unwrap();
|
||||
assert_ne!(created_timestamp.as_secs(), 0);
|
||||
|
||||
let fallback_subkey = tsk
|
||||
.secret_subkeys
|
||||
.first()
|
||||
.expect("Fallback subkey not found");
|
||||
|
||||
// Fallback subkey creation timestamp should be zero.
|
||||
// We will not be able to change this timestamp and it should not leak
|
||||
// the profile creation timestamp.
|
||||
assert_eq!(fallback_subkey.key.created_at().as_secs(), 0);
|
||||
|
||||
// Fallback subkey binding signature timestamp must match
|
||||
// the direct key signature timestamp.
|
||||
// TODO: it should be recreated each time Direct Key Signature is recreated.
|
||||
let [ref fallback_subkey_signature] = fallback_subkey.signatures[..] else {
|
||||
panic!("Fallback subkey does not have exactly one binding signature");
|
||||
};
|
||||
}
|
||||
|
||||
/// Tests that Autocrypt 2 TSK detection is not triggered for existing non-AC2 test keys.
|
||||
#[test]
|
||||
fn test_is_autocrypt2_tsk_no_false_positives() {
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::alice_keypair()));
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::bob_keypair()));
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::charlie_keypair()));
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::dom_keypair()));
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::elena_keypair()));
|
||||
assert!(!is_autocrypt2_tsk(&test_utils::pqc_keypair()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ratchet() {
|
||||
let now = Timestamp::now();
|
||||
let tsk = create_autocrypt2_keypair(now).unwrap();
|
||||
assert!(is_autocrypt2_tsk(&tsk));
|
||||
|
||||
let new_tsk = ratchet(tsk).expect("Ratchet failed");
|
||||
assert!(is_autocrypt2_tsk(&new_tsk));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_autocrypt2_key_selection() {
|
||||
let now = Timestamp::now();
|
||||
let tsk = create_autocrypt2_keypair(now).unwrap();
|
||||
|
||||
let public_key = key::secret_key_to_public_key(
|
||||
tsk.clone(),
|
||||
now.as_secs(),
|
||||
"alice@example.org",
|
||||
"alice@example.org",
|
||||
)
|
||||
.expect("Failed to convert secret key to public key");
|
||||
|
||||
// For Autocrypt 2 certificate rotating key should be selected for encryption.
|
||||
let pk_for_encryption =
|
||||
crate::pgp::select_pk_for_encryption(now.as_secs(), &public_key).unwrap();
|
||||
let [ref pk_for_encryption_signature] = pk_for_encryption.signatures[..] else {
|
||||
panic!("Selected public key has multiple signatures");
|
||||
};
|
||||
let key_flags = pk_for_encryption_signature.key_flags();
|
||||
assert!(key_flags.encrypt_comms());
|
||||
assert!(!key_flags.encrypt_storage());
|
||||
}
|
||||
}
|
||||
@@ -3456,6 +3456,8 @@ async fn test_prefer_encrypt_mutual_if_encrypted() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reception of encrypted and signed message with forged From header
|
||||
/// when the signature cannot be checked because the public key is not available.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from_and_no_valid_signatures() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4718,6 +4720,13 @@ async fn test_no_op_member_added_is_trash() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests reception of a message with a valid signature and forged From header.
|
||||
///
|
||||
/// The message is accepted because the sender contact is associated with the key
|
||||
/// rather than the address.
|
||||
///
|
||||
/// If `enforce_outer_from_key_alignment` is set, the message is trashed instead
|
||||
/// because the outer From is not a relay address of the sender's key.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
@@ -4732,8 +4741,108 @@ async fn test_forged_from() -> Result<()> {
|
||||
.payload
|
||||
.replace("bob@example.net", "notbob@example.net");
|
||||
|
||||
let msg = alice.recv_msg_opt(&sent_msg).await;
|
||||
assert!(msg.is_none());
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.text, "hi!");
|
||||
assert!(msg.get_showpadlock());
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert!(contact.is_key_contact());
|
||||
|
||||
// We take the address from the encrypted part
|
||||
// and send replies there.
|
||||
assert_eq!(contact.get_addr(), "bob@example.net");
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::EnforceOuterFromKeyAlignment, true)
|
||||
.await?;
|
||||
chat::send_text_msg(&bob, bob_chat_id, "hi again!".to_string()).await?;
|
||||
let mut sent_msg = bob.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg
|
||||
.payload
|
||||
.replace("bob@example.net", "notbob@example.net");
|
||||
assert!(alice.recv_msg_opt(&sent_msg).await.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a forged From header in the encrypted part
|
||||
/// does not change the address of the sender contact
|
||||
/// if `enforce_outer_from_key_alignment` is set.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_forged_inner_from() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::EnforceOuterFromKeyAlignment, true)
|
||||
.await?;
|
||||
let bob_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
|
||||
|
||||
// Bob claims the victim address in the encrypted part,
|
||||
// while his relay keeps the outer From at his own address.
|
||||
tcm.change_addr(&bob, "victim@example.org").await;
|
||||
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
|
||||
let mut sent_msg = bob.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg
|
||||
.payload
|
||||
.replace("victim@example.org", "bob@example.net");
|
||||
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert_eq!(contact.get_addr(), "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a sender key without relay addresses is accepted
|
||||
/// as long as the outer From matches the one in the encrypted part.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outer_from_key_without_relays() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::EnforceOuterFromKeyAlignment, true)
|
||||
.await?;
|
||||
bob.sql
|
||||
.execute("UPDATE transports SET is_published=0", ())
|
||||
.await?;
|
||||
bob.self_public_key.lock().await.take();
|
||||
|
||||
let msg = tcm.send_recv(&bob, &alice, "hi!").await;
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert_eq!(contact.get_addr(), "bob@example.net");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that a relay address added to the key after the recipient learned it
|
||||
/// is accepted as outer From, because the updated key is imported
|
||||
/// from the Autocrypt header of the encrypted part first.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_outer_from_updated_key() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
alice
|
||||
.set_config_bool(Config::EnforceOuterFromKeyAlignment, true)
|
||||
.await?;
|
||||
tcm.send_recv_accept(&bob, &alice, "hello").await;
|
||||
let bob_chat_id = bob.create_chat(&alice).await.id;
|
||||
|
||||
tcm.add_transport(&bob, "bob@relay-new.example").await;
|
||||
chat::send_text_msg(&bob, bob_chat_id, "hi!".to_string()).await?;
|
||||
let mut sent_msg = bob.pop_sent_msg().await;
|
||||
sent_msg.payload = sent_msg
|
||||
.payload
|
||||
.replace("bob@example.net", "bob@relay-new.example");
|
||||
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&alice, msg.from_id).await?;
|
||||
assert_eq!(contact.get_addr(), "bob@relay-new.example");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -213,13 +213,14 @@ impl TestContextManager {
|
||||
to.recv_msg(&sent).await
|
||||
}
|
||||
|
||||
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
|
||||
/// Adds a transport for `new_addr` without changing the primary self address.
|
||||
pub async fn add_transport(&self, test_context: &TestContext, new_addr: &str) {
|
||||
self.section(&format!(
|
||||
"{} changes her self address and reconfigures",
|
||||
test_context.name()
|
||||
"{} adds a transport for {}",
|
||||
test_context.name(),
|
||||
new_addr
|
||||
));
|
||||
|
||||
// Insert a transport for the new address.
|
||||
test_context.sql
|
||||
.execute(
|
||||
"INSERT OR IGNORE INTO transports (addr, entered_param, configured_param) VALUES (?, ?, ?)",
|
||||
@@ -230,6 +231,18 @@ impl TestContextManager {
|
||||
),
|
||||
).await.unwrap();
|
||||
|
||||
// Drop self key from cache so regen will list `new_addr` as a relay address.
|
||||
test_context.self_public_key.lock().await.take();
|
||||
}
|
||||
|
||||
pub async fn change_addr(&self, test_context: &TestContext, new_addr: &str) {
|
||||
self.section(&format!(
|
||||
"{} changes her self address and reconfigures",
|
||||
test_context.name()
|
||||
));
|
||||
|
||||
self.add_transport(test_context, new_addr).await;
|
||||
|
||||
test_context.set_primary_self_addr(new_addr).await.unwrap();
|
||||
// ensure_secret_key_exists() is called during configure
|
||||
key::ensure_secret_key_exists(test_context).await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user