snix_store/pathinfoservice/
signing_wrapper.rs

1//! This module provides a [PathInfoService] implementation that signs narinfos
2
3use super::{PathInfo, PathInfoService};
4use futures::stream::BoxStream;
5use std::path::PathBuf;
6use std::sync::Arc;
7use tonic::async_trait;
8
9use snix_castore::composition::{CompositionContext, ServiceBuilder};
10
11use snix_castore::Error;
12
13use nix_compat::narinfo::{Signature, SigningKey, parse_keypair};
14use nix_compat::nixbase32;
15use tracing::instrument;
16
17#[cfg(test)]
18use super::MemoryPathInfoService;
19
20/// PathInfoService that wraps around an inner [PathInfoService] and when put is called it extracts
21/// the underlying narinfo and signs it using a [SigningKey]. For the moment only the
22/// [ed25519::signature::Signer<ed25519::Signature>] is available using a keyfile (see
23/// [KeyFileSigningPathInfoServiceConfig] for more informations). However the implementation is
24/// generic (see [nix_compat::narinfo::SigningKey] documentation).
25///
26/// The [PathInfo] with the added signature is then put into the inner [PathInfoService].
27///
28/// The service signs the [PathInfo] **only if it has a narinfo attribute**
29pub struct SigningPathInfoService<T, S> {
30    instance_name: String,
31    /// The inner [PathInfoService]
32    inner: T,
33    /// The key to sign narinfos
34    signing_key: Arc<SigningKey<S>>,
35}
36
37impl<T, S> SigningPathInfoService<T, S> {
38    pub fn new(instance_name: String, inner: T, signing_key: Arc<SigningKey<S>>) -> Self {
39        Self {
40            instance_name,
41            inner,
42            signing_key,
43        }
44    }
45}
46
47#[async_trait]
48impl<T, S> PathInfoService for SigningPathInfoService<T, S>
49where
50    T: PathInfoService,
51    S: ed25519::signature::Signer<ed25519::Signature> + Sync + Send,
52{
53    #[instrument(level = "trace", skip_all, fields(path_info.digest = nixbase32::encode(&digest), instance_name = %self.instance_name))]
54    async fn get(&self, digest: [u8; 20]) -> Result<Option<PathInfo>, Error> {
55        self.inner.get(digest).await
56    }
57
58    async fn put(&self, mut path_info: PathInfo) -> Result<PathInfo, Error> {
59        path_info.signatures.push({
60            let mut nar_info = path_info.to_narinfo();
61            nar_info.signatures.clear();
62            nar_info.add_signature(self.signing_key.as_ref());
63
64            let s = nar_info
65                .signatures
66                .pop()
67                .expect("Snix bug: no signature after signing op");
68            debug_assert!(
69                nar_info.signatures.is_empty(),
70                "Snix bug: more than one signature appeared"
71            );
72
73            Signature::new(s.name().to_string(), *s.bytes())
74        });
75        self.inner.put(path_info).await
76    }
77
78    fn list(&self) -> BoxStream<'static, Result<PathInfo, Error>> {
79        self.inner.list()
80    }
81}
82
83/// [ServiceBuilder] implementation that builds a [SigningPathInfoService] that signs narinfos using
84/// a keyfile. The keyfile is parsed using [parse_keypair], the expected format is the nix one
85/// (`nix-store --generate-binary-cache-key` for more informations).
86#[derive(serde::Deserialize)]
87pub struct KeyFileSigningPathInfoServiceConfig {
88    /// Inner [PathInfoService], will be resolved using a [CompositionContext].
89    pub inner: String,
90    /// Path to the keyfile in the nix format. It will be accessed once when building the service
91    pub keyfile: PathBuf,
92}
93
94impl TryFrom<url::Url> for KeyFileSigningPathInfoServiceConfig {
95    type Error = Box<dyn std::error::Error + Send + Sync>;
96    fn try_from(_url: url::Url) -> Result<Self, Self::Error> {
97        Err(Error::StorageError(
98            "Instantiating a SigningPathInfoService from a url is not supported".into(),
99        )
100        .into())
101    }
102}
103
104#[async_trait]
105impl ServiceBuilder for KeyFileSigningPathInfoServiceConfig {
106    type Output = dyn PathInfoService;
107    async fn build<'a>(
108        &'a self,
109        instance_name: &str,
110        context: &CompositionContext,
111    ) -> Result<Arc<dyn PathInfoService>, Box<dyn std::error::Error + Send + Sync + 'static>> {
112        let inner = context.resolve::<Self::Output>(self.inner.clone()).await?;
113        let signing_key = Arc::new(
114            parse_keypair(tokio::fs::read_to_string(&self.keyfile).await?.trim())
115                .map_err(|e| Error::StorageError(e.to_string()))?
116                .0,
117        );
118        Ok(Arc::new(SigningPathInfoService {
119            instance_name: instance_name.to_string(),
120            inner,
121            signing_key,
122        }))
123    }
124}
125
126#[cfg(test)]
127pub(crate) fn test_signing_service() -> Arc<dyn PathInfoService> {
128    let memory_svc: Arc<dyn PathInfoService> = Arc::new(MemoryPathInfoService::default());
129    Arc::new(SigningPathInfoService {
130        instance_name: "test".into(),
131        inner: memory_svc,
132        signing_key: Arc::new(
133            parse_keypair(DUMMY_KEYPAIR)
134                .expect("DUMMY_KEYPAIR to be valid")
135                .0,
136        ),
137    })
138}
139
140#[cfg(test)]
141pub const DUMMY_KEYPAIR: &str = "do.not.use:sGPzxuK5WvWPraytx+6sjtaff866sYlfvErE6x0hFEhy5eqe7OVZ8ZMqZ/ME/HaRdKGNGvJkyGKXYTaeA6lR3A==";
142#[cfg(test)]
143pub const DUMMY_VERIFYING_KEY: &str = "do.not.use:cuXqnuzlWfGTKmfzBPx2kXShjRryZMhil2E2ngOpUdw=";
144
145#[cfg(test)]
146mod test {
147    use crate::{fixtures::PATH_INFO, pathinfoservice::PathInfoService};
148    use nix_compat::narinfo::VerifyingKey;
149
150    #[tokio::test]
151    async fn put_and_verify_signature() {
152        let svc = super::test_signing_service();
153
154        // Pick a PATH_INFO with 0 signatures…
155        assert!(
156            PATH_INFO.signatures.is_empty(),
157            "PathInfo from fixtures should have no signatures"
158        );
159
160        // Asking PathInfoService, it should not be there ...
161        assert!(
162            svc.get(*PATH_INFO.store_path.digest())
163                .await
164                .expect("no error")
165                .is_none()
166        );
167
168        // insert it
169        svc.put(PATH_INFO.clone()).await.expect("no error");
170
171        // now it should be there ...
172        let path_info = svc
173            .get(*PATH_INFO.store_path.digest())
174            .await
175            .expect("no error")
176            .unwrap();
177
178        // Ensure there's a signature now
179        let new_sig = path_info
180            .signatures
181            .last()
182            .expect("The retrieved narinfo to be signed")
183            .as_ref();
184
185        // load our keypair from the fixtures
186        let (signing_key, _verifying_key) =
187            super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed");
188
189        // ensure that the new signature is using this key name
190        assert_eq!(signing_key.name(), *new_sig.name());
191
192        // verify the new signature against the verifying key
193        let verifying_key =
194            VerifyingKey::parse(super::DUMMY_VERIFYING_KEY).expect("parsing dummy verifying key");
195
196        assert!(
197            verifying_key.verify(&path_info.to_narinfo().fingerprint(), &new_sig),
198            "expect signature to be valid"
199        );
200    }
201}