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