From e89ca39c245085d8d3a7e968e62ef51f14af2d3a Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 06 Jan 2026 05:56:36 +0000 Subject: [PATCH] feat: mkAskPass generator --- --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,8 @@ tokio = { version = "1.45.1", features = ["fs", "macros", "rt", "rt-multi-thread", "sync", "time"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } + +[profile.dev] +panic = "abort" +[profile.release] +panic = "abort" --- a/crates/fleet-base/src/fleetdata.rs +++ b/crates/fleet-base/src/fleetdata.rs @@ -121,12 +121,12 @@ }) } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct FleetSecretPart { pub raw: SecretData, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] #[must_use] pub struct FleetSecretData { @@ -144,7 +144,7 @@ pub generation_data: Value, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] #[must_use] pub struct FleetSecretDistribution { --- a/crates/fleet-base/src/opts.rs +++ b/crates/fleet-base/src/opts.rs @@ -22,7 +22,8 @@ use crate::{ fleetdata::FleetData, - host::{Config, ConfigHost, FleetConfigInternals}, primops::init_primops, + host::{Config, ConfigHost, FleetConfigInternals}, + primops::{PRIMOPS_DATA, init_primops}, }; #[derive(Clone)] @@ -213,7 +214,7 @@ std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?; let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?)); - init_primops(data.clone()); + init_primops(); let mut fetch_settings = FetchSettings::new(); fetch_settings.set(c"warn-dirty", c"false"); @@ -264,8 +265,7 @@ if cfg!(debug_assertions) { gc_now(); } - - Ok(Config(Arc::new(FleetConfigInternals { + let config = Config(Arc::new(FleetConfigInternals { directory, data, flake_outputs: flake, @@ -275,6 +275,12 @@ default_pkgs, nixpkgs, localhost: self.localhost.to_owned(), - }))) + })); + + PRIMOPS_DATA + .set(config.clone()) + .map_err(|_| ()) + .expect("only one fleet config may exist per process"); + Ok(config) } } --- a/crates/fleet-base/src/primops.rs +++ b/crates/fleet-base/src/primops.rs @@ -1,10 +1,15 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::cell::OnceCell; +use std::collections::{BTreeMap, HashMap}; +use std::sync::{Arc, Mutex, OnceLock}; -use nix_eval::{NativeFn, Value}; -use tracing::info; +use anyhow::{Context, bail}; +use itertools::Itertools; +use nix_eval::{NativeFn, Value, nix_go, nix_go_json}; +use serde::Deserialize; +use tracing::{info, warn}; use crate::fleetdata::{FleetData, FleetSecrets}; +use crate::host::Config; #[derive(thiserror::Error, Debug)] enum Error {} @@ -23,18 +28,76 @@ struct FsSecretsBackend {} -pub fn init_primops(secrets: Arc>) { +pub static PRIMOPS_DATA: OnceLock = OnceLock::new(); + +#[derive(Deserialize, Debug)] +struct GeneratorPart { + encrypted: bool, +} + +pub fn init_primops() { info!("initializing primops"); NativeFn::new( c"__fleetEnsureHostSecret", c"Ensure secret existence for a host, regenerating it in case of some mismatch", [c"host", c"secret", c"generator"], - |[host, secret, generator]| { - todo!("ensure secret"); - Ok(Value::new_attrs(HashMap::from_iter([( - "raw", - Value::new_str("rawData"), - )]))) + |es, [host, secret, generator]| { + info!("get host"); + let host = host.to_string()?; + info!("get secret"); + let secret = secret.to_string()?; + + info!("get config"); + let config = PRIMOPS_DATA + .get() + .expect("primops data should be set on init"); + + info!("get pkgs"); + let nixpkgs = &config.nixpkgs; + let default_pkgs = &config.default_pkgs; + let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators); + let generators = nix_go!(default_mk_secret_generators(Obj { + recipients: >::new(), + })); + let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?; + + info!("call package"); + let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators)); + let default_generator = call_package + .call(generator.clone()) + .context("calling callPackage with generator")? + .call(Value::new_attrs(HashMap::new())) + .context("providing extra callPackage args")?; + + info!("get parts"); + let mut parts: BTreeMap = nix_go_json!(default_generator.parts); + info!("got parts: {parts:?}"); + + let Some(existing) = config + .host_secret(&host, &secret) else { + bail!("missing secret {secret} for host {host}; secret needs regeneration") + }; + + info!("got existing: {existing:?}"); + + let mut out = HashMap::new(); + + for (part_name, part) in &existing.secret.parts { + let Some(definition) = parts.remove(part_name) else { + warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix"); + continue; + }; + if definition.encrypted != part.raw.encrypted { + bail!("secret {secret} part {part_name} is supposed to be {}, but it is {}; secret needs regeneration", if definition.encrypted {"encrypted"} else {"unencrypted"}, if part.raw.encrypted {"encrypted"} else {"unencrypted"}); + } + out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))]))); + } + if !parts.is_empty(){ + let defs = parts.keys().collect_vec(); + bail!("secret parts are defined, but not stored: {defs:?}, secret needs regeneration") + } + + Ok(Value::new_attrs(out)) }, ) .register(); --- a/crates/nix-eval/src/lib.rs +++ b/crates/nix-eval/src/lib.rs @@ -12,7 +12,7 @@ use serde::de::DeserializeOwned; pub use anyhow::Result; -use tracing::instrument; +use tracing::{info, instrument, warn}; use self::logging::{ErrorInfoBuilder, nix_logging_cxx}; use self::nix_cxx::set_fetcher_setting; @@ -22,7 +22,8 @@ GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp, PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value, bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free, - clear_err, copy_value, err_code, err_info_msg, err_msg, eval_state_build, + clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW, + err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build, eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting, expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new, flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference, @@ -36,9 +37,8 @@ make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string, realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start, realised_string_get_store_path, realised_string_get_store_path_count, register_primop, - set_err_msg, setting_set, state_free, store_copy_closure, store_get_fs_closure, store_open, - store_parse_path, store_path_free, store_path_name, string_realise, value, value_call, - value_decref, value_incref, + set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free, + store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref, }; // Contains macros helpers @@ -62,6 +62,7 @@ type nix_fetchers_settings; include!("nix-eval/src/lib.hh"); + #[allow(clippy::missing_safety_doc)] unsafe fn set_fetcher_setting( settings: *mut nix_fetchers_settings, setting: *const c_char, @@ -111,19 +112,19 @@ #[derive(Debug)] #[repr(i32)] pub enum NixErrorKind { - Unknown = 1, - Overflow = 2, - Key = 3, - Generic = 4, + Unknown = err_NIX_ERR_UNKNOWN, + Overflow = err_NIX_ERR_OVERFLOW, + Key = err_NIX_ERR_KEY, + Generic = err_NIX_ERR_NIX_ERROR, } impl NixErrorKind { fn from_int(v: c_int) -> Option { Some(match v { 0 => return None, - -1 => Self::Unknown, - -2 => Self::Overflow, - -3 => Self::Key, - -4 => Self::Generic, + nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown, + nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow, + nix_raw::err_NIX_ERR_KEY => Self::Key, + nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic, _ => { debug_assert!(false, "unexpected nix error kind: {v}"); Self::Unknown @@ -298,8 +299,10 @@ } } -static GLOBAL_STATE: LazyLock = - LazyLock::new(|| GlobalState::new().expect("global state init shouldn't fail")); +static GLOBAL_STATE: LazyLock = LazyLock::new(|| { + info!("initializing nix global state"); + GlobalState::new().expect("global state init shouldn't fail") +}); thread_local! { static THREAD_STATE: RefCell = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail")); @@ -424,7 +427,8 @@ } } -struct EvalState(*mut c_eval_state); +#[repr(transparent)] +pub struct EvalState(*mut c_eval_state); unsafe impl Send for EvalState {} unsafe impl Sync for EvalState {} @@ -697,7 +701,15 @@ let builtin = Self::eval("builtins.toString")?; builtin.call(self.clone()) } + fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> { + with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?; + Ok(()) + } pub fn to_string(&self) -> Result { + let ty = self.type_of(); + if !matches!(ty, NixType::String) { + bail!("unexpected type: {ty:?}, expected string"); + } let mut str_out = String::new(); with_default_context(|c, _| unsafe { get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast()) @@ -931,17 +943,39 @@ ret: *mut value, ) { let user_closure: &UserClosure = unsafe { &*user_data.cast_const().cast() }; + let mut e = None; let args: [&Value; N] = array::from_fn(|i| { - let v: &Value = unsafe { &*args.add(i).cast_const().cast() }; - v + let v: &mut Value = unsafe { &mut *args.add(i).cast() }; + + info!("forcing arg"); + if matches!(v.type_of(), NixType::Thunk) + && let Err(err) = v.force(state) + { + e = Some(err); + }; + v as &Value }); + info!("args forced"); let ctx: &mut NixContext = unsafe { &mut *context.cast() }; - match user_closure(args) { + if let Some(e) = e { + warn!("set err = {e}"); + unsafe { init_int(context, ret, 0) }; + return ctx.set_err( + NixErrorKind::Unknown, + &CString::new(e.to_string()).expect("forcing argument value failed"), + ); + } + + let state: &EvalState = unsafe { std::mem::transmute(&state) }; + + match user_closure(state, args) { Ok(v) => { unsafe { copy_value(context, ret, v.0) }; } Err(e) => { + unsafe { init_int(context, ret, 0) }; + warn!("set err = {e:#?}"); ctx.set_err( NixErrorKind::Unknown, &CString::new(e.to_string()).expect("error should not contain internal nuls"), @@ -950,7 +984,7 @@ } } -type UserClosure = Box Result>; +type UserClosure = Box Result>; pub struct NativeFn(*mut PrimOp); impl NativeFn { @@ -958,7 +992,7 @@ name: &'static CStr, doc: &'static CStr, args: [&'static CStr; N], - f: impl Fn([&Value; N]) -> Result + 'static, + f: impl Fn(&EvalState, [&Value; N]) -> Result + 'static, ) -> Self { // Double-boxing to make it thin pointer, as vtable gets outside of first Box let closure: Box> = Box::new(Box::new(f)); @@ -1003,7 +1037,7 @@ c"__uppercaseSuffix2", c"make string uppercase and add suffix", [c"str", c"suffix"], - |[str, suffix]: [&Value; 2]| { + |_, [str, suffix]: [&Value; 2]| { let str = str.to_string()?; let suffix = suffix.to_string()?; Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase()))) @@ -1038,7 +1072,7 @@ c"uppercase_suffix", c"make string uppercase and add suffix", [c"str", c"suffix"], - |[str, suffix]: [&Value; 2]| { + |es, [str, suffix]: [&Value; 2]| { let str = str.to_string()?; let suffix = suffix.to_string()?; Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase()))) --- a/crates/nix-eval/src/macros.rs +++ b/crates/nix-eval/src/macros.rs @@ -16,7 +16,12 @@ $(nix_expr_inner!(@obj($o) $($tt)*);)? }}; (@obj($o:ident)) => {{}}; - (Obj { $($tt:tt)* }) => {{ + (Obj { }) => {{ + use $crate::{nix_expr_inner}; + let out = std::collections::hash_map::HashMap::new(); + Value::new_attrs(out) + }}; + (Obj { $($tt:tt)+ }) => {{ use $crate::{nix_expr_inner}; let mut out = std::collections::hash_map::HashMap::new(); nix_expr_inner!(@obj(out) $($tt)*); --- a/lib/default.nix +++ b/lib/default.nix @@ -149,6 +149,23 @@ } ); + mkAskPass = + { prompt ? "Secret value", part ? "secret" }: + ( + { + kdePackages, + mkImpureSecretGenerator, + }: + mkImpureSecretGenerator { + # TODO: Escape prompt? + script = '' + ${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part} + ''; + + parts.${part}.encrypted = true; + } + ); + /** Generate a random RSA keypair @@ -251,6 +268,7 @@ mkBytes mkHexBytes mkBase64Bytes + mkAskPass ; strings = --- a/modules/nixos/secrets.nix +++ b/modules/nixos/secrets.nix @@ -19,7 +19,6 @@ submodule str attrsOf - nullOr unspecified uniq functionTo @@ -77,13 +76,12 @@ { options = { parts = mkOption { - type = attrsOf (secretPartType secretName); + type = uniq (attrsOf (secretPartType secretName)); description = "Definition of secret parts"; }; generator = mkOption { - type = uniq (nullOr (functionTo package)); + type = uniq (functionTo package); description = "Derivation to evaluate for secret generation"; - default = null; }; mode = mkOption { type = str; @@ -103,14 +101,25 @@ }; }; config = { - parts = builtins.fleetEnsureHostSecret sysConfig.networking.hostName secretName config.generator; + # C api is broken in regard to thunks + # https://github.com/NixOS/nix/issues/12800 + parts = let + hostName = sysConfig.networking.hostName; + generator = config.generator; + in builtins.deepSeq [ + hostName + secretName + generator + ] (builtins.fleetEnsureHostSecret + hostName + secretName + generator); }; } ); - secretsData = (mapAttrs (_: s: s.definition) config.secrets); secretsFile = pkgs.writeTextFile { name = "secrets.json"; - text = toJSON secretsData; + text = toJSON config.system.secretsData; }; useSysusers = (config.systemd ? sysusers && config.systemd.sysusers.enable) @@ -121,17 +130,20 @@ secrets = mkOption { type = attrsOf secretType; default = { }; - apply = v: (mapAttrs (_: secret: secret.parts // { definition = secret; }) v); + apply = mapAttrs (_: secret: secret.parts // {definition = secret;}); description = "Host-local secrets"; }; system.secretsData = mkOption { type = unspecified; - default = { }; + default = mapAttrs (_: s: + (removeAttrs s.definition ["generator"]) // { + parts = mapAttrs (_: part: removeAttrs part ["data"]) s.definition.parts; + } + ) config.secrets; description = "secrets.json contents"; }; }; config = { - system = { inherit secretsData; }; environment.systemPackages = [ pkgs.fleet-install-secrets ]; systemd.services.fleet-install-secrets = mkIf useSysusers { -- gitstuff