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