Hi Rasmus, On Fri, 16 May 2025 at 14:54, Rasmus Villemoes <r...@prevas.dk> wrote: > > Background: > > I have several customers that will be using a certain remote signing > service for signing their images, in order that the private keys are > never exposed outside that company's secure servers. This is done via > a pkcs#11 interface that talks to the remote signing server, and all > of that works quite well. > > However, the way this particular signing service works is that one > must upfront create a "signing session", where one indicates which > keys one will use and, importantly, how many times each key will (may) > be used. Then, depending on the keys requested and the customer's > configuration, one or more humans must authorize that signing session > So for example, if official release keys are to be used, maybe two > different people from upper management must authorize, while if > development keys are requested, the developer himself can authorize > the session. > > Once authorized, the requester receives a token that must then be used > for signing via one of the keys associated to that session. > > I have that integrated in Yocto in a way that when a CI starts a BSP > build, it automatically works out which keys will be needed (e.g. one > for signing U-Boot, another for signing a kernel FIT image) based on > bitbake metadata, requests an appropriate signing session, and the > appropriate people are then notified and can then look at the details > of that CI pipeline and confirm that it is legitimate. > > The problem: > > The way mkimage does FIT image signing means that the remote server > can be asked to perform a signature an unbounded number of times, or > at least a number of times that cannot be determined upfront. This > means that currently, I need to artificially say that a kernel key > will be used, say, 10 times, even when only a single FIT image with > just one configuration node is created. > > Part of the security model is that once the number of signings using a > given key has been depleted, the authorization token becomes useless > even if somehow leaked from the CI - and _if_ it is leaked/compromised > and abused before the CI has gotten around to do its signings, the > build will then fail with a clear indication of the > compromise. Clearly, having to specify a "high enough" expected use > count is counter to that part of the security model, because it will > inevitably leave some allowed uses behind. > > While not perfect, we can give a reasonable estimate of an upper bound > on the necessary extra size by simply counting the number of hash and > signature nodes in the FIT image. > > As indicated in the comments, one could probably make it even more > precise, and if there would ever be signatures larger than 512 bytes, > probably one would have to do that. But this works well enough in > practice for now, and is in fact an improvement in the normal case: > Currently, starting with size_inc of 0 is guaranteed to fail, so we > always enter the loop at least twice, even when not doing any signing > but merely filling hash values. > > Just in case I've missed anything, keep the loop incrementing 1024 > bytes at a time, and also, in case the estimate turns out to be over > 64K, ensure that we do at least one attempt by changing to a do-while > loop. > > With a little debug printf, creating a FIT image with three > configuration nodes previously resulted in > > Trying size_inc=0 > Trying size_inc=1024 > Trying size_inc=2048 > Trying size_inc=3072 > Succeeded at size_inc=3072 > > and dumping info from the signing session (where I've artifically > asked for 10 uses of the kernel key) shows > > "keyid": "kernel-dev-20250218", > "usagecount": 9, > "maxusagecount": 10 > > corresponding to 1+2+3+3 signatures requested (so while the loop count > is roughly linear in the number of config nodes, the number of > signings is quadratic). > > With this, I instead get > > Trying size_inc=3456 > Succeeded at size_inc=3456 > > and the expected > > "keyid": "kernel-dev-20250218", > "usagecount": 3, > "maxusagecount": 10 > > thus allowing me to set maxusagecount correctly. > > Signed-off-by: Rasmus Villemoes <r...@prevas.dk> > --- > tools/fit_image.c | 80 +++++++++++++++++++++++++++++++++++++++++------ > 1 file changed, 70 insertions(+), 10 deletions(-) > > diff --git a/tools/fit_image.c b/tools/fit_image.c > index caed8d5f901..65c83e0b950 100644 > --- a/tools/fit_image.c > +++ b/tools/fit_image.c > @@ -24,6 +24,65 @@ > > static struct legacy_img_hdr header; > > +static int fit_estimate_hash_sig_size(struct image_tool_params *params, > const char *fname) > +{ > + bool signing = IMAGE_ENABLE_SIGN && (params->keydir || > params->keyfile); > + struct stat sbuf; > + void *fdt; > + int fd; > + int estimate = 0; > + int depth, noffset; > + const char *name; > + > + fd = mmap_fdt(params->cmdname, fname, 0, &fdt, &sbuf, false, true); > + if (fd < 0) > + return -EIO; > + > + /* > + * Walk the FIT image, looking for nodes named hash* and > + * signature*. Since the interesting nodes are subnodes of an > + * image or configuration node, we are only interested in > + * those at depth exactly 3. > + * > + * The estimate for a hash node is based on a sha512 digest > + * being 64 bytes, with another 64 bytes added to account for > + * fdt structure overhead (the tags and the name of the > + * "value" property). > + * > + * The estimate for a signature node is based on an rsa4096 > + * signature being 512 bytes, with another 512 bytes to > + * account for fdt overhead and the various other properties > + * (hashed-nodes etc.) that will also be filled in. > + * > + * One could try to be more precise in the estimates by > + * looking at the "algo" property and, in the case of > + * configuration signatures, the sign-images property. Also, > + * when signing an already created FIT image, the hash nodes > + * already have properly sized value properties, so one could > + * also take pre-existence of "value" properties in hash nodes > + * into account. But this rather simple approach should work > + * well enough in practice. > + */ > + for (depth = 0, noffset = fdt_next_node(fdt, 0, &depth); > + noffset >= 0 && depth > 0; > + noffset = fdt_next_node(fdt, noffset, &depth)) { > + if (depth != 3) > + continue; > + > + name = fdt_get_name(fdt, noffset, NULL); > + if (!strncmp(name, FIT_HASH_NODENAME, > strlen(FIT_HASH_NODENAME))) > + estimate += 128; > + > + if (signing && !strncmp(name, FIT_SIG_NODENAME, > strlen(FIT_SIG_NODENAME))) > + estimate += 1024; > + } > + > + munmap(fdt, sbuf.st_size); > + close(fd); > + > + return estimate; > +} > + > static int fit_add_file_data(struct image_tool_params *params, size_t > size_inc, > const char *tmpfile) > { > @@ -806,16 +865,16 @@ static int fit_handle_file(struct image_tool_params > *params) > rename(tmpfile, bakfile); > > /* > - * Set hashes for images in the blob. Unfortunately we may need more > - * space in either FDT, so keep trying until we succeed. > - * > - * Note: this is pretty inefficient for signing, since we must > - * calculate the signature every time. It would be better to calculate > - * all the data and then store it in a separate step. However, this > - * would be considerably more complex to implement. Generally a few > - * steps of this loop is enough to sign with several keys. > + * Set hashes for images in the blob and compute > + * signatures. We do an attempt at estimating the expected > + * extra size, but just in case that is not sufficient, keep > + * trying adding 1K, with a reasonable upper bound of 64K > + * total, until we succeed. > */ > - for (size_inc = 0; size_inc < 64 * 1024; size_inc += 1024) { > + size_inc = fit_estimate_hash_sig_size(params, bakfile); > + if (size_inc < 0) > + goto err_system; > + do { > if (copyfile(bakfile, tmpfile) < 0) { > printf("Can't copy %s to %s\n", bakfile, tmpfile); > ret = -EIO; > @@ -824,7 +883,8 @@ static int fit_handle_file(struct image_tool_params > *params) > ret = fit_add_file_data(params, size_inc, tmpfile); > if (!ret || ret != -ENOSPC) > break; > - } > + size_inc += 1024; > + } while (size_inc < 64 * 1024); > > if (ret) { > fprintf(stderr, "%s Can't add hashes to FIT blob: %d\n", > -- > 2.49.0 >
I have no particular objection here if you really want to go this way, but it seems to me that it would be better to have a way to determine the size of signatures using a new (or existing?) API call. Then you can make this deterministic and always correct. Regards, Simon