//! Build and sign a definitions pack from a directory of OSV records. //! //! build-pack [version] //! //! If the key file does not exist a new signing key is generated into it //! at mode 0600 and its public half is printed. That public key is what //! goes into the agent's trust store; the private half never leaves the //! build machine and is never committed. //! //! Key material comes straight from /dev/urandom rather than through a //! random-number crate: it is thirty-two bytes from the kernel CSPRNG, //! and fewer moving parts between the entropy source and the key file is //! the right trade for something this consequential. use ed25519_dalek::SigningKey; use hound_defs::{osv, pack, Pack}; use std::io::Read; fn load_or_create_key(path: &str) -> SigningKey { if let Ok(bytes) = std::fs::read(path) { if bytes.len() == 32 { let mut seed = [0u8; 32]; seed.copy_from_slice(&bytes); return SigningKey::from_bytes(&seed); } eprintln!("{path} is not a 32-byte key; refusing to overwrite it"); std::process::exit(2); } let mut seed = [0u8; 32]; std::fs::File::open("/dev/urandom") .expect("opening /dev/urandom") .read_exact(&mut seed) .expect("reading key material"); std::fs::write(path, seed).expect("writing the key"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); } let key = SigningKey::from_bytes(&seed); let hex: String = key .verifying_key() .to_bytes() .iter() .map(|b| format!("{b:02x}")) .collect(); eprintln!("generated a new signing key in {path} (mode 0600)"); eprintln!("public key: {hex}"); eprintln!(" put that in the agent's trust store, or export it for development:"); eprintln!(" export HOUNDD_DEFS_KEY={hex}"); key } fn main() { let args: Vec = std::env::args().skip(1).collect(); if args.len() < 3 { eprintln!("usage: build-pack [version]"); std::process::exit(2); } let (dir, out, key_path) = (&args[0], &args[1], &args[2]); let version = args .get(3) .cloned() .unwrap_or_else(|| "0.0.0-dev".to_string()); let key = load_or_create_key(key_path); let mut indicators = Vec::new(); let mut files = 0usize; for entry in std::fs::read_dir(dir).expect("readable OSV directory").flatten() { let path = entry.path(); if path.extension().is_none_or(|e| e != "json") { continue; } files += 1; if let Ok(text) = std::fs::read_to_string(&path) { indicators.extend(osv::parse_record(&text)); } } // Sorted so the same input always produces the same bytes. A pack's // hash is its identity; directory order is not stable across machines // and would make two identical builds disagree. indicators.sort_by(|a, b| (&a.ecosystem, &a.name, &a.id).cmp(&(&b.ecosystem, &b.name, &b.id))); indicators.dedup(); let p = Pack { version: version.clone(), // Passed in rather than read from the clock, for the same reason. created: args.get(4).cloned().unwrap_or_else(|| "1970-01-01T00:00:00Z".into()), sources: vec!["ossf/malicious-packages (Apache-2.0)".into(), "osv.dev".into()], indicators, }; let signed = pack::sign(&p, &key, "dev").expect("signing the pack"); std::fs::write(out, serde_json::to_string(&signed).expect("encoding")).expect("writing"); println!("read {files} OSV records"); println!("packed {} indicators", p.indicators.len()); println!("version {version}"); println!("sha256 {}", p.sha256().expect("hashing")); println!("wrote {out}"); }