nix_compat/narinfo/
verifying_keys.rs1use std::fmt::Display;
5
6use data_encoding::BASE64;
7use ed25519_dalek::PUBLIC_KEY_LENGTH;
8
9use super::SignatureRef;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct VerifyingKey {
16 name: String,
17 verifying_key: ed25519_dalek::VerifyingKey,
18}
19
20impl VerifyingKey {
21 pub fn new(name: String, verifying_key: ed25519_dalek::VerifyingKey) -> Self {
22 Self {
23 name,
24 verifying_key,
25 }
26 }
27
28 pub fn parse(input: &str) -> Result<Self, Error> {
29 let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?;
30
31 if name.is_empty()
32 || !name
33 .chars()
34 .all(|c| char::is_alphanumeric(c) || c == '-' || c == '.')
35 {
36 return Err(Error::InvalidName(name.to_string()));
37 }
38
39 if bytes64.len() != BASE64.encode_len(PUBLIC_KEY_LENGTH) {
40 return Err(Error::InvalidVerifyingKeyLen(bytes64.len()));
41 }
42
43 let mut buf = [0; PUBLIC_KEY_LENGTH + 1];
44 let mut bytes = [0; PUBLIC_KEY_LENGTH];
45 match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) {
46 Ok(PUBLIC_KEY_LENGTH) => {
47 bytes.copy_from_slice(&buf[..PUBLIC_KEY_LENGTH]);
48 }
49 Ok(_) => unreachable!(),
50 Err(_) => return Err(Error::DecodeError(input.to_string())),
52 }
53
54 let verifying_key =
55 ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(Error::InvalidVerifyingKey)?;
56
57 Ok(Self {
58 name: name.to_string(),
59 verifying_key,
60 })
61 }
62
63 pub fn name(&self) -> &str {
64 &self.name
65 }
66
67 pub fn verify(&self, fingerprint: &str, signature: &SignatureRef<'_>) -> bool {
73 if self.name() != *signature.name() {
74 return false;
75 }
76
77 signature.verify(fingerprint.as_bytes(), &self.verifying_key)
78 }
79}
80
81#[derive(Debug, thiserror::Error)]
82pub enum Error {
83 #[error("Invalid name: {0}")]
84 InvalidName(String),
85 #[error("Missing separator")]
86 MissingSeparator,
87 #[error("Invalid pubkey len: {0}")]
88 InvalidVerifyingKeyLen(usize),
89 #[error("VerifyingKey error: {0}")]
90 InvalidVerifyingKey(ed25519_dalek::SignatureError),
91 #[error("Unable to base64-decode pubkey: {0}")]
92 DecodeError(String),
93}
94
95impl Display for VerifyingKey {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 write!(
98 f,
99 "{}:{}",
100 self.name,
101 BASE64.encode(self.verifying_key.as_bytes())
102 )
103 }
104}
105
106#[cfg(test)]
107mod test {
108 use data_encoding::BASE64;
109 use ed25519_dalek::PUBLIC_KEY_LENGTH;
110 use rstest::rstest;
111
112 use crate::narinfo::SignatureRef;
113
114 use super::VerifyingKey;
115 const FINGERPRINT: &str = "1;/nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin;sha256:1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0;196040;/nix/store/0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0,/nix/store/6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115,/nix/store/j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12,/nix/store/yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n";
116
117 #[rstest]
118 #[case::cache_nixos_org("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", "cache.nixos.org-1", &BASE64.decode(b"6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=").unwrap()[..].try_into().unwrap())]
119 #[case::cache_nixos_org_different_name("cheesecake:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", "cheesecake", &BASE64.decode(b"6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=").unwrap()[..].try_into().unwrap())]
120 #[case::test_1("test1:tLAEn+EeaBUJYqEpTd2yeerr7Ic6+0vWe+aXL/vYUpE=", "test1", &BASE64.decode(b"tLAEn+EeaBUJYqEpTd2yeerr7Ic6+0vWe+aXL/vYUpE=").unwrap()[..].try_into().unwrap())]
121 fn parse(
122 #[case] input: &'static str,
123 #[case] exp_name: &'static str,
124 #[case] exp_verifying_key_bytes: &[u8; PUBLIC_KEY_LENGTH],
125 ) {
126 let pubkey = VerifyingKey::parse(input).expect("must parse");
127 assert_eq!(exp_name, pubkey.name());
128 assert_eq!(exp_verifying_key_bytes, pubkey.verifying_key.as_bytes());
129 }
130
131 #[rstest]
132 #[case::empty_name("6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=")]
133 #[case::missing_padding("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY")]
134 #[case::wrong_length("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDS")]
135 fn parse_fail(#[case] input: &'static str) {
136 VerifyingKey::parse(input).expect_err("must fail");
137 }
138
139 #[rstest]
140 #[case::correct_cache_nixos_org("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", FINGERPRINT, "cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", true)]
141 #[case::wrong_name_mismatch("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", FINGERPRINT, "cache.nixos.org:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", false)]
142 fn verify(
143 #[case] pubkey_str: &'static str,
144 #[case] fingerprint: &'static str,
145 #[case] signature_str: &'static str,
146 #[case] expected: bool,
147 ) {
148 let pubkey = VerifyingKey::parse(pubkey_str).expect("must parse");
149 let signature = SignatureRef::parse(signature_str).expect("must parse");
150
151 assert_eq!(expected, pubkey.verify(fingerprint, &signature));
152 }
153}