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