diff --git a/Makefile-libostree-defines.am b/Makefile-libostree-defines.am index 06035157..43e09281 100644 --- a/Makefile-libostree-defines.am +++ b/Makefile-libostree-defines.am @@ -46,6 +46,8 @@ libostree_public_headers = \ src/libostree/ostree-repo-finder-mount.h \ src/libostree/ostree-repo-finder-override.h \ src/libostree/ostree-kernel-args.h \ + src/libostree/ostree-sign.h \ + src/libostree/ostree-sign-ed25519.h \ $(NULL) # This one is generated via configure.ac, and the gtk-doc diff --git a/Makefile-libostree.am b/Makefile-libostree.am index a7e7e123..c0a7ac9f 100644 --- a/Makefile-libostree.am +++ b/Makefile-libostree.am @@ -262,6 +262,20 @@ libostree_1_la_CFLAGS += $(OT_DEP_SELINUX_CFLAGS) libostree_1_la_LIBADD += $(OT_DEP_SELINUX_LIBS) endif +libostree_1_la_SOURCES += \ + src/libostree/ostree-sign.c \ + src/libostree/ostree-sign.h \ + src/libostree/ostree-sign-dummy.c \ + src/libostree/ostree-sign-dummy.h \ + src/libostree/ostree-sign-ed25519.c \ + src/libostree/ostree-sign-ed25519.h \ + $(NULL) + +if USE_LIBSODIUM +libostree_1_la_CFLAGS += $(OT_DEP_LIBSODIUM_CFLAGS) +libostree_1_la_LIBADD += $(OT_DEP_LIBSODIUM_LIBS) +endif # USE_LIBSODIUM + # XXX: work around clang being passed -fstack-clash-protection which it doesn't understand # See: https://bugzilla.redhat.com/show_bug.cgi?id=1672012 INTROSPECTION_SCANNER_ENV = CC=gcc diff --git a/Makefile-man.am b/Makefile-man.am index bc58103b..718e773c 100644 --- a/Makefile-man.am +++ b/Makefile-man.am @@ -32,7 +32,7 @@ ostree-commit.1 ostree-create-usb.1 ostree-export.1 \ ostree-config.1 ostree-diff.1 ostree-find-remotes.1 ostree-fsck.1 \ ostree-init.1 ostree-log.1 ostree-ls.1 ostree-prune.1 ostree-pull-local.1 \ ostree-pull.1 ostree-refs.1 ostree-remote.1 ostree-reset.1 \ -ostree-rev-parse.1 ostree-show.1 ostree-summary.1 \ +ostree-rev-parse.1 ostree-show.1 ostree-sign.1 ostree-summary.1 \ ostree-static-delta.1 if USE_LIBSOUP man1_files += ostree-trivial-httpd.1 diff --git a/Makefile-ostree.am b/Makefile-ostree.am index f861afe4..e5767641 100644 --- a/Makefile-ostree.am +++ b/Makefile-ostree.am @@ -43,6 +43,7 @@ ostree_SOURCES = src/ostree/main.c \ src/ostree/ot-builtin-remote.c \ src/ostree/ot-builtin-reset.c \ src/ostree/ot-builtin-rev-parse.c \ + src/ostree/ot-builtin-sign.c \ src/ostree/ot-builtin-summary.c \ src/ostree/ot-builtin-show.c \ src/ostree/ot-builtin-static-delta.c \ @@ -112,7 +113,6 @@ ostree_SOURCES += \ $(NULL) endif - if USE_CURL_OR_SOUP ostree_SOURCES += src/ostree/ot-remote-builtin-add-cookie.c \ src/ostree/ot-remote-builtin-delete-cookie.c \ @@ -162,3 +162,8 @@ if USE_LIBARCHIVE ostree_CFLAGS += $(OT_DEP_LIBARCHIVE_CFLAGS) ostree_LDADD += $(OT_DEP_LIBARCHIVE_LIBS) endif + +if USE_LIBSODIUM +ostree_CFLAGS += $(OT_DEP_LIBSODIUM_CFLAGS) +ostree_LDADD += $(OT_DEP_LIBSODIUM_LIBS) +endif # USE_LIBSODIUM diff --git a/Makefile-tests.am b/Makefile-tests.am index 83b0f1a2..3270bd9c 100644 --- a/Makefile-tests.am +++ b/Makefile-tests.am @@ -137,6 +137,9 @@ _installed_or_uninstalled_test_scripts = \ tests/test-summary-collections.sh \ tests/test-pull-collections.sh \ tests/test-config.sh \ + tests/test-signed-commit.sh \ + tests/test-signed-pull.sh \ + tests/test-signed-pull-summary.sh \ $(NULL) if USE_GPGME diff --git a/apidoc/ostree-docs.xml b/apidoc/ostree-docs.xml index 8721ffa8..1ad0de37 100644 --- a/apidoc/ostree-docs.xml +++ b/apidoc/ostree-docs.xml @@ -21,6 +21,7 @@ + diff --git a/apidoc/ostree-sections.txt b/apidoc/ostree-sections.txt index 3525d9f2..4dcdc482 100644 --- a/apidoc/ostree-sections.txt +++ b/apidoc/ostree-sections.txt @@ -705,3 +705,23 @@ ostree_kernel_args_from_string ostree_kernel_args_to_strv ostree_kernel_args_to_string + +
+ostree-sign +OstreeSign +ostree_sign_list_names +ostree_sign_commit +ostree_sign_commit_verify +ostree_sign_data +ostree_sign_data_verify +ostree_sign_get_by_name +ostree_sign_get_name +ostree_sign_add_pk +ostree_sign_clear_keys +ostree_sign_load_pk +ostree_sign_set_pk +ostree_sign_set_sk +ostree_sign_summary + +ostree_sign_get_type +
diff --git a/bash/ostree b/bash/ostree index 4aec588b..7256e40a 100644 --- a/bash/ostree +++ b/bash/ostree @@ -1484,6 +1484,48 @@ _ostree_show() { return 0 } +_ostree_sign() { + local boolean_options=" + $main_boolean_options + --delete -d + --verify -v + " + + local options_with_args=" + --sign-type + --keys-file + --keys-dir + --repo + " + + local options_with_args_glob=$( __ostree_to_extglob "$options_with_args" ) + + case "$prev" in + --keys-file|--keys-dir|--repo) + __ostree_compreply_dirs_only + return 0 + ;; + $options_with_args_glob ) + return 0 + ;; + esac + + case "$cur" in + -*) + local all_options="$boolean_options $options_with_args" + __ostree_compreply_all_options + ;; + *) + local argpos=$( __ostree_pos_first_nonflag $( __ostree_to_alternatives "$options_with_args" ) ) + + if [ $cword -eq $argpos ]; then + __ostree_compreply_commits + fi + esac + + return 0 +} + _ostree_static_delta_apply_offline() { local boolean_options=" $main_boolean_options @@ -1747,6 +1789,7 @@ _ostree() { reset rev-parse show + sign static-delta summary " diff --git a/configure.ac b/configure.ac index e2d867e7..5d9d2a23 100644 --- a/configure.ac +++ b/configure.ac @@ -242,6 +242,21 @@ dnl to link to it directly. ) AM_CONDITIONAL(USE_GPGME, test "x$have_gpgme" = xyes) + +LIBSODIUM_DEPENDENCY="1.0.14" +AC_ARG_WITH(libsodium, + AS_HELP_STRING([--with-libsodium], [Use libsodium @<:@default=no@:>@]), + [], [with_libsodium=no]) +AS_IF([test x$with_libsodium != xno], [ + AC_DEFINE([HAVE_LIBSODIUM], 1, [Define if using libsodium]) + PKG_CHECK_MODULES(OT_DEP_LIBSODIUM, libsodium >= $LIBSODIUM_DEPENDENCY, have_libsodium=yes, have_libsodium=no) + AS_IF([ test x$have_libsodium = xno ], [ + AC_MSG_ERROR([Need LIBSODIUM version $LIBSODIUM_DEPENDENCY or later]) + ]) + OSTREE_FEATURES="$OSTREE_FEATURES libsodium" +], with_libsodium=no ) +AM_CONDITIONAL(USE_LIBSODIUM, test "x$have_libsodium" = xyes) + LIBARCHIVE_DEPENDENCY="libarchive >= 2.8.0" # What's in RHEL7.2. FUSE_DEPENDENCY="fuse >= 2.9.2" @@ -626,6 +641,7 @@ echo " cryptographic checksums: $with_crypto systemd: $with_libsystemd libmount: $with_libmount + libsodium (ed25519 signatures): $with_libsodium libarchive (parse tar files directly): $with_libarchive static deltas: yes (always enabled now) O_TMPFILE: $enable_otmpfile diff --git a/man/ostree-commit.xml b/man/ostree-commit.xml index c64a7a00..2c821fc1 100644 --- a/man/ostree-commit.xml +++ b/man/ostree-commit.xml @@ -251,6 +251,39 @@ Boston, MA 02111-1307, USA. POLICY is a boolean which specifies whether fsync should be used or not. Default to true. + + + + + Use particular signature engine. Currently + available ed25519 and dummy + signature types. + + The default is ed25519. + + + + + ="KEY-ID" + + There KEY-ID is: + + + + + base64-encoded secret key for commit signing. + + + + + + + ASCII-string used as secret key. + + + + + diff --git a/man/ostree-sign.xml b/man/ostree-sign.xml new file mode 100644 index 00000000..50c0b337 --- /dev/null +++ b/man/ostree-sign.xml @@ -0,0 +1,152 @@ + + + + + + + + + ostree sign + OSTree + + + + Developer + Colin + Walters + walters@verbum.org + + + + + + ostree sign + 1 + + + + ostree-sign + Sign a commit + + + + + ostree sign OPTIONS COMMIT KEY-ID + + + + + Description + + + Add a new signature to a commit. + + Note that currently, this will append a new signature even if + the commit is already signed with a given key. + + + + There are several "well-known" system places for `ed25519` trusted and revoked public keys -- expected single base64-encoded key per line. + + + Files: + + /etc/ostree/trusted.ed25519 + /etc/ostree/revoked.ed25519 + /usr/share/ostree/trusted.ed25519 + /usr/share/ostree/revoked.ed25519 + + + + Directories containing files with keys: + + /etc/ostree/trusted.ed25519.d + /etc/ostree/revoked.ed25519.d + /usr/share/ostree/trusted.ed25519.d + /usr/share/ostree/rvokeded.ed25519.d + + + + + + Options + + + + + + + + + + base64-encoded secret (for signing) or public key (for verifying). + + + + + + + ASCII-string used as secret key and public key. + + + + + + + + + Verify signatures + + + + + + Use particular signature mechanism. Currently + available ed25519 and dummy + signature types. + + The default is ed25519. + + + + + + Read key(s) from file filename. + + + + Valid for ed25519 signature type. + For ed25519 this file must contain base64-encoded + secret key(s) (for signing) or public key(s) (for verifying) per line. + + + + + + Redefine the system path, where to search files and subdirectories with + well-known and revoked keys. + + + + + diff --git a/man/ostree-summary.xml b/man/ostree-summary.xml index 387dacd7..8305b4e1 100644 --- a/man/ostree-summary.xml +++ b/man/ostree-summary.xml @@ -51,7 +51,7 @@ Boston, MA 02111-1307, USA. - ostree summary --gpg-sign=KEYID --gpg-homedir=HOMEDIR --update --add-metadata=KEY=VALUE + ostree summary --gpg-sign=KEYID --gpg-homedir=HOMEDIR --sign=KEYID --sign-type=ENGINE --update --add-metadata=KEY=VALUE @@ -139,6 +139,39 @@ Boston, MA 02111-1307, USA. + + =ENGINE + + Use particular signature engine. Currently + available ed25519 and dummy + signature types. + + The default is ed25519. + + + + + ="KEY-ID" + + There KEY-ID is: + + + + + base64-encoded secret key for commit signing. + + + + + + + ASCII-string used as secret key. + + + + + + diff --git a/src/libostree/libostree-devel.sym b/src/libostree/libostree-devel.sym index 3d5fd3bc..4348ab8d 100644 --- a/src/libostree/libostree-devel.sym +++ b/src/libostree/libostree-devel.sym @@ -21,6 +21,21 @@ LIBOSTREE_2020.2 { global: ostree_repo_commit_modifier_set_sepolicy_from_commit; + someostree_symbol_deleteme; + ostree_sign_get_type; + ostree_sign_list_names; + ostree_sign_commit; + ostree_sign_commit_verify; + ostree_sign_data; + ostree_sign_data_verify; + ostree_sign_get_by_name; + ostree_sign_get_name; + ostree_sign_clear_keys; + ostree_sign_load_pk; + ostree_sign_set_pk; + ostree_sign_add_pk; + ostree_sign_set_sk; + ostree_sign_summary; } LIBOSTREE_2020.1; /* Stub section for the stable release *after* this development one; don't diff --git a/src/libostree/ostree-autocleanups.h b/src/libostree/ostree-autocleanups.h index c9692ebe..14017012 100644 --- a/src/libostree/ostree-autocleanups.h +++ b/src/libostree/ostree-autocleanups.h @@ -73,6 +73,7 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderOverride, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeRepoFinderResult, ostree_repo_finder_result_free) G_DEFINE_AUTO_CLEANUP_FREE_FUNC (OstreeRepoFinderResultv, ostree_repo_finder_result_freev, NULL) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeSign, g_object_unref) #endif G_END_DECLS diff --git a/src/libostree/ostree-repo-pull.c b/src/libostree/ostree-repo-pull.c index a0f6db02..454eaf8a 100644 --- a/src/libostree/ostree-repo-pull.c +++ b/src/libostree/ostree-repo-pull.c @@ -54,6 +54,8 @@ #include #endif +#include "ostree-sign.h" + #define OSTREE_MESSAGE_FETCH_COMPLETE_ID SD_ID128_MAKE(75,ba,3d,eb,0a,f0,41,a9,a4,62,72,ff,85,d9,e7,3e) #define OSTREE_REPO_PULL_CONTENT_PRIORITY (OSTREE_FETCHER_DEFAULT_PRIORITY) @@ -105,6 +107,8 @@ typedef struct { gboolean gpg_verify; gboolean gpg_verify_summary; + gboolean sign_verify; + gboolean sign_verify_summary; gboolean require_static_deltas; gboolean disable_static_deltas; gboolean has_tombstone_commits; @@ -1466,6 +1470,163 @@ process_verify_result (OtPullData *pull_data, } #endif /* OSTREE_DISABLE_GPGME */ +/* _load_public_keys: + * + * Load public keys according remote's configuration: + * inlined key passed via config option `verification-key` or + * file name with public keys via `verification-file` option. + * + * If both options are set then load all all public keys + * both from file and inlined in config. + * + * Returns: %FALSE if any source is configured but nothing has been loaded. + * Returns: %TRUE if no configuration or any key loaded. + * */ +static gboolean +_load_public_keys (OstreeSign *sign, + OstreeRepo *repo, + const gchar *remote_name, + GError **error) +{ + + g_autofree gchar *pk_ascii = NULL; + g_autofree gchar *pk_file = NULL; + gboolean loaded_from_file = TRUE; + gboolean loaded_inlined = TRUE; + g_autoptr (GError) verification_error = NULL; + + glnx_throw (&verification_error, "no public keys loaded"); + + ostree_repo_get_remote_option (repo, + remote_name, + "verification-file", NULL, + &pk_file, NULL); + + ostree_repo_get_remote_option (repo, + remote_name, + "verification-key", NULL, + &pk_ascii, NULL); + + /* return TRUE if there is no configuration for remote */ + if ((pk_file == NULL) &&(pk_ascii == NULL)) + { + /* It is expected what remote may have verification file as + * a part of configuration. Hence there is not a lot of sense + * for automatic resolve of per-remote keystore file as it + * used in find_keyring () for GPG. + * If it is needed to add the similar mechanism, it is preferable + * to pass the path to ostree_sign_load_pk () via GVariant options + * and call it here for loading with method and file structure + * specific for signature type. + */ + return TRUE; + } + + if (pk_file != NULL) + { + g_autoptr (GError) local_error = NULL; + g_autoptr (GVariantBuilder) builder = NULL; + g_autoptr (GVariant) options = NULL; + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (builder, "{sv}", "filename", g_variant_new_string (pk_file)); + options = g_variant_builder_end (builder); + + if (ostree_sign_load_pk (sign, options, &local_error)) + loaded_from_file = TRUE; + else + { + g_debug("Unable to load public keys for '%s' from file '%s': %s", + ostree_sign_get_name(sign), pk_file, local_error->message); + /* Save error message for better reason detection later if needed */ + glnx_prefix_error (&verification_error, "%s", local_error->message); + } + } + + if (pk_ascii != NULL) + { + g_autoptr (GError) local_error = NULL; + g_autoptr (GVariant) pk = g_variant_new_string(pk_ascii); + + /* Add inlined public key */ + if (loaded_from_file) + loaded_inlined = ostree_sign_add_pk (sign, pk, &local_error); + else + loaded_inlined = ostree_sign_set_pk (sign, pk, &local_error); + + if (!loaded_inlined) + { + g_debug("Unable to load public key '%s' for '%s': %s", + pk_ascii, ostree_sign_get_name (sign), local_error->message); + + /* Save error message for better reason detection later if needed */ + glnx_prefix_error (&verification_error, "%s", local_error->message); + } + } + + /* Return true if able to load from any source */ + if (loaded_from_file || loaded_inlined) + return TRUE; + + return glnx_throw (error, "%s", verification_error->message); +} + +static gboolean +_ostree_repo_sign_verify (OstreeRepo *repo, + const gchar *remote_name, + GBytes *signed_data, + GVariant *metadata, + GError **error) +{ + /* list all signature types in detached metadata and check if signed by any? */ + g_auto (GStrv) names = ostree_sign_list_names(); + g_autoptr (GError) verification_error = NULL; + + glnx_throw (&verification_error, "signed with unknown key"); + + for (char **iter=names; iter && *iter; iter++) + { + g_autoptr (OstreeSign) sign = NULL; + g_autoptr (GVariant) signatures = NULL; + const gchar *signature_key = NULL; + GVariantType *signature_format = NULL; + g_autoptr (GError) local_error = NULL; + + if ((sign = ostree_sign_get_by_name (*iter, &local_error)) == NULL) + continue; + + signature_key = ostree_sign_metadata_key (sign); + signature_format = (GVariantType *) ostree_sign_metadata_format (sign); + + signatures = g_variant_lookup_value (metadata, + signature_key, + signature_format); + + /* If not found signatures for requested signature subsystem */ + if (!signatures) + continue; + + /* Try to load public key(s) according remote's configuration */ + if (_load_public_keys (sign, repo, remote_name, &local_error)) + { + /* Return true if any signature fit to pre-loaded public keys. + * If no keys configured -- then system configuration will be used */ + if (ostree_sign_data_verify (sign, + signed_data, + signatures, + &local_error)) + return TRUE; + } + + /* Save error message for better reason detection later if needed */ + glnx_prefix_error (&verification_error, "%s", local_error->message); + } + + /* In case if there were no signatures of known type + * or metadata contains invalid data */ + return glnx_throw (error, "%s", verification_error->message); +} + static gboolean ostree_verify_unwritten_commit (OtPullData *pull_data, const char *checksum, @@ -1475,21 +1636,24 @@ ostree_verify_unwritten_commit (OtPullData *pull_data, GCancellable *cancellable, GError **error) { + + if (pull_data->gpg_verify || pull_data->sign_verify) + /* Shouldn't happen, but see comment in process_verify_result() */ + if (g_hash_table_contains (pull_data->verified_commits, checksum)) + return TRUE; + + g_autoptr(GBytes) signed_data = g_variant_get_data_as_bytes (commit); + #ifndef OSTREE_DISABLE_GPGME if (pull_data->gpg_verify) { const char *keyring_remote = NULL; - /* Shouldn't happen, but see comment in process_verify_result() */ - if (g_hash_table_contains (pull_data->verified_commits, checksum)) - return TRUE; - if (ref != NULL) keyring_remote = g_hash_table_lookup (pull_data->ref_keyring_map, ref); if (keyring_remote == NULL) keyring_remote = pull_data->remote_name; - g_autoptr(GBytes) signed_data = g_variant_get_data_as_bytes (commit); g_autoptr(OstreeGpgVerifyResult) result = _ostree_repo_gpg_verify_with_metadata (pull_data->repo, signed_data, detached_metadata, @@ -1500,6 +1664,20 @@ ostree_verify_unwritten_commit (OtPullData *pull_data, } #endif /* OSTREE_DISABLE_GPGME */ + if (pull_data->sign_verify) + { + /* Nothing to check if detached metadata is absent */ + if (detached_metadata == NULL) + return glnx_throw (error, "Can't verify commit without detached metadata"); + + if (!_ostree_repo_sign_verify (pull_data->repo, pull_data->remote_name, signed_data, detached_metadata, error)) + return glnx_prefix_error (error, "Can't verify commit"); + + /* Mark the commit as verified to avoid double verification + * see process_verify_result () for rationale */ + g_hash_table_add (pull_data->verified_commits, g_strdup (checksum)); + } + return TRUE; } @@ -1829,6 +2007,44 @@ scan_commit_object (OtPullData *pull_data, } #endif /* OSTREE_DISABLE_GPGME */ + if (pull_data->sign_verify && + !g_hash_table_contains (pull_data->verified_commits, checksum)) + { + gboolean ret = FALSE; + g_autoptr (GError) verification_error = NULL; + + /* list all signature types in detached metadata and check if signed by any? */ + g_auto (GStrv) names = ostree_sign_list_names(); + for (char **iter=names; !ret && iter && *iter; iter++) + { + g_autoptr (OstreeSign) sign = NULL; + g_autoptr (GError) local_error = NULL; + + if ((sign = ostree_sign_get_by_name (*iter, &local_error)) == NULL) + continue; + + /* Try to load public key(s) according remote's configuration */ + if (_load_public_keys (sign, pull_data->repo, pull_data->remote_name, &local_error)) + { + + /* Set return to true if any sign fit */ + if (ostree_sign_commit_verify (sign, + pull_data->repo, + checksum, + cancellable, + &local_error)) + ret = TRUE; + } + + /* Save error message for better reason detection later if needed */ + if (!ret) + glnx_prefix_error (&verification_error, "%s", local_error->message); + } + + if (!ret) + return glnx_throw (error, "Can't verify commit %s: %s", checksum, verification_error->message); + } + /* If we found a legacy transaction flag, assume we have to scan. * We always do a scan of dirtree objects; see * https://github.com/ostreedev/ostree/issues/543 @@ -3576,6 +3792,8 @@ ostree_repo_pull_with_options (OstreeRepo *self, g_autoptr(GSource) update_timeout = NULL; gboolean opt_gpg_verify_set = FALSE; gboolean opt_gpg_verify_summary_set = FALSE; + gboolean opt_sign_verify_set = FALSE; + gboolean opt_sign_verify_summary_set = FALSE; gboolean opt_collection_refs_set = FALSE; gboolean opt_n_network_retries_set = FALSE; gboolean opt_ref_keyring_map_set = FALSE; @@ -3610,6 +3828,10 @@ ostree_repo_pull_with_options (OstreeRepo *self, g_variant_lookup (options, "gpg-verify", "b", &pull_data->gpg_verify); opt_gpg_verify_summary_set = g_variant_lookup (options, "gpg-verify-summary", "b", &pull_data->gpg_verify_summary); + opt_sign_verify_set = + g_variant_lookup (options, "sign-verify", "b", &pull_data->sign_verify); + opt_sign_verify_summary_set = + g_variant_lookup (options, "sign-verify-summary", "b", &pull_data->sign_verify_summary); (void) g_variant_lookup (options, "depth", "i", &pull_data->maxdepth); (void) g_variant_lookup (options, "disable-static-deltas", "b", &pull_data->disable_static_deltas); (void) g_variant_lookup (options, "require-static-deltas", "b", &pull_data->require_static_deltas); @@ -3759,7 +3981,10 @@ ostree_repo_pull_with_options (OstreeRepo *self, /* For compatibility with pull-local, don't gpg verify local * pulls by default. */ - if ((pull_data->gpg_verify || pull_data->gpg_verify_summary) && + if ((pull_data->gpg_verify || + pull_data->gpg_verify_summary || + pull_data->sign_verify + ) && pull_data->remote_name == NULL) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, @@ -3774,7 +3999,6 @@ ostree_repo_pull_with_options (OstreeRepo *self, g_free (pull_data->remote_name); pull_data->remote_name = g_strdup (remote_name_or_baseurl); -#ifndef OSTREE_DISABLE_GPGME /* Fetch GPG verification settings from remote if it wasn't already * explicitly set in the options. */ if (!opt_gpg_verify_set) @@ -3786,7 +4010,18 @@ ostree_repo_pull_with_options (OstreeRepo *self, if (!ostree_repo_remote_get_gpg_verify_summary (self, pull_data->remote_name, &pull_data->gpg_verify_summary, error)) goto out; -#endif /* OSTREE_DISABLE_GPGME */ + /* Fetch verification settings from remote if it wasn't already + * explicitly set in the options. */ + if (!opt_sign_verify_set) + if (!ostree_repo_get_remote_boolean_option (self, pull_data->remote_name, + "sign-verify", FALSE, + &pull_data->sign_verify, error)) + goto out; + if (!opt_sign_verify_summary_set) + if (!ostree_repo_get_remote_boolean_option (self, pull_data->remote_name, + "sign-verify-summary", FALSE, + &pull_data->sign_verify_summary, error)) + goto out; /* NOTE: If changing this, see the matching implementation in * ostree-sysroot-upgrader.c @@ -4168,6 +4403,64 @@ ostree_repo_pull_with_options (OstreeRepo *self, } #endif /* OSTREE_DISABLE_GPGME */ + if (pull_data->sign_verify_summary) + { + if (!bytes_sig && pull_data->sign_verify_summary) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Signatures verification enabled, but no summary.sig found (use sign-verify-summary=false in remote config to disable)"); + goto out; + } + if (bytes_summary && bytes_sig) + { + g_autoptr(GVariant) signatures = NULL; + g_autoptr(GError) temp_error = NULL; + + signatures = g_variant_new_from_bytes (OSTREE_SUMMARY_SIG_GVARIANT_FORMAT, + bytes_sig, FALSE); + + + if (!_ostree_repo_sign_verify (pull_data->repo, pull_data->remote_name, bytes_summary, signatures, &temp_error)) + { + if (summary_from_cache) + { + /* The cached summary doesn't match, fetch a new one and verify again */ + if ((self->test_error_flags & OSTREE_REPO_TEST_ERROR_INVALID_CACHE) > 0) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Remote %s cached summary invalid and " + "OSTREE_REPO_TEST_ERROR_INVALID_CACHE specified", + pull_data->remote_name); + goto out; + } + else + g_debug ("Remote %s cached summary invalid, pulling new version", + pull_data->remote_name); + + summary_from_cache = FALSE; + g_clear_pointer (&bytes_summary, (GDestroyNotify)g_bytes_unref); + if (!_ostree_fetcher_mirrored_request_to_membuf (pull_data->fetcher, + pull_data->meta_mirrorlist, + "summary", + OSTREE_FETCHER_REQUEST_OPTIONAL_CONTENT, + pull_data->n_network_retries, + &bytes_summary, + OSTREE_MAX_METADATA_SIZE, + cancellable, error)) + goto out; + + if (!_ostree_repo_sign_verify (pull_data->repo, pull_data->remote_name, bytes_summary, signatures, error)) + goto out; + } + else + { + g_propagate_error (error, g_steal_pointer (&temp_error)); + goto out; + } + } + } + } + if (bytes_summary) { pull_data->summary_data = g_bytes_ref (bytes_summary); @@ -4648,22 +4941,26 @@ ostree_repo_pull_with_options (OstreeRepo *self, g_string_append_printf (msg, "libostree pull from '%s' for %u refs complete", pull_data->remote_name, g_hash_table_size (requested_refs_to_fetch)); - const char *verify_state; + const char *gpg_verify_state; #ifndef OSTREE_DISABLE_GPGME if (pull_data->gpg_verify_summary) { if (pull_data->gpg_verify) - verify_state = "summary+commit"; + gpg_verify_state = "summary+commit"; else - verify_state = "summary-only"; + gpg_verify_state = "summary-only"; } else - verify_state = (pull_data->gpg_verify ? "commit" : "disabled"); - g_string_append_printf (msg, "\nsecurity: GPG: %s ", verify_state); + gpg_verify_state = (pull_data->gpg_verify ? "commit" : "disabled"); + #else - verify_state = "disabled"; - g_string_append_printf (msg, "\nsecurity: %s ", verify_state); + gpg_verify_state = "disabled"; #endif /* OSTREE_DISABLE_GPGME */ + g_string_append_printf (msg, "\nsecurity: GPG: %s ", gpg_verify_state); + + const char *sign_verify_state; + sign_verify_state = (pull_data->sign_verify ? "commit" : "disabled"); + g_string_append_printf (msg, "\nsecurity: SIGN: %s ", sign_verify_state); OstreeFetcherURI *first_uri = pull_data->meta_mirrorlist->pdata[0]; g_autofree char *first_scheme = _ostree_fetcher_uri_get_scheme (first_uri); @@ -4699,7 +4996,8 @@ ostree_repo_pull_with_options (OstreeRepo *self, ot_journal_send ("MESSAGE=%s", msg->str, "MESSAGE_ID=" SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(OSTREE_MESSAGE_FETCH_COMPLETE_ID), "OSTREE_REMOTE=%s", pull_data->remote_name, - "OSTREE_GPG=%s", verify_state, + "OSTREE_SIGN=%s", sign_verify_state, + "OSTREE_GPG=%s", gpg_verify_state, "OSTREE_SECONDS=%u", n_seconds, "OSTREE_XFER_SIZE=%s", formatted_xferred, NULL); @@ -6024,6 +6322,8 @@ ostree_repo_pull_from_remotes_async (OstreeRepo *self, g_variant_dict_insert (&local_options_dict, "gpg-verify", "b", FALSE); #endif /* OSTREE_DISABLE_GPGME */ g_variant_dict_insert (&local_options_dict, "gpg-verify-summary", "b", FALSE); + g_variant_dict_insert (&local_options_dict, "sign-verify", "b", FALSE); + g_variant_dict_insert (&local_options_dict, "sign-verify-summary", "b", FALSE); g_variant_dict_insert (&local_options_dict, "inherit-transaction", "b", TRUE); if (result->remote->refspec_name != NULL) g_variant_dict_insert (&local_options_dict, "override-remote-name", "s", result->remote->refspec_name); @@ -6170,9 +6470,8 @@ ostree_repo_remote_fetch_summary_with_options (OstreeRepo *self, g_autofree char *metalink_url_string = NULL; g_autoptr(GBytes) summary = NULL; g_autoptr(GBytes) signatures = NULL; -#ifndef OSTREE_DISABLE_GPGME gboolean gpg_verify_summary; -#endif + gboolean sign_verify_summary; gboolean ret = FALSE; gboolean summary_is_from_cache; @@ -6194,37 +6493,72 @@ ostree_repo_remote_fetch_summary_with_options (OstreeRepo *self, error)) goto out; -#ifndef OSTREE_DISABLE_GPGME if (!ostree_repo_remote_get_gpg_verify_summary (self, name, &gpg_verify_summary, error)) goto out; - if (gpg_verify_summary && summary == NULL) + if (gpg_verify_summary) { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, - "GPG verification enabled, but no summary found (check that the configured URL in remote config is correct)"); - goto out; + if (summary == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "GPG verification enabled, but no summary found (check that the configured URL in remote config is correct)"); + goto out; + } + + if (signatures == NULL) + { + g_set_error (error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE, + "GPG verification enabled, but no summary signatures found (use gpg-verify-summary=false in remote config to disable)"); + goto out; + } + + /* Verify any summary signatures. */ + if (summary != NULL && signatures != NULL) + { + g_autoptr(OstreeGpgVerifyResult) result = NULL; + + result = ostree_repo_verify_summary (self, + name, + summary, + signatures, + cancellable, + error); + if (!ostree_gpg_verify_result_require_valid_signature (result, error)) + goto out; + } } - if (gpg_verify_summary && signatures == NULL) - { - g_set_error (error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE, - "GPG verification enabled, but no summary signatures found (use gpg-verify-summary=false in remote config to disable)"); + if (!ostree_repo_get_remote_boolean_option (self, name, "sign-verify-summary", + FALSE, &sign_verify_summary, error)) goto out; - } - /* Verify any summary signatures. */ - if (gpg_verify_summary && summary != NULL && signatures != NULL) + if (sign_verify_summary) { - g_autoptr(OstreeGpgVerifyResult) result = NULL; + if (summary == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Signature verification enabled, but no summary found (check that the configured URL in remote config is correct)"); + goto out; + } - result = ostree_repo_verify_summary (self, - name, - summary, - signatures, - cancellable, - error); - if (!ostree_gpg_verify_result_require_valid_signature (result, error)) - goto out; + if (signatures == NULL) + { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "Signature verification enabled, but no summary signatures found (use sign-verify-summary=false in remote config to disable)"); + goto out; + } + + /* Verify any summary signatures. */ + if (summary != NULL && signatures != NULL) + { + g_autoptr(GVariant) sig_variant = NULL; + + sig_variant = g_variant_new_from_bytes (OSTREE_SUMMARY_SIG_GVARIANT_FORMAT, + signatures, FALSE); + + if (!_ostree_repo_sign_verify (self, name, summary, sig_variant, error)) + goto out; + } } if (!summary_is_from_cache && summary && signatures) @@ -6248,10 +6582,6 @@ ostree_repo_remote_fetch_summary_with_options (OstreeRepo *self, } } -#else - g_message ("%s: GPG feature is disabled in a build time", __FUNCTION__); -#endif /* OSTREE_DISABLE_GPGME */ - if (out_summary != NULL) *out_summary = g_steal_pointer (&summary); diff --git a/src/libostree/ostree-repo.c b/src/libostree/ostree-repo.c index 40251aa2..f665106d 100644 --- a/src/libostree/ostree-repo.c +++ b/src/libostree/ostree-repo.c @@ -2028,17 +2028,8 @@ ostree_repo_remote_get_gpg_verify (OstreeRepo *self, return TRUE; } -#ifndef OSTREE_DISABLE_GPGME return ostree_repo_get_remote_boolean_option (self, name, "gpg-verify", TRUE, out_gpg_verify, error); -#else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - if (out_gpg_verify != NULL) - *out_gpg_verify = FALSE; - return FALSE; -#endif /* OSTREE_DISABLE_GPGME */ } /** @@ -2060,17 +2051,8 @@ ostree_repo_remote_get_gpg_verify_summary (OstreeRepo *self, gboolean *out_gpg_verify_summary, GError **error) { -#ifndef OSTREE_DISABLE_GPGME return ostree_repo_get_remote_boolean_option (self, name, "gpg-verify-summary", FALSE, out_gpg_verify_summary, error); -#else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - if (out_gpg_verify_summary != NULL) - *out_gpg_verify_summary = FALSE; - return FALSE; -#endif /* OSTREE_DISABLE_GPGME */ } /** @@ -2344,10 +2326,7 @@ out: return ret; #else /* OSTREE_DISABLE_GPGME */ - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - return FALSE; + return glnx_throw (error, "GPG feature is disabled in a build time"); #endif /* OSTREE_DISABLE_GPGME */ } @@ -4991,10 +4970,7 @@ ostree_repo_append_gpg_signature (OstreeRepo *self, return TRUE; #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - return FALSE; + return glnx_throw (error, "GPG feature is disabled in a build time"); #endif /* OSTREE_DISABLE_GPGME */ } @@ -5146,7 +5122,7 @@ ostree_repo_sign_commit (OstreeRepo *self, return TRUE; #else /* FIXME: Return false until refactoring */ - return FALSE; + return glnx_throw (error, "GPG feature is disabled in a build time"); #endif /* OSTREE_DISABLE_GPGME */ } @@ -5238,10 +5214,7 @@ ostree_repo_add_gpg_signature_summary (OstreeRepo *self, return TRUE; #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - return FALSE; + return glnx_throw (error, "GPG feature is disabled in a build time"); #endif /* OSTREE_DISABLE_GPGME */ } @@ -5515,10 +5488,7 @@ ostree_repo_verify_commit (OstreeRepo *self, return TRUE; #else /* FIXME: Return false until refactoring */ - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); - return FALSE; + return glnx_throw (error, "GPG feature is disabled in a build time"); #endif /* OSTREE_DISABLE_GPGME */ } @@ -5553,9 +5523,7 @@ ostree_repo_verify_commit_ext (OstreeRepo *self, cancellable, error); #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); + glnx_throw (error, "GPG feature is disabled in a build time"); return NULL; #endif /* OSTREE_DISABLE_GPGME */ } @@ -5592,9 +5560,7 @@ ostree_repo_verify_commit_for_remote (OstreeRepo *self, cancellable, error); #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); + glnx_throw (error, "GPG feature is disabled in a build time"); return NULL; #endif /* OSTREE_DISABLE_GPGME */ } @@ -5644,9 +5610,7 @@ ostree_repo_gpg_verify_data (OstreeRepo *self, cancellable, error); #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); + glnx_throw (error, "GPG feature is disabled in a build time"); return NULL; #endif /* OSTREE_DISABLE_GPGME */ } @@ -5692,9 +5656,7 @@ ostree_repo_verify_summary (OstreeRepo *self, cancellable, error); #else - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, - "'%s': GPG feature is disabled in a build time", - __FUNCTION__); + glnx_throw (error, "GPG feature is disabled in a build time"); return NULL; #endif /* OSTREE_DISABLE_GPGME */ } diff --git a/src/libostree/ostree-sign-dummy.c b/src/libostree/ostree-sign-dummy.c new file mode 100644 index 00000000..722d461b --- /dev/null +++ b/src/libostree/ostree-sign-dummy.c @@ -0,0 +1,177 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * + * 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 +#include "ostree-sign-dummy.h" +#include + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "OSTreeSign" + +#define OSTREE_SIGN_DUMMY_NAME "dummy" + +#define OSTREE_SIGN_METADATA_DUMMY_KEY "ostree.sign.dummy" +#define OSTREE_SIGN_METADATA_DUMMY_TYPE "aay" + +struct _OstreeSignDummy +{ + GObject parent; + gchar *sk_ascii; + gchar *pk_ascii; +}; + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeSignDummy, g_object_unref) +#endif + +static void +ostree_sign_dummy_iface_init (OstreeSignInterface *self); + +G_DEFINE_TYPE_WITH_CODE (OstreeSignDummy, _ostree_sign_dummy, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_SIGN, ostree_sign_dummy_iface_init)); + +static void +ostree_sign_dummy_iface_init (OstreeSignInterface *self) +{ + + self->get_name = ostree_sign_dummy_get_name; + self->data = ostree_sign_dummy_data; + self->data_verify = ostree_sign_dummy_data_verify; + self->metadata_key = ostree_sign_dummy_metadata_key; + self->metadata_format = ostree_sign_dummy_metadata_format; + self->set_sk = ostree_sign_dummy_set_sk; + self->set_pk = ostree_sign_dummy_set_pk; + /* Implementation for dummy engine just load the single public key */ + self->add_pk = ostree_sign_dummy_set_pk; +} + +static void +_ostree_sign_dummy_class_init (OstreeSignDummyClass *self) +{ +} + +static void +_ostree_sign_dummy_init (OstreeSignDummy *self) +{ + + self->sk_ascii = NULL; + self->pk_ascii = NULL; +} + +gboolean ostree_sign_dummy_set_sk (OstreeSign *self, GVariant *key, GError **error) +{ + + OstreeSignDummy *sign = _ostree_sign_dummy_get_instance_private(OSTREE_SIGN_DUMMY(self)); + + g_free(sign->sk_ascii); + + sign->sk_ascii = g_variant_dup_string (key, 0); + + return TRUE; +} + +gboolean ostree_sign_dummy_set_pk (OstreeSign *self, GVariant *key, GError **error) +{ + + OstreeSignDummy *sign = _ostree_sign_dummy_get_instance_private(OSTREE_SIGN_DUMMY(self)); + + g_free(sign->pk_ascii); + + sign->pk_ascii = g_variant_dup_string (key, 0); + + return TRUE; +} + +gboolean ostree_sign_dummy_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error) +{ + + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + OstreeSignDummy *sign = _ostree_sign_dummy_get_instance_private(OSTREE_SIGN_DUMMY(self)); + + *signature = g_bytes_new (sign->sk_ascii, strlen(sign->sk_ascii)); + + return TRUE; +} + +const gchar * ostree_sign_dummy_get_name (OstreeSign *self) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + return OSTREE_SIGN_DUMMY_NAME; +} + +const gchar * ostree_sign_dummy_metadata_key (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_DUMMY_KEY; +} + +const gchar * ostree_sign_dummy_metadata_format (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_DUMMY_TYPE; +} + +gboolean ostree_sign_dummy_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + OstreeSignDummy *sign = _ostree_sign_dummy_get_instance_private(OSTREE_SIGN_DUMMY(self)); + + if (signatures == NULL) + return glnx_throw (error, "signature: dummy: commit have no signatures of my type"); + + if (!g_variant_is_of_type (signatures, (GVariantType *) OSTREE_SIGN_METADATA_DUMMY_TYPE)) + return glnx_throw (error, "signature: dummy: wrong type passed for verification"); + + for (gsize i = 0; i < g_variant_n_children(signatures); i++) + { + g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); + g_autoptr (GBytes) signature = g_variant_get_data_as_bytes(child); + + gsize sign_size = 0; + g_bytes_get_data (signature, &sign_size); + g_autofree gchar *sign_ascii = g_strndup(g_bytes_get_data (signature, NULL), sign_size); + g_debug("Read signature %d: %s", (gint)i, sign_ascii); + g_debug("Stored signature %d: %s", (gint)i, sign->pk_ascii); + + if (!g_strcmp0(sign_ascii, sign->pk_ascii)) + return TRUE; + else + return glnx_throw (error, "signature: dummy: incorrect signature %" G_GSIZE_FORMAT, i); + } + + return glnx_throw (error, "signature: dummy: no signatures"); +} diff --git a/src/libostree/ostree-sign-dummy.h b/src/libostree/ostree-sign-dummy.h new file mode 100644 index 00000000..c37bcdfa --- /dev/null +++ b/src/libostree/ostree-sign-dummy.h @@ -0,0 +1,76 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * + * 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. + * + * Authors: + * - Denis Pynkin (d4s) + */ + +#pragma once + +#include "ostree-sign.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_SIGN_DUMMY (_ostree_sign_dummy_get_type ()) + +GType _ostree_sign_dummy_get_type (void); + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +typedef struct _OstreeSignDummy OstreeSignDummy; +typedef struct { GObjectClass parent_class; } OstreeSignDummyClass; + +static inline OstreeSignDummy *OSTREE_SIGN_DUMMY (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, _ostree_sign_dummy_get_type (), OstreeSignDummy); } +static inline gboolean OSTREE_IS_SIGN_DUMMY (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, _ostree_sign_dummy_get_type ()); } + +G_GNUC_END_IGNORE_DEPRECATIONS + +/* Have to use glib-2.44 for this +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeSignDummy, + ostree_sign_dummy, + OSTREE, + SIGN_DUMMY, + GObject) +*/ + +const gchar * ostree_sign_dummy_get_name (OstreeSign *self); + +gboolean ostree_sign_dummy_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error); + +gboolean ostree_sign_dummy_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error); + +const gchar * ostree_sign_dummy_metadata_key (OstreeSign *self); +const gchar * ostree_sign_dummy_metadata_format (OstreeSign *self); + +gboolean ostree_sign_dummy_set_sk (OstreeSign *self, GVariant *key, GError **error); +gboolean ostree_sign_dummy_set_pk (OstreeSign *self, GVariant *key, GError **error); +gboolean ostree_sign_dummy_add_pk (OstreeSign *self, GVariant *key, GError **error); + +G_END_DECLS + diff --git a/src/libostree/ostree-sign-ed25519.c b/src/libostree/ostree-sign-ed25519.c new file mode 100644 index 00000000..8df61aed --- /dev/null +++ b/src/libostree/ostree-sign-ed25519.c @@ -0,0 +1,647 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ +/* + * Copyright © 2019 Collabora Ltd. + * + * 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. + * + * Authors: + * - Denis Pynkin (d4s) + */ + +#include "config.h" + +#include +#include "ostree-sign-ed25519.h" +#ifdef HAVE_LIBSODIUM +#include +#endif + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "OSTreeSign" + +#define OSTREE_SIGN_ED25519_NAME "ed25519" + +#define OSTREE_SIGN_METADATA_ED25519_KEY "ostree.sign.ed25519" +#define OSTREE_SIGN_METADATA_ED25519_TYPE "aay" + +typedef enum +{ + ED25519_OK, + ED25519_NOT_SUPPORTED, + ED25519_FAILED_INITIALIZATION +} ed25519_state; + +struct _OstreeSignEd25519 +{ + GObject parent; + ed25519_state state; + guchar *secret_key; + GList *public_keys; + GList *revoked_keys; +}; + +#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC +G_DEFINE_AUTOPTR_CLEANUP_FUNC (OstreeSignEd25519, g_object_unref) +#endif + +static void +ostree_sign_ed25519_iface_init (OstreeSignInterface *self); + +G_DEFINE_TYPE_WITH_CODE (OstreeSignEd25519, _ostree_sign_ed25519, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSTREE_TYPE_SIGN, ostree_sign_ed25519_iface_init)); + +static void +ostree_sign_ed25519_iface_init (OstreeSignInterface *self) +{ + + self->data = ostree_sign_ed25519_data; + self->data_verify = ostree_sign_ed25519_data_verify; + self->get_name = ostree_sign_ed25519_get_name; + self->metadata_key = ostree_sign_ed25519_metadata_key; + self->metadata_format = ostree_sign_ed25519_metadata_format; + self->clear_keys = ostree_sign_ed25519_clear_keys; + self->set_sk = ostree_sign_ed25519_set_sk; + self->set_pk = ostree_sign_ed25519_set_pk; + self->add_pk = ostree_sign_ed25519_add_pk; + self->load_pk = ostree_sign_ed25519_load_pk; +} + +static void +_ostree_sign_ed25519_class_init (OstreeSignEd25519Class *self) +{ +} + +static void +_ostree_sign_ed25519_init (OstreeSignEd25519 *self) +{ + + self->state = ED25519_OK; + self->secret_key = NULL; + self->public_keys = NULL; + self->revoked_keys = NULL; + +#ifdef HAVE_LIBSODIUM + if (sodium_init() < 0) + self->state = ED25519_FAILED_INITIALIZATION; +#else + self->state = ED25519_NOT_SUPPORTED; +#endif /* HAVE_LIBSODIUM */ +} + +static gboolean +_ostree_sign_ed25519_is_initialized (OstreeSignEd25519 *self, GError **error) +{ + switch (self->state) + { + case ED25519_OK: + break; + case ED25519_NOT_SUPPORTED: + return glnx_throw(error, "ed25519: engine is not supported"); + case ED25519_FAILED_INITIALIZATION: + return glnx_throw(error, "ed25519: libsodium library isn't initialized properly"); + } + + return TRUE; +} + +gboolean ostree_sign_ed25519_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error) +{ + + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + +#ifdef HAVE_LIBSODIUM + guchar *sig = NULL; +#endif + + if (!_ostree_sign_ed25519_is_initialized (sign, error)) + return FALSE; + + if (sign->secret_key == NULL) + return glnx_throw (error, "Not able to sign: secret key is not set"); + +#ifdef HAVE_LIBSODIUM + unsigned long long sig_size = 0; + + sig = g_malloc0(crypto_sign_BYTES); + + if (crypto_sign_detached (sig, + &sig_size, + g_bytes_get_data (data, NULL), + g_bytes_get_size (data), + sign->secret_key)) + { + return glnx_throw (error, "Not able to sign: fail to sign the object"); + } + + *signature = g_bytes_new_take (sig, sig_size); + return TRUE; +#endif /* HAVE_LIBSODIUM */ + return FALSE; +} + +#ifdef HAVE_LIBSODIUM +static gint +_compare_ed25519_keys(gconstpointer a, gconstpointer b) { + return memcmp (a, b, crypto_sign_PUBLICKEYBYTES); +} +#endif + +gboolean ostree_sign_ed25519_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + g_return_val_if_fail (data != NULL, FALSE); + + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + + if (!_ostree_sign_ed25519_is_initialized (sign, error)) + return FALSE; + + if (signatures == NULL) + return glnx_throw (error, "ed25519: commit have no signatures of my type"); + + if (!g_variant_is_of_type (signatures, (GVariantType *) OSTREE_SIGN_METADATA_ED25519_TYPE)) + return glnx_throw (error, "ed25519: wrong type passed for verification"); + +#ifdef HAVE_LIBSODIUM + /* If no keys pre-loaded then, + * try to load public keys from storage(s) */ + if (sign->public_keys == NULL) + { + g_autoptr (GVariantBuilder) builder = NULL; + g_autoptr (GVariant) options = NULL; + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + options = g_variant_builder_end (builder); + + if (!ostree_sign_ed25519_load_pk (self, options, error)) + return FALSE; + } + + g_debug ("verify: data hash = 0x%x", g_bytes_hash(data)); + + for (gsize i = 0; i < g_variant_n_children(signatures); i++) + { + g_autoptr (GVariant) child = g_variant_get_child_value (signatures, i); + g_autoptr (GBytes) signature = g_variant_get_data_as_bytes(child); + + g_autofree char * hex = g_malloc0 (crypto_sign_PUBLICKEYBYTES*2 + 1); + + g_debug("Read signature %d: %s", (gint)i, g_variant_print(child, TRUE)); + + for (GList *public_key = sign->public_keys; + public_key != NULL; + public_key = public_key->next) + { + + /* TODO: use non-list for tons of revoked keys? */ + if (g_list_find_custom (sign->revoked_keys, public_key->data, _compare_ed25519_keys) != NULL) + { + g_debug("Skip revoked key '%s'", + sodium_bin2hex (hex, crypto_sign_PUBLICKEYBYTES*2+1, public_key->data, crypto_sign_PUBLICKEYBYTES)); + continue; + } + + if (crypto_sign_verify_detached ((guchar *) g_variant_get_data (child), + g_bytes_get_data (data, NULL), + g_bytes_get_size (data), + public_key->data) != 0) + { + /* Incorrect signature! */ + g_debug("Signature couldn't be verified with key '%s'", + sodium_bin2hex (hex, crypto_sign_PUBLICKEYBYTES*2+1, public_key->data, crypto_sign_PUBLICKEYBYTES)); + } + else + { + g_debug ("Signature verified successfully with key '%s'", + sodium_bin2hex (hex, crypto_sign_PUBLICKEYBYTES*2+1, public_key->data, crypto_sign_PUBLICKEYBYTES)); + return TRUE; + } + } + } + + return glnx_throw (error, "Not able to verify: no valid signatures found"); +#endif /* HAVE_LIBSODIUM */ + + return FALSE; +} + +const gchar * ostree_sign_ed25519_get_name (OstreeSign *self) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + return OSTREE_SIGN_ED25519_NAME; +} + +const gchar * ostree_sign_ed25519_metadata_key (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_ED25519_KEY; +} + +const gchar * ostree_sign_ed25519_metadata_format (OstreeSign *self) +{ + + return OSTREE_SIGN_METADATA_ED25519_TYPE; +} + +gboolean ostree_sign_ed25519_clear_keys (OstreeSign *self, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + + if (!_ostree_sign_ed25519_is_initialized (sign, error)) + return FALSE; + +#ifdef HAVE_LIBSODIUM + /* Clear secret key */ + if (sign->secret_key != NULL) + { + memset (sign->secret_key, 0, crypto_sign_SECRETKEYBYTES); + g_free (sign->secret_key); + sign->secret_key = NULL; + } + + /* Clear already loaded trusted keys */ + if (sign->public_keys != NULL) + { + g_list_free_full (sign->public_keys, g_free); + sign->public_keys = NULL; + } + + /* Clear already loaded revoked keys */ + if (sign->revoked_keys != NULL) + { + g_list_free_full (sign->revoked_keys, g_free); + sign->revoked_keys = NULL; + } + + return TRUE; +#endif /* HAVE_LIBSODIUM */ + + return FALSE; +} + +/* Support 2 representations: + * base64 ascii -- secret key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean ostree_sign_ed25519_set_sk (OstreeSign *self, + GVariant *secret_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + + if (!ostree_sign_ed25519_clear_keys (self, error)) + return FALSE; + +#ifdef HAVE_LIBSODIUM + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + + gsize n_elements = 0; + + if (g_variant_is_of_type (secret_key, G_VARIANT_TYPE_STRING)) + { + const gchar *sk_ascii = g_variant_get_string (secret_key, NULL); + sign->secret_key = g_base64_decode (sk_ascii, &n_elements); + } + else if (g_variant_is_of_type (secret_key, G_VARIANT_TYPE_BYTESTRING)) + { + sign->secret_key = (guchar *) g_variant_get_fixed_array (secret_key, &n_elements, sizeof(guchar)); + } + else + { + return glnx_throw (error, "Unknown ed25519 secret key type"); + } + + if (n_elements != crypto_sign_SECRETKEYBYTES) + return glnx_throw (error, "Incorrect ed25519 secret key"); + + return TRUE; +#endif /* HAVE_LIBSODIUM */ + + return FALSE; +} + +/* Support 2 representations: + * base64 ascii -- public key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean ostree_sign_ed25519_set_pk (OstreeSign *self, + GVariant *public_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + if (!ostree_sign_ed25519_clear_keys (self, error)) + return FALSE; + + return ostree_sign_ed25519_add_pk (self, public_key, error); +} + +/* Support 2 representations: + * base64 ascii -- public key is passed as string + * raw key -- key is passed as bytes array + * */ +gboolean ostree_sign_ed25519_add_pk (OstreeSign *self, + GVariant *public_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + + if (!_ostree_sign_ed25519_is_initialized (sign, error)) + return FALSE; + +#ifdef HAVE_LIBSODIUM + gpointer key = NULL; + gsize n_elements = 0; + + if (g_variant_is_of_type (public_key, G_VARIANT_TYPE_STRING)) + { + const gchar *pk_ascii = g_variant_get_string (public_key, NULL); + key = g_base64_decode (pk_ascii, &n_elements); + } + else if (g_variant_is_of_type (public_key, G_VARIANT_TYPE_BYTESTRING)) + { + key = (gpointer) g_variant_get_fixed_array (public_key, &n_elements, sizeof(guchar)); + } + else + { + return glnx_throw (error, "Unknown ed25519 public key type"); + } + + g_autofree char *hex = g_malloc0 (crypto_sign_PUBLICKEYBYTES*2 + 1); + g_debug ("Read ed25519 public key = %s", sodium_bin2hex (hex, crypto_sign_PUBLICKEYBYTES*2+1, key, n_elements)); + + if (n_elements != crypto_sign_PUBLICKEYBYTES) + return glnx_throw (error, "Incorrect ed25519 public key"); + + if (g_list_find_custom (sign->public_keys, key, _compare_ed25519_keys) == NULL) + { + gpointer newkey = g_memdup (key, n_elements); + sign->public_keys = g_list_prepend (sign->public_keys, newkey); + } + +#endif /* HAVE_LIBSODIUM */ + return TRUE; +} + +#ifdef HAVE_LIBSODIUM +/* Add revoked public key */ +static gboolean +_ed25519_add_revoked (OstreeSign *self, + GVariant *revoked_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + if (!g_variant_is_of_type (revoked_key, G_VARIANT_TYPE_STRING)) + return glnx_throw (error, "Unknown ed25519 revoked key type"); + + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + + const gchar *rk_ascii = g_variant_get_string (revoked_key, NULL); + gsize n_elements = 0; + gpointer key = g_base64_decode (rk_ascii, &n_elements); + + g_autofree char * hex = g_malloc0 (crypto_sign_PUBLICKEYBYTES*2 + 1); + g_debug ("Read ed25519 revoked key = %s", sodium_bin2hex (hex, crypto_sign_PUBLICKEYBYTES*2+1, key, n_elements)); + + if (n_elements != crypto_sign_PUBLICKEYBYTES) + { + return glnx_throw (error, "Incorrect ed25519 revoked key"); + } + + if (g_list_find_custom (sign->revoked_keys, key, _compare_ed25519_keys) == NULL) + { + gpointer newkey = g_memdup (key, n_elements); + sign->revoked_keys = g_list_prepend (sign->revoked_keys, newkey); + } + + return TRUE; +} +#endif /* HAVE_LIBSODIUM */ + + +static gboolean +_load_pk_from_stream (OstreeSign *self, + GDataInputStream *key_data_in, + gboolean trusted, + GError **error) +{ + g_return_val_if_fail (key_data_in, FALSE); +#ifdef HAVE_LIBSODIUM + gboolean ret = FALSE; + + /* 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) pk = NULL; + gboolean added = FALSE; + + if (*error != NULL) + return FALSE; + + if (line == NULL) + return ret; + + /* Read the key itself */ + /* base64 encoded key */ + pk = g_variant_new_string (line); + + if (trusted) + added = ostree_sign_ed25519_add_pk (self, pk, error); + else + added = _ed25519_add_revoked (self, pk, error); + + g_debug ("%s %s key: %s", + added ? "Added" : "Invalid", + trusted ? "public" : "revoked", + line); + + /* Mark what we load at least one key */ + if (added) + ret = TRUE; + } +#endif /* HAVE_LIBSODIUM */ + return FALSE; +} + +static gboolean +_load_pk_from_file (OstreeSign *self, + const gchar *filename, + gboolean trusted, + GError **error) +{ + g_debug ("Processing file '%s'", filename); + + g_autoptr (GFile) keyfile = NULL; + g_autoptr (GFileInputStream) key_stream_in = NULL; + g_autoptr (GDataInputStream) key_data_in = NULL; + + if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR)) + { + g_debug ("Can't open file '%s' with public keys", filename); + return glnx_throw (error, "File object '%s' is not a regular file", filename); + } + + keyfile = g_file_new_for_path (filename); + 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); + + if (!_load_pk_from_stream (self, key_data_in, trusted, error)) + { + if (error == NULL || *error == NULL) + return glnx_throw (error, + "signature: ed25519: no valid keys in file '%s'", + filename); + } + + return TRUE; +} + +static gboolean +_ed25519_load_pk (OstreeSign *self, + GVariant *options, + gboolean trusted, + GError **error) +{ + + gboolean ret = FALSE; + const gchar *custom_dir = NULL; + + g_autoptr (GPtrArray) base_dirs = g_ptr_array_new_with_free_func (g_free); + g_autoptr (GPtrArray) ed25519_files = g_ptr_array_new_with_free_func (g_free); + + if (g_variant_lookup (options, "basedir", "&s", &custom_dir)) + { + /* Add custom directory */ + g_ptr_array_add (base_dirs, g_strdup (custom_dir)); + } + else + { + /* Default paths where to find files with public keys */ + g_ptr_array_add (base_dirs, g_strdup ("/etc/ostree")); + g_ptr_array_add (base_dirs, g_strdup (DATADIR "/ostree")); + } + + /* Scan all well-known directories and construct the list with file names to scan keys */ + for (gint i=0; i < base_dirs->len; i++) + { + gchar *base_name = NULL; + g_autofree gchar *base_dir = NULL; + g_autoptr (GDir) dir = NULL; + + base_name = g_build_filename ((gchar *)g_ptr_array_index (base_dirs, i), + trusted ? "trusted.ed25519" : "revoked.ed25519", + NULL); + + g_debug ("Check ed25519 keys from file: %s", base_name); + g_ptr_array_add (ed25519_files, base_name); + + base_dir = g_strconcat (base_name, ".d", NULL); + dir = g_dir_open (base_dir, 0, error); + if (dir == NULL) + { + g_clear_error (error); + continue; + } + const gchar *entry = NULL; + while ((entry = g_dir_read_name (dir)) != NULL) + { + gchar *filename = g_build_filename (base_dir, entry, NULL); + g_debug ("Check ed25519 keys from file: %s", filename); + g_ptr_array_add (ed25519_files, filename); + } + } + + /* Scan all well-known files */ + for (gint i=0; i < ed25519_files->len; i++) + { + if (!_load_pk_from_file (self, (gchar *)g_ptr_array_index (ed25519_files, i), trusted, error)) + { + g_debug ("Problem with loading ed25519 %s keys from `%s`", + trusted ? "public" : "revoked", + (gchar *)g_ptr_array_index (ed25519_files, i)); + g_clear_error(error); + } + else + ret = TRUE; + } + + if (!ret && (error == NULL || *error == NULL)) + return glnx_throw (error, "signature: ed25519: no keys loaded"); + + return ret; +} + +/* + * options argument should be a{sv}: + * - filename -- single file to use to load keys from; + * - basedir -- directory containing subdirectories + * 'trusted.ed25519.d' and 'revoked.ed25519.d' with appropriate + * public keys. Used for testing and re-definition of system-wide + * directories if defaults are not suitable for any reason. + */ +gboolean +ostree_sign_ed25519_load_pk (OstreeSign *self, + GVariant *options, + GError **error) +{ + + const gchar *filename = NULL; + + OstreeSignEd25519 *sign = _ostree_sign_ed25519_get_instance_private(OSTREE_SIGN_ED25519(self)); + if (!_ostree_sign_ed25519_is_initialized (sign, error)) + return FALSE; + + /* Read keys only from single file provided */ + if (g_variant_lookup (options, "filename", "&s", &filename)) + return _load_pk_from_file (self, filename, TRUE, error); + + /* Load public keys from well-known directories and files */ + if (!_ed25519_load_pk (self, options, TRUE, error)) + return FALSE; + + /* Load untrusted keys from well-known directories and files + * Ignore the failure from this function -- it is expected to have + * empty list of revoked keys. + * */ + if (!_ed25519_load_pk (self, options, FALSE, error)) + g_clear_error(error); + + return TRUE; +} diff --git a/src/libostree/ostree-sign-ed25519.h b/src/libostree/ostree-sign-ed25519.h new file mode 100644 index 00000000..76c7e14d --- /dev/null +++ b/src/libostree/ostree-sign-ed25519.h @@ -0,0 +1,90 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * + * 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. + * + * Authors: + * - Denis Pynkin (d4s) + */ + +#pragma once + +#include "ostree-sign.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_SIGN_ED25519 (_ostree_sign_ed25519_get_type ()) + +GType _ostree_sign_ed25519_get_type (void); + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +typedef struct _OstreeSignEd25519 OstreeSignEd25519; +typedef struct { GObjectClass parent_class; } OstreeSignEd25519Class; + +static inline OstreeSignEd25519 *OSTREE_SIGN_ED25519 (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, _ostree_sign_ed25519_get_type (), OstreeSignEd25519); } +static inline gboolean OSTREE_IS_SIGN_ED25519 (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, _ostree_sign_ed25519_get_type ()); } + +G_GNUC_END_IGNORE_DEPRECATIONS + +/* Have to use glib-2.44 for this +_OSTREE_PUBLIC +G_DECLARE_FINAL_TYPE (OstreeSignEd25519, + ostree_sign_ed25519, + OSTREE, + SIGN_ED25519, + GObject) +*/ + +gboolean ostree_sign_ed25519_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error); + +gboolean ostree_sign_ed25519_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error); + +const gchar * ostree_sign_ed25519_get_name (OstreeSign *self); +const gchar * ostree_sign_ed25519_metadata_key (OstreeSign *self); +const gchar * ostree_sign_ed25519_metadata_format (OstreeSign *self); + +gboolean ostree_sign_ed25519_clear_keys (OstreeSign *self, + GError **error); + +gboolean ostree_sign_ed25519_set_sk (OstreeSign *self, + GVariant *secret_key, + GError **error); + +gboolean ostree_sign_ed25519_set_pk (OstreeSign *self, + GVariant *public_key, + GError **error); + +gboolean ostree_sign_ed25519_add_pk (OstreeSign *self, + GVariant *public_key, + GError **error); + +gboolean ostree_sign_ed25519_load_pk (OstreeSign *self, + GVariant *options, + GError **error); + +G_END_DECLS + diff --git a/src/libostree/ostree-sign.c b/src/libostree/ostree-sign.c new file mode 100644 index 00000000..281fabc8 --- /dev/null +++ b/src/libostree/ostree-sign.c @@ -0,0 +1,670 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * + * 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. + * + */ + +/** + * SECTION:ostree-sign + * @title: Signature management + * @short_description: Sign and verify commits + * + * An #OstreeSign interface allows to select and use any available engine + * for signing or verifying the commit object or summary file. + */ + +#include "config.h" + +#include +#include +#include +#include "libglnx.h" +#include "otutil.h" + +#include "ostree-autocleanups.h" +#include "ostree-core.h" +#include "ostree-sign.h" +#include "ostree-sign-dummy.h" +#ifdef HAVE_LIBSODIUM +#include "ostree-sign-ed25519.h" +#endif + +#include "ostree-autocleanups.h" +#include "ostree-repo-private.h" + +#undef G_LOG_DOMAIN +#define G_LOG_DOMAIN "OSTreeSign" + +typedef struct +{ + gchar *name; + GType type; +} _sign_type; + +_sign_type sign_types[] = +{ +#if defined(HAVE_LIBSODIUM) + {"ed25519", 0}, +#endif + {"dummy", 0} +}; + +enum +{ +#if defined(HAVE_LIBSODIUM) + SIGN_ED25519, +#endif + SIGN_DUMMY +}; + +G_DEFINE_INTERFACE (OstreeSign, ostree_sign, G_TYPE_OBJECT) + +static void +ostree_sign_default_init (OstreeSignInterface *iface) +{ + g_debug ("OstreeSign initialization"); +} + +/** + * ostree_sign_metadata_key: + * @self: an #OstreeSign object + * + * Return the pointer to the name of the key used in (detached) metadata for + * current signing engine. + * + * Returns: (transfer none): pointer to the metadata key name, + * @NULL in case of error (unlikely). + * + * Since: 2020.2 + */ +const gchar * +ostree_sign_metadata_key (OstreeSign *self) +{ + + g_return_val_if_fail (OSTREE_SIGN_GET_IFACE (self)->metadata_key != NULL, NULL); + return OSTREE_SIGN_GET_IFACE (self)->metadata_key (self); +} + +/** + * ostree_sign_metadata_format: + * @self: an #OstreeSign object + * + * Return the pointer to the string with format used in (detached) metadata for + * current signing engine. + * + * Returns: (transfer none): pointer to the metadata format, + * @NULL in case of error (unlikely). + * + * Since: 2020.2 + */ +const gchar * +ostree_sign_metadata_format (OstreeSign *self) +{ + + g_return_val_if_fail (OSTREE_SIGN_GET_IFACE (self)->metadata_format != NULL, NULL); + return OSTREE_SIGN_GET_IFACE (self)->metadata_format (self); +} + +/** + * ostree_sign_clear_keys: + * @self: an #OstreeSign object + * @error: a #GError + * + * Clear all previously preloaded secret and public keys. + * + * Returns: @TRUE in case if no errors, @FALSE in case of error + * + * Since: 2020.2 + */ +gboolean +ostree_sign_clear_keys (OstreeSign *self, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->clear_keys == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->clear_keys (self, error); +} + +/** + * ostree_sign_set_sk: + * @self: an #OstreeSign object + * @secret_key: secret key to be added + * @error: a #GError + * + * Set the secret key to be used for signing data, commits and summary. + * + * The @secret_key argument depends of the particular engine implementation. + * + * Returns: @TRUE in case if the key could be set successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_set_sk (OstreeSign *self, + GVariant *secret_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->set_sk == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->set_sk (self, secret_key, error); +} + +/** + * ostree_sign_set_pk: + * @self: an #OstreeSign object + * @public_key: single public key to be added + * @error: a #GError + * + * Set the public key for verification. It is expected what all + * previously pre-loaded public keys will be dropped. + * + * The @public_key argument depends of the particular engine implementation. + * + * Returns: @TRUE in case if the key could be set successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_set_pk (OstreeSign *self, + GVariant *public_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->set_pk == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->set_pk (self, public_key, error); +} + +/** + * ostree_sign_add_pk: + * @self: an #OstreeSign object + * @public_key: single public key to be added + * @error: a #GError + * + * Add the public key for verification. Could be called multiple times for + * adding all needed keys to be used for verification. + * + * The @public_key argument depends of the particular engine implementation. + * + * Returns: @TRUE in case if the key could be added successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_add_pk (OstreeSign *self, + GVariant *public_key, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->add_pk == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->add_pk (self, public_key, error); +} + +/** + * ostree_sign_load_pk: + * @self: an #OstreeSign object + * @options: any options + * @error: a #GError + * + * Load public keys for verification from anywhere. + * It is expected that all keys would be added to already pre-loaded keys. + * + * The @options argument depends of the particular engine implementation. + * + * For example, @ed25515 engine could use following string-formatted options: + * - @filename -- single file to use to load keys from + * - @basedir -- directory containing subdirectories + * 'trusted.ed25519.d' and 'revoked.ed25519.d' with appropriate + * public keys. Used for testing and re-definition of system-wide + * directories if defaults are not suitable for any reason. + * + * Returns: @TRUE in case if at least one key could be load successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +/* + * No need to have similar function for secret keys load -- it is expected + * what the signing software will load the secret key in it's own way. + */ +gboolean +ostree_sign_load_pk (OstreeSign *self, + GVariant *options, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->load_pk == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->load_pk (self, options, error); +} + +/** + * ostree_sign_data: + * @self: an #OstreeSign object + * @data: the raw data to be signed with pre-loaded secret key + * @signature: in case of success will contain signature + * @cancellable: A #GCancellable + * @error: a #GError + * + * Sign the given @data with pre-loaded secret key. + * + * Depending of the signing engine used you will need to load + * the secret key with #ostree_sign_set_sk. + * + * Returns: @TRUE if @data has been signed successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error) +{ + + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->data == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->data (self, data, signature, cancellable, error); +} + +/** + * ostree_sign_data_verify: + * @self: an #OstreeSign object + * @data: the raw data to check + * @signatures: the signatures to be checked + * @error: a #GError + * + * Verify given data against signatures with pre-loaded public keys. + * + * Depending of the signing engine used you will need to load + * the public key(s) with #ostree_sign_set_pk, #ostree_sign_add_pk + * or #ostree_sign_load_pk. + * + * Returns: @TRUE if @data has been signed at least with any single valid key, + * @FALSE in case of error or no valid keys are available (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + if (OSTREE_SIGN_GET_IFACE (self)->data_verify == NULL) + return glnx_throw (error, "not implemented"); + + return OSTREE_SIGN_GET_IFACE (self)->data_verify(self, data, signatures, error); +} + +/* + * Adopted version of _ostree_detached_metadata_append_gpg_sig () + */ +static GVariant * +_sign_detached_metadata_append (OstreeSign *self, + GVariant *existing_metadata, + GBytes *signature_bytes) +{ + g_return_val_if_fail (signature_bytes != NULL, FALSE); + + GVariantDict metadata_dict; + g_autoptr(GVariant) signature_data = NULL; + g_autoptr(GVariantBuilder) signature_builder = NULL; + + g_variant_dict_init (&metadata_dict, existing_metadata); + + const gchar *signature_key = ostree_sign_metadata_key(self); + GVariantType *signature_format = (GVariantType *) ostree_sign_metadata_format(self); + + signature_data = g_variant_dict_lookup_value (&metadata_dict, + signature_key, + (GVariantType*)signature_format); + + /* signature_data may be NULL */ + signature_builder = ot_util_variant_builder_from_variant (signature_data, signature_format); + + g_variant_builder_add (signature_builder, "@ay", ot_gvariant_new_ay_bytes (signature_bytes)); + + g_variant_dict_insert_value (&metadata_dict, + signature_key, + g_variant_builder_end (signature_builder)); + + return g_variant_dict_end (&metadata_dict); +} + +/** + * ostree_sign_commit_verify: + * @self: an #OstreeSign object + * @repo: an #OsreeRepo object + * @commit_checksum: SHA256 of given commit to verify + * @cancellable: A #GCancellable + * @error: a #GError + * + * Verify if commit is signed with known key. + * + * Depending of the signing engine used you will need to load + * the public key(s) for verification with #ostree_sign_set_pk, + * #ostree_sign_add_pk and/or #ostree_sign_load_pk. + * + * Returns: @TRUE if commit has been verified successfully, + * @FALSE in case of error or no valid keys are available (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_commit_verify (OstreeSign *self, + OstreeRepo *repo, + const gchar *commit_checksum, + GCancellable *cancellable, + GError **error) + +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + + g_autoptr(GVariant) commit_variant = NULL; + /* Load the commit */ + if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, + commit_checksum, &commit_variant, + error)) + return glnx_prefix_error (error, "Failed to read commit"); + + /* Load the metadata */ + g_autoptr(GVariant) metadata = NULL; + if (!ostree_repo_read_commit_detached_metadata (repo, + commit_checksum, + &metadata, + cancellable, + error)) + return glnx_prefix_error (error, "Failed to read detached metadata"); + + g_autoptr(GBytes) signed_data = g_variant_get_data_as_bytes (commit_variant); + + g_autoptr(GVariant) signatures = NULL; + + const gchar *signature_key = ostree_sign_metadata_key(self); + GVariantType *signature_format = (GVariantType *) ostree_sign_metadata_format(self); + + if (metadata) + signatures = g_variant_lookup_value (metadata, + signature_key, + signature_format); + + + return ostree_sign_data_verify (self, + signed_data, + signatures, + error); +} + +/** + * ostree_sign_get_name: + * @self: an #OstreeSign object + * + * Return the pointer to the name of currently used/selected signing engine. + * + * The list of available engines could be acquired with #ostree_sign_list_names. + * + * Returns: (transfer none): pointer to the name + * @NULL in case of error (unlikely). + * + * Since: 2020.2 + */ +const gchar * +ostree_sign_get_name (OstreeSign *self) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), NULL); + g_return_val_if_fail (OSTREE_SIGN_GET_IFACE (self)->get_name != NULL, NULL); + + return OSTREE_SIGN_GET_IFACE (self)->get_name (self); +} + +/** + * ostree_sign_commit: + * @self: an #OstreeSign object + * @repo: an #OsreeRepo object + * @commit_checksum: SHA256 of given commit to sign + * @cancellable: A #GCancellable + * @error: a #GError + * + * Add a signature to a commit. + * + * Depending of the signing engine used you will need to load + * the secret key with #ostree_sign_set_sk. + * + * Returns: @TRUE if commit has been signed successfully, + * @FALSE in case of error (@error will contain the reason). + * + * Since: 2020.2 + */ +gboolean +ostree_sign_commit (OstreeSign *self, + OstreeRepo *repo, + const gchar *commit_checksum, + GCancellable *cancellable, + GError **error) +{ + + g_autoptr(GBytes) commit_data = NULL; + g_autoptr(GBytes) signature = NULL; + g_autoptr(GVariant) commit_variant = NULL; + g_autoptr(GVariant) old_metadata = NULL; + g_autoptr(GVariant) new_metadata = NULL; + + if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, + commit_checksum, &commit_variant, error)) + return glnx_prefix_error (error, "Failed to read commit"); + + if (!ostree_repo_read_commit_detached_metadata (repo, + commit_checksum, + &old_metadata, + cancellable, + error)) + return glnx_prefix_error (error, "Failed to read detached metadata"); + + commit_data = g_variant_get_data_as_bytes (commit_variant); + + if (!ostree_sign_data (self, commit_data, &signature, + cancellable, error)) + return glnx_prefix_error (error, "Not able to sign the cobject"); + + new_metadata = + _sign_detached_metadata_append (self, old_metadata, signature); + + if (!ostree_repo_write_commit_detached_metadata (repo, + commit_checksum, + new_metadata, + cancellable, + error)) + return FALSE; + + return TRUE; +} + +/** + * ostree_sign_list_names: + * + * Return an array with all available sign engines names. + * + * Returns: (transfer full): an array of strings, free when you used it + * + * Since: 2020.2 + */ +GStrv +ostree_sign_list_names(void) +{ + + GStrv names = g_new0 (char *, G_N_ELEMENTS(sign_types) + 1); + gint i = 0; + + for (i=0; i < G_N_ELEMENTS(sign_types); i++) + { + names[i] = g_strdup(sign_types[i].name); + g_debug ("Found '%s' signing engine", names[i]); + } + + return names; +} + +/** + * ostree_sign_get_by_name: + * @name: the name of desired signature engine + * @error: return location for a #GError + * + * Tries to find and return proper signing engine by it's name. + * + * The list of available engines could be acquired with #ostree_sign_list_names. + * + * Returns: (transfer full): a constant, free when you used it + * + * Since: 2020.2 + */ +OstreeSign * +ostree_sign_get_by_name (const gchar *name, GError **error) +{ + + OstreeSign *sign = NULL; + + /* Get types if not initialized yet */ +#if defined(HAVE_LIBSODIUM) + if (sign_types[SIGN_ED25519].type == 0) + sign_types[SIGN_ED25519].type = OSTREE_TYPE_SIGN_ED25519; +#endif + if (sign_types[SIGN_DUMMY].type == 0) + sign_types[SIGN_DUMMY].type = OSTREE_TYPE_SIGN_DUMMY; + + for (gint i=0; i < G_N_ELEMENTS(sign_types); i++) + { + if (g_strcmp0 (name, sign_types[i].name) == 0) + { + g_debug ("Using '%s' signing engine", sign_types[i].name); + sign = g_object_new (sign_types[i].type, NULL); + break; + } + } + + if (sign == NULL) + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "Requested signature type is not implemented"); + + return sign; +} + +/** + * ostree_sign_summary: + * @self: Self + * @repo: ostree repository + * @keys: keys -- GVariant containing keys as GVarints specific to signature type. + * @cancellable: A #GCancellable + * @error: a #GError + * + * Add a signature to a summary file. + * Based on ostree_repo_add_gpg_signature_summary implementation. + * + * Returns: @TRUE if summary file has been signed with all provided keys + */ +gboolean +ostree_sign_summary (OstreeSign *self, + OstreeRepo *repo, + GVariant *keys, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (OSTREE_IS_SIGN (self), FALSE); + g_return_val_if_fail (OSTREE_IS_REPO (repo), FALSE); + + g_autoptr(GVariant) normalized = NULL; + g_autoptr(GBytes) summary_data = NULL; + g_autoptr(GVariant) metadata = NULL; + + glnx_autofd int fd = -1; + if (!glnx_openat_rdonly (repo->repo_dir_fd, "summary", TRUE, &fd, error)) + return FALSE; + summary_data = ot_fd_readall_or_mmap (fd, 0, error); + if (!summary_data) + return FALSE; + + /* Note that fd is reused below */ + glnx_close_fd (&fd); + + if (!ot_openat_ignore_enoent (repo->repo_dir_fd, "summary.sig", &fd, error)) + return FALSE; + + if (fd >= 0) + { + if (!ot_variant_read_fd (fd, 0, OSTREE_SUMMARY_SIG_GVARIANT_FORMAT, + FALSE, &metadata, error)) + return FALSE; + } + + if (g_variant_n_children(keys) == 0) + return glnx_throw (error, "No keys passed for signing summary"); + + GVariantIter *iter; + GVariant *key; + + g_variant_get (keys, "av", &iter); + while (g_variant_iter_loop (iter, "v", &key)) + { + g_autoptr (GBytes) signature = NULL; + + if (!ostree_sign_set_sk (self, key, error)) + return FALSE; + + if (!ostree_sign_data (self, + summary_data, + &signature, + cancellable, + error)) + return FALSE; + + g_autoptr(GVariant) old_metadata = g_steal_pointer (&metadata); + metadata = + _sign_detached_metadata_append (self, old_metadata, signature); + } + g_variant_iter_free (iter); + + normalized = g_variant_get_normal_form (metadata); + if (!_ostree_repo_file_replace_contents (repo, + repo->repo_dir_fd, + "summary.sig", + g_variant_get_data (normalized), + g_variant_get_size (normalized), + cancellable, error)) + return FALSE; + + return TRUE; +} diff --git a/src/libostree/ostree-sign.h b/src/libostree/ostree-sign.h new file mode 100644 index 00000000..678f182d --- /dev/null +++ b/src/libostree/ostree-sign.h @@ -0,0 +1,162 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright © 2019 Collabora Ltd. + * + * 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. + * + * Authors: + * - Denis Pynkin (d4s) + */ + +#pragma once + +#include +#include + +#include "ostree-ref.h" +#include "ostree-remote.h" +#include "ostree-types.h" + +G_BEGIN_DECLS + +#define OSTREE_TYPE_SIGN (ostree_sign_get_type ()) + +_OSTREE_PUBLIC +GType ostree_sign_get_type (void); + +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +typedef struct _OstreeSign OstreeSign; +typedef struct _OstreeSignInterface OstreeSignInterface; + +static inline OstreeSign *OSTREE_SIGN (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_CAST (ptr, ostree_sign_get_type (), OstreeSign); } +static inline gboolean OSTREE_IS_SIGN (gpointer ptr) { return G_TYPE_CHECK_INSTANCE_TYPE (ptr, ostree_sign_get_type ()); } +static inline OstreeSignInterface *OSTREE_SIGN_GET_IFACE (gpointer ptr) { return G_TYPE_INSTANCE_GET_INTERFACE (ptr, ostree_sign_get_type (), OstreeSignInterface); } +G_GNUC_END_IGNORE_DEPRECATIONS + +/* Have to use glib-2.44 for this +_OSTREE_PUBLIC +G_DECLARE_INTERFACE (OstreeSign, ostree_sign, OSTREE, SIGN, GObject) +*/ + +struct _OstreeSignInterface +{ + GTypeInterface g_iface; + const gchar *(* get_name) (OstreeSign *self); + gboolean (* data) (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error); + gboolean (* data_verify) (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error); + const gchar *(* metadata_key) (OstreeSign *self); + const gchar *(* metadata_format) (OstreeSign *self); + gboolean (* clear_keys) (OstreeSign *self, + GError **error); + gboolean (* set_sk) (OstreeSign *self, + GVariant *secret_key, + GError **error); + gboolean (* set_pk) (OstreeSign *self, + GVariant *public_key, + GError **error); + gboolean (* add_pk) (OstreeSign *self, + GVariant *public_key, + GError **error); + gboolean (* load_pk) (OstreeSign *self, + GVariant *options, + GError **error); +}; + +_OSTREE_PUBLIC +const gchar * ostree_sign_get_name (OstreeSign *self); + +_OSTREE_PUBLIC +gboolean ostree_sign_data (OstreeSign *self, + GBytes *data, + GBytes **signature, + GCancellable *cancellable, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_data_verify (OstreeSign *self, + GBytes *data, + GVariant *signatures, + GError **error); + +_OSTREE_PUBLIC +const gchar * ostree_sign_metadata_key (OstreeSign *self); + +_OSTREE_PUBLIC +const gchar * ostree_sign_metadata_format (OstreeSign *self); + +_OSTREE_PUBLIC +gboolean ostree_sign_commit (OstreeSign *self, + OstreeRepo *repo, + const gchar *commit_checksum, + GCancellable *cancellable, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_commit_verify (OstreeSign *self, + OstreeRepo *repo, + const gchar *commit_checksum, + GCancellable *cancellable, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_clear_keys (OstreeSign *self, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_set_sk (OstreeSign *self, + GVariant *secret_key, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_set_pk (OstreeSign *self, + GVariant *public_key, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_add_pk (OstreeSign *self, + GVariant *public_key, + GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_load_pk (OstreeSign *self, + GVariant *options, + GError **error); + + +_OSTREE_PUBLIC +GStrv ostree_sign_list_names(void); + +_OSTREE_PUBLIC +OstreeSign * ostree_sign_get_by_name (const gchar *name, GError **error); + +_OSTREE_PUBLIC +gboolean ostree_sign_summary (OstreeSign *self, + OstreeRepo *repo, + GVariant *keys, + GCancellable *cancellable, + GError **error); +G_END_DECLS + diff --git a/src/libostree/ostree.h b/src/libostree/ostree.h index 49ca919c..0308d0ed 100644 --- a/src/libostree/ostree.h +++ b/src/libostree/ostree.h @@ -40,5 +40,6 @@ #include #include #include +#include #include #include diff --git a/src/ostree/main.c b/src/ostree/main.c index a523ff9a..a9f57392 100644 --- a/src/ostree/main.c +++ b/src/ostree/main.c @@ -109,6 +109,9 @@ static OstreeCommand commands[] = { { "rev-parse", OSTREE_BUILTIN_FLAG_NONE, ostree_builtin_rev_parse, "Output the target of a rev" }, + { "sign", OSTREE_BUILTIN_FLAG_NONE, + ostree_builtin_sign, + "Sign a commit" }, { "show", OSTREE_BUILTIN_FLAG_NONE, ostree_builtin_show, "Output a metadata object" }, diff --git a/src/ostree/ot-builtin-commit.c b/src/ostree/ot-builtin-commit.c index 4cca56d0..606af2be 100644 --- a/src/ostree/ot-builtin-commit.c +++ b/src/ostree/ot-builtin-commit.c @@ -31,6 +31,7 @@ #include "parse-datetime.h" #include "ostree-repo-private.h" #include "ostree-libarchive-private.h" +#include "ostree-sign.h" static char *opt_subject; static char *opt_body; @@ -62,9 +63,11 @@ static gint opt_owner_uid = -1; static gint opt_owner_gid = -1; static gboolean opt_table_output; #ifndef OSTREE_DISABLE_GPGME -static char **opt_key_ids; +static char **opt_gpg_key_ids; static char *opt_gpg_homedir; #endif +static char **opt_key_ids; +static char *opt_sign_name; static gboolean opt_generate_sizes; static gboolean opt_disable_fsync; static char *opt_timestamp; @@ -119,9 +122,11 @@ static GOptionEntry options[] = { { "consume", 0, 0, G_OPTION_ARG_NONE, &opt_consume, "Consume (delete) content after commit (for local directories)", NULL }, { "table-output", 0, 0, G_OPTION_ARG_NONE, &opt_table_output, "Output more information in a KEY: VALUE format", NULL }, #ifndef OSTREE_DISABLE_GPGME - { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "GPG Key ID to sign the commit with", "KEY-ID"}, + { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_gpg_key_ids, "GPG Key ID to sign the commit with", "KEY-ID"}, { "gpg-homedir", 0, 0, G_OPTION_ARG_FILENAME, &opt_gpg_homedir, "GPG Homedir to use when looking for keyrings", "HOMEDIR"}, #endif + { "sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "Sign the commit with", "KEY_ID"}, + { "sign-type", 0, 0, G_OPTION_ARG_STRING, &opt_sign_name, "Signature type to use (defaults to 'ed25519')", "NAME"}, { "generate-sizes", 0, 0, G_OPTION_ARG_NONE, &opt_generate_sizes, "Generate size information along with commit metadata", NULL }, { "disable-fsync", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &opt_disable_fsync, "Do not invoke fsync()", NULL }, { "fsync", 0, 0, G_OPTION_ARG_CALLBACK, parse_fsync_cb, "Specify how to invoke fsync()", "POLICY" }, @@ -419,6 +424,7 @@ ostree_builtin_commit (int argc, char **argv, OstreeCommandInvocation *invocatio OstreeRepoTransactionStats stats; struct CommitFilterData filter_data = { 0, }; g_autofree char *commit_body = NULL; + g_autoptr (OstreeSign) sign = NULL; context = g_option_context_new ("[PATH]"); @@ -832,12 +838,42 @@ ostree_builtin_commit (int argc, char **argv, OstreeCommandInvocation *invocatio goto out; } -#ifndef OSTREE_DISABLE_GPGME if (opt_key_ids) { + /* Initialize crypto system */ + if (!opt_sign_name) + opt_sign_name = "ed25519"; + + sign = ostree_sign_get_by_name (opt_sign_name, error); + if (sign == NULL) + goto out; + char **iter; for (iter = opt_key_ids; iter && *iter; iter++) + { + const char *keyid = *iter; + g_autoptr (GVariant) secret_key = NULL; + + secret_key = g_variant_new_string (keyid); + if (!ostree_sign_set_sk (sign, secret_key, error)) + goto out; + + if (!ostree_sign_commit (sign, + repo, + commit_checksum, + cancellable, + error)) + goto out; + } + } + +#ifndef OSTREE_DISABLE_GPGME + if (opt_gpg_key_ids) + { + char **iter; + + for (iter = opt_gpg_key_ids; iter && *iter; iter++) { const char *keyid = *iter; diff --git a/src/ostree/ot-builtin-pull-local.c b/src/ostree/ot-builtin-pull-local.c index 4b3224f3..695b09e5 100644 --- a/src/ostree/ot-builtin-pull-local.c +++ b/src/ostree/ot-builtin-pull-local.c @@ -39,6 +39,8 @@ static gboolean opt_bareuseronly_files; static gboolean opt_require_static_deltas; static gboolean opt_gpg_verify; static gboolean opt_gpg_verify_summary; +static gboolean opt_sign_verify; +static gboolean opt_sign_verify_summary; static int opt_depth = 0; /* ATTENTION: @@ -55,6 +57,8 @@ static GOptionEntry options[] = { { "require-static-deltas", 0, 0, G_OPTION_ARG_NONE, &opt_require_static_deltas, "Require static deltas", NULL }, { "gpg-verify", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify, "GPG verify commits (must specify --remote)", NULL }, { "gpg-verify-summary", 0, 0, G_OPTION_ARG_NONE, &opt_gpg_verify_summary, "GPG verify summary (must specify --remote)", NULL }, + { "sign-verify", 0, 0, G_OPTION_ARG_NONE, &opt_sign_verify, "Verify commits signature (must specify --remote)", NULL }, + { "sign-verify-summary", 0, 0, G_OPTION_ARG_NONE, &opt_sign_verify, "Verify summary signature (must specify --remote)", NULL }, { "depth", 0, 0, G_OPTION_ARG_INT, &opt_depth, "Traverse DEPTH parents (-1=infinite) (default: 0)", "DEPTH" }, { NULL } }; @@ -182,6 +186,13 @@ ostree_builtin_pull_local (int argc, char **argv, OstreeCommandInvocation *invoc g_variant_builder_add (&builder, "{s@v}", "depth", g_variant_new_variant (g_variant_new_int32 (opt_depth))); + if (opt_sign_verify) + g_variant_builder_add (&builder, "{s@v}", "sign-verify", + g_variant_new_variant (g_variant_new_boolean (TRUE))); + if (opt_sign_verify_summary) + g_variant_builder_add (&builder, "{s@v}", "sign-verify-summary", + g_variant_new_variant (g_variant_new_boolean (TRUE))); + if (console.is_tty) progress = ostree_async_progress_new_and_connect (ostree_repo_pull_default_console_progress_changed, &console); else diff --git a/src/ostree/ot-builtin-sign.c b/src/ostree/ot-builtin-sign.c new file mode 100644 index 00000000..73561b43 --- /dev/null +++ b/src/ostree/ot-builtin-sign.c @@ -0,0 +1,257 @@ +/* vim:set et sw=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e2s: */ + +/* + * Copyright (C) 2015 Colin Walters + * Copyright (C) 2019 Denis Pynkin (d4s) + * + * 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. + * + * Author: Colin Walters + */ + +#include "config.h" + +#include "ot-main.h" +#include "ot-builtins.h" +#include "ostree.h" +#include "otutil.h" +#include "ostree-core-private.h" +#include "ostree-sign.h" + +static gboolean opt_delete; +static gboolean opt_verify; +static char *opt_sign_name; +static char *opt_filename; +static char *opt_keysdir; + +/* ATTENTION: + * Please remember to update the bash-completion script (bash/ostree) and + * man page (man/ostree-sign.xml) when changing the option list. + */ + +static GOptionEntry options[] = { + { "delete", 'd', 0, G_OPTION_ARG_NONE, &opt_delete, "Delete signatures having any of the KEY-IDs", NULL}, + { "verify", 0, 0, G_OPTION_ARG_NONE, &opt_verify, "Verify signatures", NULL}, + { "sign-type", 's', 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_filename, "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 +usage_error (GOptionContext *context, const char *message, GError **error) +{ + g_autofree char *help = g_option_context_get_help (context, TRUE, NULL); + g_printerr ("%s", help); + g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, message); +} + +gboolean +ostree_builtin_sign (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; + g_autofree char *resolved_commit = NULL; + const char *commit; + char **key_ids; + int n_key_ids, ii; + gboolean ret = FALSE; + + context = g_option_context_new ("COMMIT KEY-ID..."); + + + if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable, error)) + goto out; + + if (argc < 2) + { + usage_error (context, "Need a COMMIT to sign or verify", error); + goto out; + } + + commit = argv[1]; + + /* Verification could be done via system files with public keys */ + if (!opt_verify && + !opt_filename && + argc < 3) + { + usage_error (context, "Need at least one KEY-ID to sign with", error); + goto out; + } + + key_ids = argv + 2; + n_key_ids = argc - 2; + + if (!ostree_repo_resolve_rev (repo, commit, FALSE, &resolved_commit, error)) + goto out; + + /* Initialize crypto system */ + if (!opt_sign_name) + opt_sign_name = "ed25519"; + + sign = ostree_sign_get_by_name (opt_sign_name, error); + if (sign == NULL) + goto out; + + for (ii = 0; ii < n_key_ids; ii++) + { + g_autoptr (GVariant) sk = NULL; + g_autoptr (GVariant) pk = NULL; + + if (opt_verify) + { + g_autoptr (GError) local_error = NULL; + + + // Pass the key as a string + pk = g_variant_new_string(key_ids[ii]); + + if (!ostree_sign_set_pk (sign, pk, &local_error)) + continue; + + if (ostree_sign_commit_verify (sign, + repo, + resolved_commit, + cancellable, + &local_error)) + { + ret = TRUE; + goto out; + } + } + else + { + // Pass the key as a string + sk = g_variant_new_string(key_ids[ii]); + if (!ostree_sign_set_sk (sign, sk, error)) + { + ret = FALSE; + goto out; + } + + ret = ostree_sign_commit (sign, + repo, + resolved_commit, + cancellable, + error); + if (ret != TRUE) + goto out; + } + } + + /* Try to verify with user-provided file or system configuration */ + if (opt_verify) + { + if ((n_key_ids == 0) || opt_filename) + { + 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_filename) + g_variant_builder_add (builder, "{sv}", "filename", g_variant_new_string (opt_filename)); + options = g_variant_builder_end (builder); + + if (!ostree_sign_load_pk (sign, options, error)) + goto out; + + if (ostree_sign_commit_verify (sign, + repo, + resolved_commit, + cancellable, + error)) + ret = TRUE; + } /* Check via file */ + } + else + { + /* Sign with keys from provided file */ + if (opt_filename) + { + g_autoptr (GFile) keyfile = NULL; + g_autoptr (GFileInputStream) key_stream_in = NULL; + g_autoptr (GDataInputStream) key_data_in = NULL; + + if (!g_file_test (opt_filename, G_FILE_TEST_IS_REGULAR)) + { + g_warning ("Can't open file '%s' with keys", opt_filename); + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "File object '%s' is not a regular file", opt_filename); + goto out; + } + + keyfile = g_file_new_for_path (opt_filename); + key_stream_in = g_file_read (keyfile, NULL, error); + if (key_stream_in == NULL) + goto out; + + 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) + goto out; + + if (line == NULL) + break; + + + // Pass the key as a string + sk = g_variant_new_string(line); + if (!ostree_sign_set_sk (sign, sk, error)) + { + ret = FALSE; + goto out; + } + + ret = ostree_sign_commit (sign, + repo, + resolved_commit, + cancellable, + error); + if (ret != TRUE) + goto out; + } + } + } + // No valid signature found + if (opt_verify && (ret != TRUE) && (*error == NULL)) + g_set_error_literal (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "No valid signatures found"); + +out: + /* It is possible to have an error due multiple signatures check */ + if (ret == TRUE) + g_clear_error (error); + return ret; +} diff --git a/src/ostree/ot-builtin-summary.c b/src/ostree/ot-builtin-summary.c index 0f70f071..de6df835 100644 --- a/src/ostree/ot-builtin-summary.c +++ b/src/ostree/ot-builtin-summary.c @@ -27,10 +27,13 @@ #include "ot-builtins.h" #include "ostree.h" #include "otutil.h" +#include "ostree-sign.h" static gboolean opt_update, opt_view, opt_raw; -static char **opt_key_ids; +static char **opt_gpg_key_ids; static char *opt_gpg_homedir; +static char **opt_key_ids; +static char *opt_sign_name; static char **opt_metadata; /* ATTENTION: @@ -42,8 +45,10 @@ static GOptionEntry options[] = { { "update", 'u', 0, G_OPTION_ARG_NONE, &opt_update, "Update the summary", NULL }, { "view", 'v', 0, G_OPTION_ARG_NONE, &opt_view, "View the local summary file", NULL }, { "raw", 0, 0, G_OPTION_ARG_NONE, &opt_raw, "View the raw bytes of the summary file", NULL }, - { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "GPG Key ID to sign the summary with", "KEY-ID"}, + { "gpg-sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_gpg_key_ids, "GPG Key ID to sign the summary with", "KEY-ID"}, { "gpg-homedir", 0, 0, G_OPTION_ARG_FILENAME, &opt_gpg_homedir, "GPG Homedir to use when looking for keyrings", "HOMEDIR"}, + { "sign", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_key_ids, "Key ID to sign the summary with", "KEY-ID"}, + { "sign-type", 0, 0, G_OPTION_ARG_STRING, &opt_sign_name, "Signature type to use (defaults to 'ed25519')", "NAME"}, { "add-metadata", 'm', 0, G_OPTION_ARG_STRING_ARRAY, &opt_metadata, "Additional metadata field to add to the summary", "KEY=VALUE" }, { NULL } }; @@ -87,6 +92,7 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati { g_autoptr(GOptionContext) context = NULL; g_autoptr(OstreeRepo) repo = NULL; + g_autoptr (OstreeSign) sign = NULL; OstreeDumpFlags flags = OSTREE_DUMP_NONE; context = g_option_context_new (""); @@ -94,6 +100,17 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati if (!ostree_option_context_parse (context, options, &argc, &argv, invocation, &repo, cancellable, error)) return FALSE; + /* Initialize crypto system */ + if (opt_key_ids) + { + if (!opt_sign_name) + opt_sign_name = "ed25519"; + + sign = ostree_sign_get_by_name (opt_sign_name, error); + if (sign == NULL) + return FALSE; + } + if (opt_update) { g_autoptr(GVariant) additional_metadata = NULL; @@ -164,10 +181,9 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati new_summary_commit, repo_file, &new_ostree_metadata_checksum, NULL, error)) return FALSE; - - if (opt_key_ids != NULL) + if (opt_gpg_key_ids != NULL) { - for (const char * const *iter = (const char * const *) opt_key_ids; + for (const char * const *iter = (const char * const *) opt_gpg_key_ids; iter != NULL && *iter != NULL; iter++) { const char *key_id = *iter; @@ -182,6 +198,27 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati } } + if (opt_key_ids) + { + char **iter; + for (iter = opt_key_ids; iter && *iter; iter++) + { + const char *keyid = *iter; + g_autoptr (GVariant) secret_key = NULL; + + secret_key = g_variant_new_string (keyid); + if (!ostree_sign_set_sk (sign, secret_key, error)) + return FALSE; + + if (!ostree_sign_commit (sign, + repo, + new_ostree_metadata_checksum, + cancellable, + error)) + return FALSE; + } + } + ostree_repo_transaction_set_collection_ref (repo, &collection_ref, new_ostree_metadata_checksum); @@ -194,16 +231,45 @@ ostree_builtin_summary (int argc, char **argv, OstreeCommandInvocation *invocati return FALSE; #ifndef OSTREE_DISABLE_GPGME - if (opt_key_ids) + if (opt_gpg_key_ids) { if (!ostree_repo_add_gpg_signature_summary (repo, - (const gchar **) opt_key_ids, + (const gchar **) opt_gpg_key_ids, opt_gpg_homedir, cancellable, error)) return FALSE; } #endif + if (opt_key_ids) + { + g_autoptr (GVariant) secret_keys = NULL; + g_autoptr (GVariantBuilder) sk_builder = NULL; + + sk_builder = g_variant_builder_new (G_VARIANT_TYPE_ARRAY); + + char **iter; + for (iter = opt_key_ids; iter && *iter; iter++) + { + const char *keyid = *iter; + GVariant *secret_key = NULL; + + /* Currently only strings are used as keys + * for supported signature types */ + secret_key = g_variant_new_string (keyid); + + g_variant_builder_add (sk_builder, "v", secret_key); + } + + secret_keys = g_variant_builder_end (sk_builder); + + if (! ostree_sign_summary (sign, + repo, + secret_keys, + cancellable, + error)) + return FALSE; + } } else if (opt_view || opt_raw) { diff --git a/src/ostree/ot-builtins.h b/src/ostree/ot-builtins.h index 12a99b45..e372d359 100644 --- a/src/ostree/ot-builtins.h +++ b/src/ostree/ot-builtins.h @@ -53,6 +53,7 @@ BUILTINPROTO(prune); BUILTINPROTO(refs); BUILTINPROTO(reset); BUILTINPROTO(fsck); +BUILTINPROTO(sign); BUILTINPROTO(show); BUILTINPROTO(static_delta); BUILTINPROTO(summary); diff --git a/src/ostree/ot-remote-builtin-add.c b/src/ostree/ot-remote-builtin-add.c index cea0b274..e4634710 100644 --- a/src/ostree/ot-remote-builtin-add.c +++ b/src/ostree/ot-remote-builtin-add.c @@ -28,6 +28,7 @@ static char **opt_set; static gboolean opt_no_gpg_verify; +static gboolean opt_no_sign_verify; static gboolean opt_if_not_exists; static gboolean opt_force; static char *opt_gpg_import; @@ -44,6 +45,7 @@ static char *opt_repo; static GOptionEntry option_entries[] = { { "set", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_set, "Set config option KEY=VALUE for remote", "KEY=VALUE" }, { "no-gpg-verify", 0, 0, G_OPTION_ARG_NONE, &opt_no_gpg_verify, "Disable GPG verification", NULL }, + { "no-sign-verify", 0, 0, G_OPTION_ARG_NONE, &opt_no_sign_verify, "Disable signature verification", NULL }, { "if-not-exists", 0, 0, G_OPTION_ARG_NONE, &opt_if_not_exists, "Do nothing if the provided remote exists", NULL }, { "force", 0, 0, G_OPTION_ARG_NONE, &opt_force, "Replace the provided remote if it exists", NULL }, { "gpg-import", 0, 0, G_OPTION_ARG_FILENAME, &opt_gpg_import, "Import GPG key from FILE", "FILE" }, @@ -134,12 +136,18 @@ ot_remote_builtin_add (int argc, char **argv, OstreeCommandInvocation *invocatio } #ifndef OSTREE_DISABLE_GPGME - if (opt_no_gpg_verify) + /* No signature verification implies no verification for GPG signature as well */ + if (opt_no_gpg_verify || opt_no_sign_verify) g_variant_builder_add (optbuilder, "{s@v}", "gpg-verify", g_variant_new_variant (g_variant_new_boolean (FALSE))); #endif /* OSTREE_DISABLE_GPGME */ + if (opt_no_sign_verify) + g_variant_builder_add (optbuilder, "{s@v}", + "sign-verify", + g_variant_new_variant (g_variant_new_boolean (FALSE))); + if (opt_collection_id != NULL) g_variant_builder_add (optbuilder, "{s@v}", "collection-id", g_variant_new_variant (g_variant_new_take_string (g_steal_pointer (&opt_collection_id)))); diff --git a/tests/libtest.sh b/tests/libtest.sh index c82bf487..c473fd82 100755 --- a/tests/libtest.sh +++ b/tests/libtest.sh @@ -679,6 +679,43 @@ libtest_cleanup_gpg () { } libtest_exit_cmds+=(libtest_cleanup_gpg) +has_libsodium () { + local ret + ${CMD_PREFIX} ostree --version > version.txt + grep -q -e '- libsodium' version.txt + ret=$? + rm -f version.txt + return ${ret} +} + +# Keys for ed25519 signing tests +ED25519PUBLIC= +ED25519SEED= +ED25519SECRET= + +gen_ed25519_keys () +{ + # Generate private key in PEM format + pemfile="$(mktemp -p ${test_tmpdir} ed25519_XXXXXX.pem)" + openssl genpkey -algorithm ed25519 -outform PEM -out "${pemfile}" + + # Based on: http://openssl.6102.n7.nabble.com/ed25519-key-generation-td73907.html + # Extract the private and public parts from generated key. + ED25519PUBLIC="$(openssl pkey -outform DER -pubout -in ${pemfile} | tail -c 32 | base64)" + ED25519SEED="$(openssl pkey -outform DER -in ${pemfile} | tail -c 32 | base64)" + # Secret key is concantination of SEED and PUBLIC + ED25519SECRET="$(echo ${ED25519SEED}${ED25519PUBLIC} | base64 -d | base64 -w 0)" + + echo "Generated ed25519 keys:" + echo "public: ${ED25519PUBLIC}" + echo " seed: ${ED25519SEED}" +} + +gen_ed25519_random_public() +{ + openssl genpkey -algorithm ED25519 | openssl pkey -outform DER | tail -c 32 | base64 +} + is_bare_user_only_repo () { grep -q 'mode=bare-user-only' $1/config } diff --git a/tests/pull-test.sh b/tests/pull-test.sh index 2cfd8e02..a52adab9 100644 --- a/tests/pull-test.sh +++ b/tests/pull-test.sh @@ -29,7 +29,7 @@ function repo_init() { ${CMD_PREFIX} ostree --repo=repo remote add origin $(cat httpd-address)/ostree/gnomerepo "$@" } -repo_init --no-gpg-verify +repo_init --no-sign-verify # See also the copy of this in basic-test.sh COMMIT_ARGS="" @@ -62,7 +62,7 @@ else fi # Try both syntaxes -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main >out.txt assert_file_has_content out.txt "[1-9][0-9]* metadata, [1-9][0-9]* content objects fetched" ${CMD_PREFIX} ostree --repo=repo pull origin:main > out.txt @@ -164,7 +164,7 @@ echo "ok pull (bareuseronly mirror)" # Corruption tests cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify if ! is_bare_user_only_repo repo; then if ! skip_one_without_user_xattrs; then if is_bare_user_only_repo repo; then @@ -216,7 +216,7 @@ if ! skip_one_without_user_xattrs; then done # And ensure the repo is reinitialized - repo_init --no-gpg-verify + repo_init --no-sign-verify echo "ok corruption" fi else @@ -320,7 +320,7 @@ echo "ok pull specific commit" # test pull -T cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main origrev=$(${CMD_PREFIX} ostree --repo=repo rev-parse main) # Check we can pull the same commit with timestamp checking enabled @@ -350,7 +350,7 @@ ${CMD_PREFIX} ostree --repo=repo pull origin main echo "ok pull timestamp checking" cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main ${CMD_PREFIX} ostree --repo=repo fsck # Generate a delta from old to current, even though we aren't going to @@ -375,7 +375,7 @@ ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo summary -u # Explicitly test delta fetches via ref name as well as commit hash for delta_target in main ${new_rev}; do cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} ${CMD_PREFIX} ostree --repo=repo pull --dry-run --require-static-deltas origin ${delta_target} >dry-run-pull.txt # Compression can vary, so we support 400-699 @@ -388,7 +388,7 @@ done # Test pull via file:/// - this should still use the deltas path for testing cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo remote delete origin ${CMD_PREFIX} ostree --repo=repo remote add --set=gpg-verify=false origin file://$(pwd)/ostree-srv/gnomerepo ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} @@ -400,7 +400,7 @@ echo "ok pull file:// + deltas required" # Explicitly test delta fetches via ref name as well as commit hash for delta_target in main ${new_rev}; do cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas origin ${delta_target} if test ${delta_target} = main; then @@ -414,12 +414,12 @@ done # Test no-op with deltas: https://github.com/ostreedev/ostree/issues/1321 cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas origin main cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} ${CMD_PREFIX} ostree --repo=repo pull --disable-static-deltas origin main ${CMD_PREFIX} ostree --repo=repo fsck @@ -437,7 +437,7 @@ cd ${test_tmpdir} ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo static-delta generate --swap-endianness main ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo summary -u -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas --dry-run origin main >byteswapped-dry-run-pull.txt ${CMD_PREFIX} ostree --repo=repo fsck @@ -451,7 +451,7 @@ echo "ok pull byteswapped delta" cd ${test_tmpdir} rm ostree-srv/gnomerepo/deltas -rf ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo summary -u -repo_init --no-gpg-verify +repo_init --no-sign-verify if ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas origin main 2>err.txt; then assert_not_reached "--require-static-deltas unexpectedly succeeded" fi @@ -459,7 +459,7 @@ assert_file_has_content err.txt "deltas required, but none found" ${CMD_PREFIX} ostree --repo=repo fsck # Now test with a partial commit -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull --commit-metadata-only origin main@${prev_rev} if ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas origin main 2>err.txt; then assert_not_reached "--require-static-deltas unexpectedly succeeded" @@ -467,7 +467,7 @@ fi assert_file_has_content err.txt "deltas required, but none found" echo "ok delta required but don't exist" -repo_init --no-gpg-verify +repo_init --no-sign-verify ${CMD_PREFIX} ostree --repo=repo pull origin main@${prev_rev} if ${CMD_PREFIX} ostree --repo=repo pull --require-static-deltas origin ${new_rev} 2>err.txt; then assert_not_reached "--require-static-deltas unexpectedly succeeded" @@ -595,7 +595,7 @@ if has_gpgme; then fi cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify mv ostree-srv/gnomerepo/refs/heads/main{,.orig} rm ostree-srv/gnomerepo/summary (for x in $(seq 20); do echo "lots of html here "; done) > ostree-srv/gnomerepo/refs/heads/main diff --git a/tests/pull-test2.sh b/tests/pull-test2.sh index 064bbfe6..a0b699ae 100644 --- a/tests/pull-test2.sh +++ b/tests/pull-test2.sh @@ -29,7 +29,7 @@ function repo_init() { ${CMD_PREFIX} ostree --repo=repo remote add origin $(cat httpd-address)/ostree/gnomerepo "$@" } -repo_init --no-gpg-verify +repo_init --no-sign-verify # See also the copy of this in basic-test.sh COMMIT_ARGS="" @@ -48,7 +48,7 @@ fi echo "1..1" cd ${test_tmpdir} -repo_init --no-gpg-verify +repo_init --no-sign-verify prev_rev=$(ostree --repo=ostree-srv/repo rev-parse ${remote_ref}^) rev=$(ostree --repo=ostree-srv/repo rev-parse ${remote_ref}) ${CMD_PREFIX} ostree --repo=ostree-srv/repo static-delta generate ${remote_ref} diff --git a/tests/test-local-pull.sh b/tests/test-local-pull.sh index 97bb9954..2b7ca13a 100755 --- a/tests/test-local-pull.sh +++ b/tests/test-local-pull.sh @@ -28,12 +28,7 @@ unset OSTREE_GPG_HOME skip_without_user_xattrs -if has_gpgme; then - echo "1..8" -else - # Only some tests doesn't need GPG support - echo "1..5" -fi +echo "1..11" setup_test_repository "archive" echo "ok setup" @@ -68,6 +63,49 @@ cmp checkout1.files checkout2.files cmp checkout1.files checkout3.files echo "ok checkouts same" +if has_gpgme; then + # These tests are needed GPG support + mkdir repo4 + ostree_repo_init repo4 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo4 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo + + if ${CMD_PREFIX} ostree --repo=repo4 pull-local --remote=origin --gpg-verify repo test2 2>&1; then + assert_not_reached "GPG verification unexpectedly succeeded" + fi + echo "ok --gpg-verify with no signature" + + ${OSTREE} gpg-sign --gpg-homedir=${TEST_GPG_KEYHOME} test2 ${TEST_GPG_KEYID_1} + + mkdir repo5 + ostree_repo_init repo5 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo5 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo + ${CMD_PREFIX} ostree --repo=repo5 pull-local --remote=origin --gpg-verify repo test2 + echo "ok --gpg-verify" + + mkdir repo6 + ostree_repo_init repo6 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo6 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo + if ${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1; then + assert_not_reached "GPG summary verification with no summary unexpectedly succeeded" + fi + + ${OSTREE} summary --update + + if ${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1; then + assert_not_reached "GPG summary verification with signed no summary unexpectedly succeeded" + fi + + ${OSTREE} summary --update --gpg-sign=${TEST_GPG_KEYID_1} --gpg-homedir=${TEST_GPG_KEYHOME} + + ${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1 + + echo "ok --gpg-verify-summary" +else + echo "ok --gpg-verify with no signature | # SKIP due GPG unavailability" + echo "ok --gpg-verify | # SKIP due GPG unavailability" + echo "ok --gpg-verify-summary | # SKIP due GPG unavailability" +fi + mkdir repo7 ostree_repo_init repo7 --mode="archive" ${CMD_PREFIX} ostree --repo=repo7 pull-local repo @@ -78,41 +116,36 @@ for src_object in `find repo/objects -name '*.filez'`; do done echo "ok pull-local z2 to z2 default hardlink" -if ! has_gpgme; then - exit 0 +if has_libsodium; then + gen_ed25519_keys + + mkdir repo8 + ostree_repo_init repo8 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo8 remote add --set=verification-key="${ED25519PUBLIC}" origin repo + cat repo8/config + + if ${CMD_PREFIX} ostree --repo=repo8 pull-local --remote=origin --sign-verify repo test2 2>&1; then + assert_not_reached "Ed25519 signature verification unexpectedly succeeded" + fi + echo "ok --sign-verify with no signature" + + ${OSTREE} sign test2 ${ED25519SECRET} + + mkdir repo9 + ostree_repo_init repo9 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo9 remote add --set=verification-key="$(gen_ed25519_random_public)" origin repo + if ${CMD_PREFIX} ostree --repo=repo9 pull-local --remote=origin --sign-verify repo test2 2>&1; then + assert_not_reached "Ed25519 signature verification unexpectedly succeeded" + fi + echo "ok --sign-verify with wrong signature" + + mkdir repo10 + ostree_repo_init repo10 --mode="archive" + ${CMD_PREFIX} ostree --repo=repo10 remote add --set=verification-key="${ED25519PUBLIC}" origin repo + ${CMD_PREFIX} ostree --repo=repo10 pull-local --remote=origin --sign-verify repo test2 + echo "ok --sign-verify" +else + echo "ok --sign-verify with no signature | # SKIP due libsodium unavailability" + echo "ok --sign-verify with wrong signature | # SKIP due libsodium unavailability" + echo "ok --sign-verify | # SKIP libsodium unavailability" fi - -mkdir repo4 -ostree_repo_init repo4 --mode="archive" -${CMD_PREFIX} ostree --repo=repo4 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo -if ${CMD_PREFIX} ostree --repo=repo4 pull-local --remote=origin --gpg-verify repo test2 2>&1; then - assert_not_reached "GPG verification unexpectedly succeeded" -fi -echo "ok --gpg-verify with no signature" - -${OSTREE} gpg-sign --gpg-homedir=${TEST_GPG_KEYHOME} test2 ${TEST_GPG_KEYID_1} - -mkdir repo5 -ostree_repo_init repo5 --mode="archive" -${CMD_PREFIX} ostree --repo=repo5 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo -${CMD_PREFIX} ostree --repo=repo5 pull-local --remote=origin --gpg-verify repo test2 -echo "ok --gpg-verify" - -mkdir repo6 -ostree_repo_init repo6 --mode="archive" -${CMD_PREFIX} ostree --repo=repo6 remote add --gpg-import ${test_tmpdir}/gpghome/key1.asc origin repo -if ${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1; then - assert_not_reached "GPG summary verification with no summary unexpectedly succeeded" -fi - -${OSTREE} summary --update - -if ${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1; then - assert_not_reached "GPG summary verification with signed no summary unexpectedly succeeded" -fi - -${OSTREE} summary --update --gpg-sign=${TEST_GPG_KEYID_1} --gpg-homedir=${TEST_GPG_KEYHOME} - -${CMD_PREFIX} ostree --repo=repo6 pull-local --remote=origin --gpg-verify-summary repo test2 2>&1 - -echo "ok --gpg-verify-summary" diff --git a/tests/test-pull-mirrorlist.sh b/tests/test-pull-mirrorlist.sh index 85ff66e9..ed65eb64 100755 --- a/tests/test-pull-mirrorlist.sh +++ b/tests/test-pull-mirrorlist.sh @@ -75,7 +75,7 @@ EOF cd ${test_tmpdir} mkdir repo ostree_repo_init repo -${CMD_PREFIX} ostree --repo=repo remote add origin --no-gpg-verify \ +${CMD_PREFIX} ostree --repo=repo remote add origin --no-sign-verify \ mirrorlist=$(cat httpd-address)/ostree/mirrorlist ${CMD_PREFIX} ostree --repo=repo pull origin:main @@ -87,7 +87,7 @@ cd ${test_tmpdir} rm -rf repo mkdir repo ostree_repo_init repo -${CMD_PREFIX} ostree --repo=repo remote add origin --no-gpg-verify \ +${CMD_PREFIX} ostree --repo=repo remote add origin --no-sign-verify \ --contenturl=mirrorlist=$(cat httpd-address)/ostree/mirrorlist \ $(cat httpd-address)/ostree/gnomerepo ${CMD_PREFIX} ostree --repo=repo pull origin:main @@ -100,7 +100,7 @@ cd ${test_tmpdir} rm -rf repo mkdir repo ostree_repo_init repo -${CMD_PREFIX} ostree --repo=repo remote add origin --no-gpg-verify \ +${CMD_PREFIX} ostree --repo=repo remote add origin --no-sign-verify \ --contenturl=mirrorlist=$(cat httpd-address)/ostree/mirrorlist \ mirrorlist=$(cat httpd-address)/ostree/mirrorlist ${CMD_PREFIX} ostree --repo=repo pull origin:main diff --git a/tests/test-refs-collections.sh b/tests/test-refs-collections.sh index bf233970..d33f498c 100755 --- a/tests/test-refs-collections.sh +++ b/tests/test-refs-collections.sh @@ -112,7 +112,7 @@ mkdir collection-repo ostree_repo_init collection-repo --collection-id org.example.RemoteCollection mkdir -p adir ${CMD_PREFIX} ostree --repo=collection-repo commit --branch=rcommit -m rcommit -s rcommit adir -${CMD_PREFIX} ostree --repo=repo remote add --no-gpg-verify --collection-id org.example.RemoteCollection collection-repo-remote "file://${test_tmpdir}/collection-repo" +${CMD_PREFIX} ostree --repo=repo remote add --no-sign-verify --collection-id org.example.RemoteCollection collection-repo-remote "file://${test_tmpdir}/collection-repo" ${CMD_PREFIX} ostree --repo=repo pull collection-repo-remote rcommit ${CMD_PREFIX} ostree --repo=repo refs --collections > refs @@ -129,7 +129,7 @@ mkdir no-collection-repo ostree_repo_init no-collection-repo mkdir -p adir2 ${CMD_PREFIX} ostree --repo=no-collection-repo commit --branch=rcommit2 -m rcommit2 -s rcommit2 adir2 -${CMD_PREFIX} ostree --repo=repo remote add --no-gpg-verify no-collection-repo-remote "file://${test_tmpdir}/no-collection-repo" +${CMD_PREFIX} ostree --repo=repo remote add --no-sign-verify no-collection-repo-remote "file://${test_tmpdir}/no-collection-repo" ${CMD_PREFIX} ostree --repo=repo pull no-collection-repo-remote rcommit2 ${CMD_PREFIX} ostree --repo=repo refs --collections > refs assert_not_file_has_content refs "rcommit2" diff --git a/tests/test-remote-add.sh b/tests/test-remote-add.sh index bb7eae89..40a32f57 100755 --- a/tests/test-remote-add.sh +++ b/tests/test-remote-add.sh @@ -30,20 +30,20 @@ $OSTREE remote add origin http://example.com/ostree/gnome $OSTREE remote show-url origin >/dev/null echo "ok config" -$OSTREE remote add --no-gpg-verify another http://another.com/repo +$OSTREE remote add --no-sign-verify another http://another.com/repo $OSTREE remote show-url another >/dev/null echo "ok remote no gpg-verify" -if $OSTREE remote add --no-gpg-verify another http://another.example.com/anotherrepo 2>err.txt; then +if $OSTREE remote add --no-sign-verify another http://another.example.com/anotherrepo 2>err.txt; then assert_not_reached "Adding duplicate remote unexpectedly succeeded" fi echo "ok" -$OSTREE remote add --if-not-exists --no-gpg-verify another http://another.example.com/anotherrepo +$OSTREE remote add --if-not-exists --no-sign-verify another http://another.example.com/anotherrepo $OSTREE remote show-url another >/dev/null echo "ok" -$OSTREE remote add --if-not-exists --no-gpg-verify another-noexist http://another-noexist.example.com/anotherrepo +$OSTREE remote add --if-not-exists --no-sign-verify another-noexist http://another-noexist.example.com/anotherrepo $OSTREE remote show-url another-noexist >/dev/null echo "ok" @@ -69,7 +69,7 @@ cd ${test_tmpdir} rm -rf parent-repo ostree_repo_init parent-repo $OSTREE config set core.parent ${test_tmpdir}/parent-repo -${CMD_PREFIX} ostree --repo=parent-repo remote add --no-gpg-verify parent-remote http://parent-remote.example.com/parent-remote +${CMD_PREFIX} ostree --repo=parent-repo remote add --no-sign-verify parent-remote http://parent-remote.example.com/parent-remote $OSTREE remote list > list.txt assert_file_has_content list.txt "origin" assert_file_has_content list.txt "another" diff --git a/tests/test-remotes-config-dir.js b/tests/test-remotes-config-dir.js index 5588116b..f73a82ef 100755 --- a/tests/test-remotes-config-dir.js +++ b/tests/test-remotes-config-dir.js @@ -94,16 +94,22 @@ print("ok add-in-remotes-config-dir"); // Trying to set a remote config option via write_config() for a remote // defined in the config file should succeed -let [, gpg_verify] = repo.remote_get_gpg_verify('bar'); -assertEquals(gpg_verify, true); -repoConfig = repo.copy_config(); -repoConfig.set_boolean('remote "bar"', 'gpg-verify', false); -repo.write_config(repoConfig); -repo.reload_config(null); -[, gpg_verify] = repo.remote_get_gpg_verify('bar'); -assertEquals(gpg_verify, false); - -print("ok config-remote-in-config-file-succeeds"); +try { + let [, gpg_verify] = repo.remote_get_gpg_verify('bar'); + assertEquals(gpg_verify, true); + repoConfig = repo.copy_config(); + repoConfig.set_boolean('remote "bar"', 'gpg-verify', false); + repo.write_config(repoConfig); + repo.reload_config(null); + [, gpg_verify] = repo.remote_get_gpg_verify('bar'); + assertEquals(gpg_verify, false); + print("ok config-remote-in-config-file-succeeds"); +} catch (e) { + // Skip this test if GPG is not supported + if (!(e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_SUPPORTED))) + throw e; + print("ok config-remote-in-config-file-succeeds # SKIP due build without GPG support"); +} // Trying to set a remote config option via write_config() for a remote // defined in the config dir should fail with G_IO_ERROR_EXISTS diff --git a/tests/test-signed-commit.sh b/tests/test-signed-commit.sh new file mode 100755 index 00000000..6730a6df --- /dev/null +++ b/tests/test-signed-commit.sh @@ -0,0 +1,193 @@ +#!/bin/bash +# +# Copyright (C) 2019 Collabora Ltd. +# +# 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. + +set -euo pipefail + +. $(dirname $0)/libtest.sh + +echo "1..10" + +mkdir ${test_tmpdir}/repo +ostree_repo_init repo --mode="archive" + +echo "Unsigned commit" > file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +# Test `ostree sign` with dummy module first +DUMMYSIGN="dummysign" +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} ${DUMMYSIGN} + +# Ensure that detached metadata really contain expected string +EXPECTEDSIGN="$(echo $DUMMYSIGN | hexdump -n 9 -e '8/1 "0x%.2x, " 1/1 " 0x%.2x"')" +${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.dummy | grep -q -e "${EXPECTEDSIGN}" +echo "ok Detached dummy signature added" + +# Verify vith sign mechanism +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} +echo "ok dummy signature verified" + +echo "Signed commit with dummy key: ${DUMMYSIGN}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Signed with dummy module' --sign=${DUMMYSIGN} --sign-type=dummy +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} +echo "ok commit with dummy signing" + +# tests below require libsodium support +if ! has_libsodium; then + echo "ok Detached ed25519 signature # SKIP due libsodium unavailability" + echo "ok ed25519 signature verified # SKIP due libsodium unavailability" + echo "ok multiple signing # SKIP due libsodium unavailability" + echo "ok verify ed25519 keys file # SKIP due libsodium unavailability" + echo "ok sign with ed25519 keys file # SKIP due libsodium unavailability" + echo "ok verify ed25519 system-wide configuration # SKIP due libsodium unavailability" + echo "ok verify ed25519 revoking keys mechanism # SKIP due libsodium unavailability" + exit 0 +fi + +# Test ostree sign with 'ed25519' module +gen_ed25519_keys +PUBLIC=${ED25519PUBLIC} +SEED=${ED25519SEED} +SECRET=${ED25519SECRET} + +WRONG_PUBLIC="$(gen_ed25519_random_public)" + +echo "SEED = $SEED" +echo "PUBLIC = $PUBLIC" + +echo "Signed commit with ed25519: ${SECRET}" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s "Signed with ed25519 module" --sign="${SECRET}" --sign-type=ed25519 +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +# Ensure that detached metadata contain signature +${CMD_PREFIX} ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.ed25519 &>/dev/null +echo "ok Detached ed25519 signature added" + +# Verify vith sign mechanism +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed25519_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed25519_random_public) $(gen_ed25519_random_public) ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} $(gen_ed25519_random_public) $(gen_ed25519_random_public) +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} $(gen_ed25519_random_public) $(gen_ed25519_random_public) ${PUBLIC} $(gen_ed25519_random_public) $(gen_ed25519_random_public) +echo "ok ed25519 signature verified" + +# Check if we able to use all available modules to sign the same commit +echo "Unsigned commit for multi-sign" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" +# Check if we have no signatures +for mod in "dummy" "ed25519"; do + if ostree --repo=repo show ${COMMIT} --print-detached-metadata-key=ostree.sign.${mod}; then + echo "Unexpected signature for ${mod} found" + exit 1 + fi +done + +# Sign with all available modules +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy ${COMMIT} ${DUMMYSIGN} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=ed25519 ${COMMIT} ${SECRET} +# and verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 ${COMMIT} ${PUBLIC} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=dummy --verify ${COMMIT} ${DUMMYSIGN} +echo "ok multiple signing " + +# Prepare files with public ed25519 signatures +PUBKEYS="$(mktemp -p ${test_tmpdir} ed25519_XXXXXX.ed25519)" + +# Test if file contain no keys +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT}; then + exit 1 +fi + +# Test if have a problem with file object +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${test_tmpdir} ${COMMIT}; then + exit 1 +fi + +# Test with single key in list +echo ${PUBLIC} > ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} + +# Test the file with multiple keys without a valid public key +for((i=0;i<100;i++)); do + # Generate a list with some public signatures + gen_ed25519_random_public +done > ${PUBKEYS} +# Check if file contain no valid signatures +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT}; then + exit 1 +fi +# Check if no valid signatures provided via args&file +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} ${WRONG_PUBLIC}; then + exit 1 +fi + +#Test keys file and public key +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} ${PUBLIC} + +# Add correct key into the list +echo ${PUBLIC} >> ${PUBKEYS} +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} + +echo "ok verify ed25519 keys file" + +# Check ed25519 signing with secret file +echo "Unsigned commit for secret file usage" >> file.txt +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo commit -b main -s 'Unsigned commit' +COMMIT="$(ostree --repo=${test_tmpdir}/repo rev-parse main)" + +KEYFILE="$(mktemp -p ${test_tmpdir} secret_XXXXXX.ed25519)" +echo "${SECRET}" > ${KEYFILE} +# Sign +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --sign-type=ed25519 --keys-file=${KEYFILE} ${COMMIT} +# Verify +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-file=${PUBKEYS} ${COMMIT} +echo "ok sign with ed25519 keys file" + +# Check the well-known places mechanism +mkdir -p ${test_tmpdir}/{trusted,revoked}.ed25519.d +for((i=0;i<100;i++)); do + # Generate some key files with random public signatures + gen_ed25519_random_public +done +# Check no valid public keys are available +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +echo ${PUBLIC} > ${test_tmpdir}/trusted.ed25519.d/correct +# Verify with correct key +${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-dir=${test_tmpdir} ${COMMIT} + +echo "ok verify ed25519 system-wide configuration" + +# Add the public key into revoked list +echo ${PUBLIC} > ${test_tmpdir}/revoked.ed25519.d/correct +# Check if public key is not valid anymore +if ${CMD_PREFIX} ostree --repo=${test_tmpdir}/repo sign --verify --sign-type=ed25519 --keys-dir=${test_tmpdir} ${COMMIT}; then + exit 1 +fi +rm -rf ${test_tmpdir}/{trusted,revoked}.ed25519.d +echo "ok verify ed25519 revoking keys mechanism" diff --git a/tests/test-signed-pull-summary.sh b/tests/test-signed-pull-summary.sh new file mode 100755 index 00000000..ee731e86 --- /dev/null +++ b/tests/test-signed-pull-summary.sh @@ -0,0 +1,287 @@ +#!/bin/bash +# +# Copyright (C) 2019 Collabora Ltd. +# +# 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. + +# Based on test-pull-summary-sigs.sh test. + +set -euo pipefail + +. $(dirname $0)/libtest.sh + +echo "1..14" + +repo_reinit () { + ARGS="$*" + cd ${test_tmpdir} + rm -rf repo + mkdir repo + ostree_repo_init repo --mode=archive + ${OSTREE} --repo=repo remote add \ + --set=gpg-verify=false --set=gpg-verify-summary=false \ + --set=sign-verify=false --set=sign-verify-summary=true \ + ${ARGS} origin $(cat httpd-address)/ostree/gnomerepo +} + +for engine in dummy ed25519 +do + case "${engine}" in + dummy) + # Tests with dummy engine + SIGN_KEY="dummysign" + PUBLIC_KEY="dummysign" + ;; + ed25519) + if ! has_libsodium; then + echo "ok ${engine} pull mirror summary # SKIP due libsodium unavailability" + echo "ok ${engine} pull with signed summary # SKIP due libsodium unavailability" + echo "ok ${engine} prune summary cache # SKIP due libsodium unavailability" + echo "ok ${engine} pull with signed summary and cachedir # SKIP due libsodium unavailability" + echo "ok ${engine} pull with invalid ${engine} summary signature fails # SKIP due libsodium unavailability" + echo "ok ${engine} pull delta with signed summary # SKIP due libsodium unavailability" + + continue + fi + gen_ed25519_keys + SIGN_KEY="${ED25519SECRET}" + PUBLIC_KEY="${ED25519PUBLIC}" + ;; + *) + fatal "Unsupported engine ${engine}" + ;; + esac + + COMMIT_SIGN="--sign-type=${engine} --sign=${SIGN_KEY}" + + # clenup the testdir prior the next engine + rm -rf ${test_tmpdir}/ostree-srv ${test_tmpdir}/gnomerepo ${test_tmpdir}/httpd ${test_tmpdir}/repo ${test_tmpdir}/cachedir\ + ${test_tmpdir}/main-copy ${test_tmpdir}/other-copy ${test_tmpdir}/yet-another-copy + + setup_fake_remote_repo1 "archive" "${COMMIT_SIGN}" + + # Now, setup multiple branches + mkdir ${test_tmpdir}/ostree-srv/other-files + cd ${test_tmpdir}/ostree-srv/other-files + echo 'hello world another object' > hello-world + ${CMD_PREFIX} ostree --repo=${test_tmpdir}/ostree-srv/gnomerepo commit ${COMMIT_SIGN} -b other -s "A commit" -m "Another Commit body" + + mkdir ${test_tmpdir}/ostree-srv/yet-other-files + cd ${test_tmpdir}/ostree-srv/yet-other-files + echo 'hello world yet another object' > yet-another-hello-world + ${CMD_PREFIX} ostree --repo=${test_tmpdir}/ostree-srv/gnomerepo commit ${COMMIT_SIGN} -b yet-another -s "A commit" -m "Another Commit body" + + ${CMD_PREFIX} ostree --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u + + prev_dir=`pwd` + cd ${test_tmpdir} + ostree_repo_init repo --mode=archive + ${CMD_PREFIX} ostree --repo=repo remote add \ + --set=gpg-verify=false --set=gpg-verify-summary=false \ + --set=sign-verify=false --set=sign-verify-summary=false \ + origin $(cat httpd-address)/ostree/gnomerepo + ${CMD_PREFIX} ostree --repo=repo pull --mirror origin + assert_has_file repo/summary + ${CMD_PREFIX} ostree --repo=repo checkout -U main main-copy + assert_file_has_content main-copy/baz/cow "moo" + ${CMD_PREFIX} ostree --repo=repo checkout -U other other-copy + assert_file_has_content other-copy/hello-world "hello world another object" + ${CMD_PREFIX} ostree --repo=repo checkout -U yet-another yet-another-copy + assert_file_has_content yet-another-copy/yet-another-hello-world "hello world yet another object" + ${CMD_PREFIX} ostree --repo=repo fsck + echo "ok ${engine} pull mirror summary" + + + cd $prev_dir + + ${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u ${COMMIT_SIGN} + + cd ${test_tmpdir} + repo_reinit --set=verification-key=${PUBLIC_KEY} + ${OSTREE} --repo=repo pull origin main + assert_has_file repo/tmp/cache/summaries/origin + assert_has_file repo/tmp/cache/summaries/origin.sig + + rm repo/tmp/cache/summaries/origin + ${OSTREE} --repo=repo pull origin main + assert_has_file repo/tmp/cache/summaries/origin + + echo "ok ${engine} pull with signed summary" + + touch repo/tmp/cache/summaries/foo + touch repo/tmp/cache/summaries/foo.sig + ${OSTREE} --repo=repo prune + assert_not_has_file repo/tmp/cache/summaries/foo + assert_not_has_file repo/tmp/cache/summaries/foo.sig + assert_has_file repo/tmp/cache/summaries/origin + assert_has_file repo/tmp/cache/summaries/origin.sig + echo "ok ${engine} prune summary cache" + + cd ${test_tmpdir} + repo_reinit --set=verification-key=${PUBLIC_KEY} + mkdir cachedir + ${OSTREE} --repo=repo pull --cache-dir=cachedir origin main + assert_not_has_file repo/tmp/cache/summaries/origin + assert_not_has_file repo/tmp/cache/summaries/origin.sig + assert_has_file cachedir/summaries/origin + assert_has_file cachedir/summaries/origin.sig + + rm cachedir/summaries/origin + ${OSTREE} --repo=repo pull --cache-dir=cachedir origin main + assert_not_has_file repo/tmp/cache/summaries/origin + assert_has_file cachedir/summaries/origin + + echo "ok ${engine} pull with signed summary and cachedir" + + cd ${test_tmpdir} + repo_reinit --set=verification-key=${PUBLIC_KEY} + mv ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{,.good} + echo invalid > ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig + if ${OSTREE} --repo=repo pull origin main 2>err.txt; then + assert_not_reached "Successful pull with invalid ${engine} signature" + fi + assert_file_has_content err.txt "signed with unknown key" + mv ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{.good,} + echo "ok ${engine} pull with invalid ${engine} summary signature fails" + + # Generate a delta + ${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo static-delta generate --empty main + ${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u ${COMMIT_SIGN} + + cd ${test_tmpdir} + repo_reinit --set=verification-key=${PUBLIC_KEY} + ${OSTREE} --repo=repo pull origin main + echo "ok ${engine} pull delta with signed summary" + +done + +if ! has_libsodium; then + echo "ok ${engine} pull with signed summary remote old summary # SKIP due libsodium unavailability" + echo "ok ${engine} pull with signed summary broken cache # SKIP due libsodium unavailability" + exit 0 +fi + +gen_ed25519_keys +SIGN_KEY="${ED25519SECRET}" +PUBLIC_KEY="${ED25519PUBLIC}" +COMMIT_SIGN="--sign-type=${engine} --sign=${SIGN_KEY}" + + +# Verify 'ostree remote summary' output. +${OSTREE} --repo=repo remote summary origin > summary.txt +assert_file_has_content summary.txt "* main" +assert_file_has_content summary.txt "* other" +assert_file_has_content summary.txt "* yet-another" +grep static-deltas summary.txt > static-deltas.txt +assert_file_has_content static-deltas.txt \ + $(${OSTREE} --repo=repo rev-parse origin:main) + +## Tests for handling of cached summaries while racing with remote summary updates + +# Make 2 different but valid summary/signature pairs to test races with +${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u ${COMMIT_SIGN} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{,.1} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{,.1} +mkdir ${test_tmpdir}/ostree-srv/even-another-files +cd ${test_tmpdir}/ostree-srv/even-another-files +echo 'hello world even another object' > even-another-hello-world +${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo commit ${COMMIT_SIGN} -b even-another -s "A commit" -m "Another Commit body" +${OSTREE} --repo=${test_tmpdir}/ostree-srv/gnomerepo summary -u ${COMMIT_SIGN} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{,.2} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{,.2} +cd ${test_tmpdir} + +# Reset to the old valid summary and pull to cache it +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{.1,} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{.1,} +repo_reinit --set=verification-key=${PUBLIC_KEY} +${OSTREE} --repo=repo pull origin main +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.1 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.1 >&2 + +# Simulate a pull race where the client gets the old summary and the new +# summary signature since it was generated on the server between the +# requests +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{.2,} +if ${OSTREE} --repo=repo pull origin main 2>err.txt; then + assert_not_reached "Successful pull with old summary" +fi +assert_file_has_content err.txt "signed with unknown key" +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.1 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.1 >&2 + +# Publish correct summary and check that subsequent pull succeeds +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{.2,} +${OSTREE} --repo=repo pull origin main +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.2 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.2 >&2 + +echo "ok ${engine} pull with signed summary remote old summary" + + +# Reset to the old valid summary and pull to cache it +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{.1,} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{.1,} +repo_reinit --set=verification-key=${PUBLIC_KEY} +${OSTREE} --repo=repo pull origin main +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.1 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.1 >&2 + +# Simulate a broken summary cache to see if it can be recovered from. +# Prior to commit c4c2b5eb the client would save the summary to the +# cache before validating the signature. That would mean the cache would +# have mismatched summary and signature and ostree would remain +# deadlocked there until the remote published a new signature. +# +# First pull with OSTREE_REPO_TEST_ERROR=invalid-cache to see the +# invalid cache is detected. Then pull again to check if it can be +# recovered from. +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.2 repo/tmp/cache/summaries/origin +if OSTREE_REPO_TEST_ERROR=invalid-cache ${OSTREE} --repo=repo pull origin main 2>err.txt; then + assert_not_reached "Should have hit OSTREE_REPO_TEST_ERROR_INVALID_CACHE" +fi +assert_file_has_content err.txt "OSTREE_REPO_TEST_ERROR_INVALID_CACHE" +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.2 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.1 >&2 +${OSTREE} --repo=repo pull origin main +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.1 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.1 >&2 + +# Publish new signature and check that subsequent pull succeeds +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary{.2,} +cp ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig{.2,} +${OSTREE} --repo=repo pull origin main +assert_has_file repo/tmp/cache/summaries/origin +assert_has_file repo/tmp/cache/summaries/origin.sig +cmp repo/tmp/cache/summaries/origin ${test_tmpdir}/ostree-srv/gnomerepo/summary.2 >&2 +cmp repo/tmp/cache/summaries/origin.sig ${test_tmpdir}/ostree-srv/gnomerepo/summary.sig.2 >&2 + +echo "ok ${engine} pull with signed summary broken cache" + diff --git a/tests/test-signed-pull.sh b/tests/test-signed-pull.sh new file mode 100755 index 00000000..f222db4f --- /dev/null +++ b/tests/test-signed-pull.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# +# Copyright (C) 2019 Collabora Ltd. +# +# 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. + +set -euo pipefail + +. $(dirname $0)/libtest.sh + +echo "1..11" + +setup_fake_remote_repo1 "archive" + +repo_mode="archive" + +function repo_init() { + cd ${test_tmpdir} + rm repo -rf + mkdir repo + ostree_repo_init repo --mode=${repo_mode} + ${CMD_PREFIX} ostree --repo=repo remote add --set=gpg-verify=false --set=sign-verify-summary=false origin $(cat httpd-address)/ostree/gnomerepo "$@" +} + +function test_signed_pull() { + local sign_type="$1" + local comment="$2" + cd ${test_tmpdir} + ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo commit ${COMMIT_ARGS} \ + -b main -s "A signed commit" --tree=ref=main + + ${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo summary -u + # make sure gpg verification is correctly on + csum=$(${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo rev-parse main) + objpath=objects/${csum::2}/${csum:2}.commitmeta + remotesig=ostree-srv/gnomerepo/$objpath + localsig=repo/$objpath + mv $remotesig $remotesig.bak + if ${CMD_PREFIX} ostree --repo=repo --depth=0 pull origin main; then + assert_not_reached "pull with sign-verify unexpectedly succeeded?" + fi + # ok now check that we can pull correctly + mv $remotesig.bak $remotesig + ${CMD_PREFIX} ostree --repo=repo pull origin main + echo "ok ${sign_type}${comment} pull signed commit" + rm $localsig + ${CMD_PREFIX} ostree --repo=repo pull origin main + test -f $localsig + echo "ok ${sign_type}${comment} re-pull signature for stored commit" +} + +DUMMYSIGN="dummysign" +COMMIT_ARGS="--sign=${DUMMYSIGN} --sign-type=dummy" +repo_init --set=sign-verify=true + +# Check if verification-key and verification-file options throw error with wrong keys +cd ${test_tmpdir} +${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo commit ${COMMIT_ARGS} \ + -b main -s "A signed commit" --tree=ref=main +${CMD_PREFIX} ostree --repo=ostree-srv/gnomerepo summary -u +if ${CMD_PREFIX} ostree --repo=repo pull origin main; then + assert_not_reached "pull without keys unexpectedly succeeded" +fi +echo "ok pull failure without keys preloaded" + +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-key "somewrongkey" +if ${CMD_PREFIX} ostree --repo=repo pull origin main; then + assert_not_reached "pull with unknown key unexpectedly succeeded" +fi +echo "ok pull failure with incorrect key option" + +${CMD_PREFIX} ostree --repo=repo config unset 'remote "origin"'.verification-key +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-file "/non/existing/file" +if ${CMD_PREFIX} ostree --repo=repo pull origin main; then + assert_not_reached "pull with unknown keys file unexpectedly succeeded" +fi +echo "ok pull failure with incorrect keys file option" + +# Test with correct dummy key +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-key "${DUMMYSIGN}" +test_signed_pull "dummy" "" + +if ! has_libsodium; then + echo "ok ed25519-key pull signed commit # SKIP due libsodium unavailability" + echo "ok ed25519-key re-pull signature for stored commit # SKIP due libsodium unavailability" + echo "ok ed25519-key+file pull signed commit # SKIP due libsodium unavailability" + echo "ok ed25519-key+file re-pull signature for stored commit # SKIP due libsodium unavailability" + echo "ok ed25519-file pull signed commit # SKIP due libsodium unavailability" + echo "ok ed25519-file re-pull signature for stored commit # SKIP due libsodium unavailability" + exit 0 +fi + +# Test ostree sign with 'ed25519' module +gen_ed25519_keys +PUBLIC=${ED25519PUBLIC} +SEED=${ED25519SEED} +SECRET=${ED25519SECRET} + +COMMIT_ARGS="--sign=${SECRET} --sign-type=ed25519" + +repo_init --set=sign-verify=true +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-key "${PUBLIC}" +test_signed_pull "ed25519" "key" + +# Prepare files with public ed25519 signatures +PUBKEYS="$(mktemp -p ${test_tmpdir} ed25519_XXXXXX.ed25519)" + +# Test the file with multiple keys without a valid public key +for((i=0;i<100;i++)); do + # Generate a list with some public signatures + gen_ed25519_random_public +done > ${PUBKEYS} + +# Test case with the file containing incorrect signatures and with the correct key set +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-file "${PUBKEYS}" +test_signed_pull "ed25519" "key+file" + +# Add correct key into the list +echo ${PUBLIC} >> ${PUBKEYS} + +repo_init --set=sign-verify=true +${CMD_PREFIX} ostree --repo=repo config set 'remote "origin"'.verification-file "${PUBKEYS}" +test_signed_pull "ed25519" "file" + diff --git a/tests/test-summary-collections.sh b/tests/test-summary-collections.sh index 777d3d0c..9885c5ea 100755 --- a/tests/test-summary-collections.sh +++ b/tests/test-summary-collections.sh @@ -63,7 +63,7 @@ mkdir collection-repo ostree_repo_init collection-repo --collection-id org.example.RemoteCollection mkdir -p adir ${CMD_PREFIX} ostree --repo=collection-repo commit --branch=rcommit -m rcommit -s rcommit adir -${CMD_PREFIX} ostree --repo=repo remote add --no-gpg-verify --collection-id org.example.RemoteCollection collection-repo-remote "file://${test_tmpdir}/collection-repo" +${CMD_PREFIX} ostree --repo=repo remote add --no-sign-verify --collection-id org.example.RemoteCollection collection-repo-remote "file://${test_tmpdir}/collection-repo" ${CMD_PREFIX} ostree --repo=repo pull collection-repo-remote rcommit ${CMD_PREFIX} ostree --repo=repo summary --update @@ -75,7 +75,7 @@ mkdir no-collection-repo ostree_repo_init no-collection-repo mkdir -p adir2 ${CMD_PREFIX} ostree --repo=no-collection-repo commit --branch=rcommit2 -m rcommit2 -s rcommit2 adir2 -${CMD_PREFIX} ostree --repo=repo remote add --no-gpg-verify no-collection-repo-remote "file://${test_tmpdir}/no-collection-repo" +${CMD_PREFIX} ostree --repo=repo remote add --no-sign-verify no-collection-repo-remote "file://${test_tmpdir}/no-collection-repo" ${CMD_PREFIX} ostree --repo=repo pull no-collection-repo-remote rcommit2 ${CMD_PREFIX} ostree --repo=repo summary --update