Skip to main content

blockle_core/
keys.rs

1//! Post-quantum keys and addresses.
2//!
3//! Transparent spends are authorized by **ML-DSA-44** (FIPS 204, the
4//! standardized Dilithium2 — NIST security category 2): 1312-byte public
5//! keys, 2420-byte signatures. Addresses are the 32-byte Blake2b hash of a
6//! public key, encoded as bech32m with HRP `block` (addresses read
7//! `block1…`). Shielded addresses will get their own HRP in the privacy
8//! milestone.
9
10use bech32::{Bech32m, Hrp};
11use fips204::ml_dsa_44;
12use fips204::traits::{SerDes, Signer, Verifier};
13use thiserror::Error;
14
15use crate::hash::address_hash;
16
17pub const ADDRESS_HRP: &str = "block";
18pub const PUBLIC_KEY_LEN: usize = ml_dsa_44::PK_LEN;
19pub const SECRET_KEY_LEN: usize = ml_dsa_44::SK_LEN;
20pub const SIGNATURE_LEN: usize = ml_dsa_44::SIG_LEN;
21
22/// Domain-separation context for transaction signatures.
23const SIG_CONTEXT: &[u8] = b"BlklTxV1";
24
25pub type Address = [u8; 32];
26
27#[derive(Debug, Error)]
28pub enum KeyError {
29    #[error("invalid address: {0}")]
30    InvalidAddress(String),
31    #[error("invalid key material")]
32    InvalidKey,
33    #[error("signature verification failed")]
34    BadSignature,
35}
36
37/// An ML-DSA-44 keypair controlling a transparent address.
38pub struct Keypair {
39    secret: ml_dsa_44::PrivateKey,
40    public_bytes: Vec<u8>,
41}
42
43impl Keypair {
44    pub fn generate() -> Self {
45        let (public, secret) = ml_dsa_44::try_keygen().expect("OS RNG available");
46        let public_bytes = public.into_bytes().to_vec();
47        Keypair { secret, public_bytes }
48    }
49
50    pub fn from_bytes(secret: &[u8], public: &[u8]) -> Result<Self, KeyError> {
51        let sk: [u8; SECRET_KEY_LEN] = secret.try_into().map_err(|_| KeyError::InvalidKey)?;
52        let pk: [u8; PUBLIC_KEY_LEN] = public.try_into().map_err(|_| KeyError::InvalidKey)?;
53        let secret = ml_dsa_44::PrivateKey::try_from_bytes(sk).map_err(|_| KeyError::InvalidKey)?;
54        // Parse to validate, then keep only the canonical bytes.
55        let public = ml_dsa_44::PublicKey::try_from_bytes(pk).map_err(|_| KeyError::InvalidKey)?;
56        let public_bytes = public.into_bytes().to_vec();
57        Ok(Keypair { secret, public_bytes })
58    }
59
60    pub fn secret_bytes(&self) -> Vec<u8> {
61        self.secret.clone().into_bytes().to_vec()
62    }
63
64    pub fn public_bytes(&self) -> Vec<u8> {
65        self.public_bytes.clone()
66    }
67
68    pub fn address(&self) -> Address {
69        address_hash(&self.public_bytes)
70    }
71
72    pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
73        self.secret
74            .try_sign(msg, SIG_CONTEXT)
75            .expect("OS RNG available")
76            .to_vec()
77    }
78}
79
80/// Verify an ML-DSA-44 signature made by `pubkey` over `msg`.
81pub fn verify_signature(pubkey: &[u8], msg: &[u8], signature: &[u8]) -> Result<(), KeyError> {
82    let pk: [u8; PUBLIC_KEY_LEN] = pubkey.try_into().map_err(|_| KeyError::InvalidKey)?;
83    let pk = ml_dsa_44::PublicKey::try_from_bytes(pk).map_err(|_| KeyError::InvalidKey)?;
84    let sig: [u8; SIGNATURE_LEN] = signature.try_into().map_err(|_| KeyError::BadSignature)?;
85    if pk.verify(msg, &sig, SIG_CONTEXT) {
86        Ok(())
87    } else {
88        Err(KeyError::BadSignature)
89    }
90}
91
92/// Derive the address committed to by a public key.
93pub fn pubkey_to_address(pubkey: &[u8]) -> Address {
94    address_hash(pubkey)
95}
96
97pub fn encode_address(addr: &Address) -> String {
98    let hrp = Hrp::parse(ADDRESS_HRP).expect("static hrp");
99    bech32::encode::<Bech32m>(hrp, addr).expect("32 bytes always encodes")
100}
101
102pub fn decode_address(s: &str) -> Result<Address, KeyError> {
103    let err = || KeyError::InvalidAddress(s.to_string());
104    let (hrp, data) = bech32::decode(s).map_err(|_| err())?;
105    if hrp.as_str() != ADDRESS_HRP || data.len() != 32 {
106        return Err(err());
107    }
108    let mut out = [0u8; 32];
109    out.copy_from_slice(&data);
110    Ok(out)
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn address_roundtrip() {
119        let kp = Keypair::generate();
120        let addr = kp.address();
121        let s = encode_address(&addr);
122        assert!(s.starts_with("block1"), "got {s}");
123        assert_eq!(decode_address(&s).unwrap(), addr);
124        assert!(decode_address("block1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqxxxxxx").is_err());
125    }
126
127    #[test]
128    fn sign_verify() {
129        let kp = Keypair::generate();
130        assert_eq!(kp.public_bytes().len(), PUBLIC_KEY_LEN);
131        let sig = kp.sign(b"blockle");
132        assert_eq!(sig.len(), SIGNATURE_LEN);
133        verify_signature(&kp.public_bytes(), b"blockle", &sig).unwrap();
134        assert!(verify_signature(&kp.public_bytes(), b"tampered", &sig).is_err());
135    }
136
137    #[test]
138    fn keypair_restore() {
139        let kp = Keypair::generate();
140        let restored = Keypair::from_bytes(&kp.secret_bytes(), &kp.public_bytes()).unwrap();
141        assert_eq!(kp.address(), restored.address());
142        let sig = restored.sign(b"msg");
143        verify_signature(&kp.public_bytes(), b"msg", &sig).unwrap();
144    }
145}