intel/src/bundle.rs
//! The online side's last step: an [`AdvisorySet`] becomes a signed
//! `holger.update-bundle/v1` with `product = holger.intel/v1`.
//!
//! Nothing here is a new format. The set is the bundle's one payload, the feed
//! record is its `Advisory` evidence, and `update-bundle`'s own `Preparation`,
//! `add_approval` and `finalize` do the hashing, the approvals and the release
//! signature — so `holger-verify` verifies an intel bundle exactly like a
//! firmware one, and `finalize` refuses to emit one that does not verify.
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use holger_update_bundle::core::{EvidenceKind, PolicyResult, SourceProvenance};
use holger_update_bundle::digest::sha256_hex;
use holger_update_bundle::finalize::{add_approval, finalize};
use holger_update_bundle::prepare::{EvidenceInput, PayloadInput, Preparation};
use holger_update_bundle::{pack_bundle, TrustPolicy, VerifyReport};
use crate::set::AdvisorySet;
use crate::{ADVISORIES_MEDIA_TYPE, ADVISORIES_REL, INTEL_PRODUCT, SOURCES_REL};
/// One signer's certificate (DER) and ed25519 key (PKCS#8 DER). The key never
/// enters the bundle and is never printed.
pub struct SignerFiles {
pub cert_der: Vec<u8>,
pub key_pkcs8: Vec<u8>,
}
impl SignerFiles {
pub fn read(cert: &Path, key: &Path) -> Result<Self> {
Ok(Self {
cert_der: std::fs::read(cert)
.with_context(|| format!("read cert {}", cert.display()))?,
key_pkcs8: std::fs::read(key).with_context(|| format!("read key {}", key.display()))?,
})
}
}
/// What the bundle says about itself, and who signs it.
pub struct BuildOptions {
/// Monotonic per `(supplier, holger.intel/v1)`. The secure side refuses a
/// bundle whose sequence is below the one it already holds.
pub sequence: u64,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub supplier: String,
pub producer: String,
/// The trust policy the downstream verifies against; `finalize` checks the
/// bundle under it before it is written.
pub policy: TrustPolicy,
/// Approvals, `(role, signer)`, as many as the policy's two-person rule asks.
pub approvals: Vec<(String, SignerFiles)>,
pub release: SignerFiles,
pub out: PathBuf,
}
/// The written bundle.
pub struct BuiltBundle {
pub path: PathBuf,
pub bundle_id: String,
pub sequence: u64,
pub advisories: usize,
pub advisories_sha256: String,
pub report: VerifyReport,
}
/// The default sequence for a bundle built at `t`: `YYYYMMDDHHMMSS`, which only
/// grows as long as the builder's clock does.
pub fn sequence_for(t: DateTime<Utc>) -> u64 {
t.format("%Y%m%d%H%M%S").to_string().parse().unwrap_or(0)
}
/// Sign `set` into a bundle at `opts.out`.
pub fn build_bundle(set: &AdvisorySet, opts: BuildOptions) -> Result<BuiltBundle> {
let payload = set.to_json_bytes()?;
let digest = sha256_hex(&payload);
let payloads = vec![PayloadInput {
relative_path: ADVISORIES_REL.into(),
bytes: payload,
media_type: ADVISORIES_MEDIA_TYPE.into(),
}];
sign_payloads(set, digest, payloads, opts)
}
/// Sign a **delta** intel bundle (feature `advisory-feed`, lane cve-znippy): the advisory
/// feed's delta from `previous` (the set at `previous_sequence`, the sequence the box holds)
/// to `set`, plus the new set's supply-chain section. A few MB where the full set is 884 MB.
/// The box takes it only on top of exactly `previous_sequence`.
#[cfg(feature = "advisory-feed")]
pub fn build_delta_bundle(
set: &AdvisorySet,
previous: &AdvisorySet,
previous_sequence: u64,
opts: BuildOptions,
) -> Result<BuiltBundle> {
anyhow::ensure!(
opts.sequence > previous_sequence,
"a delta must move the sequence forward ({} -> {})",
previous_sequence,
opts.sequence
);
let dir = tempfile::tempdir().context("delta staging dir")?;
let out = dir.path().join("delta.znippy");
let date = opts.created_at.format("%Y-%m-%d");
let rev = format!("holger-intel-{}-{date}#{}", opts.sequence, opts.sequence);
let rows = |s: &AdvisorySet| -> Vec<modgunn::advisory_feed::AdvisoryRow> {
s.advisories.iter().map(|a| a.to_feed_row()).collect()
};
modgunn::advisory_feed::write_delta(
&out,
&rows(previous),
&rows(set),
previous_sequence,
opts.sequence,
&rev,
None,
)?;
let delta = std::fs::read(&out)?;
let digest = sha256_hex(&delta);
let payloads = vec![
PayloadInput {
relative_path: crate::ADVISORIES_DELTA_REL.into(),
bytes: delta,
media_type: crate::ADVISORIES_DELTA_MEDIA_TYPE.into(),
},
PayloadInput {
relative_path: crate::SUPPLY_REL.into(),
bytes: serde_json::to_vec(&set.supply)?,
media_type: "application/json".into(),
},
];
sign_payloads(set, digest, payloads, opts)
}
fn sign_payloads(
set: &AdvisorySet,
digest: String,
payloads: Vec<PayloadInput>,
opts: BuildOptions,
) -> Result<BuiltBundle> {
let sources = serde_json::to_vec_pretty(&set.sources)?;
let reported: u64 = payloads.iter().map(|p| p.bytes.len() as u64).sum();
let date = opts.created_at.format("%Y-%m-%d");
let bundle_id = format!("holger-intel-{}-{date}", opts.sequence);
let fetched_at = set.sources.iter().map(|s| s.fetched_at).max();
let policy_digest = sha256_hex(opts.policy.to_ron()?.as_bytes());
let prep = Preparation {
bundle_id: bundle_id.clone(),
sequence: opts.sequence,
created_at: opts.created_at,
expires_at: opts.expires_at,
producer: opts.producer.clone(),
supplier: opts.supplier.clone(),
product: INTEL_PRODUCT.to_string(),
release_version: format!("{date}.{}", opts.sequence),
supported_hardware_models: vec![],
supported_hardware_revisions: vec![],
source_provenance: SourceProvenance {
source_kind: "intel-feeds".into(),
source_uri: None,
source_repository: None,
source_asset_path: None,
fetched_at,
reported_size: Some(reported),
vendor_checksum: None,
vendor_signature_status: None,
tls_origin: None,
transfer_receipt_digest: None,
},
policy_result: PolicyResult {
policy_digest,
advisory_snapshot_digest: Some(digest.clone()),
verdict: "pass".into(),
non_production: false,
},
supersedes: vec![],
withdraws: vec![],
payloads,
evidence: vec![EvidenceInput {
relative_path: SOURCES_REL.into(),
bytes: sources,
media_type: "application/json".into(),
kind: EvidenceKind::Advisory,
}],
};
let candidate = prep
.build()
.map_err(|e| anyhow::anyhow!("prepare intel bundle: {e}"))?;
let mut members = candidate.members;
let now = Utc::now();
for (role, s) in &opts.approvals {
add_approval(&mut members, role, &s.cert_der, &s.key_pkcs8, None, now)
.map_err(|e| anyhow::anyhow!("approve intel bundle as {role}: {e}"))?;
}
let report = finalize(
&mut members,
&opts.policy,
&opts.release.cert_der,
&opts.release.key_pkcs8,
now,
)
.map_err(|e| anyhow::anyhow!("sign intel bundle: {e}"))?;
if let Some(p) = opts.out.parent().filter(|p| !p.as_os_str().is_empty()) {
std::fs::create_dir_all(p).ok();
}
pack_bundle(&members, &opts.out).map_err(|e| anyhow::anyhow!("pack intel bundle: {e}"))?;
Ok(BuiltBundle {
path: opts.out,
bundle_id,
sequence: opts.sequence,
advisories: set.advisories.len(),
advisories_sha256: digest,
report,
})
}