diff options
author | Florian Klink <flokli@flokli.de> | 2024-08-19T13·57+0300 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2024-08-19T19·46+0000 |
commit | a259613c76a17f7a6719c18809e054605ef2cfa2 (patch) | |
tree | 379659675b04623a026e6f40af44c35b14a84d95 /tvix/nix-compat/src | |
parent | 7612cb4c31fadd7ccaa7672cf551b9d21d7884b4 (diff) |
feat(nix-compat/narinfo/signature): generalize name field r/8540
Requiring `name` to be a `&str` means it'll get annoying to pass around `Signature`, but being able to pass them around in an owned fashion is kinda a requirement for a stronger typed `PathInfo` struct, where we want to have full ownership. Rework the `Signature` struct to become generic over the type of the `name` field. This means, it becomes possible to have owned versions of it. We don't want to impose `String` or `SmolStr` for example, but want to leave it up to the nix-compat user to decide. Provide a type alias for the existing `&str` variant (`SignatureRef`), and use it where we previously used the non-generic `Signature` one. Add some tests to ensure it's possible to *use* `Signature` with both `String` and `SmolStr` (but only pull in `smol_str` as dev dependency for the tests). Also, add some more docstrings, these were a bit sparse. Change-Id: I3f75691498c6bda9cd072d2d9dac83c4f6c57287 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12253 Autosubmit: flokli <flokli@flokli.de> Tested-by: BuildkiteCI Reviewed-by: raitobezarius <tvl@lahfa.xyz>
Diffstat (limited to 'tvix/nix-compat/src')
-rw-r--r-- | tvix/nix-compat/src/narinfo/mod.rs | 8 | ||||
-rw-r--r-- | tvix/nix-compat/src/narinfo/signature.rs | 110 | ||||
-rw-r--r-- | tvix/nix-compat/src/narinfo/signing_keys.rs | 8 | ||||
-rw-r--r-- | tvix/nix-compat/src/narinfo/verifying_keys.rs | 10 |
4 files changed, 101 insertions, 35 deletions
diff --git a/tvix/nix-compat/src/narinfo/mod.rs b/tvix/nix-compat/src/narinfo/mod.rs index a77eba200f8d..21aecf80b5a2 100644 --- a/tvix/nix-compat/src/narinfo/mod.rs +++ b/tvix/nix-compat/src/narinfo/mod.rs @@ -32,7 +32,7 @@ mod signing_keys; mod verifying_keys; pub use fingerprint::fingerprint; -pub use signature::{Error as SignatureError, Signature}; +pub use signature::{Error as SignatureError, Signature, SignatureRef}; pub use signing_keys::parse_keypair; pub use signing_keys::{Error as SigningKeyError, SigningKey}; pub use verifying_keys::{Error as VerifyingKeyError, VerifyingKey}; @@ -51,7 +51,7 @@ pub struct NarInfo<'a> { pub references: Vec<StorePathRef<'a>>, // authenticity /// Ed25519 signature over the path fingerprint - pub signatures: Vec<Signature<'a>>, + pub signatures: Vec<SignatureRef<'a>>, /// Content address (for content-defined paths) pub ca: Option<CAHash>, // derivation metadata @@ -246,7 +246,7 @@ impl<'a> NarInfo<'a> { }; } "Sig" => { - let val = Signature::parse(val) + let val = SignatureRef::parse(val) .map_err(|e| Error::UnableToParseSignature(signatures.len(), e))?; signatures.push(val); @@ -578,7 +578,7 @@ CA: fixed:r:sha1:1ak1ymbmsfx7z8kh09jzkr3a4dvkrfjw // ensure the signature is added let new_sig = narinfo.signatures.last().unwrap(); - assert_eq!(signing_key.name(), new_sig.name()); + 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) diff --git a/tvix/nix-compat/src/narinfo/signature.rs b/tvix/nix-compat/src/narinfo/signature.rs index e5567f44109a..33c49128c2d5 100644 --- a/tvix/nix-compat/src/narinfo/signature.rs +++ b/tvix/nix-compat/src/narinfo/signature.rs @@ -1,4 +1,7 @@ -use std::fmt::{self, Display}; +use std::{ + fmt::{self, Display}, + ops::Deref, +}; use data_encoding::BASE64; use serde::{Deserialize, Serialize}; @@ -6,17 +9,36 @@ use serde::{Deserialize, Serialize}; const SIGNATURE_LENGTH: usize = std::mem::size_of::<ed25519::SignatureBytes>(); #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Signature<'a> { - name: &'a str, +pub struct Signature<S> { + name: S, bytes: ed25519::SignatureBytes, } -impl<'a> Signature<'a> { - pub fn new(name: &'a str, bytes: ed25519::SignatureBytes) -> Self { +/// Type alias of a [Signature] using a `&str` as `name` field. +pub type SignatureRef<'a> = Signature<&'a str>; + +/// Represents the signatures that Nix emits. +/// It consists of a name (an identifier for a public key), and an ed25519 +/// signature (64 bytes). +/// It is generic over the string type that's used for the name, and there's +/// [SignatureRef] as a type alias for one containing &str. +impl<S> Signature<S> +where + S: Deref<Target = str>, +{ + /// Constructs a new [Signature] from a name and public key. + pub fn new(name: S, bytes: ed25519::SignatureBytes) -> Self { Self { name, bytes } } - pub fn parse(input: &'a str) -> Result<Self, Error> { + /// Parses a [Signature] from a string containing the name, a colon, and 64 + /// base64-encoded bytes (plus padding). + /// These strings are commonly seen in the `Signature:` field of a NARInfo + /// file. + pub fn parse<'a>(input: &'a str) -> Result<Self, Error> + where + S: From<&'a str>, + { let (name, bytes64) = input.split_once(':').ok_or(Error::MissingSeparator)?; if name.is_empty() @@ -40,13 +62,18 @@ impl<'a> Signature<'a> { Err(_) => return Err(Error::DecodeError(input.to_string())), } - Ok(Signature { name, bytes }) + Ok(Self { + name: name.into(), + bytes, + }) } - pub fn name(&self) -> &'a str { - self.name + /// Returns the name field of the signature. + pub fn name(&self) -> &S { + &self.name } + /// Returns the 64 bytes of signatures. pub fn bytes(&self) -> &ed25519::SignatureBytes { &self.bytes } @@ -57,9 +84,21 @@ impl<'a> Signature<'a> { verifying_key.verify_strict(fingerprint, &signature).is_ok() } + + /// Constructs a [SignatureRef] from this signature. + pub fn as_ref(&self) -> SignatureRef<'_> { + SignatureRef { + name: self.name.deref(), + bytes: self.bytes, + } + } } -impl<'de: 'a, 'a> Deserialize<'de> for Signature<'a> { +impl<'a, 'de, S> Deserialize<'de> for Signature<S> +where + S: Deref<Target = str> + From<&'a str>, + 'de: 'a, +{ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: serde::Deserializer<'de>, @@ -71,10 +110,13 @@ impl<'de: 'a, 'a> Deserialize<'de> for Signature<'a> { } } -impl<'a> Serialize for Signature<'a> { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> +impl<S: Display> Serialize for Signature<S> +where + S: Deref<Target = str>, +{ + fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error> where - S: serde::Serializer, + SR: serde::Serializer, { let string: String = self.to_string(); @@ -82,6 +124,15 @@ impl<'a> Serialize for Signature<'a> { } } +impl<S> Display for Signature<S> +where + S: Display, +{ + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes)) + } +} + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Invalid name: {0}")] @@ -94,12 +145,6 @@ pub enum Error { DecodeError(String), } -impl Display for Signature<'_> { - fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { - write!(w, "{}:{}", self.name, BASE64.encode(&self.bytes)) - } -} - #[cfg(test)] mod test { use data_encoding::BASE64; @@ -144,7 +189,7 @@ mod test { #[case] fp: &str, #[case] expect_valid: bool, ) { - let sig = Signature::parse(sig_str).expect("must parse"); + let sig = Signature::<&str>::parse(sig_str).expect("must parse"); assert_eq!(expect_valid, sig.verify(fp.as_bytes(), verifying_key)); } @@ -159,7 +204,7 @@ mod test { "u01BybwQhyI5H1bW1EIWXssMDhDDIvXOG5uh8Qzgdyjz6U1qg6DHhMAvXZOUStIj6X5t4/ufFgR8i3fjf0bMAw==" )] fn parse_fail(#[case] input: &'static str) { - Signature::parse(input).expect_err("must fail"); + Signature::<&str>::parse(input).expect_err("must fail"); } #[test] @@ -178,8 +223,29 @@ mod test { let serialized = serde_json::to_string(&signature_actual).expect("must serialize"); assert_eq!(signature_str_json, &serialized); - let deserialized: Signature<'_> = + let deserialized: Signature<&str> = serde_json::from_str(signature_str_json).expect("must deserialize"); assert_eq!(&signature_actual, &deserialized); } + + /// Construct a [Signature], using different String types for the name field. + #[test] + fn signature_owned() { + let signature1 = Signature::<String>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + let signature2 = Signature::<smol_str::SmolStr>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + let signature3 = Signature::<&str>::parse("cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ==").expect("must parse"); + + assert!( + signature1.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + assert!( + signature2.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + assert!( + signature3.verify(FINGERPRINT.as_bytes(), &PUB_CACHE_NIXOS_ORG_1), + "must verify" + ); + } } diff --git a/tvix/nix-compat/src/narinfo/signing_keys.rs b/tvix/nix-compat/src/narinfo/signing_keys.rs index e33687bc88f8..cf513b7ba475 100644 --- a/tvix/nix-compat/src/narinfo/signing_keys.rs +++ b/tvix/nix-compat/src/narinfo/signing_keys.rs @@ -7,7 +7,7 @@ use data_encoding::BASE64; use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH}; -use super::{Signature, VerifyingKey}; +use super::{SignatureRef, VerifyingKey}; pub struct SigningKey<S> { name: String, @@ -23,9 +23,9 @@ where 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()) + /// Signs a fingerprint using the internal signing key, returns the [SignatureRef] + pub(crate) fn sign<'a>(&'a self, fp: &[u8]) -> SignatureRef<'a> { + SignatureRef::new(&self.name, self.signing_key.sign(fp).to_bytes()) } pub fn name(&self) -> &str { diff --git a/tvix/nix-compat/src/narinfo/verifying_keys.rs b/tvix/nix-compat/src/narinfo/verifying_keys.rs index b8ed2b9531c1..67ef2e3a459c 100644 --- a/tvix/nix-compat/src/narinfo/verifying_keys.rs +++ b/tvix/nix-compat/src/narinfo/verifying_keys.rs @@ -6,7 +6,7 @@ use std::fmt::Display; use data_encoding::BASE64; use ed25519_dalek::PUBLIC_KEY_LENGTH; -use super::Signature; +use super::SignatureRef; /// This represents a ed25519 public key and "name". /// These are normally passed in the `trusted-public-keys` Nix config option, @@ -69,8 +69,8 @@ impl VerifyingKey { /// which means the name in the signature has to match, /// and the signature bytes themselves need to be a valid signature made by /// the signing key identified by [Self::verifying key]. - pub fn verify(&self, fingerprint: &str, signature: &Signature) -> bool { - if self.name() != signature.name() { + pub fn verify(&self, fingerprint: &str, signature: &SignatureRef<'_>) -> bool { + if self.name() != *signature.name() { return false; } @@ -109,7 +109,7 @@ mod test { use ed25519_dalek::PUBLIC_KEY_LENGTH; use rstest::rstest; - use crate::narinfo::Signature; + use crate::narinfo::SignatureRef; use super::VerifyingKey; 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"; @@ -146,7 +146,7 @@ mod test { #[case] expected: bool, ) { let pubkey = VerifyingKey::parse(pubkey_str).expect("must parse"); - let signature = Signature::parse(signature_str).expect("must parse"); + let signature = SignatureRef::parse(signature_str).expect("must parse"); assert_eq!(expected, pubkey.verify(fingerprint, &signature)); } |