nix_compat/narinfo/
signature.rs

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
18/// Type alias of a [Signature] using a `&str` as `name` field.
19pub type SignatureRef<'a> = Signature<&'a str>;
20
21/// Represents the signatures that Nix emits.
22/// It consists of a name (an identifier for a public key), and an ed25519
23/// signature (64 bytes).
24/// It is generic over the string type that's used for the name, and there's
25/// [SignatureRef] as a type alias for one containing &str.
26impl<S> Signature<S>
27where
28    S: Deref<Target = str>,
29{
30    /// Constructs a new [Signature] from a name and public key.
31    pub fn new(name: S, bytes: ed25519::SignatureBytes) -> Self {
32        Self { name, bytes }
33    }
34
35    /// Parses a [Signature] from a string containing the name, a colon, and 64
36    /// base64-encoded bytes (plus padding).
37    /// These strings are commonly seen in the `Signature:` field of a NARInfo
38    /// file.
39    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            // keeping DecodePartial gets annoying lifetime-wise
63            Err(_) => return Err(Error::DecodeError(input.to_string())),
64        }
65
66        Ok(Self {
67            name: name.into(),
68            bytes,
69        })
70    }
71
72    /// Returns the name field of the signature.
73    pub fn name(&self) -> &S {
74        &self.name
75    }
76
77    /// Returns the 64 bytes of signatures.
78    pub fn bytes(&self) -> &ed25519::SignatureBytes {
79        &self.bytes
80    }
81
82    /// For a given fingerprint and ed25519 verifying key, ensure if the signature is valid.
83    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    /// Constructs a [SignatureRef] from this signature.
90    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    /// The signing key labelled as `cache.nixos.org-1`,
181    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    /// Construct a [Signature], using different String types for the name field.
251    #[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}