Files
archy/scripts/tests/test_kernel_security.py

138 lines
8.0 KiB
Python

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()