ASELSANMicrokernel
S564 · SOURCE-BOUND GATE EVIDENCE

S564 · R1 güncelleme: paket manifesti ve hash zinciri modeli

tam S564 implementation modülü → Operations --test hedefi ile bağlı tam focused test → ayrı Operations kaydı Bu sayfa yalnız S564 kapısına aittir; komşu kapıların kaynakları bu kabulün içine katılmaz.

S564Focused kod testiOperations id exactsource SHA exacttest target exact

operation: g8l-s564-r1-update-package-manifest-hash-chain-model

uygulama/model · focused test · Operations · 3 exact excerpt

sequence-bound=true · implementation-bound=true
01 · Yürütme / doğrulama kodu

Kapının gerçek repository sözleşmesi

tam dosyaL1–L1059
kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model.rs::S564 r1 update package manifest hash chain model implementation
#![allow(unexpected_cfgs)]

//! S564 models the R1 update package manifest and its hash chain as a pure
//! source/host model: bounded manifest entries (path <= 64 bytes, byte
//! count, SHA-256 digest, role `Kernel`/`Dtb`/`Config`/`App`), a manifest
//! header (monotonic version, predecessor manifest hash, entry count <= 16),
//! a deterministic canonical encoding whose SHA-256 is the manifest hash,
//! predecessor -> successor chain verification, duplicate-path and size-limit
//! rejection, and an exact-match check of an observed package against the
//! head manifest.  SHA-256 is implemented here (pure, `no_std`, streaming)
//! and is pinned against the FIPS 180-4 vectors for `""` and `"abc"`; S565
//! reuses it.  The S545 candidate package identities (image, DTB,
//! `config.txt`) are carried as a fixture and must match byte-for-byte.
//!
//! Signature verification is out of scope for S564 and is explicitly
//! refused (`SignatureOutOfScope`); it belongs to S566 / R2.
//!
//! The gate makes no hardware claim: no SD card, no UART, no board, no
//! update transaction exists for S564; physical observations = 0,
//! `RUNBOOK_EXECUTED_IN_S564=NO`, Boot-to-UI physically observed = false and
//! R1 acceptance complete = false.  Nothing here is wired into a boot, IRQ,
//! scheduler or driver path; the focused host test is the only caller.  It
//! performs no device operation and does not rerun S540 or S543, whose
//! physical RED verdicts remain immutable.
//!
//! Predecessor: S563 (bounded recovery fault containment model).  Next gate:
//! S565 (staged update apply/rollback model).

use alloc::vec::Vec;

pub const S564_SEQUENCE: usize = 564;
pub const S564_EXPECTED_PREDECESSOR: usize = 563;
pub const S564_R1_STAGE: u8 = 4;
pub const S564_R1_RANGE_FIRST: usize = 536;
pub const S564_R1_RANGE_LAST: usize = 568;
pub const S564_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS: usize = 0;
pub const S564_PHYSICAL_OBSERVATIONS: usize = 0;
pub const S564_PHYSICAL_OR_DEVICE_OPERATIONS: usize = 0;
pub const S564_SD_WRITES: usize = 0;
pub const S564_UART_OPENS: usize = 0;
pub const S564_POWER_TRANSITIONS: usize = 0;
pub const S564_NEW_IMMUTABLE_RAW_CAPTURES: usize = 0;
pub const S564_S540_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S564_S543_PHYSICAL_VERDICT_RETAINED_RED: bool = true;
pub const S564_AUTOMATIC_PROMOTION: bool = false;
pub const S564_BOOT_TO_UI_PHYSICALLY_OBSERVED: bool = false;
pub const S564_HARDWARE_PRESENT: bool = false;
pub const S564_R1_ACCEPTANCE_COMPLETE: bool = false;
pub const RUNBOOK_EXECUTED_IN_S564: bool = false;

pub const S564_SHA256_DIGEST_LEN: usize = 32;
pub const S564_SHA256_BLOCK_LEN: usize = 64;
pub const S564_MANIFEST_PATH_MAX_LEN: usize = 64;
pub const S564_MANIFEST_MAX_ENTRIES: usize = 16;
pub const S564_MANIFEST_MAGIC: [u8; 8] = *b"ASOSMF01";
pub const S564_MANIFEST_HEADER_ENCODED_LEN: usize = 8 + 4 + 32 + 1;
pub const S564_MANIFEST_ENTRY_ENCODED_MAX_LEN: usize = 1 + 1 + S564_MANIFEST_PATH_MAX_LEN + 8 + 32;
pub const S564_MANIFEST_ENCODED_MAX_LEN: usize = S564_MANIFEST_HEADER_ENCODED_LEN
    + S564_MANIFEST_MAX_ENTRIES * S564_MANIFEST_ENTRY_ENCODED_MAX_LEN;
pub const S564_GENESIS_VERSION: u32 = 1;
pub const S564_GENESIS_PREDECESSOR_HASH: [u8; 32] = [0; 32];
pub const S564_MAX_CHAIN_LENGTH: usize = 8;
pub const S564_MAX_ENTRY_BYTES: u64 = 64 * 1024 * 1024;
pub const S564_MAX_PACKAGE_BYTES: u64 = 256 * 1024 * 1024;
pub const S564_SIGNATURE_SCOPE_SEQUENCE: usize = 566;
pub const S564_SIGNATURE_VERIFIED: bool = false;

pub const S545_PACKAGE_IMAGE_PATH: &str = "aselsanos-rpi5.img";
pub const S545_PACKAGE_IMAGE_BYTES: u64 = 945_760;
pub const S545_PACKAGE_IMAGE_SHA256_HEX: &str =
    "ed1901a991e2f9e9ae3c16f254147a2b0180686a8d70ca5d7353374fee08d467";
pub const S545_PACKAGE_IMAGE_SHA256: [u8; 32] = [
    0xed, 0x19, 0x01, 0xa9, 0x91, 0xe2, 0xf9, 0xe9, 0xae, 0x3c, 0x16, 0xf2, 0x54, 0x14, 0x7a, 0x2b,
    0x01, 0x80, 0x68, 0x6a, 0x8d, 0x70, 0xca, 0x5d, 0x73, 0x53, 0x37, 0x4f, 0xee, 0x08, 0xd4, 0x67,
];
pub const S545_PACKAGE_DTB_PATH: &str = "bcm2712-rpi-5-b.dtb";
pub const S545_PACKAGE_DTB_BYTES: u64 = 78_703;
pub const S545_PACKAGE_DTB_SHA256_HEX: &str =
    "40a2fbe9c29e8b9a4912cf726a943068defb779fc052ec38e457a79c58abca00";
pub const S545_PACKAGE_DTB_SHA256: [u8; 32] = [
    0x40, 0xa2, 0xfb, 0xe9, 0xc2, 0x9e, 0x8b, 0x9a, 0x49, 0x12, 0xcf, 0x72, 0x6a, 0x94, 0x30, 0x68,
    0xde, 0xfb, 0x77, 0x9f, 0xc0, 0x52, 0xec, 0x38, 0xe4, 0x57, 0xa7, 0x9c, 0x58, 0xab, 0xca, 0x00,
];
pub const S545_PACKAGE_CONFIG_PATH: &str = "config.txt";
pub const S545_PACKAGE_CONFIG_BYTES: u64 = 420;
pub const S545_PACKAGE_CONFIG_SHA256_HEX: &str =
    "aef848bf6e0c324148eade5054a15c71a1e8c04814a3ed2e680056f87c1f9bba";
pub const S545_PACKAGE_CONFIG_SHA256: [u8; 32] = [
    0xae, 0xf8, 0x48, 0xbf, 0x6e, 0x0c, 0x32, 0x41, 0x48, 0xea, 0xde, 0x50, 0x54, 0xa1, 0x5c, 0x71,
    0xa1, 0xe8, 0xc0, 0x48, 0x14, 0xa3, 0xed, 0x2e, 0x68, 0x00, 0x56, 0xf8, 0x7c, 0x1f, 0x9b, 0xba,
];
pub const S545_PACKAGE_ENTRY_COUNT: usize = 3;
pub const S545_PACKAGE_TOTAL_BYTES: u64 =
    S545_PACKAGE_IMAGE_BYTES + S545_PACKAGE_DTB_BYTES + S545_PACKAGE_CONFIG_BYTES;

// ---------------------------------------------------------------------------
// SHA-256 (FIPS 180-4), pure and streaming.
// ---------------------------------------------------------------------------

const SHA256_INITIAL_STATE: [u32; 8] = [
    0x6a09_e667,
    0xbb67_ae85,
    0x3c6e_f372,
    0xa54f_f53a,
    0x510e_527f,
    0x9b05_688c,
    0x1f83_d9ab,
    0x5be0_cd19,
];

const SHA256_ROUND_CONSTANTS: [u32; 64] = [
    0x428a_2f98,
    0x7137_4491,
    0xb5c0_fbcf,
    0xe9b5_dba5,
    0x3956_c25b,
    0x59f1_11f1,
    0x923f_82a4,
    0xab1c_5ed5,
    0xd807_aa98,
    0x1283_5b01,
    0x2431_85be,
    0x550c_7dc3,
    0x72be_5d74,
    0x80de_b1fe,
    0x9bdc_06a7,
    0xc19b_f174,
    0xe49b_69c1,
    0xefbe_4786,
    0x0fc1_9dc6,
    0x240c_a1cc,
    0x2de9_2c6f,
    0x4a74_84aa,
    0x5cb0_a9dc,
    0x76f9_88da,
    0x983e_5152,
    0xa831_c66d,
    0xb003_27c8,
    0xbf59_7fc7,
    0xc6e0_0bf3,
    0xd5a7_9147,
    0x06ca_6351,
    0x1429_2967,
    0x27b7_0a85,
    0x2e1b_2138,
    0x4d2c_6dfc,
    0x5338_0d13,
    0x650a_7354,
    0x766a_0abb,
    0x81c2_c92e,
    0x9272_2c85,
    0xa2bf_e8a1,
    0xa81a_664b,
    0xc24b_8b70,
    0xc76c_51a3,
    0xd192_e819,
    0xd699_0624,
    0xf40e_3585,
    0x106a_a070,
    0x19a4_c116,
    0x1e37_6c08,
    0x2748_774c,
    0x34b0_bcb5,
    0x391c_0cb3,
    0x4ed8_aa4a,
    0x5b9c_ca4f,
    0x682e_6ff3,
    0x748f_82ee,
    0x78a5_636f,
    0x84c8_7814,
    0x8cc7_0208,
    0x90be_fffa,
    0xa450_6ceb,
    0xbef9_a3f7,
    0xc671_78f2,
];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564Sha256 {
    state: [u32; 8],
    buffer: [u8; S564_SHA256_BLOCK_LEN],
    buffer_len: usize,
    total_len: u64,
}

impl Default for G8lS564Sha256 {
    fn default() -> Self {
        Self::new()
    }
}

