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("cache.nixos.org-1:o1DTsjCz0PofLJ216P2RBuSulI8BAb6zHxWE4N+tzlcELk5Uk/GO2SCxWTRN5wJutLZZ+cHTMdWqOHF8")]
206 #[case::wrong_name_newline("test\n:u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw==")]
207 #[case::wrong_name_space("test :u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw==")]
208 #[case::empty_name(
209 ":u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
210 )]
211 #[case::b64_only(
212 "u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw=="
213 )]
214 fn parse_fail(#[case] input: &'static str) {
215 Signature::<&str>::parse(input).expect_err("must fail");
216 }
217
218 #[test]
219 fn serialize_deserialize() {
220 let signature_actual = Signature {
221 name: "cache.nixos.org-1",
222 bytes: hex!(
223 r#"4e c4 d3 6f 75 86 4d 92 a9 86 f6 1d 04 75 f0 a3
224 ac 1e 54 82 e6 4f 2b 54 8c b0 7e bd c5 fc f5 f3
225 a3 8d 18 9c 08 79 8a 03 84 42 3c c5 4b 92 3e 93
226 30 9e 06 31 7d c7 3d 55 91 74 3d 61 91 e2 99 05"#
227 ),
228 };
229 let signature_str_json = "\"cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==\"";
230
231 let serialized = serde_json::to_string(&signature_actual).expect("must serialize");
232 assert_eq!(signature_str_json, &serialized);
233
234 let deserialized: Signature<&str> =
235 serde_json::from_str(signature_str_json).expect("must deserialize");
236 assert_eq!(&signature_actual, &deserialized);
237 }
238
239 #[test]
241 fn signature_owned() {
242 let signature1 = Signature::<String>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
243 let signature2 = Signature::<smol_str::SmolStr>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
244 let signature3 = Signature::<&str>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse");
245
246 assert!(
247 signature1.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
248 "must verify"
249 );
250 assert!(
251 signature2.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
252 "must verify"
253 );
254 assert!(
255 signature3.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1),
256 "must verify"
257 );
258 }
259}