Commit 4d098529 authored by Simon Glass's avatar Simon Glass Committed by Tom Rini
Browse files

image: Add support for signing of FIT configurations



While signing images is useful, it does not provide complete protection
against several types of attack. For example, it it possible to create a
FIT with the same signed images, but with the configuration changed such
that a different one is selected (mix and match attack). It is also possible
to substitute a signed image from an older FIT version into a newer FIT
(roll-back attack).

Add support for signing of FIT configurations using the libfdt's region
support.

Please see doc/uImage.FIT/signature.txt for more information.
Signed-off-by: default avatarSimon Glass <sjg@chromium.org>
parent 3e06cd1f
......@@ -25,10 +25,11 @@
#include <malloc.h>
DECLARE_GLOBAL_DATA_PTR;
#endif /* !USE_HOSTCC*/
#include <errno.h>
#include <image.h>
#include <rsa.h>
#define IMAGE_MAX_HASHED_NODES 100
struct image_sig_algo image_sig_algos[] = {
{
"sha1,rsa2048",
......@@ -50,6 +51,50 @@ struct image_sig_algo *image_get_sig_algo(const char *name)
return NULL;
}
/**
* fit_region_make_list() - Make a list of image regions
*
* Given a list of fdt_regions, create a list of image_regions. This is a
* simple conversion routine since the FDT and image code use different
* structures.
*
* @fit: FIT image
* @fdt_regions: Pointer to FDT regions
* @count: Number of FDT regions
* @region: Pointer to image regions, which must hold @count records. If
* region is NULL, then (except for an SPL build) the array will be
* allocated.
* @return: Pointer to image regions
*/
struct image_region *fit_region_make_list(const void *fit,
struct fdt_region *fdt_regions, int count,
struct image_region *region)
{
int i;
debug("Hash regions:\n");
debug("%10s %10s\n", "Offset", "Size");
/*
* Use malloc() except in SPL (to save code size). In SPL the caller
* must allocate the array.
*/
#ifndef CONFIG_SPL_BUILD
if (!region)
region = calloc(sizeof(*region), count);
#endif
if (!region)
return NULL;
for (i = 0; i < count; i++) {
debug("%10x %10x\n", fdt_regions[i].offset,
fdt_regions[i].size);
region[i].data = fit + fdt_regions[i].offset;
region[i].size = fdt_regions[i].size;
}
return region;
}
static int fit_image_setup_verify(struct image_sign_info *info,
const void *fit, int noffset, int required_keynode,
char **err_msgp)
......@@ -191,3 +236,187 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
return 0;
}
int fit_config_check_sig(const void *fit, int noffset, int required_keynode,
char **err_msgp)
{
char * const exc_prop[] = {"data"};
const char *prop, *end, *name;
struct image_sign_info info;
const uint32_t *strings;
uint8_t *fit_value;
int fit_value_len;
int max_regions;
int i, prop_len;
char path[200];
int count;
debug("%s: fdt=%p, conf='%s', sig='%s'\n", __func__, gd_fdt_blob(),
fit_get_name(fit, noffset, NULL),
fit_get_name(gd_fdt_blob(), required_keynode, NULL));
*err_msgp = NULL;
if (fit_image_setup_verify(&info, fit, noffset, required_keynode,
err_msgp))
return -1;
if (fit_image_hash_get_value(fit, noffset, &fit_value,
&fit_value_len)) {
*err_msgp = "Can't get hash value property";
return -1;
}
/* Count the number of strings in the property */
prop = fdt_getprop(fit, noffset, "hashed-nodes", &prop_len);
end = prop ? prop + prop_len : prop;
for (name = prop, count = 0; name < end; name++)
if (!*name)
count++;
if (!count) {
*err_msgp = "Can't get hashed-nodes property";
return -1;
}
/* Add a sanity check here since we are using the stack */
if (count > IMAGE_MAX_HASHED_NODES) {
*err_msgp = "Number of hashed nodes exceeds maximum";
return -1;
}
/* Create a list of node names from those strings */
char *node_inc[count];
debug("Hash nodes (%d):\n", count);
for (name = prop, i = 0; name < end; name += strlen(name) + 1, i++) {
debug(" '%s'\n", name);
node_inc[i] = (char *)name;
}
/*
* Each node can generate one region for each sub-node. Allow for
* 7 sub-nodes (hash@1, signature@1, etc.) and some extra.
*/
max_regions = 20 + count * 7;
struct fdt_region fdt_regions[max_regions];
/* Get a list of regions to hash */
count = fdt_find_regions(fit, node_inc, count,
exc_prop, ARRAY_SIZE(exc_prop),
fdt_regions, max_regions - 1,
path, sizeof(path), 0);
if (count < 0) {
*err_msgp = "Failed to hash configuration";
return -1;
}
if (count == 0) {
*err_msgp = "No data to hash";
return -1;
}
if (count >= max_regions - 1) {
*err_msgp = "Too many hash regions";
return -1;
}
/* Add the strings */
strings = fdt_getprop(fit, noffset, "hashed-strings", NULL);
if (strings) {
fdt_regions[count].offset = fdt_off_dt_strings(fit) +
fdt32_to_cpu(strings[0]);
fdt_regions[count].size = fdt32_to_cpu(strings[1]);
count++;
}
/* Allocate the region list on the stack */
struct image_region region[count];
fit_region_make_list(fit, fdt_regions, count, region);
if (info.algo->verify(&info, region, count, fit_value,
fit_value_len)) {
*err_msgp = "Verification failed";
return -1;
}
return 0;
}
static int fit_config_verify_sig(const void *fit, int conf_noffset,
const void *sig_blob, int sig_offset)
{
int noffset;
char *err_msg = "";
int verified = 0;
int ret;
/* Process all hash subnodes of the component conf node */
for (noffset = fdt_first_subnode(fit, conf_noffset);
noffset >= 0;
noffset = fdt_next_subnode(fit, noffset)) {
const char *name = fit_get_name(fit, noffset, NULL);
if (!strncmp(name, FIT_SIG_NODENAME,
strlen(FIT_SIG_NODENAME))) {
ret = fit_config_check_sig(fit, noffset, sig_offset,
&err_msg);
if (ret) {
puts("- ");
} else {
puts("+ ");
verified = 1;
break;
}
}
}
if (noffset == -FDT_ERR_TRUNCATED || noffset == -FDT_ERR_BADSTRUCTURE) {
err_msg = "Corrupted or truncated tree";
goto error;
}
return verified ? 0 : -EPERM;
error:
printf(" error!\n%s for '%s' hash node in '%s' config node\n",
err_msg, fit_get_name(fit, noffset, NULL),
fit_get_name(fit, conf_noffset, NULL));
return -1;
}
int fit_config_verify_required_sigs(const void *fit, int conf_noffset,
const void *sig_blob)
{
int noffset;
int sig_node;
/* Work out what we need to verify */
sig_node = fdt_subnode_offset(sig_blob, 0, FIT_SIG_NODENAME);
if (sig_node < 0) {
debug("%s: No signature node found: %s\n", __func__,
fdt_strerror(sig_node));
return 0;
}
for (noffset = fdt_first_subnode(sig_blob, sig_node);
noffset >= 0;
noffset = fdt_next_subnode(sig_blob, noffset)) {
const char *required;
int ret;
required = fdt_getprop(sig_blob, noffset, "required", NULL);
if (!required || strcmp(required, "conf"))
continue;
ret = fit_config_verify_sig(fit, conf_noffset, sig_blob,
noffset);
if (ret) {
printf("Failed to verify required signature '%s'\n",
fit_get_name(sig_blob, noffset, NULL));
return ret;
}
}
return 0;
}
int fit_config_verify(const void *fit, int conf_noffset)
{
return !fit_config_verify_required_sigs(fit, conf_noffset,
gd_fdt_blob());
}
/dts-v1/;
/ {
description = "Chrome OS kernel image with one or more FDT blobs";
#address-cells = <1>;
images {
kernel@1 {
data = /incbin/("test-kernel.bin");
type = "kernel_noload";
arch = "sandbox";
os = "linux";
compression = "lzo";
load = <0x4>;
entry = <0x8>;
kernel-version = <1>;
hash@1 {
algo = "sha1";
};
};
fdt@1 {
description = "snow";
data = /incbin/("sandbox-kernel.dtb");
type = "flat_dt";
arch = "sandbox";
compression = "none";
fdt-version = <1>;
hash@1 {
algo = "sha1";
};
};
};
configurations {
default = "conf@1";
conf@1 {
kernel = "kernel@1";
fdt = "fdt@1";
signature@1 {
algo = "sha1,rsa2048";
key-name-hint = "dev";
sign-images = "fdt", "kernel";
};
};
};
};
......@@ -105,8 +105,27 @@ When the image is signed, the following properties are optional:
- comment: Additional information about the signer or image
For config bindings (see Signed Configurations below), the following
additional properties are optional:
Example: See sign-images.its for an example image tree source file.
- sign-images: A list of images to sign, each being a property of the conf
node that contains then. The default is "kernel,fdt" which means that these
two images will be looked up in the config and signed if present.
For config bindings, these properties are added by the signer:
- hashed-nodes: A list of nodes which were hashed by the signer. Each is
a string - the full path to node. A typical value might be:
hashed-nodes = "/", "/configurations/conf@1", "/images/kernel@1",
"/images/kernel@1/hash@1", "/images/fdt@1",
"/images/fdt@1/hash@1";
- hashed-strings: The start and size of the string region of the FIT that
was hashed
Example: See sign-images.its for an example image tree source file and
sign-configs.its for config signing.
Public Key Storage
......@@ -144,6 +163,153 @@ For RSA the following are mandatory:
- rsa,n0-inverse: -1 / modulus[0] mod 2^32
Signed Configurations
---------------------
While signing images is useful, it does not provide complete protection
against several types of attack. For example, it it possible to create a
FIT with the same signed images, but with the configuration changed such
that a different one is selected (mix and match attack). It is also possible
to substitute a signed image from an older FIT version into a newer FIT
(roll-back attack).
As an example, consider this FIT:
/ {
images {
kernel@1 {
data = <data for kernel1>
signature@1 {
algo = "sha1,rsa2048";
value = <...kernel signature 1...>
};
};
kernel@2 {
data = <data for kernel2>
signature@1 {
algo = "sha1,rsa2048";
value = <...kernel signature 2...>
};
};
fdt@1 {
data = <data for fdt1>;
signature@1 {
algo = "sha1,rsa2048";
vaue = <...fdt signature 1...>
};
};
fdt@2 {
data = <data for fdt2>;
signature@1 {
algo = "sha1,rsa2048";
vaue = <...fdt signature 2...>
};
};
};
configurations {
default = "conf@1";
conf@1 {
kernel = "kernel@1";
fdt = "fdt@1";
};
conf@1 {
kernel = "kernel@2";
fdt = "fdt@2";
};
};
};
Since both kernels are signed it is easy for an attacker to add a new
configuration 3 with kernel 1 and fdt 2:
configurations {
default = "conf@1";
conf@1 {
kernel = "kernel@1";
fdt = "fdt@1";
};
conf@1 {
kernel = "kernel@2";
fdt = "fdt@2";
};
conf@3 {
kernel = "kernel@1";
fdt = "fdt@2";
};
};
With signed images, nothing protects against this. Whether it gains an
advantage for the attacker is debatable, but it is not secure.
To solved this problem, we support signed configurations. In this case it
is the configurations that are signed, not the image. Each image has its
own hash, and we include the hash in the configuration signature.
So the above example is adjusted to look like this:
/ {
images {
kernel@1 {
data = <data for kernel1>
hash@1 {
algo = "sha1";
value = <...kernel hash 1...>
};
};
kernel@2 {
data = <data for kernel2>
hash@1 {
algo = "sha1";
value = <...kernel hash 2...>
};
};
fdt@1 {
data = <data for fdt1>;
hash@1 {
algo = "sha1";
value = <...fdt hash 1...>
};
};
fdt@2 {
data = <data for fdt2>;
hash@1 {
algo = "sha1";
value = <...fdt hash 2...>
};
};
};
configurations {
default = "conf@1";
conf@1 {
kernel = "kernel@1";
fdt = "fdt@1";
signature@1 {
algo = "sha1,rsa2048";
value = <...conf 1 signature...>;
};
};
conf@2 {
kernel = "kernel@2";
fdt = "fdt@2";
signature@1 {
algo = "sha1,rsa2048";
value = <...conf 1 signature...>;
};
};
};
};
You can see that we have added hashes for all images (since they are no
longer signed), and a signature to each configuration. In the above example,
mkimage will sign configurations/conf@1, the kernel and fdt that are
pointed to by the configuration (/images/kernel@1, /images/kernel@1/hash@1,
/images/fdt@1, /images/fdt@1/hash@1) and the root structure of the image
(so that it isn't possible to add or remove root nodes). The signature is
written into /configurations/conf@1/signature@1/value. It can easily be
verified later even if the FIT has been signed with other keys in the
meantime.
Verification
------------
FITs are verified when loaded. After the configuration is selected a list
......
......@@ -964,6 +964,22 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
int fit_image_check_sig(const void *fit, int noffset, const void *data,
size_t size, int required_keynode, char **err_msgp);
/**
* fit_region_make_list() - Make a list of regions to hash
*
* Given a list of FIT regions (offset, size) provided by libfdt, create
* a list of regions (void *, size) for use by the signature creationg
* and verification code.
*
* @fit: FIT image to process
* @fdt_regions: Regions as returned by libfdt
* @count: Number of regions returned by libfdt
* @region: Place to put list of regions (NULL to allocate it)
* @return pointer to list of regions, or NULL if out of memory
*/
struct image_region *fit_region_make_list(const void *fit,
struct fdt_region *fdt_regions, int count,
struct image_region *region);
static inline int fit_image_check_target_arch(const void *fdt, int node)
{
......
......@@ -341,10 +341,326 @@ int fit_image_add_verification_data(const char *keydir, void *keydest,
return 0;
}
struct strlist {
int count;
char **strings;
};
static void strlist_init(struct strlist *list)
{
memset(list, '\0', sizeof(*list));
}
static void strlist_free(struct strlist *list)
{
int i;
for (i = 0; i < list->count; i++)
free(list->strings[i]);
free(list->strings);
}
static int strlist_add(struct strlist *list, const char *str)
{
char *dup;
dup = strdup(str);
list->strings = realloc(list->strings,
(list->count + 1) * sizeof(char *));
if (!list || !str)
return -1;
list->strings[list->count++] = dup;
return 0;
}
static const char *fit_config_get_image_list(void *fit, int noffset,
int *lenp, int *allow_missingp)
{
static const char default_list[] = FIT_KERNEL_PROP "\0"
FIT_FDT_PROP;
const char *prop;
/* If there is an "image" property, use that */
prop = fdt_getprop(fit, noffset, "sign-images", lenp);
if (prop) {
*allow_missingp = 0;
return *lenp ? prop : NULL;
}
/* Default image list */
*allow_missingp = 1;
*lenp = sizeof(default_list);
return default_list;
}
static int fit_config_get_hash_list(void *fit, int conf_noffset,
int sig_offset, struct strlist *node_inc)
{
int allow_missing;
const char *prop, *iname, *end;
const char *conf_name, *sig_name;
char name[200], path[200];
int image_count;
int ret, len;
conf_name = fit_get_name(fit, conf_noffset, NULL);
sig_name = fit_get_name(fit, sig_offset, NULL);
/*
* Build a list of nodes we need to hash. We always need the root
* node and the configuration.
*/
strlist_init(node_inc);
snprintf(name, sizeof(name), "%s/%s", FIT_CONFS_PATH, conf_name);
if (strlist_add(node_inc, "/") ||
strlist_add(node_inc, name))
goto err_mem;
/* Get a list of images that we intend to sign */
prop = fit_config_get_image_list(fit, conf_noffset, &len,
&allow_missing);
if (!prop)
return 0;
/* Locate the images */
end = prop + len;
image_count = 0;
for (iname = prop; iname < end; iname += strlen(iname) + 1) {
int noffset;
int image_noffset;
int hash_count;
image_noffset = fit_conf_get_prop_node(fit, conf_noffset,
iname);
if (image_noffset < 0) {
printf("Failed to find image '%s' in configuration '%s/%s'\n",
iname, conf_name, sig_name);
if (allow_missing)
continue;
return -ENOENT;
}
ret = fdt_get_path(fit, image_noffset, path, sizeof(path));
if (ret < 0)
goto err_path;
if (strlist_add(node_inc, path))
goto err_mem;
snprintf(name, sizeof(name), "%s/%s", FIT_CONFS_PATH,
conf_name);
/* Add all this image's hashes */
hash_count = 0;
for (noffset = fdt_first_subnode(fit, image_noffset);
noffset >= 0;
noffset = fdt_next_subnode(fit, noffset)) {
const char *name = fit_get_name(fit, noffset, NULL);
if (strncmp(name, FIT_HASH_NODENAME,
strlen(FIT_HASH_NODENAME)))
continue;
ret = fdt_get_path(fit, noffset, path, sizeof(path));
if (ret < 0)
goto err_path;
if (strlist_add(node_inc, path))