From a3194e9280da1d0d1d34cc3254dc135e982488a1 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Sun, 21 Jul 2024 22:55:36 +0200 Subject: feat(tvix/nix-compat): add SigningKey, NARInfo::add_signature This adds a generic `SigningKey` struct that can be used to sign NARInfos with signers. It also includes tooling to parse keypairs from bytes generated by Nix, returning a specialized ed25519_dalek variant. Change-Id: Ic9780c370939af54e7177c93cde3321adf189fc3 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12014 Reviewed-by: raitobezarius Autosubmit: flokli Tested-by: BuildkiteCI --- tvix/nix-compat/src/narinfo/mod.rs | 66 +++++++++++++++ tvix/nix-compat/src/narinfo/signing_keys.rs | 119 ++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 tvix/nix-compat/src/narinfo/signing_keys.rs (limited to 'tvix') diff --git a/tvix/nix-compat/src/narinfo/mod.rs b/tvix/nix-compat/src/narinfo/mod.rs index 1b657d0b1792..a77eba200f8d 100644 --- a/tvix/nix-compat/src/narinfo/mod.rs +++ b/tvix/nix-compat/src/narinfo/mod.rs @@ -28,10 +28,13 @@ use crate::{nixbase32, nixhash::CAHash, store_path::StorePathRef}; mod fingerprint; mod signature; +mod signing_keys; mod verifying_keys; pub use fingerprint::fingerprint; pub use signature::{Error as SignatureError, Signature}; +pub use signing_keys::parse_keypair; +pub use signing_keys::{Error as SigningKeyError, SigningKey}; pub use verifying_keys::{Error as VerifyingKeyError, VerifyingKey}; #[derive(Debug)] @@ -296,6 +299,21 @@ impl<'a> NarInfo<'a> { self.references.iter(), ) } + + /// Adds a signature, using the passed signer to sign. + /// This is generic over algo implementations / providers, + /// so users can bring their own signers. + pub fn add_signature(&mut self, signer: &'a SigningKey) + where + S: ed25519::signature::Signer, + { + // calculate the fingerprint to sign + let fp = self.fingerprint(); + + let sig = signer.sign(fp.as_bytes()); + + self.signatures.push(sig); + } } impl Display for NarInfo<'_> { @@ -390,6 +408,12 @@ pub enum Error { UnableToParseCA(String), } +#[cfg(test)] +const DUMMY_KEYPAIR: &str = "cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JEw=="; +#[cfg(test)] +const DUMMY_VERIFYING_KEY: &str = + "cache.example.com-1:yKUSiqP9yaMSduDmGtw8U9iVVd/Coyv9csB1rjHtiRM="; + #[cfg(test)] mod test { use hex_literal::hex; @@ -523,4 +547,46 @@ Sig: cache.nixos.org-1:HhaiY36Uk3XV1JGe9d9xHnzAapqJXprU1YZZzSzxE97jCuO5RR7vlG2kF parsed.nar_hash, ); } + + /// Adds a signature to a NARInfo, using key material parsed from DUMMY_KEYPAIR. + /// It then ensures signature verification with the parsed + /// DUMMY_VERIFYING_KEY succeeds. + #[test] + fn sign() { + let mut narinfo = NarInfo::parse( + r#"StorePath: /nix/store/0vpqfxbkx0ffrnhbws6g9qwhmliksz7f-perl-HTTP-Cookies-6.01 +URL: nar/0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi.nar.xz +Compression: xz +FileHash: sha256:0i5biw0g01514llhfswxy6xfav8lxxdq1xg6ik7hgsqbpw0f06yi +FileSize: 7120 +NarHash: sha256:0h1bm4sj1cnfkxgyhvgi8df1qavnnv94sd0v09wcrm971602shfg +NarSize: 22552 +References: +CA: fixed:r:sha1:1ak1ymbmsfx7z8kh09jzkr3a4dvkrfjw +"#, + ) + .expect("should parse"); + + let fp = narinfo.fingerprint(); + + // load our keypair from the fixtures + let (signing_key, _verifying_key) = + super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed"); + + // add signature + narinfo.add_signature(&signing_key); + + // ensure the signature is added + let new_sig = narinfo.signatures.last().unwrap(); + assert_eq!(signing_key.name(), new_sig.name()); + + // verify the new signature against the verifying key + let verifying_key = super::VerifyingKey::parse(super::DUMMY_VERIFYING_KEY) + .expect("parsing dummy verifying key"); + + assert!( + verifying_key.verify(&fp, new_sig), + "expect signature to be valid" + ); + } } diff --git a/tvix/nix-compat/src/narinfo/signing_keys.rs b/tvix/nix-compat/src/narinfo/signing_keys.rs new file mode 100644 index 000000000000..e33687bc88f8 --- /dev/null +++ b/tvix/nix-compat/src/narinfo/signing_keys.rs @@ -0,0 +1,119 @@ +//! This module provides tooling to parse private key (pairs) produced by Nix +//! and its +//! `nix-store --generate-binary-cache-key name path.secret path.pub` command. +//! It produces `ed25519_dalek` keys, but the `NarInfo::add_signature` function +//! is generic, allowing other signers. + +use data_encoding::BASE64; +use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH}; + +use super::{Signature, VerifyingKey}; + +pub struct SigningKey { + name: String, + signing_key: S, +} + +impl SigningKey +where + S: ed25519::signature::Signer, +{ + /// Constructs a singing key, using a name and a signing key. + pub fn new(name: String, signing_key: S) -> Self { + Self { name, signing_key } + } + + /// Signs a fingerprint using the internal signing key, returns the [Signature] + pub(crate) fn sign<'a>(&'a self, fp: &[u8]) -> Signature<'a> { + Signature::new(&self.name, self.signing_key.sign(fp).to_bytes()) + } + + pub fn name(&self) -> &str { + &self.name + } +} + +/// Parses a SigningKey / VerifyingKey from a byte slice in the format that Nix uses. +pub fn parse_keypair( + input: &str, +) -> Result<(SigningKey, VerifyingKey), 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())); + } + + const DECODED_BYTES_LEN: usize = SECRET_KEY_LENGTH + PUBLIC_KEY_LENGTH; + if bytes64.len() != BASE64.encode_len(DECODED_BYTES_LEN) { + return Err(Error::InvalidSigningKeyLen(bytes64.len())); + } + + let mut buf = [0; DECODED_BYTES_LEN + 2]; // 64 bytes + 2 bytes padding + let mut bytes = [0; DECODED_BYTES_LEN]; + match BASE64.decode_mut(bytes64.as_bytes(), &mut buf) { + Ok(len) if len == DECODED_BYTES_LEN => { + bytes.copy_from_slice(&buf[..DECODED_BYTES_LEN]); + } + Ok(_) => unreachable!(), + // keeping DecodePartial gets annoying lifetime-wise + Err(_) => return Err(Error::DecodeError(input.to_string())), + } + + let bytes_signing_key: [u8; SECRET_KEY_LENGTH] = { + let mut b = [0u8; SECRET_KEY_LENGTH]; + b.copy_from_slice(&bytes[0..SECRET_KEY_LENGTH]); + b + }; + let bytes_verifying_key: [u8; PUBLIC_KEY_LENGTH] = { + let mut b = [0u8; PUBLIC_KEY_LENGTH]; + b.copy_from_slice(&bytes[SECRET_KEY_LENGTH..]); + b + }; + + let signing_key = SigningKey::new( + name.to_string(), + ed25519_dalek::SigningKey::from_bytes(&bytes_signing_key), + ); + + let verifying_key = VerifyingKey::new( + name.to_string(), + ed25519_dalek::VerifyingKey::from_bytes(&bytes_verifying_key) + .map_err(Error::InvalidVerifyingKey)?, + ); + + Ok((signing_key, verifying_key)) +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid name: {0}")] + InvalidName(String), + #[error("Missing separator")] + MissingSeparator, + #[error("Invalid signing key len: {0}")] + InvalidSigningKeyLen(usize), + #[error("Unable to base64-decode signing key: {0}")] + DecodeError(String), + #[error("VerifyingKey error: {0}")] + InvalidVerifyingKey(ed25519_dalek::SignatureError), +} + +#[cfg(test)] +mod test { + use crate::narinfo::DUMMY_KEYPAIR; + #[test] + fn parse() { + let (_signing_key, _verifying_key) = + super::parse_keypair(DUMMY_KEYPAIR).expect("must succeed"); + } + + #[test] + fn parse_fail() { + assert!(super::parse_keypair("cache.example.com-1:cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err()); + assert!(super::parse_keypair("cache.example.com-1cCta2MEsRNuYCgWYyeRXLyfoFpKhQJKn8gLMeXWAb7vIpRKKo/3JoxJ24OYa3DxT2JVV38KjK/1ywHWuMe2JE").is_err()); + } +} -- cgit 1.4.1