[U-Boot] [verified-boot] Multiple levels of signing keys

Teddy Reed teddy.reed at gmail.com
Mon May 30 02:29:18 CEST 2016


On Mon, 2 May 2016 16:29:12 -0600
Simon Glass <sjg at chromium.org> wrote:

Hey Simon, sorry for the delayed response!

> Hi Teddy,
> 
> On 2 May 2016 at 02:57, Teddy Reed <teddy.reed at gmail.com> wrote:
> > On Sun, May 1, 2016 at 11:56 AM, Simon Glass <sjg at chromium.org> wrote:
> >> Hi Teddy,
> >>
> >> On 27 April 2016 at 11:32, Teddy Reed <teddy.reed at gmail.com> wrote:
> >>> Hello all,
> >>>
> >>> I'm looking to support "multiple levels" of keys within u-boot's
> >>> verified boot. I need something similar to UEFI's key enrollment key
> >>> (KEK) and db/dbx model such that I can support on-line signing of new
> >>> kernels/rootfs/configurations.
> >>>
> >>> To make this work we need a KEK that is not online (kept in a safe),
> >>> that can be used to sign expirations (revocations) of on-line signing
> >>> keys in the case of compromise or private key reveals. I know Chrome's
> >>> Coreboot verified boot model supports this, wondering if there's any
> >>> staged / WIP for u-boot?
> >>>
> >>> Off the top of my head I'd imagine this requires extending the FIT to
> >>> include sets of public keys and a blacklist of keys and expired or bad
> >>> kernel/rootfs/etc hashes. Then either extending the boot code to
> >>> inspect multiple FITs or extending mkimage to combine multiple sources
> >>> to amalgamate a FIT containing the PK-signed set of keys + hashes and
> >>> the on-line key-signed kernels/rootfs/configurations.
> >>>
> >>> P.S. This may be strongly linked to the need for a TPM to prevent
> >>> rollbacks. But as far as I can tell, the two features are distinct and
> >>> a TPM is not completely required for a multi-level key approach to
> >>> signing FITs.
> >>
> >> I don't know of anything in this area. Of course it is fairly easy to
> >> add new information to the FIT format, and mkimage is in somewhat
> >> better shape for modifying these days. If you do this, please do
> >> update fit_checksign.c to check an image.
> >>
> >
> > Ok sounds great! I'll work on a patch, something mimicking the
> > configuration hashing/signing for the existing top-level node name
> > "signature" (signature of a set of key-* subnodes). The most
> > challenging API changes will be the verification methods. Right now
> > they are very elegant in that they assume a single bundled FDT
> > containing keying material. For extensions to work, that will need to
> > become stateful and include a discovery phase.
> 
> Can you please explain that a bit? I'm not sure what you are referring to.
>

Sure! I decided to make a PoC to better familiarize myself with the
verified-boot diction so I could better explain what I'm working towards.
I'll include that below.

Here's what I would like to do:

1. Generate a key-store DTB that 'only' includes a signature and single
key node. I call this single node the ROM's key enrollment key, KEK.
This DTB is not signed, the key subnode has required=images set, and sets
the hint to something like "kek".

2. Generate one or many additional subordinate key-store DTBs that include
top-level signature nodes. Each of these DTBs may contain several keys. I
call them subordinate as they are responsible for signing images and confs.
We will sign these stores with the KEK. Consider the example below which
will be compiled to subordinate-store.dtb:

/dts-v1/;
/ {
    signature {
        key-subordinate {
            required = "image";
            algo = "sha256,rsa4096";
            rsa,r-squared = <...>;
            rsa,modulus = <...>;
            rsa,exponent = <0x0 0x10001>;
            rsa,n0-inverse = <0xf04eec87>;
            rsa,num-bits = <0x1000>;
            key-name-hint = "subordinate";
        };
    };
};

3. Write a DTS that contains subordinate keys in a new top-level-node
called "keys". These subnodes will be signed by the KEK. In theory this
signing will happen very rarely. Consider the example below:

/dts-v1/;
/ {
    keys {
        store at 1 {
            description = "Subordinate key store, only contains 1 key";
            data = /incbin/("/path/to/subordinate-store.dtb");
            hash at 1 {
                algo = "sha256";
            };
            signature at 1 {
                algo = "sha256,rsa4096";
                key-name-hint = "kek";
            };
        };
    };
};

Now that completes the "rare" steps. (1) happens when you build the ROM.
(2) and (3) as well or when new subordinate keys are required.

4. Create an amalgamation DTS that includes the signed subordinate key
store and the normal images and configuration data. Consider the example:

