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 canon;
19pub mod handoff;
20pub mod mandate;
21pub mod toolcall;
22
23pub const NAMESPACE: &[u8] = b"polychrome.v1";
25
26#[derive(Clone)]
28pub struct Signer(PrivateKey);
29
30impl Signer {
31 #[must_use]
36 pub fn from_seed(seed: u64) -> Self {
37 Self(PrivateKey::from_seed(seed))
38 }
39
40 #[must_use]
42 pub fn public_key_bytes(&self) -> Vec<u8> {
43 self.0.public_key().encode().to_vec()
44 }
45
46 #[must_use]
49 pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
50 self.0.sign(NAMESPACE, msg).encode().to_vec()
51 }
52}
53
54#[must_use]
58pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
59 let Ok(pk) = PublicKey::decode(public_key) else {
60 return false;
61 };
62 let Ok(sig) = Signature::decode(sig) else {
63 return false;
64 };
65 pk.verify(NAMESPACE, msg, &sig)
66}
67
68#[cfg(test)]
69mod tests {
70 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
71
72 use super::*;
73
74 #[test]
75 fn sign_then_verify_round_trips() {
76 let signer = Signer::from_seed(1);
77 let pk = signer.public_key_bytes();
78 let msg = b"tool-call canonical bytes";
79 let sig = signer.sign(msg);
80 assert!(verify(&pk, msg, &sig));
81 }
82
83 #[test]
84 fn wrong_message_fails() {
85 let signer = Signer::from_seed(1);
86 let pk = signer.public_key_bytes();
87 let sig = signer.sign(b"original");
88 assert!(!verify(&pk, b"tampered", &sig));
89 }
90
91 #[test]
92 fn wrong_key_fails() {
93 let signer = Signer::from_seed(1);
94 let other = Signer::from_seed(2);
95 let msg = b"msg";
96 let sig = signer.sign(msg);
97 assert!(!verify(&other.public_key_bytes(), msg, &sig));
98 }
99
100 #[test]
101 fn garbage_inputs_return_false_not_panic() {
102 assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
103 assert!(!verify(&[], &[], &[]));
104 }
105}