blob: 457a2fbe83993250eb821eb79b9a09e5ce276b1c [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 image signer logic
Terminology:
Keyset: Set of keys used for signing, similar to a keyring
Signer: Object able to sign a type of image: bios, ec, kernel, etc...
Image: Fle that can be signed by a Signer
Signing requires an image to sign and its required keys.
A Signer is expected to understand how to take an input and output signed
artifacts using the stored Keychain.
A Keyset consists of keys and signed objects called keyblocks.
Signing Flow:
Key+------+->Keyset+---+->Signer+->Image Out
| |
Keyblock+-+ Image In+-+
"""
from __future__ import print_function
import os
import re
import six
from six.moves import configparser
from chromite.lib import cros_build_lib
class SigningFailedError(Exception):
"""Raise when a signing failed"""
class SignerOutputTemplateError(Exception):
"""Raise when there is an issue with filling a signer output template"""
class SignerInstructionConfig(object):
"""Signer Configuration based on ini file.
See Signer Documentation - Instruction File Format:
https://goto.google.com/cros-signer-instruction-file
"""
DEFAULT_TEMPLATE = ('chromeos_@VERSION@_@BOARD@_@TYPE@_@CHANNEL@-channel_'
'@KEYSET@.bin')
def __init__(self, archive='', board='', artifact_type='', version='',
versionrev='', keyset='', channel='', input_files=(),
output_files=()):
"""Initialize Configuration."""
# [general] section
self.archive = archive
self.board = board
self.artifact_type = artifact_type
self.version = version
self.versionrev = versionrev
# [insns] section
self.keyset = keyset
self.channel = channel
# Wrap files in tuple if given as a string
self.input_files = (
(input_files,) if isinstance(input_files, six.string_types)
else input_files)
self.output_files = (
(output_files,) if isinstance(output_files, six.string_types)
else output_files)
def __eq__(self, other):
return self.ToIniDict() == other.ToIniDict()
def ToIniDict(self):
"""Return ini layout."""
# [general] section
general_dict = {}
if self.archive:
general_dict['archive'] = self.archive
if self.board:
general_dict['board'] = self.board
if self.artifact_type:
general_dict['type'] = self.artifact_type
if self.version:
general_dict['version'] = self.version
if self.versionrev:
general_dict['versionrev'] = self.versionrev
# [insns] section
insns_dict = {}
if self.keyset:
insns_dict['keyset'] = self.keyset
if self.channel:
insns_dict['channel'] = self.channel
if self.input_files:
insns_dict['input_files'] = ' '.join(self.input_files)
if self.output_files:
insns_dict['output_names'] = ' '.join(self.output_files)
return {'general': general_dict,
'insns': insns_dict}
def ReadIniFile(self, fd):
"""Reads given file descriptor into configuration"""
config = configparser.ConfigParser(self.ToIniDict())
config.read_file(fd)
self.archive = config.get('general', 'archive')
self.board = config.get('general', 'board')
self.artifact_type = config.get('general', 'type')
self.version = config.get('general', 'version')
self.versionrev = config.get('general', 'versionrev')
self.keyset = config.get('insns', 'keyset')
self.channel = config.get('insns', 'channel')
# Optional options
if config.has_option('insns', 'input_files'):
self.input_files = config.get('insns', 'input_files').split(' ')
if config.has_option('insns', 'output_names'):
self.output_files = config.get('insns', 'output_names').split(' ')
def GetFilePairs(self):
"""Returns list of (input_file,output_file) tuples"""
files = []
if self.output_files:
out_files = self.output_files
else:
out_files = [SignerInstructionConfig.DEFAULT_TEMPLATE]
if len(out_files) == 1:
out_file = out_files[0]
# Check template generate unique output files
if (len(self.input_files) > 1 and
not re.search('(@BASENAME@)|(@ROOTNAME@)', out_file)):
raise SignerOutputTemplateError('@BASENAME@ or @ROOTNAME@ required for'
'templates with multiple input files')
for in_file in self.input_files:
files.append((in_file, self.FillTemplate(out_file, filename=in_file)))
elif len(self.input_files) == len(out_files):
for in_file, out_file in zip(self.input_files, out_files):
files.append((in_file, self.FillTemplate(out_file, filename=in_file)))
else:
raise IndexError('Equal number of input_files and output_names required')
return files
def FillTemplate(self, template_str, filename=''):
"""Return string based on given template."""
rep_dict = {'@BOARD@': self.board,
'@CHANNEL@': self.channel,
'@KEYSET@': self.keyset,
'@TYPE@': self.artifact_type,
'@VERSION@': self.version,
}
if filename:
basename = os.path.basename(filename)
rep_dict['@BASENAME@'] = basename
rep_dict['@ROOTNAME@'] = os.path.splitext(basename)[0]
return re.sub('@[A-Z]+@',
lambda x: rep_dict.get(x.group(0), x.group(0)),
template_str)
class BaseSigner(object):
"""Base Signer object."""
# Override the following lists to enforce key requirements
required_keys = ()
required_keys_public = ()
required_keys_private = ()
required_keyblocks = ()
def CheckKeyset(self, keyset):
"""Returns true if all required keys and keyblocks are in keyset."""
for k in self.required_keys:
if k not in keyset.keys:
return False
for k in self.required_keys_public:
if not keyset.KeyExists(k, require_public=True):
return False
for k in self.required_keys_private:
if not keyset.KeyExists(k, require_private=True):
return False
for kb in self.required_keyblocks:
if not keyset.KeyblockExists(kb):
return False
return True
def Sign(self, keyset, input_name, output_name):
"""Sign given input to output. Raises SigningFailedError on error"""
raise NotImplementedError
class FutilitySigner(BaseSigner):
"""Base class for signers that use futility command."""
def GetFutilityArgs(self, keyset, input_name, output_name):
"""Return list of arguments to use with futility."""
raise NotImplementedError
def Sign(self, keyset, input_name, output_name):
if self.CheckKeyset(keyset):
if not RunFutility(self.GetFutilityArgs(keyset, input_name, output_name)):
raise SigningFailedError('Signing Command Failed for ' + input_name)
def RunFutility(args):
"""Runs futility with the given args, returns True if success"""
cmd = ['futility']
cmd += args
return cros_build_lib.run(cmd, check=False).returncode == 0