// SPDX-License-Identifier: AGPL-3.0-only
//! What the caller hands the guest beside the bundle bytes. The guest has no
//! clock, no file and no network, so everything it decides against arrives here:
//! the instant, Holger's trust policy, the target, the anti-rollback state and
//! the pinned DSSE attestation signers.
//!
//! The format is one RON document:
//!
//! ```ron
//! VerifyParams(
//! now: "2026-10-30T12:00:00Z",
//! policy: ( schema_version: "holger.bundle-trust-policy/v1", trust_mode: Some(PinnedSigners), ... ),
//! target: (product: Some("acme-fw")),
//! accepted: Some([(sequence: 4, bundle_id: "b-4", core_digest: "…")]),
//! dsse: (trusted_signers: ["<sha256 of the attestation signer certificate DER>"]),
//! emit: Content,
//! )
//! ```
//!
//! `policy` is the body of a Holger trust-policy file, pasted as is.
//!
//! Heimdall's job parameters are string pairs, so the same document can also
//! arrive key by key ([`VerifyParams::from_pairs`]): `now` (RFC 3339),
//! `policy` (the trust-policy RON file, verbatim), `product`, `model`,
//! `revision`, `accepted` (RON list), `dsse` (RON), `emit` (`Content`, `All`
//! or `Report`). An unknown key is a refusal, never ignored.
use chrono::{DateTime, Utc};
use serde::Deserialize;
use crate::model::{AcceptedEntry, TrustPolicy};
use crate::refusal::{Refusal, Result};
/// The largest parameter document the guest reads.
pub const MAX_PARAMS_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TargetSpec {
#[serde(default)]
pub product: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub revision: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct DsseParams {
/// SHA-256 fingerprints (hex) of the certificates whose DSSE signatures are
/// believed. A DSSE envelope signed by anyone else is refused.
#[serde(default)]
pub trusted_signers: Vec<String>,
/// Pinned raw public keys (PEM `PUBLIC KEY` or hex SPKI DER); a signature
/// whose keyid is sha256(SPKI DER) of one of them is checked against it.
#[serde(default)]
pub keys: Vec<String>,
/// Extra signer certificates (hex DER) for keyids the bundle does not carry.
#[serde(default)]
pub certificates: Vec<String>,
/// Refuse a bundle that carries no DSSE envelope at all.
#[serde(default)]
pub require_envelope: bool,
}
/// Which outputs an accepted bundle emits.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
pub enum Emit {
/// The report plus every `payload/**` and `evidence/**` member.
#[default]
Content,
/// The report plus every member, control members included.
All,
/// The report only.
Report,
}
#[derive(Debug, Clone, Deserialize)]
pub struct VerifyParams {
pub now: DateTime<Utc>,
pub policy: TrustPolicy,
#[serde(default)]
pub target: TargetSpec,
/// `None`: no anti-rollback check (the caller does it). `Some(list)`:
/// Holger's `check_admissible` against that list.
#[serde(default)]
pub accepted: Option<Vec<AcceptedEntry>>,
#[serde(default)]
pub dsse: DsseParams,
#[serde(default)]
pub emit: Emit,
}
fn ron_field<T: serde::de::DeserializeOwned>(key: &str, v: &str) -> Result<T> {
ron::from_str(v).map_err(|e| Refusal::Params(format!("{key}: {e}")))
}
impl VerifyParams {
/// Build the parameters from string pairs (Heimdall's `Params`).
pub fn from_pairs<'a>(pairs: impl IntoIterator<Item = (&'a str, &'a str)>) -> Result<Self> {
let mut now = None;
let mut policy = None;
let mut target = TargetSpec::default();
let mut accepted = None;
let mut dsse = DsseParams::default();
let mut emit = Emit::default();
let mut total = 0usize;
for (k, v) in pairs {
total = total.saturating_add(k.len() + v.len());
if total > MAX_PARAMS_BYTES {
return Err(Refusal::Params(format!(
"over the {MAX_PARAMS_BYTES}-byte cap"
)));
}
match k {
"now" => {
now = Some(
v.parse::<DateTime<Utc>>()
.map_err(|e| Refusal::Params(format!("now: {e}")))?,
)
}
"policy" => policy = Some(ron_field::<TrustPolicy>(k, v)?),
"product" => target.product = Some(v.to_string()),
"model" => target.model = Some(v.to_string()),
"revision" => target.revision = Some(v.to_string()),
"accepted" => accepted = Some(ron_field::<Vec<AcceptedEntry>>(k, v)?),
"dsse" => dsse = ron_field::<DsseParams>(k, v)?,
"emit" => emit = ron_field::<Emit>(k, v)?,
other => return Err(Refusal::Params(format!("unknown key {other:?}"))),
}
}
let p = VerifyParams {
now: now.ok_or_else(|| Refusal::Params("missing key \"now\"".into()))?,
policy: policy.ok_or_else(|| Refusal::Params("missing key \"policy\"".into()))?,
target,
accepted,
dsse,
emit,
};
p.policy.check_schema()?;
p.policy.trust_mode()?;
Ok(p)
}
pub fn from_ron_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() > MAX_PARAMS_BYTES {
return Err(Refusal::Params(format!(
"{} bytes, over the {MAX_PARAMS_BYTES}-byte cap",
bytes.len()
)));
}
let text = core::str::from_utf8(bytes).map_err(|_| Refusal::Params("not UTF-8".into()))?;
let p: VerifyParams = ron::from_str(text).map_err(|e| Refusal::Params(e.to_string()))?;
p.policy.check_schema()?;
p.policy.trust_mode()?;
Ok(p)
}
}