use crate::crypto::TrustAnchorPublicKey;
use crate::error::CellosError;
use crate::trust_keys::Signer;
use crate::types::CloudEventV1;
use crate::{sign_event_with, verify_signed_event_envelope, SignedEventEnvelopeV1};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const HOST_BASELINE_ATTESTATION_TYPE: &str =
"dev.cellos.events.cell.host.v1.baseline-attestation";
pub const BASELINE_CONTROLS: &[&str] =
&["user-namespaces", "cgroup-v2", "seccomp", "nftables", "kvm"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BaselineControl {
pub name: String,
pub satisfied: bool,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostBaselineAttestationV1 {
pub schema_version: String,
pub probe_mode: String,
pub controls: Vec<BaselineControl>,
pub all_satisfied: bool,
}
#[cfg(target_os = "linux")]
fn probe_mode() -> &'static str {
"linux-probed"
}
#[cfg(not(target_os = "linux"))]
fn probe_mode() -> &'static str {
"declared-unsupported"
}
#[cfg(target_os = "linux")]
fn probe_one(name: &str) -> (bool, String) {
use std::path::Path;
let exists = |p: &str| {
if Path::new(p).exists() {
(true, format!("{p} present"))
} else {
(false, format!("{p} absent"))
}
};
match name {
"user-namespaces" => exists("/proc/self/ns/user"),
"cgroup-v2" => exists("/sys/fs/cgroup/cgroup.controllers"),
"seccomp" => match std::fs::read_to_string("/proc/self/status") {
Ok(s) if s.contains("Seccomp:") => (
true,
"/proc/self/status carries a Seccomp field".to_string(),
),
Ok(_) => (false, "no Seccomp field in /proc/self/status".to_string()),
Err(e) => (false, format!("read /proc/self/status: {e}")),
},
"nftables" => exists("/proc/sys/net/netfilter"),
"kvm" => exists("/dev/kvm"),
_ => (false, "unknown control".to_string()),
}
}
#[cfg(not(target_os = "linux"))]
fn probe_one(_name: &str) -> (bool, String) {
(
false,
"not a Linux host — declared-audit, out of accreditation scope (ADR-0033)".to_string(),
)
}
#[must_use]
pub fn probe_host_baseline() -> HostBaselineAttestationV1 {
probe_host_baseline_with_forced(&[])
}
#[must_use]
pub fn probe_host_baseline_with_forced(forced_missing: &[&str]) -> HostBaselineAttestationV1 {
let controls: Vec<BaselineControl> = BASELINE_CONTROLS
.iter()
.map(|&name| {
let force = forced_missing.contains(&name);
let (satisfied, detail) = if force {
(false, "forced missing (test override)".to_string())
} else {
probe_one(name)
};
BaselineControl {
name: name.to_string(),
satisfied,
detail,
}
})
.collect();
let all_satisfied = controls.iter().all(|c| c.satisfied);
HostBaselineAttestationV1 {
schema_version: "1.0.0".to_string(),
probe_mode: probe_mode().to_string(),
controls,
all_satisfied,
}
}
pub fn host_baseline_event(
attestation: &HostBaselineAttestationV1,
) -> Result<CloudEventV1, CellosError> {
let data = serde_json::to_value(attestation).map_err(|e| {
CellosError::InvalidSpec(format!("host-baseline attestation: serialize: {e}"))
})?;
Ok(CloudEventV1 {
specversion: "1.0".into(),
id: "host-baseline".into(),
source: "cellos-supervisor".into(),
ty: HOST_BASELINE_ATTESTATION_TYPE.into(),
datacontenttype: Some("application/json".into()),
data: Some(data),
time: None,
traceparent: None,
cex: None,
})
}
pub fn sign_host_baseline_attestation(
signer: &dyn Signer,
attestation: &HostBaselineAttestationV1,
) -> Result<SignedEventEnvelopeV1, CellosError> {
let event = host_baseline_event(attestation)?;
sign_event_with(signer, &event)
}
pub fn verify_host_baseline_attestation(
envelope: &SignedEventEnvelopeV1,
verifying_keys: &HashMap<String, TrustAnchorPublicKey>,
) -> Result<HostBaselineAttestationV1, CellosError> {
let hmac: HashMap<String, Vec<u8>> = HashMap::new();
let event = verify_signed_event_envelope(envelope, verifying_keys, &hmac)?;
let data = event
.data
.as_ref()
.ok_or_else(|| CellosError::InvalidSpec("host-baseline: missing data".into()))?;
serde_json::from_value(data.clone())
.map_err(|e| CellosError::InvalidSpec(format!("host-baseline: parse: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SoftwareSigner;
#[test]
fn probe_enumerates_all_baseline_controls() {
let att = probe_host_baseline();
assert_eq!(att.controls.len(), BASELINE_CONTROLS.len());
for (c, expected) in att.controls.iter().zip(BASELINE_CONTROLS) {
assert_eq!(&c.name, expected, "controls are in the stable order");
}
}
#[test]
fn forced_missing_control_is_unsatisfied_and_blocks_all() {
let att = probe_host_baseline_with_forced(&["seccomp"]);
let seccomp = att.controls.iter().find(|c| c.name == "seccomp").unwrap();
assert!(!seccomp.satisfied, "forced control must be unsatisfied");
assert!(seccomp.detail.contains("forced"));
assert!(
!att.all_satisfied,
"a single missing control blocks all_satisfied"
);
}
#[test]
fn host_baseline_event_carries_attestation_and_type() {
let att = probe_host_baseline();
let event = host_baseline_event(&att).expect("build event");
assert_eq!(event.ty, HOST_BASELINE_ATTESTATION_TYPE);
let parsed: HostBaselineAttestationV1 =
serde_json::from_value(event.data.expect("data present")).expect("parse back");
assert_eq!(parsed, att, "event body round-trips to the attestation");
}
#[test]
fn attestation_signs_and_verifies_offline() {
let att = probe_host_baseline();
let signer = SoftwareSigner::from_seed("org-root-2026", [8u8; 32]).unwrap();
let envelope = sign_host_baseline_attestation(&signer, &att).unwrap();
use crate::trust_keys::Signer as _;
let mut keys = HashMap::new();
keys.insert("org-root-2026".to_string(), signer.verifying_key());
let verified = verify_host_baseline_attestation(&envelope, &keys)
.expect("host-baseline attestation verifies offline");
assert_eq!(verified, att, "round-trips through sign+verify");
let mut tampered = envelope;
tampered.event.id = "tampered".into();
assert!(verify_host_baseline_attestation(&tampered, &keys).is_err());
}
}