snix_store/pathinfoservice/
signing_wrapper.rs1use 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
20pub struct SigningPathInfoService<T, S> {
30 instance_name: String,
31 inner: T,
33 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#[derive(serde::Deserialize)]
87pub struct KeyFileSigningPathInfoServiceConfig {
88 pub inner: String,
90 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 assert!(
156 PATH_INFO.signatures.is_empty(),
157 "PathInfo from fixtures should have no signatures"
158 );
159
160 assert!(
162 svc.get(*PATH_INFO.store_path.digest())
163 .await
164 .expect("no error")
165 .is_none()
166 );
167
168 svc.put(PATH_INFO.clone()).await.expect("no error");
170
171 let path_info = svc
173 .get(*PATH_INFO.store_path.digest())
174 .await
175 .expect("no error")
176 .unwrap();
177
178 let new_sig = path_info
180 .signatures
181 .last()
182 .expect("The retrieved narinfo to be signed")
183 .as_ref();
184
185 let (signing_key, _verifying_key) =
187 super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed");
188
189 assert_eq!(signing_key.name(), *new_sig.name());
191
192 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}