1use ml_dsa::signature::{Signer, Verifier};
12use ml_dsa::{
13 EncodedSignature, EncodedVerifyingKey, Generate, Keypair, MlDsa65, Signature, SigningKey,
14 VerifyingKey,
15};
16
17pub struct Probe {
19 signing: SigningKey<MlDsa65>,
20}
21
22impl Probe {
23 pub fn spawn() -> Self {
25 Self {
26 signing: SigningKey::<MlDsa65>::generate(),
27 }
28 }
29
30 pub fn verifying_key(&self) -> VerifyingKey<MlDsa65> {
32 self.signing.verifying_key()
33 }
34
35 pub fn pubkey_hex(&self) -> String {
37 hex::encode(self.verifying_key().encode())
38 }
39
40 pub fn id(&self) -> String {
42 probe_id(&self.pubkey_hex())
43 }
44
45 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
52pub 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
58pub 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}