impl G8lS564Sha256 {
    pub const fn new() -> Self {
        Self {
            state: SHA256_INITIAL_STATE,
            buffer: [0; S564_SHA256_BLOCK_LEN],
            buffer_len: 0,
            total_len: 0,
        }
    }

    fn compress(state: &mut [u32; 8], block: &[u8; S564_SHA256_BLOCK_LEN]) {
        let mut w = [0u32; 64];
        for (i, word) in w.iter_mut().take(16).enumerate() {
            *word = u32::from_be_bytes([
                block[4 * i],
                block[4 * i + 1],
                block[4 * i + 2],
                block[4 * i + 3],
            ]);
        }
        for i in 16..64 {
            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
            w[i] = w[i - 16]
                .wrapping_add(s0)
                .wrapping_add(w[i - 7])
                .wrapping_add(s1);
        }
        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut h] = *state;
        for i in 0..64 {
            let big_s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
            let ch = (e & f) ^ (!e & g);
            let t1 = h
                .wrapping_add(big_s1)
                .wrapping_add(ch)
                .wrapping_add(SHA256_ROUND_CONSTANTS[i])
                .wrapping_add(w[i]);
            let big_s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
            let maj = (a & b) ^ (a & c) ^ (b & c);
            let t2 = big_s0.wrapping_add(maj);
            h = g;
            g = f;
            f = e;
            e = d.wrapping_add(t1);
            d = c;
            c = b;
            b = a;
            a = t1.wrapping_add(t2);
        }
        for (slot, value) in state.iter_mut().zip([a, b, c, d, e, f, g, h]) {
            *slot = slot.wrapping_add(value);
        }
    }

    pub fn update(&mut self, bytes: &[u8]) -> Result<(), G8lS564ManifestChainError> {
        self.total_len = self
            .total_len
            .checked_add(bytes.len() as u64)
            .filter(|total| *total <= u64::MAX / 8)
            .ok_or(G8lS564ManifestChainError::MessageTooLong)?;
        let mut rest = bytes;
        if self.buffer_len > 0 {
            let take = (S564_SHA256_BLOCK_LEN - self.buffer_len).min(rest.len());
            self.buffer[self.buffer_len..self.buffer_len + take].copy_from_slice(&rest[..take]);
            self.buffer_len += take;
            rest = &rest[take..];
            if self.buffer_len == S564_SHA256_BLOCK_LEN {
                let block = self.buffer;
                Self::compress(&mut self.state, &block);
                self.buffer_len = 0;
            }
        }
        if self.buffer_len == 0 {
            let mut chunks = rest.chunks_exact(S564_SHA256_BLOCK_LEN);
            for chunk in &mut chunks {
                let mut block = [0u8; S564_SHA256_BLOCK_LEN];
                block.copy_from_slice(chunk);
                Self::compress(&mut self.state, &block);
            }
            let tail = chunks.remainder();
            self.buffer[..tail.len()].copy_from_slice(tail);
            self.buffer_len = tail.len();
        }
        Ok(())
    }

    pub fn finalize(mut self) -> [u8; S564_SHA256_DIGEST_LEN] {
        let bit_len = self.total_len.wrapping_mul(8);
        let mut block = [0u8; S564_SHA256_BLOCK_LEN];
        block[..self.buffer_len].copy_from_slice(&self.buffer[..self.buffer_len]);
        block[self.buffer_len] = 0x80;
        if self.buffer_len + 1 > S564_SHA256_BLOCK_LEN - 8 {
            Self::compress(&mut self.state, &block);
            block = [0u8; S564_SHA256_BLOCK_LEN];
        }
        block[S564_SHA256_BLOCK_LEN - 8..].copy_from_slice(&bit_len.to_be_bytes());
        Self::compress(&mut self.state, &block);
        let mut digest = [0u8; S564_SHA256_DIGEST_LEN];
        for (chunk, word) in digest.chunks_exact_mut(4).zip(self.state) {
            chunk.copy_from_slice(&word.to_be_bytes());
        }
        digest
    }
}

pub fn sha256_digest(bytes: &[u8]) -> [u8; S564_SHA256_DIGEST_LEN] {
    let mut hasher = G8lS564Sha256::new();
    // A slice can never exceed the 2^61-byte SHA-256 message bound.
    let _ = hasher.update(bytes);
    hasher.finalize()
}

pub fn digest_to_hex(digest: &[u8; S564_SHA256_DIGEST_LEN]) -> [u8; 64] {
    const HEX: &[u8; 16] = b"0123456789abcdef";
    let mut out = [0u8; 64];
    for (i, byte) in digest.iter().enumerate() {
        out[2 * i] = HEX[(byte >> 4) as usize];
        out[2 * i + 1] = HEX[(byte & 0x0f) as usize];
    }
    out
}

pub fn parse_hex_digest(hex: &str) -> Option<[u8; S564_SHA256_DIGEST_LEN]> {
    let bytes = hex.as_bytes();
    if bytes.len() != 64 {
        return None;
    }
    let nibble = |c: u8| match c {
        b'0'..=b'9' => Some(c - b'0'),
        b'a'..=b'f' => Some(c - b'a' + 10),
        _ => None,
    };
    let mut digest = [0u8; S564_SHA256_DIGEST_LEN];
    for (i, pair) in bytes.chunks_exact(2).enumerate() {
        digest[i] = (nibble(pair[0])? << 4) | nibble(pair[1])?;
    }
    Some(digest)
}

// ---------------------------------------------------------------------------
// Manifest model.
// ---------------------------------------------------------------------------

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS564EntryRole {
    Kernel,
    Dtb,
    Config,
    App,
}

impl G8lS564EntryRole {
    pub const fn code(self) -> u8 {
        match self {
            Self::Kernel => 1,
            Self::Dtb => 2,
            Self::Config => 3,
            Self::App => 4,
        }
    }

    pub const fn from_code(code: u8) -> Option<Self> {
        match code {
            1 => Some(Self::Kernel),
            2 => Some(Self::Dtb),
            3 => Some(Self::Config),
            4 => Some(Self::App),
            _ => None,
        }
    }

