update-bundle/src/manifest.rs
//! The release manifest and envelope.
//!
//! `signatures/release-manifest.json` records `{path, size, sha256, blake3}` for
//! **every** member except itself, `release.sig`, and `release.crt`. The release
//! signature covers this manifest's exact bytes, so it transitively covers every
//! payload, evidence file, `core.json`, `manifest.json`, and every approval
//! statement/signature/certificate. Any changed byte in any covered member changes
//! a digest here and fails verification before the signature is even checked.
use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::digest::{blake3_hex, sha256_hex};
use crate::error::{BundleError, Result};
use crate::schema::{BUNDLE_SCHEMA, RELEASE_CRT, RELEASE_MANIFEST_JSON, RELEASE_SIG};
use crate::MemberMap;
/// A single member's digest record.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemberDigest {
pub relative_path: String,
pub size_bytes: u64,
pub sha256: String,
pub blake3: String,
}
/// `signatures/release-manifest.json`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleaseManifest {
pub schema_version: String,
pub bundle_id: String,
/// SHA-256 of the exact `core.json` bytes.
pub candidate_digest: String,
/// Every covered member, sorted by path for deterministic bytes.
pub members: Vec<MemberDigest>,
}
/// The members that the release manifest does NOT (and cannot) cover: itself and
/// the release signature + certificate.
pub fn is_self_referential(path: &str) -> bool {
path == RELEASE_MANIFEST_JSON || path == RELEASE_SIG || path == RELEASE_CRT
}
impl ReleaseManifest {
/// Build the manifest over every member except the self-referential three.
pub fn build(members: &MemberMap, bundle_id: &str, candidate_digest: &str) -> Self {
let mut entries: Vec<MemberDigest> = members
.iter()
.filter(|(p, _)| !is_self_referential(p))
.map(|(p, bytes)| MemberDigest {
relative_path: p.clone(),
size_bytes: bytes.len() as u64,
sha256: sha256_hex(bytes),
blake3: blake3_hex(bytes),
})
.collect();
entries.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
ReleaseManifest {
schema_version: BUNDLE_SCHEMA.to_string(),
bundle_id: bundle_id.to_string(),
candidate_digest: candidate_digest.to_string(),
members: entries,
}
}
pub fn to_json_bytes(&self) -> Result<Vec<u8>> {
serde_json::to_vec_pretty(self).map_err(Into::into)
}
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
serde_json::from_slice(bytes).map_err(Into::into)
}
/// Index the members by path for O(1) lookup.
pub fn index(&self) -> BTreeMap<&str, &MemberDigest> {
self.members
.iter()
.map(|m| (m.relative_path.as_str(), m))
.collect()
}
/// Verify every covered member's bytes against this manifest, and that there
/// are no extra unreferenced members and no missing referenced members. Any
/// changed byte in any covered member is caught here.
pub fn verify_members(&self, members: &MemberMap) -> Result<()> {
let index = self.index();
// Every covered member must be present and match.
for (path, bytes) in members {
if is_self_referential(path) {
continue;
}
let entry = index
.get(path.as_str())
.ok_or_else(|| BundleError::UnreferencedMember(path.clone()))?;
if entry.size_bytes != bytes.len() as u64 {
return Err(BundleError::DigestMismatch {
path: path.clone(),
which: "size",
});
}
if entry.sha256 != sha256_hex(bytes) {
return Err(BundleError::DigestMismatch {
path: path.clone(),
which: "sha256",
});
}
if entry.blake3 != blake3_hex(bytes) {
return Err(BundleError::DigestMismatch {
path: path.clone(),
which: "blake3",
});
}
}
// Every referenced member must exist.
for m in &self.members {
if !members.contains_key(&m.relative_path) {
return Err(BundleError::MissingMember(m.relative_path.clone()));
}
}
Ok(())
}
}
/// `manifest.json` — a small human/inspect-friendly envelope, itself covered by
/// the release manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvelopeManifest {
pub schema_version: String,
pub bundle_id: String,
pub sequence: u64,
pub supplier: String,
pub product: String,
pub release_version: String,
pub created_at: DateTime<Utc>,
pub member_count: usize,
/// SHA-256 of the exact `core.json` bytes.
pub core_digest: String,
}
impl EnvelopeManifest {
pub fn to_json_bytes(&self) -> Result<Vec<u8>> {
serde_json::to_vec_pretty(self).map_err(Into::into)
}
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self> {
serde_json::from_slice(bytes).map_err(Into::into)
}
}