/dts-v1/;
/ {
    description = "U-Boot";
    #address-cells = <1>;
    images {
        firmware at 1 {
            description = "u-boot";
            data = /incbin/("/path/to/u-boot-nodtb.bin");
            type = "firmware";
            arch = "arm";
            os = "u-boot";
            compression = "none";
            hash at 1 {
                algo = "sha256";
            };
            signature at 1 {
                algo = "sha256,rsa4096";
                key-name-hint = "subordinate";
            };
        };
    };
    configurations {
        default = "conf at 1";
        conf at 1 {
            firmware = "firmware at 1";
        };
    };
    keys {
        store at 1 {
            no-resign;
            description = "Subordinate key store, only contains 1 key";
            data = [<FTD_BLOB>];
            hash at 1 {
                value = <HASH_VALUE_OF_FTD_BLOB>;
                algo = "sha256";
            };
            signature at 1 {
                timestamp = <0x574b6795>;
                signer-version = "";
                signer-name = "mkimage";
                value = <SIGNATURE_OF_FTD_BLOB_HASH>;
                algo = "sha256,rsa4096";
                key-name-hint = "kek";
            };
        };
    };
};

Imagine, you write this once, and seldom change the images or confs.
The keys will only be updated if the KEK is used to re-sign a subordinate
DTB. The process or obtaining this amalgamation is a bit 'manual'.

There is a new property "no-resign" that asks mkimage to skip attempting
to sign this subnode since the "kek" private key will not be found. ;)

5. Now, changes to the run-time image and configuration signature checks
are needed. At a very high level the gd_fdt_blob() contains the signature
node with the KEK. And the target FIT contains several FTDs within data
properties within the top-level "keys" node. When attempting to verify
an image or configuration, the "keys" node is checked for subnodes.
Each is treated as an image, a hash verification by the KEK is attempted,
upon success the data property can now be used as a sig_blob for checking
the target FIT's image(s) and configuration(s).

See below for the example PoC changes.

> >
> > This feature really only makes sense for an SPL, and the board I'm
> > supporting does not relocate the SPL (only a simple malloc and 8kB of
> > stack space), so it should be a good test case.
> >
> > The first round of implementation code might leave mkimage out of the
> > mix, and assume building the key extending DTB manually. But thanks
> > for pointing out fit_checksign as the appropriate test target-- will
> > do! Stay tuned.

Here's an example patch with the run-time changes needed to support using
subordinate key-stores for image and configuration checking. There are some
changes which will need very very careful inspection.

---
 common/image-fit.c   |  4 ++--
 common/image-sig.c   | 30 ++++++++++++++++--------------
 include/image.h      | 13 ++++++++++---
 lib/rsa/rsa-verify.c |  2 +-
 tools/image-host.c   | 23 +++++++++++++++++++++--
 5 files changed, 50 insertions(+), 22 deletions(-)

