about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorian Klink <flokli@flokli.de>2024-07-21T20·55+0200
committerclbot <clbot@tvl.fyi>2024-07-21T21·41+0000
commita3194e9280da1d0d1d34cc3254dc135e982488a1 (patch)
treef5231da423ee6e1253e1c740f46abcdb6373aac8
parent05b4e805eeda9dc827e1464d5fd5f672b8daf26e (diff)
feat(tvix/nix-compat): add SigningKey, NARInfo::add_signature r/8395
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 <tvl@lahfa.xyz>
Autosubmit: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
-rw-r--r--tvix/nix-compat/src/narinfo/mod.rs66
-rw-r--r--tvix/nix-compat/src/narinfo/signing_keys.rs119
2 files changed, 185 insertions, 0 deletions
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<S>(&mut self, signer: &'a SigningKey<S>)
+    where
+        S: ed25519::signature::Signer<ed25519::Signature>,
+    {
+        // calculate the fingerprint to sign
+        let fp = self.fingerprint();
+
+        let sig = signer.sign(fp.as_bytes());
+
+        self.signatures.push(sig);
+    }
 }
 
 impl Display for NarInfo<'_> {
@@ -391,6 +409,12 @@ pub enum Error {
 }
 
 #[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;
     use lazy_static::lazy_static;
@@ -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<S> {
+    name: String,
+    signing_key: S,
+}
+
+impl<S> SigningKey<S>
+where
+    S: ed25519::signature::Signer<ed25519::Signature>,
+{
+    /// 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<ed25519_dalek::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());
+    }
+}