about summary refs log tree commit diff
path: root/tvix/nix-compat/src/narinfo/public_keys.rs
blob: 90759110827eb8c65e0d79c8ff6bd385b5a919d7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! This module defines data structures and parsers for the public key format
//! used inside Nix to verify signatures on .narinfo files.

use std::fmt::Display;

use data_encoding::BASE64;
use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH};

/// This represents a ed25519 public key and "name".
/// These are normally passed in the `trusted-public-keys` Nix config option,
/// and consist of a name and base64-encoded ed25519 pubkey, separated by a `:`.
#[derive(Debug)]
pub struct PubKey {
    name: String,
    verifying_key: VerifyingKey,
}

impl PubKey {
    pub fn new(name: String, verifying_key: VerifyingKey) -> Self {
        Self {
            name,
            verifying_key,
        }
    }

    pub fn parse(input: &str) -> Result<Self, Error> {
        let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?;

        if name.is_empty()
            || !name
                .chars()
                .all(|c| char::is_alphanumeric(c) || c == '-' || c == '.')
        {
            return Err(Error::InvalidName(name.to_string()));
        }

        if bytes64.len() != BASE64.encode_len(PUBLIC_KEY_LENGTH) {
            return Err(Error::InvalidPubKeyLen(bytes64.len()));
        }

        let mut buf = [0; PUBLIC_KEY_LENGTH + 1];
        let mut bytes = [0; PUBLIC_KEY_LENGTH];
        match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) {
            Ok(PUBLIC_KEY_LENGTH) => {
                bytes.copy_from_slice(&buf[..PUBLIC_KEY_LENGTH]);
            }
            Ok(_) => unreachable!(),
            // keeping DecodePartial gets annoying lifetime-wise
            Err(_) => return Err(Error::DecodeError(input.to_string())),
        }

        let verifying_key = VerifyingKey::from_bytes(&bytes).map_err(Error::InvalidVerifyingKey)?;

        Ok(Self {
            name: name.to_string(),
            verifying_key,
        })
    }

    pub fn name(&self) -> &str {
        &self.name
    }
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Invalid name: {0}")]
    InvalidName(String),
    #[error("Missing separator")]
    MissingSeparator,
    #[error("Invalid pubkey len: {0}")]
    InvalidPubKeyLen(usize),
    #[error("VerifyingKey error: {0}")]
    InvalidVerifyingKey(ed25519_dalek::SignatureError),
    #[error("Unable to base64-decode pubkey: {0}")]
    DecodeError(String),
}

impl Display for PubKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{}",
            self.name,
            BASE64.encode(self.verifying_key.as_bytes())
        )
    }
}

#[cfg(test)]
mod test {
    use data_encoding::BASE64;
    use ed25519_dalek::PUBLIC_KEY_LENGTH;
    use test_case::test_case;

    use super::PubKey;

    #[test_case("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", "cache.nixos.org-1", BASE64.decode(b"6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=").unwrap()[..].try_into().unwrap(); "cache.nixos.org")]
    #[test_case("cheesecake:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=", "cheesecake", BASE64.decode(b"6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=").unwrap()[..].try_into().unwrap(); "cache.nixos.org different name")]
    #[test_case("test1:tLAEn+EeaBUJYqEpTd2yeerr7Ic6+0vWe+aXL/vYUpE=", "test1", BASE64.decode(b"tLAEn+EeaBUJYqEpTd2yeerr7Ic6+0vWe+aXL/vYUpE=").unwrap()[..].try_into().unwrap(); "test-1")]
    fn parse(
        input: &'static str,
        exp_name: &'static str,
        exp_verifying_key_bytes: &[u8; PUBLIC_KEY_LENGTH],
    ) {
        let pubkey = PubKey::parse(input).expect("must parse");
        assert_eq!(exp_name, pubkey.name());
        assert_eq!(exp_verifying_key_bytes, pubkey.verifying_key.as_bytes());
    }

    #[test_case("6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="; "empty name")]
    #[test_case("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY"; "missing padding")]
    #[test_case("cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDS"; "wrong length")]
    fn parse_fail(input: &'static str) {
        PubKey::parse(input).expect_err("must fail");
    }
}