feat(tvix/store): Add a signing PathInfoService
- 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
This commit is contained in:
parent
1f5a20736a
commit
3b16bacb8c
6 changed files with 216 additions and 0 deletions
2
tvix/Cargo.lock
generated
2
tvix/Cargo.lock
generated
|
@ -4695,6 +4695,8 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"count-write",
|
"count-write",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
|
"ed25519",
|
||||||
|
"ed25519-dalek",
|
||||||
"futures",
|
"futures",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|
|
@ -15568,6 +15568,14 @@ rec {
|
||||||
name = "data-encoding";
|
name = "data-encoding";
|
||||||
packageId = "data-encoding";
|
packageId = "data-encoding";
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
name = "ed25519";
|
||||||
|
packageId = "ed25519";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "ed25519-dalek";
|
||||||
|
packageId = "ed25519-dalek";
|
||||||
|
}
|
||||||
{
|
{
|
||||||
name = "futures";
|
name = "futures";
|
||||||
packageId = "futures";
|
packageId = "futures";
|
||||||
|
|
|
@ -13,6 +13,8 @@ bytes = { workspace = true }
|
||||||
clap = { workspace = true, features = ["derive", "env"] }
|
clap = { workspace = true, features = ["derive", "env"] }
|
||||||
count-write = { workspace = true }
|
count-write = { workspace = true }
|
||||||
data-encoding = { workspace = true }
|
data-encoding = { workspace = true }
|
||||||
|
ed25519 = { workspace = true }
|
||||||
|
ed25519-dalek = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
nix-compat = { path = "../nix-compat", features = ["async"] }
|
nix-compat = { path = "../nix-compat", features = ["async"] }
|
||||||
|
|
|
@ -5,6 +5,7 @@ mod lru;
|
||||||
mod memory;
|
mod memory;
|
||||||
mod nix_http;
|
mod nix_http;
|
||||||
mod redb;
|
mod redb;
|
||||||
|
mod signing_wrapper;
|
||||||
mod sled;
|
mod sled;
|
||||||
|
|
||||||
#[cfg(any(feature = "fuse", feature = "virtiofs"))]
|
#[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::memory::{MemoryPathInfoService, MemoryPathInfoServiceConfig};
|
||||||
pub use self::nix_http::{NixHTTPPathInfoService, NixHTTPPathInfoServiceConfig};
|
pub use self::nix_http::{NixHTTPPathInfoService, NixHTTPPathInfoServiceConfig};
|
||||||
pub use self::redb::{RedbPathInfoService, RedbPathInfoServiceConfig};
|
pub use self::redb::{RedbPathInfoService, RedbPathInfoServiceConfig};
|
||||||
|
pub use self::signing_wrapper::{KeyFileSigningPathInfoServiceConfig, SigningPathInfoService};
|
||||||
pub use self::sled::{SledPathInfoService, SledPathInfoServiceConfig};
|
pub use self::sled::{SledPathInfoService, SledPathInfoServiceConfig};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) use self::signing_wrapper::test_signing_service;
|
||||||
|
|
||||||
#[cfg(feature = "cloud")]
|
#[cfg(feature = "cloud")]
|
||||||
mod bigtable;
|
mod bigtable;
|
||||||
#[cfg(feature = "cloud")]
|
#[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>>, NixHTTPPathInfoServiceConfig>("nix");
|
||||||
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, SledPathInfoServiceConfig>("sled");
|
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>>, RedbPathInfoServiceConfig>("redb");
|
||||||
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, KeyFileSigningPathInfoServiceConfig>("keyfile-signing");
|
||||||
#[cfg(feature = "cloud")]
|
#[cfg(feature = "cloud")]
|
||||||
{
|
{
|
||||||
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, BigtableParameters>(
|
reg.register::<Box<dyn ServiceBuilder<Output = dyn PathInfoService>>, BigtableParameters>(
|
||||||
|
|
195
tvix/store/src/pathinfoservice/signing_wrapper.rs
Normal file
195
tvix/store/src/pathinfoservice/signing_wrapper.rs
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
//! 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;
|
||||||
|
|
||||||
|
/// Wraps around an inner [PathInfoService].
|
||||||
|
/// When put is called, extracts the underlying narinfo and signs it using a [SigningKey].
|
||||||
|
/// The reconstructed [PathInfo] is then put into the inner [PathInfoService].
|
||||||
|
pub struct SigningPathInfoService<T, S> {
|
||||||
|
/// The inner [PathInfoService]
|
||||||
|
inner: T,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [ServiceConfig] 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 {
|
||||||
|
pub inner: String,
|
||||||
|
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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,8 @@ use crate::proto::PathInfo;
|
||||||
use crate::tests::fixtures::DUMMY_PATH_DIGEST;
|
use crate::tests::fixtures::DUMMY_PATH_DIGEST;
|
||||||
use tvix_castore::proto as castorepb;
|
use tvix_castore::proto as castorepb;
|
||||||
|
|
||||||
|
use crate::pathinfoservice::test_signing_service;
|
||||||
|
|
||||||
mod utils;
|
mod utils;
|
||||||
pub use self::utils::make_grpc_path_info_service_client;
|
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::sled(SledPathInfoService::new_temporary().unwrap())]
|
||||||
#[case::redb(RedbPathInfoService::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))]
|
#[cfg_attr(all(feature = "cloud",feature="integration"), case::bigtable(make_bigtable_path_info_service().await))]
|
||||||
pub fn path_info_services(#[case] svc: impl PathInfoService) {}
|
pub fn path_info_services(#[case] svc: impl PathInfoService) {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue