import importlib.util from pathlib import Path import unittest from types import SimpleNamespace from unittest.mock import patch import tempfile import hashlib import json spec = importlib.util.spec_from_file_location('kernel_security', Path(__file__).parents[1] / 'kernel-security.py') security = importlib.util.module_from_spec(spec) spec.loader.exec_module(security) class KernelSecurity(unittest.TestCase): def test_each_supported_stable_boundary(self): for (major, minor), patch in security.FIXED.items(): self.assertFalse(security.patched_version(f'{major}.{minor}.{patch-1}-1')) self.assertTrue(security.patched_version(f'{major}.{minor}.{patch}-1')) self.assertTrue(security.patched_version(f'{major}.{minor}.{patch}-1+deb13u1')) def test_unknown_and_release_candidates_are_not_claimed_fixed(self): for version in ('6.12', 'custom', '6.19.1', '7.3.0-rc1', '7.3.0~rc7', '8.0.1', '6.12.113-', '6.12.113-custom', '6.12.113-0', '6.12.113-1\n'): self.assertIsNone(security.patched_version(version), version) self.assertTrue(security.patched_version('7.3.0-1')) def test_vendor_kernel_does_not_inherit_debian_acceptance(self): result = security.assess({'package': 'linux-image-6.12.113-rpi', 'version': '6.12.113-1', 'source': 'raspberrypi-kernel'}) self.assertEqual(result['status'], 'unknown-vendor-kernel') result = security.assess({'package': 'linux-image-6.12.113+rpt-rpi-v8', 'version': '6.12.113-1', 'source': 'linux'}) self.assertEqual(result['status'], 'unknown-vendor-kernel') def test_installed_fixed_kernel_is_not_booted_kernel_acceptance(self): row = {'package': 'linux-image-6.12.113+deb13-amd64', 'version': '6.12.113-1', 'source': 'linux-signed-amd64'} self.assertEqual(security.assess(row)['scope'], 'installed') self.assertEqual(security.assess(row, '6.12.74+deb13+1-amd64')['status'], 'running-package-mismatch') self.assertEqual(security.assess(row, '6.12.113+deb13-amd64')['status'], 'fixed-package') def test_only_installed_concrete_images_count(self): rows = security.package_rows('linux-image-amd64\tinstalled\t6.12.113-1\tlinux-signed-amd64\n' 'linux-image-6.12.113+deb13-amd64\tinstalled\t6.12.113-1\tlinux-signed-amd64\n' 'linux-image-6.12.113-unsigned\tnot-installed\t6.12.113-1\tlinux\n') self.assertEqual(len(rows), 1) def test_iso_documentation_stripping_does_not_hide_kernel_changes(self): result = SimpleNamespace(returncode=0, stderr='', stdout='') self.assertTrue(security.verification_clean(result)) result.stdout = 'missing /usr/share/doc/linux-image-example/changelog.gz\n' self.assertTrue(security.verification_clean(result)) for line in ('missing /boot/vmlinuz-example', '??5?????? /lib/modules/example/kernel/net/tcp.ko', '??5?????? /usr/share/doc/linux-image-example/changelog.gz'): result.stdout = line self.assertFalse(security.verification_clean(result), line) def test_authenticated_receipt_checks_actual_payload_and_stripping(self): row = {'package': 'linux-image-6.12.113-test', 'version': '6.12.113-1', 'source': 'linux'} content = b'actual fixture payload' sha = hashlib.sha256(content).hexdigest() receipt = dict(row, origin='Debian', archive_sha256='a' * 64, files={ '/boot/vmlinuz-6.12.113-test': sha, '/lib/modules/6.12.113-test/kernel/tcp.ko': sha, '/usr/share/doc/kernel/changelog.gz': sha}) with tempfile.TemporaryDirectory() as temp: root = Path(temp) for name in receipt['files']: target = root / name.lstrip('/') target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(content) security.verify_receipt(row, receipt, root) doc = root / 'usr/share/doc/kernel/changelog.gz' doc.unlink() security.verify_receipt(row, receipt, root) doc.write_bytes(b'modified documentation is not stripping') with self.assertRaisesRegex(ValueError, 'digest mismatch'): security.verify_receipt(row, receipt, root) doc.unlink() kernel = root / 'boot/vmlinuz-6.12.113-test' kernel.write_bytes(b'tampered kernel') with self.assertRaisesRegex(ValueError, 'digest mismatch'): security.verify_receipt(row, receipt, root) kernel.unlink() with self.assertRaises(FileNotFoundError): security.verify_receipt(row, receipt, root) receipt['files'] = {} with self.assertRaisesRegex(ValueError, 'lacks kernel/modules'): security.verify_receipt(row, receipt) def test_audit_requires_provenance_and_distinguishes_boot(self): release = '6.12.113+deb13-amd64' query = SimpleNamespace(returncode=0, stdout=f'linux-image-{release}\tinstalled\t6.12.113-1\tlinux-signed-amd64\n') verified = SimpleNamespace(returncode=0, stdout='', stderr='') receipt = {'archive_sha256': 'a' * 64} with tempfile.TemporaryDirectory() as temp, patch.object(security.subprocess, 'run', side_effect=lambda args, **kw: query if args[0] == 'dpkg-query' else verified), patch.object(security.platform, 'release', return_value=release), patch.object(security.platform, 'version', return_value='#1 SMP Debian 6.12.74-2 (date)'): directory = Path(temp) self.assertFalse(security.audit(installed=True, directory=directory)['fixed']) (directory / f'linux-image-{release}.json').write_text(json.dumps(receipt)) with patch.object(security, 'verify_receipt'): result = security.audit(installed=True, directory=directory) self.assertTrue(result['fixed']) self.assertFalse(result['reboot_acceptance']) result = security.audit(image=release, directory=directory) self.assertTrue(result['fixed']) self.assertEqual(result['kernels'][0]['scope'], 'image') self.assertFalse(result['reboot_acceptance']) self.assertFalse(security.audit(directory=directory)['fixed']) with patch.object(security.platform, 'version', return_value='#1 SMP Debian 6.12.113-1 (date)'): self.assertTrue(security.audit(directory=directory)['reboot_acceptance']) query.returncode = 1 self.assertFalse(security.audit(installed=True, directory=directory)['fixed']) def test_untrusted_or_missing_archive_metadata_cannot_record(self): import sys row = {'package': 'linux-image-6.12.113-test', 'version': '6.12.113-1', 'source': 'linux'} origin = SimpleNamespace(trusted=False, origin='Debian', archive='stable-security') version = SimpleNamespace(origins=[origin], record={}, source_name='linux') cache = {row['package']: SimpleNamespace(versions={row['version']: version})} with patch.dict(sys.modules, apt=SimpleNamespace(Cache=lambda: cache)): with self.assertRaisesRegex(ValueError, 'authenticated Debian'): security.record_provenance(row) origin.trusted = True with self.assertRaisesRegex(ValueError, 'SHA256'): security.record_provenance(row) origin.archive = 'now' with self.assertRaisesRegex(ValueError, 'authenticated Debian'): security.record_provenance(row) def test_malformed_receipts_fail_closed(self): row = {'package': 'linux-image-6.12.113-test', 'version': '6.12.113-1', 'source': 'linux'} for receipt in (None, [], {}, dict(row, origin='Debian', archive_sha256=123), dict(row, origin='Debian', archive_sha256='a' * 64, files=[])): with self.assertRaises((ValueError, KeyError)): security.verify_receipt(row, receipt) if __name__ == '__main__': unittest.main()