diff --git a/package/system/fwtool/Makefile b/package/system/fwtool/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..901081c1f4acb942c084a01ff9cc69249636455b
--- /dev/null
+++ b/package/system/fwtool/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/package/system/fwtool/src/crc32.h b/package/system/fwtool/src/crc32.h
new file mode 100644
index 0000000000000000000000000000000000000000..022c69fc7fd0b0751ae15697a14b202d8baceb12
--- /dev/null
+++ b/package/system/fwtool/src/crc32.h
@@ -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/package/system/fwtool/src/fwimage.h b/package/system/fwtool/src/fwimage.h
new file mode 100644
index 0000000000000000000000000000000000000000..52dcfb1bacc07f553341ba21e660388c8dfe5f12
--- /dev/null
+++ b/package/system/fwtool/src/fwimage.h
@@ -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/package/system/fwtool/src/fwtool.c b/package/system/fwtool/src/fwtool.c
new file mode 100644
index 0000000000000000000000000000000000000000..e77b8b58e11297d8e827bc7463bbdf69dccee5e2
--- /dev/null
+++ b/package/system/fwtool/src/fwtool.c
@@ -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/package/system/fwtool/src/utils.h b/package/system/fwtool/src/utils.h
new file mode 100644
index 0000000000000000000000000000000000000000..c2e665e54a66c8c23a6758fce6a0709b687bb3c9
--- /dev/null
+++ b/package/system/fwtool/src/utils.h
@@ -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