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> {
27 instance_name: String,
28 inner: T,
30 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#[derive(serde::Deserialize)]
96pub struct KeyFileSigningPathInfoServiceConfig {
97 pub inner: String,
99 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 assert!(
160 PATH_INFO.signatures.is_empty(),
161 "PathInfo from fixtures should have no signatures"
162 );
163
164 assert!(
166 svc.get(*PATH_INFO.store_path.digest())
167 .await
168 .expect("no error")
169 .is_none()
170 );
171
172 svc.put(PATH_INFO.clone()).await.expect("no error");
174
175 let path_info = svc
177 .get(*PATH_INFO.store_path.digest())
178 .await
179 .expect("no error")
180 .unwrap();
181
182 let new_sig = path_info
184 .signatures
185 .last()
186 .expect("The retrieved narinfo to be signed")
187 .as_ref();
188
189 let (signing_key, _verifying_key) =
191 super::parse_keypair(super::DUMMY_KEYPAIR).expect("must succeed");
192
193 assert_eq!(signing_key.name(), *new_sig.name());
195
196 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}