snix_store/pathinfoservice/
signing_wrapper.rs1use 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
17pub struct SigningPathInfoService<T, S> {
24 instance_name: String,
25 inner: T,
27 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#[derive(serde::Deserialize)]
95pub struct KeyFileSigningPathInfoServiceConfig {
96 pub inner: String,
98 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)]
132pub 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 assert!(
163 PATH_INFO.signatures.is_empty(),
164 "PathInfo from fixtures should have no signatures"
165 );
166
167 assert!(
169 svc.get(*PATH_INFO.store_path.digest())
170 .await
171 .expect("no error")
172 .is_none()
173 );
174
175 svc.put(PATH_INFO.clone()).await.expect("no error");
177
178 let path_info = svc
180 .get(*PATH_INFO.store_path.digest())
181 .await
182 .expect("no error")
183 .unwrap();
184
185 let new_sig = path_info
187 .signatures
188 .last()
189 .expect("The retrieved narinfo to be signed")
190 .as_ref();
191
192 let verifying_key =
194 VerifyingKey::parse(super::DUMMY_VERIFYING_KEY).expect("parsing dummy verifying key");
195
196 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}