1use 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
23pub 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
40pub struct BuildOptions {
42 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 pub policy: TrustPolicy,
52 pub approvals: Vec<(String, SignerFiles)>,
54 pub release: SignerFiles,
55 pub out: PathBuf,
56}
57
58pub 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
68pub fn sequence_for(t: DateTime<Utc>) -> u64 {
71 t.format("%Y%m%d%H%M%S").to_string().parse().unwrap_or(0)
72}
73
74pub 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}