diff --git a/Cargo.lock b/Cargo.lock index e650c8ca9a..12cb85acac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1378,6 +1378,7 @@ dependencies = [ "futures", "futures-lite", "hex", + "hkdf", "http-body-util", "humansize", "hyper", @@ -1710,7 +1711,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -2733,7 +2734,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2 0.5.9", + "socket2 0.6.3", "tokio", "tower-service", "tracing", @@ -3880,7 +3881,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.1", ] [[package]] @@ -5281,7 +5282,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] @@ -6147,7 +6148,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.1", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ed2050334a..3df37458f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ format-flowed = { path = "./format-flowed" } ratelimit = { path = "./deltachat-ratelimit" } anyhow = { workspace = true } +astral-tokio-tar = { version = "0.6.2", default-features = false } async-broadcast = "0.7.2" async-channel = { workspace = true } async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] } @@ -61,6 +62,7 @@ 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" @@ -103,7 +105,6 @@ thiserror = { workspace = true } tokio-io-timeout = "1.2.1" tokio-rustls = { version = "0.26.2", default-features = false, features = ["aws-lc-rs", "tls12"] } tokio-stream = { version = "0.1.17", features = ["fs"] } -astral-tokio-tar = { version = "0.6.2", default-features = false } tokio-util = { workspace = true } tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] } toml = "0.9" diff --git a/src/config.rs b/src/config.rs index ac2fc84408..5182d6da3e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -477,6 +477,10 @@ pub enum Config { /// and incoming unencrypted messages are not fetched and not processed. #[strum(props(default = "1"))] ForceEncryption, + + /// Generate Autocrypt 2 instead of Autocrypt 1 key. + #[strum(props(default = "1"))] + Autocrypt2, } impl Config { diff --git a/src/context.rs b/src/context.rs index 046be81910..95d3af7d51 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1033,6 +1033,10 @@ impl Context { .await? .to_string(), ); + res.insert( + "autocrypt2", + self.get_config_bool(Config::Autocrypt2).await?.to_string(), + ); let elapsed = time_elapsed(&self.creation_time); res.insert("uptime", duration_to_str(elapsed)); diff --git a/src/key.rs b/src/key.rs index cd76678049..8eb214c15c 100644 --- a/src/key.rs +++ b/src/key.rs @@ -17,10 +17,12 @@ 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; @@ -147,7 +149,7 @@ pub(crate) fn secret_key_to_public_key( }; Ok(vec![ - Subpacket::regular(SubpacketData::SignatureCreationTime(timestamp))?, + Subpacket::critical(SubpacketData::SignatureCreationTime(timestamp))?, Subpacket::regular(SubpacketData::IssuerFingerprint( signed_secret_key.fingerprint(), ))?, @@ -461,9 +463,16 @@ async fn generate_keypair(context: &Context) -> Result { None => { let start = tools::Time::now(); info!(context, "Generating keypair."); - let keypair = Handle::current() - .spawn_blocking(move || crate::pgp::create_keypair(addr)) - .await??; + let keypair = if context.get_config_bool(Config::Autocrypt2).await? { + let now = PgpTimestamp::now(); + Handle::current() + .spawn_blocking(move || crate::pgp::autocrypt2::create_autocrypt2_keypair(now)) + .await?? + } else { + Handle::current() + .spawn_blocking(move || crate::pgp::create_keypair(addr)) + .await?? + }; store_self_keypair(context, &keypair).await?; info!( diff --git a/src/pgp.rs b/src/pgp.rs index 32178a0a77..1fc9d9d292 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -27,6 +27,8 @@ use tokio::runtime::Handle; use crate::key::{DcKey, Fingerprint}; +pub(crate) mod autocrypt2; + /// Preferred symmetric encryption algorithm. const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128; diff --git a/src/pgp/autocrypt2.rs b/src/pgp/autocrypt2.rs new file mode 100644 index 0000000000..9893fb6489 --- /dev/null +++ b/src/pgp/autocrypt2.rs @@ -0,0 +1,588 @@ +//! 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. +/// +/// +pub(crate) fn create_autocrypt2_keypair(now: Timestamp) -> Result { + 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 + // + 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: + // + 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. +/// +/// +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 + // + 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: + // + // No key expiration () + // and no signature expiration (). + // + // 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: + // + 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 + m[0] &= 248; + m[31] &= 127; + m[31] |= 64; +} + +/// Generates new rotating subkey from a previous one. +/// +/// +fn ratchet(mut tsk: SignedSecretKey) -> Result { + // Extract the last rotating subkey. + // Other rotating subkeys do not matter. + // This corresponds to + // + 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")?; + + // + 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::::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: + // + 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. + // + // 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()); + } +}