Skip to main content

tf_types/
profile.rs

1//! Profile selection runtime — Rust mirror of `tools/tf-types-ts/src/core/profile.ts`.
2//!
3//! `select_profile(spec, gate)` checks a `ProfileSpec` (parsed from
4//! profile-spec.schema.json) against a `ProfileFeatureGate` describing
5//! the running daemon. Built-in profiles are exposed via
6//! [`builtin_profiles`].
7
8use 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
203/// The four built-in conformance profiles. Mirrors `BUILTIN_PROFILES` in
204/// `tools/tf-types-ts/src/core/profile.ts`.
205pub 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
294/// Lookup a built-in profile by id.
295pub fn builtin_profile(id: &str) -> Option<ProfileSpec> {
296    builtin_profiles().into_iter().find(|p| p.profile_id == id)
297}