# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2015-2016, 2026 InterGenJLU
"""Empirical test: GRUB `check_signatures=enforce` against PE/COFF sbsigned chain.

Load-bearing question:
  Does GRUB `check_signatures=enforce` refuse to load our PE/COFF
  sbsigned kernel? `enforce` targets GRUB's PGP verifier, which expects
  detached `.sig` companion files — NOT the CMS blocks that sbsign
  embeds inside PE/COFF kernel images.

If the hypothesis holds: setting `check_signatures=enforce` on a working
InterGenOS install would refuse boot, because the kernel that shim+GRUB
already SB-verified via the PE chain has no PGP `.sig` sitting next to it.
Result: we do NOT ship that flag.

If refuted: `enforce` would have to be actively aware of PE-embedded
signatures (unexpected given upstream GRUB source), and we could consider
it as defense-in-depth.

This file answers the question in two tiers:

  TestGrubBuildSanity — host-level, runs anywhere grub-mkimage exists.
    Probes host GRUB module library for the infrastructure `enforce` needs
    (pgp + verifiers modules), and validates that a grub.cfg containing
    `set check_signatures=enforce` parses cleanly via grub-script-check.

  TestGrubCheckSignaturesVM — integration, skip-gated.
    Exercises secboot_vm_profile.provision() and (eventually) runs the
    real empirical test against an installed InterGenOS target. Fully
    gated on (a) libvirtd active, (b) OVMF/swtpm prerequisites, (c) an
    existing disk image at TEST_DISK_IMAGE (env var) or the conventional
    /var/lib/libvirt/images/intergenos-target.qcow2. The hypothesis tests
    themselves are SKELETONS until post-install plumbing lands; skip
    messages enumerate what's still missing.

Run:
    python3 -m unittest installer.tests.test_grub_check_signatures -v
"""

from __future__ import annotations

import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path

from installer.tests import secboot_vm_profile as vm


# --- Host-level GRUB module library paths ------------------------------------

GRUB_MOD_DIR_CANDIDATES = [
    Path("/usr/lib/grub/x86_64-efi"),
    Path("/usr/lib/grub/x86_64-efi-signed"),
]

TEST_DISK_IMAGE = Path(os.environ.get(
    "TEST_DISK_IMAGE",
    "/var/lib/libvirt/images/intergenos-target.qcow2",
))


def _grub_mod_dir() -> Path | None:
    for p in GRUB_MOD_DIR_CANDIDATES:
        if p.is_dir():
            return p
    return None


def _host_has_grub_tools() -> bool:
    return bool(shutil.which("grub-mkimage") and shutil.which("grub-script-check"))


# --- TestGrubBuildSanity ----------------------------------------------------


@unittest.skipUnless(_host_has_grub_tools(),
                     "grub-mkimage / grub-script-check not in PATH")
class TestGrubBuildSanity(unittest.TestCase):
    """Host-level checks: does the host's GRUB install carry what `enforce` needs?

    These run on any dev box with grub2-tools. They don't prove anything
    about an installed InterGenOS target — they prove our assumptions about
    what modules the enforce path requires.
    """

    def test_grub_mkimage_available(self):
        """Sanity: grub-mkimage responds to --help."""
        proc = subprocess.run(
            ["grub-mkimage", "--help"],
            capture_output=True, text=True, timeout=5,
        )
        self.assertEqual(proc.returncode, 0, proc.stderr)
        self.assertIn("--format", proc.stdout)

    def test_grub_script_check_accepts_check_signatures_enforce(self):
        """A grub.cfg with `set check_signatures=enforce` parses without syntax error.

        This validates the grammar — it does NOT prove the flag has
        semantic effect. That's what the VM integration test is for.
        """
        cfg = (
            "# generated by test\n"
            "set check_signatures=enforce\n"
            "insmod part_gpt\n"
            "insmod ext2\n"
            "menuentry 'InterGenOS' {\n"
            "    linux /boot/vmlinuz root=/dev/vda2\n"
            "    initrd /boot/initrd.img\n"
            "}\n"
        )
        with tempfile.NamedTemporaryFile(mode="w", suffix=".cfg",
                                         delete=False) as f:
            f.write(cfg)
            cfg_path = f.name
        try:
            proc = subprocess.run(
                ["grub-script-check", cfg_path],
                capture_output=True, text=True, timeout=5,
            )
        finally:
            Path(cfg_path).unlink(missing_ok=True)
        self.assertEqual(proc.returncode, 0,
                         f"grub-script-check rejected cfg: {proc.stderr}")

    def test_pgp_module_present_in_grub_mod_dir(self):
        """The pgp verifier module must exist — `enforce` calls into it.

        Upstream GRUB's check_signatures=enforce dispatches verification
        through the verifiers framework; the pgp verifier registers
        itself to handle "does this file have a .sig next to it?" If
        pgp.mod is absent from the host's build, enforce would error at
        runtime with "no signature policy registered" rather than
        refusing unsigned files cleanly.
        """
        mod_dir = _grub_mod_dir()
        if mod_dir is None:
            self.skipTest("no grub module library dir on host "
                          f"(checked: {GRUB_MOD_DIR_CANDIDATES})")
        pgp_mod = mod_dir / "pgp.mod"
        self.assertTrue(pgp_mod.exists(),
                        f"pgp.mod missing from {mod_dir} — enforce would "
                        "have no verifier backend")

    def test_signature_test_module_present_in_grub_mod_dir(self):
        """signature_test.mod present — supporting utility for the sig path.

        Note on `verifiers` framework: since GRUB 2.04 the verifiers API is
        compiled INTO the core image (no standalone verifiers.mod exists on
        modern builds). pgp.mod registers with it at module load time. So
        presence of pgp.mod + signature_test.mod on the host is the
        meaningful signal — absence of a verifiers.mod file is expected.
        """
        mod_dir = _grub_mod_dir()
        if mod_dir is None:
            self.skipTest("no grub module library dir on host")
        sig_test = mod_dir / "signature_test.mod"
        self.assertTrue(sig_test.exists(),
                        f"signature_test.mod missing from {mod_dir}")


