1use std::{
2 fmt::{self, Display},
3 ops::Deref,
4};
5
6use data_encoding::BASE64;
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10const SIGNATURE_LENGTH: usize = std::mem::size_of::<ed25519::SignatureBytes>();
11
12#[derive(Clone, Debug, Eq, PartialEq)]
13pub struct Signature<S> {
14 name: S,
15 bytes: ed25519::SignatureBytes,
16}
17
18pub type SignatureRef<'a> = Signature<&'a str>;
20
21impl<S> Signature<S>
27where
28 S: Deref<Target = str>,
29{
30 pub fn new(name: S, bytes: ed25519::SignatureBytes) -> Self {
32 Self { name, bytes }
33 }
34
35 pub fn parse<'a>(input: &'a str) -> Result<Self, Error>
40 where
41 S: From<&'a str>,
42 {
43 let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?;
44
45 if name.is_empty()
46 || !name
47 .chars()
48 .all(|c| char::is_alphanumeric(c) || c == '-' || c == '.')
49 {
50 return Err(Error::InvalidName(name.to_string()));
51 }
52
53 if bytes64.len() != BASE64.encode_len(SIGNATURE_LENGTH) {
54 return Err(Error::InvalidSignatureLen(bytes64.len()));
55 }
56
57 let mut bytes = [0; SIGNATURE_LENGTH];
58 let mut buf = [0; SIGNATURE_LENGTH + 2];
59 match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) {
60 Ok(SIGNATURE_LENGTH) => bytes.copy_from_slice(&buf[..SIGNATURE_LENGTH]),
61 Ok(_) => unreachable!(),
62 Err(_) => return Err(Error::DecodeError(input.to_string())),
64 }
65
66 Ok(Self {
67 name: name.into(),
68 bytes,
69 })
70 }
71
72 pub fn name(&self) -> &S {
74 &self.name
75 }
76
77 pub fn bytes(&self) -> &ed25519::SignatureBytes {
79 &self.bytes
80 }
81
82 pub fn verify(&self, fingerprint: &[u8], verifying_key: &ed25519_dalek::VerifyingKey) -> bool {
84 let signature = ed25519_dalek::Signature::from_bytes(self.bytes());
85
86 verifying_key.verify_strict(fingerprint, &signature).is_ok()
87 }
88
89 pub fn as_ref(&self) -> SignatureRef<'_> {
91 SignatureRef {
92 name: self.name.deref(),
93 bytes: self.bytes,
94 }
95 }
96 pub fn to_owned(&self) -> Signature<String> {
97 Signature {
98 name: self.name.to_string(),
99 bytes: self.bytes,
100 }
101 }
102}
103
104#[cfg(feature = "serde")]
105impl<'a, 'de, S> Deserialize<'de> for Signature<S>
106where
107 S: Deref<Target = str> + From<&'a str>,
108 'de: 'a,
109{
110 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
111 where
112 D: serde::Deserializer<'de>,
113 {
114 let str: &'de str = Deserialize::deserialize(deserializer)?;
115 Self::parse(str).map_err(|_| {
116 serde::de::Error::invalid_value(serde::de::Unexpected::Str(str), &"Signature")
117 })
118 }
119}
120
121#[cfg(feature = "serde")]
122impl<S: Display> Serialize for Signature<S>
123where
124 S: Deref<Target = str>,
125{
126 fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error>
127 where
128 SR: serde::Serializer,
129 {
130 let string: String = self.to_string();
131
132 string.serialize(serializer)
133 }
134}
135
136impl<S> Display for Signature<S>
137where
138 S: Display,
139{
140 fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
141 write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes))
142 }
143}
144
145impl<S> std::hash::Hash for Signature<S>
146where
147 S: AsRef<str>,
148{
149 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
150 state.write(self.name.as_ref().as_bytes());
151 state.write(&self.bytes);
152 }
153}
154
155#[derive(Debug, thiserror::Error, PartialEq, Eq)]
156pub enum Error {
157 #[error("Invalid name: {0}")]
158 InvalidName(String),
159 #[error("Missing separator")]
160 MissingSeparator,
161 #[error("Invalid signature len: (expected {} b64-encoded, got {}", BASE64.encode_len(SIGNATURE_LENGTH), .0)]
162 InvalidSignatureLen(usize),
163 #[error("Unable to base64-decode signature: {0}")]
164 DecodeError(String),
165}
166
167#[cfg(test)]
168mod test {
169 use data_encoding::BASE64;
170 use ed25519_dalek::VerifyingKey;
171 #[cfg(feature = "serde")]
172 use hex_literal::hex;
173 use std::sync::LazyLock;
174
175 use super::Signature;
176 use rstest::rstest;
177
178 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";
179
180 static PUB_CACHE_NIXOS_ORG_1: LazyLock<VerifyingKey> = LazyLock::new(|| {
182 ed25519_dalek::VerifyingKey::from_bytes(
183 BASE64
184 .decode(b"6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=")
185 .unwrap()[..]
186 .try_into()
187 .unwrap(),
188 )
189 .expect("embedded public key is valid")
190 });
191
192 #[rstest]
193 #[case::valid_cache_nixos_org_1(&PUB_CACHE_NIXOS_ORG_1, &"cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", FINGERPRINT, true)]
194 #[case::valid_test1(&PUB_CACHE_NIXOS_ORG_1, &"cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", FINGERPRINT, true)]
195 #[case::valid_cache_nixos_org_different_name(&PUB_CACHE_NIXOS_ORG_1, &"cache.nixos.org-2:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", FINGERPRINT, true)]
196 #[case::fail_invalid_cache_nixos_org_1_signature(&PUB_CACHE_NIXOS_ORG_1, &"cache.nixos.org-1:TsTTb000000000000000000000000ytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", FINGERPRINT, false)]
197 #[case::fail_valid_sig_but_wrong_fp_cache_nixos_org_1(&PUB_CACHE_NIXOS_ORG_1, &"cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==", &FINGERPRINT[0..5], false)]
198 fn verify_sigs(
199 #[case] verifying_key: &VerifyingKey,
200 #[case] sig_str: &'static str,
201 #[case] fp: &str,
202 #[case] expect_valid: bool,
203 ) {
204 let sig = Signature::<&str>::parse(sig_str).expect("must parse");
205 assert_eq!(expect_valid, sig.verify(fp.as_bytes(), verifying_key));
206 }
207
208 #[rstest]
209 #[case::wrong_length(
210 "cache.nixos.org-1:o1DTsjCz0PofLJ216P2RBuSulI8BAb6zHxWE4N+tzlcELk5Uk/GO2SCxWTRN5wJutLZZ+cHTMdWqOHF8"
211 )]
212 #[case::wrong_name_newline(
213 "test\n:u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
214 )]
215 #[case::wrong_name_space(
216 "test :u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
217 )]
218 #[case::empty_name(
219 ":u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
220 )]
221 #[case::b64_only(
222 "u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
223 )]
224 fn parse_fail(#[case] input: &'static str) {
225 Signature::<&str>::parse(input).expect_err("must fail");
226 }
227
228 #[cfg(feature = "serde")]
229 #[test]
230 fn serialize_deserialize() {
231 let signature_actual = Signature {
232 name: "cache.nixos.org-1",
233 bytes: hex!(
234 r#"4e c4 d3 6f 75 86 4d 92 a9 86 f6 1d 04 75 f0 a3
235 ac 1e 54 82 e6 4f 2b 54 8c b0 7e bd c5 fc f5 f3
236 a3 8d 18 9c 08 79 8a 03 84 42 3c c5 4b 92 3e 93
237 30 9e 06 31 7d c7 3d 55 91 74 3d 61 91 e2 99 05"#
238 ),
239 };
240 let signature_str_json = "\"cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==\"";
241
242 let serialized = serde_json::to_string(&signature_actual).expect("must serialize");
243 assert_eq!(signature_str_json, &serialized);
244
245 let deserialized: Signature<&str> =
246 serde_json::from_str(signature_str_json).expect("must deserialize");
247 assert_eq!(&signature_actual, &deserialized);
248 }
249
250 #[test]
252 fn signature_owned() {
253 let signature1 = Signature::<String>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
254 let signature2 = Signature::<smol_str::SmolStr>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
255 let signature3 = Signature::<&str>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
256
257 assert!(
258 signature1.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
259 "must verify"
260 );
261 assert!(
262 signature2.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
263 "must verify"
264 );
265 assert!(
266 signature3.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
267 "must verify"
268 );
269 }
270}