about summary refs log tree commit diff
diff options
context:
space:
mode:
authorsinavir <tvix@sinavir.fr>2024-07-22T13·09+0200
committerclbot <clbot@tvl.fyi>2024-09-26T12·29+0000
commit9f36632509078d65d1229828237f64e876ffd48f (patch)
treec06753deaef09fde92c8fdfbcc5203eab9cdcfe4
parente4378f01433cc52300ac68d79406e0d5e6a05d50 (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.lock2
-rw-r--r--tvix/Cargo.nix8
-rw-r--r--tvix/store/Cargo.toml2
-rw-r--r--tvix/store/src/pathinfoservice/mod.rs6
-rw-r--r--tvix/store/src/pathinfoservice/signing_wrapper.rs204
-rw-r--r--tvix/store/src/pathinfoservice/tests/mod.rs3
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) {}