1use commonware_codec::{DecodeExt, Encode};
12use commonware_cryptography::{
13 Signer as _, Verifier as _,
14 ed25519::{PrivateKey, PublicKey, Signature},
15};
16
17pub mod approval;
18pub mod handoff;
19pub mod toolcall;
20
21pub const NAMESPACE: &[u8] = b"polychrome.v1";
23
24#[derive(Clone)]
26pub struct Signer(PrivateKey);
27
28impl Signer {
29 #[must_use]
34 pub fn from_seed(seed: u64) -> Self {
35 Self(PrivateKey::from_seed(seed))
36 }
37
38 #[must_use]
40 pub fn public_key_bytes(&self) -> Vec<u8> {
41 self.0.public_key().encode().to_vec()
42 }
43
44 #[must_use]
47 pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
48 self.0.sign(NAMESPACE, msg).encode().to_vec()
49 }
50}
51
52#[must_use]
56pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
57 let Ok(pk) = PublicKey::decode(public_key) else {
58 return false;
59 };
60 let Ok(sig) = Signature::decode(sig) else {
61 return false;
62 };
63 pk.verify(NAMESPACE, msg, &sig)
64}
65
66#[cfg(test)]
67mod tests {
68 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
69
70 use super::*;
71
72 #[test]
73 fn sign_then_verify_round_trips() {
74 let signer = Signer::from_seed(1);
75 let pk = signer.public_key_bytes();
76 let msg = b"tool-call canonical bytes";
77 let sig = signer.sign(msg);
78 assert!(verify(&pk, msg, &sig));
79 }
80
81 #[test]
82 fn wrong_message_fails() {
83 let signer = Signer::from_seed(1);
84 let pk = signer.public_key_bytes();
85 let sig = signer.sign(b"original");
86 assert!(!verify(&pk, b"tampered", &sig));
87 }
88
89 #[test]
90 fn wrong_key_fails() {
91 let signer = Signer::from_seed(1);
92 let other = Signer::from_seed(2);
93 let msg = b"msg";
94 let sig = signer.sign(msg);
95 assert!(!verify(&other.public_key_bytes(), msg, &sig));
96 }
97
98 #[test]
99 fn garbage_inputs_return_false_not_panic() {
100 assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
101 assert!(!verify(&[], &[], &[]));
102 }
103}