diff options
author | sinavir <tvix@sinavir.fr> | 2024-07-22T13·09+0200 |
---|---|---|
committer | clbot <clbot@tvl.fyi> | 2024-09-26T12·29+0000 |
commit | 9f36632509078d65d1229828237f64e876ffd48f (patch) | |
tree | c06753deaef09fde92c8fdfbcc5203eab9cdcfe4 | |
parent | e4378f01433cc52300ac68d79406e0d5e6a05d50 (diff) |
feat(tvix/store): Add a signing PathInfoService r/8716
- Add a new PathInfoService implementation that wraps transparently around another except that it dynamically signs all the incoming path-infos with the provided signer. - Add a ServiceBuilder for this PathInfoService that provides a SigningPathInfoService with a keyfile signer Change-Id: I845ddfdf01d14c503c796b2b80c720dab98be091 Reviewed-on: https://cl.tvl.fyi/c/depot/+/12032 Reviewed-by: flokli <flokli@flokli.de> Autosubmit: sinavir <tvix@sinavir.fr> Tested-by: BuildkiteCI
-rw-r--r-- | tvix/Cargo.lock | 2 | ||||
-rw-r--r-- | tvix/Cargo.nix | 8 | ||||
-rw-r--r-- | tvix/store/Cargo.toml | 2 | ||||
-rw-r--r-- | tvix/store/src/pathinfoservice/mod.rs | 6 | ||||
-rw-r--r-- | tvix/store/src/pathinfoservice/signing_wrapper.rs | 204 | ||||
-rw-r--r-- | tvix/store/src/pathinfoservice/tests/mod.rs | 3 |
6 files changed, 225 insertions, 0 deletions
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock index 96562e9f2f7a..19adc7bfe2a2 100644 --- a/tvix/Cargo.lock +++ b/tvix/Cargo.lock @@ -4696,6 +4696,8 @@ dependencies = [ "clap", "count-write", "data-encoding", + "ed25519", + "ed25519-dalek", "futures", "hyper-util", "lazy_static", diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix index 2cc3aa64da37..77719d745ad7 100644 --- a/tvix/Cargo.nix +++ b/tvix/Cargo.nix @@ -15573,6 +15573,14 @@ rec { packageId = "data-encoding"; } { + name = "ed25519"; + packageId = "ed25519"; + } + { + name = "ed25519-dalek"; + packageId = "ed25519-dalek"; + } + { name = "futures"; packageId = "futures"; } diff --git a/tvix/store/Cargo.toml b/tvix/store/Cargo.toml index 9d54ad1ea760..328e2ae2e80b 100644 --- a/tvix/store/Cargo.toml +++ b/tvix/store/Cargo.toml @@ -13,6 +13,8 @@ bytes = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } count-write = { workspace = true } data-encoding = { workspace = true } +ed25519 = { workspace = true } +ed25519-dalek = { workspace = true } futures = { workspace = true } lazy_static = { workspace = true } nix-compat = { path = "../nix-compat", features = ["async"] } diff --git a/tvix/store/src/pathinfoservice/mod.rs b/tvix/store/src/pathinfoservice/mod.rs index d118a8af1e73..06ff74b519d8 100644 --- a/tvix/store/src/pathinfoservice/mod.rs +++ b/tvix/store/src/pathinfoservice/mod.rs @@ -5,6 +5,7 @@ mod lru; mod memory; mod nix_http; mod redb; +mod signing_wrapper; mod sled; #[cfg(any(feature = "fuse", feature = "virtiofs"))] @@ -30,8 +31,12 @@ pub use self::lru::{LruPathInfoService, LruPathInfoServiceConfig}; pub use self::memory::{MemoryPathInfoService, MemoryPathInfoServiceConfig}; pub use self::nix_http::{NixHTTPPathInfoService, NixHTTPPathInfoServiceConfig}; pub use self::redb::{RedbPathInfoService, RedbPathInfoServiceConfig}; +pub use self::signing_wrapper::{KeyFileSigningPathInfoServiceConfig, SigningPathInfoService}; pub use self::sled::{SledPathInfoService, SledPathInfoServiceConfig}; +#[cfg(test)] +pub(crate) use self::signing_wrapper::test_signing_service; + #[cfg(feature = "cloud")] mod bigtable; #[cfg(feature = "cloud")] @@ -91,6 +96,7 @@ pub(crate) fn register_pathinfo_services(reg: &mut Registry) { reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, NixHTTPPathInfoServiceConfig>("nix"); reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, SledPathInfoServiceConfig>("sled"); reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, RedbPathInfoServiceConfig>("redb"); + reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, KeyFileSigningPathInfoServiceConfig>("keyfile-signing"); #[cfg(feature = "cloud")] { reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, BigtableParameters>( diff --git a/tvix/store/src/pathinfoservice/signing_wrapper.rs b/tvix/store/src/pathinfoservice/signing_wrapper.rs new file mode 100644 index 000000000000..7f754ec49849 --- /dev/null +++ b/tvix/store/src/pathinfoservice/signing_wrapper.rs @@ -0,0 +1,204 @@ +//! This module provides a [PathInfoService] implementation that signs narinfos + +use super::PathInfoService; +use crate::proto::PathInfo; +use futures::stream::BoxStream; +use std::path::PathBuf; +use std::sync::Arc; +use tonic::async_trait; + +use tvix_castore::composition::{CompositionContext, ServiceBuilder}; + +use tvix_castore::Error; + +use nix_compat::narinfo::{parse_keypair, SigningKey}; +use nix_compat::nixbase32; +use tracing::{instrument, warn}; + +#[cfg(test)] +use super::MemoryPathInfoService; + +/// PathInfoService that wraps around an inner [PathInfoService] and when put is called it extracts +/// the underlying narinfo and signs it using a [SigningKey]. For the moment only the +/// [ed25519::signature::Signer<ed25519::Signature>] is available using a keyfile (see +/// [KeyFileSigningPathInfoServiceConfig] for more informations). However the implementation is +/// generic (see [nix_compat::narinfo::SigningKey] documentation). +/// +/// The [PathInfo] with the added signature is then put into the inner [PathInfoService]. +/// +/// The service signs the [PathInfo] **only if it has a narinfo attribute** +pub struct SigningPathInfoService<T, S> { + /// The inner [PathInfoService] + inner: T, + /// The key to sign narinfos + signing_key: Arc<SigningKey<S>>, +} + +impl<T, S> SigningPathInfoService<T, S> { + pub fn new(inner: T, signing_key: Arc<SigningKey<S>>) -> Self { + Self { inner, signing_key } + } +} + +#[async_trait] +impl<T, S> PathInfoService for SigningPathInfoService<T, S> +where + T: PathInfoService, + S: ed25519::signature::Signer<ed25519::Signature> + Sync + Send, +{ + #[instrument(level = "trace", skip_all, fields(path_info.digest = nixbase32::encode(&digest)))] + async fn get(&self, digest: [u8; 20]) -> Result<Option<PathInfo>, Error> { + self.inner.get(digest).await + } + + async fn put(&self, path_info: PathInfo) -> Result<PathInfo, Error> { + let store_path = path_info.validate().map_err(|e| { + warn!(err=%e, "invalid PathInfo"); + Error::StorageError(e.to_string()) + })?; + let root_node = path_info.node.clone(); + // If we have narinfo then sign it, else passthrough to the upper pathinfoservice + let path_info_to_put = match path_info.to_narinfo(store_path.as_ref()) { + Some(mut nar_info) => { + nar_info.add_signature(self.signing_key.as_ref()); + let mut signed_path_info = PathInfo::from(&nar_info); + signed_path_info.node = root_node; + signed_path_info + } + None => path_info, + }; + self.inner.put(path_info_to_put).await + } + + fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>> { + self.inner.list() + } +} + +/// [ServiceBuilder] implementation that builds a [SigningPathInfoService] that signs narinfos using +/// a keyfile. The keyfile is parsed using [parse_keypair], the expected format is the nix one +/// (`nix-store --generate-binary-cache-key` for more informations). +#[derive(serde::Deserialize)] +pub struct KeyFileSigningPathInfoServiceConfig { + /// Inner [PathInfoService], will be resolved using a [CompositionContext]. + pub inner: String, + /// Path to the keyfile in the nix format. It will be accessed once when building the service + pub keyfile: PathBuf, +} + +impl TryFrom<url::Url> for KeyFileSigningPathInfoServiceConfig { + type Error = Box<dyn std::error::Error + Send + Sync>; + fn try_from(_url: url::Url) -> Result<Self, Self::Error> { + Err(Error::StorageError( + "Instantiating a SigningPathInfoService from a url is not supported".into(), + ) + .into()) + } +} + +#[async_trait] +impl ServiceBuilder for KeyFileSigningPathInfoServiceConfig { + type Output = dyn PathInfoService; + async fn build<'a>( + &'a self, + _instance_name: &str, + context: &CompositionContext, + ) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> { + let inner = context.resolve(self.inner.clone()).await?; + let signing_key = Arc::new( + parse_keypair(tokio::fs::read_to_string(&self.keyfile).await?.trim()) + .map_err(|e| Error::StorageError(e.to_string()))? + .0, + ); + Ok(Arc::new(SigningPathInfoService { inner, signing_key })) + } +} + +#[cfg(test)] +pub(crate) fn test_signing_service() -> Arc<dyn PathInfoService> { + let memory_svc: Arc<dyn PathInfoService> = Arc::new(MemoryPathInfoService::default()); + Arc::new(SigningPathInfoService { + inner: memory_svc, + signing_key: Arc::new( + parse_keypair(DUMMY_KEYPAIR) + .expect("DUMMY_KEYPAIR to be valid") + .0, + ), + }) +} + +#[cfg(test)] +pub const DUMMY_KEYPAIR: &str = "do.not.use:sGPzxuK5WvWPraytx+6sjtaff866sYlfvErE6x0hFEhy5eqe7OVZ8ZMqZ/ME/HaRdKGNGvJkyGKXYTaeA6lR3A=="; +#[cfg(test)] +pub const DUMMY_VERIFYING_KEY: &str = "do.not.use:cuXqnuzlWfGTKmfzBPx2kXShjRryZMhil2E2ngOpUdw="; + +#[cfg(test)] +mod test { + use crate::{ + pathinfoservice::PathInfoService, + proto::PathInfo, + tests::fixtures::{DUMMY_PATH, PATH_INFO_WITH_NARINFO}, + }; + use nix_compat::narinfo::VerifyingKey; + + use lazy_static::lazy_static; + use nix_compat::store_path::StorePath; + + lazy_static! { + static ref PATHINFO_1: PathInfo = PATH_INFO_WITH_NARINFO.clone(); + static ref PATHINFO_1_DIGEST: [u8; 20] = [0; 20]; + } + + #[tokio::test] + async fn put_and_verify_signature() { + let svc = super::test_signing_service(); + + // pathinfo_1 should not be there ... + assert!(svc + .get(*PATHINFO_1_DIGEST) + .await + .expect("no error") + .is_none()); + + // ... and not be signed + assert!(PATHINFO_1.narinfo.clone().unwrap().signatures.is_empty()); + + // insert it + svc.put(PATHINFO_1.clone()).await.expect("no error"); + + // now it should be there ... + let signed = svc + .get(*PATHINFO_1_DIGEST) + .await + .expect("no error") + .unwrap(); + + // and signed + let narinfo = signed + .to_narinfo( + StorePath::from_bytes(DUMMY_PATH.as_bytes()).expect("DUMMY_PATH to be parsed"), + ) + .expect("no error"); + let fp = narinfo.fingerprint(); + + // load our keypair from the fixtures + let (signing_key, _verifying_key) = + super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed"); + + // ensure the signature is added + let new_sig = narinfo + .signatures + .last() + .expect("The retrieved narinfo to be signed"); + assert_eq!(signing_key.name(), *new_sig.name()); + + // verify the new signature against the verifying key + let verifying_key = + 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/store/src/pathinfoservice/tests/mod.rs b/tvix/store/src/pathinfoservice/tests/mod.rs index 777588e9beda..26e3a1f311e4 100644 --- a/tvix/store/src/pathinfoservice/tests/mod.rs +++ b/tvix/store/src/pathinfoservice/tests/mod.rs @@ -14,6 +14,8 @@ use crate::proto::PathInfo; use crate::tests::fixtures::DUMMY_PATH_DIGEST; use tvix_castore::proto as castorepb; +use crate::pathinfoservice::test_signing_service; + mod utils; pub use self::utils::make_grpc_path_info_service_client; @@ -29,6 +31,7 @@ use self::utils::make_bigtable_path_info_service; })] #[case::sled(SledPathInfoService::new_temporary().unwrap())] #[case::redb(RedbPathInfoService::new_temporary().unwrap())] +#[case::signing(test_signing_service())] #[cfg_attr(all(feature = "cloud",feature="integration"), case::bigtable(make_bigtable_path_info_service().await))] pub fn path_info_services(#[case] svc: impl PathInfoService) {} |