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