1use std::fmt;
4use std::io;
5use std::io::Write;
6use std::str::FromStr;
7
8use base64::Engine as _;
9use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
10use ed25519_dalek::Signer as _;
11use ed25519_dalek::Verifier as _;
12use serde::Deserialize;
13use serde::Serialize;
14use thiserror::Error;
15
16use crate::ContentHash;
17
18#[derive(Debug, Error)]
20pub enum KeyError {
21 #[error("invalid OpenSSH key: {0}")]
23 InvalidOpenSshKey(String),
24
25 #[error("OpenSSH key is not an Ed25519 key")]
27 WrongAlgorithm,
28}
29
30#[derive(Debug, Error)]
32pub enum SignatureError {
33 #[error("signature is not valid base64")]
35 InvalidBase64,
36
37 #[error("signature must be exactly 64 bytes; got {0}")]
39 WrongLength(usize),
40}
41
42#[derive(Debug, Error)]
44pub enum SignatureFileError {
45 #[error("invalid `module.sig` JSON")]
47 InvalidJson(#[from] serde_json::Error),
48
49 #[error(transparent)]
52 Key(#[from] KeyError),
53
54 #[error(transparent)]
57 Signature(#[from] SignatureError),
58}
59
60#[derive(Debug, Error)]
62#[error("Ed25519 signature does not match the supplied content hash")]
63pub struct VerifyError;
64
65#[derive(Clone, Debug)]
67pub struct SigningKey(ed25519_dalek::SigningKey);
68
69impl SigningKey {
70 pub fn from_openssh(text: &str) -> Result<Self, KeyError> {
73 let key = ssh_key::PrivateKey::from_openssh(text)
74 .map_err(|e| KeyError::InvalidOpenSshKey(e.to_string()))?;
75 let ed = key.key_data().ed25519().ok_or(KeyError::WrongAlgorithm)?;
76 let bytes: &[u8; 32] = &ed.private.to_bytes();
77 Ok(Self(ed25519_dalek::SigningKey::from_bytes(bytes)))
78 }
79
80 pub fn verifying_key(&self) -> VerifyingKey {
82 VerifyingKey(self.0.verifying_key())
83 }
84
85 pub fn sign(&self, digest: &ContentHash) -> Signature {
87 Signature(self.0.sign(digest.as_bytes()))
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(into = "String", try_from = "String")]
94pub struct VerifyingKey(ed25519_dalek::VerifyingKey);
95
96impl VerifyingKey {
97 pub fn from_openssh(text: &str) -> Result<Self, KeyError> {
102 let key = ssh_key::PublicKey::from_openssh(text.trim())
103 .map_err(|e| KeyError::InvalidOpenSshKey(e.to_string()))?;
104 let ed = key.key_data().ed25519().ok_or(KeyError::WrongAlgorithm)?;
105 let inner = ed25519_dalek::VerifyingKey::from_bytes(&ed.0)
106 .map_err(|e| KeyError::InvalidOpenSshKey(e.to_string()))?;
107 Ok(Self(inner))
108 }
109
110 pub fn to_openssh(&self) -> String {
113 let ed = ssh_key::public::Ed25519PublicKey(*self.0.as_bytes());
114 let key = ssh_key::PublicKey::from(ssh_key::public::KeyData::Ed25519(ed));
115 key.to_openssh().unwrap()
118 }
119
120 pub fn verify(&self, digest: &ContentHash, sig: &Signature) -> Result<(), VerifyError> {
123 self.0
124 .verify(digest.as_bytes(), &sig.0)
125 .map_err(|_| VerifyError)
126 }
127
128 pub fn as_bytes(&self) -> &[u8; 32] {
130 self.0.as_bytes()
131 }
132}
133
134impl fmt::Display for VerifyingKey {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.to_openssh())
137 }
138}
139
140impl FromStr for VerifyingKey {
141 type Err = KeyError;
142
143 fn from_str(s: &str) -> Result<Self, Self::Err> {
144 Self::from_openssh(s)
145 }
146}
147
148impl From<VerifyingKey> for String {
149 fn from(key: VerifyingKey) -> Self {
150 key.to_openssh()
151 }
152}
153
154impl TryFrom<String> for VerifyingKey {
155 type Error = KeyError;
156
157 fn try_from(s: String) -> Result<Self, Self::Error> {
158 Self::from_openssh(&s)
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
164#[serde(into = "String", try_from = "String")]
165pub struct Signature(ed25519_dalek::Signature);
166
167impl Signature {
168 pub fn from_base64(s: &str) -> Result<Self, SignatureError> {
170 let bytes = BASE64_STANDARD
171 .decode(s)
172 .map_err(|_| SignatureError::InvalidBase64)?;
173 let array: [u8; 64] = bytes
174 .as_slice()
175 .try_into()
176 .map_err(|_| SignatureError::WrongLength(bytes.len()))?;
177 Ok(Self(ed25519_dalek::Signature::from_bytes(&array)))
178 }
179
180 pub fn to_base64(&self) -> String {
182 BASE64_STANDARD.encode(self.0.to_bytes())
183 }
184}
185
186impl fmt::Display for Signature {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(&self.to_base64())
189 }
190}
191
192impl FromStr for Signature {
193 type Err = SignatureError;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 Self::from_base64(s)
197 }
198}
199
200impl From<Signature> for String {
201 fn from(sig: Signature) -> Self {
202 sig.to_base64()
203 }
204}
205
206impl TryFrom<String> for Signature {
207 type Error = SignatureError;
208
209 fn try_from(s: String) -> Result<Self, Self::Error> {
210 Self::from_base64(&s)
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(deny_unknown_fields)]
217pub struct ModuleSignature {
218 pub public_key: VerifyingKey,
220 pub signature: Signature,
222}
223
224impl ModuleSignature {
225 pub fn parse(bytes: &[u8]) -> Result<Self, SignatureFileError> {
227 Ok(crate::strict_json::from_slice(bytes)?)
228 }
229
230 pub fn write(&self, w: impl Write) -> io::Result<()> {
232 serde_json::to_writer_pretty(w, self).map_err(io::Error::other)
233 }
234
235 pub fn verify(&self, digest: &ContentHash) -> Result<(), VerifyError> {
238 self.public_key.verify(digest, &self.signature)
239 }
240}
241
242#[cfg(any(test, feature = "test-utils"))]
244pub mod test_utils {
245 use sha2::Digest;
246 use sha2::Sha256;
247
248 use super::*;
249
250 pub fn signing_key_from_seed(seed: u64) -> SigningKey {
257 let mut hasher = Sha256::new();
258 hasher.update(seed.to_le_bytes());
259 let bytes: [u8; 32] = hasher.finalize().into();
260 SigningKey(ed25519_dalek::SigningKey::from_bytes(&bytes))
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::test_utils::signing_key_from_seed;
267 use super::*;
268
269 #[test]
270 fn signs_and_verifies_round_trip() {
271 let signer = signing_key_from_seed(42);
272 let verifier = signer.verifying_key();
273 let digest = ContentHash::from([0xAB; 32]);
274
275 let sig = signer.sign(&digest);
276 verifier.verify(&digest, &sig).unwrap();
277 }
278
279 #[test]
280 fn detects_tampered_signature() {
281 let signer = signing_key_from_seed(42);
282 let verifier = signer.verifying_key();
283 let digest = ContentHash::from([0xAB; 32]);
284
285 let sig = signer.sign(&digest);
286 let tampered = ContentHash::from([0xAC; 32]);
287 assert!(verifier.verify(&tampered, &sig).is_err());
288 }
289
290 #[test]
291 fn verifying_key_round_trips_through_openssh() {
292 let signer = signing_key_from_seed(1);
293 let key = signer.verifying_key();
294 let openssh = key.to_openssh();
295 assert!(openssh.starts_with("ssh-ed25519 "));
296 let parsed = VerifyingKey::from_openssh(&openssh).unwrap();
297 assert_eq!(parsed.as_bytes(), key.as_bytes());
298 }
299
300 #[test]
301 fn verifying_key_accepts_openssh_with_comment() {
302 let signer = signing_key_from_seed(2);
303 let key = signer.verifying_key();
304 let with_comment = format!("{} user@example.com", key.to_openssh());
305 let parsed = VerifyingKey::from_openssh(&with_comment).unwrap();
306 assert_eq!(parsed.as_bytes(), key.as_bytes());
307 }
308
309 #[test]
310 fn signature_round_trips_through_base64() {
311 let signer = signing_key_from_seed(3);
312 let digest = ContentHash::from([0x11; 32]);
313 let sig = signer.sign(&digest);
314 let b64 = sig.to_base64();
315 let parsed = Signature::from_base64(&b64).unwrap();
316 assert_eq!(parsed, sig);
317 }
318
319 #[test]
320 fn module_signature_round_trips_through_json() {
321 let signer = signing_key_from_seed(4);
322 let digest = ContentHash::from([0x22; 32]);
323 let module_sig = ModuleSignature {
324 public_key: signer.verifying_key(),
325 signature: signer.sign(&digest),
326 };
327
328 let mut buf = Vec::new();
329 module_sig.write(&mut buf).unwrap();
330 let parsed = ModuleSignature::parse(&buf).unwrap();
331 assert_eq!(parsed, module_sig);
332 parsed.verify(&digest).unwrap();
333 }
334
335 #[test]
336 fn module_signature_rejects_unknown_keys() {
337 let signer = signing_key_from_seed(5);
338 let digest = ContentHash::from([0x33; 32]);
339 let json = format!(
340 r#"{{
341 "public_key": {},
342 "signature": {},
343 "unexpected": true
344 }}"#,
345 serde_json::to_string(&signer.verifying_key()).unwrap(),
346 serde_json::to_string(&signer.sign(&digest)).unwrap()
347 );
348
349 assert!(ModuleSignature::parse(json.as_bytes()).is_err());
350 }
351
352 #[test]
353 fn module_signature_rejects_duplicate_keys() {
354 let signer = signing_key_from_seed(6);
355 let digest = ContentHash::from([0x44; 32]);
356 let json = format!(
357 r#"{{
358 "public_key": {},
359 "public_key": {},
360 "signature": {}
361 }}"#,
362 serde_json::to_string(&signer.verifying_key()).unwrap(),
363 serde_json::to_string(&signer.verifying_key()).unwrap(),
364 serde_json::to_string(&signer.sign(&digest)).unwrap()
365 );
366
367 let err = ModuleSignature::parse(json.as_bytes()).unwrap_err();
368 assert!(
369 err.to_string().contains("invalid `module.sig` JSON"),
370 "wrong error: {err}"
371 );
372 }
373}