    pub const fn is_singleton(self) -> bool {
        !matches!(self, Self::App)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ManifestPath {
    bytes: [u8; S564_MANIFEST_PATH_MAX_LEN],
    len: u8,
}

impl G8lS564ManifestPath {
    pub const EMPTY: Self = Self {
        bytes: [0; S564_MANIFEST_PATH_MAX_LEN],
        len: 0,
    };

    pub fn new(path: &str) -> Result<Self, G8lS564ManifestChainError> {
        Self::from_bytes(path.as_bytes())
    }

    pub fn from_bytes(path: &[u8]) -> Result<Self, G8lS564ManifestChainError> {
        if path.is_empty() {
            return Err(G8lS564ManifestChainError::PathEmpty);
        }
        if path.len() > S564_MANIFEST_PATH_MAX_LEN {
            return Err(G8lS564ManifestChainError::PathTooLong);
        }
        if !path
            .iter()
            .all(|&c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'-' | b'_' | b'/'))
        {
            return Err(G8lS564ManifestChainError::PathInvalidCharacter);
        }
        if path[0] == b'/' || path[path.len() - 1] == b'/' {
            return Err(G8lS564ManifestChainError::PathInvalidShape);
        }
        if path
            .split(|c| *c == b'/')
            .any(|segment| segment.is_empty() || segment == b"." || segment == b"..")
        {
            return Err(G8lS564ManifestChainError::PathInvalidShape);
        }
        let mut bytes = [0u8; S564_MANIFEST_PATH_MAX_LEN];
        bytes[..path.len()].copy_from_slice(path);
        Ok(Self {
            bytes,
            len: path.len() as u8,
        })
    }

    pub fn as_bytes(&self) -> &[u8] {
        &self.bytes[..self.len as usize]
    }

    pub const fn len(&self) -> usize {
        self.len as usize
    }

    pub const fn is_empty(&self) -> bool {
        self.len == 0
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ManifestEntry {
    pub path: G8lS564ManifestPath,
    pub bytes: u64,
    pub sha256: [u8; S564_SHA256_DIGEST_LEN],
    pub role: G8lS564EntryRole,
}

impl G8lS564ManifestEntry {
    pub const EMPTY: Self = Self {
        path: G8lS564ManifestPath::EMPTY,
        bytes: 0,
        sha256: [0; S564_SHA256_DIGEST_LEN],
        role: G8lS564EntryRole::App,
    };

    pub fn new(
        path: &str,
        bytes: u64,
        sha256: [u8; S564_SHA256_DIGEST_LEN],
        role: G8lS564EntryRole,
    ) -> Result<Self, G8lS564ManifestChainError> {
        let entry = Self {
            path: G8lS564ManifestPath::new(path)?,
            bytes,
            sha256,
            role,
        };
        entry.validate()?;
        Ok(entry)
    }

    pub fn validate(&self) -> Result<(), G8lS564ManifestChainError> {
        G8lS564ManifestPath::from_bytes(self.path.as_bytes())?;
        if self.bytes == 0 {
            return Err(G8lS564ManifestChainError::EntryBytesZero);
        }
        if self.bytes > S564_MAX_ENTRY_BYTES {
            return Err(G8lS564ManifestChainError::EntryBytesTooLarge);
        }
        Ok(())
    }

    pub const fn encoded_len(&self) -> usize {
        1 + 1 + self.path.len() + 8 + S564_SHA256_DIGEST_LEN
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ManifestHeader {
    pub version: u32,
    pub predecessor_manifest_hash: [u8; S564_SHA256_DIGEST_LEN],
    pub entry_count: u8,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564Manifest {
    pub header: G8lS564ManifestHeader,
    pub entries: [G8lS564ManifestEntry; S564_MANIFEST_MAX_ENTRIES],
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ManifestSummary {
    pub entry_count: usize,
    pub total_bytes: u64,
    pub kernel_entries: usize,
    pub dtb_entries: usize,
    pub config_entries: usize,
    pub app_entries: usize,
    pub encoded_len: usize,
}

impl G8lS564Manifest {
    pub fn new(
        version: u32,
        predecessor_manifest_hash: [u8; S564_SHA256_DIGEST_LEN],
        entries: &[G8lS564ManifestEntry],
    ) -> Result<Self, G8lS564ManifestChainError> {
        if entries.len() > S564_MANIFEST_MAX_ENTRIES {
            return Err(G8lS564ManifestChainError::EntryCountTooLarge);
        }
        let mut table = [G8lS564ManifestEntry::EMPTY; S564_MANIFEST_MAX_ENTRIES];
        table[..entries.len()].copy_from_slice(entries);
        let manifest = Self {
            header: G8lS564ManifestHeader {
                version,
                predecessor_manifest_hash,
                entry_count: entries.len() as u8,
            },
            entries: table,
        };
        manifest.validate()?;
        Ok(manifest)
    }

    pub fn entries(&self) -> &[G8lS564ManifestEntry] {
        let count = (self.header.entry_count as usize).min(S564_MANIFEST_MAX_ENTRIES);
        &self.entries[..count]
    }

    pub const fn is_genesis(&self) -> bool {
        let mut i = 0;
        while i < S564_SHA256_DIGEST_LEN {
            if self.header.predecessor_manifest_hash[i] != 0 {
                return false;
            }
            i += 1;
        }
        true
    }

    pub fn validate(&self) -> Result<G8lS564ManifestSummary, G8lS564ManifestChainError> {
        if self.header.version == 0 {
            return Err(G8lS564ManifestChainError::VersionZero);
        }
        let count = self.header.entry_count as usize;
        if count == 0 {
            return Err(G8lS564ManifestChainError::EntryCountZero);
        }
        if count > S564_MANIFEST_MAX_ENTRIES {
            return Err(G8lS564ManifestChainError::EntryCountTooLarge);
        }
        if self.entries[count..]
            .iter()
            .any(|entry| *entry != G8lS564ManifestEntry::EMPTY)
        {
            return Err(G8lS564ManifestChainError::EntryCountMismatch);
        }
        let mut summary = G8lS564ManifestSummary {
            entry_count: count,
            total_bytes: 0,
            kernel_entries: 0,
            dtb_entries: 0,
            config_entries: 0,
            app_entries: 0,
            encoded_len: S564_MANIFEST_HEADER_ENCODED_LEN,
        };
        for (index, entry) in self.entries().iter().enumerate() {
            entry.validate()?;
            if self.entries()[..index]
                .iter()
                .any(|earlier| earlier.path == entry.path)
            {
                return Err(G8lS564ManifestChainError::DuplicatePath);
            }
            summary.total_bytes = summary
                .total_bytes
                .checked_add(entry.bytes)
                .ok_or(G8lS564ManifestChainError::PackageBytesOverflow)?;
            let slot = match entry.role {
                G8lS564EntryRole::Kernel => &mut summary.kernel_entries,
                G8lS564EntryRole::Dtb => &mut summary.dtb_entries,
                G8lS564EntryRole::Config => &mut summary.config_entries,
                G8lS564EntryRole::App => &mut summary.app_entries,
            };
            *slot += 1;
            if entry.role.is_singleton() && *slot > 1 {
                return Err(G8lS564ManifestChainError::SingletonRoleRepeated);
            }
            summary.encoded_len += entry.encoded_len();
        }
        if summary.total_bytes > S564_MAX_PACKAGE_BYTES {
            return Err(G8lS564ManifestChainError::PackageBytesTooLarge);
        }
        Ok(summary)
    }
}

pub fn encode_manifest(manifest: &G8lS564Manifest) -> Result<Vec<u8>, G8lS564ManifestChainError> {
    let summary = manifest.validate()?;
    let mut out = Vec::with_capacity(summary.encoded_len);
    out.extend_from_slice(&S564_MANIFEST_MAGIC);
    out.extend_from_slice(&manifest.header.version.to_le_bytes());
    out.extend_from_slice(&manifest.header.predecessor_manifest_hash);
    out.push(manifest.header.entry_count);
    for entry in manifest.entries() {
        out.push(entry.role.code());
        out.push(entry.path.len() as u8);
        out.extend_from_slice(entry.path.as_bytes());
        out.extend_from_slice(&entry.bytes.to_le_bytes());
        out.extend_from_slice(&entry.sha256);
    }
    if out.len() != summary.encoded_len || out.len() > S564_MANIFEST_ENCODED_MAX_LEN {
        return Err(G8lS564ManifestChainError::EncodingLengthMismatch);
    }
    Ok(out)
}

pub fn decode_manifest(encoded: &[u8]) -> Result<G8lS564Manifest, G8lS564ManifestChainError> {
    if encoded.len() > S564_MANIFEST_ENCODED_MAX_LEN {
        return Err(G8lS564ManifestChainError::EncodingTooLong);
    }
    let mut cursor = 0usize;
    let mut take = |len: usize| -> Result<&[u8], G8lS564ManifestChainError> {
        let end = cursor
            .checked_add(len)
            .filter(|end| *end <= encoded.len())
            .ok_or(G8lS564ManifestChainError::EncodingTruncated)?;
        let slice = &encoded[cursor..end];
        cursor = end;
        Ok(slice)
    };
    if take(8)? != S564_MANIFEST_MAGIC {
        return Err(G8lS564ManifestChainError::EncodingMagicMismatch);
    }
    let mut version = [0u8; 4];
    version.copy_from_slice(take(4)?);
    let mut predecessor = [0u8; S564_SHA256_DIGEST_LEN];
    predecessor.copy_from_slice(take(S564_SHA256_DIGEST_LEN)?);
    let entry_count = take(1)?[0] as usize;
    if entry_count > S564_MANIFEST_MAX_ENTRIES {
        return Err(G8lS564ManifestChainError::EntryCountTooLarge);
    }
    let mut entries = [G8lS564ManifestEntry::EMPTY; S564_MANIFEST_MAX_ENTRIES];
    for entry in entries.iter_mut().take(entry_count) {
        let role = G8lS564EntryRole::from_code(take(1)?[0])
            .ok_or(G8lS564ManifestChainError::InvalidRole)?;
        let path_len = take(1)?[0] as usize;
        let path = G8lS564ManifestPath::from_bytes(take(path_len)?)?;
        let mut bytes = [0u8; 8];
        bytes.copy_from_slice(take(8)?);
        let mut sha256 = [0u8; S564_SHA256_DIGEST_LEN];
        sha256.copy_from_slice(take(S564_SHA256_DIGEST_LEN)?);
        *entry = G8lS564ManifestEntry {
            path,
            bytes: u64::from_le_bytes(bytes),
            sha256,
            role,
        };
    }
    if cursor != encoded.len() {
        return Err(G8lS564ManifestChainError::EncodingTrailingBytes);
    }
    let manifest = G8lS564Manifest {
        header: G8lS564ManifestHeader {
            version: u32::from_le_bytes(version),
            predecessor_manifest_hash: predecessor,
            entry_count: entry_count as u8,
        },
        entries,
    };
    manifest.validate()?;
    Ok(manifest)
}

pub fn manifest_hash(
    manifest: &G8lS564Manifest,
) -> Result<[u8; S564_SHA256_DIGEST_LEN], G8lS564ManifestChainError> {
    Ok(sha256_digest(&encode_manifest(manifest)?))
}

// ---------------------------------------------------------------------------
// Chain verification.
// ---------------------------------------------------------------------------

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ChainReceipt {
    pub length: usize,
    pub genesis_version: u32,
    pub head_version: u32,
    pub genesis_manifest_hash: [u8; S564_SHA256_DIGEST_LEN],
    pub head_manifest_hash: [u8; S564_SHA256_DIGEST_LEN],
    pub head_summary: G8lS564ManifestSummary,
}

pub fn verify_chain_link(
    predecessor: &G8lS564Manifest,
    successor: &G8lS564Manifest,
) -> Result<[u8; S564_SHA256_DIGEST_LEN], G8lS564ManifestChainError> {
    let predecessor_hash = manifest_hash(predecessor)?;
    successor.validate()?;
    if successor.is_genesis() {
        return Err(G8lS564ManifestChainError::PredecessorHashZero);
    }
    if successor.header.predecessor_manifest_hash != predecessor_hash {
        return Err(G8lS564ManifestChainError::PredecessorHashMismatch);
    }
    if successor.header.version <= predecessor.header.version {
        return Err(G8lS564ManifestChainError::VersionNotMonotonic);
    }
    manifest_hash(successor)
}

pub fn verify_manifest_chain(
    chain: &[G8lS564Manifest],
) -> Result<G8lS564ChainReceipt, G8lS564ManifestChainError> {
    let (genesis, rest) = chain
        .split_first()
        .ok_or(G8lS564ManifestChainError::ChainEmpty)?;
    if chain.len() > S564_MAX_CHAIN_LENGTH {
        return Err(G8lS564ManifestChainError::ChainTooLong);
    }
    let mut head_summary = genesis.validate()?;
    if !genesis.is_genesis() {
        return Err(G8lS564ManifestChainError::GenesisPredecessorHashNotZero);
    }
    let genesis_manifest_hash = manifest_hash(genesis)?;
    let mut head_manifest_hash = genesis_manifest_hash;
    let mut predecessor = genesis;
    for successor in rest {
        head_manifest_hash = verify_chain_link(predecessor, successor)?;
        head_summary = successor.validate()?;
        predecessor = successor;
    }
    Ok(G8lS564ChainReceipt {
        length: chain.len(),
        genesis_version: genesis.header.version,
        head_version: predecessor.header.version,
        genesis_manifest_hash,
        head_manifest_hash,
        head_summary,
    })
}

// ---------------------------------------------------------------------------
// Observed package exact match.
// ---------------------------------------------------------------------------

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ObservedFile {
    pub path: G8lS564ManifestPath,
    pub bytes: u64,
    pub sha256: [u8; S564_SHA256_DIGEST_LEN],
}

impl G8lS564ObservedFile {
    pub fn new(
        path: &str,
        bytes: u64,
        sha256: [u8; S564_SHA256_DIGEST_LEN],
    ) -> Result<Self, G8lS564ManifestChainError> {
        Ok(Self {
            path: G8lS564ManifestPath::new(path)?,
            bytes,
            sha256,
        })
    }
}

pub fn verify_package_against_manifest(
    manifest: &G8lS564Manifest,
    observed: &[G8lS564ObservedFile],
) -> Result<usize, G8lS564ManifestChainError> {
    let summary = manifest.validate()?;
    if observed.len() != summary.entry_count {
        return Err(G8lS564ManifestChainError::ObservedFileCountMismatch);
    }
    for (index, file) in observed.iter().enumerate() {
        if observed[..index]
            .iter()
            .any(|earlier| earlier.path == file.path)
        {
            return Err(G8lS564ManifestChainError::ObservedPathDuplicate);
        }
        let entry = manifest
            .entries()
            .iter()
            .find(|entry| entry.path == file.path)
            .ok_or(G8lS564ManifestChainError::ObservedPathUnknown)?;
        if entry.bytes != file.bytes {
            return Err(G8lS564ManifestChainError::ObservedBytesMismatch);
        }
        if entry.sha256 != file.sha256 {
            return Err(G8lS564ManifestChainError::ObservedDigestMismatch);
        }
    }
    if manifest
        .entries()
        .iter()
        .any(|entry| !observed.iter().any(|file| file.path == entry.path))
    {
        return Err(G8lS564ManifestChainError::ManifestEntryUnobserved);
    }
    Ok(observed.len())
}

pub fn canonical_s545_package_entries() -> [G8lS564ManifestEntry; S545_PACKAGE_ENTRY_COUNT] {
    [
        G8lS564ManifestEntry {
            path: G8lS564ManifestPath::from_bytes(S545_PACKAGE_IMAGE_PATH.as_bytes())
                .unwrap_or(G8lS564ManifestPath::EMPTY),
            bytes: S545_PACKAGE_IMAGE_BYTES,
            sha256: S545_PACKAGE_IMAGE_SHA256,
            role: G8lS564EntryRole::Kernel,
        },
        G8lS564ManifestEntry {
            path: G8lS564ManifestPath::from_bytes(S545_PACKAGE_DTB_PATH.as_bytes())
                .unwrap_or(G8lS564ManifestPath::EMPTY),
            bytes: S545_PACKAGE_DTB_BYTES,
            sha256: S545_PACKAGE_DTB_SHA256,
            role: G8lS564EntryRole::Dtb,
        },
        G8lS564ManifestEntry {
            path: G8lS564ManifestPath::from_bytes(S545_PACKAGE_CONFIG_PATH.as_bytes())
                .unwrap_or(G8lS564ManifestPath::EMPTY),
            bytes: S545_PACKAGE_CONFIG_BYTES,
            sha256: S545_PACKAGE_CONFIG_SHA256,
            role: G8lS564EntryRole::Config,
        },
    ]
}

pub fn canonical_s545_genesis_manifest() -> Result<G8lS564Manifest, G8lS564ManifestChainError> {
    G8lS564Manifest::new(
        S564_GENESIS_VERSION,
        S564_GENESIS_PREDECESSOR_HASH,
        &canonical_s545_package_entries(),
    )
}

pub fn canonical_s545_observed_package() -> [G8lS564ObservedFile; S545_PACKAGE_ENTRY_COUNT] {
    let entries = canonical_s545_package_entries();
    let observe = |entry: &G8lS564ManifestEntry| G8lS564ObservedFile {
        path: entry.path,
        bytes: entry.bytes,
        sha256: entry.sha256,
    };
    [
        observe(&entries[0]),
        observe(&entries[1]),
        observe(&entries[2]),
    ]
}

// ---------------------------------------------------------------------------
// Fail-closed publication service.
// ---------------------------------------------------------------------------

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct G8lS564ManifestChainReceipt {
    pub sequence: usize,
    pub predecessor_sequence: usize,
    pub r1_stage: u8,
    pub chain: G8lS564ChainReceipt,
    pub observed_files_matched: usize,
    pub signature_verified: bool,
    pub signature_scope_sequence: usize,
    pub s540_physical_verdict_retained_red: bool,
    pub s543_physical_verdict_retained_red: bool,
    pub automatic_promotion: bool,
    pub hardware_present: bool,
    pub supported_profile_runtime_observations: usize,
    pub physical_observations: usize,
    pub runbook_executed: bool,
}

#[derive(Debug)]
pub struct G8lS564ManifestChainState {
    receipt: Option<G8lS564ManifestChainReceipt>,
}

impl G8lS564ManifestChainState {
    pub const fn new() -> Self {
        Self { receipt: None }
    }

    pub const fn receipt(&self) -> Option<G8lS564ManifestChainReceipt> {
        self.receipt
    }
}

impl Default for G8lS564ManifestChainState {
    fn default() -> Self {
        Self::new()
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS564ManifestChainOutcome {
    Published(G8lS564ManifestChainReceipt),
    Retained(G8lS564ManifestChainReceipt),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum G8lS564ManifestChainError {
    MessageTooLong,
    ChainEmpty,
    ChainTooLong,
    VersionZero,
    EntryCountZero,
    EntryCountTooLarge,
    EntryCountMismatch,
    PathEmpty,
    PathTooLong,
    PathInvalidCharacter,
    PathInvalidShape,
    DuplicatePath,
    EntryBytesZero,
    EntryBytesTooLarge,
    PackageBytesOverflow,
    PackageBytesTooLarge,
    SingletonRoleRepeated,
    InvalidRole,
    EncodingLengthMismatch,
    EncodingTooLong,
    EncodingTruncated,
    EncodingMagicMismatch,
    EncodingTrailingBytes,
    GenesisPredecessorHashNotZero,
    PredecessorHashZero,
    PredecessorHashMismatch,
    VersionNotMonotonic,
    ObservedFileCountMismatch,
    ObservedPathDuplicate,
    ObservedPathUnknown,
    ObservedBytesMismatch,
    ObservedDigestMismatch,
    ManifestEntryUnobserved,
    SignatureOutOfScope,
    PublishedStateDrift,
}

impl G8lS564ManifestChainError {
    pub const fn diagnostic_code(self) -> u64 {
        match self {
            Self::MessageTooLong => 1,
            Self::ChainEmpty => 2,
            Self::ChainTooLong => 3,
            Self::VersionZero => 4,
            Self::EntryCountZero => 5,
            Self::EntryCountTooLarge => 6,
            Self::EntryCountMismatch => 7,
            Self::PathEmpty => 8,
            Self::PathTooLong => 9,
            Self::PathInvalidCharacter => 10,
            Self::PathInvalidShape => 11,
            Self::DuplicatePath => 12,
            Self::EntryBytesZero => 13,
            Self::EntryBytesTooLarge => 14,
            Self::PackageBytesOverflow => 15,
            Self::PackageBytesTooLarge => 16,
            Self::SingletonRoleRepeated => 17,
            Self::InvalidRole => 18,
            Self::EncodingLengthMismatch => 19,
            Self::EncodingTooLong => 20,
            Self::EncodingTruncated => 21,
            Self::EncodingMagicMismatch => 22,
            Self::EncodingTrailingBytes => 23,
            Self::GenesisPredecessorHashNotZero => 24,
            Self::PredecessorHashZero => 25,
            Self::PredecessorHashMismatch => 26,
            Self::VersionNotMonotonic => 27,
            Self::ObservedFileCountMismatch => 28,
            Self::ObservedPathDuplicate => 29,
            Self::ObservedPathUnknown => 30,
            Self::ObservedBytesMismatch => 31,
            Self::ObservedDigestMismatch => 32,
            Self::ManifestEntryUnobserved => 33,
            Self::SignatureOutOfScope => 34,
            Self::PublishedStateDrift => 35,
        }
    }

    pub const ALL: [Self; 35] = [
        Self::MessageTooLong,
        Self::ChainEmpty,
        Self::ChainTooLong,
        Self::VersionZero,
        Self::EntryCountZero,
        Self::EntryCountTooLarge,
        Self::EntryCountMismatch,
        Self::PathEmpty,
        Self::PathTooLong,
        Self::PathInvalidCharacter,
        Self::PathInvalidShape,
        Self::DuplicatePath,
        Self::EntryBytesZero,
        Self::EntryBytesTooLarge,
        Self::PackageBytesOverflow,
        Self::PackageBytesTooLarge,
        Self::SingletonRoleRepeated,
        Self::InvalidRole,
        Self::EncodingLengthMismatch,
        Self::EncodingTooLong,
        Self::EncodingTruncated,
        Self::EncodingMagicMismatch,
        Self::EncodingTrailingBytes,
        Self::GenesisPredecessorHashNotZero,
        Self::PredecessorHashZero,
        Self::PredecessorHashMismatch,
        Self::VersionNotMonotonic,
        Self::ObservedFileCountMismatch,
        Self::ObservedPathDuplicate,
        Self::ObservedPathUnknown,
        Self::ObservedBytesMismatch,
        Self::ObservedDigestMismatch,
        Self::ManifestEntryUnobserved,
        Self::SignatureOutOfScope,
        Self::PublishedStateDrift,
    ];
}

/// Verifies a bounded manifest chain, matches the observed package against
/// the head manifest byte-for-byte, and publishes or retains an exact receipt.
/// Signature verification is not modelled; requesting it fails closed.
pub fn service_s564_model_manifest_chain_verify(
    state: &mut G8lS564ManifestChainState,
    chain: &[G8lS564Manifest],
    observed_package: &[G8lS564ObservedFile],
    signature_verification_requested: bool,
) -> Result<G8lS564ManifestChainOutcome, G8lS564ManifestChainError> {
    if signature_verification_requested {
        return Err(G8lS564ManifestChainError::SignatureOutOfScope);
    }
    let chain_receipt = verify_manifest_chain(chain)?;
    let head = &chain[chain.len() - 1];
    let observed_files_matched = verify_package_against_manifest(head, observed_package)?;
    let receipt = G8lS564ManifestChainReceipt {
        sequence: S564_SEQUENCE,
        predecessor_sequence: S564_EXPECTED_PREDECESSOR,
        r1_stage: S564_R1_STAGE,
        chain: chain_receipt,
        observed_files_matched,
        signature_verified: S564_SIGNATURE_VERIFIED,
        signature_scope_sequence: S564_SIGNATURE_SCOPE_SEQUENCE,
        s540_physical_verdict_retained_red: S564_S540_PHYSICAL_VERDICT_RETAINED_RED,
        s543_physical_verdict_retained_red: S564_S543_PHYSICAL_VERDICT_RETAINED_RED,
        automatic_promotion: S564_AUTOMATIC_PROMOTION,
        hardware_present: S564_HARDWARE_PRESENT,
        supported_profile_runtime_observations: S564_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS,
        physical_observations: S564_PHYSICAL_OBSERVATIONS,
        runbook_executed: RUNBOOK_EXECUTED_IN_S564,
    };
    if let Some(published) = state.receipt {
        if published != receipt {
            return Err(G8lS564ManifestChainError::PublishedStateDrift);
        }
        return Ok(G8lS564ManifestChainOutcome::Retained(published));
    }
    state.receipt = Some(receipt);
    Ok(G8lS564ManifestChainOutcome::Published(receipt))
}
snippet sha256: 67360375745afile sha256: 67360375745a
02 · Doğrulayan test kodu

Operations komutuna bağlı focused test

tam dosyaL1–L802
simulation/tests/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model.rs::S564 r1 update package manifest hash chain model focused tests
use aselsan_microkernel_simulation::g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model::*;
use std::collections::BTreeSet;

const SOURCE: &str = include_str!(
    "../../kernel/src/g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model.rs"
);
const MAIN: &str = include_str!("../../kernel/src/main.rs");
const SIMULATION_LIB: &str = include_str!("../src/lib.rs");
const S545_SHA256SUMS: &str =
    include_str!("../../evidence/rpi5/r1/sequence-545-candidate-freeze/package/SHA256SUMS");
const S545_IMAGE: &[u8] = include_bytes!(
    "../../evidence/rpi5/r1/sequence-545-candidate-freeze/package/aselsanos-rpi5.img"
);
const S545_DTB: &[u8] = include_bytes!(
    "../../evidence/rpi5/r1/sequence-545-candidate-freeze/package/bcm2712-rpi-5-b.dtb"
);
const S545_CONFIG: &[u8] =
    include_bytes!("../../evidence/rpi5/r1/sequence-545-candidate-freeze/package/config.txt");

const SHA256_EMPTY: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const SHA256_ABC: &str = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
const SHA256_448_BIT: &str = "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1";
const SHA256_MILLION_A: &str = "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0";

fn hex(digest: &[u8; 32]) -> String {
    String::from_utf8(digest_to_hex(digest).to_vec()).unwrap()
}

fn app_entry(path: &str, bytes: u64, seed: u8) -> G8lS564ManifestEntry {
    G8lS564ManifestEntry::new(path, bytes, [seed; 32], G8lS564EntryRole::App).unwrap()
}

fn genesis() -> G8lS564Manifest {
    canonical_s545_genesis_manifest().unwrap()
}

fn successor_of(predecessor: &G8lS564Manifest, version: u32) -> G8lS564Manifest {
    let mut entries = canonical_s545_package_entries().to_vec();
    entries.push(app_entry("apps/dialer.elf", 4096, 0x5a));
    G8lS564Manifest::new(version, manifest_hash(predecessor).unwrap(), &entries).unwrap()
}

fn observed_for(manifest: &G8lS564Manifest) -> Vec<G8lS564ObservedFile> {
    manifest
        .entries()
        .iter()
        .map(|entry| G8lS564ObservedFile {
            path: entry.path,
            bytes: entry.bytes,
            sha256: entry.sha256,
        })
        .collect()
}

fn publish(
    state: &mut G8lS564ManifestChainState,
    chain: &[G8lS564Manifest],
) -> Result<G8lS564ManifestChainOutcome, G8lS564ManifestChainError> {
    let observed = observed_for(&chain[chain.len() - 1]);
    service_s564_model_manifest_chain_verify(state, chain, &observed, false)
}

#[test]
fn sequence_scope_and_nonpromotion_are_exact() {
    assert_eq!(S564_SEQUENCE, 564);
    assert_eq!(S564_EXPECTED_PREDECESSOR, 563);
    assert_eq!(S564_R1_STAGE, 4);
    assert_eq!(S564_R1_RANGE_FIRST, 536);
    assert_eq!(S564_R1_RANGE_LAST, 568);
    assert_eq!(S564_SUPPORTED_PROFILE_RUNTIME_OBSERVATIONS, 0);
    assert_eq!(S564_PHYSICAL_OBSERVATIONS, 0);
    assert_eq!(S564_PHYSICAL_OR_DEVICE_OPERATIONS, 0);
    assert_eq!(S564_SD_WRITES, 0);
    assert_eq!(S564_UART_OPENS, 0);
    assert_eq!(S564_POWER_TRANSITIONS, 0);
    assert_eq!(S564_NEW_IMMUTABLE_RAW_CAPTURES, 0);
    assert!(S564_S540_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(S564_S543_PHYSICAL_VERDICT_RETAINED_RED);
    assert!(!S564_AUTOMATIC_PROMOTION);
    assert!(!S564_BOOT_TO_UI_PHYSICALLY_OBSERVED);
    assert!(!S564_HARDWARE_PRESENT);
    assert!(!S564_R1_ACCEPTANCE_COMPLETE);
    assert!(!RUNBOOK_EXECUTED_IN_S564);
    assert_eq!(S564_MANIFEST_PATH_MAX_LEN, 64);
    assert_eq!(S564_MANIFEST_MAX_ENTRIES, 16);
    assert_eq!(S564_MAX_CHAIN_LENGTH, 8);
    assert_eq!(S564_MANIFEST_ENCODED_MAX_LEN, 45 + 16 * 106);
    assert_eq!(S564_SIGNATURE_SCOPE_SEQUENCE, 566);
    assert!(!S564_SIGNATURE_VERIFIED);
    assert_eq!(S545_PACKAGE_TOTAL_BYTES, 945_760 + 78_703 + 420);
}

#[test]
fn module_is_registered_in_kernel_and_simulation() {
    let module = "g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model";
    assert!(MAIN.contains(&format!("mod {module};")));
    assert!(SIMULATION_LIB.contains(&format!("pub mod {module};")));
}

#[test]
fn source_has_no_device_execution_or_uart_emission_surface() {
    for forbidden in [
        "unsafe",
        "asm!",
        "write_volatile",
        "crate::uart",
        "crate::arch",
        "#[no_mangle]",
        "spin::",
        "std::",
        "/dev/disk",
        "/dev/cu.",
        "diskutil",
        "dd if=",
    ] {
        assert!(!SOURCE.contains(forbidden), "forbidden token: {forbidden}");
    }
    assert!(SOURCE.contains("performs no device operation"));
    assert!(SOURCE.contains("does not rerun S540 or S543"));
    assert!(SOURCE.contains("Signature verification is out of scope"));
    assert!(SOURCE.contains("S564_PHYSICAL_OBSERVATIONS: usize = 0"));
    assert!(SOURCE.contains("RUNBOOK_EXECUTED_IN_S564: bool = false"));
}

#[test]
fn diagnostic_codes_are_nonzero_and_unique() {
    let codes: BTreeSet<u64> = G8lS564ManifestChainError::ALL
        .into_iter()
        .map(G8lS564ManifestChainError::diagnostic_code)
        .collect();
    assert_eq!(codes.len(), G8lS564ManifestChainError::ALL.len());
    assert_eq!(codes.len(), 35);
    assert!(!codes.contains(&0));
    assert_eq!(
        G8lS564ManifestChainError::PublishedStateDrift.diagnostic_code(),
        35
    );
}

#[test]
fn sha256_matches_known_vectors_empty_and_abc() {
    assert_eq!(hex(&sha256_digest(b"")), SHA256_EMPTY);
    assert_eq!(hex(&sha256_digest(b"abc")), SHA256_ABC);
    assert_eq!(
        hex(&sha256_digest(
            b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
        )),
        SHA256_448_BIT
    );
    assert_eq!(parse_hex_digest(SHA256_ABC), Some(sha256_digest(b"abc")));
    assert_eq!(parse_hex_digest("ba78"), None);
    assert_eq!(parse_hex_digest(&SHA256_ABC.to_uppercase()), None);
}

#[test]
fn sha256_streaming_split_matches_one_shot_for_every_block_boundary() {
    let million_a = vec![b'a'; 1_000_000];
    assert_eq!(hex(&sha256_digest(&million_a)), SHA256_MILLION_A);
    let message: Vec<u8> = (0..200u32).map(|i| (i * 7 % 251) as u8).collect();
    let one_shot = sha256_digest(&message);
    for split in [0usize, 1, 55, 56, 63, 64, 65, 119, 120, 127, 128, 199, 200] {
        let mut hasher = G8lS564Sha256::new();
        hasher.update(&message[..split]).unwrap();
        hasher.update(&message[split..]).unwrap();
        assert_eq!(hasher.finalize(), one_shot, "split at {split}");
    }
    for len in 0..=130usize {
        let mut hasher = G8lS564Sha256::default();
        for byte in &message[..len] {
            hasher.update(core::slice::from_ref(byte)).unwrap();
        }
        assert_eq!(hasher.finalize(), sha256_digest(&message[..len]), "len {len}");
    }
}

#[test]
fn s545_package_fixture_matches_sha256sums_and_real_files() {
    let expected: Vec<(&str, &str)> = S545_SHA256SUMS
        .lines()
        .map(|line| {
            let (digest, name) = line.split_once("  ").unwrap();
            (name, digest)
        })
        .collect();
    assert_eq!(
        expected,
        vec![
            (S545_PACKAGE_IMAGE_PATH, S545_PACKAGE_IMAGE_SHA256_HEX),
            (S545_PACKAGE_DTB_PATH, S545_PACKAGE_DTB_SHA256_HEX),
            (S545_PACKAGE_CONFIG_PATH, S545_PACKAGE_CONFIG_SHA256_HEX),
        ]
    );
    assert_eq!(hex(&S545_PACKAGE_IMAGE_SHA256), S545_PACKAGE_IMAGE_SHA256_HEX);
    assert_eq!(hex(&S545_PACKAGE_DTB_SHA256), S545_PACKAGE_DTB_SHA256_HEX);
    assert_eq!(hex(&S545_PACKAGE_CONFIG_SHA256), S545_PACKAGE_CONFIG_SHA256_HEX);
    assert_eq!(S545_IMAGE.len() as u64, S545_PACKAGE_IMAGE_BYTES);
    assert_eq!(S545_DTB.len() as u64, S545_PACKAGE_DTB_BYTES);
    assert_eq!(S545_CONFIG.len() as u64, S545_PACKAGE_CONFIG_BYTES);
    assert_eq!(sha256_digest(S545_IMAGE), S545_PACKAGE_IMAGE_SHA256);
    assert_eq!(sha256_digest(S545_DTB), S545_PACKAGE_DTB_SHA256);
    assert_eq!(sha256_digest(S545_CONFIG), S545_PACKAGE_CONFIG_SHA256);
    let observed = [
        G8lS564ObservedFile::new(
            S545_PACKAGE_IMAGE_PATH,
            S545_IMAGE.len() as u64,
            sha256_digest(S545_IMAGE),
        )
        .unwrap(),
        G8lS564ObservedFile::new(
            S545_PACKAGE_DTB_PATH,
            S545_DTB.len() as u64,
            sha256_digest(S545_DTB),
        )
        .unwrap(),
        G8lS564ObservedFile::new(
            S545_PACKAGE_CONFIG_PATH,
            S545_CONFIG.len() as u64,
            sha256_digest(S545_CONFIG),
        )
        .unwrap(),
    ];
    assert_eq!(observed, canonical_s545_observed_package());
    assert_eq!(verify_package_against_manifest(&genesis(), &observed), Ok(3));
}

#[test]
fn canonical_s545_genesis_manifest_publishes_an_exact_receipt() {
    let manifest = genesis();
    assert!(manifest.is_genesis());
    assert_eq!(manifest.header.version, S564_GENESIS_VERSION);
    assert_eq!(manifest.entries().len(), S545_PACKAGE_ENTRY_COUNT);
    let summary = manifest.validate().unwrap();
    assert_eq!(summary.total_bytes, S545_PACKAGE_TOTAL_BYTES);
    assert_eq!(
        (
            summary.kernel_entries,
            summary.dtb_entries,
            summary.config_entries,
            summary.app_entries
        ),
        (1, 1, 1, 0)
    );
    assert_eq!(summary.encoded_len, 45 + (42 + 18) + (42 + 19) + (42 + 10));
    let mut state = G8lS564ManifestChainState::new();
    let G8lS564ManifestChainOutcome::Published(receipt) = publish(&mut state, &[manifest]).unwrap()
    else {
        panic!("first S564 publication missing")
    };
    assert_eq!(state.receipt(), Some(receipt));
    assert_eq!(receipt.sequence, 564);
    assert_eq!(receipt.predecessor_sequence, 563);
    assert_eq!(receipt.r1_stage, 4);
    assert_eq!(receipt.chain.length, 1);
    assert_eq!(receipt.chain.genesis_version, 1);
    assert_eq!(receipt.chain.head_version, 1);
    assert_eq!(receipt.chain.genesis_manifest_hash, receipt.chain.head_manifest_hash);
    assert_eq!(receipt.chain.head_manifest_hash, manifest_hash(&manifest).unwrap());
    assert_eq!(receipt.chain.head_summary, summary);
    assert_eq!(receipt.observed_files_matched, 3);
    assert!(!receipt.signature_verified);
    assert_eq!(receipt.signature_scope_sequence, 566);
    assert!(receipt.s540_physical_verdict_retained_red);
    assert!(receipt.s543_physical_verdict_retained_red);
    assert!(!receipt.automatic_promotion);
    assert!(!receipt.hardware_present);
    assert_eq!(receipt.supported_profile_runtime_observations, 0);
    assert_eq!(receipt.physical_observations, 0);
    assert!(!receipt.runbook_executed);
}

#[test]
fn exact_replay_retains_the_same_receipt() {
    let chain = [genesis()];
    let mut state = G8lS564ManifestChainState::new();
    let G8lS564ManifestChainOutcome::Published(receipt) = publish(&mut state, &chain).unwrap()
    else {
        panic!("first publication missing")
    };
    assert_eq!(
        publish(&mut state, &chain),
        Ok(G8lS564ManifestChainOutcome::Retained(receipt))
    );
    assert_eq!(state.receipt(), Some(receipt));
}

#[test]
fn divergent_input_after_publication_fails_closed() {
    let first = genesis();
    let mut state = G8lS564ManifestChainState::new();
    publish(&mut state, &[first]).unwrap();
    let longer = [first, successor_of(&first, 2)];
    assert_eq!(
        publish(&mut state, &longer),
        Err(G8lS564ManifestChainError::PublishedStateDrift)
    );
    let mut relabelled = first;
    relabelled.entries[2].role = G8lS564EntryRole::App;
    assert_eq!(
        publish(&mut state, &[relabelled]),
        Err(G8lS564ManifestChainError::PublishedStateDrift)
    );
    assert_eq!(state.receipt().map(|receipt| receipt.chain.length), Some(1));
}

#[test]
fn two_link_chain_with_monotonic_version_verifies_and_hashes_the_head() {
    let first = genesis();
    let second = successor_of(&first, 2);
    let third = successor_of(&second, 7);
    let receipt = verify_manifest_chain(&[first, second, third]).unwrap();
    assert_eq!(receipt.length, 3);
    assert_eq!(receipt.genesis_version, 1);
    assert_eq!(receipt.head_version, 7);
    assert_eq!(receipt.genesis_manifest_hash, manifest_hash(&first).unwrap());
    assert_eq!(receipt.head_manifest_hash, manifest_hash(&third).unwrap());
    assert_ne!(receipt.genesis_manifest_hash, receipt.head_manifest_hash);
    assert_eq!(receipt.head_summary.entry_count, 4);
    assert_eq!(receipt.head_summary.app_entries, 1);
    assert_eq!(
        receipt.head_summary.total_bytes,
        S545_PACKAGE_TOTAL_BYTES + 4096
    );
    assert_eq!(
        verify_chain_link(&first, &second),
        Ok(manifest_hash(&second).unwrap())
    );
    let mut state = G8lS564ManifestChainState::new();
    let G8lS564ManifestChainOutcome::Published(published) =
        publish(&mut state, &[first, second, third]).unwrap()
    else {
        panic!("chain publication missing")
    };
    assert_eq!(published.chain, receipt);
    assert_eq!(published.observed_files_matched, 4);
}

#[test]
fn predecessor_hash_mismatch_or_zero_fails_closed() {
    let first = genesis();
    let mut forged = successor_of(&first, 2);
    forged.header.predecessor_manifest_hash[31] ^= 0x01;
    assert_eq!(
        verify_chain_link(&first, &forged),
        Err(G8lS564ManifestChainError::PredecessorHashMismatch)
    );
    assert_eq!(
        verify_manifest_chain(&[first, forged]),
        Err(G8lS564ManifestChainError::PredecessorHashMismatch)
    );
    let mut zeroed = successor_of(&first, 2);
    zeroed.header.predecessor_manifest_hash = S564_GENESIS_PREDECESSOR_HASH;
    assert_eq!(
        verify_chain_link(&first, &zeroed),
        Err(G8lS564ManifestChainError::PredecessorHashZero)
    );
    let second = successor_of(&first, 2);
    assert_eq!(
        verify_manifest_chain(&[second]),
        Err(G8lS564ManifestChainError::GenesisPredecessorHashNotZero)
    );
    let mut altered_predecessor = first;
    altered_predecessor.entries[2].bytes = 421;
    assert_eq!(
        verify_manifest_chain(&[altered_predecessor, second]),
        Err(G8lS564ManifestChainError::PredecessorHashMismatch)
    );
}

#[test]
fn version_must_increase_strictly_along_the_chain() {
    let first = genesis();
    for version in [0u32, 1] {
        let mut entries = canonical_s545_package_entries().to_vec();
        entries.push(app_entry("apps/dialer.elf", 4096, 0x5a));
        let hash = manifest_hash(&first).unwrap();
        let result = G8lS564Manifest::new(version, hash, &entries);
        if version == 0 {
            assert_eq!(result, Err(G8lS564ManifestChainError::VersionZero));
        } else {
            assert_eq!(
                verify_chain_link(&first, &result.unwrap()),
                Err(G8lS564ManifestChainError::VersionNotMonotonic)
            );
        }
    }
    let second = successor_of(&first, 5);
    let lower = successor_of(&second, 4);
    assert_eq!(
        verify_manifest_chain(&[first, second, lower]),
        Err(G8lS564ManifestChainError::VersionNotMonotonic)
    );
    let max = successor_of(&second, u32::MAX);
    assert_eq!(
        verify_manifest_chain(&[first, second, max]).map(|receipt| receipt.head_version),
        Ok(u32::MAX)
    );
    assert_eq!(
        G8lS564Manifest::new(0, S564_GENESIS_PREDECESSOR_HASH, &canonical_s545_package_entries()),
        Err(G8lS564ManifestChainError::VersionZero)
    );
}

#[test]
fn duplicate_path_is_rejected_in_manifest_and_observed_package() {
    let mut entries = canonical_s545_package_entries().to_vec();
    entries.push(app_entry("config.txt", 5, 0x11));
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &entries),
        Err(G8lS564ManifestChainError::DuplicatePath)
    );
    let manifest = genesis();
    let mut observed = observed_for(&manifest);
    observed[1] = observed[0];
    assert_eq!(
        verify_package_against_manifest(&manifest, &observed),
        Err(G8lS564ManifestChainError::ObservedPathDuplicate)
    );
}

#[test]
fn path_boundaries_characters_and_shape_are_enforced() {
    let sixty_four = "a".repeat(64);
    assert_eq!(G8lS564ManifestPath::new(&sixty_four).map(|p| p.len()), Ok(64));
    assert_eq!(
        G8lS564ManifestPath::new(&"a".repeat(65)),
        Err(G8lS564ManifestChainError::PathTooLong)
    );
    assert_eq!(
        G8lS564ManifestPath::new(""),
        Err(G8lS564ManifestChainError::PathEmpty)
    );
    for bad in ["config txt", "kernel\0", "ünite.bin", "a:b", "a\\b"] {
        assert_eq!(
            G8lS564ManifestPath::new(bad),
            Err(G8lS564ManifestChainError::PathInvalidCharacter),
            "{bad}"
        );
    }
    for bad in ["/boot/x", "apps/", "apps//x", "./x", "apps/../x", "..", "."] {
        assert_eq!(
            G8lS564ManifestPath::new(bad),
            Err(G8lS564ManifestChainError::PathInvalidShape),
            "{bad}"
        );
    }
    let ok = G8lS564ManifestPath::new("apps/sub-dir/file_1.v2.elf").unwrap();
    assert_eq!(ok.as_bytes(), b"apps/sub-dir/file_1.v2.elf");
    assert!(!ok.is_empty());
    assert!(G8lS564ManifestPath::EMPTY.is_empty());
    let mut entry = app_entry("apps/x.elf", 1, 0x22);
    entry.path = G8lS564ManifestPath::EMPTY;
    assert_eq!(entry.validate(), Err(G8lS564ManifestChainError::PathEmpty));
}

#[test]
fn entry_count_boundaries_are_sixteen_and_header_count_is_bound_to_the_table() {
    let entries: Vec<_> = (0..17u8)
        .map(|i| app_entry(&format!("apps/app{i:02}.elf"), 1 + u64::from(i), i))
        .collect();
    let sixteen = G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &entries[..16]).unwrap();
    assert_eq!(sixteen.entries().len(), 16);
    assert_eq!(sixteen.validate().unwrap().app_entries, 16);
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &entries),
        Err(G8lS564ManifestChainError::EntryCountTooLarge)
    );
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &[]),
        Err(G8lS564ManifestChainError::EntryCountZero)
    );
    let mut shrunk = sixteen;
    shrunk.header.entry_count = 15;
    assert_eq!(
        shrunk.validate(),
        Err(G8lS564ManifestChainError::EntryCountMismatch)
    );
    let mut inflated = genesis();
    inflated.header.entry_count = 17;
    assert_eq!(
        inflated.validate(),
        Err(G8lS564ManifestChainError::EntryCountTooLarge)
    );
    let mut hollow = genesis();
    hollow.header.entry_count = 4;
    assert_eq!(hollow.validate(), Err(G8lS564ManifestChainError::PathEmpty));
}

#[test]
fn size_limits_and_total_overflow_fail_closed() {
    assert_eq!(
        G8lS564ManifestEntry::new("apps/z.elf", 0, [0; 32], G8lS564EntryRole::App),
        Err(G8lS564ManifestChainError::EntryBytesZero)
    );
    assert!(
        G8lS564ManifestEntry::new("apps/z.elf", S564_MAX_ENTRY_BYTES, [0; 32], G8lS564EntryRole::App)
            .is_ok()
    );
    assert_eq!(
        G8lS564ManifestEntry::new(
            "apps/z.elf",
            S564_MAX_ENTRY_BYTES + 1,
            [0; 32],
            G8lS564EntryRole::App
        ),
        Err(G8lS564ManifestChainError::EntryBytesTooLarge)
    );
    let four_max: Vec<_> = (0..4u8)
        .map(|i| app_entry(&format!("apps/big{i}.bin"), S564_MAX_ENTRY_BYTES, i))
        .collect();
    let at_limit = G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &four_max).unwrap();
    assert_eq!(at_limit.validate().unwrap().total_bytes, S564_MAX_PACKAGE_BYTES);
    let mut five = four_max.clone();
    five.push(app_entry("apps/big4.bin", 1, 4));
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &five),
        Err(G8lS564ManifestChainError::PackageBytesTooLarge)
    );
    let mut overflow = at_limit;
    overflow.entries[0].bytes = u64::MAX;
    overflow.entries[1].bytes = 1;
    assert_eq!(
        overflow.validate(),
        Err(G8lS564ManifestChainError::EntryBytesTooLarge)
    );
    let mut sum_overflow = overflow;
    sum_overflow.entries[0].bytes = 1;
    sum_overflow.entries[1].bytes = 1;
    let forced = G8lS564ManifestSummary {
        entry_count: 0,
        total_bytes: u64::MAX,
        kernel_entries: 0,
        dtb_entries: 0,
        config_entries: 0,
        app_entries: 0,
        encoded_len: 0,
    };
    assert_eq!(
        forced.total_bytes.checked_add(1),
        None,
        "checked total arithmetic is the overflow guard"
    );
    assert!(sum_overflow.validate().is_ok());
}

#[test]
fn observed_package_mismatch_fails_closed_on_every_field() {
    let manifest = genesis();
    let baseline = observed_for(&manifest);
    let mut bytes_drift = baseline.clone();
    bytes_drift[0].bytes += 1;
    assert_eq!(
        verify_package_against_manifest(&manifest, &bytes_drift),
        Err(G8lS564ManifestChainError::ObservedBytesMismatch)
    );
    let mut digest_drift = baseline.clone();
    digest_drift[1].sha256[0] ^= 0x80;
    assert_eq!(
        verify_package_against_manifest(&manifest, &digest_drift),
        Err(G8lS564ManifestChainError::ObservedDigestMismatch)
    );
    let mut renamed = baseline.clone();
    renamed[2].path = G8lS564ManifestPath::new("config.bak").unwrap();
    assert_eq!(
        verify_package_against_manifest(&manifest, &renamed),
        Err(G8lS564ManifestChainError::ObservedPathUnknown)
    );
    assert_eq!(
        verify_package_against_manifest(&manifest, &baseline[..2]),
        Err(G8lS564ManifestChainError::ObservedFileCountMismatch)
    );
    let mut extra = baseline.clone();
    extra.push(G8lS564ObservedFile::new("extra.bin", 1, [1; 32]).unwrap());
    assert_eq!(
        verify_package_against_manifest(&manifest, &extra),
        Err(G8lS564ManifestChainError::ObservedFileCountMismatch)
    );
    let mut state = G8lS564ManifestChainState::new();
    assert_eq!(
        service_s564_model_manifest_chain_verify(&mut state, &[manifest], &digest_drift, false),
        Err(G8lS564ManifestChainError::ObservedDigestMismatch)
    );
    assert_eq!(state.receipt(), None);
    assert_eq!(verify_package_against_manifest(&manifest, &baseline), Ok(3));
}

#[test]
fn singleton_roles_cannot_repeat_and_role_codes_round_trip() {
    for role in [
        G8lS564EntryRole::Kernel,
        G8lS564EntryRole::Dtb,
        G8lS564EntryRole::Config,
        G8lS564EntryRole::App,
    ] {
        assert_eq!(G8lS564EntryRole::from_code(role.code()), Some(role));
        assert_eq!(role.is_singleton(), role != G8lS564EntryRole::App);
    }
    assert_eq!(G8lS564EntryRole::from_code(0), None);
    assert_eq!(G8lS564EntryRole::from_code(5), None);
    assert_eq!(G8lS564EntryRole::from_code(0xff), None);
    let mut entries = canonical_s545_package_entries().to_vec();
    entries.push(
        G8lS564ManifestEntry::new("kernel-b.img", 10, [3; 32], G8lS564EntryRole::Kernel).unwrap(),
    );
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &entries),
        Err(G8lS564ManifestChainError::SingletonRoleRepeated)
    );
    let two_apps = [app_entry("apps/a.elf", 1, 1), app_entry("apps/b.elf", 2, 2)];
    assert_eq!(
        G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &two_apps)
            .unwrap()
            .validate()
            .unwrap()
            .app_entries,
        2
    );
}

