fwtool: add utility for appending and extracting firmware metadata/signatures
authorFelix Fietkau <nbd@nbd.name>
Mon, 14 Nov 2016 15:02:46 +0000 (16:02 +0100)
committerFelix Fietkau <nbd@nbd.name>
Sat, 19 Nov 2016 10:24:09 +0000 (11:24 +0100)
This will be used to append extra information to images which allows the
system to verify if an image is compatible with the system.

The extra data is appended to the end of the image, where it will be
ignored when upgrading from systems that do not process this data yet:

If the image is a squashfs or jffs2 image, the extra data will land
after the end-of-filesystem marker, where it will be overwritten once
the system boots for the first timee.

If the image is a sysupgrade tar file, tar will simply ignore the extra
data when unpacking.

The layout of the metadata/signature chunks is constructed in a way
that the last part contains just a magic and size information, so that
the tool can quickly check if any valid data is present without having
to do a pattern search throughout the full image.

Chunks also contain CRC32 information to detect file corruption, even
when the image is not signed.

Signed-off-by: Felix Fietkau <nbd@nbd.name>
Makefile [new file with mode: 0644]
src/crc32.h [new file with mode: 0644]
src/fwimage.h [new file with mode: 0644]
src/fwtool.c [new file with mode: 0644]
src/utils.h [new file with mode: 0644]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..901081c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,47 @@
+#
+# Copyright (C) Felix Fietkau <nbd@nbd.name>
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=fwtool
+PKG_RELEASE:=1
+
+PKG_FLAGS:=nonshared
+
+PKG_MAINTAINER := Felix Fietkau <nbd@nbd.name>
+PKG_BUILD_DEPENDS := fwtool/host
+
+include $(INCLUDE_DIR)/package.mk
+include $(INCLUDE_DIR)/host-build.mk
+
+HOST_BUILD_PREFIX:=$(STAGING_DIR_HOST)
+
+define Package/fwtool
+  SECTION:=utils
+  CATEGORY:=Base system
+  TITLE:=Utility for appending and extracting firmware metadata and signatures
+endef
+
+define Host/Compile
+       $(HOSTCC) $(HOST_CFLAGS) $(HOST_LDFLAGS) -o $(HOST_BUILD_DIR)/fwtool ./src/fwtool.c
+endef
+
+define Host/Install
+       $(INSTALL_BIN) $(HOST_BUILD_DIR)/fwtool $(1)/bin/
+endef
+
+define Build/Compile
+       $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) -o $(PKG_BUILD_DIR)/fwtool ./src/fwtool.c
+endef
+
+define Package/fwtool/install
+       $(INSTALL_DIR) $(1)/usr/bin
+       $(INSTALL_BIN) $(PKG_BUILD_DIR)/fwtool $(1)/usr/bin/
+endef
+
+$(eval $(call HostBuild))
+$(eval $(call BuildPackage,fwtool))
diff --git a/src/crc32.h b/src/crc32.h
new file mode 100644 (file)
index 0000000..022c69f
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2016 Felix Fietkau <nbd@nbd.name>
+ *
+ * Based on busybox code:
+ *   CRC32 table fill function
+ *   Copyright (C) 2006 by Rob Sullivan <cogito.ergo.cogito@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+#ifndef __BB_CRC32_H
+#define __BB_CRC32_H
+
+static inline void
+crc32_filltable(uint32_t *crc_table)
+{
+       uint32_t polynomial = 0xedb88320;
+       uint32_t c;
+       int i, j;
+
+       for (i = 0; i < 256; i++) {
+               c = i;
+               for (j = 8; j; j--)
+                       c = (c&1) ? ((c >> 1) ^ polynomial) : (c >> 1);
+
+               *crc_table++ = c;
+       }
+}
+
+static inline uint32_t
+crc32_block(uint32_t val, const void *buf, unsigned len, uint32_t *crc_table)
+{
+       const void *end = (uint8_t*)buf + len;
+
+       while (buf != end) {
+               val = crc_table[(uint8_t)val ^ *(uint8_t*)buf] ^ (val >> 8);
+               buf = (uint8_t*)buf + 1;
+       }
+       return val;
+}
+
+#endif
diff --git a/src/fwimage.h b/src/fwimage.h
new file mode 100644 (file)
index 0000000..52dcfb1
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 Felix Fietkau <nbd@nbd.name>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+#ifndef __FWIMAGE_H
+#define __FWIMAGE_H
+
+#include <stdint.h>
+
+#define FWIMAGE_MAGIC          0x46577830 /* FWx0 */
+
+struct fwimage_header {
+       uint32_t version;
+       uint32_t flags;
+       char data[];
+};
+
+struct fwimage_trailer {
+       uint32_t magic;
+       uint32_t crc32;
+       uint8_t type;
+       uint8_t __pad[3];
+       uint32_t size;
+};
+
+enum fwimage_type {
+       FWIMAGE_SIGNATURE,
+       FWIMAGE_INFO,
+};
+
+#endif
diff --git a/src/fwtool.c b/src/fwtool.c
new file mode 100644 (file)
index 0000000..e77b8b5
--- /dev/null
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2016 Felix Fietkau <nbd@nbd.name>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2
+ * as published by the Free Software Foundation
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+#include <sys/types.h>
+#include <stdio.h>
+#include <getopt.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "fwimage.h"
+#include "utils.h"
+#include "crc32.h"
+
+#define METADATA_MAXLEN                30 * 1024
+#define SIGNATURE_MAXLEN       1 * 1024
+
+#define BUFLEN                 (METADATA_MAXLEN + SIGNATURE_MAXLEN + 1024)
+
+enum {
+       MODE_DEFAULT = -1,
+       MODE_EXTRACT = 0,
+       MODE_APPEND = 1,
+};
+
+struct data_buf {
+       char *cur;
+       char *prev;
+       int cur_len;
+       int file_len;
+};
+
+static FILE *signature_file, *metadata_file, *firmware_file;
+static int file_mode = MODE_DEFAULT;
+static bool truncate_file;
+static bool quiet = false;
+
+static uint32_t crc_table[256];
+
+#define msg(...)                                       \
+       do {                                            \
+               if (!quiet)                             \
+                       fprintf(stderr, __VA_ARGS__);   \
+       } while (0)
+
+static int
+usage(const char *progname)
+{
+       fprintf(stderr, "Usage: %s <options> <firmware>\n"
+               "\n"
+               "Options:\n"
+               "  -S <file>:           Append signature file to firmware image\n"
+               "  -I <file>:           Append metadata file to firmware image\n"
+               "  -s <file>:           Extract signature file from firmware image\n"
+               "  -i <file>:           Extract metadata file from firmware image\n"
+               "  -t:                  Remove extracted chunks from firmare image (using -s, -i)\n"
+               "  -q:                  Quiet (suppress error messages)\n"
+               "\n", progname);
+       return 1;
+}
+
+static FILE *
+open_file(const char *name, bool write)
+{
+       FILE *ret;
+
+       if (!strcmp(name, "-"))
+               return write ? stdout : stdin;
+
+       ret = fopen(name, write ? "w" : "r+");
+       if (!ret && !write)
+               ret = fopen(name, "r");
+
+       return ret;
+}
+
+static int
+set_file(FILE **file, const char *name, int mode)
+{
+       if (file_mode < 0)
+               file_mode = mode;
+       else if (file_mode != mode) {
+               msg("Error: mixing appending and extracting data is not supported\n");
+               return 1;
+       }
+
+       if (*file) {
+               msg("Error: the same append/extract option cannot be used multiple times\n");
+               return 1;
+       }
+
+       *file = open_file(name, mode == MODE_EXTRACT);
+       return !*file;
+}
+
+static void
+trailer_update_crc(struct fwimage_trailer *tr, void *buf, int len)
+{
+       tr->crc32 = cpu_to_be32(crc32_block(be32_to_cpu(tr->crc32), buf, len, crc_table));
+}
+
+static int
+append_data(FILE *in, FILE *out, struct fwimage_trailer *tr, int maxlen)
+{
+       while (1) {
+               char buf[512];
+               int len;
+
+               len = fread(buf, 1, sizeof(buf), in);
+               if (!len)
+                       break;
+
+               maxlen -= len;
+               if (maxlen < 0)
+                       return 1;
+
+               tr->size += len;
+               trailer_update_crc(tr, buf, len);
+               fwrite(buf, len, 1, out);
+       }
+
+       return 0;
+}
+
+static void
+append_trailer(FILE *out, struct fwimage_trailer *tr)
+{
+       tr->size = cpu_to_be32(tr->size);
+       fwrite(tr, sizeof(*tr), 1, out);
+       trailer_update_crc(tr, tr, sizeof(*tr));
+}
+
+static int
+add_metadata(struct fwimage_trailer *tr)
+{
+       struct fwimage_header hdr = {};
+
+       tr->type = FWIMAGE_INFO;
+       tr->size = sizeof(hdr) + sizeof(*tr);
+
+       trailer_update_crc(tr, &hdr, sizeof(hdr));
+       fwrite(&hdr, sizeof(hdr), 1, firmware_file);
+
+       if (append_data(metadata_file, firmware_file, tr, METADATA_MAXLEN))
+               return 1;
+
+       append_trailer(firmware_file, tr);
+
+       return 0;
+}
+
+static int
+add_signature(struct fwimage_trailer *tr)
+{
+       if (!signature_file)
+               return 0;
+
+       tr->type = FWIMAGE_SIGNATURE;
+       tr->size = sizeof(*tr);
+
+       if (append_data(signature_file, firmware_file, tr, SIGNATURE_MAXLEN))
+               return 1;
+
+       append_trailer(firmware_file, tr);
+
+       return 0;
+}
+
+static int
+add_data(const char *name)
+{
+       struct fwimage_trailer tr = {
+               .magic = cpu_to_be32(FWIMAGE_MAGIC),
+               .crc32 = ~0,
+       };
+       int file_len = 0;
+       int ret = 0;
+
+       firmware_file = fopen(name, "r+");
+       if (!firmware_file) {
+               msg("Failed to open firmware file\n");
+               return 1;
+       }
+
+       while (1) {
+               char buf[512];
+               int len;
+
+               len = fread(buf, 1, sizeof(buf), firmware_file);
+               if (!len)
+                       break;
+
+               file_len += len;
+               trailer_update_crc(&tr, buf, len);
+       }
+
+       if (metadata_file)
+               ret = add_metadata(&tr);
+       else if (signature_file)
+               ret = add_signature(&tr);
+
+       if (ret) {
+               fflush(firmware_file);
+               ftruncate(fileno(firmware_file), file_len);
+       }
+
+       return ret;
+}
+
+static void
+remove_tail(struct data_buf *dbuf, int len)
+{
+       dbuf->cur_len -= len;
+       dbuf->file_len -= len;
+
+       if (dbuf->cur_len)
+               return;
+
+       free(dbuf->cur);
+       dbuf->cur = dbuf->prev;
+       dbuf->prev = NULL;
+       dbuf->cur_len = BUFLEN;
+}
+
+static int
+extract_tail(struct data_buf *dbuf, void *dest, int len)
+{
+       int cur_len = dbuf->cur_len;
+
+       if (!dbuf->cur)
+               return 1;
+
+       if (cur_len >= len)
+               cur_len = len;
+
+       memcpy(dest + (len - cur_len), dbuf->cur + dbuf->cur_len - cur_len, cur_len);
+       remove_tail(dbuf, cur_len);
+
+       cur_len = len - cur_len;
+       if (cur_len && !dbuf->cur)
+               return 1;
+
+       memcpy(dest, dbuf->cur + dbuf->cur_len - cur_len, cur_len);
+       remove_tail(dbuf, cur_len);
+
+       return 0;
+}
+
+static uint32_t
+tail_crc32(struct data_buf *dbuf, uint32_t crc32)
+{
+       if (dbuf->prev)
+               crc32 = crc32_block(crc32, dbuf->prev, BUFLEN, crc_table);
+
+       return crc32_block(crc32, dbuf->cur, dbuf->cur_len, crc_table);
+}
+
+static int
+validate_metadata(struct fwimage_header *hdr, int data_len)
+{
+        if (hdr->version != 0)
+                return 1;
+        return 0;
+}
+
+static int
+extract_data(const char *name)
+{
+       struct fwimage_header *hdr;
+       struct fwimage_trailer tr;
+       struct data_buf dbuf = {};
+       uint32_t crc32 = ~0;
+       int ret = 1;
+       void *buf;
+
+       firmware_file = open_file(name, false);
+       if (!firmware_file) {
+               msg("Failed to open firmware file\n");
+               return 1;
+       }
+
+       if (truncate_file && firmware_file == stdin) {
+               msg("Cannot truncate file when reading from stdin\n");
+               return 1;
+       }
+
+       buf = malloc(BUFLEN);
+       if (!buf)
+               return 1;
+
+       do {
+               char *tmp = dbuf.cur;
+
+               dbuf.cur = dbuf.prev;
+               dbuf.prev = tmp;
+
+               if (dbuf.cur)
+                       crc32 = crc32_block(crc32, dbuf.cur, BUFLEN, crc_table);
+               else
+                       dbuf.cur = malloc(BUFLEN);
+
+               if (!dbuf.cur)
+                       goto out;
+
+               dbuf.cur_len = fread(dbuf.cur, 1, BUFLEN, firmware_file);
+               dbuf.file_len += dbuf.cur_len;
+       } while (dbuf.cur_len == BUFLEN);
+
+       while (1) {
+               int data_len;
+
+               if (extract_tail(&dbuf, &tr, sizeof(tr)))
+                       break;
+
+               data_len = be32_to_cpu(tr.size) - sizeof(tr);
+               if (tr.magic != cpu_to_be32(FWIMAGE_MAGIC)) {
+                       msg("Data not found\n");
+                       break;
+               }
+
+               if (be32_to_cpu(tr.crc32) != tail_crc32(&dbuf, crc32)) {
+                       msg("CRC error\n");
+                       break;
+               }
+
+               if (data_len > BUFLEN) {
+                       msg("Size error\n");
+                       break;
+               }
+
+               extract_tail(&dbuf, buf, data_len);
+
+               if (tr.type == FWIMAGE_SIGNATURE) {
+                       if (!signature_file)
+                               continue;
+                       fwrite(buf, data_len, 1, signature_file);
+                       ret = 0;
+                       break;
+               } else if (tr.type == FWIMAGE_INFO) {
+                       if (!metadata_file)
+                               break;
+
+                       hdr = buf;
+                       data_len -= sizeof(*hdr);
+                       if (validate_metadata(hdr, data_len))
+                               continue;
+
+                       fwrite(hdr + 1, data_len, 1, metadata_file);
+                       ret = 0;
+                       break;
+               } else {
+                       continue;
+               }
+       }
+
+       if (!ret && truncate_file)
+               ftruncate(fileno(firmware_file), dbuf.file_len);
+
+out:
+       free(buf);
+       free(dbuf.cur);
+       free(dbuf.prev);
+       return ret;
+}
+
+static void cleanup(void)
+{
+       if (signature_file)
+               fclose(signature_file);
+       if (metadata_file)
+               fclose(metadata_file);
+       if (firmware_file)
+               fclose(firmware_file);
+}
+
+int main(int argc, char **argv)
+{
+       const char *progname = argv[0];
+       int ret, ch;
+
+       crc32_filltable(crc_table);
+
+       while ((ch = getopt(argc, argv, "i:I:qs:S:t")) != -1) {
+               ret = 0;
+               switch(ch) {
+               case 'S':
+                       ret = set_file(&signature_file, optarg, MODE_APPEND);
+                       break;
+               case 'I':
+                       ret = set_file(&metadata_file, optarg, MODE_APPEND);
+                       break;
+               case 's':
+                       ret = set_file(&signature_file, optarg, MODE_EXTRACT);
+                       break;
+               case 'i':
+                       ret = set_file(&metadata_file, optarg, MODE_EXTRACT);
+                       break;
+               case 't':
+                       truncate_file = true;
+                       break;
+               case 'q':
+                       quiet = true;
+                       break;
+               }
+
+               if (ret)
+                       goto out;
+       }
+
+       if (optind >= argc) {
+               ret = usage(progname);
+               goto out;
+       }
+
+       if (file_mode == MODE_DEFAULT) {
+               ret = usage(progname);
+               goto out;
+       }
+
+       if (signature_file && metadata_file) {
+               msg("Cannot append/extract metadata and signature in one run\n");
+               return 1;
+       }
+
+       if (file_mode)
+               ret = add_data(argv[optind]);
+       else
+               ret = extract_data(argv[optind]);
+
+out:
+       cleanup();
+       return ret;
+}
diff --git a/src/utils.h b/src/utils.h
new file mode 100644 (file)
index 0000000..c2e665e
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * utils - misc libubox utility functions
+ *
+ * Copyright (C) 2012 Felix Fietkau <nbd@openwrt.org>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __LIBUBOX_UTILS_H
+#define __LIBUBOX_UTILS_H
+
+#include <sys/types.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+#endif
+
+#ifdef __GNUC__
+#define _GNUC_MIN_VER(maj, min) (((__GNUC__ << 8) + __GNUC_MINOR__) >= (((maj) << 8) + (min)))
+#else
+#define _GNUC_MIN_VER(maj, min) 0
+#endif
+
+#if defined(__linux__) || defined(__CYGWIN__)
+#include <byteswap.h>
+#include <endian.h>
+
+#elif defined(__APPLE__)
+#include <machine/endian.h>
+#include <machine/byte_order.h>
+#define bswap_32(x) OSSwapInt32(x)
+#define bswap_64(x) OSSwapInt64(x)
+#elif defined(__FreeBSD__)
+#include <sys/endian.h>
+#define bswap_32(x) bswap32(x)
+#define bswap_64(x) bswap64(x)
+#else
+#include <machine/endian.h>
+#define bswap_32(x) swap32(x)
+#define bswap_64(x) swap64(x)
+#endif
+
+#ifndef __BYTE_ORDER
+#define __BYTE_ORDER BYTE_ORDER
+#endif
+#ifndef __BIG_ENDIAN
+#define __BIG_ENDIAN BIG_ENDIAN
+#endif
+#ifndef __LITTLE_ENDIAN
+#define __LITTLE_ENDIAN LITTLE_ENDIAN
+#endif
+
+static inline uint16_t __u_bswap16(uint16_t val)
+{
+       return ((val >> 8) & 0xffu) | ((val & 0xffu) << 8);
+}
+
+#if _GNUC_MIN_VER(4, 2)
+#define __u_bswap32(x) __builtin_bswap32(x)
+#define __u_bswap64(x) __builtin_bswap64(x)
+#else
+#define __u_bswap32(x) bswap_32(x)
+#define __u_bswap64(x) bswap_64(x)
+#endif
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+
+#define cpu_to_be64(x) __u_bswap64(x)
+#define cpu_to_be32(x) __u_bswap32(x)
+#define cpu_to_be16(x) __u_bswap16((uint16_t) (x))
+
+#define be64_to_cpu(x) __u_bswap64(x)
+#define be32_to_cpu(x) __u_bswap32(x)
+#define be16_to_cpu(x) __u_bswap16((uint16_t) (x))
+
+#define cpu_to_le64(x) (x)
+#define cpu_to_le32(x) (x)
+#define cpu_to_le16(x) (x)
+
+#define le64_to_cpu(x) (x)
+#define le32_to_cpu(x) (x)
+#define le16_to_cpu(x) (x)
+
+#else /* __BYTE_ORDER == __LITTLE_ENDIAN */
+
+#define cpu_to_le64(x) __u_bswap64(x)
+#define cpu_to_le32(x) __u_bswap32(x)
+#define cpu_to_le16(x) __u_bswap16((uint16_t) (x))
+
+#define le64_to_cpu(x) __u_bswap64(x)
+#define le32_to_cpu(x) __u_bswap32(x)
+#define le16_to_cpu(x) __u_bswap16((uint16_t) (x))
+
+#define cpu_to_be64(x) (x)
+#define cpu_to_be32(x) (x)
+#define cpu_to_be16(x) (x)
+
+#define be64_to_cpu(x) (x)
+#define be32_to_cpu(x) (x)
+#define be16_to_cpu(x) (x)
+
+#endif
+
+#endif