blob: 2daeeff3ef4c4e8aef1b3fae89c8655eb253c425 [file] [log] [blame]
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests Firmware related signing"""
import csv
import io
import os
import shutil
import textwrap
from chromite.lib import cros_test_lib
from chromite.lib import osutils
from chromite.lib import partial_mock
from chromite.signing.lib import firmware
from chromite.signing.lib import keys
from chromite.signing.lib import keys_unittest
from chromite.signing.lib import signer_unittest
def MockDumpFmap(rc, ec_ro=True) -> None:
"""Add futility dump_fmap mock for bios.bin and ec.bin."""
bios_output = textwrap.dedent(
"""
SI_ALL 0 2097152
SI_DESC 0 4096
SI_ME 4096 2093056
SI_BIOS 2097152 14680064
RW_SECTION_A 2097152 4096000
VBLOCK_A 2097152 65536
FW_MAIN_A 2162688 4030400
RW_FWID_A 6193088 64
RW_SECTION_B 6193152 4096000
VBLOCK_B 6193152 65536
FW_MAIN_B 6258688 4030400
RW_FWID_B 10289088 64
RW_MISC 10289152 196608
UNIFIED_MRC_CACHE 10289152 131072
RECOVERY_MRC_CACHE 10289152 65536
RW_MRC_CACHE 10354688 65536
RW_ELOG 10420224 16384
RW_SHARED 10436608 16384
SHARED_DATA 10436608 8192
VBLOCK_DEV 10444800 8192
RW_VPD 10452992 8192
RW_NVRAM 10461184 24576
RW_LEGACY 10485760 2097152
WP_RO 12582912 4194304
RO_VPD 12582912 16384
RO_UNUSED 12599296 49152
RO_SECTION 12648448 4128768
FMAP 12648448 2048
RO_FRID 12650496 64
RO_FRID_PAD 12650560 1984
GBB 12652544 978944
COREBOOT 13631488
"""
)
ec_output = textwrap.dedent(
"""
EC_RO 64 131072
FR_MAIN 64 131072
RO_FRID 388 32
FMAP 104000 518
WP_RO 0 262144
EC_RW 262144 131072
RW_FWID 262468 32
SIG_RW 392192 1024
EC_RW_B 393216 131072
"""
)
if ec_ro:
ec_output += "KEY_RO 130112 1024"
rc.AddCmdResult(
partial_mock.ListRegex("futility dump_fmap -p .*bios.bin"),
stdout=bios_output,
)
rc.AddCmdResult(
partial_mock.ListRegex("futility dump_fmap -p .*ec.bin"),
stdout=ec_output,
)
def MockBiosSigner(rc) -> None:
"""Add Bios Signing Mocks to |rc|."""
def _copy_firmware(cmd, *_args, **_kwargs) -> None:
"""Copy file_in to file_out, if file_in exists."""
file_in = cmd[-2]
file_out = cmd[-1]
if os.path.exists(file_in):
shutil.copy(file_in, file_out)
rc.AddCmdResult(
partial_mock.ListRegex("futility sign --type bios .*"),
side_effect=_copy_firmware,
)
def MockECSigner(rc, ec_ro=True) -> None:
"""Add EC Signing Mocks to |rc|.
Args:
rc: RunCommandMock that cmds are added to
ec_ro: Treat EC as RO in fmap
"""
rc.AddCmdResult(partial_mock.ListRegex("futility sign --type rwsig .*"))
rc.AddCmdResult(partial_mock.ListRegex("openssl dgst -sha256 -binary .*"))
rc.AddCmdResult(partial_mock.ListRegex("store_file_in_cbfs .*"))
MockDumpFmap(rc, ec_ro=ec_ro)
def MockGBBSigner(rc) -> None:
"""Add GBB Signer Mock commands to |rc|"""
rc.AddCmdResult(partial_mock.ListRegex("futility gbb"))
def MockFirmwareSigner(rc) -> None:
"""Add mocks to |rc| for Firmware signing."""
keys_unittest.MockVbutilKey(rc)
MockBiosSigner(rc)
MockECSigner(rc, ec_ro=False)
MockGBBSigner(rc)
class TestBiosSigner(cros_test_lib.RunCommandTempDirTestCase):
"""Test BiosSigner."""
def setUp(self) -> None:
MockBiosSigner(self.rc)
def testGetCmdArgs(self) -> None:
bs = firmware.BiosSigner()
ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
bios_bin = os.path.join(self.tempdir, "bios.bin")
bios_out = os.path.join(self.tempdir, "bios.out")
fw_key = ks.keys["firmware_data_key"]
kernel_key = ks.keys["kernel_subkey"]
self.assertListEqual(
bs.GetFutilityArgs(ks, bios_bin, bios_out),
[
"sign",
"--type",
"bios",
"--signprivate",
fw_key.private,
"--keyblock",
fw_key.keyblock,
"--kernelkey",
kernel_key.public,
"--version",
str(fw_key.version),
bios_bin,
bios_out,
],
)
def testGetCmdArgsWithDevKeys(self) -> None:
bs = firmware.BiosSigner()
ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
bios_bin = os.path.join(self.tempdir, "bios.bin")
bios_out = os.path.join(self.tempdir, "bios.out")
fw_key = ks.keys["firmware_data_key"]
kernel_key = ks.keys["kernel_subkey"]
self.assertListEqual(
bs.GetFutilityArgs(ks, bios_bin, bios_out),
[
"sign",
"--type",
"bios",
"--signprivate",
fw_key.private,
"--keyblock",
fw_key.keyblock,
"--kernelkey",
kernel_key.public,
"--version",
str(fw_key.version),
bios_bin,
bios_out,
],
)
def testGetCmdArgsWithPreamble(self) -> None:
bs = firmware.BiosSigner(preamble_flags=1)
ks = signer_unittest.KeysetFromSigner(bs, self.tempdir)
bios_bin = os.path.join(self.tempdir, "bios.bin")
bios_out = os.path.join(self.tempdir, "bios.out")
args = bs.GetFutilityArgs(ks, bios_bin, bios_out)
self.assertIn("--flags", args)
self.assertEqual(args[args.index("--flags") + 1], "1")
def testGetCmdArgsWithSig(self) -> None:
loem_dir = os.path.join(self.tempdir, "loem1", "keyset")
loem_id = "loem1"
bs = firmware.BiosSigner(sig_id=loem_id, sig_dir=loem_dir)
ks = signer_unittest.KeysetFromSigner(bs, keydir=self.tempdir)
bios_bin = os.path.join(self.tempdir, loem_id, "bios.bin")
fw_key = ks.keys["firmware_data_key"]
kernel_key = ks.keys["kernel_subkey"]
self.assertListEqual(
bs.GetFutilityArgs(ks, bios_bin, loem_dir),
[
"sign",
"--type",
"bios",
"--signprivate",
fw_key.private,
"--keyblock",
fw_key.keyblock,
"--kernelkey",
kernel_key.public,
"--version",
str(fw_key.version),
"--loemdir",
loem_dir,
"--loemid",
loem_id,
bios_bin,
loem_dir,
],
)
class TestECSigner(cros_test_lib.RunCommandTempDirTestCase):
"""Test ECSigner."""
def testIsROSignedRW(self) -> None:
MockECSigner(self.rc, ec_ro=False)
ec_signer = firmware.ECSigner()
ec_bin = os.path.join(self.tempdir, "ec.bin")
self.assertFalse(ec_signer.IsROSigned(ec_bin))
self.assertCommandContains(["futility", "dump_fmap", "-p", ec_bin])
def testIsROSignedRO(self) -> None:
MockECSigner(self.rc)
ec_signer = firmware.ECSigner()
ec_bin = os.path.join(self.tempdir, "ec.bin")
self.assertTrue(ec_signer.IsROSigned(ec_bin))
def testSign(self) -> None:
MockECSigner(self.rc, ec_ro=False)
ec_signer = firmware.ECSigner()
ks = signer_unittest.KeysetFromSigner(ec_signer, self.tempdir)
ec_bin = os.path.join(self.tempdir, "ec.bin")
bios_bin = os.path.join(self.tempdir, "bios.bin")
ec_signer.Sign(ks, ec_bin, bios_bin)
self.assertCommandContains(
[
"futility",
"sign",
"--type",
"rwsig",
"--prikey",
ks.keys["key_ec_efs"].private,
ec_bin,
]
)
self.assertCommandContains(["openssl", "dgst", "-sha256", "-binary"])
self.assertCommandContains(["store_file_in_cbfs", bios_bin, "ecrw"])
self.assertCommandContains(
["store_file_in_cbfs", bios_bin, "ecrw.hash"]
)
class TestFirmwareSigner(cros_test_lib.RunCommandTempDirTestCase):
"""Test FirmwareSigner."""
def setUp(self) -> None:
MockFirmwareSigner(self.rc)
def testSignOneSimple(self) -> None:
fs = firmware.FirmwareSigner()
ks = signer_unittest.KeysetFromSigner(fs, self.tempdir)
shellball_dir = os.path.join(self.tempdir, "shellball")
bios_path = os.path.join(shellball_dir, "bios.bin")
fs.SignOne(ks, self.tempdir, bios_path)
self.assertCommandContains(["futility", "sign", "--type", "bios"])
self.assertCommandContains(["futility", "gbb"])
def testSignOneWithEC(self) -> None:
fs = firmware.FirmwareSigner()
keyset_dir = os.path.join(self.tempdir, "keyset")
ks = keys_unittest.KeysetMock(keyset_dir)
ks.CreateStubKeys()
shellball_dir = os.path.join(self.tempdir, "shellball")
bios_path = os.path.join(shellball_dir, "bios.bin")
ec_path = os.path.join(shellball_dir, "ec.bin")
fs.SignOne(ks, self.tempdir, bios_path, ec_image=ec_path)
self.assertCommandContains(
["futility", "sign", "--type", "rwsig", ec_path]
)
def testSignOneWithLoem(self) -> None:
fs = firmware.FirmwareSigner()
keyset_dir = os.path.join(self.tempdir, "keyset")
ks = keys_unittest.KeysetMock(keyset_dir)
ks.CreateStubKeys()
ks_subset = ks.GetBuildKeyset("ACME")
shellball_dir = os.path.join(self.tempdir, "shellball")
bios_path = os.path.join(shellball_dir, "bios.bin")
fs.SignOne(
ks, shellball_dir, bios_path, model_name="acme-board", key_id="ACME"
)
self.assertExists(os.path.join(shellball_dir, "rootkey.acme-board"))
self.assertCommandContains(
[
"futility",
"sign",
"--type",
"bios",
"--signprivate",
ks_subset.keys["firmware_data_key"].private,
]
)
def testSignWithSignerConfig(self) -> None:
fs = firmware.FirmwareSigner()
keyset_dir = os.path.join(self.tempdir, "keyset")
ks = keys_unittest.KeysetMock(keyset_dir)
ks.CreateStubKeys()
shellball_dir = os.path.join(self.tempdir, "shellball")
osutils.SafeMakedirs(shellball_dir)
# Create signer_config.csv
signer_config_csv = os.path.join(shellball_dir, "signer_config.csv")
with open(signer_config_csv, "w", encoding="utf-8") as csv_file:
csv_file.write(SignerConfigsFromCSVTest.CreateCSV().read())
fs.Sign(ks, shellball_dir, None)
for board_config in SignerConfigsFromCSVTest.CreateBoardDicts():
bios_path = os.path.join(
shellball_dir, board_config["firmware_image"]
)
self.assertCommandContains(["futility", "sign", bios_path])
def testSignWithNoSignerConfigUnified(self) -> None:
"""Test signing unified builds with no signer_config.csv provided."""
fs = firmware.FirmwareSigner()
keyset_dir = os.path.join(self.tempdir, "keyset")
ks = keys_unittest.KeysetMock(keyset_dir)
ks.CreateStubKeys()
shellball_dir = os.path.join(self.tempdir, "shellball")
test_bios = ("bios.loem1.bin", "bios.loem2.bin")
for bios in test_bios:
osutils.Touch(os.path.join(shellball_dir, bios), makedirs=True)
fs.Sign(ks, shellball_dir, None)
for bios in test_bios:
bios_path = os.path.join(shellball_dir, bios)
self.assertCommandContains(["futility", "sign", bios_path])
self.assertExists(os.path.join(shellball_dir, "keyset.loem1"))
self.assertExists(os.path.join(shellball_dir, "keyset.loem2"))
def testSignWithNoSignerConfigNonUnified(self) -> None:
"""Test signing non-unified build with no signer_config.csv provided."""
fs = firmware.FirmwareSigner()
keyset_dir = os.path.join(self.tempdir, "keyset")
ks = keys_unittest.KeysetMock(keyset_dir, has_loem_ini=False)
ks.CreateStubKeys()
shellball_dir = os.path.join(self.tempdir, "shellball")
test_bios = ("bios.bin",)
for bios in test_bios:
osutils.Touch(os.path.join(shellball_dir, bios), makedirs=True)
fs.Sign(ks, shellball_dir, None)
for bios in test_bios:
bios_path = os.path.join(shellball_dir, bios)
self.assertCommandContains(["futility", "sign", bios_path])
self.assertExists(os.path.join(shellball_dir, "keyset"))
class TestGBBSigner(cros_test_lib.RunCommandTempDirTestCase):
"""Test GBBSigner."""
def setUp(self) -> None:
MockGBBSigner(self.rc)
def testGetFutilityArgs(self) -> None:
gb_signer = firmware.GBBSigner()
ks = signer_unittest.KeysetFromSigner(gb_signer, self.tempdir)
bios_in = os.path.join(self.tempdir, "bin.orig")
bios_out = os.path.join(self.tempdir, "bios.bin")
self.assertListEqual(
gb_signer.GetFutilityArgs(ks, bios_in, bios_out),
[
"gbb",
"--set",
"--recoverykey=" + ks.keys["recovery_key"].public,
"--rootkey=" + ks.keys["root_key"].public,
bios_in,
bios_out,
],
)
class ShellballTest(cros_test_lib.RunCommandTempDirTestCase):
"""Verify that shellball is being called with correct arguments."""
@staticmethod
def CmdMock(rc) -> None:
"""Add mock commands to |rc|"""
rc.AddCmdResult(partial_mock.ListRegex(".* --sb_extract .*"))
rc.AddCmdResult(partial_mock.ListRegex(".* --sb_repack .*"))
def setUp(self) -> None:
"""Setup simple Shellball instance for mock testing."""
ShellballTest.CmdMock(self.rc)
self.sb1name = os.path.join(self.tempdir, "fooball")
osutils.Touch(self.sb1name)
self.sb1 = firmware.Shellball(self.sb1name)
def testExtractCall(self) -> None:
"""Test arguments for image extract."""
out_dir = "bar"
expected_args = ["--sb_extract", out_dir]
self.sb1.Extract(out_dir)
self.assertCommandContains(expected_args)
def testRepackCall(self) -> None:
"""Test arguments for image repack."""
from_dir = "bar"
expected_args = ["--sb_repack", from_dir]
self.sb1.Repack(from_dir)
self.assertCommandContains(expected_args)
def testContextManager(self) -> None:
with self.sb1 as sb_dir:
self.assertExists(sb_dir)
self.assertCommandContains(["--sb_extract"])
self.assertCommandContains(["--sb_repack"])
class SignerConfigsFromCSVTest(cros_test_lib.TestCase):
"""Test SignerConfigsFromCSV function."""
FIELDS = ("model_name", "firmware_image", "key_id", "ec_image")
BOARDS = (
("acme", "models/acme/bios.bin", "ACME", "models/acme/ec.bin"),
("shinra", "models/shinra/bios.bin", "SHINRA", "models/shinra/ec.bin"),
("acme-loem3", "models/acme/bios.bin", "loem3", "models/acme/ec.bin"),
)
@staticmethod
def CreateBoardDicts(fields=None, boards=None):
"""Returns list of board dicts with the given fields"""
fields = fields or SignerConfigsFromCSVTest.FIELDS
boards = boards or SignerConfigsFromCSVTest.BOARDS
board_dicts = []
for board in boards:
board_dicts.append(dict(zip(fields, board)))
return board_dicts
@staticmethod
def CreateCSV(fields=None, boards=None, board_dict=None):
fields = fields or SignerConfigsFromCSVTest.FIELDS
boards = boards or SignerConfigsFromCSVTest.BOARDS
board_dicts = board_dict or SignerConfigsFromCSVTest.CreateBoardDicts(
fields=fields, boards=boards
)
csv_file = io.StringIO()
csv_writer = csv.DictWriter(csv_file, fields)
csv_writer.writeheader()
csv_writer.writerows(board_dicts)
csv_file.seek(0)
return csv_file
def testMissingRow(self) -> None:
fields = SignerConfigsFromCSVTest.BOARDS[:-1]
csv_file = SignerConfigsFromCSVTest.CreateCSV(fields=fields)
with self.assertRaises(csv.Error):
firmware.SignerConfigsFromCSV(csv_file)
def testSimple(self) -> None:
orig_boards = SignerConfigsFromCSVTest.CreateBoardDicts()
csv_file = SignerConfigsFromCSVTest.CreateCSV(board_dict=orig_boards)
signer_configs = firmware.SignerConfigsFromCSV(csv_file)
self.assertListEqual(orig_boards, signer_configs)
class TestWriteSignerNotes(cros_test_lib.RunCommandTempDirTestCase):
"""Test WriteSignerNotes function."""
def setUp(self) -> None:
keys_unittest.MockVbutilKey(self.rc)
def testSingleKey(self) -> None:
"""Test function's output with fixed sha1sum."""
recovery_key = keys.KeyPair("recovery_key", self.tempdir)
root_key = keys.KeyPair("root_key", self.tempdir)
keyset = keys.Keyset()
keyset.AddKey(recovery_key)
keyset.AddKey(root_key)
sha1sum = keys_unittest.MOCK_SHA1SUM
expected_output = [
f"Signed with keyset in {self.tempdir}",
f"recovery: {sha1sum}",
f"root: {sha1sum}",
]
version_signer = io.StringIO()
firmware.WriteSignerNotes(keyset, version_signer)
self.assertListEqual(
expected_output, version_signer.getvalue().splitlines()
)
def testLoemKeys(self) -> None:
"""Test function's output with multiple loem keys."""
recovery_key = keys.KeyPair("recovery_key", self.tempdir)
root_keys = {
"loem%d" % idx: keys.KeyPair("root_key.loem%d" % idx, self.tempdir)
for idx in range(1, 4)
}
lines = ["[loem]"] + [
"%d = loem%d" % (int(loem[4:]), int(loem[4:]))
for loem in sorted(root_keys.keys())
]
contents = "\n".join(lines) + "\n"
osutils.WriteFile(os.path.join(self.tempdir, "loem.ini"), contents)
keyset = keys.Keyset(self.tempdir)
keyset.AddKey(recovery_key)
for k in root_keys.values():
keyset.AddKey(k)
sha1sum = keys_unittest.MOCK_SHA1SUM
expected_header = [
f"Signed with keyset in {self.tempdir}",
f"recovery: {sha1sum}",
"List sha1sum of all loem/model's signatures:",
]
expected_loems = [
"loem1: " + sha1sum,
"loem2: " + sha1sum,
"loem3: " + sha1sum,
]
version_signer = io.StringIO()
firmware.WriteSignerNotes(keyset, version_signer)
output = version_signer.getvalue().splitlines()
output_header = output[: len(expected_header)]
output_loem = output[len(expected_header) :]
output_loem.sort() # loem's can be out of order, so sort first.
self.assertListEqual(expected_header, output_header)
self.assertListEqual(expected_loems, output_loem)