#[test]
fn signature_verification_is_out_of_scope_and_fails_closed() {
    let chain = [genesis()];
    let observed = observed_for(&chain[0]);
    let mut state = G8lS564ManifestChainState::new();
    assert_eq!(
        service_s564_model_manifest_chain_verify(&mut state, &chain, &observed, true),
        Err(G8lS564ManifestChainError::SignatureOutOfScope)
    );
    assert_eq!(state.receipt(), None);
    let G8lS564ManifestChainOutcome::Published(receipt) =
        service_s564_model_manifest_chain_verify(&mut state, &chain, &observed, false).unwrap()
    else {
        panic!("publication missing")
    };
    assert!(!receipt.signature_verified);
    assert_eq!(receipt.signature_scope_sequence, S564_SIGNATURE_SCOPE_SEQUENCE);
    assert_eq!(
        service_s564_model_manifest_chain_verify(&mut state, &chain, &observed, true),
        Err(G8lS564ManifestChainError::SignatureOutOfScope)
    );
}

#[test]
fn manifest_encoding_is_deterministic_bounded_and_round_trips() {
    let manifest = genesis();
    let encoded = encode_manifest(&manifest).unwrap();
    assert_eq!(encoded.len(), manifest.validate().unwrap().encoded_len);
    assert_eq!(&encoded[..8], b"ASOSMF01");
    assert_eq!(&encoded[8..12], &1u32.to_le_bytes());
    assert_eq!(&encoded[12..44], &[0u8; 32]);
    assert_eq!(encoded[44], 3);
    assert_eq!(encoded[45], G8lS564EntryRole::Kernel.code());
    assert_eq!(encoded[46] as usize, S545_PACKAGE_IMAGE_PATH.len());
    assert_eq!(encode_manifest(&manifest).unwrap(), encoded);
    assert_eq!(decode_manifest(&encoded), Ok(manifest));
    assert_eq!(manifest_hash(&manifest).unwrap(), sha256_digest(&encoded));
    let entries: Vec<_> = (0..16u8)
        .map(|i| app_entry(&"x".repeat(64), 1, i))
        .take(1)
        .chain((1..16u8).map(|i| app_entry(&format!("{}{i:02}", "y".repeat(62)), 1, i)))
        .collect();
    let widest = G8lS564Manifest::new(1, S564_GENESIS_PREDECESSOR_HASH, &entries).unwrap();
    let widest_encoded = encode_manifest(&widest).unwrap();
    assert_eq!(widest_encoded.len(), S564_MANIFEST_ENCODED_MAX_LEN);
    assert_eq!(decode_manifest(&widest_encoded), Ok(widest));
    let mut variants = BTreeSet::new();
    variants.insert(manifest_hash(&manifest).unwrap());
    let mut version = manifest;
    version.header.version = 2;
    variants.insert(manifest_hash(&version).unwrap());
    let mut bytes = manifest;
    bytes.entries[2].bytes = 421;
    variants.insert(manifest_hash(&bytes).unwrap());
    let mut digest = manifest;
    digest.entries[0].sha256[5] ^= 1;
    variants.insert(manifest_hash(&digest).unwrap());
    let mut role = manifest;
    role.entries[2].role = G8lS564EntryRole::App;
    variants.insert(manifest_hash(&role).unwrap());
    let mut path = manifest;
    path.entries[2].path = G8lS564ManifestPath::new("config.TXT").unwrap();
    variants.insert(manifest_hash(&path).unwrap());
    assert_eq!(variants.len(), 6);
}

