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-----