mirror of
https://github.com/chatmail/core.git
synced 2026-07-10 21:13:03 +03:00
WIP: feat: Autocrypt2
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
17
src/key.rs
17
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<SignedSecretKey> {
|
||||
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!(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
588
src/pgp/autocrypt2.rs
Normal file
588
src/pgp/autocrypt2.rs
Normal file
@@ -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.
|
||||
///
|
||||
/// <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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user