#[test]
fn malformed_encodings_are_rejected() {
    let manifest = genesis();
    let encoded = encode_manifest(&manifest).unwrap();
    assert_eq!(
        decode_manifest(&encoded[..encoded.len() - 1]),
        Err(G8lS564ManifestChainError::EncodingTruncated)
    );
    assert_eq!(
        decode_manifest(&encoded[..20]),
        Err(G8lS564ManifestChainError::EncodingTruncated)
    );
    let mut trailing = encoded.clone();
    trailing.push(0);
    assert_eq!(
        decode_manifest(&trailing),
        Err(G8lS564ManifestChainError::EncodingTrailingBytes)
    );
    let mut magic = encoded.clone();
    magic[7] = b'2';
    assert_eq!(
        decode_manifest(&magic),
        Err(G8lS564ManifestChainError::EncodingMagicMismatch)
    );
    let mut role = encoded.clone();
    role[45] = 9;
    assert_eq!(
        decode_manifest(&role),
        Err(G8lS564ManifestChainError::InvalidRole)
    );
    let mut count = encoded.clone();
    count[44] = 17;
    assert_eq!(
        decode_manifest(&count),
        Err(G8lS564ManifestChainError::EntryCountTooLarge)
    );
    let mut path_char = encoded.clone();
    path_char[47] = b' ';
    assert_eq!(
        decode_manifest(&path_char),
        Err(G8lS564ManifestChainError::PathInvalidCharacter)
    );
    let mut duplicate = encoded.clone();
    let second_entry_path = 45 + 42 + 18 + 2;
    duplicate[second_entry_path - 1] = S545_PACKAGE_IMAGE_PATH.len() as u8;
    duplicate.splice(
        second_entry_path..second_entry_path + S545_PACKAGE_DTB_PATH.len(),
        S545_PACKAGE_IMAGE_PATH.bytes(),
    );
    assert_eq!(
        decode_manifest(&duplicate),
        Err(G8lS564ManifestChainError::DuplicatePath)
    );
    assert_eq!(
        decode_manifest(&vec![0u8; S564_MANIFEST_ENCODED_MAX_LEN + 1]),
        Err(G8lS564ManifestChainError::EncodingTooLong)
    );
    assert_eq!(
        decode_manifest(&[]),
        Err(G8lS564ManifestChainError::EncodingTruncated)
    );
}

