update-bundle/src/core.rs
//! `core.json` — the immutable release candidate (`holger.update-bundle/v1`).
//!
//! `core.json` is written to disk **once**; every downstream digest and signature
//! is computed over those exact stored bytes. The struct here is only used to
//! *produce* those bytes (at prepare time) and to *read* fields during verify —
//! never to recompute the candidate digest by reserialization.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::schema::BUNDLE_SCHEMA;
/// The immutable release candidate.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BundleCore {
/// Must equal [`crate::schema::BUNDLE_SCHEMA`].
pub schema_version: String,
/// Globally-unique identifier for this bundle (opaque; anti-rollback keys on it).
pub bundle_id: String,
/// Monotonic per-`(supplier, product)` sequence number. Anti-rollback rejects
/// anything not strictly newer than the highest accepted sequence.
pub sequence: u64,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub expires_at: Option<DateTime<Utc>>,
pub producer: String,
pub supplier: String,
pub product: String,
pub release_version: String,
#[serde(default)]
pub supported_hardware_models: Vec<String>,
#[serde(default)]
pub supported_hardware_revisions: Vec<String>,
pub source_provenance: SourceProvenance,
#[serde(default)]
pub payloads: Vec<PayloadEntry>,
#[serde(default)]
pub evidence: Vec<EvidenceEntry>,
pub policy_result: PolicyResult,
#[serde(default)]
pub supersedes: Vec<String>,
#[serde(default)]
pub withdraws: Vec<String>,
}
/// Where the artifact came from, recorded so every byte is traceable to its source.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceProvenance {
pub source_kind: String,
#[serde(default)]
pub source_uri: Option<String>,
#[serde(default)]
pub source_repository: Option<String>,
#[serde(default)]
pub source_asset_path: Option<String>,
#[serde(default)]
pub fetched_at: Option<DateTime<Utc>>,
#[serde(default)]
pub reported_size: Option<u64>,
#[serde(default)]
pub vendor_checksum: Option<String>,
#[serde(default)]
pub vendor_signature_status: Option<String>,
#[serde(default)]
pub tls_origin: Option<String>,
/// SHA-256 of the strict transfer receipt embedded under `evidence/source-receipt.json`.
#[serde(default)]
pub transfer_receipt_digest: Option<String>,
}
/// A payload artifact (firmware / installer / archive) carried in `payload/`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PayloadEntry {
pub relative_path: String,
pub media_type: String,
pub size_bytes: u64,
pub sha256: String,
pub blake3: String,
}
/// The kind of an evidence document.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EvidenceKind {
SourceReceipt,
Sbom,
Vex,
Scan,
Advisory,
Exception,
Other,
}
/// An evidence document carried in `evidence/`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceEntry {
pub relative_path: String,
pub media_type: String,
pub size_bytes: u64,
pub sha256: String,
pub blake3: String,
pub evidence_kind: EvidenceKind,
}
/// The recorded policy verdict + the exact advisory/policy snapshot it was made against.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyResult {
/// Digest of the policy document the review was made under.
pub policy_digest: String,
/// Digest of the advisory-DB snapshot the review used.
#[serde(default)]
pub advisory_snapshot_digest: Option<String>,
/// The reviewers' verdict. It is part of core.json, so the approvals and the
/// release signature cover it. Only exactly `"pass"` is admitted: every other
/// value (`"block"`, `"warn"`, `""`, `"PASS"`, `"pass-with-exceptions"`) is
/// refused by [`BundleCore::require_pass_verdict`] at finalize, at verify and
/// at import.
pub verdict: String,
/// True unless a real production advisory snapshot was supplied. The demo
/// advisory DB is never claimed to be production vulnerability intelligence.
#[serde(default = "default_true")]
pub non_production: bool,
}
fn default_true() -> bool {
true
}
impl BundleCore {
/// The one verdict a bundle may carry and still be released or imported.
pub const PASS_VERDICT: &'static str = "pass";
/// Refuse, by name, a bundle whose recorded verdict is not exactly `pass`.
pub fn require_pass_verdict(&self) -> crate::error::Result<()> {
if self.policy_result.verdict == Self::PASS_VERDICT {
Ok(())
} else {
Err(crate::error::BundleError::VerdictNotPass(
self.policy_result.verdict.clone(),
))
}
}
/// Serialize to the exact bytes that become `core.json`. Deterministic
/// (serde_json preserves struct field order + Vec order).
pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec_pretty(self)
}
/// Parse from stored `core.json` bytes.
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
/// True if the schema version is the one this crate understands.
pub fn schema_ok(&self) -> bool {
self.schema_version == BUNDLE_SCHEMA
}
/// True if at least one evidence entry of `kind` is present.
pub fn has_evidence_kind(&self, kind: EvidenceKind) -> bool {
self.evidence.iter().any(|e| e.evidence_kind == kind)
}
}