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 approval_assertion;
19pub mod canon;
20pub mod edge_identity;
21pub mod grant;
22pub mod handoff;
23pub mod hex;
24pub mod mandate;
25pub mod question;
26pub mod routine_observation;
27mod salted_hash;
28pub mod sensitive;
29pub mod session;
30pub mod session_grant;
31mod signed;
32pub mod signing_role;
33pub mod subagent;
34pub mod tls;
35pub mod toolcall;
36
37pub use salted_hash::hash_salted_secret;
38
39pub const NAMESPACE: &[u8] = b"polychrome.v1";
41
42#[must_use]
58pub fn random_secret_bytes() -> [u8; 32] {
59 use rand::RngCore as _;
60 let mut bytes = [0u8; 32];
61 rand::rngs::OsRng
62 .try_fill_bytes(&mut bytes)
63 .expect("the OS entropy source must be available to mint secrets");
64 bytes
65}
66
67#[derive(Debug, thiserror::Error)]
71pub enum SignerError {
72 #[error("invalid ed25519 private key material: {0}")]
75 InvalidKeyMaterial(#[from] commonware_codec::Error),
76}
77
78#[derive(Clone)]
80pub struct Signer(PrivateKey);
81
82impl Signer {
83 #[must_use]
88 pub fn from_seed(seed: u64) -> Self {
89 Self(PrivateKey::from_seed(seed))
90 }
91
92 pub fn from_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
102 Ok(Self(PrivateKey::decode(bytes)?))
103 }
104
105 #[must_use]
107 pub fn public_key_bytes(&self) -> Vec<u8> {
108 self.0.public_key().encode().to_vec()
109 }
110
111 #[must_use]
114 pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
115 self.0.sign(NAMESPACE, msg).encode().to_vec()
116 }
117}
118
119#[must_use]
123pub fn verify(public_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
124 let Ok(pk) = PublicKey::decode(public_key) else {
125 return false;
126 };
127 let Ok(sig) = Signature::decode(sig) else {
128 return false;
129 };
130 pk.verify(NAMESPACE, msg, &sig)
131}
132
133#[cfg(test)]
134mod tests {
135 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
136
137 use super::*;
138
139 #[test]
140 fn sign_then_verify_round_trips() {
141 let signer = Signer::from_seed(1);
142 let pk = signer.public_key_bytes();
143 let msg = b"tool-call canonical bytes";
144 let sig = signer.sign(msg);
145 assert!(verify(&pk, msg, &sig));
146 }
147
148 #[test]
149 fn wrong_message_fails() {
150 let signer = Signer::from_seed(1);
151 let pk = signer.public_key_bytes();
152 let sig = signer.sign(b"original");
153 assert!(!verify(&pk, b"tampered", &sig));
154 }
155
156 #[test]
157 fn wrong_key_fails() {
158 let signer = Signer::from_seed(1);
159 let other = Signer::from_seed(2);
160 let msg = b"msg";
161 let sig = signer.sign(msg);
162 assert!(!verify(&other.public_key_bytes(), msg, &sig));
163 }
164
165 #[test]
166 fn garbage_inputs_return_false_not_panic() {
167 assert!(!verify(b"not-a-key", b"m", b"not-a-sig"));
168 assert!(!verify(&[], &[], &[]));
169 }
170
171 #[test]
176 fn from_key_bytes_round_trips_and_rejects_bad_length() {
177 let key_bytes = [7u8; 32];
178 let signer = Signer::from_key_bytes(&key_bytes).expect("valid 32-byte key material");
179 let pk = signer.public_key_bytes();
180 let msg = b"approval-response canonical bytes";
181 let sig = signer.sign(msg);
182 assert!(verify(&pk, msg, &sig));
183
184 assert!(Signer::from_key_bytes(&[7u8; 31]).is_err());
186 assert!(Signer::from_key_bytes(&[7u8; 33]).is_err());
187 assert!(Signer::from_key_bytes(&[]).is_err());
188 }
189}