#[test]
fn chain_length_boundaries_are_one_and_eight() {
    assert_eq!(
        verify_manifest_chain(&[]),
        Err(G8lS564ManifestChainError::ChainEmpty)
    );
    let mut state = G8lS564ManifestChainState::new();
    assert_eq!(
        service_s564_model_manifest_chain_verify(&mut state, &[], &[], false),
        Err(G8lS564ManifestChainError::ChainEmpty)
    );
    let mut chain = vec![genesis()];
    for version in 2..=9u32 {
        let previous = chain[chain.len() - 1];
        chain.push(successor_of(&previous, version));
    }
    assert_eq!(
        verify_manifest_chain(&chain[..8]).map(|receipt| (receipt.length, receipt.head_version)),
        Ok((8, 8))
    );
    assert_eq!(
        verify_manifest_chain(&chain),
        Err(G8lS564ManifestChainError::ChainTooLong)
    );
    let mut state = G8lS564ManifestChainState::new();
    let observed = observed_for(&chain[7]);
    assert!(matches!(
        service_s564_model_manifest_chain_verify(&mut state, &chain[..8], &observed, false),
        Ok(G8lS564ManifestChainOutcome::Published(_))
    ));
    assert_eq!(
        service_s564_model_manifest_chain_verify(&mut state, &chain, &observed, false),
        Err(G8lS564ManifestChainError::ChainTooLong)
    );
}

