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