1use crate::signing::CacheSigner;
29use sui_castore::storage::StorageBackend;
30use sui_compat::narinfo::NarInfo;
31
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
34pub struct ResignReport {
35 pub total: usize,
37 pub resigned: usize,
39 pub unchanged: usize,
42 pub failed: usize,
45}
46
47pub async fn resign_all(
58 storage: &dyn StorageBackend,
59 signer: &CacheSigner,
60) -> Result<ResignReport, crate::CacheError> {
61 let hashes = storage
62 .list_narinfos()
63 .await
64 .map_err(|e| crate::CacheError::NarInfo(e.to_string()))?;
65
66 let key_prefix = format!("{}:", signer.key_name());
67 let mut report = ResignReport {
68 total: hashes.len(),
69 ..Default::default()
70 };
71
72 for hash in hashes {
73 let Ok(Some(content)) = storage.get_narinfo(&hash).await else {
74 report.failed += 1;
75 continue;
76 };
77 let Ok(mut info) = NarInfo::parse(&content) else {
78 report.failed += 1;
79 continue;
80 };
81
82 let before: Vec<String> = info
83 .signatures
84 .iter()
85 .filter(|s| s.starts_with(&key_prefix))
86 .cloned()
87 .collect();
88
89 info.signatures.retain(|s| !s.starts_with(&key_prefix));
93 let sig = signer.sign_narinfo(&info);
94
95 if before.len() == 1 && before[0] == sig {
96 report.unchanged += 1;
97 continue;
98 }
99
100 info.signatures.push(sig);
101 if storage
102 .put_narinfo_record(&hash, &info.serialize())
103 .await
104 .is_err()
105 {
106 report.failed += 1;
107 } else {
108 report.resigned += 1;
109 }
110 }
111
112 Ok(report)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use sui_castore::storage::LocalStorage;
119
120 const SECRET: &str =
121 "test-key-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
122
123 fn narinfo_with_sig(sig: Option<&str>) -> String {
124 let mut s = String::from(
129 "StorePath: /nix/store/00000000000000000000000000000000-x\n\
130 URL: nar/x.nar\n\
131 Compression: none\n\
132 FileHash: sha256:0000000000000000000000000000000000000000000000000000000000000000\n\
133 FileSize: 1\n\
134 NarHash: sha256:0000000000000000000000000000000000000000000000000000000000000000\n\
135 NarSize: 1\n\
136 References: \n",
137 );
138 if let Some(sig) = sig {
139 s.push_str(&format!("Sig: {sig}\n"));
140 }
141 s
142 }
143
144 async fn seed(dir: &std::path::Path, hash: &str, body: &str) -> LocalStorage {
145 let st = LocalStorage::new(dir);
146 st.put_narinfo_record(hash, body).await.unwrap();
147 st
148 }
149
150 #[tokio::test]
155 async fn replaces_a_stale_signature_under_our_own_key() {
156 let dir = tempfile::tempdir().unwrap();
157 let hash = "00000000000000000000000000000000";
158 let stale = "test-key-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
159 let st = seed(dir.path(), hash, &narinfo_with_sig(Some(stale))).await;
160 let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
161
162 let r = resign_all(&st, &signer).await.unwrap();
163 assert_eq!(r.resigned, 1, "a stale same-key signature must be replaced");
164 assert_eq!(r.unchanged, 0);
165
166 let out = st.get_narinfo(hash).await.unwrap().unwrap();
167 assert!(!out.contains(stale), "the stale signature must be gone");
168 assert!(out.contains("test-key-1:"), "a fresh one must be present");
169 }
170
171 #[tokio::test]
174 async fn second_sweep_is_a_no_op() {
175 let dir = tempfile::tempdir().unwrap();
176 let hash = "00000000000000000000000000000000";
177 let st = seed(dir.path(), hash, &narinfo_with_sig(None)).await;
178 let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
179
180 assert_eq!(resign_all(&st, &signer).await.unwrap().resigned, 1);
181 let second = resign_all(&st, &signer).await.unwrap();
182 assert_eq!(second.resigned, 0);
183 assert_eq!(second.unchanged, 1);
184 }
185
186 #[tokio::test]
188 async fn preserves_signatures_by_other_keys() {
189 let dir = tempfile::tempdir().unwrap();
190 let hash = "00000000000000000000000000000000";
191 let foreign = "cache.nixos.org-1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB==";
192 let st = seed(dir.path(), hash, &narinfo_with_sig(Some(foreign))).await;
193 let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
194
195 resign_all(&st, &signer).await.unwrap();
196 let out = st.get_narinfo(hash).await.unwrap().unwrap();
197 assert!(out.contains(foreign), "a foreign signature must survive");
198 assert!(out.contains("test-key-1:"), "ours must be added");
199 }
200}