1use crate::crypto::TrustAnchorPublicKey;
13use crate::error::CellosError;
14use crate::trust_keys::Signer;
15use crate::types::CloudEventV1;
16use crate::{sign_event_with, verify_signed_event_envelope, SignedEventEnvelopeV1};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20pub const HOST_BASELINE_ATTESTATION_TYPE: &str =
22 "dev.cellos.events.cell.host.v1.baseline-attestation";
23
24pub const BASELINE_CONTROLS: &[&str] =
26 &["user-namespaces", "cgroup-v2", "seccomp", "nftables", "kvm"];
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub struct BaselineControl {
32 pub name: String,
34 pub satisfied: bool,
36 pub detail: String,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct HostBaselineAttestationV1 {
44 pub schema_version: String,
46 pub probe_mode: String,
49 pub controls: Vec<BaselineControl>,
51 pub all_satisfied: bool,
53}
54
55#[cfg(target_os = "linux")]
56fn probe_mode() -> &'static str {
57 "linux-probed"
58}
59#[cfg(not(target_os = "linux"))]
60fn probe_mode() -> &'static str {
61 "declared-unsupported"
62}
63
64#[cfg(target_os = "linux")]
65fn probe_one(name: &str) -> (bool, String) {
66 use std::path::Path;
67 let exists = |p: &str| {
68 if Path::new(p).exists() {
69 (true, format!("{p} present"))
70 } else {
71 (false, format!("{p} absent"))
72 }
73 };
74 match name {
75 "user-namespaces" => exists("/proc/self/ns/user"),
76 "cgroup-v2" => exists("/sys/fs/cgroup/cgroup.controllers"),
77 "seccomp" => match std::fs::read_to_string("/proc/self/status") {
78 Ok(s) if s.contains("Seccomp:") => (
79 true,
80 "/proc/self/status carries a Seccomp field".to_string(),
81 ),
82 Ok(_) => (false, "no Seccomp field in /proc/self/status".to_string()),
83 Err(e) => (false, format!("read /proc/self/status: {e}")),
84 },
85 "nftables" => exists("/proc/sys/net/netfilter"),
89 "kvm" => exists("/dev/kvm"),
90 _ => (false, "unknown control".to_string()),
91 }
92}
93#[cfg(not(target_os = "linux"))]
94fn probe_one(_name: &str) -> (bool, String) {
95 (
96 false,
97 "not a Linux host — declared-audit, out of accreditation scope (ADR-0033)".to_string(),
98 )
99}
100
101#[must_use]
106pub fn probe_host_baseline() -> HostBaselineAttestationV1 {
107 probe_host_baseline_with_forced(&[])
108}
109
110#[must_use]
115pub fn probe_host_baseline_with_forced(forced_missing: &[&str]) -> HostBaselineAttestationV1 {
116 let controls: Vec<BaselineControl> = BASELINE_CONTROLS
117 .iter()
118 .map(|&name| {
119 let force = forced_missing.contains(&name);
120 let (satisfied, detail) = if force {
121 (false, "forced missing (test override)".to_string())
122 } else {
123 probe_one(name)
124 };
125 BaselineControl {
126 name: name.to_string(),
127 satisfied,
128 detail,
129 }
130 })
131 .collect();
132 let all_satisfied = controls.iter().all(|c| c.satisfied);
133
134 HostBaselineAttestationV1 {
135 schema_version: "1.0.0".to_string(),
136 probe_mode: probe_mode().to_string(),
137 controls,
138 all_satisfied,
139 }
140}
141
142pub fn host_baseline_event(
154 attestation: &HostBaselineAttestationV1,
155) -> Result<CloudEventV1, CellosError> {
156 let data = serde_json::to_value(attestation).map_err(|e| {
157 CellosError::InvalidSpec(format!("host-baseline attestation: serialize: {e}"))
158 })?;
159 Ok(CloudEventV1 {
160 specversion: "1.0".into(),
161 id: "host-baseline".into(),
162 source: "cellos-supervisor".into(),
163 ty: HOST_BASELINE_ATTESTATION_TYPE.into(),
164 datacontenttype: Some("application/json".into()),
165 data: Some(data),
166 time: None,
167 traceparent: None,
168 cex: None,
169 })
170}
171
172pub fn sign_host_baseline_attestation(
179 signer: &dyn Signer,
180 attestation: &HostBaselineAttestationV1,
181) -> Result<SignedEventEnvelopeV1, CellosError> {
182 let event = host_baseline_event(attestation)?;
183 sign_event_with(signer, &event)
184}
185
186pub fn verify_host_baseline_attestation(
193 envelope: &SignedEventEnvelopeV1,
194 verifying_keys: &HashMap<String, TrustAnchorPublicKey>,
195) -> Result<HostBaselineAttestationV1, CellosError> {
196 let hmac: HashMap<String, Vec<u8>> = HashMap::new();
197 let event = verify_signed_event_envelope(envelope, verifying_keys, &hmac)?;
198 let data = event
199 .data
200 .as_ref()
201 .ok_or_else(|| CellosError::InvalidSpec("host-baseline: missing data".into()))?;
202 serde_json::from_value(data.clone())
203 .map_err(|e| CellosError::InvalidSpec(format!("host-baseline: parse: {e}")))
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::SoftwareSigner;
210
211 #[test]
212 fn probe_enumerates_all_baseline_controls() {
213 let att = probe_host_baseline();
214 assert_eq!(att.controls.len(), BASELINE_CONTROLS.len());
215 for (c, expected) in att.controls.iter().zip(BASELINE_CONTROLS) {
216 assert_eq!(&c.name, expected, "controls are in the stable order");
217 }
218 }
219
220 #[test]
221 fn forced_missing_control_is_unsatisfied_and_blocks_all() {
222 let att = probe_host_baseline_with_forced(&["seccomp"]);
223 let seccomp = att.controls.iter().find(|c| c.name == "seccomp").unwrap();
224 assert!(!seccomp.satisfied, "forced control must be unsatisfied");
225 assert!(seccomp.detail.contains("forced"));
226 assert!(
227 !att.all_satisfied,
228 "a single missing control blocks all_satisfied"
229 );
230 }
231
232 #[test]
233 fn host_baseline_event_carries_attestation_and_type() {
234 let att = probe_host_baseline();
235 let event = host_baseline_event(&att).expect("build event");
236 assert_eq!(event.ty, HOST_BASELINE_ATTESTATION_TYPE);
237 let parsed: HostBaselineAttestationV1 =
238 serde_json::from_value(event.data.expect("data present")).expect("parse back");
239 assert_eq!(parsed, att, "event body round-trips to the attestation");
240 }
241
242 #[test]
243 fn attestation_signs_and_verifies_offline() {
244 let att = probe_host_baseline();
245 let signer = SoftwareSigner::from_seed("org-root-2026", [8u8; 32]).unwrap();
246 let envelope = sign_host_baseline_attestation(&signer, &att).unwrap();
247
248 use crate::trust_keys::Signer as _;
249 let mut keys = HashMap::new();
250 keys.insert("org-root-2026".to_string(), signer.verifying_key());
251 let verified = verify_host_baseline_attestation(&envelope, &keys)
252 .expect("host-baseline attestation verifies offline");
253 assert_eq!(verified, att, "round-trips through sign+verify");
254
255 let mut tampered = envelope;
257 tampered.event.id = "tampered".into();
258 assert!(verify_host_baseline_attestation(&tampered, &keys).is_err());
259 }
260}