1use async_trait::async_trait;
12use ed25519_dalek::{Signer as EdSigner, SigningKey, Verifier as EdVerifier, VerifyingKey};
13use klieo_provenance::{BundleSigner, ProvenanceError, SignatureBytes};
14use rand::rngs::OsRng;
15use thiserror::Error;
16
17const ED25519_ALGORITHM: &str = "ed25519";
20
21const KEY_ID_PREFIX_LEN: usize = 8;
24
25#[derive(Clone, Debug)]
29#[non_exhaustive]
30pub struct SignedPayload {
31 pub signature: [u8; 64],
33 pub verifying_key: [u8; 32],
35}
36
37impl SignedPayload {
38 #[must_use]
41 pub fn new(signature: [u8; 64], verifying_key: [u8; 32]) -> Self {
42 Self {
43 signature,
44 verifying_key,
45 }
46 }
47}
48
49#[derive(Debug, Error)]
51#[non_exhaustive]
52pub enum SignerError {
53 #[error("signer backend: {0}")]
55 Backend(String),
56 #[error("internal: {0}")]
58 Internal(String),
59}
60
61#[async_trait]
63pub trait SignerProvider: Send + Sync {
64 async fn sign_handoff(&self, payload: &[u8]) -> Result<SignedPayload, SignerError>;
66}
67
68pub struct SoftwareSigner {
74 key: SigningKey,
75}
76
77impl SoftwareSigner {
78 #[must_use]
80 pub fn new(key: SigningKey) -> Self {
81 Self { key }
82 }
83
84 #[must_use]
86 pub fn from_bytes(seed: [u8; 32]) -> Self {
87 Self::new(SigningKey::from_bytes(&seed))
88 }
89
90 #[must_use]
98 pub fn generate() -> Self {
99 Self::new(SigningKey::generate(&mut OsRng))
100 }
101
102 #[must_use]
106 pub fn public_key_hex(&self) -> String {
107 hex::encode(self.key.verifying_key().to_bytes())
108 }
109
110 #[must_use]
114 pub fn key_id(&self) -> String {
115 let pub_hex = self.public_key_hex();
116 format!("klieo-ops-{}", &pub_hex[..KEY_ID_PREFIX_LEN])
117 }
118
119 #[must_use]
128 pub fn verify(&self, payload: &[u8], signature_hex: &str) -> bool {
129 let Ok(sig_bytes) = hex::decode(signature_hex) else {
130 return false;
131 };
132 let Ok(sig) = ed25519_dalek::Signature::from_slice(&sig_bytes) else {
133 return false;
134 };
135 self.verifying_key().verify(payload, &sig).is_ok()
136 }
137
138 fn verifying_key(&self) -> VerifyingKey {
139 self.key.verifying_key()
140 }
141}
142
143#[async_trait]
150impl BundleSigner for SoftwareSigner {
151 async fn sign(&self, payload: &[u8]) -> Result<SignatureBytes, ProvenanceError> {
152 let signature = self.key.sign(payload);
153 Ok(SignatureBytes {
154 algorithm: ED25519_ALGORITHM.to_string(),
155 signature_hex: hex::encode(signature.to_bytes()),
156 public_key_hex: self.public_key_hex(),
157 key_id: Some(self.key_id()),
158 })
159 }
160}
161
162#[async_trait]
163impl SignerProvider for SoftwareSigner {
164 async fn sign_handoff(&self, payload: &[u8]) -> Result<SignedPayload, SignerError> {
165 let sig = self.key.sign(payload);
166 Ok(SignedPayload {
167 signature: sig.to_bytes(),
168 verifying_key: self.key.verifying_key().to_bytes(),
169 })
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
177 use klieo_provenance::BundleSigner;
178
179 #[tokio::test]
180 async fn software_signer_produces_verifiable_signature() {
181 let signer = SoftwareSigner::from_bytes([7u8; 32]);
182 let payload = b"hello";
183 let signed = signer.sign_handoff(payload).await.expect("sign");
184 let vk = VerifyingKey::from_bytes(&signed.verifying_key).expect("vk");
185 let sig = Signature::from_bytes(&signed.signature);
186 vk.verify(payload, &sig).expect("verify");
187 }
188
189 #[tokio::test]
190 async fn bundle_signer_emits_ed25519_signature_bytes() {
191 let signer = SoftwareSigner::from_bytes([7u8; 32]);
192 let payload = b"evidence-bundle-payload";
193 let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
194 assert_eq!(sig.algorithm, "ed25519");
195 assert_eq!(sig.public_key_hex, signer.public_key_hex());
196
197 let key_id = signer.key_id();
198 assert_eq!(
199 sig.key_id.as_deref(),
200 Some(key_id.as_str()),
201 "bundle key_id must match the signer's stable key_id"
202 );
203 const KEY_ID_PREFIX: &str = "klieo-ops-";
204 assert!(
205 key_id.starts_with(KEY_ID_PREFIX),
206 "key_id must use the documented prefix"
207 );
208 assert_eq!(
209 key_id.len(),
210 KEY_ID_PREFIX.len() + KEY_ID_PREFIX_LEN,
211 "key_id is the prefix plus {KEY_ID_PREFIX_LEN} hex chars of the verifying key"
212 );
213 }
214
215 #[tokio::test]
216 async fn bundle_signature_round_trips_against_public_key() {
217 let signer = SoftwareSigner::from_bytes([7u8; 32]);
218 let payload = b"evidence-bundle-payload";
219 let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
220 assert!(
221 signer.verify(payload, &sig.signature_hex),
222 "real payload must verify"
223 );
224 assert!(
225 !signer.verify(b"tampered-payload", &sig.signature_hex),
226 "tampered payload must be rejected"
227 );
228 assert!(
229 !signer.verify(payload, "deadbeef"),
230 "malformed signature hex must be rejected"
231 );
232 }
233
234 #[tokio::test]
235 async fn generated_signer_round_trips() {
236 let signer = SoftwareSigner::generate();
237 let payload = b"fresh-keypair-payload";
238 let sig = BundleSigner::sign(&signer, payload).await.expect("sign");
239 assert!(signer.verify(payload, &sig.signature_hex));
240 assert!(!signer.verify(b"other", &sig.signature_hex));
241 }
242}