Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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)
    }
}