feat(nix-compat/narinfo/signature): generalize name field

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>
This commit is contained in:
Florian Klink 2024-08-19 16:57:26 +03:00 committed by clbot
parent 7612cb4c31
commit a259613c76
9 changed files with 117 additions and 45 deletions

9
tvix/Cargo.lock generated
View file

@ -2357,6 +2357,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"smol_str",
"thiserror",
"tokio",
"tokio-test",
@ -2917,7 +2918,7 @@ checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1"
dependencies = [
"bytes",
"heck",
"itertools 0.10.5",
"itertools 0.13.0",
"log",
"multimap",
"once_cell",
@ -2950,7 +2951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.13.0",
"proc-macro2",
"quote",
"syn 2.0.48",
@ -3878,9 +3879,9 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "smol_str"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49"
checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead"
dependencies = [
"serde",
]

View file

@ -7480,6 +7480,10 @@ rec {
name = "serde_json";
packageId = "serde_json";
}
{
name = "smol_str";
packageId = "smol_str";
}
{
name = "tokio-test";
packageId = "tokio-test";
@ -9301,7 +9305,7 @@ rec {
}
{
name = "itertools";
packageId = "itertools 0.10.5";
packageId = "itertools 0.13.0";
usesDefaultFeatures = false;
features = [ "use_alloc" ];
}
@ -9421,7 +9425,7 @@ rec {
}
{
name = "itertools";
packageId = "itertools 0.10.5";
packageId = "itertools 0.13.0";
}
{
name = "proc-macro2";
@ -12491,9 +12495,9 @@ rec {
};
"smol_str" = rec {
crateName = "smol_str";
version = "0.2.1";
version = "0.2.2";
edition = "2018";
sha256 = "0jca0hyrwnv428q5gxhn2s8jsvrrkyrb0fyla9x37056mmimb176";
sha256 = "1bfylqf2vnqaglw58930vpxm2rfzji5gjp15a2c0kh8aj6v8ylyx";
authors = [
"Aleksey Kladov <aleksey.kladov@gmail.com>"
];

View file

@ -46,6 +46,7 @@ mimalloc = "0.1.43"
pretty_assertions = "1.4.0"
rstest = "0.19.0"
serde_json = "1.0"
smol_str = "0.2.2"
tokio-test = "0.4.3"
zstd = "^0.13.0"

View file

@ -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)

View file

@ -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: serde::Serializer,
S: Deref<Target = str>,
{
fn serialize<SR>(&self, serializer: SR) -> Result<SR::Ok, SR::Error>
where
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"
);
}
}

View file

@ -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 {

View file

@ -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));
}

View file

@ -221,7 +221,7 @@ impl PathInfo {
.signatures
.iter()
.map(|sig| {
nix_compat::narinfo::Signature::new(
nix_compat::narinfo::SignatureRef::new(
&sig.name,
// This shouldn't pass validation
sig.data[..].try_into().expect("invalid signature len"),

View file

@ -187,7 +187,7 @@ impl FrameBuilder {
assert!(entry.signatures.len() <= 1);
self.signature
.append_option(entry.signatures.get(0).map(|sig| {
assert_eq!(sig.name(), "cache.nixos.org-1");
assert_eq!(sig.name(), &"cache.nixos.org-1");
sig.bytes()
}));