xref: /aosp_15_r20/external/vboot_reference/scripts/image_signing/sign_uefi_unittest.py (revision 8617a60d3594060b7ecbd21bc622a7c14f3cf2bc)
1#!/usr/bin/env python3
2# Copyright 2022 The ChromiumOS Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Tests for sign_uefi.py.
7
8This is run as part of `make runtests`, or `make runtestscripts` if you
9want something a little faster.
10"""
11
12from pathlib import Path
13import tempfile
14import unittest
15from unittest import mock
16
17import sign_uefi
18
19
20class TestSign(unittest.TestCase):
21    """Test signing functions in sign_uefi.py."""
22
23    def setUp(self):
24        # pylint: disable=consider-using-with
25        self.tempdir = tempfile.TemporaryDirectory()
26        tempdir = Path(self.tempdir.name)
27
28        # Get key paths.
29        self.keys = sign_uefi.Keys(
30            private_key=tempdir / "private_key.rsa",
31            sign_cert=tempdir / "sign_cert.pem",
32            verify_cert=tempdir / "verify_cert.pem",
33            kernel_subkey_vbpubk=tempdir / "kernel_subkey.vbpubk",
34            crdyshim_private_key=tempdir / "crdyshim.priv.pem",
35        )
36
37        # Get target paths.
38        self.target_dir = tempdir / "boot"
39        self.syslinux_dir = self.target_dir / "syslinux"
40        self.efi_boot_dir = self.target_dir / "efi/boot"
41
42        # Make test dirs.
43        self.syslinux_dir.mkdir(parents=True)
44        self.efi_boot_dir.mkdir(parents=True)
45
46        # Make key files.
47        (self.keys.private_key).touch()
48        (self.keys.sign_cert).touch()
49        (self.keys.verify_cert).touch()
50        (self.keys.kernel_subkey_vbpubk).touch()
51        (self.keys.crdyshim_private_key).touch()
52
53        # Make EFI files.
54        (self.efi_boot_dir / "bootia32.efi").touch()
55        (self.efi_boot_dir / "bootx64.efi").touch()
56        (self.efi_boot_dir / "testia32.efi").touch()
57        (self.efi_boot_dir / "testx64.efi").touch()
58        (self.efi_boot_dir / "crdybootia32.efi").touch()
59        (self.efi_boot_dir / "crdybootx64.efi").touch()
60        (self.syslinux_dir / "vmlinuz.A").touch()
61        (self.syslinux_dir / "vmlinuz.B").touch()
62        (self.target_dir / "vmlinuz-5.10.156").touch()
63        (self.target_dir / "vmlinuz").symlink_to(
64            self.target_dir / "vmlinuz-5.10.156"
65        )
66
67    def tearDown(self):
68        self.tempdir.cleanup()
69
70    @mock.patch("sign_uefi.inject_vbpubk")
71    @mock.patch.object(sign_uefi.Signer, "create_detached_signature")
72    @mock.patch.object(sign_uefi.Signer, "sign_efi_file")
73    def test_sign_target_dir(
74        self, mock_sign, mock_detached_sig, mock_inject_vbpubk
75    ):
76        # Set an EFI glob that matches only some of the EFI files.
77        efi_glob = "test*.efi"
78
79        # Sign, but with the actual signing mocked out.
80        sign_uefi.sign_target_dir(self.target_dir, self.keys, efi_glob)
81
82        # Check that the correct list of files got signed.
83        self.assertEqual(
84            mock_sign.call_args_list,
85            [
86                # The test*.efi files match the glob,
87                # the boot*.efi files don't.
88                mock.call(self.efi_boot_dir / "testia32.efi"),
89                mock.call(self.efi_boot_dir / "testx64.efi"),
90                # Two syslinux kernels.
91                mock.call(self.syslinux_dir / "vmlinuz.A"),
92                mock.call(self.syslinux_dir / "vmlinuz.B"),
93                # One kernel in the target dir.
94                mock.call(self.target_dir / "vmlinuz-5.10.156"),
95            ],
96        )
97
98        # Check that `inject_vbpubk` was called on both the crdyboot
99        # executables.
100        self.assertEqual(
101            mock_inject_vbpubk.call_args_list,
102            [
103                mock.call(self.efi_boot_dir / "crdybootia32.efi", self.keys),
104                mock.call(self.efi_boot_dir / "crdybootx64.efi", self.keys),
105            ],
106        )
107
108        # Check that `create_detached_signature` was called on both
109        # the crdyboot executables.
110        self.assertEqual(
111            mock_detached_sig.call_args_list,
112            [
113                mock.call(self.efi_boot_dir / "crdybootia32.efi"),
114                mock.call(self.efi_boot_dir / "crdybootx64.efi"),
115            ],
116        )
117
118    @mock.patch("sign_uefi.inject_vbpubk")
119    @mock.patch.object(sign_uefi.Signer, "create_detached_signature")
120    @mock.patch.object(sign_uefi.Signer, "sign_efi_file")
121    def test_no_crdyshim_key(
122        self, _mock_sign, _mock_detached_sig, _mock_inject_vbpubk
123    ):
124        """Test for older keysets that don't have the crdyshim key."""
125        self.keys.crdyshim_private_key.unlink()
126
127        # Error: crdyboot files are supposed to be signed, but the
128        # crdyshim key isn't present.
129        with self.assertRaises(SystemExit):
130            sign_uefi.sign_target_dir(
131                self.target_dir, self.keys, "crdyboot*.efi"
132            )
133
134        # Success: the crdyboot files aren't present, so the crdyshim
135        # key is not required.
136        (self.efi_boot_dir / "crdybootia32.efi").unlink()
137        (self.efi_boot_dir / "crdybootx64.efi").unlink()
138        sign_uefi.sign_target_dir(self.target_dir, self.keys, "crdyboot*.efi")
139
140    @mock.patch.object(sign_uefi.Signer, "sign_efi_file")
141    def test_sign_target_file(self, mock_sign):
142        # Test signing a specific file.
143        sign_uefi.sign_target_file(
144            self.efi_boot_dir / "bootia32.efi", self.keys
145        )
146
147        # Check that we made the expected signer call.
148        self.assertIn(
149            [
150                mock.call(self.efi_boot_dir / "bootia32.efi"),
151            ],
152            mock_sign.call_args_list,
153        )
154
155    @mock.patch("sign_uefi.subprocess.run")
156    def test_inject_vbpubk(self, mock_run):
157        efi_file = self.efi_boot_dir / "crdybootx64.efi"
158        sign_uefi.inject_vbpubk(efi_file, self.keys)
159
160        # Check that the expected command runs.
161        self.assertEqual(
162            mock_run.call_args_list,
163            [
164                mock.call(
165                    [
166                        "sudo",
167                        "objcopy",
168                        "--update-section",
169                        f".vbpubk={self.keys.kernel_subkey_vbpubk}",
170                        efi_file,
171                    ],
172                    check=True,
173                )
174            ],
175        )
176
177    @mock.patch("sign_uefi.subprocess.run")
178    def test_create_detached_signature(self, mock_run):
179        with tempfile.TemporaryDirectory() as tempdir:
180            tempdir = Path(tempdir)
181            signer = sign_uefi.Signer(tempdir, self.keys)
182
183            efi_file = self.efi_boot_dir / "crdybootx64.efi"
184            signer.create_detached_signature(efi_file)
185
186            # Check that the expected commands run.
187            self.assertEqual(
188                mock_run.call_args_list,
189                [
190                    mock.call(
191                        [
192                            "openssl",
193                            "pkeyutl",
194                            "-sign",
195                            "-rawin",
196                            "-in",
197                            efi_file,
198                            "-inkey",
199                            self.keys.crdyshim_private_key,
200                            "-out",
201                            tempdir / "crdybootx64.sig",
202                        ],
203                        check=True,
204                    ),
205                    mock.call(
206                        [
207                            "sudo",
208                            "cp",
209                            tempdir / "crdybootx64.sig",
210                            self.efi_boot_dir / "crdybootx64.sig",
211                        ],
212                        check=True,
213                    ),
214                ],
215            )
216
217
218class TestUtils(unittest.TestCase):
219    """Test utility functions in sign_uefi.py."""
220
221    def test_is_pkcs11_key_path(self):
222        self.assertFalse(sign_uefi.is_pkcs11_key_path(Path("private_key.rsa")))
223
224        self.assertTrue(
225            sign_uefi.is_pkcs11_key_path("pkcs11:object=private_key")
226        )
227
228
229if __name__ == "__main__":
230    unittest.main()
231