1use std::collections::BTreeSet;
9
10use crate::generated::common::{EnforcementLevel, ProofLevel};
11use crate::generated::profile_spec::{
12 Feature, ProfileSpec, ProfileSpec_MinEnforcementLevel, ProfileSpec_ProfileVersion,
13 ProfileSpec_RequiredAnchors_Item,
14};
15
16#[derive(Debug, Clone)]
17pub struct ProfileFeatureGate {
18 pub features: BTreeSet<String>,
19 pub enforcement_level: EnforcementLevel,
20 pub proof_level_floor: ProofLevel,
21 pub bridges: BTreeSet<String>,
22 pub anchors: BTreeSet<String>,
23}
24
25#[derive(Debug, Clone)]
26pub struct ProfileVerdict {
27 pub ok: bool,
28 pub profile: String,
29 pub failures: Vec<String>,
30 pub warnings: Vec<String>,
31}
32
33fn enf_rank(e: &EnforcementLevel) -> u8 {
34 match e {
35 EnforcementLevel::E0 => 0,
36 EnforcementLevel::E1 => 1,
37 EnforcementLevel::E2 => 2,
38 EnforcementLevel::E3 => 3,
39 EnforcementLevel::E4 => 4,
40 EnforcementLevel::E5 => 5,
41 }
42}
43
44fn min_enf_rank(e: &ProfileSpec_MinEnforcementLevel) -> u8 {
45 match e {
46 ProfileSpec_MinEnforcementLevel::E0 => 0,
47 ProfileSpec_MinEnforcementLevel::E1 => 1,
48 ProfileSpec_MinEnforcementLevel::E2 => 2,
49 ProfileSpec_MinEnforcementLevel::E3 => 3,
50 ProfileSpec_MinEnforcementLevel::E4 => 4,
51 ProfileSpec_MinEnforcementLevel::E5 => 5,
52 }
53}
54
55fn proof_rank(p: &ProofLevel) -> u8 {
56 match p {
57 ProofLevel::L0 => 0,
58 ProofLevel::L1 => 1,
59 ProofLevel::L2 => 2,
60 ProofLevel::L3 => 3,
61 ProofLevel::L4 => 4,
62 ProofLevel::L5 => 5,
63 }
64}
65
66fn enf_label(e: &EnforcementLevel) -> &'static str {
67 match e {
68 EnforcementLevel::E0 => "E0",
69 EnforcementLevel::E1 => "E1",
70 EnforcementLevel::E2 => "E2",
71 EnforcementLevel::E3 => "E3",
72 EnforcementLevel::E4 => "E4",
73 EnforcementLevel::E5 => "E5",
74 }
75}
76
77fn min_enf_label(e: &ProfileSpec_MinEnforcementLevel) -> &'static str {
78 match e {
79 ProfileSpec_MinEnforcementLevel::E0 => "E0",
80 ProfileSpec_MinEnforcementLevel::E1 => "E1",
81 ProfileSpec_MinEnforcementLevel::E2 => "E2",
82 ProfileSpec_MinEnforcementLevel::E3 => "E3",
83 ProfileSpec_MinEnforcementLevel::E4 => "E4",
84 ProfileSpec_MinEnforcementLevel::E5 => "E5",
85 }
86}
87
88fn proof_label(p: &ProofLevel) -> &'static str {
89 match p {
90 ProofLevel::L0 => "L0",
91 ProofLevel::L1 => "L1",
92 ProofLevel::L2 => "L2",
93 ProofLevel::L3 => "L3",
94 ProofLevel::L4 => "L4",
95 ProofLevel::L5 => "L5",
96 }
97}
98
99fn anchor_id(a: &ProfileSpec_RequiredAnchors_Item) -> &'static str {
100 match a {
101 ProfileSpec_RequiredAnchors_Item::Rfc6962 => "rfc6962",
102 ProfileSpec_RequiredAnchors_Item::Sigstore => "sigstore",
103 ProfileSpec_RequiredAnchors_Item::Rfc3161 => "rfc3161",
104 ProfileSpec_RequiredAnchors_Item::Memory => "memory",
105 ProfileSpec_RequiredAnchors_Item::Custom => "custom",
106 }
107}
108
109pub fn select_profile(spec: &ProfileSpec, gate: &ProfileFeatureGate) -> ProfileVerdict {
110 let mut failures = Vec::new();
111 let mut warnings = Vec::new();
112
113 for m in &spec.must {
114 if !gate.features.contains(&m.id) {
115 failures.push(format!(
116 "profile {} requires feature \"{}\" — missing",
117 spec.profile_id, m.id
118 ));
119 }
120 }
121
122 if let Some(must_not) = &spec.must_not {
123 for n in must_not {
124 if gate.features.contains(&n.id) {
125 failures.push(format!(
126 "profile {} forbids feature \"{}\"",
127 spec.profile_id, n.id
128 ));
129 }
130 }
131 }
132
133 for s in &spec.should {
134 if !gate.features.contains(&s.id) {
135 warnings.push(format!(
136 "profile {} recommends feature \"{}\"",
137 spec.profile_id, s.id
138 ));
139 }
140 }
141
142 if let Some(min) = &spec.min_enforcement_level {
143 if enf_rank(&gate.enforcement_level) < min_enf_rank(min) {
144 failures.push(format!(
145 "profile {} requires EnforcementLevel ≥ {}, daemon at {}",
146 spec.profile_id,
147 min_enf_label(min),
148 enf_label(&gate.enforcement_level)
149 ));
150 }
151 }
152
153 if let Some(min) = &spec.min_proof_level {
154 if proof_rank(&gate.proof_level_floor) < proof_rank(min) {
155 failures.push(format!(
156 "profile {} requires proof level floor ≥ {}, daemon at {}",
157 spec.profile_id,
158 proof_label(min),
159 proof_label(&gate.proof_level_floor)
160 ));
161 }
162 }
163
164 if let Some(bridges) = &spec.required_bridges {
165 for b in bridges {
166 if !gate.bridges.contains(b) {
167 failures.push(format!(
168 "profile {} requires bridge {} — missing",
169 spec.profile_id, b
170 ));
171 }
172 }
173 }
174
175 if let Some(anchors) = &spec.required_anchors {
176 for a in anchors {
177 let id = anchor_id(a);
178 if !gate.anchors.contains(id) {
179 failures.push(format!(
180 "profile {} requires anchor {} — missing",
181 spec.profile_id, id
182 ));
183 }
184 }
185 }
186
187 ProfileVerdict {
188 ok: failures.is_empty(),
189 profile: spec.profile_id.clone(),
190 failures,
191 warnings,
192 }
193}
194
195fn feature(id: &str) -> Feature {
196 Feature {
197 id: id.to_string(),
198 description: None,
199 spec_ref: None,
200 }
201}
202
203pub fn builtin_profiles() -> Vec<ProfileSpec> {
206 vec![
207 ProfileSpec {
208 profile_version: ProfileSpec_ProfileVersion::V1,
209 profile_id: "tf-home-compatible".into(),
210 label: "TrustForge home / personal-network profile".into(),
211 description: None,
212 must: vec![
213 feature("agent-contract"),
214 feature("proof-log"),
215 feature("ed25519"),
216 feature("vault"),
217 ],
218 should: vec![feature("webauthn"), feature("shadow-mode")],
219 must_not: None,
220 min_enforcement_level: Some(ProfileSpec_MinEnforcementLevel::E3),
221 min_proof_level: Some(ProofLevel::L1),
222 required_bridges: None,
223 required_anchors: None,
224 },
225 ProfileSpec {
226 profile_version: ProfileSpec_ProfileVersion::V1,
227 profile_id: "tf-enterprise-compatible".into(),
228 label: "TrustForge enterprise profile".into(),
229 description: None,
230 must: vec![
231 feature("policy-engine"),
232 feature("quorum-collector"),
233 feature("continuous-reauth"),
234 feature("transparency-anchor.any"),
235 feature("federation"),
236 feature("webauthn"),
237 feature("agent-contract"),
238 ],
239 should: vec![feature("shadow-mode"), feature("hybrid-pq")],
240 must_not: None,
241 min_enforcement_level: Some(ProfileSpec_MinEnforcementLevel::E4),
242 min_proof_level: Some(ProofLevel::L2),
243 required_bridges: Some(vec!["webauthn".into(), "oauth".into(), "spiffe".into()]),
244 required_anchors: Some(vec![ProfileSpec_RequiredAnchors_Item::Rfc6962]),
245 },
246 ProfileSpec {
247 profile_version: ProfileSpec_ProfileVersion::V1,
248 profile_id: "tf-constrained-compatible".into(),
249 label: "TrustForge constrained / LoRa / offline profile".into(),
250 description: None,
251 must: vec![
252 feature("packet-mode"),
253 feature("fragment-reassembly"),
254 feature("offline-revocation-list"),
255 feature("emergency-authority"),
256 ],
257 should: vec![feature("cbor-encoding"), feature("deflate-compression")],
258 must_not: Some(vec![
259 feature("transport.websocket-only"),
260 feature("transparency-anchor.always-online"),
261 ]),
262 min_enforcement_level: Some(ProfileSpec_MinEnforcementLevel::E3),
263 min_proof_level: Some(ProofLevel::L1),
264 required_bridges: None,
265 required_anchors: None,
266 },
267 ProfileSpec {
268 profile_version: ProfileSpec_ProfileVersion::V1,
269 profile_id: "tf-compliance-evidence-compatible".into(),
270 label: "TrustForge compliance / legal-evidence profile".into(),
271 description: None,
272 must: vec![
273 feature("policy-engine"),
274 feature("quorum-collector"),
275 feature("signed-log-events"),
276 feature("evidence-bundle"),
277 feature("l4-encrypted-bundle"),
278 feature("l5-rfc3161-anchor"),
279 feature("continuous-reauth"),
280 ],
281 should: vec![feature("redaction"), feature("federation")],
282 must_not: None,
283 min_enforcement_level: Some(ProfileSpec_MinEnforcementLevel::E4),
284 min_proof_level: Some(ProofLevel::L3),
285 required_bridges: None,
286 required_anchors: Some(vec![
287 ProfileSpec_RequiredAnchors_Item::Rfc6962,
288 ProfileSpec_RequiredAnchors_Item::Rfc3161,
289 ]),
290 },
291 ]
292}
293
294pub fn builtin_profile(id: &str) -> Option<ProfileSpec> {
296 builtin_profiles().into_iter().find(|p| p.profile_id == id)
297}