138 lines
8.0 KiB
Python
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()
|