hashiverse_lib/tools/keys_post_quantum.rs
1//! # Post-Quantum Cryptography resilience
2//!
3//! ## The problem
4//!
5//! Hashiverse identities are derived from public keys:
6//! `ID = Blake3[ ed25519_pub || Blake3[Falcon_pub][0..16] || Blake3[Dilithium_pub][0..16] ]`
7//!
8//! Because the identity *is* the key, there is no upgrade path that preserves identity —
9//! switching algorithms would require replacing every server and client ID on the network.
10//! Post-quantum support must therefore be baked in from the start, not bolted on later.
11//!
12//! ## Current design
13//!
14//! Rather than signing everything with PQ algorithms today (which carry large key and signature
15//! sizes and lack `window.crypto.subtle` browser support), each identity commits to two PQ
16//! public keys via their 16-byte Blake3 hashes:
17//!
18//! - **FN-DSA / Falcon (FIPS 205)** — smaller signatures than Dilithium; preferred first-step
19//! upgrade when Ed25519 is threatened.
20//! - **ML-DSA / Dilithium (FIPS 204)** — fallback if Falcon is later compromised. Already
21//! under consideration for `window.crypto.subtle`, making it the most likely candidate for
22//! full browser-native signing support in future.
23//!
24//! The commitments are shipped with every message as part of the identity header; the full PQ
25//! public keys are not shipped today and private keys remain unused until quantum-day.
26//!
27//! ## Upgrade path
28//!
29//! - **Ed25519 compromised** → network upgrades to Falcon. The full Falcon public key begins
30//! shipping with every message and is used to verify Falcon signatures. Receivers confirm
31//! `Blake3[Falcon_pub][0..16] == commitment_falcon` embedded in the sender's identity.
32//! - **Falcon compromised** → network similarly upgrades to Dilithium.
33//! - **Dilithium threatened** → a migration to future PQ algorithms would be needed, likely
34//! requiring a "please follow me on my new identity" protocol. The same protocol would also
35//! serve other identity migration use cases (e.g. a compromised key).
36//!
37//! ## Why not SPHINCS+ (FIPS 205)?
38//!
39//! SPHINCS+ has the strongest security assumptions of the standardised PQ algorithms, but its
40//! signatures are multiple kilobytes — infeasible for a high-throughput messaging network.
41//! A 16-byte commitment to a SPHINCS+ key was considered as an ultimate fallback, but the
42//! cost of including it in every message header outweighs the benefit of covering an already
43//! unlikely scenario (both Falcon and Dilithium broken simultaneously).
44
45use crate::tools::types::PQCommitmentBytes;
46use falcon_rust::falcon512;
47use ml_dsa::{KeyGen, MlDsa44};
48use ml_dsa::signature::Keypair;
49
50pub fn pq_commitment_bytes_from_seed(seed: &[u8; 32]) -> anyhow::Result<PQCommitmentBytes> {
51 // The Falcon PQ public key commitment
52 let pq_commitment_falcon: [u8; 16] = {
53 let falcon_seed: [u8; 32] = blake3::derive_key("hashiverse-pk-falcon", seed);
54 let (_, verifying_key) = falcon512::keygen(falcon_seed);
55 let verifying_key_bytes = verifying_key.to_bytes();
56
57 let mut pq_commitment_falcon = [0u8; 16];
58 let hash = blake3::hash(&verifying_key_bytes);
59 pq_commitment_falcon.copy_from_slice(&hash.as_bytes()[..16]);
60
61 pq_commitment_falcon
62 };
63
64 // The Dilithium public key commitment
65 let pq_commitment_dilithium: [u8; 16] = {
66 let mut dilithium_seed = [0u8; 32];
67 dilithium_seed.copy_from_slice(&blake3::derive_key("hashiverse-pk-dilithium", seed));
68
69 // trace!("Generating dilithium key from seed");
70 let key_pair = MlDsa44::from_seed(&dilithium_seed.into());
71 let verifying_key = key_pair.verifying_key();
72 let verifying_key_bytes = verifying_key.encode();
73
74 // *** TODO: This will only stabilise at v1c.0 of crate ml-dsa - until then the generated keys may change!!
75
76 let mut pq_commitment_dilithium = [0u8; 16];
77 let hash = blake3::hash(&verifying_key_bytes);
78 pq_commitment_dilithium.copy_from_slice(&hash.as_bytes()[..16]);
79
80 pq_commitment_dilithium
81 };
82
83 // Combine the PQ commitments into a single 32-byte array as we are constantly going to be hashing this and doing nothing else until quantum-day.
84 let pq_commitment_bytes = PQCommitmentBytes::from(pq_commitment_falcon, pq_commitment_dilithium);
85 Ok(pq_commitment_bytes)
86}