nix_compat/narinfo/
signature.rs

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