1use 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
22const 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
37pub 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 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
80pub 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
92pub 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}