136 lines
6.8 KiB
Python
136 lines
6.8 KiB
Python
"""Offline boot-chain layout/refusal tests; never build or sign an image."""
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
spec = importlib.util.spec_from_file_location('secure_boot', Path(__file__).parents[1] / 'secure_boot.py')
|
|
boot = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(boot)
|
|
|
|
|
|
class SecureBootTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name) / 'root'
|
|
self.esp = Path(self.temp.name) / 'esp'
|
|
self.root.mkdir()
|
|
self.esp.mkdir()
|
|
|
|
def fixture(self, arch='amd64'):
|
|
receipt = {'schema': 1, 'architecture': arch, 'origin': 'Debian', 'packages': {}}
|
|
for package, files in boot.required_files(arch).items():
|
|
claim = {'version': 'fixture-only', 'archive_sha256': 'a' * 64, 'files': {}}
|
|
for name in files:
|
|
path = self.root / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(('distinct-fixture:' + name).encode())
|
|
claim['files'][name] = boot.digest(path)
|
|
receipt['packages'][package] = claim
|
|
target = self.root / boot.RECEIPT
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(json.dumps(receipt))
|
|
return receipt
|
|
|
|
@patch.object(boot, 'signed_payload')
|
|
def test_both_architectures_use_shim_then_distinct_grub_and_preserve_other_vendor(self, signature):
|
|
for arch, suffix in [('amd64', 'x64'), ('arm64', 'aa64')]:
|
|
self.fixture(arch)
|
|
vendor = self.esp / 'EFI' / 'other-vendor'
|
|
vendor.mkdir(parents=True, exist_ok=True)
|
|
(vendor / 'boot.efi').write_bytes(b'preserve')
|
|
boot.stage(self.root, self.esp, arch, uuid='12345678-1234-1234-1234-123456789abc')
|
|
fallback = self.esp / 'EFI/BOOT'
|
|
self.assertEqual((fallback / f'BOOT{suffix.upper()}.EFI').read_bytes(),
|
|
(self.root / f'usr/lib/shim/shim{suffix}.efi.signed').read_bytes())
|
|
self.assertNotEqual((fallback / f'BOOT{suffix.upper()}.EFI').read_bytes(),
|
|
(fallback / f'grub{suffix}.efi').read_bytes())
|
|
self.assertTrue((fallback / f'mm{suffix}.efi').is_file())
|
|
self.assertEqual((fallback / 'grub.cfg').read_text(),
|
|
(self.esp / 'EFI/debian/grub.cfg').read_text())
|
|
self.assertIn('search --no-floppy --fs-uuid --set=root 12345678-', (fallback / 'grub.cfg').read_text())
|
|
self.assertEqual((vendor / 'boot.efi').read_bytes(), b'preserve')
|
|
self.assertEqual(signature.call_count, 6)
|
|
|
|
@patch.object(boot, 'signed_payload')
|
|
def test_installer_label_bootstraps_full_iso_menu(self, signature):
|
|
self.fixture()
|
|
boot.stage(self.root, self.esp, 'amd64', label='ARCHIPELAGO')
|
|
config = (self.esp / 'EFI/BOOT/grub.cfg').read_text()
|
|
self.assertIn('--label --set=root ARCHIPELAGO', config)
|
|
self.assertIn('configfile $prefix/grub.cfg', config)
|
|
|
|
@patch.object(boot, 'signed_payload')
|
|
def test_tampered_stripped_missing_payload_refuses_before_esp_changes(self, signature):
|
|
for mutation in ('tamper', 'strip', 'missing'):
|
|
self.fixture()
|
|
path = self.root / 'usr/lib/shim/shimx64.efi.signed'
|
|
if mutation == 'missing':
|
|
path.unlink()
|
|
elif mutation == 'strip':
|
|
path.write_bytes(path.read_bytes()[:3])
|
|
else:
|
|
path.write_bytes(b'other signed or unsigned binary')
|
|
with self.assertRaises((ValueError, FileNotFoundError)):
|
|
boot.stage(self.root, self.esp, 'amd64', label='ARCHIPELAGO')
|
|
self.assertEqual(list(self.esp.iterdir()), [])
|
|
|
|
def test_missing_provenance_and_wrong_architecture_refuse(self):
|
|
with self.assertRaises(FileNotFoundError):
|
|
boot.stage(self.root, self.esp, 'amd64', label='ARCHIPELAGO')
|
|
self.fixture()
|
|
with self.assertRaisesRegex(ValueError, 'provenance'):
|
|
boot.stage(self.root, self.esp, 'arm64', label='ARCHIPELAGO')
|
|
|
|
@patch.object(boot, 'signed_payload')
|
|
def test_unsafe_configuration_and_symlink_escape_refuse(self, signature):
|
|
self.fixture()
|
|
with self.assertRaises(ValueError):
|
|
boot.stage(self.root, self.esp, 'amd64', label='ARCHIPELAGO\nreboot')
|
|
(self.esp / 'EFI').symlink_to(self.root, target_is_directory=True)
|
|
with self.assertRaisesRegex(ValueError, 'escapes'):
|
|
boot.stage(self.root, self.esp, 'amd64', label='ARCHIPELAGO')
|
|
|
|
def test_verifier_unavailable_unsigned_and_failure_refuse(self):
|
|
for result in (FileNotFoundError('sbverify'), subprocess.CalledProcessError(1, 'sbverify'),
|
|
SimpleNamespace(stdout='No signature table present\n')):
|
|
with patch.object(boot.subprocess, 'run', side_effect=result if isinstance(result, Exception) else None,
|
|
return_value=result):
|
|
with self.assertRaises((FileNotFoundError, subprocess.CalledProcessError, ValueError)):
|
|
boot.signed_payload(Path('fixture'))
|
|
|
|
def test_debian_origin_and_sha256_are_required(self):
|
|
good = SimpleNamespace(archive='stable', trusted=True, origin='Debian')
|
|
version = SimpleNamespace(origins=[good], record={'SHA256': 'b' * 64})
|
|
self.assertEqual(boot.authenticated_version(version), 'b' * 64)
|
|
for origin in (SimpleNamespace(archive='now', trusted=True, origin='Debian'),
|
|
SimpleNamespace(archive='stable', trusted=False, origin='Debian'),
|
|
SimpleNamespace(archive='stable', trusted=True, origin='Other')):
|
|
version.origins = [origin]
|
|
with self.assertRaises(ValueError):
|
|
boot.authenticated_version(version)
|
|
version.origins = [good]
|
|
version.record = {'SHA256': ''}
|
|
with self.assertRaises(ValueError):
|
|
boot.authenticated_version(version)
|
|
|
|
def test_reachable_builder_preserves_signed_chain_in_media_and_target(self):
|
|
root = Path(__file__).resolve().parents[2]
|
|
source = (root / 'image-recipe/_archived/build-auto-installer-iso.sh').read_text()
|
|
self.assertNotIn('dpkg --purge shim', source)
|
|
self.assertNotIn('grub-mkstandalone -O', source)
|
|
self.assertNotIn('for shim_file in', source)
|
|
self.assertIn('secure_boot.py record', source)
|
|
self.assertIn('--esp /output/efi-tree', source)
|
|
self.assertIn('--esp /boot/efi --uuid', source)
|
|
self.assertIn('mcopy -s -i /output/efi.img /output/efi-tree/EFI ::/', source)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|