diff --git a/common/image-fit.c b/common/image-fit.c
index 25f8a11..54812f9 100644
--- a/common/image-fit.c
+++ b/common/image-fit.c
@@ -1041,8 +1041,8 @@ int fit_image_verify(const void *fit, int image_noffset)
 		} else if (IMAGE_ENABLE_VERIFY && verify_all &&
 				!strncmp(name, FIT_SIG_NODENAME,
 					strlen(FIT_SIG_NODENAME))) {
-			ret = fit_image_check_sig(fit, noffset, data,
-							size, -1, &err_msg);
+			ret = fit_image_check_sig(fit, noffset, data, size,
+						  gd_fdt_blob(), -1,&err_msg);
 
 			/*
 			 * Show an indication on failure, but do not return
diff --git a/common/image-sig.c b/common/image-sig.c
index eda5e13..b30bdb9 100644
--- a/common/image-sig.c
+++ b/common/image-sig.c
@@ -146,8 +146,8 @@ struct image_region *fit_region_make_list(const void *fit,
 }
 
 static int fit_image_setup_verify(struct image_sign_info *info,
-		const void *fit, int noffset, int required_keynode,
-		char **err_msgp)
+		const void *fit, int noffset, const void *sig_blob,
+		int required_keynode, char **err_msgp)
 {
 	char *algo_name;
 
@@ -160,7 +160,7 @@ static int fit_image_setup_verify(struct image_sign_info *info,
 	info->fit = (void *)fit;
 	info->node_offset = noffset;
 	info->algo = image_get_sig_algo(algo_name);
-	info->fdt_blob = gd_fdt_blob();
+	info->fdt_blob = sig_blob;
 	info->required_keynode = required_keynode;
 	printf("%s:%s", algo_name, info->keyname);
 
@@ -173,7 +173,8 @@ static int fit_image_setup_verify(struct image_sign_info *info,
 }
 
 int fit_image_check_sig(const void *fit, int noffset, const void *data,
-		size_t size, int required_keynode, char **err_msgp)
+		size_t size, const void *sig_blob, int required_keynode,
+		char **err_msgp)
 {
 	struct image_sign_info info;
 	struct image_region region;
@@ -181,8 +182,8 @@ int fit_image_check_sig(const void *fit, int noffset, const void *data,
 	int fit_value_len;
 
 	*err_msgp = NULL;
-	if (fit_image_setup_verify(&info, fit, noffset, required_keynode,
-				   err_msgp))
+	if (fit_image_setup_verify(&info, fit, noffset, sig_blob,
+				   required_keynode, err_msgp))
 		return -1;
 
 	if (fit_image_hash_get_value(fit, noffset, &fit_value,
@@ -218,7 +219,8 @@ static int fit_image_verify_sig(const void *fit, int image_noffset,
 		if (!strncmp(name, FIT_SIG_NODENAME,
 			     strlen(FIT_SIG_NODENAME))) {
 			ret = fit_image_check_sig(fit, noffset, data,
-							size, -1, &err_msg);
+						  size, sig_blob, sig_offset,
+						  &err_msg);
 			if (ret) {
 				puts("- ");
 			} else {
@@ -283,8 +285,8 @@ 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)
+int fit_config_check_sig(const void *fit, int noffset, const void *sig_blob,
+			 int required_keynode, char **err_msgp)
 {
 	char * const exc_prop[] = {"data"};
 	const char *prop, *end, *name;
@@ -299,10 +301,10 @@ int fit_config_check_sig(const void *fit, int noffset, int required_keynode,
 
 	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));
+	      fit_get_name(sig_blob, required_keynode, NULL));
 	*err_msgp = NULL;
-	if (fit_image_setup_verify(&info, fit, noffset, required_keynode,
-				   err_msgp))
+	if (fit_image_setup_verify(&info, fit, noffset, sig_blob,
+				   required_keynode, err_msgp))
 		return -1;
 
 	if (fit_image_hash_get_value(fit, noffset, &fit_value,
@@ -398,8 +400,8 @@ static int fit_config_verify_sig(const void *fit, int conf_noffset,
 
 		if (!strncmp(name, FIT_SIG_NODENAME,
 			     strlen(FIT_SIG_NODENAME))) {
-			ret = fit_config_check_sig(fit, noffset, sig_offset,
-						   &err_msg);
+			ret = fit_config_check_sig(fit, noffset, sig_blob,
+						   sig_offset, &err_msg);
 			if (ret) {
 				puts("- ");
 			} else {
diff --git a/include/image.h b/include/image.h
index f9ee564..65eb79a 100644
--- a/include/image.h
+++ b/include/image.h
@@ -783,6 +783,7 @@ int bootz_setup(ulong image, ulong *start, ulong *end);
 
 #define FIT_IMAGES_PATH		"/images"
 #define FIT_CONFS_PATH		"/configurations"
+#define FIT_KEYS_PATH		"/keys"
 
 /* hash/signature node */
 #define FIT_HASH_NODENAME	"hash"
@@ -810,6 +811,10 @@ int bootz_setup(ulong image, ulong *start, ulong *end);
 #define FIT_DEFAULT_PROP	"default"
 #define FIT_SETUP_PROP		"setup"
 
+/* key node */
+#define FIT_NOSIGN_PROP		"no-resign"
+
 #define FIT_MAX_HASH_LEN	HASH_MAX_DIGEST_SIZE
 
 #if IMAGE_ENABLE_FIT
@@ -1090,8 +1095,9 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
  * @noffset:		Offset of signature node to check
  * @data:		Image data to check
  * @size:		Size of image data
- * @required_keynode:	Offset in the control FDT of the required key node,
- *			if any. If this is given, then the image wil not
+ * @sig_blob:		FDT to use as the key store
+ * @required_keynode:	Offset in the key store of the required key node,
+ *			if any. If this is given, then the image will not
  *			pass verification unless that key is used. If this is
  *			-1 then any signature will do.
  * @err_msgp:		In the event of an error, this will be pointed to a
@@ -1099,7 +1105,8 @@ int fit_image_verify_required_sigs(const void *fit, int image_noffset,
  * @return 0 if all verified ok, <0 on error
  */
 int fit_image_check_sig(const void *fit, int noffset, const void *data,
-		size_t size, int required_keynode, char **err_msgp);
+		size_t size, const void *sig_blob, int required_keynode,
+		char **err_msgp);
 
 /**
  * fit_region_make_list() - Make a list of regions to hash
diff --git a/lib/rsa/rsa-verify.c b/lib/rsa/rsa-verify.c
index 60126d2..cbd01be 100644
--- a/lib/rsa/rsa-verify.c
+++ b/lib/rsa/rsa-verify.c
@@ -56,7 +56,7 @@ static int rsa_verify_key(struct key_prop *prop, const uint8_t *sig,
 		return -EINVAL;
 	}
 
-	debug("Checksum algorithm: %s", algo->name);
+	debug("Checksum algorithm: %s\n", algo->name);
 
 	/* Sanity check for stack size */
 	if (sig_len > RSA_MAX_SIG_BITS / 8) {
diff --git a/tools/image-host.c b/tools/image-host.c
index 7effb6c..cdfa6e8 100644
--- a/tools/image-host.c
+++ b/tools/image-host.c
@@ -655,7 +655,7 @@ static int fit_config_add_verification_data(const char *keydir, void *keydest,
 int fit_add_verification_data(const char *keydir, void *keydest, void *fit,
 			      const char *comment, int require_keys)
 {
-	int images_noffset, confs_noffset;
+	int images_noffset, confs_noffset, keys_noffset;
 	int noffset;
 	int ret;
 
@@ -685,10 +685,29 @@ int fit_add_verification_data(const char *keydir, void *keydest, void *fit,
 	if (!IMAGE_ENABLE_SIGN || !keydir)
 		return 0;
 
+	/**
+	 * Inputs may request key signing.
+	 * Signed keys are intended to extend the keystore used to verify
+	 * images and configurations. They work similarly to images
+	 * in that the key's data property includes the signed content.
+	 */
+	keys_noffset = fdt_path_offset(fit, FIT_KEYS_PATH);
+	for (noffset = fdt_first_subnode(fit, keys_noffset);
+	     noffset >= 0;
+	     noffset = fdt_next_subnode(fit, noffset)) {
+		if (fdt_getprop(fit, noffset, FIT_NOSIGN_PROP, NULL) != NULL) {
+			/* Resigning keys should be cowardly avoided. */
+			continue;
+		}
+		ret = fit_image_add_verification_data(keydir, keydest,
+						      fit, noffset, comment,
+						      require_keys);
+	}
+
 	/* Find configurations parent node offset */
 	confs_noffset = fdt_path_offset(fit, FIT_CONFS_PATH);
 	if (confs_noffset < 0) {
-		printf("Can't find images parent node '%s' (%s)\n",
+		printf("Can't find configurations parent node '%s' (%s)\n",
 		       FIT_CONFS_PATH, fdt_strerror(confs_noffset));
 		return -ENOENT;
 	}
-- 

Finally, here is some pseudo code implementation:

        const void *fit = somehow_find_your_fit();
	const void *signature_store = gd_fdt_blob();
	if (signature_store == NULL) {
		/* It is possible the spl_init method did not find a fdt. */
		printf("No signature store was included in the SPL.\n");
		hang();
	}

	/* Node path to subordinate keys. */
	int keys = fdt_path_offset(fit, FIT_KEYS_PATH);
	if (keys < 0) {
		debug("%s: Cannot find /keys node: %d\n", __func__, keys);
		hang();
	}

	/* Be simple, select the first key. */
	int key_node = fdt_first_subnode(fit, keys);
	int subordinate_verified = 0;

	/* Now verify subordinates as images with inline "data". */
	const void* key_data;
	size_t key_data_size;
	if (fit_image_get_data(fit, key_node, &key_data, &key_data_size)) {
		printf("Unable to find subordinate key data.\n");
		hang();
	}

	if (fit_image_verify_required_sigs(fit, key_node, key_data,
					   key_data_size, signature_store,
					   &subordinate_verified)) {
		printf("Unable to verify required subordinate signature.\n");
		hang();
	}

	if (subordinate_verified != 0) {
		printf("No subordinate keys were verified.\n");
		hang();
	}

	/* Now change the signature store to use the verified subordinate. */
	signature_store = key_data;

	/*
	 * Check that at least 1 image was verified.
	 * This is an interesting error state communication, but it is the API
	 * given, so let's make it as clear as possible.
	 */
	int verified = 0;

	/* Verify all required signatures, keys must be marked required. */
	if (fit_image_verify_required_sigs(fit, node, data, data_size,
					   signature_store, &verified)) {
		printf("Unable to verify required signature.\n");
		hang();
	}

	if (verified != 0) {
		/* When verified is 0, then an image was verified. */
		printf("No images were verified.\n");
		hang();
	}

	printf("Images verified\n");

Sorry for the long mail.

For now I'll continue working on the flexibility of the implementation.
- Multiple subordinate key stores.
- Code to assemble the amalgamation DTS.
- Error-state checking and invalid signature state verification.

>From there we'll need rollback protection and key/hash blacklisting. :D

Thanks and take care!
-Teddy

-- 
Teddy Reed <teddy.reed at gmail.com>


More information about the U-Boot mailing list