nix_compat/narinfo/
signing_keys.rs

1//! This module provides tooling to parse private key (pairs) produced by Nix
2//! and its
3//! `nix-store --generate-binary-cache-key name path.secret path.pub` command.
4//! It produces `ed25519_dalek` keys, but the `NarInfo::add_signature` function
5//! is generic, allowing other signers.
6
7use data_encoding::BASE64;
8use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH};
9
10use super::{SignatureRef, VerifyingKey};
11
12pub struct SigningKey<S> {
13    name: String,
14    signing_key: S,
15}
16
17impl<S> SigningKey<S>
18where
19    S: ed25519::signature::Signer<ed25519::Signature>,
20{
21    /// Constructs a signing key, using a name and a signing key.
22    pub fn new(name: String, signing_key: S) -> Self {
23        Self { name, signing_key }
24    }
25
26    /// Signs a fingerprint using the internal signing key, returns the [SignatureRef]
27    pub(crate) fn sign<'a>(&'a self, fp: &[u8]) -> SignatureRef<'a> {
28        SignatureRef::new(&self.name, self.signing_key.sign(fp).to_bytes())
29    }
30
31    pub fn name(&self) -> &str {
32        &self.name
33    }
34}
35
36/// Parses a SigningKey / VerifyingKey from a byte slice in the format that Nix uses.
37pub fn parse_keypair(
38    input: &str,
39) -> Result<(SigningKey<ed25519_dalek::SigningKey>, VerifyingKey), Error> {
40    let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?;
41
42    if name.is_empty()
43        || !name
44            .chars()
45            .all(|c| char::is_alphanumeric(c) || c == '-' || c == '.')
46    {
47        return Err(Error::InvalidName(name.to_string()));
48    }
49
50    const DECODED_BYTES_LEN: usize = SECRET_KEY_LENGTH + PUBLIC_KEY_LENGTH;
51    if bytes64.len() != BASE64.encode_len(DECODED_BYTES_LEN) {
52        return Err(Error::InvalidSigningKeyLen(bytes64.len()));
53    }
54
55    let mut buf = [0; DECODED_BYTES_LEN + 2]; // 64 bytes + 2 bytes padding
56    let mut bytes = [0; DECODED_BYTES_LEN];
57    match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) {
58        Ok(len) if len == DECODED_BYTES_LEN => {
59            bytes.copy_from_slice(&buf[..DECODED_BYTES_LEN]);
60        }
61        Ok(_) => unreachable!(),
62        // keeping DecodePartial gets annoying lifetime-wise
63        Err(_) => return Err(Error::DecodeError(input.to_string())),
64    }
65
66    let bytes_signing_key: [u8; SECRET_KEY_LENGTH] = {
67        let mut b = [0u8; SECRET_KEY_LENGTH];
68        b.copy_from_slice(&bytes[0..SECRET_KEY_LENGTH]);
69        b
70    };
71    let bytes_verifying_key: [u8; PUBLIC_KEY_LENGTH] = {
72        let mut b = [0u8; PUBLIC_KEY_LENGTH];
73        b.copy_from_slice(&bytes[SECRET_KEY_LENGTH..]);
74        b
75    };
76
77    let signing_key = SigningKey::new(
78        name.to_string(),
79        ed25519_dalek::SigningKey::from_bytes(&bytes_signing_key),
80    );
81
82    let verifying_key = VerifyingKey::new(
83        name.to_string(),
84        ed25519_dalek::VerifyingKey::from_bytes(&bytes_verifying_key)
85            .map_err(Error::InvalidVerifyingKey)?,
86    );
87
88    Ok((signing_key, verifying_key))
89}
90
91#[derive(Debug, thiserror::Error)]
92pub enum Error {
93    #[error("Invalid name: {0}")]
94    InvalidName(String),
95    #[error("Missing separator")]
96    MissingSeparator,
97    #[error("Invalid signing key len: {0}")]
98    InvalidSigningKeyLen(usize),
99    #[error("Unable to base64-decode signing key: {0}")]
100    DecodeError(String),
101    #[error("VerifyingKey error: {0}")]
102    InvalidVerifyingKey(ed25519_dalek::SignatureError),
103}
104
105#[cfg(test)]
106mod test {
107    use crate::narinfo::DUMMY_KEYPAIR;
108    #[test]
109    fn parse() {
110        let (_signing_key, _verifying_key) =
111            super::parse_keypair(DUMMY_KEYPAIR).expect("must succeed");
112    }
113
114    #[test]
115    fn parse_fail() {
116        assert!(super::parse_keypair("cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err());
117        assert!(super::parse_keypair("cache.example.com-1cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err());
118    }
119}