#
-# Copyright 2019-2021 Canonical Ltd.
+# Copyright 2019-2022 Canonical Ltd.
# Authors:
# - dann frazier <dann.frazier@canonical.com>
#
class GrubShellBootableIsoImage(EfiBootableIsoImage):
- def __init__(self, efi_arch, use_signed):
- EfiArchToGrubArch = {
- 'X64': "x86_64",
- 'AA64': "arm64",
- }
+ def __init__(self, efi_arch, shim_path, grub_path):
efi_img = FatFsImage(64)
efi_img.makedirs(os.path.join('EFI', 'BOOT'))
removable_media_path = os.path.join(
'EFI', 'BOOT', 'BOOT%s.EFI' % (efi_arch.upper())
)
- efi_ext = 'efi'
- grub_subdir = "%s-efi" % EfiArchToGrubArch[efi_arch.upper()]
- if use_signed:
- efi_ext = "%s.signed" % (efi_ext)
- grub_subdir = "%s-signed" % (grub_subdir)
-
- shim_src = os.path.join(
- os.path.sep, 'usr', 'lib', 'shim',
- 'shim%s.%s' % (efi_arch.lower(), efi_ext)
- )
- grub_src = os.path.join(
- os.path.sep, 'usr', 'lib', 'grub',
- '%s' % (grub_subdir),
- "" if use_signed else "monolithic",
- 'grub%s.%s' % (efi_arch.lower(), efi_ext)
- )
grub_dest = os.path.join(
'EFI', 'BOOT', 'GRUB%s.EFI' % (efi_arch.upper())
)
- efi_img.insert_file(shim_src, removable_media_path)
- efi_img.insert_file(grub_src, grub_dest)
+ efi_img.insert_file(shim_path, removable_media_path)
+ efi_img.insert_file(grub_path, grub_dest)
super().__init__(efi_img)
--- /dev/null
+#
+# Copyright 2022 Canonical Ltd.
+# Authors:
+# - dann frazier <dann.frazier@canonical.com>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License version 3, as published
+# by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+import os
+import subprocess
+import tempfile
+
+
+class SignedBinary:
+ def __init__(self, binary_path, key_path, cert_path, password=None):
+ openssl_password_args = []
+ if password:
+ openssl_password_args = [
+ "-passin", f"pass:{password}"
+ ]
+ with tempfile.NamedTemporaryFile() as keytmp:
+ subprocess.check_call(
+ [
+ "openssl", "rsa",
+ ] + openssl_password_args + [
+ "-in", f"{key_path}",
+ "-out", f"{keytmp.name}",
+ ]
+ )
+ with tempfile.NamedTemporaryFile(delete=False) as f:
+ self.path = f.name
+
+ subprocess.check_call(
+ [
+ "sbsign", "--key", f"{keytmp.name}",
+ "--cert", f"{cert_path}",
+ binary_path, "--output", f"{self.path}"
+ ]
+ )
+
+ def __del__(self):
+ os.unlink(self.path)
#!/usr/bin/env python3
#
-# Copyright 2019-2021 Canonical Ltd.
+# Copyright 2019-2022 Canonical Ltd.
# Authors:
# - dann frazier <dann.frazier@canonical.com>
#
#
import enum
+import os
import pexpect
import subprocess
import sys
+import time
import unittest
from UEFI.Filesystems import GrubShellBootableIsoImage
+from UEFI.SignedBinary import SignedBinary
from UEFI.Qemu import QemuEfiMachine, QemuEfiVariant, QemuEfiFlashSize
from UEFI import Qemu
['dpkg', '--print-architecture']
).decode().rstrip()
+EfiArchToGrubArch = {
+ 'X64': "x86_64",
+ 'AA64': "arm64",
+}
+
+TEST_TIMEOUT = 120
+
+
+def get_local_grub_path(efi_arch, signed=False):
+ grub_subdir = "%s-efi" % EfiArchToGrubArch[efi_arch.upper()]
+ ext = "efi"
+ if signed:
+ grub_subdir = f"{grub_subdir}-signed"
+ ext = f"{ext}.signed"
+
+ grub_path = os.path.join(
+ os.path.sep, 'usr', 'lib', 'grub',
+ '%s' % (grub_subdir),
+ "" if signed else "monolithic",
+ 'grub%s.%s' % (efi_arch.lower(), ext)
+ )
+ return grub_path
+
+
+def get_local_shim_path(efi_arch, signed=False):
+ ext = 'efi'
+ if signed:
+ ext = f"{ext}.signed"
+ shim_path = os.path.join(
+ os.path.sep, 'usr', 'lib', 'shim',
+ 'shim%s.%s' % (efi_arch.lower(), ext)
+ )
+ return shim_path
+
class BootToShellTest(unittest.TestCase):
debug = True
+ def setUp(self):
+ self.startTime = time.time()
+
+ def tearDown(self):
+ t = time.time() - self.startTime
+ sys.stdout.write("%s runtime: %.3fs\n" % (self.id(), t))
+
def run_cmd_check_shell(self, cmd):
- child = pexpect.spawn(' '.join(cmd))
+ child = pexpect.spawn(' '.join(cmd), encoding='UTF-8')
if self.debug:
- child.logfile = sys.stdout.buffer
+ child.logfile = sys.stdout
try:
while True:
i = child.expect(
'Press .* or any other key to continue',
'Shell> '
],
- timeout=60,
+ timeout=TEST_TIMEOUT,
)
if i == 0:
child.sendline('\x1b')
child.sendline('reset -s\r')
continue
except pexpect.EOF:
- return
+ child.close()
+ if child.exitstatus != 0:
+ self.fail("ERROR: exit code %d\n" % (child.exitstatus))
except pexpect.TIMEOUT as err:
self.fail("%s\n" % (err))
PRE_EXEC = 1
POST_EXEC = 2
- child = pexpect.spawn(' '.join(cmd))
+ child = pexpect.spawn(' '.join(cmd), encoding='UTF-8')
if self.debug:
- child.logfile = sys.stdout.buffer
+ child.logfile = sys.stdout
try:
state = State.PRE_EXEC
while True:
'grub> ',
'Command Error Status: Access Denied',
],
- timeout=60,
+ timeout=TEST_TIMEOUT,
)
if i == 0:
child.sendline('\x1b')
if i == 4:
verified = False
continue
+ except pexpect.EOF:
+ child.close()
+ if child.exitstatus != 0:
+ self.fail("ERROR: exit code %d\n" % (child.exitstatus))
except pexpect.TIMEOUT as err:
self.fail("%s\n" % (err))
- except pexpect.EOF:
- pass
self.assertEqual(should_verify, verified)
def test_aavmf(self):
self.run_cmd_check_shell(q.command)
@unittest.skipUnless(DPKG_ARCH == 'arm64', "Requires grub-efi-arm64")
+ @unittest.skipUnless(
+ subprocess.run(
+ ['dpkg-vendor', '--derives-from', 'Ubuntu']
+ ).returncode == 0,
+ "Debian does not provide a signed shim for arm64, see #992073"
+ )
def test_aavmf_ms_secure_boot_signed(self):
q = Qemu.QemuCommand(
QemuEfiMachine.AAVMF,
variant=QemuEfiVariant.MS,
)
- iso = GrubShellBootableIsoImage('AA64', use_signed=True)
+ grub = get_local_grub_path('AA64', signed=True)
+ shim = get_local_shim_path('AA64', signed=True)
+ iso = GrubShellBootableIsoImage('AA64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'aa64', True)
QemuEfiMachine.AAVMF,
variant=QemuEfiVariant.MS,
)
- iso = GrubShellBootableIsoImage('AA64', use_signed=False)
+ grub = get_local_grub_path('AA64', signed=False)
+ shim = get_local_shim_path('AA64', signed=False)
+ iso = GrubShellBootableIsoImage('AA64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'aa64', False)
variant=QemuEfiVariant.MS,
flash_size=QemuEfiFlashSize.SIZE_2MB,
)
- iso = GrubShellBootableIsoImage('X64', use_signed=True)
+ grub = get_local_grub_path('X64', signed=True)
+ shim = get_local_shim_path('X64', signed=True)
+ iso = GrubShellBootableIsoImage('X64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'x64', True)
variant=QemuEfiVariant.MS,
flash_size=QemuEfiFlashSize.SIZE_2MB,
)
- iso = GrubShellBootableIsoImage('X64', use_signed=False)
+ grub = get_local_grub_path('X64', signed=False)
+ shim = get_local_shim_path('X64', signed=False)
+ iso = GrubShellBootableIsoImage('X64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'x64', False)
variant=QemuEfiVariant.MS,
flash_size=QemuEfiFlashSize.SIZE_4MB,
)
- iso = GrubShellBootableIsoImage('X64', use_signed=True)
+ grub = get_local_grub_path('X64', signed=True)
+ shim = get_local_shim_path('X64', signed=True)
+ iso = GrubShellBootableIsoImage('X64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'x64', True)
variant=QemuEfiVariant.MS,
flash_size=QemuEfiFlashSize.SIZE_4MB,
)
- iso = GrubShellBootableIsoImage('X64', use_signed=False)
+ grub = get_local_grub_path('X64', signed=False)
+ shim = get_local_shim_path('X64', signed=False)
+ iso = GrubShellBootableIsoImage('X64', shim, grub)
+ q.add_disk(iso.path)
+ self.run_cmd_check_secure_boot(q.command, 'x64', False)
+
+ @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only")
+ def test_ovmf_snakeoil_secure_boot_signed(self):
+ q = Qemu.QemuCommand(
+ QemuEfiMachine.OVMF_Q35,
+ variant=QemuEfiVariant.SNAKEOIL,
+ )
+ shim = SignedBinary(
+ get_local_shim_path('X64', signed=False),
+ "/usr/share/ovmf/PkKek-1-snakeoil.key",
+ "/usr/share/ovmf/PkKek-1-snakeoil.pem",
+ "snakeoil",
+ )
+ grub = SignedBinary(
+ get_local_grub_path('X64', signed=False),
+ "/usr/share/ovmf/PkKek-1-snakeoil.key",
+ "/usr/share/ovmf/PkKek-1-snakeoil.pem",
+ "snakeoil",
+ )
+ iso = GrubShellBootableIsoImage('X64', shim.path, grub.path)
+ q.add_disk(iso.path)
+ self.run_cmd_check_secure_boot(q.command, 'x64', True)
+
+ @unittest.skipUnless(DPKG_ARCH == 'amd64', "amd64-only")
+ def test_ovmf_snakeoil_secure_boot_unsigned(self):
+ q = Qemu.QemuCommand(
+ QemuEfiMachine.OVMF_Q35,
+ variant=QemuEfiVariant.SNAKEOIL,
+ flash_size=QemuEfiFlashSize.DEFAULT,
+ )
+ grub = get_local_grub_path('X64', signed=False)
+ shim = get_local_shim_path('X64', signed=False)
+ iso = GrubShellBootableIsoImage('X64', shim, grub)
q.add_disk(iso.path)
self.run_cmd_check_secure_boot(q.command, 'x64', False)