Skip to main content

nix_compat/narinfo/
signature.rs

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