1use ed25519_dalek::{Signature, Verifier, VerifyingKey};
6use thiserror::Error;
7
8use crate::hash::base64_encode;
9
10pub trait SignatureVerifier: Send + Sync {
15 fn verify(&self, fingerprint: &[u8], signature: &[u8], public_key: &[u8]) -> Result<(), SignatureError>;
17}
18
19pub struct Ed25519Verifier;
21
22impl SignatureVerifier for Ed25519Verifier {
23 fn verify(&self, fingerprint: &[u8], signature: &[u8], public_key: &[u8]) -> Result<(), SignatureError> {
24 let key_bytes: [u8; 32] = public_key.try_into()
25 .map_err(|_| SignatureError::InvalidPublicKey)?;
26 let sig_bytes: [u8; 64] = signature.try_into()
27 .map_err(|_| SignatureError::InvalidFormat("signature must be 64 bytes".to_string()))?;
28
29 let verifying_key = VerifyingKey::from_bytes(&key_bytes)
30 .map_err(|_| SignatureError::InvalidPublicKey)?;
31 let sig = Signature::from_bytes(&sig_bytes);
32
33 verifying_key.verify(fingerprint, &sig)
34 .map_err(|_| SignatureError::VerificationFailed)
35 }
36}
37
38#[derive(Debug, Error)]
39pub enum SignatureError {
40 #[error("invalid signature format: {0}")]
41 InvalidFormat(String),
42 #[error("base64 decode error")]
43 Base64Decode,
44 #[error("invalid public key")]
45 InvalidPublicKey,
46 #[error("verification failed")]
47 VerificationFailed,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct StorePathSignature {
53 pub key_name: String,
55 pub signature: Vec<u8>,
57}
58
59impl StorePathSignature {
60 pub fn parse(s: &str) -> Result<Self, SignatureError> {
62 let (key_name, b64) = s
63 .split_once(':')
64 .ok_or_else(|| SignatureError::InvalidFormat("missing colon".to_string()))?;
65
66 let signature = b64_decode(b64)
67 .map_err(|_| SignatureError::Base64Decode)?;
68
69 Ok(Self {
70 key_name: key_name.to_string(),
71 signature,
72 })
73 }
74
75 #[must_use]
77 pub fn to_string_repr(&self) -> String {
78 self.to_string()
79 }
80
81 pub fn verify(&self, fingerprint: &str, public_key: &[u8; 32]) -> Result<(), SignatureError> {
86 self.verify_with(fingerprint, public_key, &Ed25519Verifier)
87 }
88
89 pub fn verify_with(
91 &self,
92 fingerprint: &str,
93 public_key: &[u8],
94 verifier: &dyn SignatureVerifier,
95 ) -> Result<(), SignatureError> {
96 verifier.verify(fingerprint.as_bytes(), &self.signature, public_key)
97 }
98}
99
100impl std::fmt::Display for StorePathSignature {
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 write!(f, "{}:{}", self.key_name, base64_encode(&self.signature))
103 }
104}
105
106#[must_use]
126pub fn compute_fingerprint(
127 store_path: &str,
128 nar_hash: &str,
129 nar_size: u64,
130 references: &[String],
131) -> String {
132 let mut sorted_refs: Vec<&str> = references.iter().map(String::as_str).collect();
133 sorted_refs.sort_unstable();
134 let refs = sorted_refs.join(",");
135 format!("1;{store_path};{nar_hash};{nar_size};{refs}")
136}
137
138fn b64_decode(input: &str) -> Result<Vec<u8>, ()> {
140 crate::hash::base64_decode(input).map_err(|_| ())
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use ed25519_dalek::SigningKey;
147
148 #[test]
149 fn parse_signature() {
150 let sig_str = "cache.nixos.org-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
152 let sig = StorePathSignature::parse(sig_str).unwrap();
153 assert_eq!(sig.key_name, "cache.nixos.org-1");
154 assert_eq!(sig.signature.len(), 64);
155 }
156
157 #[test]
158 fn roundtrip_signature_format() {
159 let sig = StorePathSignature {
160 key_name: "test-key-1".to_string(),
161 signature: vec![0u8; 64],
162 };
163 let s = sig.to_string_repr();
164 let parsed = StorePathSignature::parse(&s).unwrap();
165 assert_eq!(parsed.key_name, sig.key_name);
166 assert_eq!(parsed.signature, sig.signature);
167 }
168
169 #[test]
170 fn sign_and_verify() {
171 use ed25519_dalek::Signer;
172
173 let signing_key = SigningKey::from_bytes(&[1u8; 32]);
174 let verifying_key = signing_key.verifying_key();
175
176 let fingerprint = compute_fingerprint(
177 "/nix/store/abc-hello-2.12.1",
178 "sha256:deadbeef",
179 226552,
180 &["glibc-2.37".to_string()],
181 );
182
183 let sig = signing_key.sign(fingerprint.as_bytes());
184
185 let store_sig = StorePathSignature {
186 key_name: "test-key".to_string(),
187 signature: sig.to_bytes().to_vec(),
188 };
189
190 assert!(store_sig.verify(&fingerprint, verifying_key.as_bytes()).is_ok());
191 }
192
193 #[test]
194 fn verify_wrong_fingerprint() {
195 use ed25519_dalek::Signer;
196
197 let signing_key = SigningKey::from_bytes(&[2u8; 32]);
198 let verifying_key = signing_key.verifying_key();
199
200 let fingerprint = "1;/nix/store/abc;sha256:aaa;100;";
201 let sig = signing_key.sign(fingerprint.as_bytes());
202
203 let store_sig = StorePathSignature {
204 key_name: "test-key".to_string(),
205 signature: sig.to_bytes().to_vec(),
206 };
207
208 assert!(store_sig.verify("wrong fingerprint", verifying_key.as_bytes()).is_err());
209 }
210
211 #[test]
212 fn compute_fingerprint_format() {
213 let fp = compute_fingerprint(
214 "/nix/store/abc-hello",
215 "sha256:deadbeef",
216 1024,
217 &["dep1".to_string(), "dep2".to_string()],
218 );
219 assert_eq!(fp, "1;/nix/store/abc-hello;sha256:deadbeef;1024;dep1,dep2");
220 }
221
222 #[test]
223 fn invalid_signature_format() {
224 assert!(StorePathSignature::parse("no-colon-here").is_err());
225 }
226
227 #[test]
228 fn multiple_signatures_on_same_path() {
229 use ed25519_dalek::Signer;
230
231 let fingerprint = compute_fingerprint(
232 "/nix/store/abc-hello-2.12.1",
233 "sha256:deadbeef",
234 226552,
235 &["glibc-2.37".to_string()],
236 );
237
238 let key1 = SigningKey::from_bytes(&[1u8; 32]);
240 let key2 = SigningKey::from_bytes(&[2u8; 32]);
241 let vk1 = key1.verifying_key();
242 let vk2 = key2.verifying_key();
243
244 let sig1 = StorePathSignature {
245 key_name: "cache.nixos.org-1".to_string(),
246 signature: key1.sign(fingerprint.as_bytes()).to_bytes().to_vec(),
247 };
248 let sig2 = StorePathSignature {
249 key_name: "my-cache-1".to_string(),
250 signature: key2.sign(fingerprint.as_bytes()).to_bytes().to_vec(),
251 };
252
253 assert!(sig1.verify(&fingerprint, vk1.as_bytes()).is_ok());
255 assert!(sig2.verify(&fingerprint, vk2.as_bytes()).is_ok());
256
257 assert!(sig1.verify(&fingerprint, vk2.as_bytes()).is_err());
259 assert!(sig2.verify(&fingerprint, vk1.as_bytes()).is_err());
260 }
261
262 #[test]
263 fn signature_with_very_long_key_name() {
264 let long_name = "a".repeat(500);
265 let sig = StorePathSignature {
266 key_name: long_name.clone(),
267 signature: vec![0u8; 64],
268 };
269 let s = sig.to_string_repr();
270 assert!(s.starts_with(&long_name));
271 let parsed = StorePathSignature::parse(&s).unwrap();
272 assert_eq!(parsed.key_name, long_name);
273 }
274
275 #[test]
276 fn fingerprint_with_empty_references() {
277 let fp = compute_fingerprint(
278 "/nix/store/abc-hello",
279 "sha256:deadbeef",
280 1024,
281 &[],
282 );
283 assert_eq!(fp, "1;/nix/store/abc-hello;sha256:deadbeef;1024;");
284 }
285
286 #[test]
287 fn fingerprint_with_many_references() {
288 let refs: Vec<String> = (0..20)
289 .map(|i| format!("dep-{i:02}"))
290 .collect();
291 let fp = compute_fingerprint(
292 "/nix/store/abc-hello",
293 "sha256:deadbeef",
294 1024,
295 &refs,
296 );
297 let parts: Vec<&str> = fp.split(';').collect();
299 assert_eq!(parts.len(), 5);
300 let ref_part = parts[4];
301 let ref_entries: Vec<&str> = ref_part.split(',').collect();
302 assert_eq!(ref_entries.len(), 20);
303 assert_eq!(ref_entries[0], "dep-00");
304 assert_eq!(ref_entries[19], "dep-19");
305 }
306
307 #[test]
308 fn signature_roundtrip_with_nonzero_bytes() {
309 let sig = StorePathSignature {
310 key_name: "test-key-1".to_string(),
311 signature: (0..64).collect::<Vec<u8>>(),
312 };
313 let s = sig.to_string_repr();
314 let parsed = StorePathSignature::parse(&s).unwrap();
315 assert_eq!(parsed.key_name, sig.key_name);
316 assert_eq!(parsed.signature, sig.signature);
317 }
318
319 struct AlwaysValidVerifier;
322 impl SignatureVerifier for AlwaysValidVerifier {
323 fn verify(&self, _: &[u8], _: &[u8], _: &[u8]) -> Result<(), SignatureError> { Ok(()) }
324 }
325
326 struct AlwaysInvalidVerifier;
327 impl SignatureVerifier for AlwaysInvalidVerifier {
328 fn verify(&self, _: &[u8], _: &[u8], _: &[u8]) -> Result<(), SignatureError> {
329 Err(SignatureError::VerificationFailed)
330 }
331 }
332
333 #[test]
334 fn verify_with_always_valid() {
335 let sig = StorePathSignature { key_name: "k".into(), signature: vec![0; 64] };
336 assert!(sig.verify_with("fp", &[0; 32], &AlwaysValidVerifier).is_ok());
337 }
338
339 #[test]
340 fn verify_with_always_invalid() {
341 let sig = StorePathSignature { key_name: "k".into(), signature: vec![0; 64] };
342 assert!(sig.verify_with("fp", &[0; 32], &AlwaysInvalidVerifier).is_err());
343 }
344
345 #[test]
346 fn verifier_object_safe() {
347 fn _assert(_: &dyn SignatureVerifier) {}
348 _assert(&AlwaysValidVerifier);
349 }
350
351 #[test]
354 fn parse_empty_string() {
355 assert!(StorePathSignature::parse("").is_err());
356 }
357
358 #[test]
359 fn parse_only_colon() {
360 let sig = StorePathSignature::parse(":");
361 assert!(sig.is_ok());
363 let s = sig.unwrap();
364 assert_eq!(s.key_name, "");
365 assert!(s.signature.is_empty());
366 }
367
368 #[test]
369 fn parse_invalid_base64_after_colon() {
370 let result = StorePathSignature::parse("key:!!!not-base64!!!");
371 assert!(result.is_err());
372 }
373
374 #[test]
377 fn fingerprint_with_single_reference() {
378 let fp = compute_fingerprint("/nix/store/abc", "sha256:xxx", 500, &["dep".to_string()]);
379 assert_eq!(fp, "1;/nix/store/abc;sha256:xxx;500;dep");
380 }
381
382 #[test]
383 fn fingerprint_with_zero_nar_size() {
384 let fp = compute_fingerprint("/nix/store/empty", "sha256:000", 0, &[]);
385 assert_eq!(fp, "1;/nix/store/empty;sha256:000;0;");
386 }
387
388 #[test]
389 fn fingerprint_with_large_nar_size() {
390 let fp = compute_fingerprint("/nix/store/big", "sha256:aaa", u64::MAX, &[]);
391 assert!(fp.contains(&u64::MAX.to_string()));
392 }
393
394 #[test]
397 fn ed25519_verifier_invalid_key_length() {
398 let verifier = Ed25519Verifier;
399 let result = verifier.verify(b"data", &[0; 64], &[0; 16]);
400 assert!(result.is_err());
401 }
402
403 #[test]
404 fn ed25519_verifier_invalid_signature_length() {
405 let verifier = Ed25519Verifier;
406 let result = verifier.verify(b"data", &[0; 32], &[0; 32]);
407 assert!(result.is_err());
408 }
409
410 #[test]
413 fn to_string_repr_format() {
414 let sig = StorePathSignature {
415 key_name: "cache.nixos.org-1".to_string(),
416 signature: vec![1; 64],
417 };
418 let s = sig.to_string_repr();
419 assert!(s.starts_with("cache.nixos.org-1:"));
420 assert!(s.len() > "cache.nixos.org-1:".len());
421 }
422
423 #[test]
426 fn display_trait_matches_to_string_repr() {
427 let sig = StorePathSignature {
428 key_name: "k".to_string(),
429 signature: vec![0xab; 64],
430 };
431 let displayed = format!("{sig}");
432 let manual = sig.to_string_repr();
433 assert_eq!(displayed, manual);
434 }
435
436 #[test]
439 fn verify_with_tampered_signature() {
440 use ed25519_dalek::Signer;
441 let signing_key = SigningKey::from_bytes(&[3u8; 32]);
442 let verifying_key = signing_key.verifying_key();
443
444 let fingerprint = "1;/nix/store/abc;sha256:def;100;";
445 let mut sig_bytes = signing_key.sign(fingerprint.as_bytes()).to_bytes().to_vec();
446
447 sig_bytes[0] ^= 0x01;
449
450 let store_sig = StorePathSignature {
451 key_name: "k".to_string(),
452 signature: sig_bytes,
453 };
454
455 let result = store_sig.verify(fingerprint, verifying_key.as_bytes());
456 assert!(result.is_err());
457 match result {
458 Err(SignatureError::VerificationFailed) => {}
459 other => panic!("expected VerificationFailed, got {other:?}"),
460 }
461 }
462
463 #[test]
464 fn verify_with_truncated_signature_fails() {
465 let store_sig = StorePathSignature {
466 key_name: "k".to_string(),
467 signature: vec![0u8; 32], };
469 let result = store_sig.verify("fp", &[0u8; 32]);
470 assert!(result.is_err());
471 }
472
473 #[test]
474 fn verify_with_oversized_signature_fails() {
475 let store_sig = StorePathSignature {
476 key_name: "k".to_string(),
477 signature: vec![0u8; 128], };
479 let result = store_sig.verify("fp", &[0u8; 32]);
480 assert!(result.is_err());
481 }
482
483 #[test]
486 fn verify_with_zero_public_key_fails() {
487 use ed25519_dalek::Signer;
488 let signing_key = SigningKey::from_bytes(&[5u8; 32]);
489 let fingerprint = "fingerprint";
490 let sig = signing_key.sign(fingerprint.as_bytes());
491
492 let store_sig = StorePathSignature {
493 key_name: "k".to_string(),
494 signature: sig.to_bytes().to_vec(),
495 };
496
497 let result = store_sig.verify(fingerprint, &[0u8; 32]);
500 assert!(result.is_err());
501 }
502
503 #[test]
504 fn verify_with_random_public_key_fails() {
505 use ed25519_dalek::Signer;
506 let signing_key = SigningKey::from_bytes(&[6u8; 32]);
507 let other_key = SigningKey::from_bytes(&[7u8; 32]);
508
509 let fingerprint = "fp";
510 let sig = signing_key.sign(fingerprint.as_bytes());
511 let store_sig = StorePathSignature {
512 key_name: "k".to_string(),
513 signature: sig.to_bytes().to_vec(),
514 };
515
516 let result = store_sig.verify(fingerprint, other_key.verifying_key().as_bytes());
517 assert!(result.is_err());
518 }
519
520 #[test]
523 fn parse_signature_no_colon_error_variant() {
524 match StorePathSignature::parse("just-a-key-name") {
525 Err(SignatureError::InvalidFormat(s)) => assert!(s.contains("colon")),
526 other => panic!("expected InvalidFormat, got {other:?}"),
527 }
528 }
529
530 #[test]
531 fn parse_signature_invalid_base64_error_variant() {
532 match StorePathSignature::parse("k:!!!not-base64!!!") {
533 Err(SignatureError::Base64Decode) => {}
534 other => panic!("expected Base64Decode, got {other:?}"),
535 }
536 }
537
538 #[test]
541 fn fingerprint_format_with_spaces_in_path() {
542 let fp = compute_fingerprint("/nix/store/abc with spaces", "sha256:abc", 1, &[]);
543 assert_eq!(fp, "1;/nix/store/abc with spaces;sha256:abc;1;");
544 }
545
546 #[test]
547 fn fingerprint_with_three_references_comma_separated() {
548 let refs = vec!["a".to_string(), "b".to_string(), "c".to_string()];
549 let fp = compute_fingerprint("/p", "h", 1, &refs);
550 assert!(fp.ends_with(";a,b,c"));
551 }
552
553 #[test]
556 fn fingerprint_sorts_unsorted_references() {
557 let unsorted = vec!["c".to_string(), "a".to_string(), "b".to_string()];
560 let fp = compute_fingerprint("/p", "h", 1, &unsorted);
561 assert!(fp.ends_with(";a,b,c"), "references must be sorted, got {fp}");
562 }
563
564 #[test]
565 fn fingerprint_order_independent() {
566 let a = compute_fingerprint("/p", "h", 1, &["z".into(), "a".into(), "m".into()]);
570 let b = compute_fingerprint("/p", "h", 1, &["a".into(), "m".into(), "z".into()]);
571 let c = compute_fingerprint("/p", "h", 1, &["m".into(), "z".into(), "a".into()]);
572 assert_eq!(a, b);
573 assert_eq!(b, c);
574 }
575
576 #[test]
577 fn fingerprint_sorts_full_store_paths() {
578 let refs = vec![
581 "/nix/store/zzz-b".to_string(),
582 "/nix/store/aaa-a".to_string(),
583 ];
584 let fp = compute_fingerprint("/nix/store/x-pkg", "sha256:h", 1, &refs);
585 assert!(fp.ends_with(";/nix/store/aaa-a,/nix/store/zzz-b"), "got {fp}");
586 }
587
588 #[test]
591 fn ed25519_verifier_correct_size_inputs_with_invalid_data() {
592 let verifier = Ed25519Verifier;
593 let result = verifier.verify(b"data", &[0u8; 64], &[0u8; 32]);
595 assert!(result.is_err());
597 }
598
599 #[test]
600 fn ed25519_verifier_short_key_returns_invalid_public_key() {
601 let verifier = Ed25519Verifier;
602 let result = verifier.verify(b"data", &[0u8; 64], &[0u8; 16]);
603 match result {
604 Err(SignatureError::InvalidPublicKey) => {}
605 other => panic!("expected InvalidPublicKey, got {other:?}"),
606 }
607 }
608
609 #[test]
610 fn ed25519_verifier_short_signature_returns_invalid_format() {
611 let verifier = Ed25519Verifier;
612 let result = verifier.verify(b"data", &[0u8; 32], &[0u8; 32]);
613 match result {
614 Err(SignatureError::InvalidFormat(s)) => assert!(s.contains("64 bytes")),
615 other => panic!("expected InvalidFormat, got {other:?}"),
616 }
617 }
618
619 struct CountingVerifier {
622 count: std::cell::Cell<usize>,
623 }
624
625 impl CountingVerifier {
626 fn new() -> Self {
627 Self { count: std::cell::Cell::new(0) }
628 }
629 }
630
631 struct AtomicCountingVerifier {
634 count: std::sync::atomic::AtomicUsize,
635 }
636
637 impl AtomicCountingVerifier {
638 fn new() -> Self {
639 Self { count: std::sync::atomic::AtomicUsize::new(0) }
640 }
641 fn count(&self) -> usize {
642 self.count.load(std::sync::atomic::Ordering::SeqCst)
643 }
644 }
645
646 impl SignatureVerifier for AtomicCountingVerifier {
647 fn verify(&self, _: &[u8], _: &[u8], _: &[u8]) -> Result<(), SignatureError> {
648 self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
649 Ok(())
650 }
651 }
652
653 #[test]
654 fn verify_with_custom_verifier_invokes_verify_method() {
655 let counter = AtomicCountingVerifier::new();
656 let sig = StorePathSignature {
657 key_name: "k".to_string(),
658 signature: vec![0u8; 64],
659 };
660 sig.verify_with("fp", &[0u8; 32], &counter).unwrap();
661 sig.verify_with("fp", &[0u8; 32], &counter).unwrap();
662 assert_eq!(counter.count(), 2);
663 }
664
665 #[test]
667 fn _counting_verifier_unused_field_workaround() {
668 let v = CountingVerifier::new();
669 v.count.set(1);
670 assert_eq!(v.count.get(), 1);
671 }
672
673 #[test]
676 fn signature_clone_eq() {
677 let sig = StorePathSignature {
678 key_name: "k".to_string(),
679 signature: vec![1, 2, 3, 4, 5],
680 };
681 let cloned = sig.clone();
682 assert_eq!(sig, cloned);
683 }
684
685 #[test]
688 fn fingerprint_with_long_store_path_and_hash() {
689 let path = format!("/nix/store/{}", "a".repeat(200));
690 let hash = format!("sha256:{}", "0".repeat(64));
691 let fp = compute_fingerprint(&path, &hash, 999_999_999, &[]);
692 assert!(fp.contains(&path));
693 assert!(fp.contains(&hash));
694 }
695
696 #[test]
699 fn sign_verify_multiple_distinct_keys() {
700 use ed25519_dalek::Signer;
701 for seed_byte in [10u8, 20, 30, 40, 50] {
702 let signing_key = SigningKey::from_bytes(&[seed_byte; 32]);
703 let verifying_key = signing_key.verifying_key();
704 let fingerprint = format!("1;/nix/store/p-{seed_byte};sha256:h;100;");
705 let sig = signing_key.sign(fingerprint.as_bytes());
706 let store_sig = StorePathSignature {
707 key_name: format!("k{seed_byte}"),
708 signature: sig.to_bytes().to_vec(),
709 };
710 assert!(
711 store_sig.verify(&fingerprint, verifying_key.as_bytes()).is_ok(),
712 "key seed {seed_byte} should verify",
713 );
714 }
715 }
716
717 #[test]
720 fn parse_then_verify_roundtrip() {
721 use ed25519_dalek::Signer;
722 let signing_key = SigningKey::from_bytes(&[42u8; 32]);
723 let verifying_key = signing_key.verifying_key();
724
725 let fingerprint = "1;/nix/store/abc;sha256:def;1024;";
726 let raw_sig = signing_key.sign(fingerprint.as_bytes());
727 let original = StorePathSignature {
728 key_name: "test-k".to_string(),
729 signature: raw_sig.to_bytes().to_vec(),
730 };
731
732 let s = original.to_string_repr();
734 let parsed = StorePathSignature::parse(&s).unwrap();
735 assert_eq!(parsed, original);
736
737 assert!(parsed.verify(fingerprint, verifying_key.as_bytes()).is_ok());
738 }
739
740 #[test]
743 fn b64_decode_helper_returns_error_on_garbage() {
744 let result = StorePathSignature::parse("k:zzz!!!");
746 assert!(result.is_err());
747 }
748}