//! macOS MDM managed-preferences layer. //! //! Admins push a device profile with standard-base64 (`padded`) TOML under //! preference domain `ai.x.kigi` (`requirements_toml_base64`). Only admin-*forced* //! values are read, so a local user can't forge it via their own preference //! domain; trusted on every launch, independent of network/cache. `None` off macOS. #[cfg(target_os = "macos")] const MANAGED_PREFERENCES_DOMAIN: &str = "ai.x.kigi"; #[cfg(target_os = "macos")] const REQUIREMENTS_KEY: &str = "requirements_toml_base64"; /// Synthetic source label for the MDM layer (no file on disk); diagnostics only. pub const MDM_REQUIREMENTS_SOURCE: &str = "ai.x.kigi:requirements_toml_base64"; /// The MDM-forced requirements TOML, or `None` when none is forced (or not macOS). pub(crate) fn managed_preferences_requirements() -> Option { // Read once and cache for the process lifetime: the forced policy is fixed per // launch, so a mid-session profile change isn't picked up until restart — fine // for a short-lived CLI, and it avoids re-crossing the CoreFoundation boundary. static CACHED: std::sync::OnceLock> = std::sync::OnceLock::new(); CACHED .get_or_init(|| managed_requirements_from(read_forced_requirements)) .clone() } /// Decode the forced requirements from a raw-string reader. Split from the FFI /// read (`read_forced_requirements`) so the forced → decode path is unit-testable /// without CoreFoundation (the CFPreferences read/downcast itself stays FFI). fn managed_requirements_from(read: impl FnOnce() -> Option) -> Option { decode_managed_toml(&read()?) } /// Decode a base64 TOML payload into a non-empty table. The forced payload is /// used **verbatim** — `$VAR`/`${VAR}` are deliberately NOT expanded: this is /// the trusted, non-forgeable admin layer, and expanding from the local process /// environment would let the very user the forced check excludes influence the /// policy (which feeds yolo / permission / minimum-version enforcement). FFI-free, /// so unit-tested on every platform; invalid base64/UTF-8/TOML or an empty table /// yields `None`. fn decode_managed_toml(encoded: &str) -> Option { use base64::Engine as _; // Strip all whitespace: profile tooling line-wraps payloads and the STANDARD // engine rejects interior whitespace. let compact: String = encoded .chars() .filter(|c| !c.is_ascii_whitespace()) .collect(); let decoded = base64::engine::general_purpose::STANDARD .decode(compact.as_bytes()) .map_err(|e| tracing::warn!("managed preference is not valid base64: {e}")) .ok()?; let toml_str = String::from_utf8(decoded) .map_err(|e| tracing::warn!("managed preference is not valid UTF-8: {e}")) .ok()?; let value = toml::from_str::(&toml_str) .map_err(|e| { // Redact via the span-only detail: a TOML error's Display echoes the // offending source line, and an admin payload may carry secrets. tracing::warn!( "managed preference is not valid TOML: {}", crate::loader::toml_error_detail(&toml_str, &e) ) }) .ok()?; value .as_table() .is_some_and(|t| !t.is_empty()) .then_some(value) } /// The raw forced `requirements_toml_base64` MDM string via CoreFoundation, or /// `None`. macOS only. #[cfg(target_os = "macos")] fn read_forced_requirements() -> Option { use core_foundation::base::{CFType, CFTypeRef, TCFType}; use core_foundation::string::{CFString, CFStringRef}; // `CFPreferencesCopyAppValue` returns a +1 CFPropertyListRef (Copy rule). #[link(name = "CoreFoundation", kind = "framework")] unsafe extern "C" { fn CFPreferencesCopyAppValue(key: CFStringRef, application_id: CFStringRef) -> CFTypeRef; fn CFPreferencesAppValueIsForced(key: CFStringRef, application_id: CFStringRef) -> u8; } let cf_key = CFString::new(REQUIREMENTS_KEY); let cf_app = CFString::new(MANAGED_PREFERENCES_DOMAIN); // Trust only admin-forced values: otherwise the lookup falls through to the // per-user domain, which a local user can set (`defaults write ai.x.kigi`) // to forge an `is_system`-trusted layer. let forced = unsafe { CFPreferencesAppValueIsForced(cf_key.as_concrete_TypeRef(), cf_app.as_concrete_TypeRef()) }; if forced == 0 { return None; } let value_ref = unsafe { CFPreferencesCopyAppValue(cf_key.as_concrete_TypeRef(), cf_app.as_concrete_TypeRef()) }; if value_ref.is_null() { return None; } // Type-check before reading as text: reading a non-CFString through CFString // APIs is UB. `wrap_under_create_rule` owns the +1, freeing it even if the // downcast fails. let value = unsafe { CFType::wrap_under_create_rule(value_ref) }; value.downcast_into::().map(|s| s.to_string()) } #[cfg(not(target_os = "macos"))] fn read_forced_requirements() -> Option { None } #[cfg(test)] mod tests { use super::*; use base64::Engine as _; fn b64(s: &str) -> String { base64::engine::general_purpose::STANDARD.encode(s.as_bytes()) } #[test] fn decodes_valid_base64_toml_table() { let v = decode_managed_toml(&b64("allowed_sandbox_modes = [\"read-only\"]\n")) .expect("valid payload decodes"); assert_eq!( v.get("allowed_sandbox_modes") .and_then(|m| m.as_array()) .map(|a| a.len()), Some(1) ); } #[test] fn decodes_line_wrapped_base64() { // Profile tooling line-wraps base64; interior newlines must be tolerated. let raw = b64("allowed_sandbox_modes = [\"read-only\"]\n"); let wrapped = format!("{}\n{}", &raw[..4], &raw[4..]); assert!(decode_managed_toml(&wrapped).is_some()); } /// The forced payload is used verbatim: `$VAR`/`${VAR}` must NOT be expanded, /// so a local user can't influence the trusted admin layer through their env. #[test] fn forced_payload_is_not_env_expanded() { // SAFETY: process-global env mutation, restored before return. let prior = std::env::var("KIGI_MDM_NO_EXPAND_TEST").ok(); unsafe { std::env::set_var("KIGI_MDM_NO_EXPAND_TEST", "attacker") }; let decoded = decode_managed_toml(&b64("base_url = \"${KIGI_MDM_NO_EXPAND_TEST}/v1\"\n")); unsafe { match prior { Some(p) => std::env::set_var("KIGI_MDM_NO_EXPAND_TEST", p), None => std::env::remove_var("KIGI_MDM_NO_EXPAND_TEST"), } } assert_eq!( decoded .as_ref() .and_then(|v| v.get("base_url")) .and_then(|v| v.as_str()), Some("${KIGI_MDM_NO_EXPAND_TEST}/v1"), "forced payload must keep ${{VAR}} literal, not expand from the user env", ); } /// The forced gating around the FFI read: nothing forced → no layer; a forced /// valid payload → a layer; a forced but unparseable payload → no layer (never /// a partial/garbage layer). Exercises the seam without CoreFoundation. #[test] fn managed_requirements_gated_on_the_forced_read() { assert!(managed_requirements_from(|| None).is_none()); assert!( managed_requirements_from(|| Some(b64("allowed_sandbox_modes = [\"read-only\"]\n"))) .is_some() ); assert!(managed_requirements_from(|| Some("not base64!!!".to_string())).is_none()); } #[test] fn rejects_garbage_and_empty() { // not valid base64 assert!(decode_managed_toml("not base64!!!").is_none()); // valid base64, but the bytes aren't valid UTF-8 let bad_utf8 = base64::engine::general_purpose::STANDARD.encode([0xff, 0xfe]); assert!(decode_managed_toml(&bad_utf8).is_none()); // valid base64 + valid UTF-8, but not parseable TOML assert!(decode_managed_toml(&b64("= not toml =")).is_none()); // empty table → skipped (no managed preference effectively) assert!(decode_managed_toml(&b64("")).is_none()); } }