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