From 691e02d6525376aaf06a43cd8f17db86b1054bd0 Mon Sep 17 00:00:00 2001 From: Richard Hughes Date: Wed, 15 Jul 2015 17:31:16 +0100 Subject: [PATCH] Add a simple signing server that operates on .cab files --- configure.ac | 5 + contrib/fwupd.spec.in | 19 ++- data/Makefile.am | 6 + data/fwsignd.conf | 14 ++ po/POTFILES.in | 1 + src/Makefile.am | 28 ++++ src/fu-keyring.c | 90 +++++++++++++ src/fu-keyring.h | 3 + src/fu-sign.c | 290 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 data/fwsignd.conf create mode 100644 src/fu-sign.c diff --git a/configure.ac b/configure.ac index 92a82c0a3..1456d1a1f 100644 --- a/configure.ac +++ b/configure.ac @@ -156,6 +156,11 @@ if test x$enable_uefi != xno; then fi AM_CONDITIONAL(HAVE_UEFI, test x$enable_uefi = xyes) +# Build the signing server +AC_ARG_ENABLE(fwsignd, AS_HELP_STRING([--enable-fwsignd],[Enable fwsignd support]), + enable_fwsignd=$enableval, enable_fwsignd=no) +AM_CONDITIONAL(HAVE_FWSIGND, test x$enable_fwsignd = xyes) + # systemd support AC_ARG_WITH([systemdunitdir], AS_HELP_STRING([--with-systemdunitdir=DIR], [Directory for systemd service files]), diff --git a/contrib/fwupd.spec.in b/contrib/fwupd.spec.in index decd36708..e319ee031 100644 --- a/contrib/fwupd.spec.in +++ b/contrib/fwupd.spec.in @@ -44,11 +44,22 @@ Requires: %{name} = %{version}-%{release} %description devel Files for development with %{name}. +%package sign +Summary: Firmware signing server +Requires: %{name} = %{version}-%{release} + +%description sign +This package provides a signing server suitable for automatically signing +firmware in .cab archives. +The values in /etc/fwsignd.conf file should be set before starting the daemon. +You probably don't need this package unless you're implementing a LVFS clone. + %prep %setup -q %build %configure \ + --enable-fwsignd \ --disable-static \ --disable-rpath \ --disable-silent-rules \ @@ -86,7 +97,8 @@ find %{buildroot} -name '*.la' -exec rm -f {} ';' %{_datadir}/polkit-1/rules.d/org.freedesktop.fwupd.rules %{_datadir}/dbus-1/system-services/org.freedesktop.fwupd.service %{_datadir}/man/man1/fwupdmgr.1.gz -%{_unitdir}/*.service +%{_unitdir}/fwupd-offline-update.service +%{_unitdir}/fwupd.service %{_unitdir}/system-update.target.wants/*.service %dir %{_localstatedir}/lib/fwupd %{_libdir}/lib*.so.* @@ -96,6 +108,11 @@ find %{buildroot} -name '*.la' -exec rm -f {} ';' %dir %{_localstatedir}/cache/app-info/xmls /usr/lib/udev/rules.d/*.rules +%files sign +%config(noreplace)%{_sysconfdir}/fwsignd.conf +%{_unitdir}/fwsignd.service +%{_libexecdir}/fwsignd + %files devel %{_includedir}/fwupd-1 %{_libdir}/lib*.so diff --git a/data/Makefile.am b/data/Makefile.am index 3a1c04b12..331e27509 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -21,6 +21,12 @@ systemdservice_in_files = \ fwupd-offline-update.service.in systemdservice_DATA = $(systemdservice_in_files:.service.in=.service) +if HAVE_FWSIGND +systemdservice_in_files += fwsignd.service.in +configdir = $(sysconfdir) +dist_config_DATA = fwsignd.conf +endif + install-data-hook: mkdir -p $(DESTDIR)$(systemdunitdir)/system-update.target.wants ln -sf ../fwupd-offline-update.service $(DESTDIR)$(systemdunitdir)/system-update.target.wants/fwupd-offline-update.service diff --git a/data/fwsignd.conf b/data/fwsignd.conf new file mode 100644 index 000000000..4ef91e2fe --- /dev/null +++ b/data/fwsignd.conf @@ -0,0 +1,14 @@ +[fwupd] + +# The path where we search for .cab files +# +# IMPORTANT: the files we could process are deleted from this location when +# they have been signed successfully. +SourceDirectory=/mnt/source + +# The path where we write new signed .cab files +DestinationDirectory=/mnt/destination + +# The GPG key ID used for signing -- you probably want to create a key without +# a password, e.g. gpg2 --gen-key && gpg2 --edit-key; passwd; quit +KeyID=12345678 diff --git a/po/POTFILES.in b/po/POTFILES.in index 7312a5e00..98d478160 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,4 +1,5 @@ policy/org.freedesktop.fwupd.policy.in src/fu-debug.c src/fu-main.c +src/fu-sign.c src/fu-util.c diff --git a/src/Makefile.am b/src/Makefile.am index eebb29aaa..58510509b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -138,6 +138,34 @@ fwupd_CFLAGS = \ -DFU_OFFLINE_DESTDIR=\"\" \ $(WARNINGFLAGS_C) +if HAVE_FWSIGND +libexec_PROGRAMS += fwsignd + +fwsignd_SOURCES = \ + fu-cleanup.h \ + fu-cab.c \ + fu-cab.h \ + fu-keyring.c \ + fu-keyring.h \ + fu-debug.c \ + fu-debug.h \ + fu-sign.c + +fwsignd_LDADD = \ + $(FWUPD_LIBS) \ + $(APPSTREAM_GLIB_LIBS) \ + $(GPGME_LIBS) \ + $(GCAB_LIBS) \ + $(GLIB_LIBS) + +fwsignd_LDFLAGS = \ + $(PIE_LDFLAGS) + +fwsignd_CFLAGS = \ + $(WARNINGFLAGS_C) + +endif + TESTS_ENVIRONMENT = \ libtool --mode=execute valgrind \ --quiet \ diff --git a/src/fu-keyring.c b/src/fu-keyring.c index 25b7ef541..d24c061eb 100644 --- a/src/fu-keyring.c +++ b/src/fu-keyring.c @@ -95,6 +95,96 @@ fu_keyring_setup (FuKeyring *keyring, GError **error) /* enable armor mode */ gpgme_set_armor (keyring->priv->ctx, TRUE); + /* never interactive */ + gpgme_set_pinentry_mode (keyring->priv->ctx, GPGME_PINENTRY_MODE_ERROR); + + return TRUE; +} + +/** + * fu_keyring_list_private_keys: + **/ +static void +fu_keyring_list_private_keys (FuKeyring *keyring) +{ + gpgme_key_t key; + gpgme_error_t err; + + err = gpgme_op_keylist_start (keyring->priv->ctx, NULL, 1); + while (!err) { + _cleanup_string_free_ GString *str = NULL; + err = gpgme_op_keylist_next (keyring->priv->ctx, &key); + if (err) + break; + str = g_string_new (key->subkeys->keyid); + g_string_append_printf (str, "\t[secret:%i, sign:%i]", + key->subkeys->secret, + key->subkeys->can_sign); + if (key->uids && key->uids->name) + g_string_append_printf (str, " %s", key->uids->name); + if (key->uids && key->uids->email) + g_string_append_printf (str, " <%s>", key->uids->email); + g_debug ("%s", str->str); + gpgme_key_release (key); + } + + if (gpg_err_code (err) != GPG_ERR_EOF) { + g_warning ("can not list keys: %s\n", gpgme_strerror (err)); + return; + } +} + +/** + * fu_keyring_set_signing_key: + **/ +gboolean +fu_keyring_set_signing_key (FuKeyring *keyring, const gchar *key_id, GError **error) +{ + gint n_signers; + gpgme_error_t rc; + gpgme_key_t key; + + + /* setup context */ + if (!fu_keyring_setup (keyring, error)) + return FALSE; + + /* list possible keys */ + fu_keyring_list_private_keys (keyring); + + /* find key */ + rc = gpgme_get_key (keyring->priv->ctx, key_id, &key, 1); + if (rc != GPG_ERR_NO_ERROR) { + g_set_error (error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "failed to find key %s: %s", + key_id, gpgme_strerror (rc)); + return FALSE; + } + + /* select it to be used */ + gpgme_signers_clear (keyring->priv->ctx); + rc = gpgme_signers_add (keyring->priv->ctx, key); + if (rc != GPG_ERR_NO_ERROR) { + g_set_error (error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "failed to add signing key %s: %s", + key_id, gpgme_strerror (rc)); + return FALSE; + } + gpgme_key_unref (key); + + /* check it's selected */ + n_signers = gpgme_signers_count (keyring->priv->ctx); + if (n_signers != 1) { + g_set_error (error, + FWUPD_ERROR, + FWUPD_ERROR_INTERNAL, + "failed to check signing key %s", key_id); + return FALSE; + } return TRUE; } diff --git a/src/fu-keyring.h b/src/fu-keyring.h index 26db21f64..4b4386763 100644 --- a/src/fu-keyring.h +++ b/src/fu-keyring.h @@ -59,6 +59,9 @@ gboolean fu_keyring_add_public_keys (FuKeyring *keyring, gboolean fu_keyring_add_public_key (FuKeyring *keyring, const gchar *filename, GError **error); +gboolean fu_keyring_set_signing_key (FuKeyring *keyring, + const gchar *key_id, + GError **error); gboolean fu_keyring_verify_file (FuKeyring *keyring, const gchar *filename, const gchar *signature, diff --git a/src/fu-sign.c b/src/fu-sign.c new file mode 100644 index 000000000..5f31dbeca --- /dev/null +++ b/src/fu-sign.c @@ -0,0 +1,290 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2015 Richard Hughes + * + * Licensed under the GNU General Public License Version 2 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "fu-cleanup.h" +#include "fu-cab.h" +#include "fu-debug.h" +#include "fu-keyring.h" + +typedef struct { + gchar *source; + gchar *destination; + gchar *key_id; +} FuSignPrivate; + +/** + * fu_sign_process_file: + **/ +static gboolean +fu_sign_process_file (FuSignPrivate *priv, + const gchar *fn_src, + const gchar *fn_dst, + GError **error) +{ + gsize fw_len = 0; + _cleanup_bytes_unref_ GBytes *fw = NULL; + _cleanup_bytes_unref_ GBytes *sig = NULL; + _cleanup_free_ gchar *fw_data = NULL; + _cleanup_free_ gchar *fn_bin_asc = NULL; + _cleanup_object_unref_ FuCab *cab = NULL; + _cleanup_object_unref_ FuKeyring *kr = NULL; + _cleanup_object_unref_ GFile *src = NULL; + _cleanup_object_unref_ GFile *dst = NULL; + const gchar *fn_bin; + + g_info ("processing %s", fn_src); + + /* open the .cab file and extract firmware */ + cab = fu_cab_new (); + src = g_file_new_for_path (fn_src); + if (!fu_cab_load_file (cab, src, NULL, error)) + return FALSE; + if (!fu_cab_extract (cab, FU_CAB_EXTRACT_FLAG_ALL, error)) + return FALSE; + + /* sign the .bin */ + fn_bin = fu_cab_get_filename_firmware (cab); + g_info ("signing %s with key %s", fn_bin, priv->key_id); + kr = fu_keyring_new (); + if (priv->key_id != NULL) { + if (!fu_keyring_set_signing_key (kr, priv->key_id, error)) + return FALSE; + } + if (!g_file_get_contents (fn_bin, &fw_data, &fw_len, error)) + return FALSE; + fw = g_bytes_new_static (fw_data, fw_len); + sig = fu_keyring_sign_data (kr, fw, error); + if (sig == NULL) + return FALSE; + + /* write new detached signature */ + fn_bin_asc = g_strdup_printf ("%s.asc", fn_bin); + g_debug ("writing to %s", fn_bin_asc); + if (!g_file_set_contents (fn_bin_asc, + g_bytes_get_data (sig, NULL), + g_bytes_get_size (sig), + error)) + return FALSE; + + /* add to file list */ + fu_cab_add_file (cab, fn_bin_asc); + + /* add the detached signature to the .cab file */ + g_debug ("saving %s", fn_dst); + dst = g_file_new_for_path (fn_dst); + if (!fu_cab_save_file (cab, dst, NULL, error)) + return FALSE; + + /* delete the source file */ + g_debug ("deleting %s", fn_src); + g_unlink (fn_src); + + /* delete the working space */ + g_unlink (fn_bin_asc); + if (!fu_cab_delete_temp_files (cab, error)) + return FALSE; + return TRUE; +} + +/** + * fu_sign_coldplug: + **/ +static gboolean +fu_sign_coldplug (FuSignPrivate *priv, GError **error) +{ + _cleanup_dir_close_ GDir *dir = NULL; + + dir = g_dir_open (priv->source, 0, error); + if (dir == NULL) + return FALSE; + while (TRUE) { + const gchar *fn; + _cleanup_free_ gchar *fn_src = NULL; + _cleanup_free_ gchar *fn_dst = NULL; + fn = g_dir_read_name (dir); + if (fn == NULL) + break; + fn_src = g_build_filename (priv->source, fn, NULL); + fn_dst = g_build_filename (priv->destination, fn, NULL); + if (!fu_sign_process_file (priv, fn_src, fn_dst, error)) + return FALSE; + } + return TRUE; +} + +/** + * fu_sign_monitor_changed_cb: + **/ +static void +fu_sign_monitor_changed_cb (GFileMonitor *monitor, + GFile *file, GFile *other_file, + GFileMonitorEvent event_type, + FuSignPrivate *priv) +{ + _cleanup_error_free_ GError *error = NULL; + _cleanup_free_ gchar *basename = NULL; + _cleanup_free_ gchar *fn_dst = NULL; + _cleanup_free_ gchar *fn_src = NULL; + + /* only new files */ + if (event_type != G_FILE_MONITOR_EVENT_CREATED) + return; + fn_src = g_file_get_path (file); + if (fn_src == NULL) + return; + + /* process file */ + basename = g_file_get_basename (file); + fn_dst = g_build_filename (priv->destination, basename, NULL); + if (!fu_sign_process_file (priv, fn_src, fn_dst, &error)) { + g_warning ("failed to process %s: %s", fn_src, error->message); + return; + } +} + +/** + * main: + **/ +int +main (int argc, char *argv[]) +{ + FuSignPrivate *priv = NULL; + GOptionContext *context; + GMainLoop *loop = NULL; + gboolean ret; + gboolean one_shot = FALSE; + guint retval = 1; + _cleanup_error_free_ GError *error = NULL; + _cleanup_free_ gchar *config_file = NULL; + _cleanup_free_ gchar *destination = NULL; + _cleanup_free_ gchar *key_id = NULL; + _cleanup_free_ gchar *source = NULL; + _cleanup_keyfile_unref_ GKeyFile *config = NULL; + const GOptionEntry options[] = { + { "one-shot", '\0', 0, G_OPTION_ARG_NONE, &one_shot, + /* TRANSLATORS: exit after we've started up, used for user profiling */ + _("Exit after signing queue"), NULL }, + { "source", 's', 0, G_OPTION_ARG_FILENAME, &source, + /* TRANSLATORS: input location to watch */ + _("Source path for files"), NULL }, + { "destination", 'd', 0, G_OPTION_ARG_FILENAME, &destination, + /* TRANSLATORS: output location to put files */ + _("Destination path for files"), NULL }, + { "key-id", 'd', 0, G_OPTION_ARG_STRING, &key_id, + /* TRANSLATORS: output location to put files */ + _("GPG key used to sign the firmware"), NULL }, + { NULL} + }; + + setlocale (LC_ALL, ""); + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + /* TRANSLATORS: program name */ + g_set_application_name (_("fwsignd")); + context = g_option_context_new (NULL); + g_option_context_add_main_entries (context, options, NULL); + g_option_context_add_group (context, fu_debug_get_option_group ()); + /* TRANSLATORS: program summary */ + g_option_context_set_summary (context, _("Firmware signing server")); + ret = g_option_context_parse (context, &argc, &argv, &error); + if (!ret) { + g_print ("failed to parse command line arguments: %s\n", + error->message); + retval = EXIT_FAILURE; + goto out; + } + + /* fall back to keyfile */ + config = g_key_file_new (); + config_file = g_build_filename (SYSCONFDIR, "fwsignd.conf", NULL); + if (!g_key_file_load_from_file (config, config_file, + G_KEY_FILE_NONE, &error)) { + g_print ("failed to load config file %s: %s\n", + config_file, error->message); + retval = EXIT_FAILURE; + goto out; + } + if (source == NULL) + source = g_key_file_get_string (config, "fwupd", "SourceDirectory", NULL); + if (destination == NULL) + destination = g_key_file_get_string (config, "fwupd", "DestinationDirectory", NULL); + if (key_id == NULL) + key_id = g_key_file_get_string (config, "fwupd", "KeyID", NULL); + if (source == NULL || destination == NULL) { + g_print ("source and destination required\n"); + retval = EXIT_FAILURE; + goto out; + } + priv = g_new0 (FuSignPrivate, 1); + priv->source = g_strdup (source); + priv->destination = g_strdup (destination); + priv->key_id = g_strdup (key_id); + + /* process */ + g_debug ("clearing queue"); + if (!fu_sign_coldplug (priv, &error)) { + g_print ("failed to clear queue: %s\n", error->message); + retval = EXIT_FAILURE; + goto out; + } + if (!one_shot) { + _cleanup_object_unref_ GFileMonitor *monitor = NULL; + _cleanup_object_unref_ GFile *src = NULL; + g_debug ("waiting for files to appear in %s", source); + src = g_file_new_for_path (source); + monitor = g_file_monitor (src, G_FILE_MONITOR_NONE, NULL, &error); + if (monitor == NULL) { + g_print ("failed to watch %s: %s\n", source, error->message); + retval = EXIT_FAILURE; + goto out; + } + g_signal_connect (monitor, "changed", + G_CALLBACK (fu_sign_monitor_changed_cb), priv); + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + } + + /* success */ + retval = 0; +out: + if (priv != NULL) { + g_free (priv->source); + g_free (priv->destination); + g_free (priv->key_id); + g_free (priv); + } + g_option_context_free (context); + if (loop != NULL) + g_main_loop_unref (loop); + return retval; +} +