"""Unsigned UAT tooling fixtures, not candidate ISO/boot acceptance.""" import importlib.util import io import json from pathlib import Path import shutil import subprocess import sys import tarfile import tempfile import unittest from unittest.mock import patch SCRIPTS = Path(__file__).resolve().parents[1] sys.path.insert(0, str(SCRIPTS)) def load(name, path): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module uat = load('uat', SCRIPTS / 'build-iso-uat.py') security = load('kernel_security', SCRIPTS / 'kernel-security.py') class UATTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory(prefix='unsigned-uat-fixture-', dir='/tmp/opencode') self.addCleanup(self.temp.cleanup) self.root = Path(self.temp.name) self.staged = self.root / 'staged' self.image = self.root / 'image/archipelago' self.staged.mkdir() (self.image / 'bin').mkdir(parents=True) for name, installed in [('backend', 'archipelago'), ('reticulum', 'archy-reticulum-daemon'), ('rnodeconf', 'archy-rnodeconf'), ('esptool', 'archy-esptool')]: (self.staged / name).write_bytes(('synthetic ' + name).encode()) shutil.copyfile(self.staged / name, self.image / 'bin' / installed) for root in (self.staged, self.image): (root / 'unsigned-uat-oci').mkdir() (root / 'unsigned-uat-oci/BUNDLE.json').write_text('{"fixture":true}') (root / 'web-ui/packages').mkdir(parents=True) (root / 'web-ui/index.html').write_text('synthetic dashboard') (root / 'web-ui/packages/archipelago-companion.apk').write_bytes(b'synthetic APK') (root / 'web-ui/packages/archipelago-companion.json').write_text('{"fixture":true}') def test_real_iso_roundtrip_and_byte_tamper(self): iso = self.root / 'fixture.iso' uat.run(['xorriso', '-as', 'mkisofs', '-R', '-o', iso, self.root / 'image'], self.root, self.root / 'mkiso.log') extracted = self.root / 'extracted' uat.run(['xorriso', '-osirrox', 'on', '-indev', iso, '-extract', '/', extracted], self.root, self.root / 'extract.log') actual = extracted / 'archipelago' uat.compare_payload(actual, self.staged) for name in ('bin/archipelago', 'web-ui/index.html', 'web-ui/packages/archipelago-companion.apk', 'web-ui/packages/archipelago-companion.json', 'bin/archy-rnodeconf'): path = actual / name original = path.read_bytes() path.write_bytes(original + b'tampered') with self.subTest(name=name), self.assertRaisesRegex(ValueError, 'byte mismatch'): uat.compare_payload(actual, self.staged) path.write_bytes(original) def test_extra_and_symlink_payload_rejected(self): extra = self.image / 'web-ui/extra' extra.write_bytes(b'stale') with self.assertRaises(ValueError): uat.compare_payload(self.image, self.staged) extra.unlink() extra.symlink_to(self.staged / 'backend') with self.assertRaisesRegex(ValueError, 'symlink'): uat.compare_payload(self.image, self.staged) def receipt(self): result = {'schema': 'archipelago-unsigned-uat-input-v3', 'qualification': 'qualified-for-unsigned-uat', 'reviewed_by': 'fixture reviewer'} for name in (*uat.ARTIFACTS, 'evidence'): path = self.root / name path.write_text('synthetic input ' + name) result[name] = {'path': str(path), 'sha256': uat.sha(path)} archive = self.root / 'kernel.deb' archive.write_bytes(b'fixture archive') pin = dict(architecture='amd64', package='linux-image-6.12.113+deb13-amd64', version='6.12.113-1', source='linux-signed-amd64', archive_sha256=uat.sha(archive), vmlinuz_sha256=uat.payload.digest(b'kernel')) payload = self.root / 'kernel-payload.json' payload.write_text(json.dumps({**pin, 'origin': 'Debian', 'files': {'/boot/vmlinuz-6.12.113+deb13-amd64': pin['vmlinuz_sha256']}})) result['kernel'] = {'pin': pin, **{key: {'path': str(path), 'sha256': uat.sha(path)} for key, path in [('archive', archive), ('payload_receipt', payload), ('evidence', payload)]}} return result def test_qualification_and_artifact_binding(self): receipt = self.receipt() self.assertEqual(len(uat.inputs(receipt)), 9) for name in (*uat.ARTIFACTS, 'evidence'): old = receipt[name]['sha256'] receipt[name]['sha256'] = 'wrong' with self.subTest(name=name), self.assertRaises(ValueError): uat.inputs(receipt) receipt[name]['sha256'] = old receipt['qualification'] = 'not yet qualified' with self.assertRaises(ValueError): uat.inputs(receipt) def test_signed_manifest_is_not_uat_receipt(self): with self.assertRaisesRegex(ValueError, 'not an unsigned UAT'): uat.inputs({'version': '2.0.1-alpha', 'signature': 'fixture'}) def test_dirty_source_refused_before_build(self): root = SCRIPTS.parent commit = 'a' * 40 # Deliberately synthetic identity; no Git commit is created. with patch.object(uat, 'git', side_effect=[commit, ' M source']), patch.object(uat, 'run') as runner: with self.assertRaisesRegex(ValueError, 'clean frozen source'): uat.source(root, commit, '2.0.1-alpha') runner.assert_not_called() def test_source_commit_mismatch(self): with self.assertRaisesRegex(ValueError, 'exact full source commit'): uat.source(SCRIPTS.parent, 'not-a-commit', '2.0.1-alpha') def kernel_fixture(self, version='6.12.113-1'): release = '6.12.113+deb13-amd64' files = {'/boot/vmlinuz-' + release: b'synthetic kernel', '/usr/lib/modules/' + release + '/kernel/test.ko': b'synthetic module'} receipt = dict(package='linux-image-' + release, version=version, source='linux', origin='Debian', archive_sha256=uat.payload.digest(b'synthetic deb'), files={n: uat.payload.digest(v) for n, v in files.items()}) return receipt, files def kernel_tar(self, receipt, files): path = self.root / 'rootfs.tar' contents = {**files, '/var/lib/archipelago/kernel-security/kernel.json': json.dumps(receipt).encode()} with tarfile.open(path, 'w') as archive: for name, raw in contents.items(): member = tarfile.TarInfo('./' + name.lstrip('/')) member.size = len(raw) archive.addfile(member, io.BytesIO(raw)) return path def test_rootfs_kernel_receipt_and_module_integrity(self): receipt, files = self.kernel_fixture() uat.kernel_tar(self.kernel_tar(receipt, files), security) module = next(n for n in files if n.endswith('.ko')) files[module] = b'stale module' with self.assertRaisesRegex(ValueError, 'hash mismatch'): uat.kernel_tar(self.kernel_tar(receipt, files), security) def test_old_unknown_and_missing_kernel_provenance(self): for version in ('6.12.112-1', 'vendor-unknown'): receipt, files = self.kernel_fixture(version) with self.subTest(version=version), self.assertRaisesRegex(ValueError, 'known fixed'): uat.kernel_tar(self.kernel_tar(receipt, files), security) receipt, files = self.kernel_fixture() receipt['files'] = {} with self.assertRaisesRegex(ValueError, 'incomplete'): uat.validate_kernel_receipt(receipt, security) def test_boot_log_requires_exact_kernel_build(self): receipt, _ = self.kernel_fixture() log = self.root / 'serial.log' log.write_text('Linux version 6.12.113+deb13-amd64 (fixture) #1 SMP Debian 6.12.113-1 (fixture)\n') uat.boot_kernel(log, receipt) for text in ('Welcome to Debian', 'Linux version 6.12.112+deb13-amd64 Debian 6.12.112-1 '): log.write_text(text) with self.assertRaises(ValueError): uat.boot_kernel(log, receipt) def test_gate_command_failure_propagates_with_retained_log(self): log = self.root / 'failure.log' with self.assertRaises(subprocess.CalledProcessError): uat.run([sys.executable, '-c', 'raise SystemExit(7)'], self.root, log) self.assertTrue(log.exists()) self.assertFalse((self.root / 'UNSIGNED-UAT.json').exists()) if __name__ == '__main__': unittest.main()