From 9908be06dca530da0bbd19ab54f36c5bd8c61f58 Mon Sep 17 00:00:00 2001 From: link2xt Date: Thu, 30 Apr 2026 23:09:10 +0200 Subject: [PATCH] feat: enable draft-pqc feature on pgp crate This is needed to have support of v6 PQC keys by the time users start generating profiles using such keys. Test key was generated with rsop/v0.10.0-16-gd98265f (commit d98265f821e7bb181d06da1d634c5c4668d89e83) using the command cargo run --features draft-pqc generate-key \ --profile draft-ietf-openpgp-pqc-14-v6-ed25519-mlkem768x25519 --- Cargo.lock | 80 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- deny.toml | 8 +++- src/key.rs | 6 ++- src/mimeparser.rs | 2 +- src/pgp.rs | 17 ++++++++ src/test_utils.rs | 18 ++++++++ test-data/key/pqc-secret.asc | 39 ++++++++++++++++++ 8 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 test-data/key/pqc-secret.asc diff --git a/Cargo.lock b/Cargo.lock index f754c4f6a..d118c482e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2608,6 +2608,25 @@ dependencies = [ "libm", ] +[[package]] +name = "hybrid-array" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d35805454dc9f8662a98d6d61886ffe26bd465f5960e0e55345c70d5c0d2a9" +dependencies = [ + "typenum", +] + +[[package]] +name = "hybrid-array" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d15931895091dea5c47afa5b3c9a01ba634b311919fd4d41388fa0e3d76af" +dependencies = [ + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.9.0" @@ -3257,6 +3276,16 @@ dependencies = [ "cpufeatures 0.2.17", ] +[[package]] +name = "kem" +version = "0.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8645470337db67b01a7f966decf7d0bafedbae74147d33e641c67a91df239f" +dependencies = [ + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3470,6 +3499,35 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "ml-dsa" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac4a46643af2001eafebcc37031fc459eb72d45057aac5d7a15b00046a2ad6db" +dependencies = [ + "const-oid", + "hybrid-array 0.3.1", + "num-traits", + "pkcs8", + "rand_core 0.6.4", + "sha3", + "signature", + "zeroize", +] + +[[package]] +name = "ml-kem" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de49b3df74c35498c0232031bb7e85f9389f913e2796169c8ab47a53993a18f" +dependencies = [ + "hybrid-array 0.2.3", + "kem", + "rand_core 0.6.4", + "sha3", + "zeroize", +] + [[package]] name = "moka" version = "0.12.10" @@ -4205,6 +4263,8 @@ dependencies = [ "k256", "log", "md-5", + "ml-dsa", + "ml-kem", "nom 8.0.0", "num-bigint-dig", "num-traits", @@ -4223,6 +4283,7 @@ dependencies = [ "sha2", "sha3", "signature", + "slh-dsa", "smallvec", "snafu", "twofish", @@ -5687,6 +5748,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slh-dsa" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2f20f4049197e03db1104a6452f4d9e96665d79f880198dce4a7026ba5f267" +dependencies = [ + "const-oid", + "digest", + "hmac", + "hybrid-array 0.3.1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "sha3", + "signature", + "typenum", + "zerocopy", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 79948032c..559c60adb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ num-derive = "0.4" num-traits = { workspace = true } parking_lot = "0.12.4" percent-encoding = "2.3" -pgp = { version = "0.19.0", default-features = false } +pgp = { version = "0.19.0", features = ["draft-pqc"], default-features = false } pin-project = "1" qrcodegen = "1.7.0" quick-xml = { version = "0.39", features = ["escape-html"] } diff --git a/deny.toml b/deny.toml index 2371a3d05..189043103 100644 --- a/deny.toml +++ b/deny.toml @@ -43,7 +43,12 @@ ignore = [ # hickory-proto 0.25.2 quadratic complexity issue. # Dependency of iroh 0.35.0, cannot be updated as of 2026-05-02. # - "RUSTSEC-2026-0119" + "RUSTSEC-2026-0119", + + # Timing side channel in ml-dsa dependency of rPGP. + # We enable PQC for encryption rather than signatures. + # + "RUSTSEC-2025-0144", ] [bans] @@ -62,6 +67,7 @@ skip = [ { name = "getrandom", version = "0.2.12" }, { name = "heck", version = "0.4.1" }, { name = "http", version = "0.2.12" }, + { name = "hybrid-array", version = "0.2.3" }, { name = "linux-raw-sys", version = "0.4.14" }, { name = "lru", version = "0.12.5" }, { name = "netlink-packet-route", version = "0.17.1" }, diff --git a/src/key.rs b/src/key.rs index c16b847d2..133819c93 100644 --- a/src/key.rs +++ b/src/key.rs @@ -570,9 +570,11 @@ pub async fn preconfigure_keypair(context: &Context, secret_data: &str) -> Resul pub struct Fingerprint(Vec); impl Fingerprint { - /// Creates new 160-bit (20 bytes) fingerprint. + /// Creates new fingerprint. + /// + /// It is 160-bit (20 bytes) for v4 keys and 32 bytes for v6 keys. pub fn new(v: Vec) -> Fingerprint { - debug_assert_eq!(v.len(), 20); + debug_assert!(v.len() == 20 || v.len() == 32); Fingerprint(v) } diff --git a/src/mimeparser.rs b/src/mimeparser.rs index b5107b295..80272bc15 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -356,7 +356,7 @@ impl MimeMessage { let decrypted_msg; // Decrypted signed OpenPGP message. let expected_sender_fingerprint: Option; - let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await { + let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await { Ok(Some((mut msg, expected_sender_fp))) => { mail_raw = msg.as_data_vec().unwrap_or_default(); diff --git a/src/pgp.rs b/src/pgp.rs index 1417369aa..75b975a99 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -847,4 +847,21 @@ mod tests { assert!(merge_openpgp_certificates(alice.clone(), bob.clone()).is_err()); assert!(merge_openpgp_certificates(bob.clone(), alice.clone()).is_err()); } + + /// Test PQC support. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_pqc() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let pqc = &tcm.pqc().await; + + let pqc_received_message = tcm.send_recv_accept(alice, pqc, "Hi!").await; + let pqc_chat_id = pqc_received_message.chat_id; + let pqc_sent = pqc.send_text(pqc_chat_id, "Hello back!").await; + + let alice_rcvd = alice.recv_msg(&pqc_sent).await; + assert_eq!(alice_rcvd.text, "Hello back!"); + + Ok(()) + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index e545d1b8b..47f023545 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -137,6 +137,17 @@ impl TestContextManager { .await } + /// Returns a new "device" with a preconfigured v6 PQC key. + pub async fn pqc(&mut self) -> TestContext { + TestContext::builder() + .with_key_pair(pqc_keypair()) + .with_address("pqc@example.org".to_string()) + .with_id_offset(7000) + .with_log_sink(self.log_sink.clone()) + .build(Some(&mut self.used_names)) + .await + } + /// Creates a new unconfigured test account. pub async fn unconfigured(&mut self) -> TestContext { TestContext::builder() @@ -1426,6 +1437,13 @@ pub fn fiona_keypair() -> SignedSecretKey { key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")).unwrap() } +/// Loads a pre-generated v6 PQC keypair from disk. +/// +/// Like [alice_keypair] but a different key and identity. +pub fn pqc_keypair() -> SignedSecretKey { + key::SignedSecretKey::from_asc(include_str!("../test-data/key/pqc-secret.asc")).unwrap() +} + /// Utility to help wait for and retrieve events. /// /// This buffers the events in order they are emitted. This allows consuming events in diff --git a/test-data/key/pqc-secret.asc b/test-data/key/pqc-secret.asc new file mode 100644 index 000000000..91c470fb0 --- /dev/null +++ b/test-data/key/pqc-secret.asc @@ -0,0 +1,39 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +-----END PGP PRIVATE KEY BLOCK-----