Skip to main content

truthlinked_core/
pq_identity.rs

1//! Post-quantum identity helpers for TruthLinked accounts.
2//!
3//! The module provides ML-DSA signing, ML-KEM key material, deterministic account
4//! derivation, and keyfile helpers shared by node and CLI code. Signing contexts
5//! are domain-separated through constants from `truthlinked_core::constants`.
6
7use fips203::ml_kem_768;
8use fips203::traits::SerDes as KyberSerDes;
9use fips203::traits::{Decaps, Encaps, KeyGen};
10use fips204::ml_dsa_65;
11use fips204::traits::{SerDes, Signer, Verifier};
12use rand_chacha::ChaCha20Rng;
13use rand_core::SeedableRng;
14use serde::{Deserialize, Serialize};
15use std::fs;
16use std::path::Path;
17
18use crate::constants::{GENERIC_SIGN_CONTEXT, TX_SIGN_CONTEXT};
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct PQKeypair {
22    pub secret: Vec<u8>,
23    pub public: Vec<u8>,
24}
25
26#[derive(Clone)]
27pub struct DualKeypair {
28    pub dilithium_pk: ml_dsa_65::PublicKey,
29    pub dilithium_sk: ml_dsa_65::PrivateKey,
30    pub kyber_ek: ml_kem_768::EncapsKey,
31    pub kyber_dk: ml_kem_768::DecapsKey,
32    pub mnemonic: String,
33}
34
35impl PQKeypair {
36    pub fn generate() -> Self {
37        let (pk, sk) =
38            ml_dsa_65::try_keygen_with_rng(&mut rand::thread_rng()).expect("Key generation failed");
39
40        Self {
41            secret: sk.into_bytes().to_vec(),
42            public: pk.into_bytes().to_vec(),
43        }
44    }
45
46    pub fn from_seed(seed: &[u8; 32]) -> Self {
47        let mut rng = ChaCha20Rng::from_seed(*seed);
48        let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut rng).expect("Key generation failed");
49
50        Self {
51            secret: sk.into_bytes().to_vec(),
52            public: pk.into_bytes().to_vec(),
53        }
54    }
55
56    pub fn sign(&self, message: &[u8]) -> Vec<u8> {
57        let sk_bytes: [u8; 4032] = self
58            .secret
59            .as_slice()
60            .try_into()
61            .expect("Invalid secret key length");
62        let sk = ml_dsa_65::PrivateKey::try_from_bytes(sk_bytes).expect("Invalid secret key");
63
64        sk.try_sign(message, GENERIC_SIGN_CONTEXT)
65            .expect("Signing failed")
66            .to_vec()
67    }
68
69    pub fn verify(message: &[u8], signature: &[u8], public_key: &[u8]) -> bool {
70        let pk_bytes: [u8; 1952] = match public_key.try_into() {
71            Ok(b) => b,
72            Err(_) => return false,
73        };
74
75        let sig_bytes: [u8; 3309] = match signature.try_into() {
76            Ok(b) => b,
77            Err(_) => return false,
78        };
79
80        if let Ok(pk) = ml_dsa_65::PublicKey::try_from_bytes(pk_bytes) {
81            pk.verify(message, &sig_bytes, GENERIC_SIGN_CONTEXT)
82        } else {
83            false
84        }
85    }
86
87    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
88        let json = serde_json::to_string_pretty(self)
89            .map_err(|e| format!("Failed to serialize: {}", e))?;
90        fs::write(path, json).map_err(|e| format!("Failed to write file: {}", e))?;
91        Ok(())
92    }
93
94    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
95        let json = fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
96        serde_json::from_str(&json).map_err(|e| format!("Failed to deserialize: {}", e))
97    }
98}
99
100impl DualKeypair {
101    pub fn generate() -> Self {
102        let (pk, sk) =
103            ml_dsa_65::try_keygen_with_rng(&mut rand::thread_rng()).expect("Key generation failed");
104        let (ek, dk) = ml_kem_768::KG::try_keygen().expect("Kyber key generation failed");
105
106        Self {
107            dilithium_pk: pk,
108            dilithium_sk: sk,
109            kyber_ek: ek,
110            kyber_dk: dk,
111            mnemonic: String::from("random-generation"),
112        }
113    }
114
115    pub fn from_mnemonic(mnemonic: String) -> Self {
116        Self::from_mnemonic_with_passphrase(mnemonic, "")
117    }
118
119    pub fn from_mnemonic_with_passphrase(mnemonic: String, passphrase: &str) -> Self {
120        let seed = Self::mnemonic_to_seed(&mnemonic, passphrase);
121
122        let dilithium_seed = Self::derive_child_seed(&seed, b"dilithium");
123        let mut rng = ChaCha20Rng::from_seed(dilithium_seed);
124        let (pk, sk) = ml_dsa_65::try_keygen_with_rng(&mut rng).expect("Key generation failed");
125
126        let kyber_seed = Self::derive_child_seed(&seed, b"kyber");
127        let mut kyber_rng = ChaCha20Rng::from_seed(kyber_seed);
128        let (ek, dk) = ml_kem_768::KG::try_keygen_with_rng(&mut kyber_rng)
129            .expect("Kyber key generation failed");
130
131        Self {
132            dilithium_pk: pk,
133            dilithium_sk: sk,
134            kyber_ek: ek,
135            kyber_dk: dk,
136            mnemonic,
137        }
138    }
139
140    fn mnemonic_to_seed(mnemonic: &str, passphrase: &str) -> [u8; 64] {
141        use pbkdf2::pbkdf2_hmac;
142        use sha2::Sha512;
143
144        let salt = format!("mnemonic{}", passphrase);
145        let mut seed = [0u8; 64];
146        pbkdf2_hmac::<Sha512>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed);
147        seed
148    }
149
150    fn derive_child_seed(master_seed: &[u8], context: &[u8]) -> [u8; 32] {
151        use hkdf::Hkdf;
152        use sha2::Sha256;
153
154        let hk = Hkdf::<Sha256>::new(Some(context), master_seed);
155        let mut okm = [0u8; 32];
156        hk.expand(b"truthlinked-v1", &mut okm)
157            .expect("HKDF expand failed");
158        okm
159    }
160
161    pub fn save_with_password<P: AsRef<Path>>(
162        &self,
163        path: P,
164        password: Option<&str>,
165    ) -> Result<(), String> {
166        let data = serde_json::json!({
167            "mnemonic": self.mnemonic,
168            "dilithium_public": hex::encode(self.dilithium_pk.clone().into_bytes()),
169            "kyber_encaps_key": hex::encode(self.kyber_ek.clone().into_bytes()),
170            "dilithium_secret": hex::encode(self.dilithium_sk.clone().into_bytes()),
171            "kyber_decaps_key": hex::encode(self.kyber_dk.clone().into_bytes()),
172        });
173
174        let json_str = serde_json::to_string_pretty(&data)
175            .map_err(|e| format!("Failed to serialize keypair: {}", e))?;
176
177        let final_data = if let Some(pwd) = password {
178            let encrypted = Self::encrypt_keyfile(&json_str, pwd)?;
179            serde_json::to_string_pretty(&serde_json::json!({
180                "encrypted": true,
181                "version": 1,
182                "data": encrypted,
183            }))
184            .map_err(|e| format!("Failed to wrap encrypted keypair: {}", e))?
185        } else {
186            json_str
187        };
188
189        let path_ref = path.as_ref();
190        fs::write(path_ref, final_data).map_err(|e| e.to_string())?;
191
192        #[cfg(unix)]
193        {
194            use std::os::unix::fs::PermissionsExt;
195            let mut perms = fs::metadata(path_ref)
196                .map_err(|e| e.to_string())?
197                .permissions();
198            perms.set_mode(0o600);
199            fs::set_permissions(path_ref, perms).map_err(|e| e.to_string())?;
200        }
201
202        Ok(())
203    }
204
205    fn encrypt_keyfile(plaintext: &str, password: &str) -> Result<String, String> {
206        use aes_gcm::aead::Aead;
207        use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
208        use rand::RngCore;
209
210        let mut salt = [0u8; 32];
211        rand::thread_rng().fill_bytes(&mut salt);
212
213        let mut key = [0u8; 32];
214        argon2::Argon2::default()
215            .hash_password_into(password.as_bytes(), &salt, &mut key)
216            .map_err(|e| format!("Argon2 failed: {}", e))?;
217
218        let cipher =
219            Aes256Gcm::new_from_slice(&key).map_err(|e| format!("Cipher init failed: {}", e))?;
220
221        let mut nonce_bytes = [0u8; 12];
222        rand::thread_rng().fill_bytes(&mut nonce_bytes);
223        let nonce = Nonce::from_slice(&nonce_bytes);
224
225        let ciphertext = cipher
226            .encrypt(nonce, plaintext.as_bytes())
227            .map_err(|e| format!("Encrypt failed: {}", e))?;
228
229        let payload = serde_json::json!({
230            "salt": hex::encode(salt),
231            "nonce": hex::encode(nonce_bytes),
232            "ciphertext": hex::encode(ciphertext),
233        });
234
235        Ok(payload.to_string())
236    }
237
238    fn decrypt_keyfile(payload: &str, password: &str) -> Result<String, String> {
239        use aes_gcm::aead::Aead;
240        use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
241
242        let obj: serde_json::Value = serde_json::from_str(payload)
243            .map_err(|e| format!("Invalid encrypted keyfile: {}", e))?;
244
245        let salt_hex = obj
246            .get("salt")
247            .and_then(|v| v.as_str())
248            .ok_or("Missing salt")?;
249        let nonce_hex = obj
250            .get("nonce")
251            .and_then(|v| v.as_str())
252            .ok_or("Missing nonce")?;
253        let ct_hex = obj
254            .get("ciphertext")
255            .and_then(|v| v.as_str())
256            .ok_or("Missing ciphertext")?;
257
258        let salt = hex::decode(salt_hex).map_err(|e| format!("Invalid salt: {}", e))?;
259        let nonce = hex::decode(nonce_hex).map_err(|e| format!("Invalid nonce: {}", e))?;
260        let ciphertext = hex::decode(ct_hex).map_err(|e| format!("Invalid ciphertext: {}", e))?;
261
262        let mut key = [0u8; 32];
263        argon2::Argon2::default()
264            .hash_password_into(password.as_bytes(), &salt, &mut key)
265            .map_err(|e| format!("Argon2 failed: {}", e))?;
266
267        let cipher =
268            Aes256Gcm::new_from_slice(&key).map_err(|e| format!("Cipher init failed: {}", e))?;
269
270        let nonce = Nonce::from_slice(&nonce);
271        let plaintext = cipher
272            .decrypt(nonce, ciphertext.as_ref())
273            .map_err(|e| format!("Decrypt failed: {}", e))?;
274
275        String::from_utf8(plaintext).map_err(|e| format!("Invalid UTF-8: {}", e))
276    }
277
278    pub fn load_with_password<P: AsRef<Path>>(
279        path: P,
280        password: Option<&str>,
281    ) -> Result<Self, String> {
282        let path_ref = path.as_ref();
283        let json =
284            fs::read_to_string(path_ref).map_err(|e| format!("Failed to read file: {}", e))?;
285
286        let wrapper: serde_json::Value =
287            serde_json::from_str(&json).map_err(|e| format!("Failed to parse keypair: {}", e))?;
288
289        let is_encrypted = wrapper
290            .get("encrypted")
291            .and_then(|v| v.as_bool())
292            .unwrap_or(false);
293
294        let data_json = if is_encrypted {
295            let pwd = password.ok_or("Password required")?;
296            let payload = wrapper
297                .get("data")
298                .and_then(|v| v.as_str())
299                .ok_or("Missing data")?;
300            Self::decrypt_keyfile(payload, pwd)?
301        } else {
302            json
303        };
304
305        let obj: serde_json::Value = serde_json::from_str(&data_json)
306            .map_err(|e| format!("Failed to parse keypair: {}", e))?;
307
308        let mnemonic = obj
309            .get("mnemonic")
310            .and_then(|v| v.as_str())
311            .unwrap_or("")
312            .to_string();
313        let dilithium_public = obj
314            .get("dilithium_public")
315            .and_then(|v| v.as_str())
316            .ok_or("Missing dilithium_public")?;
317        let dilithium_secret = obj
318            .get("dilithium_secret")
319            .and_then(|v| v.as_str())
320            .ok_or("Missing dilithium_secret")?;
321
322        let pk_bytes: [u8; 1952] = hex::decode(dilithium_public)
323            .map_err(|e| e.to_string())?
324            .try_into()
325            .map_err(|_| "Invalid public key length")?;
326        let sk_bytes: [u8; 4032] = hex::decode(dilithium_secret)
327            .map_err(|e| e.to_string())?
328            .try_into()
329            .map_err(|_| "Invalid secret key length")?;
330
331        let dilithium_pk = ml_dsa_65::PublicKey::try_from_bytes(pk_bytes)
332            .map_err(|e| format!("Invalid public key: {:?}", e))?;
333        let dilithium_sk = ml_dsa_65::PrivateKey::try_from_bytes(sk_bytes)
334            .map_err(|e| format!("Invalid secret key: {:?}", e))?;
335
336        let kyber_encaps_key = obj.get("kyber_encaps_key").and_then(|v| v.as_str());
337        let kyber_decaps_key = obj.get("kyber_decaps_key").and_then(|v| v.as_str());
338
339        let (kyber_ek, kyber_dk) = match (kyber_encaps_key, kyber_decaps_key) {
340            (Some(ek_hex), Some(dk_hex)) => {
341                let ek_bytes: [u8; 1184] = hex::decode(ek_hex)
342                    .map_err(|e| e.to_string())?
343                    .try_into()
344                    .map_err(|_| "Invalid encaps key length")?;
345                let dk_bytes: [u8; 2400] = hex::decode(dk_hex)
346                    .map_err(|e| e.to_string())?
347                    .try_into()
348                    .map_err(|_| "Invalid decaps key length")?;
349                let ek = ml_kem_768::EncapsKey::try_from_bytes(ek_bytes)
350                    .map_err(|e| format!("Invalid encaps key: {:?}", e))?;
351                let dk = ml_kem_768::DecapsKey::try_from_bytes(dk_bytes)
352                    .map_err(|e| format!("Invalid decaps key: {:?}", e))?;
353                (ek, dk)
354            }
355            _ => {
356                if mnemonic.is_empty() {
357                    return Err("Missing kyber keys and mnemonic".to_string());
358                }
359                let derived = Self::from_mnemonic(mnemonic.clone());
360                (derived.kyber_ek, derived.kyber_dk)
361            }
362        };
363
364        Ok(Self {
365            dilithium_pk,
366            dilithium_sk,
367            kyber_ek,
368            kyber_dk,
369            mnemonic,
370        })
371    }
372
373    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, String> {
374        let path_ref = path.as_ref();
375        let json =
376            fs::read_to_string(path_ref).map_err(|e| format!("Failed to read file: {}", e))?;
377
378        let wrapper: serde_json::Value =
379            serde_json::from_str(&json).map_err(|e| format!("Failed to parse keypair: {}", e))?;
380
381        let is_encrypted = wrapper
382            .get("encrypted")
383            .and_then(|v| v.as_bool())
384            .unwrap_or(false);
385
386        if is_encrypted {
387            let password = rpassword::prompt_password("Enter password: ")
388                .map_err(|e| format!("Failed to read password: {}", e))?;
389            Self::load_with_password(path_ref, Some(&password))
390        } else {
391            Self::load_with_password(path_ref, None)
392        }
393    }
394
395    pub fn sign_transaction(
396        &self,
397        tx: &crate::pq_execution::Transaction,
398    ) -> Result<crate::pq_execution::Transaction, String> {
399        let mut msg = Vec::new();
400        msg.extend_from_slice(&(tx.genesis_fingerprint.len() as u32).to_le_bytes());
401        msg.extend_from_slice(&tx.genesis_fingerprint);
402        msg.extend_from_slice(&(tx.sender.len() as u32).to_le_bytes());
403        msg.extend_from_slice(&tx.sender);
404        msg.extend_from_slice(&tx.nonce.to_le_bytes());
405        msg.extend_from_slice(&tx.timestamp.to_le_bytes());
406        msg.extend_from_slice(&tx.expiration_height.to_le_bytes());
407
408        let intent_bytes = postcard::to_allocvec(&tx.intent)
409            .map_err(|e| format!("Failed to serialize intent: {}", e))?;
410        msg.extend_from_slice(&(intent_bytes.len() as u32).to_le_bytes());
411        msg.extend_from_slice(&intent_bytes);
412
413        let signature = (&self.dilithium_sk)
414            .try_sign(&msg, TX_SIGN_CONTEXT)
415            .map_err(|e| format!("Signing failed: {:?}", e))?
416            .to_vec();
417
418        Ok(crate::pq_execution::Transaction {
419            sender: tx.sender,
420            intent: tx.intent.clone(),
421            signature,
422            nonce: tx.nonce,
423            timestamp: tx.timestamp,
424            genesis_fingerprint: tx.genesis_fingerprint,
425            expiration_height: tx.expiration_height,
426        })
427    }
428}
429
430pub fn account_id_from_pubkey(pubkey: &[u8]) -> [u8; 32] {
431    use sha2::{Digest, Sha256};
432    let mut hasher = Sha256::new();
433    hasher.update(b"truthlinked-account-id-v1");
434    hasher.update(pubkey);
435    hasher.finalize().into()
436}
437
438fn build_tx_signing_message(
439    genesis_fingerprint: [u8; 32],
440    sender: [u8; 32],
441    timestamp: u64,
442    expiration_height: u64,
443    intent_bytes: &[u8],
444) -> Vec<u8> {
445    let mut msg = Vec::new();
446    msg.extend_from_slice(&(genesis_fingerprint.len() as u32).to_le_bytes());
447    msg.extend_from_slice(&genesis_fingerprint);
448    msg.extend_from_slice(&(sender.len() as u32).to_le_bytes());
449    msg.extend_from_slice(&sender);
450    msg.extend_from_slice(&timestamp.to_le_bytes());
451    msg.extend_from_slice(&expiration_height.to_le_bytes());
452    msg.extend_from_slice(&(intent_bytes.len() as u32).to_le_bytes());
453    msg.extend_from_slice(intent_bytes);
454    msg
455}
456
457pub fn sign_transaction(
458    genesis_fingerprint: [u8; 32],
459    sender: [u8; 32],
460    timestamp: u64,
461    expiration_height: u64,
462    intent: &[u8],
463    dilithium_sk: &ml_dsa_65::PrivateKey,
464) -> Vec<u8> {
465    let msg = build_tx_signing_message(genesis_fingerprint, sender, timestamp, expiration_height, intent);
466
467    dilithium_sk
468        .try_sign(&msg, TX_SIGN_CONTEXT)
469        .expect("Signing failed")
470        .to_vec()
471}
472
473pub fn kyber_encapsulate(encaps_key: &ml_kem_768::EncapsKey) -> ([u8; 1088], [u8; 32]) {
474    let (ssk, ct) = encaps_key.try_encaps().expect("Encapsulation failed");
475    (ct.into_bytes(), ssk.into_bytes())
476}
477
478pub fn kyber_decapsulate(decaps_key: &ml_kem_768::DecapsKey, ciphertext: &[u8; 1088]) -> [u8; 32] {
479    let ct = ml_kem_768::CipherText::try_from_bytes(*ciphertext).expect("Invalid ciphertext");
480    decaps_key
481        .try_decaps(&ct)
482        .expect("Decapsulation failed")
483        .into_bytes()
484}