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