Skip to main content

entropa_core/
pqc.rs

1//! Post-quantum identity + signatures — ML-DSA (NIST FIPS 204).
2//!
3//! Every actor on Entropa is a **Probe**: an autonomous agent that proposes and
4//! validates blocks. A Probe's identity is an ML-DSA-65 keypair. Its public
5//! *fingerprint* is a blake3 hash of the verifying key, rendered as `PROBE-XXXXXXXX`
6//! — short enough to read on the star-map, unforgeable because it's bound to the key.
7//!
8//! Signing and verifying use post-quantum lattice cryptography, so the chain's
9//! authenticity survives a future quantum adversary — the whole point of Entropa.
10
11use ml_dsa::signature::{Signer, Verifier};
12use ml_dsa::{
13    EncodedSignature, EncodedVerifyingKey, Generate, Keypair, MlDsa65, Signature, SigningKey,
14    VerifyingKey,
15};
16
17/// A Probe — a post-quantum identity that can sign blocks and transactions.
18pub struct Probe {
19    signing: SigningKey<MlDsa65>,
20}
21
22impl Probe {
23    /// Spawn a fresh Probe with a new ML-DSA-65 keypair.
24    pub fn spawn() -> Self {
25        Self {
26            signing: SigningKey::<MlDsa65>::generate(),
27        }
28    }
29
30    /// The Probe's verifying (public) key.
31    pub fn verifying_key(&self) -> VerifyingKey<MlDsa65> {
32        self.signing.verifying_key()
33    }
34
35    /// Hex-encoded ML-DSA verifying key — the Probe's on-chain public identity.
36    pub fn pubkey_hex(&self) -> String {
37        hex::encode(self.verifying_key().encode())
38    }
39
40    /// Human-readable fingerprint, e.g. `PROBE-1A2B3C4D`.
41    pub fn id(&self) -> String {
42        probe_id(&self.pubkey_hex())
43    }
44
45    /// Post-quantum sign `msg`, returning a hex signature.
46    pub fn sign_hex(&self, msg: &[u8]) -> String {
47        let sig: Signature<MlDsa65> = self.signing.sign(msg);
48        hex::encode(sig.encode())
49    }
50}
51
52/// Derive a Probe's short fingerprint from its hex public key.
53pub fn probe_id(pubkey_hex: &str) -> String {
54    let digest = blake3::hash(pubkey_hex.as_bytes());
55    format!("PROBE-{}", digest.to_hex()[..8].to_uppercase())
56}
57
58/// Verify a hex ML-DSA signature over `msg` against a hex verifying key.
59/// Returns `false` on any decode failure or signature mismatch — never panics.
60pub fn verify_hex(pubkey_hex: &str, msg: &[u8], sig_hex: &str) -> bool {
61    let Ok(pk_bytes) = hex::decode(pubkey_hex) else {
62        return false;
63    };
64    let Ok(sig_bytes) = hex::decode(sig_hex) else {
65        return false;
66    };
67    let Ok(enc_vk) = EncodedVerifyingKey::<MlDsa65>::try_from(pk_bytes.as_slice()) else {
68        return false;
69    };
70    let Ok(enc_sig) = EncodedSignature::<MlDsa65>::try_from(sig_bytes.as_slice()) else {
71        return false;
72    };
73    let vk = VerifyingKey::<MlDsa65>::decode(&enc_vk);
74    let Some(sig) = Signature::<MlDsa65>::decode(&enc_sig) else {
75        return false;
76    };
77    vk.verify(msg, &sig).is_ok()
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn sign_and_verify_round_trip() {
86        let probe = Probe::spawn();
87        let msg = b"finalize block 42";
88        let sig = probe.sign_hex(msg);
89        assert!(verify_hex(&probe.pubkey_hex(), msg, &sig));
90    }
91
92    #[test]
93    fn rejects_tampered_message() {
94        let probe = Probe::spawn();
95        let sig = probe.sign_hex(b"pay 10 to alice");
96        assert!(!verify_hex(&probe.pubkey_hex(), b"pay 99 to alice", &sig));
97    }
98
99    #[test]
100    fn rejects_wrong_key() {
101        let a = Probe::spawn();
102        let b = Probe::spawn();
103        let sig = a.sign_hex(b"hello");
104        assert!(!verify_hex(&b.pubkey_hex(), b"hello", &sig));
105    }
106
107    #[test]
108    fn id_is_stable_and_prefixed() {
109        let probe = Probe::spawn();
110        assert_eq!(probe.id(), probe.id());
111        assert!(probe.id().starts_with("PROBE-"));
112        assert_eq!(probe.id().len(), "PROBE-".len() + 8);
113    }
114}