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