1use std::path::Path;
15
16use anyhow::{Context, Result};
17use ed25519_dalek::{Signature, Signer, SigningKey as DalekSigningKey, Verifier, VerifyingKey};
18
19#[derive(Clone)]
21pub struct SigningKey {
22 inner: DalekSigningKey,
23}
24
25impl SigningKey {
26 pub fn generate_and_save(path: &Path) -> Result<Self> {
28 let mut rng = rand::thread_rng();
29 let inner = DalekSigningKey::generate(&mut rng);
30 std::fs::write(path, inner.to_bytes())
31 .with_context(|| format!("failed to write signing key to {}", path.display()))?;
32 Ok(Self { inner })
33 }
34
35 pub fn load(path: &Path) -> Result<Self> {
37 let bytes = std::fs::read(path)
38 .with_context(|| format!("failed to read signing key from {}", path.display()))?;
39 let bytes: [u8; 32] = bytes
40 .try_into()
41 .map_err(|_| anyhow::anyhow!("signing key must be exactly 32 bytes"))?;
42 let inner = DalekSigningKey::from_bytes(&bytes);
43 Ok(Self { inner })
44 }
45
46 pub fn load_or_generate(path: &Path) -> Result<Self> {
48 if path.exists() {
49 Self::load(path)
50 } else {
51 if let Some(parent) = path.parent() {
52 std::fs::create_dir_all(parent)?;
53 }
54 Self::generate_and_save(path)
55 }
56 }
57
58 pub fn sign(&self, msg: &[u8]) -> [u8; 64] {
60 self.inner.sign(msg).to_bytes()
61 }
62
63 pub fn verify(&self, msg: &[u8], sig_bytes: &[u8; 64]) -> bool {
65 let Ok(sig) = Signature::from_slice(sig_bytes) else {
66 return false;
67 };
68 self.inner.verifying_key().verify(msg, &sig).is_ok()
69 }
70
71 pub fn public_key_bytes(&self) -> [u8; 32] {
73 self.inner.verifying_key().to_bytes()
74 }
75
76 pub fn public_key_hex(&self) -> String {
78 hex::encode(self.public_key_bytes())
79 }
80
81 pub fn sign_checkpoint(&self, checkpoint_body: &[u8]) -> String {
83 let sig = self.sign(checkpoint_body);
84 format!(
85 "sig/ed25519:{}:{}\n",
86 self.public_key_hex(),
87 hex::encode(sig)
88 )
89 }
90}
91
92pub fn verify_checkpoint_signature(pubkey_hex: &str, msg: &[u8], sig_hex: &str) -> bool {
95 let Ok(pk_bytes) = hex::decode(pubkey_hex) else {
96 return false;
97 };
98 let Ok(pk_arr): Result<[u8; 32], _> = pk_bytes.try_into() else {
99 return false;
100 };
101 let Ok(vk) = VerifyingKey::from_bytes(&pk_arr) else {
102 return false;
103 };
104 let Ok(sig_bytes) = hex::decode(sig_hex) else {
105 return false;
106 };
107 let Ok(sig) = Signature::from_slice(&sig_bytes) else {
108 return false;
109 };
110 vk.verify(msg, &sig).is_ok()
111}
112
113pub fn parse_sig_extension(line: &str) -> Option<(&str, &str)> {
116 let rest = line.strip_prefix("sig/ed25519:")?;
117 let rest = rest.strip_suffix('\n').unwrap_or(rest);
118 let (pubkey, sig) = rest.split_once(':')?;
119 if pubkey.len() == 64 && sig.len() == 128 {
120 Some((pubkey, sig))
121 } else {
122 None
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use tempfile::tempdir;
130
131 #[test]
132 fn generate_and_load_roundtrip() {
133 let dir = tempdir().unwrap();
134 let path = dir.path().join("signing_key");
135 let sk1 = SigningKey::generate_and_save(&path).unwrap();
136 let sk2 = SigningKey::load(&path).unwrap();
137 assert_eq!(sk1.public_key_bytes(), sk2.public_key_bytes());
138 }
139
140 #[test]
141 fn sign_and_verify() {
142 let dir = tempdir().unwrap();
143 let path = dir.path().join("signing_key");
144 let sk = SigningKey::generate_and_save(&path).unwrap();
145 let msg = b"punkgo/kernel\n42\nhash=\n";
146 let sig = sk.sign(msg);
147 assert!(sk.verify(msg, &sig));
148 }
149
150 #[test]
151 fn wrong_message_fails_verify() {
152 let dir = tempdir().unwrap();
153 let path = dir.path().join("signing_key");
154 let sk = SigningKey::generate_and_save(&path).unwrap();
155 let sig = sk.sign(b"correct");
156 assert!(!sk.verify(b"wrong", &sig));
157 }
158
159 #[test]
160 fn load_or_generate_creates_on_first_call() {
161 let dir = tempdir().unwrap();
162 let path = dir.path().join("signing_key");
163 assert!(!path.exists());
164 let sk = SigningKey::load_or_generate(&path).unwrap();
165 assert!(path.exists());
166 assert_eq!(sk.public_key_hex().len(), 64);
167 }
168
169 #[test]
170 fn sign_checkpoint_produces_valid_extension() {
171 let dir = tempdir().unwrap();
172 let path = dir.path().join("signing_key");
173 let sk = SigningKey::generate_and_save(&path).unwrap();
174
175 let body = b"punkgo/kernel\n100\naBcDeFgH=\n";
176 let ext = sk.sign_checkpoint(body);
177
178 assert!(ext.starts_with("sig/ed25519:"));
179 assert!(ext.ends_with('\n'));
180
181 let (pubkey, sig) = parse_sig_extension(&ext).unwrap();
183 assert!(verify_checkpoint_signature(pubkey, body, sig));
184 }
185
186 #[test]
187 fn parse_sig_extension_rejects_garbage() {
188 assert!(parse_sig_extension("garbage").is_none());
189 assert!(parse_sig_extension("sig/ed25519:short:short").is_none());
190 assert!(parse_sig_extension("sig/rsa:abc:def").is_none());
191 }
192
193 #[test]
194 fn verify_checkpoint_signature_rejects_wrong_key() {
195 let dir = tempdir().unwrap();
196 let sk1 = SigningKey::generate_and_save(&dir.path().join("k1")).unwrap();
197 let sk2 = SigningKey::generate_and_save(&dir.path().join("k2")).unwrap();
198
199 let msg = b"test message";
200 let sig = sk1.sign(msg);
201 assert!(!verify_checkpoint_signature(
203 &sk2.public_key_hex(),
204 msg,
205 &hex::encode(sig)
206 ));
207 }
208}