nardl: can figure out which nars to download now
This commit is contained in:
parent
9cbb752b0d
commit
21d5613c4d
1
rust/.gitignore
vendored
Normal file
1
rust/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
target/
|
1915
rust/nardl/Cargo.lock
generated
Normal file
1915
rust/nardl/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
15
rust/nardl/Cargo.toml
Normal file
15
rust/nardl/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "nardl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
base64 = "0.22.1"
|
||||||
|
color-eyre = "0.6"
|
||||||
|
ed25519-dalek = "2.1.1"
|
||||||
|
env_logger = "0.11"
|
||||||
|
log = "0.4"
|
||||||
|
narinfo = "1.0.1"
|
||||||
|
nix-nar = "0.3.0"
|
||||||
|
reqwest = { version = "0.12.5", features = ["http2", "rustls-tls", "zstd"], default-features = false }
|
||||||
|
tokio = { version = "1.38.0", features = ["full"] }
|
191
rust/nardl/src/main.rs
Normal file
191
rust/nardl/src/main.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
use base64::{
|
||||||
|
engine::general_purpose::{
|
||||||
|
STANDARD as BASE64_STANDARD, STANDARD_NO_PAD as BASE64_STANDARD_NO_PAD,
|
||||||
|
},
|
||||||
|
Engine,
|
||||||
|
};
|
||||||
|
use color_eyre::{
|
||||||
|
eyre::{self, Context, OptionExt},
|
||||||
|
owo_colors::styles::ReversedDisplay,
|
||||||
|
};
|
||||||
|
use ed25519_dalek::{Signature, VerifyingKey};
|
||||||
|
use reqwest::ResponseBuilderExt;
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
// oh no i'm too lazy to add arguments for now
|
||||||
|
const SUBSTITUERS: [&str; 2] = ["https://cache.nixos.org", "https://cache.lix.systems"];
|
||||||
|
|
||||||
|
const KEYS: [&str; 2] = [
|
||||||
|
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=",
|
||||||
|
"cache.lix.systems:aBnZUw8zA7H35Cz2RyKFVs3H4PlGTLawyY5KRbvJR8o",
|
||||||
|
];
|
||||||
|
|
||||||
|
const OUTPUT: &str = "/nix/store/n50jk09x9hshwx1lh6k3qaiygc7yxbv9-lix-2.90.0-rc1";
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> eyre::Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
color_eyre::install()?;
|
||||||
|
|
||||||
|
let cache_base_urls = SUBSTITUERS
|
||||||
|
.iter()
|
||||||
|
.map(|url| reqwest::Url::parse(*url))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
let output_path = Path::new(OUTPUT);
|
||||||
|
let store_dir = output_path
|
||||||
|
.parent()
|
||||||
|
.ok_or_eyre("No parent of output file")?;
|
||||||
|
log::debug!("Want store dir {:?}", store_dir);
|
||||||
|
|
||||||
|
let output_name = output_path
|
||||||
|
.file_name()
|
||||||
|
.ok_or_eyre("output is not a filename")?
|
||||||
|
.to_str()
|
||||||
|
.ok_or_eyre("output not valid utf-8")?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
log::debug!("Want output {}", output_name);
|
||||||
|
|
||||||
|
let trusted_keys = KEYS
|
||||||
|
.iter()
|
||||||
|
.map(|key| -> eyre::Result<_> {
|
||||||
|
let (name, public_str) = key.split_once(":").ok_or_eyre("Key has no name")?;
|
||||||
|
let public_bytes = BASE64_STANDARD_NO_PAD
|
||||||
|
.decode(public_str.trim_end_matches("="))
|
||||||
|
.wrap_err("Invalid base64 in key")?;
|
||||||
|
Ok((
|
||||||
|
name,
|
||||||
|
ed25519_dalek::VerifyingKey::from_bytes(public_bytes.as_slice().try_into()?)?,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<eyre::Result<HashMap<&str, VerifyingKey>>>()?;
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// TODO: Handle priority here
|
||||||
|
for cache in cache_base_urls.iter() {
|
||||||
|
let info_str = client
|
||||||
|
.get(cache.join("nix-cache-info")?)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let parsed = narinfo::NixCacheInfo::parse(&info_str).unwrap();
|
||||||
|
|
||||||
|
log::trace!("narinfo {}: {:?}", cache, parsed);
|
||||||
|
|
||||||
|
if &parsed.store_dir != store_dir.to_str().unwrap() {
|
||||||
|
eyre::bail!("Cache {} has a different store directory", cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outputs_remaining = vec![output_name];
|
||||||
|
let mut outputs_done = HashSet::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(output) = outputs_remaining.pop() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
outputs_done.insert(output.clone());
|
||||||
|
|
||||||
|
log::debug!("Requesting output {}", output);
|
||||||
|
|
||||||
|
let (fingerprint, _) = &output
|
||||||
|
.split_once("-")
|
||||||
|
.ok_or_else(|| eyre::eyre!("Invalid output name {}", output))?;
|
||||||
|
|
||||||
|
let narinfo_text = get_narinfo(client.clone(), cache_base_urls.as_slice(), &fingerprint)
|
||||||
|
.await
|
||||||
|
.wrap_err_with(|| format!("While processing {}", output))?;
|
||||||
|
let narinfo_parsed = narinfo::NarInfo::parse(&narinfo_text).unwrap();
|
||||||
|
|
||||||
|
verify_signature(&narinfo_parsed, &trusted_keys, store_dir, &output)
|
||||||
|
.wrap_err_with(|| format!("While processing {}", output))?;
|
||||||
|
|
||||||
|
for reference in narinfo_parsed.references.iter() {
|
||||||
|
if reference.is_empty() || outputs_done.contains(reference.as_ref()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
outputs_remaining.push(reference.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
async fn get_narinfo(
|
||||||
|
client: reqwest::Client,
|
||||||
|
cache_base_urls: &[reqwest::Url],
|
||||||
|
fingerprint: &str,
|
||||||
|
) -> eyre::Result<String> {
|
||||||
|
for cache in cache_base_urls.iter() {
|
||||||
|
let response = client
|
||||||
|
.get(cache.join(&format!("{}.narinfo", fingerprint))?)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text().await.wrap_err("Could not download narinfo");
|
||||||
|
}
|
||||||
|
|
||||||
|
eyre::bail!("No cache has fingerprint {}", fingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_signature(
|
||||||
|
info: &narinfo::NarInfo,
|
||||||
|
trusted_keys: &HashMap<&str, VerifyingKey>,
|
||||||
|
store_dir: &Path,
|
||||||
|
output_name: &str,
|
||||||
|
) -> eyre::Result<()> {
|
||||||
|
for sig_info in &info.sigs {
|
||||||
|
let Some(key) = trusted_keys.get(sig_info.key_name.as_ref()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let signature_bytes = BASE64_STANDARD.decode(sig_info.sig.as_ref())?;
|
||||||
|
|
||||||
|
let signature = Signature::from_bytes(
|
||||||
|
signature_bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.wrap_err("Invalid signature length")?,
|
||||||
|
);
|
||||||
|
|
||||||
|
// no one documents it, but this is all that's actually signed:
|
||||||
|
// https://git.lix.systems/lix-project/lix/src/commit/d461cc1d7b2f489c3886f147166ba5b5e0e37541/src/libstore/path-info.cc#L25
|
||||||
|
let fingerprint = format!(
|
||||||
|
"1;{};{};{};{}",
|
||||||
|
store_dir
|
||||||
|
.join(output_name)
|
||||||
|
.to_str()
|
||||||
|
.ok_or_eyre("Path not valid UTF-8")?,
|
||||||
|
info.nar_hash,
|
||||||
|
info.nar_size,
|
||||||
|
info.references
|
||||||
|
.iter()
|
||||||
|
.filter(|reference| !reference.is_empty())
|
||||||
|
.map(|reference| store_dir
|
||||||
|
.join(reference.as_ref())
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
log::trace!("narinfo fingerprint: `{}`", fingerprint);
|
||||||
|
|
||||||
|
key.verify_strict(&fingerprint.as_bytes(), &signature)
|
||||||
|
.wrap_err("Invalid signature")?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
eyre::bail!("No signatures by a trusted key");
|
||||||
|
}
|
Loading…
Reference in a new issue