From 6512b8d2cc66bc51c6ea21faad7499c5e5535038 Mon Sep 17 00:00:00 2001 From: Artemis Tosini Date: Mon, 8 Jul 2024 21:43:17 +0000 Subject: [PATCH] Initial work on substituter --- Cargo.lock | 281 ++++++++++++++ Cargo.toml | 5 + .../base/fetch_and_unpack_nix_substituter.rs | 360 ++++++++++++++++++ src/action/base/mod.rs | 2 + 4 files changed, 648 insertions(+) create mode 100644 src/action/base/fetch_and_unpack_nix_substituter.rs diff --git a/Cargo.lock b/Cargo.lock index 0d953e0..a36d1de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -134,6 +134,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -146,6 +152,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -177,11 +192,22 @@ dependencies = [ "serde", ] +[[package]] +name = "camino" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" + [[package]] name = "cc" version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] [[package]] name = "cfg-if" @@ -282,6 +308,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -298,6 +330,52 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.20.8" @@ -333,6 +411,16 @@ dependencies = [ "syn", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -343,6 +431,16 @@ dependencies = [ "serde", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.1" @@ -391,6 +489,31 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.11.0" @@ -459,6 +582,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filetime" version = "0.2.23" @@ -553,6 +682,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.14" @@ -817,6 +956,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -829,6 +977,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -877,17 +1034,20 @@ name = "lix-installer" version = "0.17.1" dependencies = [ "async-trait", + "base64 0.22.1", "bytes 1.6.0", "clap", "color-eyre", "dirs", "dyn-clone", + "ed25519-dalek", "eyre", "glob", "indexmap 2.2.6", "is_ci", "nix", "nix-config-parser", + "nix-nar", "os-release", "owo-colors 4.0.0", "plist", @@ -897,6 +1057,7 @@ dependencies = [ "serde", "serde_json", "serde_with", + "sha2", "strum", "sysctl", "tar", @@ -914,6 +1075,7 @@ dependencies = [ "walkdir", "which", "xz2", + "zstd", ] [[package]] @@ -1007,6 +1169,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nix-nar" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5549158a8b179c4fcd06a19f4bcc557db60c9cbd6771add9563f46c8d0325b5" +dependencies = [ + "camino", + "is_executable", + "symlink", + "thiserror", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1140,6 +1314,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1367,6 +1551,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -1574,6 +1767,17 @@ dependencies = [ "syn", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1592,6 +1796,15 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -1623,6 +1836,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1657,6 +1880,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "supports-color" version = "2.1.0" @@ -1667,6 +1896,12 @@ dependencies = [ "is_ci", ] +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + [[package]] name = "syn" version = "2.0.60" @@ -1995,6 +2230,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "typetag" version = "0.2.16" @@ -2079,6 +2320,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.5.0" @@ -2421,3 +2668,37 @@ dependencies = [ "lzma-sys", "tokio-io", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.12+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 5a510a0..63b0af1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,11 @@ which = "6.0.0" sysctl = "0.5.4" walkdir = "2.3.3" indexmap = { version = "2.0.2", features = ["serde"] } +nix-nar = "0.3.0" +zstd = { version = "0.13.2", default-features = false } +sha2 = "0.10.8" +ed25519-dalek = { version = "2.1.1", features = ["serde"] } +base64 = "0.22.1" [dev-dependencies] eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ] } diff --git a/src/action/base/fetch_and_unpack_nix_substituter.rs b/src/action/base/fetch_and_unpack_nix_substituter.rs new file mode 100644 index 0000000..6127f26 --- /dev/null +++ b/src/action/base/fetch_and_unpack_nix_substituter.rs @@ -0,0 +1,360 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use base64::Engine; +use ed25519_dalek::VerifyingKey; +use reqwest::Url; +use tracing::{span, Span}; + +use crate::{ + action::{Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction}, + parse_ssl_cert, +}; + +/// Fetch an output and its dependencies from a set of substituters, +/// given an output path, subsititer URLs, and trusted keys. +/// Also generates a ".reginfo" compatible with `nix-store --load-db` +/// Only implements a subset of nix substitution features: +/// * Substituter priorites are highest to lowest as given to [`plan`], +/// instead of priority from nix-cache-info +/// * narinfo signatures are always required +/// * ca-derivations are not supported +/// * NarHash must be sha256 +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct FetchAndUnpackNixSubstituter { + /// Map from key name (e.g. cache.nixos.org-1) to parsed ed25519 key + trusted_keys: HashMap, + /// Base URLs for substituters, e.g. https://cache.nixos.org/ + substituters: Vec, + /// Desired derivation output, e.g. + /// `/nix/store/n50jk09x9hshwx1lh6k3qaiygc7yxbv9-lix-2.90.0-rc1` + target: StorePath, + /// Destination directory, normally temporary. + /// For compatibility with tarballs, files will be placed in + /// the nix/store subdirectory of the destination + dest: PathBuf, + /// Proxy used for all requests from substituters + proxy: Option, + /// Extra SSL certificates trusted for all requests + ssl_cert_file: Option, +} + +/// Root directory of the nix store. +/// Technically this could be something other than /nix/store, +/// but that is rarely done in production +const STORE_DIR: &str = "/nix/store/"; + +impl FetchAndUnpackNixSubstituter { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan( + target: PathBuf, + dest: PathBuf, + trusted_keys: Vec, + substituters: Vec, + proxy: Option, + ssl_cert_file: Option, + ) -> Result, ActionError> { + let trusted_keys_parsed = trusted_keys + .iter() + .map(|key| parse_key(key)) + .collect::, _>>() + .map_err(Self::error)?; + + if let Some(proxy) = &proxy { + match proxy.scheme() { + "https" | "http" | "socks5" => (), + _ => return Err(Self::error(SubstitutionError::UnknownProxyScheme)), + }; + } + + if let Some(ssl_cert_file) = &ssl_cert_file { + parse_ssl_cert(ssl_cert_file).await.map_err(Self::error)?; + } + + Ok(Self { + target: StorePath::from_full_path(target) + .ok_or_else(|| Self::error(SubstitutionError::InvalidStorePath))?, + trusted_keys: trusted_keys_parsed, + dest, + proxy, + substituters, + ssl_cert_file, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "fetch_and_unpack_nix")] +impl Action for FetchAndUnpackNixSubstituter { + fn action_tag() -> ActionTag { + ActionTag("fetch_and_unpack_nix_substituter") + } + fn tracing_synopsis(&self) -> String { + format!( + "Fetch `{}` from substituters to `{}`", + self.target.full_path.to_string_lossy(), + self.dest.display() + ) + } + + fn tracing_span(&self) -> Span { + let span = span!( + tracing::Level::DEBUG, + "fetch_and_unpack_nix_substituter", + target = tracing::field::debug(&self.target.full_path), + proxy = tracing::field::Empty, + ssl_cert_file = tracing::field::Empty, + dest = tracing::field::display(self.dest.display()), + ); + if let Some(proxy) = &self.proxy { + span.record("proxy", tracing::field::display(&proxy)); + } + if let Some(ssl_cert_file) = &self.ssl_cert_file { + span.record( + "ssl_cert_file", + tracing::field::display(&ssl_cert_file.display()), + ); + } + span + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new(self.tracing_synopsis(), vec![])] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![/* Deliberately empty -- this is a noop */] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + Ok(()) + } +} + +/// Parse a nix trusted key into name and ed25519 +fn parse_key(key: &str) -> Result<(String, VerifyingKey), SubstitutionError> { + let (name, key_base64) = key + .split_once(':') + .ok_or_else(|| SubstitutionError::PublicKey)?; + + // seems to be the best way to handle keys both with and without padding + let key_bytes = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(key_base64.trim_end_matches("=").as_bytes()) + .map_err(|_| SubstitutionError::PublicKey)?; + + let key_array: [u8; ed25519_dalek::PUBLIC_KEY_LENGTH] = key_bytes + .try_into() + .map_err(|_| SubstitutionError::PublicKey)?; + + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&key_array) + .map_err(|_| SubstitutionError::PublicKey)?; + + Ok((name.to_string(), verifying_key)) +} + +/// Utility struct representing a store path +#[derive(Clone, Eq, Debug)] +struct StorePath { + /// Full path of the output including STORE_DIR, + /// as seen in StorePath in narinfo + /// e.g. `/nix/store/n50jk09x9hshwx1lh6k3qaiygc7yxbv9-lix-2.90.0-rc1` + pub full_path: String, + /// The base32 hash part of a store path, + /// e.g. `n50jk09x9hshwx1lh6k3qaiygc7yxbv9` + pub digest: String, + /// The full name of a path, not including STORE_DIR, + /// e.g. `n50jk09x9hshwx1lh6k3qaiygc7yxbv9-lix-2.90.0-rc1` + pub full_name: String, +} + +impl StorePath { + pub fn from_full_path(full_path: &str) -> Option { + if !full_path.starts_with(STORE_DIR) { + return None; + } + + let remaining = + + let full_name = full_path.file_name()?.to_str()?.to_owned(); + + let (digest, _) = full_name.split_once('-')?; + Some(Self { + full_path, + digest: digest.to_owned(), + full_name, + }) + } + + pub fn from_full_name(full_name: String) -> Option { + let (digest, _) = full_name.split_once('-')?; + + Some(Self { + full_path: Path::new(STORE_DIR).join(&full_name), + digest: digest.to_string(), + full_name, + }) + } +} + +impl PartialEq for StorePath { + fn eq(&self, other: &Self) -> bool { + self.full_path == other.full_path + } +} + +impl serde::Serialize for StorePath { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.full_path.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for StorePath { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_string(StorePathVisitor) + } +} + +struct StorePathVisitor; +impl serde::de::Visitor<'_> for StorePathVisitor { + type Value = StorePath; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a valid nix store path starting with /nix/store") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + StorePath::from_full_path(Path::new(value).to_owned()) + .ok_or_else(|| E::custom("invalid store path")) + } +} + +/// Compression types for nar files. +/// Not exhaustive, this is just what +/// I've seen in the real world +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum NarCompression { + Zstd, + Xz, +} + +impl NarCompression { + pub fn from_name(name: &str) -> Option { + match name { + "zstd" => Some(Self::Zstd), + "xz" => Some(Self::Zstd), + _ => None, + } + } +} + +/// Extracted data from a narinfo. +/// May only be constructed by verifying signature +/// Missing some fields, like Deriver and FileHash because they aren't signed +#[derive(Debug, Clone, PartialEq, Eq)] +struct NarInfo { + /// Store path represented by the nar + pub store_path: StorePath, + /// Full URL to download the nar + pub url: Url, + /// Method used to compress the nar + pub compression: NarCompression, + /// sha256 hash of the nar after decompression + /// encoded in nix base32 format + pub nar_hash: String, + /// Size of the decompressed nar in bytes + pub nar_size: u64, + /// Other store paths referenced by + pub references: Vec, +} + +impl NarInfo { + fn parse(substituter_url: &Url, contents: &bytes::Bytes) -> Result { + Ok(Self { + store_path: todo!(), + url: todo!(), + compression: todo!(), + nar_hash: todo!(), + nar_size: todo!(), + references: todo!(), + }) + } + + fn verify( + &self, + trusted_keys: &HashMap, + ) -> Result<(), SubstitutionError> { + // Fingerprint format not documented, but implemented in lix: + // https://git.lix.systems/lix-project/lix/src/commit/d461cc1d7b2f489c3886f147166ba5b5e0e37541/src/libstore/path-info.cc#L25 + let fingerprint = format!( + "1;{};{};{};{}", + self.store_path.full_path.to_string_lossy(), + self.nar_hash, + self.nar_size, + self.references + .iter() + .map(|reference| reference.full_path) + .collect::>() + .join(",") + ); + + todo!() + } + + pub fn parse_and_verify( + trusted_keys: &HashMap, + substituter_url: &Url, + expected_store_path: &StorePath, + contents: &bytes::Bytes, + ) -> Result { + let parsed = Self::parse(substituter_url, contents)?; + + if &parsed.store_path != expected_store_path { + return Err(SubstitutionError::BadNarInfo); + } + + parsed.verify(trusted_keys)?; + + Ok(parsed) + } +} + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum SubstitutionError { + #[error("Unarchiving error")] + Unarchive(#[source] std::io::Error), + #[error("Unknown proxy scheme, `https://`, `socks5://`, and `http://` supported")] + UnknownProxyScheme, + #[error("Invalid public key")] + /// Normally an ed25519_dalek::SignatureError, + /// but that comes with no extra information so no need to include it + PublicKey, + #[error("Bad narinfo signature")] + BadNarInfo, + #[error("Invalid nix store path")] + InvalidStorePath, +} + +impl From for ActionErrorKind { + fn from(val: SubstitutionError) -> Self { + ActionErrorKind::Custom(Box::new(val)) + } +} diff --git a/src/action/base/mod.rs b/src/action/base/mod.rs index 71a4c02..376d0f6 100644 --- a/src/action/base/mod.rs +++ b/src/action/base/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod create_or_merge_nix_config; pub(crate) mod create_user; pub(crate) mod delete_user; pub(crate) mod fetch_and_unpack_nix; +pub(crate) mod fetch_and_unpack_nix_substituter; pub(crate) mod move_unpacked_nix; pub(crate) mod remove_directory; pub(crate) mod setup_default_profile; @@ -22,6 +23,7 @@ pub use create_or_merge_nix_config::CreateOrMergeNixConfig; pub use create_user::CreateUser; pub use delete_user::DeleteUser; pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError}; +pub use fetch_and_unpack_nix_substituter::{FetchAndUnpackNixSubstituter, SubstitutionError}; pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError}; pub use remove_directory::RemoveDirectory; pub use setup_default_profile::{SetupDefaultProfile, SetupDefaultProfileError};