#!/usr/bin/env python3 """Exercise comparison and frozen-lock rejection contracts without claiming an OS rebuild.""" import hashlib import importlib.machinery import importlib.util import json from pathlib import Path import shutil import subprocess import sys import tempfile project = Path(__file__).resolve().parents[2] loader = importlib.machinery.SourceFileLoader('fds_frozen_fixture', str(project / 'tools/frozen-inputs')) spec = importlib.util.spec_from_loader(loader.name, loader) frozen = importlib.util.module_from_spec(spec) loader.exec_module(frozen) work = Path(tempfile.mkdtemp(prefix='m12-release-contracts.', dir=project / 'out')) def invoke(*args, ok=True): result = subprocess.run(list(map(str, args)), capture_output=True, text=True, timeout=60) with (work / 'commands.log').open('a') as log: log.write(repr(args) + '\n' + result.stdout + result.stderr) assert (result.returncode == 0) == ok, (args, result.returncode, result.stdout, result.stderr) return result lock = {'format': 1, 'version': '0.1.0', 'void_commit': 'a' * 40, 'source_epoch': 123, 'source_sha256': 'b' * 64, 'rust_toolchain': '1.98.0', 'host_prerequisites': {}, 'files': []} snapshot = work / 'snapshot' (snapshot / 'project').mkdir(parents=True) (snapshot / 'project/source.txt').write_bytes(b'bounded input fixture\n') (snapshot / 'temporary').mkdir() (snapshot / 'temporary').chmod(0o1777) (snapshot / 'project/executable').write_bytes(b'archive mode fixture\n') (snapshot / 'project/executable').chmod(0o775) (snapshot / 'source-link').symlink_to('project/source.txt') lock['files'] = frozen.inventory(snapshot) lock['source_sha256'] = frozen.source_digest(lock['files']) (snapshot / 'lock.json').write_text(json.dumps(lock)) verify = project / 'tools/frozen-inputs' invoke(verify, 'verify', snapshot) for case in ('bytes', 'mode', 'extra', 'missing', 'link', 'metadata'): damaged = work / ('damaged-' + case) shutil.copytree(snapshot, damaged, symlinks=True) leaf = damaged / 'project/source.txt' if case == 'bytes': leaf.write_bytes(b'changed input fixture\n') elif case == 'mode': leaf.chmod(0o600) elif case == 'extra': (damaged / 'extra').mkdir() elif case == 'missing': leaf.unlink() elif case == 'link': (damaged / 'source-link').unlink() (damaged / 'source-link').symlink_to('/etc/passwd') elif case == 'metadata': wrong = dict(lock, rust_toolchain='../unexpected') (damaged / 'lock.json').write_text(json.dumps(wrong)) invoke(verify, 'verify', damaged, ok=False) print('PASS: frozen input byte/mode/path/symlink and metadata tampering rejected', flush=True) # Exercise the actual release archive writer and the documented extraction # recipe. Default tar extraction masks modes and strips sticky bits, which would # make an otherwise intact build-input archive fail its frozen lock immediately. sys.path.insert(0, str(project / 'tools')) archive_loader = importlib.machinery.SourceFileLoader('fds_assembly_fixture', str(project / 'tools/assemble-release')) archive_spec = importlib.util.spec_from_loader(archive_loader.name, archive_loader) assembly = importlib.util.module_from_spec(archive_spec) archive_loader.exec_module(assembly) for name in ('one', 'two'): assembly.packed_tree(snapshot, work / f'{name}.tar.zst', lock['source_epoch']) assert frozen.digest(work / 'one.tar.zst') == frozen.digest(work / 'two.tar.zst') unpacked = work / 'unpacked' unpacked.mkdir() invoke('tar', '--extract', '--zstd', '--same-permissions', '--no-same-owner', '--file', work / 'one.tar.zst', '--directory', unpacked) invoke(verify, 'verify', unpacked) print('PASS: deterministic release archive and complete mode/symlink-preserving extraction', flush=True) first, second = work / 'first', work / 'second' paths = [f'out/{name}' for name in ('fds-system-cli.img', 'fds-system-development.img', 'fds-recovery.img', 'fds-boot.img', 'fds-internal.img', 'rootfs-cli.tar', 'rootfs-development.tar', 'rootfs-recovery.tar')] paths += ['out/initramfs/initramfs.cpio' + suffix for suffix in ('', '.gz', '.lz4', '.zst')] paths += ['out/kernel/boot/' + name for name in ('kernel_2712.img', 'bcm2712-rpi-5-b.dtb')] paths += ['out/eeprom-production-latest/' + name for name in ('configured.bin', 'rollback.bin', 'configured.conf', 'original.conf')] for name in ('base', 'base-files', 'init', 'cli', 'cartridged', 'dasungd', 'kernel', 'dhcpcd', 'eink'): paths.append(f'out/packages/fds-{name}-0.1.0_1.aarch64.xbps') for name in paths: path = first / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(hashlib.sha256(name.encode()).digest()) (first / '.host/frozen').mkdir(parents=True) (first / '.host/frozen/lock.json').write_text(json.dumps(lock)) shutil.copytree(first, second) compare = project / 'tools/compare-builds' invoke(compare, first, second, '--report', work / 'equal.json') result = json.loads((work / 'equal.json').read_text()) assert result['status'] == 'passed' and len(result['artifacts']) == 27 invoke(compare, first, first, '--report', work / 'same-tree.json', ok=False) invoke(compare, first, second, '--report', work / 'equal.json', ok=False) changed = second / 'out/fds-system-cli.img' changed.write_bytes(b'changed image fixture') invoke(compare, first, second, '--report', work / 'different.json', ok=False) result = json.loads((work / 'different.json').read_text()) assert result['status'] == 'failed' assert [x['name'] for x in result['artifacts'] if not x['identical']] == ['fds-system-cli.img'] changed.unlink() invoke(compare, first, second, '--report', work / 'missing.json', ok=False) assert not (work / 'missing.json').exists() shutil.copyfile(first / 'out/fds-system-cli.img', changed) (second / '.host/frozen/lock.json').write_text(json.dumps(dict(lock, source_epoch=456))) invoke(compare, first, second, '--report', work / 'wrong-inputs.json', ok=False) assert not (work / 'wrong-inputs.json').exists() print('PASS: complete comparison, changed/missing artifact, same-tree, existing-report and different-input rejection', flush=True) # Even before a complete build exists, assembly must reject an existing output # and must not resolve a private-key symlink past the signer's no-follow policy. key = work / 'test.key' key.write_bytes(bytes(32)) key.chmod(0o600) output = work / 'existing-output' output.mkdir() (output / 'marker').write_text('preserve') command = [project / 'tools/assemble-release', '--inputs', snapshot, '--comparison-build', second, '--output-directory', output, '--key', key] invoke(*command, ok=False) assert (output / 'marker').read_text() == 'preserve' and len(list(output.iterdir())) == 1 linked = work / 'linked.key' linked.symlink_to(key) command[-1] = linked result = invoke(*command, ok=False) assert 'not a symlink' in result.stderr print(f'PASS: release output preservation and private-key symlink rejection: {work}') print('NOTE: these are small contract fixtures; actual source-to-image offline reproduction remains a separate gate')