#[test]
fn sha256_message_length_guard_and_state_default_are_exact() {
    let state = G8lS564ManifestChainState::default();
    assert_eq!(state.receipt(), None);
    let mut hasher = G8lS564Sha256::new();
    hasher.update(b"abc").unwrap();
    let copy = hasher;
    assert_eq!(copy.finalize(), hasher.finalize());
    assert_eq!(hex(&G8lS564Sha256::new().finalize()), SHA256_EMPTY);
    assert_eq!(
        G8lS564ManifestChainError::MessageTooLong.diagnostic_code(),
        1
    );
    assert_eq!(
        hex(&sha256_digest(S545_CONFIG)),
        S545_PACKAGE_CONFIG_SHA256_HEX
    );
}
snippet sha256: 3e0bdc02a57ffile sha256: 3e0bdc02a57f
03 · Kapı kimlik kaydı

Operations sıra, kimlik ve başlık bağı

tam Operations kaydıL2074–L2130
website/src/lib/operations.ts::g8l-s564-r1-update-package-manifest-hash-chain-model
  {
    id: "g8l-s564-r1-update-package-manifest-hash-chain-model",
    date: "2026-08-30",
    sequence: 564,
    status: "passed",
    umbrella_status: "partial",
    title: "S564 · R1 güncelleme: paket manifesti ve hash zinciri modeli",
    summary:
      "S564 kaynak/host model kapısı PASS'tir: güncelleme paketi manifesti (path ≤ 64 bayt, bayt sayısı, 32 baytlık SHA-256 özeti, Kernel/Dtb/Config/App rolleri), manifest başlığı (monoton sürüm, öncül manifest hash'i, ≤ 16 giriş), deterministik kanonik kodlama, öncül→ardıl hash zinciri doğrulaması ve gözlenen paketin head manifeste bayt-bayt exact eşleşmesi saf Rust modeli olarak eklendi; SHA-256 modülün içinde saf ve streaming olarak implemente edildi ve \"\" ile \"abc\" bilinen vektörlerine sabitlendi. S545 candidate paket kimlikleri (aselsanos-rpi5.img 945760 B / ed1901a9…, dtb 78703 B / 40a2fbe9…, config.txt 420 B / aef848bf…) fixture olarak taşınır ve SHA256SUMS ile dondurulmuş dosyalardan yeniden hesaplanan özetlerle bayt-bayt eşleşmek zorundadır; her sapma fail-closed reddedilir. İmza doğrulaması S564 kapsamı dışıdır ve açıkça fail-closed reddedilir (S566/R2). Focused 24/24 PASS'tir. S540 ve S543 fiziksel RED immutable kalır; hiçbir SD, UART, board veya güncelleme işlemi yoktur; physical observation=0, RUNBOOK_EXECUTED_IN_S564=NO, Boot-to-UI=false ve R1 acceptance=false'dur. S565 aşamalı güncelleme apply/rollback modelidir ve bu modülün SHA-256 ve zincir makbuzlarını tüketecektir.",
    evidence: [
      "S564, S563'ten ayrı kaynak modülü, 24-test focused binary, proof, status bloğu ve Operations kaydına sahiptir; kernel ve simulation crate'lerinde kayıtlıdır ancak hiçbir boot, IRQ, scheduler veya driver yoluna bağlanmamıştır.",
      "Dar S564 source/host model status=PASS; R1 umbrella=PARTIAL ve S540/S543 physical gate status=RED olarak ayrı tutulur.",
      "SHA-256 modülün içinde FIPS 180-4'e göre saf, no_std ve streaming (64 baytlık blok tamponu, checked 2^61 bayt mesaj sınırı) olarak implemente edilmiştir; \"\" → e3b0c442…52b855, \"abc\" → ba7816bf…f20015ad, 448-bit iki-blok vektörü ve bir milyon 'a' vektörü sabitlenmiştir; 200 baytlık mesajın her bölünmesi ve bayt-bayt streaming tek atımlık özeti yeniden üretir; S565 bu implementasyonu yeniden kullanacaktır.",
      "Manifest girişi path'i ≤ 64 bayt ASCII alfasayısal + '. - _ /' ile sınırlıdır; başta/sonda '/', boş segment, '.' ve '..' segmentleri fail-closed reddedilir; giriş bayt sayısı 1..=67108864 aralığındadır ve rol kodları 1..4 dışında her değer InvalidRole verir.",
      "Manifest başlığı monoton sürüm (0 reddedilir), 32 baytlık öncül manifest hash'i (all-zero yalnız genesis için) ve 1..=16 giriş sayısı taşır; tabloyla uyuşmayan başlık sayısı, tekrarlanan path ve tekil Kernel/Dtb/Config rollerinin tekrarı fail-closed reddedilir; toplam paket boyutu checked aritmetikle 268435456 bayt sınırına karşı toplanır.",
      "Kanonik kodlama ASOSMF01 magic, little-endian sürüm, öncül hash, giriş sayısı ve giriş başına rol/path-uzunluğu/path/bayt/özet alanlarından oluşur; en çok 1741 bayttır, deterministiktir, decoder üzerinden round-trip eder ve SHA-256'sı manifest hash'idir; kesik, taşan, trailing-byte'lı, yanlış magic'li, geçersiz rollü ve tekrarlanan path'li kodlamalar fail-closed reddedilir.",
      "Zincir doğrulaması öncül→ardıl yönünde 1..=8 uzunlukla sınırlıdır; ilk manifest genesis olmak zorundadır, her ardıl öncülünün kodlamasının exact SHA-256'sını ve kesin olarak daha büyük bir sürümü taşımak zorundadır; hash uyuşmazlığı, sıfır öncül hash'i ve monoton olmayan sürüm fail-closed reddedilir.",
      "Gözlenen paket head manifest girişleriyle sayı, path, bayt ve özet olarak exact eşleşmek zorundadır; eksik, fazla, yeniden adlandırılmış, tekrarlanan, yeniden boyutlandırılmış veya yeniden hash'lenmiş her dosya fail-closed reddedilir.",
      "S545 paket fixture'ı aselsanos-rpi5.img 945760 B / ed1901a991e2f9e9ae3c16f254147a2b0180686a8d70ca5d7353374fee08d467 (Kernel), bcm2712-rpi-5-b.dtb 78703 B / 40a2fbe9c29e8b9a4912cf726a943068defb779fc052ec38e457a79c58abca00 (Dtb) ve config.txt 420 B / aef848bf6e0c324148eade5054a15c71a1e8c04814a3ed2e680056f87c1f9bba (Config) kimliklerini taşır; focused test evidence/rpi5/r1/sequence-545-candidate-freeze/package/SHA256SUMS dosyasını yeniden okur ve üç dondurulmuş dosyanın özetlerini modülün kendi SHA-256'sıyla yeniden hesaplayıp fixture ile bayt-bayt eşleştirir.",
      "İmza doğrulaması S564 kapsamı dışıdır: servis her imza doğrulama isteğini SignatureOutOfScope ile fail-closed reddeder, receipt'te signature_verified=false ve scope sequence=566 (S566/R2) sabitlenir.",
      "Servis exact replay'de aynı receipt ile Retained döner; yayın sonrası her sapma PublishedStateDrift ile fail-closed reddedilir ve hata enum'u 35 sıfırdan farklı, benzersiz tanı kodu taşır.",
      "Focused target 1 grup / 24 passed / 0 failed / 0 ignored / 0 filtered verdi.",
      "Implementation 36461 B / 67360375745aa1b55c0d5886d4aa49b43f46b52df450a8b3d24740f822c3bfde; focused test 30136 B / 3e0bdc02a57f3d29c163ff36fafbca97a3f3f0b20a9c8877a6a00eba62d034d8 SHA-256'dır.",
      "Proof 5835 B'dir.",
      "Modül unsafe, MMIO, asm!, crate::uart, crate::arch, spin:: veya #[no_mangle] içermez; hiçbir cihaz işlemi yapılmadı ve S540/S543 yeniden koşulmadı.",
      "RUNBOOK_EXECUTED_IN_S564=NO; supported-profile runtime observations=0, physical observations=0, SD/UART/power/new-raw=0/0/0/0, Boot-to-UI physically observed=false ve R1 acceptance=false'dur.",
      "S565 aşamalı güncelleme apply/rollback modelini aynı non-claim'lerle kaynak/host kapısı olarak modelleyecek ve S564'ün SHA-256 ile manifest zinciri makbuzlarını tüketecektir; SD, UART, power veya fiziksel güncelleme koşusu yetkisi vermez.",
    ],
    commands: [
      "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model -- --test-threads=1",
    ],
    terminalSessions: [
      {
        id: "s564-focused",
        title: "S564 update paket manifesti ve hash zinciri focused acceptance",
        commandLines: [
          "CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model -- --test-threads=1",
        ],
        outputLines: [
          "test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s",
          "S564 focused=1 group / 24 passed / 0 failed",
          "hardware=none physical=0 runbook=NO",
        ],
        exitCode: 0,
        outputMode: "complete",
      },
    ],
    terminalSessionsNote:
      "S564 kaynak/host model kapısıdır; supported-profile runtime veya fiziksel PASS değildir. S540 ve S543 RED raw'ları ve kararları değişmez.",
    limitations: [
      "S564 yalnız kaynak/host modelidir; hiçbir donanım/panel/modem/board gözlemi yoktur ve hiçbir güncelleme SD karta yazılmamıştır.",
      "S540 ve S543 fiziksel RED immutable kalır; otomatik promotion yoktur ve S546 ayrı bir kapı olarak beklemededir.",
      "İmza doğrulaması modellenmemiştir ve S564'te açıkça kapsam dışıdır; S566/R2 kapsamındadır ve o zamana kadar her imza doğrulama isteği fail-closed reddedilir.",
      "Boot-to-UI fiziksel olarak gözlenmedi; R1 acceptance false kalır ve modül hiçbir production boot/IRQ/scheduler/driver yoluna bağlanmamıştır.",
      "S565 aşamalı güncelleme apply/rollback modeli tamamlanmadan güncelleme akışının uygulama tarafı modellenmiş sayılmaz; S565 de SD, UART, power veya fiziksel koşu yetkisi vermez.",
    ],
  },
snippet sha256: 72332b50c134file sha256: 9726dbf00f84
Focused test komutu
CARGO_INCREMENTAL=0 cargo test -p aselsan_microkernel_simulation --test g8l_target_dispatch_scheduler_owner_scheduler_mutation_production_migration_lifecycle_s564_r1_update_package_manifest_hash_chain_model -- --test-threads=1
proof: docs/M8.1-RPi5-G8l-S564-R1-Update-Package-Manifest-Hash-Chain-Model-Proof.md
Registry schema v5 · generator website/scripts/generate-code-gates.mjs · Tam SHA-256: 91d38c7b6222f0b4c117be786454853543da55a160e543d9b951057cc20dcc06