1use serde::{Deserialize, Serialize};
13use tatara_lisp::DeriveTataraDomain;
14
15use crate::SpecError;
16
17#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
18#[tatara(keyword = "deftrust-model")]
19pub struct TrustModel {
20 pub name: String,
21 #[serde(rename = "trustedPublicKeys")]
23 pub trusted_public_keys: Vec<String>,
24 #[serde(rename = "trustedSubstituters")]
27 pub trusted_substituters: Vec<String>,
28 #[serde(rename = "trustedUsers")]
30 pub trusted_users: Vec<String>,
31 pub posture: TrustPosture,
33}
34
35#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum TrustPosture {
37 Permissive,
40 MultiUser,
43 Sealed,
46}
47
48pub const CANONICAL_TRUST_LISP: &str = include_str!("../specs/trust_model.lisp");
49
50pub fn load_canonical() -> Result<Vec<TrustModel>, SpecError> {
56 crate::loader::load_all::<TrustModel>(CANONICAL_TRUST_LISP)
57}
58
59pub fn load_named(name: &str) -> Result<TrustModel, SpecError> {
65 load_canonical()?
66 .into_iter()
67 .find(|t| t.name == name)
68 .ok_or_else(|| SpecError::Load(format!("no (deftrust-model) with :name {name:?}")))
69}
70
71#[must_use]
78pub fn user_can_build(model: &TrustModel, user: &str) -> bool {
79 for entry in &model.trusted_users {
80 if entry == "*" || entry == user {
81 return true;
82 }
83 if entry.starts_with('@') {
87 return true;
88 }
89 }
90 false
91}
92
93#[must_use]
96pub fn substituter_trusted(model: &TrustModel, url: &str) -> bool {
97 model.trusted_substituters.iter().any(|s| s == url)
98}
99
100#[must_use]
103pub fn key_trusted(model: &TrustModel, key_name: &str) -> bool {
104 model.trusted_public_keys.iter().any(|k| {
105 k.split(':').next() == Some(key_name)
108 })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn canonical_trust_models_parse() {
117 let models = load_canonical().unwrap();
118 assert!(!models.is_empty());
119 }
120
121 #[test]
122 fn three_postures_present() {
123 let models = load_canonical().unwrap();
124 let postures: std::collections::HashSet<TrustPosture> =
125 models.iter().map(|m| m.posture).collect();
126 for required in [TrustPosture::Permissive, TrustPosture::MultiUser, TrustPosture::Sealed] {
127 assert!(postures.contains(&required), "missing posture {required:?}");
128 }
129 }
130
131 #[test]
132 fn sealed_has_no_substituters() {
133 let m = load_named("sealed-compliance").unwrap();
134 assert!(
135 m.trusted_substituters.is_empty(),
136 "Sealed posture must have no trusted substituters",
137 );
138 }
139
140 #[test]
143 fn permissive_lets_anyone_build() {
144 let m = load_named("single-user-permissive").unwrap();
145 assert!(user_can_build(&m, "anyone"));
146 assert!(user_can_build(&m, "drzzln"));
147 }
148
149 #[test]
150 fn sealed_only_lets_root_build() {
151 let m = load_named("sealed-compliance").unwrap();
152 assert!(user_can_build(&m, "root"));
153 assert!(!user_can_build(&m, "drzzln"));
154 }
155
156 #[test]
157 fn cache_nixos_org_is_trusted_in_default_model() {
158 let m = load_named("multi-user-default").unwrap();
159 assert!(substituter_trusted(&m, "https://cache.nixos.org"));
160 assert!(!substituter_trusted(&m, "https://untrusted.example.com"));
161 }
162
163 #[test]
164 fn cache_nixos_key_recognized_by_name_prefix() {
165 let m = load_named("multi-user-default").unwrap();
166 assert!(key_trusted(&m, "cache.nixos.org-1"));
167 assert!(!key_trusted(&m, "random-untrusted-key"));
168 }
169}