From 0bb4c3d073e51f48d93ac041f72c313f4624bfc8 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 | 11 +++-- src/mimeparser.rs | 2 +- src/pgp.rs | 37 +++++++++++++++++ src/test_utils.rs | 18 ++++++++ test-data/key/pqc-secret.asc | 39 ++++++++++++++++++ 8 files changed, 191 insertions(+), 6 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..d9257b8bd 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) } @@ -625,7 +627,10 @@ impl std::str::FromStr for Fingerprint { .filter(|&c| c.is_ascii_hexdigit()) .collect(); let v: Vec = hex::decode(&hex_repr)?; - ensure!(v.len() == 20, "wrong fingerprint length: {hex_repr}"); + ensure!( + v.len() == 20 || v.len() == 32, + "wrong fingerprint length: {hex_repr}" + ); let fp = Fingerprint::new(v); Ok(fp) } 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..8e1d27555 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -847,4 +847,41 @@ 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(()) + } + + /// Tests securejoin with inviter using PQC key. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_securejoin_pqc_inviter() { + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let pqc = &tcm.pqc().await; + + tcm.execute_securejoin(pqc, alice).await; + } + + /// Tests securejoin with joiner using PQC key. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_securejoin_pqc_joiner() { + let mut tcm = TestContextManager::new(); + let pqc = &tcm.pqc().await; + let bob = &tcm.bob().await; + + tcm.execute_securejoin(bob, pqc).await; + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 9e40f49f7..ee428675b 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-----