blob: 08ad3f90ca93fa074e370d7a71856b690320a691 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""ChromeOS firmware Signers"""
from __future__ import print_function
import csv
import glob
import os
import re
import shutil
import tempfile
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import image_lib
from chromite.lib import osutils
from chromite.signing.lib import signer
class BiosSigner(signer.FutilitySigner):
"""Sign bios.bin file using futility."""
required_keys_private = ('firmware_data_key',)
required_keys_public = ('kernel_subkey',)
required_keyblocks = ('firmware_data_key',)
def __init__(self, sig_id='', sig_dir='', preamble_flags=None):
"""Init BiosSigner
Args:
sig_id: Signature ID (aka loem id)
sig_dir: Signature Output Directory (i.e shellball/keyset)
preamble_flags: preamble flags passed to futility
"""
self.sig_id = sig_id
self.sig_dir = sig_dir
self.preamble_flags = preamble_flags
def GetFutilityArgs(self, keyset, input_name, output_name):
"""Returns futility arguments for signing bios
Args:
keyset: keyset used for signing
input_name: bios image
output_name: output firmware file
"""
fw_key = keyset.keys['firmware_data_key']
kernel_key = keyset.keys['kernel_subkey']
dev_fw_key = keyset.keys.get('dev_firmware_data_key', fw_key)
args = ['sign',
'--type', 'bios',
'--signprivate', fw_key.private,
'--keyblock', fw_key.keyblock,
'--kernelkey', kernel_key.public,
'--version', str(fw_key.version),
'--devsign', dev_fw_key.private,
'--devkeyblock', dev_fw_key.keyblock]
if self.preamble_flags is not None:
args += ['--flags', str(self.preamble_flags)]
# Add loem related arguments
if self.sig_id and self.sig_dir:
args += ['--loemdir', self.sig_dir,
'--loemid', self.sig_id]
# Add final input/output arguments
args += [input_name, output_name]
return args
class ECSigner(signer.BaseSigner):
"""Sign EC bin file."""
required_keys_private = ('key_ec_efs',)
def IsROSigned(self, ec_image):
"""Returns True if the given ec.bin is RO signed"""
# Check fmap for KEY_RO
fmap = cros_build_lib.run(['futility', 'dump_fmap', '-p', ec_image],
capture_output=True)
return b'KEY_RO' in fmap.stdout
def Sign(self, keyset, input_name, output_name):
"""Sign EC image
Args:
keyset: keyset used for this signing step
input_name: ec image path to be signed (i.e. to ec.bin)
output_name: bios image path to be updated with new hashes
Raises:
SigningFailedError: if a signing fails
"""
# Use absolute paths since we use a temp directory
ec_path = os.path.abspath(input_name)
bios_path = os.path.abspath(output_name)
if self.IsROSigned(ec_path):
# Only sign if not read-only, nothing to do
return
logging.info('Signing EC %s', ec_path)
# Run futility in temp_dir to avoid cwd artifacts
with osutils.TempDir() as temp_dir:
ec_rw_bin = os.path.join(temp_dir, 'EC_RW.bin')
ec_rw_hash = os.path.join(temp_dir, 'EC_RW.hash')
try:
cros_build_lib.run(['futility', 'sign', '--type', 'rwsig', '--prikey',
keyset.keys['key_ec_efs'].private, ec_path],
cwd=temp_dir)
cros_build_lib.run(['openssl', 'dgst', '-sha256', '-binary', ec_rw_bin],
stdout=ec_rw_hash, cwd=temp_dir)
cros_build_lib.run(['store_file_in_cbfs', bios_path, ec_rw_bin, 'ecrw'])
cros_build_lib.run(['store_file_in_cbfs', bios_path, ec_rw_hash,
'ecrw.hash'])
except cros_build_lib.RunCommandError as err:
logging.warning('Signing EC failed: %s', str(err))
raise signer.SigningFailedError('Signing EC failed')
class GBBSigner(signer.FutilitySigner):
"""Sign GBB"""
required_keys_public = ('recovery_key',)
required_keys_private = ('root_key',)
def GetFutilityArgs(self, keyset, input_name, output_name):
"""Return args for signing GBB
Args:
keyset: Keyset used for signing
input_name: Firmware image
output_name: Bios path (i.e. tobios.bin)
"""
return ['gbb',
'--set',
'--recoverykey=' + keyset.keys['recovery_key'].public,
input_name,
output_name]
class FirmwareSigner(signer.BaseSigner):
"""Signs all firmware related to the given configuration."""
required_keys_private = (BiosSigner.required_keys_private +
GBBSigner.required_keys_private)
required_keys_public = (BiosSigner.required_keys_public +
GBBSigner.required_keys_public)
required_keyblocks = (BiosSigner.required_keyblocks +
GBBSigner.required_keyblocks)
def SignOne(self, keyset, shellball_dir, bios_image, ec_image='',
model_name='', key_id='', keyset_out_dir='keyset'):
"""Perform one signing based on the given args.
Args:
keyset: master keyset used for signing,
shellball_dir: location of extracted shellball
bios_image: relitive path of bios.bin in shellball
ec_image: relative path of ec.bin in shellball
model_name: name of target's model_name as define in signer_config.csv
key_id: subkey id to be used for signing
keyset_out_dir: relative path of keyset output dir in shellball
Raises:
SigningFailedError: if a signing fails
"""
if key_id:
keyset = keyset.GetBuildKeyset(key_id)
shellball_keydir = os.path.join(shellball_dir, keyset_out_dir)
osutils.SafeMakedirs(shellball_keydir)
if model_name:
shutil.copy(keyset.keys['root_key'].public,
os.path.join(shellball_dir, 'rootkey.' + model_name))
bios_path = os.path.join(shellball_dir, bios_image)
if ec_image:
ec_path = os.path.join(shellball_dir, ec_image)
logging.info('Signing EC: %s', ec_path)
ECSigner().Sign(keyset, ec_path, bios_path)
logging.info('Signing BIOS: %s', bios_path)
with tempfile.NamedTemporaryFile() as temp_fw:
bios_signer = BiosSigner(sig_id=model_name, sig_dir=shellball_keydir)
bios_signer.Sign(keyset, bios_path, temp_fw.name)
GBBSigner().Sign(keyset, temp_fw.name, bios_path)
def Sign(self, keyset, input_name, output_name):
"""Sign Firmware shellball.
Signing is based on if 'signer_config.csv', then all rows defined in file
are signed. Else all bios*.bin in shellball will be signed.
Args:
keyset: master keyset, with subkeys[key_id] if defined
input_name: location of extracted shellball
output_name: unused
Raises:
SigningFailedError: if a signing step fails
"""
shellball_dir = input_name
signerconfig_csv = os.path.join(shellball_dir, 'signer_config.csv')
if os.path.exists(signerconfig_csv):
with open(signerconfig_csv) as csv_file:
signerconfigs = SignerConfigsFromCSV(csv_file)
for signerconfig in signerconfigs:
self.SignOne(keyset,
shellball_dir,
signerconfig['firmware_image'],
ec_image=signerconfig['ec_image'],
model_name=signerconfig['model_name'],
key_id=signerconfig['key_id'])
else:
# Sign all ./bios*.bin
for bios_path in glob.glob(os.path.join(input_name, 'bios*.bin')):
key_id_match = re.match(r'.*bios\.(\w+)\.bin', bios_path)
key_id = key_id_match.group(1) if key_id_match else ''
keyset_out_dir = 'keyset.' + key_id if key_id else 'keyset'
self.SignOne(keyset,
shellball_dir,
bios_path,
model_name=key_id,
key_id=key_id,
keyset_out_dir=keyset_out_dir)
class ShellballError(Exception):
"""Error occurred with firmware shellball"""
class ShellballExtractError(ShellballError):
"""Raised when extracting fails."""
class ShellballRepackError(ShellballError):
"""Raised when repacking fails."""
class Shellball(object):
"""Firmware shellball image created from pack_firmware.
Can be called as a Context Manager which will extract itself to a temp
directory and repack itself on exit.
https://sites.google.com/a/google.com/chromeos-partner/platforms/creating-a-firmware-updater
"""
def __init__(self, filename):
"""Initial Shellball, no disk changes.
Args:
filename: filename of shellball
"""
self.filename = filename
self._extract_dir = None
def __enter__(self):
"""Extract the shellball to a temp directory, returns directory."""
self._extract_dir = osutils.TempDir()
self.Extract(self._extract_dir.tempdir)
return self._extract_dir.tempdir
def __exit__(self, exc_type, exc_value, traceback):
"""Repack shellball and delete temp directory."""
try:
if exc_type is None:
self.Repack(self._extract_dir.tempdir)
finally:
if self._extract_dir:
# Always clear up temp directory
self._extract_dir.Cleanup()
def Extract(self, out_dir):
"""Extract self to given directory, raises ExtractFail on fail"""
try:
self._Run('--sb_extract', out_dir)
except cros_build_lib.RunCommandError as err:
logging.error('Extracting firmware shellball failed')
raise ShellballExtractError(err.msg)
def Repack(self, src_dir):
"""Repack shellball with the given directory, raises RepackFailed on fail.
Only supports shellballs that honor '--sb_repack' which should include
everything that has been signed since 2014
"""
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
orig_file = self.filename
self.filename = tmp_file.name
try:
shutil.copy(orig_file, tmp_file.name)
self._Run('--sb_repack', src_dir)
shutil.move(tmp_file.name, orig_file)
except cros_build_lib.RunCommandError as err:
logging.error('Repacking firmware shellball failed')
raise ShellballRepackError(err.msg)
finally:
self.filename = orig_file
# Clean up file if still exists
if os.path.exists(tmp_file.name):
os.remove(tmp_file.name)
def _Run(self, *args):
"""Execute shellball with given arguments."""
cmd = [os.path.realpath(self.filename)]
cmd += args
cros_build_lib.run(cmd)
# TODO(vapier): Should switch this to image_lib.LoopbackPartitions or
# signing.lib.imagefile as makes sense.
def _MountImagePartition(image_file, part_id, destination, gpt_table=None,
sudo=True, makedirs=True, mount_opts=('ro', ),
skip_mtab=False):
"""Mount a |partition| from |image_file| to |destination|.
If there is a GPT table (GetImageDiskPartitionInfo), it will be used for
start offset and size of the selected partition. Otherwise, the GPT will
be read again from |image_file|.
The mount option will be:
-o offset=XXX,sizelimit=YYY,(*mount_opts)
Args:
image_file: A path to the image file (chromiumos_base_image.bin).
part_id: A partition name or number.
destination: A path to the mount point.
gpt_table: A list of PartitionInfo objects. See
image_lib.GetImageDiskPartitionInfo.
sudo: Same as MountDir.
makedirs: Same as MountDir.
mount_opts: Same as MountDir.
skip_mtab: Same as MountDir.
"""
if gpt_table is None:
gpt_table = image_lib.GetImageDiskPartitionInfo(image_file)
for part in gpt_table:
if part_id == part.name or part_id == part.number:
break
else:
part = None
raise ValueError('Partition number %s not found in the GPT %r.' %
(part_id, gpt_table))
opts = ['loop', 'offset=%d' % part.start, 'sizelimit=%d' % part.size]
opts += mount_opts
osutils.MountDir(image_file, destination, sudo=sudo, makedirs=makedirs,
mount_opts=opts, skip_mtab=skip_mtab)
def ResignImageFirmware(image_file, keyset):
"""Resign the given firmware image.
Args:
image_file: string path to image.
keyset: Keyset to use for signing.
Raises SignerFailedError
"""
with osutils.TempDir() as rootfs_dir:
with _MountImagePartition(image_file, 'ROOT-A', rootfs_dir):
sb_file = os.path.join(rootfs_dir, 'usr/sbin/chromeos-firmware')
if os.path.exists(sb_file):
logging.info('Found firmware, signing')
with Shellball(sb_file) as sb_dir:
fw_signer = FirmwareSigner()
if not fw_signer.Sign(keyset, sb_dir, None):
raise signer.SigningFailedError('Signing Firmware Image Failed: %s'
% sb_file)
version_signer_path = os.path.join(sb_dir, 'VERSION.signer')
with open(version_signer_path, 'w') as version_signer:
WriteSignerNotes(keyset, version_signer)
else:
logging.warning('No firmware found in image. Not signing firmware')
def SignerConfigsFromCSV(signer_config_file):
"""Returns list of SignerConfigs from a signer_config.csv file
CSV should have a header with fields model_name, firmware_image, key_id, and
ec_image
go/cros-unibuild-signing
Args:
signer_config_file: File descriptor for signer_configs.csv.
Returns:
List of dicts in the signer_configs file.
"""
csv_reader = csv.DictReader(signer_config_file)
for field in ('model_name', 'firmware_image', 'key_id', 'ec_image'):
if field not in csv_reader.fieldnames:
raise csv.Error('Missing field: ' + field)
return list(csv_reader)
def WriteSignerNotes(keyset, outfile):
"""Writes signer notes (a.k.a. VERSION.signer) to file.
Args:
keyset: keyset used for generating signer file.
outfile: file object that signer notes are written to.
"""
recovery_key = keyset.keys['recovery_key']
outfile.write(u'Signed with keyset in %s\n' % recovery_key.keydir)
outfile.write(u'recovery: %s\n' % recovery_key.GetSHA1sum())
root_keys = keyset.GetRootOfTrustKeys('root_key')
if 'root_key' in root_keys and len(root_keys) == 1:
outfile.write(u'root: %s\n' % (root_keys['root_key'].GetSHA1sum()))
else:
outfile.write(u"List sha1sum of all loem/model's signatures:\n")
for key_id, key in root_keys.items():
outfile.write(u'%s: %s\n' % (key_id, key.GetSHA1sum()))