# --- TestGrubCheckSignaturesVM ----------------------------------------------


def _vm_prereqs() -> tuple[bool, str]:
    """Return (all-prereqs-present, reason-if-not) for VM-tier tests."""
    prereqs = vm.check_prerequisites()
    if not prereqs.ok:
        return False, prereqs.reason()
    return True, ""


_vm_ok, _vm_skip_reason = _vm_prereqs()


@unittest.skipUnless(_vm_ok, f"VM prerequisites missing: {_vm_skip_reason}")
class TestGrubCheckSignaturesVM(unittest.TestCase):
    """Integration tier — exercises the SecBoot VM profile.

    Plumbing test (test_provision_and_destroy) runs as soon as libvirtd
    + OVMF + swtpm are available. The actual hypothesis tests are
    SKELETONS awaiting post-install plumbing (disk image with installed
    InterGenOS + grub.cfg write path + serial console capture + log
    parsing for "file has no signature"). They skip explicitly until the
    disk image exists — this is a Phase A-2 deliverable, not Phase A-1.
    """

    VM_NAME = "intergenos-secboot-test"

    def test_provision_and_destroy(self):
        """Plumbing: VM can be provisioned + destroyed cleanly (scratch disk).

        Does not prove the hypothesis — proves we can stand up and tear
        down a SB-enabled VM repeatably. Uses the default scratch 2GB
        disk (no install media) so it's independent of any disk image.
        """
        try:
            profile = vm.provision(name=self.VM_NAME)
            self.assertEqual(profile.name, self.VM_NAME)
            self.assertIsNotNone(profile.domain_uuid,
                                 "virsh should return a UUID post-define")
        finally:
            # Clean up even on failure — leaving test VMs lying around
            # pollutes libvirt state.
            rc = vm.destroy(self.VM_NAME)
            self.assertEqual(rc, 0, f"destroy returned {rc}")

    @unittest.skipUnless(TEST_DISK_IMAGE.exists(),
                         f"disk image {TEST_DISK_IMAGE} not found "
                         "(requires post-install InterGenOS target — "
                         "Phase A-2 plumbing not yet written)")
    def test_enforce_refuses_pecoff_kernel(self):
        """HYPOTHESIS: check_signatures=enforce refuses sbsigned PE kernel.

        SKELETON — implementation awaits:
          1. disk-image-aware grub.cfg write path (mount ESP, replace cfg)
          2. serial console capture during VM boot (virsh console or
             qemu -serial stdio piped)
          3. parser for grub error output ("file .* has no signature"
             or similar) vs successful kernel handoff
        """
        self.skipTest("Phase A-2 skeleton — see docstring for missing plumbing")

    @unittest.skipUnless(TEST_DISK_IMAGE.exists(),
                         f"disk image {TEST_DISK_IMAGE} not found "
                         "(Phase A-2)")
    def test_noenforce_boots_pecoff_kernel(self):
        """NULL: without enforce, the PE/COFF sbsigned kernel boots cleanly.

        SKELETON — same plumbing gap as enforce test above. Serves as
        the positive control: any failure here would suggest our PE/COFF
        chain itself is broken, independent of the check_signatures flag.
        """
        self.skipTest("Phase A-2 skeleton — see enforce test docstring")


if __name__ == "__main__":
    unittest.main()
