Skip to main content

holger_intel/
bundle.rs

1//! The online side's last step: an [`AdvisorySet`] becomes a signed
2//! `holger.update-bundle/v1` with `product = holger.intel/v1`.
3//!
4//! Nothing here is a new format. The set is the bundle's one payload, the feed
5//! record is its `Advisory` evidence, and `update-bundle`'s own `Preparation`,
6//! `add_approval` and `finalize` do the hashing, the approvals and the release
7//! signature — so `holger-verify` verifies an intel bundle exactly like a
8//! firmware one, and `finalize` refuses to emit one that does not verify.
9
10use std::path::{Path, PathBuf};
11
12use anyhow::{Context, Result};
13use chrono::{DateTime, Utc};
14use holger_update_bundle::core::{EvidenceKind, PolicyResult, SourceProvenance};
15use holger_update_bundle::digest::sha256_hex;
16use holger_update_bundle::finalize::{add_approval, finalize};
17use holger_update_bundle::prepare::{EvidenceInput, PayloadInput, Preparation};
18use holger_update_bundle::{pack_bundle, TrustPolicy, VerifyReport};
19
20use crate::set::AdvisorySet;
21use crate::{ADVISORIES_MEDIA_TYPE, ADVISORIES_REL, INTEL_PRODUCT, SOURCES_REL};
22
23/// One signer's certificate (DER) and ed25519 key (PKCS#8 DER). The key never
24/// enters the bundle and is never printed.
25pub struct SignerFiles {
26    pub cert_der: Vec<u8>,
27    pub key_pkcs8: Vec<u8>,
28}
29
30impl SignerFiles {
31    pub fn read(cert: &Path, key: &Path) -> Result<Self> {
32        Ok(Self {
33            cert_der: std::fs::read(cert)
34                .with_context(|| format!("read cert {}", cert.display()))?,
35            key_pkcs8: std::fs::read(key).with_context(|| format!("read key {}", key.display()))?,
36        })
37    }
38}
39
40/// What the bundle says about itself, and who signs it.
41pub struct BuildOptions {
42    /// Monotonic per `(supplier, holger.intel/v1)`. The secure side refuses a
43    /// bundle whose sequence is below the one it already holds.
44    pub sequence: u64,
45    pub created_at: DateTime<Utc>,
46    pub expires_at: Option<DateTime<Utc>>,
47    pub supplier: String,
48    pub producer: String,
49    /// The trust policy the downstream verifies against; `finalize` checks the
50    /// bundle under it before it is written.
51    pub policy: TrustPolicy,
52    /// Approvals, `(role, signer)`, as many as the policy's two-person rule asks.
53    pub approvals: Vec<(String, SignerFiles)>,
54    pub release: SignerFiles,
55    pub out: PathBuf,
56}
57
58/// The written bundle.
59pub struct BuiltBundle {
60    pub path: PathBuf,
61    pub bundle_id: String,
62    pub sequence: u64,
63    pub advisories: usize,
64    pub advisories_sha256: String,
65    pub report: VerifyReport,
66}
67
68/// The default sequence for a bundle built at `t`: `YYYYMMDDHHMMSS`, which only
69/// grows as long as the builder's clock does.
70pub fn sequence_for(t: DateTime<Utc>) -> u64 {
71    t.format("%Y%m%d%H%M%S").to_string().parse().unwrap_or(0)
72}
73
74/// Sign `set` into a bundle at `opts.out`.
75pub fn build_bundle(set: &AdvisorySet, opts: BuildOptions) -> Result<BuiltBundle> {
76    let payload = set.to_json_bytes()?;
77    let sources = serde_json::to_vec_pretty(&set.sources)?;
78    let digest = sha256_hex(&payload);
79    let date = opts.created_at.format("%Y-%m-%d");
80    let bundle_id = format!("holger-intel-{}-{date}", opts.sequence);
81    let fetched_at = set.sources.iter().map(|s| s.fetched_at).max();
82    let policy_digest = sha256_hex(opts.policy.to_ron()?.as_bytes());
83
84    let prep = Preparation {
85        bundle_id: bundle_id.clone(),
86        sequence: opts.sequence,
87        created_at: opts.created_at,
88        expires_at: opts.expires_at,
89        producer: opts.producer.clone(),
90        supplier: opts.supplier.clone(),
91        product: INTEL_PRODUCT.to_string(),
92        release_version: format!("{date}.{}", opts.sequence),
93        supported_hardware_models: vec![],
94        supported_hardware_revisions: vec![],
95        source_provenance: SourceProvenance {
96            source_kind: "intel-feeds".into(),
97            source_uri: None,
98            source_repository: None,
99            source_asset_path: None,
100            fetched_at,
101            reported_size: Some(payload.len() as u64),
102            vendor_checksum: None,
103            vendor_signature_status: None,
104            tls_origin: None,
105            transfer_receipt_digest: None,
106        },
107        policy_result: PolicyResult {
108            policy_digest,
109            advisory_snapshot_digest: Some(digest.clone()),
110            verdict: "pass".into(),
111            non_production: false,
112        },
113        supersedes: vec![],
114        withdraws: vec![],
115        payloads: vec![PayloadInput {
116            relative_path: ADVISORIES_REL.into(),
117            bytes: payload,
118            media_type: ADVISORIES_MEDIA_TYPE.into(),
119        }],
120        evidence: vec![EvidenceInput {
121            relative_path: SOURCES_REL.into(),
122            bytes: sources,
123            media_type: "application/json".into(),
124            kind: EvidenceKind::Advisory,
125        }],
126    };
127    let candidate = prep
128        .build()
129        .map_err(|e| anyhow::anyhow!("prepare intel bundle: {e}"))?;
130    let mut members = candidate.members;
131    let now = Utc::now();
132    for (role, s) in &opts.approvals {
133        add_approval(&mut members, role, &s.cert_der, &s.key_pkcs8, None, now)
134            .map_err(|e| anyhow::anyhow!("approve intel bundle as {role}: {e}"))?;
135    }
136    let report = finalize(
137        &mut members,
138        &opts.policy,
139        &opts.release.cert_der,
140        &opts.release.key_pkcs8,
141        now,
142    )
143    .map_err(|e| anyhow::anyhow!("sign intel bundle: {e}"))?;
144    if let Some(p) = opts.out.parent().filter(|p| !p.as_os_str().is_empty()) {
145        std::fs::create_dir_all(p).ok();
146    }
147    pack_bundle(&members, &opts.out).map_err(|e| anyhow::anyhow!("pack intel bundle: {e}"))?;
148    Ok(BuiltBundle {
149        path: opts.out,
150        bundle_id,
151        sequence: opts.sequence,
152        advisories: set.advisories.len(),
153        advisories_sha256: digest,
154        report,
155    })
156}