/*
* Copyright (C) 2013 Colin Walters <walters@verbum.org>
*
* SPDX-License-Identifier: LGPL-2.0+
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*/
#include "config.h"
#include "ot-main.h"
#include "ot-builtins.h"
#include "ostree.h"
#include "ostree-cmdprivate.h"
#include "ot-main.h"
#include "otutil.h"
static char *opt_from_rev;
static char *opt_to_rev;
static char *opt_min_fallback_size;
static char *opt_max_bsdiff_size;
static char *opt_max_chunk_size;
static char *opt_endianness;
static char *opt_filename;
static gboolean opt_empty;
static gboolean opt_swap_endianness;
static gboolean opt_inline;
static gboolean opt_disable_bsdiff;
static gboolean opt_if_not_exists;
static char **opt_key_ids;
static char *opt_sign_name;
static char *opt_keysfilename;
static char *opt_keysdir;
#define BUILTINPROTO(name) static gboolean ot_static_delta_builtin_ ## name (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
BUILTINPROTO(list);
BUILTINPROTO(show);
BUILTINPROTO(delete);
BUILTINPROTO(generate);
BUILTINPROTO(apply_offline);
BUILTINPROTO(verify);
#undef BUILTINPROTO
static OstreeCommand static_delta_subcommands[] = {
{ "list", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_list,
"List static delta files" },
{ "show", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_show,
"Dump information on a delta" },
{ "delete", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_delete,
"Remove a delta" },
{ "generate", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_generate,
"Generate static delta files" },
{ "apply-offline", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_apply_offline,
"Apply static delta file" },
{ "verify", OSTREE_BUILTIN_FLAG_NONE,
ot_static_delta_builtin_verify,
"Verify static delta signatures" },
{ NULL, 0, NULL, NULL }
};
/* ATTENTION:
* Please remember to update the bash-completion script (bash/ostree) and
* man page (man/ostree-static-delta.xml) when changing the option list(s).
*/
static GOptionEntry generate_options[] = {
{ "from", 0, 0, G_OPTION_ARG_STRING, &opt_from_rev, "Create delta from revision REV", "REV" },
{ "empty", 0, 0, G_OPTION_ARG_NONE, &opt_empty, "Create delta from scratch", NULL },
{ "inline", 0, 0, G_OPTION_ARG_NONE, &opt_inline, "Inline delta parts into main delta", NULL },
{ "to", 0, 0, G_OPTION_ARG_STRING, &opt_to_rev, "Create delta to revision REV", "REV" },
{ "disable-bsdiff", 0, 0, G_OPTION_ARG_NONE, &opt_disable_bsdiff, "Disable use of bsdiff", NULL },
{ "if-not-exists", 'n', 0, G_OPTION_ARG_NONE, &opt_if_not_exists, "Only generate if a delta does not already exist", NULL },
{ "set-endianness", 0, 0, G_OPTION_ARG_STRING, &opt_endianness, "Choose metadata endianness ('l' or 'B')", "ENDIAN" },
{ "swap-endianness", 0, 0, G_OPTION_ARG_NONE, &opt_swap_endianness, "Swap metadata endianness from host order", NULL },
{ "min-fallback-size", 0, 0, G_OPTION_ARG_STRING, &opt_min_fallback_size, "Minimum uncompressed size in megabytes for individual HTTP request", NULL},
{ "max-bsdiff-size", 0, 0, G_OPTION_ARG_STRING, &opt_max_bsdiff_size, "Maximum size in megabytes to consider bsdiff compression for input files", NULL},
{ "max-chunk-size", 0, 0, G_OPTION_ARG_STRING, &opt_max_chunk_size, "Maximum size of delta chunks in megabytes", NULL},
{ "filename", 0, 0, G_OPTION_ARG_FILENAME, &opt_filename, "Write the delta content to PATH (a directory). If not specified, the OSTree repository is used", "PATH"},
{ "sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "Sign the delta with", "KEY_ID"},
{ "sign-type", 0, 0, G_OPTION_ARG_STRING, &opt_sign_name, "Signature type to use (defaults to 'ed25519')", "NAME"},
#if defined(HAVE_LIBSODIUM)
{ "keys-file", 0, 0, G_OPTION_ARG_STRING, &opt_keysfilename, "Read key(s) from file", "NAME"},
#endif
{ NULL }
};
static GOptionEntry apply_offline_options[] = {
{ "sign-type", 0, 0, G_OPTION_ARG_STRING, &opt_sign_name, "Signature type to use (defaults to 'ed25519')", "NAME"},
#if defined(HAVE_LIBSODIUM)
{ "keys-file", 0, 0, G_OPTION_ARG_STRING, &opt_keysfilename, "Read key(s) from file", "NAME"},
{ "keys-dir", 0, 0, G_OPTION_ARG_STRING, &opt_keysdir, "Redefine system-wide directories with public and revoked keys for verification", "NAME"},
#endif
{ NULL }
};
static GOptionEntry list_options[] = {
{ NULL }
};
static GOptionEntry verify_options[] = {
{ "sign-type", 0, 0, G_OPTION_ARG_STRING, &opt_sign_name, "Signature type to use (defaults to 'ed25519')", "NAME"},
#if defined(HAVE_LIBSODIUM)
{ "keys-file", 0, 0, G_OPTION_ARG_STRING, &opt_keysfilename, "Read key(s) from file", "NAME"},
{ "keys-dir", 0, 0, G_OPTION_ARG_STRING, &opt_keysdir, "Redefine system-wide directories with public and revoked keys for verification", "NAME"},
#endif
{ NULL }
};
static void
static_delta_usage (char **argv,
gboolean is_error)
{
OstreeCommand *command = static_delta_subcommands;
void (*print_func) (const gchar *format, ...);
if (is_error)
print_func = g_printerr;
else
print_func = g_print;
print_func ("Usage:\n");
print_func (" ostree static-delta [OPTION...] COMMAND\n\n");
print_func ("Builtin \"static-delta\" Commands:\n");
while (command->name)
{
if ((command->flags & OSTREE_BUILTIN_FLAG_HIDDEN) == 0)
print_func (" %-17s%s\n", command->name, command->description ?: "");
command++;
}
print_func ("\n");
}
static gboolean
ot_static_delta_builtin_list (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr(OstreeRepo) repo = NULL;
g_autoptr(GOptionContext) context = g_option_context_new ("");
if (!ostree_option_context_parse (context, list_options, &argc, &argv,
invocation, &repo, cancellable, error))
return FALSE;
g_autoptr(GPtrArray) delta_names = NULL;
if (!ostree_repo_list_static_delta_names (repo, &delta_names, cancellable, error))
return FALSE;
if (delta_names->len == 0)
g_print ("(No static deltas)\n");
else
{
for (guint i = 0; i < delta_names->len; i++)
g_print ("%s\n", (char*)delta_names->pdata[i]);
}
return TRUE;
}
static gboolean
ot_static_delta_builtin_show (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr(GOptionContext) context = g_option_context_new ("");
g_autoptr(OstreeRepo) repo = NULL;
if (!ostree_option_context_parse (context, list_options, &argc, &argv, invocation, &repo, cancellable, error))
return FALSE;
if (argc < 3)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"DELTA must be specified");
return FALSE;
}
const char *delta_id = argv[2];
if (!ostree_cmd__private__ ()->ostree_static_delta_dump (repo, delta_id, cancellable, error))
return FALSE;
return TRUE;
}
static gboolean
ot_static_delta_builtin_delete (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr(GOptionContext) context = g_option_context_new ("");
g_autoptr(OstreeRepo) repo = NULL;
if (!ostree_option_context_parse (context, list_options, &argc, &argv, invocation, &repo, cancellable, error))
return FALSE;
if (argc < 3)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"DELTA must be specified");
return FALSE;
}
const char *delta_id = argv[2];
if (!ostree_cmd__private__ ()->ostree_static_delta_delete (repo, delta_id, cancellable, error))
return FALSE;
return TRUE;
}
static gboolean
ot_static_delta_builtin_generate (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr(GOptionContext) context = g_option_context_new ("[TO]");
g_autoptr(OstreeRepo) repo = NULL;
if (!ostree_option_context_parse (context, generate_options, &argc, &argv, invocation, &repo, cancellable, error))
return FALSE;
if (!ostree_ensure_repo_writable (repo, error))
return FALSE;
if (argc >= 3 && opt_to_rev == NULL)
opt_to_rev = argv[2];
if (argc < 3 && opt_to_rev == NULL)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"TO revision must be specified");
return FALSE;
}
else
{
const char *from_source;
g_autofree char *from_resolved = NULL;
g_autofree char *to_resolved = NULL;
g_autofree char *from_parent_str = NULL;
g_autoptr(GVariantBuilder) parambuilder = NULL;
int endianness;
g_assert (opt_to_rev);
if (opt_empty)
{
if (opt_from_rev)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Cannot specify both --empty and --from=REV");
return FALSE;
}
from_source = NULL;
}
else if (opt_from_rev == NULL)
{
from_parent_str = g_strconcat (opt_to_rev, "^", NULL);
from_source = from_parent_str;
}
else
{
from_source = opt_from_rev;
}
if (from_source)
{
if (!ostree_repo_resolve_rev (repo, from_source, FALSE, &from_resolved, error))
return FALSE;
}
if (!ostree_repo_resolve_rev (repo, opt_to_rev, FALSE, &to_resolved, error))
return FALSE;
if (opt_if_not_exists)
{
gboolean does_exist;
g_autofree char *delta_id = from_resolved ? g_strconcat (from_resolved, "-", to_resolved, NULL) : g_strdup (to_resolved);
if (!ostree_cmd__private__ ()->ostree_static_delta_query_exists (repo, delta_id, &does_exist, cancellable, error))
return FALSE;
if (does_exist)
{
g_print ("Delta %s already exists.\n", delta_id);
return TRUE;
}
}
if (opt_endianness)
{
if (strcmp (opt_endianness, "l") == 0)
endianness = G_LITTLE_ENDIAN;
else if (strcmp (opt_endianness, "B") == 0)
endianness = G_BIG_ENDIAN;
else
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Invalid endianness '%s'", opt_endianness);
return FALSE;
}
}
else
endianness = G_BYTE_ORDER;
if (opt_swap_endianness)
{
switch (endianness)
{
case G_LITTLE_ENDIAN:
endianness = G_BIG_ENDIAN;
break;
case G_BIG_ENDIAN:
endianness = G_LITTLE_ENDIAN;
break;
default:
g_assert_not_reached ();
}
}
parambuilder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
if (opt_min_fallback_size)
g_variant_builder_add (parambuilder, "{sv}",
"min-fallback-size", g_variant_new_uint32 (g_ascii_strtoull (opt_min_fallback_size, NULL, 10)));
if (opt_max_bsdiff_size)
g_variant_builder_add (parambuilder, "{sv}",
"max-bsdiff-size", g_variant_new_uint32 (g_ascii_strtoull (opt_max_bsdiff_size, NULL, 10)));
if (opt_max_chunk_size)
g_variant_builder_add (parambuilder, "{sv}",
"max-chunk-size", g_variant_new_uint32 (g_ascii_strtoull (opt_max_chunk_size, NULL, 10)));
if (opt_disable_bsdiff)
g_variant_builder_add (parambuilder, "{sv}",
"bsdiff-enabled", g_variant_new_boolean (FALSE));
if (opt_inline)
g_variant_builder_add (parambuilder, "{sv}",
"inline-parts", g_variant_new_boolean (TRUE));
if (opt_filename)
g_variant_builder_add (parambuilder, "{sv}",
"filename", g_variant_new_bytestring (opt_filename));
g_variant_builder_add (parambuilder, "{sv}", "verbose", g_variant_new_boolean (TRUE));
if (opt_endianness || opt_swap_endianness)
g_variant_builder_add (parambuilder, "{sv}", "endianness", g_variant_new_uint32 (endianness));
if (opt_key_ids || opt_keysfilename)
{
g_autoptr(GPtrArray) key_ids = g_ptr_array_new ();
for (char **iter = opt_key_ids; iter != NULL && *iter != NULL; ++iter)
g_ptr_array_add (key_ids, *iter);
if (opt_keysfilename)
{
g_autoptr (GFile) keyfile = NULL;
g_autoptr (GFileInputStream) key_stream_in = NULL;
g_autoptr (GDataInputStream) key_data_in = NULL;
if (!g_file_test (opt_keysfilename, G_FILE_TEST_IS_REGULAR))
{
g_warning ("Can't open file '%s' with keys", opt_keysfilename);
return glnx_throw (error, "File object '%s' is not a regular file", opt_keysfilename);
}
keyfile = g_file_new_for_path (opt_keysfilename);
key_stream_in = g_file_read (keyfile, NULL, error);
if (key_stream_in == NULL)
return FALSE;
key_data_in = g_data_input_stream_new (G_INPUT_STREAM(key_stream_in));
g_assert (key_data_in != NULL);
/* Use simple file format with just a list of base64 public keys per line */
while (TRUE)
{
gsize len = 0;
g_autofree char *line = g_data_input_stream_read_line (key_data_in, &len, NULL, error);
g_autoptr (GVariant) sk = NULL;
if (*error != NULL)
return FALSE;
if (line == NULL)
break;
// Pass the key as a string
g_ptr_array_add (key_ids, g_strdup (line));
}
}
g_autoptr(GVariant) key_ids_v = g_variant_new_strv ((const char *const *)key_ids->pdata,
key_ids->len);
g_variant_builder_add (parambuilder, "{s@v}", "sign-key-ids",
g_variant_new_variant (g_steal_pointer (&key_ids_v)));
}
opt_sign_name = opt_sign_name ?: OSTREE_SIGN_NAME_ED25519;
g_variant_builder_add (parambuilder, "{sv}", "sign-name",
g_variant_new_bytestring (opt_sign_name));
g_print ("Generating static delta:\n");
g_print (" From: %s\n", from_resolved ? from_resolved : "empty");
g_print (" To: %s\n", to_resolved);
{ g_autoptr(GVariant) params = g_variant_ref_sink (g_variant_builder_end (parambuilder));
if (!ostree_repo_static_delta_generate (repo, OSTREE_STATIC_DELTA_GENERATE_OPT_MAJOR,
from_resolved, to_resolved, NULL,
params,
cancellable, error))
return FALSE;
}
}
return TRUE;
}
static gboolean
ot_static_delta_builtin_apply_offline (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr(GOptionContext) context = NULL;
g_autoptr(OstreeRepo) repo = NULL;
g_autoptr (OstreeSign) sign = NULL;
char **key_ids;
int n_key_ids;
context = g_option_context_new ("");
if (!ostree_option_context_parse (context, apply_offline_options, &argc, &argv, invocation, &repo, cancellable, error))
return FALSE;
if (!ostree_ensure_repo_writable (repo, error))
return FALSE;
if (argc < 3)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"PATH must be specified");
return FALSE;
}
#if defined(HAVE_LIBSODIUM)
/* Initialize crypto system */
opt_sign_name = opt_sign_name ?: OSTREE_SIGN_NAME_ED25519;
#endif
if (opt_sign_name)
{
sign = ostree_sign_get_by_name (opt_sign_name, error);
if (!sign)
return glnx_throw (error, "Signing type %s is not supported", opt_sign_name);
key_ids = argv + 3;
n_key_ids = argc - 3;
for (int i = 0; i < n_key_ids; i++)
{
g_autoptr (GVariant) pk = g_variant_new_string(key_ids[i]);
if (!ostree_sign_add_pk(sign, pk, error))
return FALSE;
}
if ((n_key_ids == 0) || opt_keysfilename)
{
g_autoptr (GVariantBuilder) builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
g_autoptr (GVariant) options = NULL;
/* Use custom directory with public and revoked keys instead of system-wide directories */
if (opt_keysdir)
g_variant_builder_add (builder, "{sv}", "basedir", g_variant_new_string (opt_keysdir));
/* The last chance for verification source -- system files */
if (opt_keysfilename)
g_variant_builder_add (builder, "{sv}", "filename", g_variant_new_string (opt_keysfilename));
options = g_variant_builder_end (builder);
if (!ostree_sign_load_pk (sign, options, error))
{
/* If it fails to load system default public keys, consider there no signature engine */
if (!opt_keysdir && !opt_keysfilename)
{
g_clear_error(error);
g_clear_object(&sign);
}
else
return FALSE;
}
}
}
const char *patharg = argv[2];
g_autoptr(GFile) path = g_file_new_for_path (patharg);
if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
return FALSE;
if (!ostree_repo_static_delta_execute_offline_with_signature (repo, path, sign, FALSE, cancellable, error))
return FALSE;
if (!ostree_repo_commit_transaction (repo, NULL, cancellable, error))
return FALSE;
return TRUE;
}
static gboolean
ot_static_delta_builtin_verify (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
g_autoptr (GOptionContext) context = g_option_context_new ("STATIC-DELTA-FILE [KEY-ID...]");
g_autoptr (OstreeRepo) repo = NULL;
gboolean verified;
char **key_ids;
int n_key_ids;
if (!ostree_option_context_parse (context, verify_options, &argc, &argv, invocation, &repo, cancellable, error))
return FALSE;
if (argc < 3)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"DELTA must be specified");
return FALSE;
}
opt_sign_name = opt_sign_name ?: OSTREE_SIGN_NAME_ED25519;
const char *delta_id = argv[2];
g_autoptr (OstreeSign) sign = ostree_sign_get_by_name (opt_sign_name, error);
if (!sign)
{
g_print("Sign-type not supported\n");
return FALSE;
}
key_ids = argv + 3;
n_key_ids = argc - 3;
for (int i = 0; i < n_key_ids; i++)
{
g_autoptr (GVariant) pk = g_variant_new_string(key_ids[i]);
if (!ostree_sign_add_pk(sign, pk, error))
return FALSE;
}
if ((n_key_ids == 0) || opt_keysfilename)
{
g_autoptr (GVariantBuilder) builder = NULL;
g_autoptr (GVariant) options = NULL;
builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
/* Use custom directory with public and revoked keys instead of system-wide directories */
if (opt_keysdir)
g_variant_builder_add (builder, "{sv}", "basedir", g_variant_new_string (opt_keysdir));
/* The last chance for verification source -- system files */
if (opt_keysfilename)
g_variant_builder_add (builder, "{sv}", "filename", g_variant_new_string (opt_keysfilename));
options = g_variant_builder_end (builder);
if (!ostree_sign_load_pk (sign, options, error))
return FALSE;
}
verified = ostree_repo_static_delta_verify_signature (repo, delta_id, sign, NULL, error);
g_print ("Verification %s\n", verified ? "OK" : "fails");
return verified;
}
gboolean
ostree_builtin_static_delta (int argc, char **argv, OstreeCommandInvocation *invocation, GCancellable *cancellable, GError **error)
{
gboolean want_help = FALSE;
const char *cmdname = NULL;
for (int i = 1; i < argc; i++)
{
if (argv[i][0] != '-')
{
cmdname = argv[i];
break;
}
else if (g_str_equal (argv[i], "--help") || g_str_equal (argv[i], "-h"))
{
want_help = TRUE;
break;
}
}
if (!cmdname && !want_help)
{
static_delta_usage (argv, TRUE);
return glnx_throw (error, "No command specified");
}
OstreeCommand *command = NULL;
if (cmdname)
{
command = static_delta_subcommands;
while (command->name)
{
if (g_strcmp0 (cmdname, command->name) == 0)
break;
command++;
}
}
if (want_help && command == NULL)
{
static_delta_usage (argv, FALSE);
return TRUE; /* Note early return */
}
if (!command->fn)
{
static_delta_usage (argv, TRUE);
return glnx_throw (error, "Unknown \"static-delta\" subcommand '%s'", cmdname);
}
g_autofree char *prgname = g_strdup_printf ("%s %s", g_get_prgname (), cmdname);
g_set_prgname (prgname);
OstreeCommandInvocation sub_invocation = { .command = command };
return command->fn (argc, argv, &sub_invocation, cancellable, error);
}