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