use crate::signing::CacheSigner;
use sui_castore::storage::StorageBackend;
use sui_compat::narinfo::NarInfo;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ResignReport {
pub total: usize,
pub resigned: usize,
pub unchanged: usize,
pub failed: usize,
}
pub async fn resign_all(
storage: &dyn StorageBackend,
signer: &CacheSigner,
) -> Result<ResignReport, crate::CacheError> {
let hashes = storage
.list_narinfos()
.await
.map_err(|e| crate::CacheError::NarInfo(e.to_string()))?;
let key_prefix = format!("{}:", signer.key_name());
let mut report = ResignReport {
total: hashes.len(),
..Default::default()
};
for hash in hashes {
let Ok(Some(content)) = storage.get_narinfo(&hash).await else {
report.failed += 1;
continue;
};
let Ok(mut info) = NarInfo::parse(&content) else {
report.failed += 1;
continue;
};
let before: Vec<String> = info
.signatures
.iter()
.filter(|s| s.starts_with(&key_prefix))
.cloned()
.collect();
info.signatures.retain(|s| !s.starts_with(&key_prefix));
let sig = signer.sign_narinfo(&info);
if before.len() == 1 && before[0] == sig {
report.unchanged += 1;
continue;
}
info.signatures.push(sig);
if storage
.put_narinfo_record(&hash, &info.serialize())
.await
.is_err()
{
report.failed += 1;
} else {
report.resigned += 1;
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use sui_castore::storage::LocalStorage;
const SECRET: &str =
"test-key-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
fn narinfo_with_sig(sig: Option<&str>) -> String {
let mut s = String::from(
"StorePath: /nix/store/00000000000000000000000000000000-x\n\
URL: nar/x.nar\n\
Compression: none\n\
FileHash: sha256:0000000000000000000000000000000000000000000000000000000000000000\n\
FileSize: 1\n\
NarHash: sha256:0000000000000000000000000000000000000000000000000000000000000000\n\
NarSize: 1\n\
References: \n",
);
if let Some(sig) = sig {
s.push_str(&format!("Sig: {sig}\n"));
}
s
}
async fn seed(dir: &std::path::Path, hash: &str, body: &str) -> LocalStorage {
let st = LocalStorage::new(dir);
st.put_narinfo_record(hash, body).await.unwrap();
st
}
#[tokio::test]
async fn replaces_a_stale_signature_under_our_own_key() {
let dir = tempfile::tempdir().unwrap();
let hash = "00000000000000000000000000000000";
let stale = "test-key-1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";
let st = seed(dir.path(), hash, &narinfo_with_sig(Some(stale))).await;
let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
let r = resign_all(&st, &signer).await.unwrap();
assert_eq!(r.resigned, 1, "a stale same-key signature must be replaced");
assert_eq!(r.unchanged, 0);
let out = st.get_narinfo(hash).await.unwrap().unwrap();
assert!(!out.contains(stale), "the stale signature must be gone");
assert!(out.contains("test-key-1:"), "a fresh one must be present");
}
#[tokio::test]
async fn second_sweep_is_a_no_op() {
let dir = tempfile::tempdir().unwrap();
let hash = "00000000000000000000000000000000";
let st = seed(dir.path(), hash, &narinfo_with_sig(None)).await;
let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
assert_eq!(resign_all(&st, &signer).await.unwrap().resigned, 1);
let second = resign_all(&st, &signer).await.unwrap();
assert_eq!(second.resigned, 0);
assert_eq!(second.unchanged, 1);
}
#[tokio::test]
async fn preserves_signatures_by_other_keys() {
let dir = tempfile::tempdir().unwrap();
let hash = "00000000000000000000000000000000";
let foreign = "cache.nixos.org-1:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB==";
let st = seed(dir.path(), hash, &narinfo_with_sig(Some(foreign))).await;
let signer = CacheSigner::from_secret_key_string(SECRET).unwrap();
resign_all(&st, &signer).await.unwrap();
let out = st.get_narinfo(hash).await.unwrap().unwrap();
assert!(out.contains(foreign), "a foreign signature must survive");
assert!(out.contains("test-key-1:"), "ours must be added");
}
}