-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathtest_varstored_sb.py
More file actions
316 lines (265 loc) · 12.1 KB
/
Copy pathtest_varstored_sb.py
File metadata and controls
316 lines (265 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
import pytest
import logging
from lib.commands import SSHCommandFailed
from lib.efi import EFI_AT_ATTRS, EFI_VARIABLE_APPEND_WRITE, SB_CERTS, EFIAuth, image_security_database_guid
from lib.snapshot import Snapshot
from lib.vm import VM
from .utils import (
_test_key_exchanges,
boot_and_check_no_sb_errors,
boot_and_check_sb_failed,
boot_and_check_sb_succeeded,
generate_keys,
revert_vm_state,
sign_efi_bins,
)
from typing import Generator
# These tests check the behaviour of XAPI and varstored as they are in XCP-ng 8.3
# For XCP-ng 8.2, see test_uefistored_sb.py
# Requirements:
# On the test runner:
# - See requirements documented in the project's README.md for Guest UEFI Secure Boot tests
# From --hosts parameter:
# - host: XCP-ng host >= 8.3
# From --vm parameter
# - A UEFI VM to import
# Some tests are Linux-only and some tests are Windows-only.
# The Windows tests here (e.g. test_key_upgrade_bitlocker) require Windows Server.
pytestmark = pytest.mark.default_vm('mini-linux-x86_64-uefi')
@pytest.mark.small_vm
@pytest.mark.usefixtures("host_at_least_8_3")
@pytest.mark.usefixtures("skip_if_not_unix_vm")
class TestGuestLinuxUEFISecureBoot:
PK: EFIAuth
KEK: EFIAuth
db: EFIAuth
dbx: EFIAuth
@pytest.fixture(autouse=True)
def setup_and_cleanup(self, uefi_vm_and_snapshot: tuple[VM, Snapshot]) -> Generator[None, None, None]:
vm, snapshot = uefi_vm_and_snapshot
self.PK, self.KEK, self.db, self.dbx = generate_keys()
yield
revert_vm_state(vm, snapshot)
@pytest.mark.multi_vms # test that SB works on various UEFI unix/linux VMs, not just on `small_vm`
def test_boot_success_when_vm_db_set_and_images_signed(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.install_uefi_certs([self.PK, self.KEK, self.db])
sign_efi_bins(vm, self.db)
vm.param_set('platform', True, key='secureboot')
boot_and_check_sb_succeeded(vm)
def test_boot_fails_when_vm_db_set_and_images_unsigned(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.install_uefi_certs([self.PK, self.KEK, self.db])
vm.param_set('platform', True, key='secureboot')
boot_and_check_sb_failed(vm)
def test_boot_succeeds_when_vm_certs_set_and_sb_disabled(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.install_uefi_certs([self.PK, self.KEK, self.db])
vm.param_set('platform', False, key='secureboot')
boot_and_check_no_sb_errors(vm)
def test_boot_fails_when_vm_dbx_revokes_signed_images(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.install_uefi_certs([self.PK, self.KEK, self.db, self.dbx])
sign_efi_bins(vm, self.db)
vm.param_set('platform', True, key='secureboot')
boot_and_check_sb_failed(vm)
def test_boot_success_when_initial_vm_keys_not_signed_by_parent(self, uefi_vm: VM) -> None:
vm = uefi_vm
PK, KEK, db, _ = generate_keys(self_signed=True)
vm.install_uefi_certs([PK, KEK, db])
sign_efi_bins(vm, db)
vm.param_set('platform', True, key='secureboot')
boot_and_check_sb_succeeded(vm)
def test_sb_off_really_means_off(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.install_uefi_certs([self.PK, self.KEK, self.db])
sign_efi_bins(vm, self.db)
vm.param_set('platform', False, key='secureboot')
vm.start()
vm.wait_for_vm_running_and_ssh_up()
logging.info("Check that SB is NOT enabled according to the OS.")
assert not vm.booted_with_secureboot()
def test_append_with_default(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.host.pool.clear_custom_uefi_certs()
vm.set_uefi_user_mode()
vm.set_variable_from_file(
SB_CERTS.dbx_hashes_ms_amd64(),
image_security_database_guid,
"dbx",
EFI_AT_ATTRS | EFI_VARIABLE_APPEND_WRITE,
)
vm.start()
vm.wait_for_vm_running_and_ssh_up()
def test_append_with_poison(self, uefi_vm: VM) -> None:
"""
Context: https://xcp-ng.org/blog/2025/10/30/xcp-ng-8-3-varstored-update-unbootable-vm-risk-and-remediation/
In short, the dbx variable previously used in the bad update did not use the Microsoft owner GUID, preventing
deduplication of EFI signature data entries during an append call. Normally, this would not cause the VM to
crash; except varstored does not check for the variable data length on append, triggering the issue.
"""
vm = uefi_vm
vm.host.pool.clear_custom_uefi_certs()
vm.set_uefi_user_mode()
vm.set_variable_from_file(SB_CERTS.dbx_poison(), image_security_database_guid, "dbx", EFI_AT_ATTRS)
try:
vm.set_variable_from_file(
SB_CERTS.dbx_hashes_ms_amd64(),
image_security_database_guid,
"dbx",
EFI_AT_ATTRS | EFI_VARIABLE_APPEND_WRITE,
)
except SSHCommandFailed:
# Appending the MS dbx may succeed or fail, doesn't matter, as appending the poison may not necessarily take
# dbx over the DATA_LIMIT. The important thing is that the VM boots up following this append attempt.
pass
vm.start()
vm.wait_for_vm_running_and_ssh_up()
@pytest.mark.usefixtures("host_at_least_8_3")
@pytest.mark.usefixtures("skip_if_not_windows_vm")
class TestGuestWindowsUEFISecureBoot:
@pytest.fixture(autouse=True)
def setup_and_cleanup(self, uefi_vm_and_snapshot: tuple[VM, Snapshot]) -> Generator[None, None, None]:
vm, snapshot = uefi_vm_and_snapshot
yield
revert_vm_state(vm, snapshot)
@pytest.mark.small_vm # test on the smallest Windows VM, if that means anything with Windows
def test_windows_fails(self, uefi_vm: VM) -> None:
vm = uefi_vm
PK, KEK, db, _ = generate_keys(self_signed=True)
vm.install_uefi_certs([PK, KEK, db])
vm.param_set('platform', True, key='secureboot')
boot_and_check_sb_failed(vm)
@pytest.mark.multi_vms # test that SB works on every Windows VM we have
def test_windows_succeeds(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.param_set('platform', True, key='secureboot')
# Install certs in the VM. They must be official MS certs.
# We install them first in the pool with `secureboot-certs install`, which requires internet access
logging.info("Install MS certs on pool with secureboot-certs install")
vm.host.ssh('secureboot-certs install')
# Now install the default pool certs in the VM
vm.set_uefi_user_mode()
boot_and_check_sb_succeeded(vm)
@pytest.mark.small_vm
@pytest.mark.usefixtures("host_at_least_8_3")
class TestCertsMissingAndSbOn:
@pytest.fixture(autouse=True)
def setup_and_cleanup(self, uefi_vm_and_snapshot: tuple[VM, Snapshot]) -> Generator[None, None, None]:
vm, snapshot = uefi_vm_and_snapshot
vm.param_set('platform', True, key='secureboot')
yield
revert_vm_state(vm, snapshot)
def test_setup_mode_and_sb_on(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.set_uefi_setup_mode()
boot_and_check_no_sb_errors(vm)
def test_only_pk_present_but_sb_on(self, uefi_vm: VM) -> None:
vm = uefi_vm
PK, _, _, _ = generate_keys()
vm.install_uefi_certs([PK])
boot_and_check_sb_failed(vm)
def test_only_pk_and_kek_present_but_sb_on(self, uefi_vm: VM) -> None:
vm = uefi_vm
PK, KEK, _, _ = generate_keys()
vm.install_uefi_certs([PK, KEK])
boot_and_check_sb_failed(vm)
def test_only_pk_and_db_present_but_sb_on(self, uefi_vm: VM) -> None:
vm = uefi_vm
PK, _, db, _ = generate_keys()
vm.install_uefi_certs([PK, db])
boot_and_check_sb_failed(vm)
@pytest.mark.small_vm
@pytest.mark.usefixtures("host_at_least_8_3")
@pytest.mark.usefixtures("unix_vm")
class TestUEFIKeyExchange:
@pytest.fixture(autouse=True)
def setup_and_cleanup(self, uefi_vm_and_snapshot: tuple[VM, Snapshot]) -> Generator[None, None, None]:
vm, snapshot = uefi_vm_and_snapshot
yield
revert_vm_state(vm, snapshot)
def test_key_exchanges(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.set_uefi_setup_mode()
_test_key_exchanges(vm)
@pytest.mark.small_vm
@pytest.mark.usefixtures("host_at_least_8_3")
@pytest.mark.usefixtures("windows_vm")
class TestGuestWindowsUEFIKeyUpgrade:
@pytest.fixture(autouse=True)
def setup_and_cleanup(self, uefi_vm_and_snapshot: tuple[VM, Snapshot]) -> Generator[None, None, None]:
vm, snapshot = uefi_vm_and_snapshot
if not vm.get_vtpm_uuid():
vm.create_vtpm()
yield
revert_vm_state(vm, snapshot)
def install_old_certs(self, vm: VM) -> list[EFIAuth]:
"""Populate a key set that looks like the old defaults."""
PK = EFIAuth.self_signed("PK")
KEK = EFIAuth.self_signed("KEK", other_certs=[SB_CERTS.kek_ms_2011()])
db = EFIAuth("db", other_certs=[SB_CERTS.db_uefi_2011(), SB_CERTS.db_win_2011()])
# Some test VMs don't like an empty dbx when their own dbx is empty, so just put whatever in there
dbx = EFIAuth.self_signed("dbx")
PK.sign_auth(PK)
PK.sign_auth(KEK)
KEK.sign_auth(db)
KEK.sign_auth(dbx)
vm.install_uefi_certs([PK, KEK, db, dbx])
return [PK, KEK, db, dbx]
def install_new_certs(self, vm: VM, signer: EFIAuth) -> None:
"""Populate a key set that looks like the new defaults with 2023 MS keys."""
newPK = EFIAuth.self_signed("PK")
newKEK = EFIAuth("KEK", other_certs=[SB_CERTS.kek_ms_2011(), SB_CERTS.kek_ms_2023()])
newdb = EFIAuth(
"db",
other_certs=[
SB_CERTS.db_win_2011(),
SB_CERTS.db_win_2023(),
SB_CERTS.db_uefi_2011(),
SB_CERTS.db_uefi_2023(),
SB_CERTS.db_oprom_2023(),
],
)
newdbx = EFIAuth("dbx")
newPK.sign_auth(newPK)
# Technically, there's no need to sign the other databases since we're setting them from Dom0.
# If signing with the old PK works, there'd be no need to test signing with the new PK.
# We use an invalid signer to test scenarios where the user mixes and matches default and custom keys.
signer.sign_auth(newKEK)
signer.sign_auth(newdb)
signer.sign_auth(newdbx)
vm.install_uefi_certs([newPK, newKEK, newdb, newdbx])
def test_key_upgrade(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.param_set("platform", True, key="secureboot")
PK, _, _, _ = self.install_old_certs(vm)
boot_and_check_sb_succeeded(vm)
vm.shutdown(verify=True)
self.install_new_certs(vm, PK)
boot_and_check_sb_succeeded(vm)
def test_key_upgrade_bitlocker(self, uefi_vm: VM) -> None:
vm = uefi_vm
vm.param_set("platform", True, key="secureboot")
PK, _, _, _ = self.install_old_certs(vm)
boot_and_check_sb_succeeded(vm)
vm.execute_powershell_script("Add-WindowsFeature BitLocker,EnhancedStorage")
vm.reboot(verify=True)
vm.execute_powershell_script("Enable-BitLocker $Env:SystemDrive -TpmProtector -UsedSpaceOnly")
# Confirm if PCR7 is bound.
assert (
vm.execute_powershell_script(
r"""Get-CimInstance -Namespace Root\CIMV2\Security\MicrosoftVolumeEncryption `
-Query "select * from Win32_EncryptableVolume where VolumeType=0" |
Invoke-CimMethod -MethodName GetSecureBootBindingState |
Where-Object ReturnValue -eq 0 |
Select-Object -ExpandProperty BindingState"""
)
== "3"
) # Bound
vm.execute_powershell_script("Suspend-BitLocker $Env:SystemDrive")
vm.shutdown(verify=True)
self.install_new_certs(vm, PK)
boot_and_check_sb_succeeded(vm)
# After Enable-BitLocker, Windows would boot into encryption test.
# If the test failed, Windows would cancel the encryption and give the status FullyDecrypted.
assert vm.execute_powershell_script("(Get-BitLockerVolume $Env:SystemDrive).VolumeStatus") != "FullyDecrypted"