Skip to main content

snix_store/pathinfoservice/
signing_wrapper.rs

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