aboutsummaryrefslogtreecommitdiffstats
path: root/lib/oeqa/selftest/cases/secureboot.py
blob: 4c059e2581f382c132933f4707c610457a07c5fa (plain)
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
#!/usr/bin/env python
# ex:ts=4:sw=4:sts=4:et
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
#
# Copyright (c) 2017, Intel Corporation.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 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 warranty of
# MERCHANTABILITY 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# AUTHORS
# Mikko Ylinen <mikko.ylinen@linux.intel.com>
#
# Based on meta/lib/oeqa/selftest/* and meta-refkit/lib/oeqa/selftest/*

"""Test cases for secure boot with QEMU running OVMF."""

import os
import unittest
import re
import glob
from shutil import rmtree, copy

from oeqa.core.decorator.depends import OETestDepends
from oeqa.selftest.case import OESelftestTestCase
from oeqa.utils.commands import runCmd, bitbake, get_bb_var, get_bb_vars, runqemu

class SecureBootTests(OESelftestTestCase):
    """Secure Boot test class."""

    ovmf_keys_enrolled = False
    ovmf_qemuparams = ''
    ovmf_dir = ''
    test_image_unsigned = 'secureboot-selftest-image-unsigned'
    test_image_signed = 'secureboot-selftest-image-signed'
    correct_key = 'refkit-db'
    incorrect_key = 'incorrect'

    @classmethod
    def setUpLocal(self):

        if not SecureBootTests.ovmf_keys_enrolled:
            bitbake('ovmf ovmf-shell-image-enrollkeys', output_log=self.logger)

            bb_vars = get_bb_vars(['TMPDIR', 'DEPLOY_DIR_IMAGE'])

            SecureBootTests.ovmf_dir = os.path.join(bb_vars['TMPDIR'], 'oeselftest', 'secureboot', 'ovmf')
            bb.utils.mkdirhier(SecureBootTests.ovmf_dir)

            # Copy (all) OVMF in a temporary location
            for src in glob.glob('%s/ovmf.*' % bb_vars['DEPLOY_DIR_IMAGE']):
                copy(src, SecureBootTests.ovmf_dir)

            SecureBootTests.ovmf_qemuparams = '-drive if=pflash,format=qcow2,file=%s/ovmf.secboot.qcow2' % SecureBootTests.ovmf_dir

            cmd = ("runqemu "
                   "qemuparams='%s' "
                   "ovmf-shell-image-enrollkeys wic intel-corei7-64 "
                   "nographic slirp") % SecureBootTests.ovmf_qemuparams
            print('Running "%s"' % cmd)
            status = runCmd(cmd)

            if not re.search('info: success', status.output, re.M):
                self.fail('Failed to enroll keys. EFI shell log:\n%s' % status.output)
            else:
                # keys enrolled in ovmf.secboot.vars
                SecureBootTests.ovmf_keys_enrolled = True

    @classmethod
    def tearDownLocal(self):
        # Seems this is mandatory between the tests (a signed image is booted
        # when running test_boot_unsigned_image after test_boot_signed_image).
        # bitbake('-c clean %s' % test_image, output_log=self.logger)
        #
        # Whatever the problem was, it no longer seems to be necessary, so
        # we can skip the time-consuming clean + full rebuild (5:04 min instead
        # of 6:55min here).
        pass

    @classmethod
    def tearDownClass(self):
        bitbake('ovmf-shell-image-enrollkeys:do_cleanall', output_log=self.logger)
        rmtree(self.ovmf_dir, ignore_errors=True)

    def secureboot_with_image(self, boot_timeout=300, signing_key=None):
        """Boot the image with UEFI SecureBoot enabled and see the result. """

        config = ""

        if signing_key:
            test_image = self.test_image_signed
            config += 'SECURE_BOOT_SIGNING_KEY = "${THISDIR}/files/%s.key"\n' % signing_key
            config += 'SECURE_BOOT_SIGNING_CERT = "${THISDIR}/files/%s.crt"\n' % signing_key
        else:
            test_image = self.test_image_unsigned

        self.write_config(config)
        bitbake(test_image, output_log=self.logger)
        self.remove_config(config)

        # Some of the cases depend on the timeout to expire. Allow overrides
        # so that we don't have to wait 1000s which is the default.
        overrides = {
            'TEST_QEMUBOOT_TIMEOUT': boot_timeout,
            }

        print('Booting %s' % test_image)

        try:
            with runqemu(test_image, ssh=False,
                          runqemuparams='nographic slirp',
                          qemuparams=self.ovmf_qemuparams,
                          overrides=overrides,
                          image_fstype='wic') as qemu:

                cmd  = 'uname -a'

                status, output = qemu.run_serial(cmd)

                self.assertTrue(status, 'Could not run \'uname -a\' (status=%s):\n%s' % (status, output))

                # if we got this far without a correctly signed image, something went wrong
                if signing_key != self.correct_key:
                    self.fail('The image not give a Security violation when expected. Boot log:\n%s' % output)


        except Exception:

            # Currently runqemu() fails if 'login:' prompt is not seen and it's
            # not possible to login as 'root'. Those conditions aren't met when
            # booting to EFI shell (See [YOCTO #11438]). We catch the failure
            # and parse the boot log to determine the success. Note: the
            # timeout triggers verbose bb.error() but that's normal with some
            # of the test cases.

            workdir = get_bb_var('WORKDIR', test_image)
            bootlog = "%s/testimage/qemu_boot_log" % workdir

            with open(bootlog, "r") as log:

                # This isn't right but all we can do at this point. The right
                # approach would run commands in the EFI shell to determine
                # the BIOS rejects unsigned and/or images signed with keys in
                # dbx key store but that needs changes in oeqa framework.

                output = log.read()

                # PASS if we see a security violation on unsigned or incorrectly signed images, otherwise fail
                if signing_key == self.correct_key:
                    self.fail('Correctly signed image failed to boot. Boot log:\n%s' % output)
                elif not re.search('Security Violation', output):
                    self.fail('The image not give a Security violation when expected. Boot log:\n%s' % output)

    def test_boot_unsigned_image(self):
        """ Boot unsigned image with secureboot enabled in UEFI."""
        self.secureboot_with_image(boot_timeout=120, signing_key=None)

    @OETestDepends(['secureboot.SecureBootTests.test_boot_unsigned_image'])
    def test_boot_incorrectly_signed_image(self):
        """ Boot (correctly) signed image with secureboot enabled in UEFI."""
        self.secureboot_with_image(boot_timeout=120, signing_key=self.incorrect_key)

    @OETestDepends(['secureboot.SecureBootTests.test_boot_incorrectly_signed_image'])
    def test_boot_correctly_signed_image(self):
        """ Boot (correctly) signed image with secureboot enabled in UEFI."""
        self.secureboot_with_image(boot_timeout=150, signing_key=self.correct_key)