common: Check signatures when installing OCI images

Co-authored-by: Sebastian Wick <sebastian.wick@redhat.com>
This commit is contained in:
Owen W. Taylor
2024-12-17 14:57:47 -05:00
committed by Sebastian Wick
parent 841f33e451
commit 404aa33ce1
10 changed files with 453 additions and 31 deletions

View File

@@ -68,7 +68,7 @@ import_oci (OstreeRepo *repo, GFile *file,
ref = flatpak_image_source_get_ref (image_source);
commit_checksum = flatpak_pull_from_oci (repo, image_source, NULL,
commit_checksum = flatpak_pull_from_oci (repo, image_source, NULL, NULL,
ref, FLATPAK_PULL_FLAGS_NONE,
NULL, NULL, cancellable, error);
if (commit_checksum == NULL)

View File

@@ -172,6 +172,13 @@ void flatpak_remote_state_add_sideload_dir (FlatpakRemoteState *self,
GFile *path);
void flatpak_remote_state_add_sideload_image_collection (FlatpakRemoteState *self,
FlatpakImageCollection *image_collection);
FlatpakImageSource * flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self,
FlatpakDir *dir,
const char *ref,
const char *opt_rev,
const char *token,
GCancellable *cancellable,
GError **error);
G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakDir, g_object_unref)

View File

@@ -185,6 +185,9 @@ static gboolean flatpak_dir_lookup_remote_filter (FlatpakDir *self,
GRegex **deny_regex,
GError **error);
static char *flatpak_dir_get_remote_signature_lookaside (FlatpakDir *self,
const char *remote_name);
static void ensure_http_session (FlatpakDir *self);
static void flatpak_dir_log (FlatpakDir *self,
@@ -1200,15 +1203,17 @@ lookup_oci_registry_uri_from_summary (GVariant *summary,
}
static FlatpakImageSource *
flatpak_remote_state_new_image_source (FlatpakRemoteState *self,
const char *oci_repository,
const char *digest,
const char *token,
GCancellable *cancellable,
GError **error)
flatpak_remote_state_new_image_source (FlatpakRemoteState *self,
FlatpakDir *dir,
const char *oci_repository,
const char *digest,
const char *token,
GCancellable *cancellable,
GError **error)
{
g_autofree char *registry_uri = NULL;
g_autoptr(FlatpakImageSource) image_source = NULL;
g_autofree char *signature_lookaside = NULL;
if (!flatpak_remote_state_ensure_summary (self, error))
return NULL;
@@ -1217,14 +1222,21 @@ flatpak_remote_state_new_image_source (FlatpakRemoteState *self,
if (registry_uri == NULL)
return NULL;
image_source = flatpak_image_source_new_remote (registry_uri, oci_repository, digest, token, NULL, error);
signature_lookaside = flatpak_dir_get_remote_signature_lookaside (dir, self->remote_name);
image_source = flatpak_image_source_new_remote (registry_uri,
oci_repository,
digest,
token,
signature_lookaside,
NULL, error);
if (image_source == NULL)
return NULL;
return g_steal_pointer (&image_source);
}
static FlatpakImageSource *
FlatpakImageSource *
flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self,
FlatpakDir *dir,
const char *ref,
@@ -1262,7 +1274,7 @@ flatpak_remote_state_fetch_image_source (FlatpakRemoteState *self,
oci_digest = g_strconcat ("sha256:", opt_rev ? opt_rev : latest_rev, NULL);
image_source = flatpak_remote_state_new_image_source (self, oci_repository, oci_digest, token, cancellable, error);
image_source = flatpak_remote_state_new_image_source (self, dir, oci_repository, oci_digest, token, cancellable, error);
if (image_source == NULL)
return NULL;
@@ -6867,7 +6879,7 @@ flatpak_dir_pull_oci (FlatpakDir *self,
g_info ("Pulling OCI image %s", oci_digest);
checksum = flatpak_pull_from_oci (repo, image_source,
checksum = flatpak_pull_from_oci (repo, image_source, NULL,
state->remote_name, ref, flatpak_flags, oci_pull_progress_cb, progress, cancellable, error);
if (checksum == NULL)
@@ -15297,6 +15309,21 @@ flatpak_dir_get_remote_disabled (FlatpakDir *self,
return FALSE;
}
static char *
flatpak_dir_get_remote_signature_lookaside (FlatpakDir *self,
const char *remote_name)
{
GKeyFile *config = flatpak_dir_get_repo_config (self);
g_autofree char *group = get_group (remote_name);
g_autofree char *signature_lookaside = NULL;
signature_lookaside = g_key_file_get_string (config, group, "xa.signature-lookaside", NULL);
if (signature_lookaside == NULL || *signature_lookaside == '\0')
return NULL;
return g_steal_pointer (&signature_lookaside);
}
static char *
flatpak_dir_get_remote_install_authenticator_name (FlatpakDir *self,
const char *remote_name)

View File

@@ -46,6 +46,7 @@ FlatpakImageSource *flatpak_image_source_new_remote (const char *uri,
const char *oci_repository,
const char *digest,
const char *token,
const char *signature_lookaside,
GCancellable *cancellable,
GError **error);
FlatpakImageSource *flatpak_image_source_new_for_location (const char *location,

View File

@@ -184,6 +184,7 @@ flatpak_image_source_new_remote (const char *uri,
const char *oci_repository,
const char *digest,
const char *token,
const char *signature_lookaside,
GCancellable *cancellable,
GError **error)
{
@@ -194,6 +195,7 @@ flatpak_image_source_new_remote (const char *uri,
return NULL;
flatpak_oci_registry_set_token (registry, token);
flatpak_oci_registry_set_signature_lookaside (registry, signature_lookaside);
return flatpak_image_source_new (registry, oci_repository, digest, cancellable, error);
}

View File

@@ -61,6 +61,8 @@ FlatpakOciRegistry * flatpak_oci_registry_new_for_archive (GFile *archi
GError **error);
void flatpak_oci_registry_set_token (FlatpakOciRegistry *self,
const char *token);
void flatpak_oci_registry_set_signature_lookaside (FlatpakOciRegistry *self,
const char *signature_lookaside);
gboolean flatpak_oci_registry_is_local (FlatpakOciRegistry *self);
const char * flatpak_oci_registry_get_uri (FlatpakOciRegistry *self);
FlatpakOciIndex * flatpak_oci_registry_load_index (FlatpakOciRegistry *self,
@@ -164,16 +166,6 @@ gboolean flatpak_archive_read_open_fd_with_checksum (struct archive *a,
GChecksum *checksum,
GError **error);
GBytes *flatpak_oci_sign_data (GBytes *data,
const gchar **okey_ids,
const char *homedir,
GError **error);
FlatpakOciSignature *flatpak_oci_verify_signature (OstreeRepo *repo,
const char *remote_name,
GBytes *signature,
GError **error);
gboolean flatpak_oci_index_ensure_cached (FlatpakHttpSession *http_session,
const char *uri,
GFile *index,
@@ -202,6 +194,7 @@ typedef void (*FlatpakOciPullProgress) (guint64 total_size,
char * flatpak_pull_from_oci (OstreeRepo *repo,
FlatpakImageSource *image_source,
FlatpakImageSource *opt_dst_image_source,
const char *remote,
const char *ref,
FlatpakPullFlags flags,

View File

@@ -30,6 +30,7 @@
#include <archive_entry.h>
#include "flatpak-image-source-private.h"
#include "flatpak-oci-registry-private.h"
#include "flatpak-oci-signatures-private.h"
#include "flatpak-repo-utils-private.h"
#include "flatpak-utils-base-private.h"
#include "flatpak-utils-private.h"
@@ -73,6 +74,7 @@ struct FlatpakOciRegistry
GFile *archive;
int tmp_dfd;
char *token;
char *signature_lookaside;
/* Local repos */
int dfd;
@@ -134,11 +136,12 @@ flatpak_oci_registry_finalize (GObject *object)
g_clear_pointer (&self->http_session, flatpak_http_session_free);
g_clear_pointer (&self->base_uri, g_uri_unref);
g_free (self->uri);
g_free (self->token);
g_clear_pointer (&self->uri, g_free);
g_clear_pointer (&self->token, g_free);
g_clear_object (&self->archive);
g_clear_pointer (&self->tmp_dir, glnx_tmpdir_free);
g_clear_pointer (&self->certificates, flatpak_certificates_free);
g_clear_pointer (&self->signature_lookaside, g_free);
G_OBJECT_CLASS (flatpak_oci_registry_parent_class)->finalize (object);
}
@@ -288,6 +291,21 @@ flatpak_oci_registry_set_token (FlatpakOciRegistry *self,
0, NULL, NULL);
}
void
flatpak_oci_registry_set_signature_lookaside (FlatpakOciRegistry *self,
const char *signature_lookaside)
{
g_set_str (&self->signature_lookaside, signature_lookaside);
if (self->signature_lookaside != NULL)
{
size_t last = strlen (self->signature_lookaside) - 1;
if (self->signature_lookaside[last] == '/')
self->signature_lookaside[last] = '\0';
}
}
FlatpakOciRegistry *
flatpak_oci_registry_new (const char *uri,
gboolean for_write,
@@ -2321,6 +2339,91 @@ flatpak_oci_registry_find_delta_manifest (FlatpakOciRegistry *registry,
return NULL;
}
static FlatpakOciSignatures *
remote_load_signatures (FlatpakOciRegistry *self,
const char *oci_repository,
const char *digest,
GCancellable *cancellable,
GError **error)
{
g_autoptr(FlatpakOciSignatures) signatures = flatpak_oci_signatures_new ();
g_autofree char *digest_algorithm = NULL;
g_autofree char *digest_value = NULL;
guint i;
const char *colon;
if (self->signature_lookaside == NULL)
return g_steal_pointer (&signatures);
/*
* Look for signatures via the containers/image separate storage protocol:
*
* https://github.com/containers/image/blob/main/docs/signature-protocols.md
*/
colon = strchr (digest, ':');
if (colon == NULL)
{
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"can't parse digest %s", digest);
return NULL;
}
digest_algorithm = g_strndup (digest, colon - digest);
digest_value = g_strdup (colon + 1);
for (i = 1; i < G_MAXUINT; i++)
{
g_autoptr(GBytes) bytes = NULL;
g_autoptr(GError) local_error = NULL;
g_autofree char *uri_s = NULL;
uri_s = g_strdup_printf ("%s/%s@%s=%s/signature-%u", self->signature_lookaside,
oci_repository, digest_algorithm, digest_value, i);
bytes = flatpak_load_uri (self->http_session,
uri_s, FLATPAK_HTTP_FLAGS_ACCEPT_OCI,
NULL,
NULL, NULL, NULL,
cancellable, &local_error);
if (bytes == NULL)
{
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
break;
else
{
g_propagate_error (error, g_steal_pointer (&local_error));
return NULL;
}
}
g_info ("Found OCI signature at %s", uri_s);
flatpak_oci_signatures_add_signature (signatures, g_steal_pointer (&bytes));
}
return g_steal_pointer (&signatures);
}
static FlatpakOciSignatures *
flatpak_oci_registry_load_signatures (FlatpakOciRegistry *self,
const char *oci_repository,
const char *digest,
GCancellable *cancellable,
GError **error)
{
if (self->dfd != -1)
{
g_autoptr(FlatpakOciSignatures) signatures = flatpak_oci_signatures_new ();
if (!flatpak_oci_signatures_load_from_dfd (signatures, self->dfd, cancellable, error))
return NULL;
return g_steal_pointer (&signatures);
}
else
return remote_load_signatures (self, oci_repository, digest, cancellable, error);
}
static const char *
get_image_metadata (FlatpakOciIndexImage *img, const char *key)
{
@@ -3079,6 +3182,7 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry,
OstreeRepoCommitState old_state = 0;
g_autofree char *old_diffid = NULL;
g_autoptr(FlatpakOciIndex) index = NULL;
g_autoptr(FlatpakOciSignatures) signatures = NULL;
int n_layers;
int i;
@@ -3179,12 +3283,21 @@ flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry,
if (!flatpak_oci_registry_save_index (dst_registry, index, cancellable, error))
return FALSE;
signatures = flatpak_oci_registry_load_signatures (registry, oci_repository, digest,
cancellable, error);
if (!signatures)
return FALSE;
if (!flatpak_oci_signatures_save_to_dfd (signatures, dst_registry->dfd, cancellable, error))
return FALSE;
return TRUE;
}
char *
flatpak_pull_from_oci (OstreeRepo *repo,
FlatpakImageSource *image_source,
FlatpakImageSource *opt_dst_image_source,
const char *remote,
const char *ref,
FlatpakPullFlags flags,
@@ -3216,13 +3329,31 @@ flatpak_pull_from_oci (OstreeRepo *repo,
FlatpakOciPullProgressData progress_data = { progress_cb, progress_user_data };
g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
g_autoptr(GVariant) metadata = NULL;
g_autoptr(FlatpakOciSignatures) signatures = NULL;
FlatpakOciRegistry *dst_registry = opt_dst_image_source ?
flatpak_image_source_get_registry (opt_dst_image_source) : registry;
const char *dest_oci_repository = opt_dst_image_source ?
flatpak_image_source_get_oci_repository (opt_dst_image_source) : oci_repository;
int n_layers;
int i;
g_assert (g_str_has_prefix (digest, "sha256:"));
manifest_ref = flatpak_image_source_get_ref (image_source);
signatures = flatpak_oci_registry_load_signatures (dst_registry,
dest_oci_repository,
digest,
cancellable, error);
if (!signatures)
return FALSE;
if (!flatpak_oci_signatures_verify (signatures, repo, remote,
dst_registry->uri,
dest_oci_repository,
digest,
error))
return FALSE;
manifest_ref = flatpak_image_source_get_ref (image_source);
if (manifest_ref == NULL)
{
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("No ref specified for OCI image %s"), digest);

View File

@@ -9,9 +9,33 @@
typedef struct _FlatpakOciSignatures FlatpakOciSignatures;
FlatpakOciSignature *flatpak_oci_verify_signature (OstreeRepo *repo,
const char *remote_name,
GBytes *signature,
GError **error);
gboolean flatpak_remote_has_gpg_key (OstreeRepo *repo,
const char *remote_name);
FlatpakOciSignatures *flatpak_oci_signatures_new (void);
void flatpak_oci_signatures_free (FlatpakOciSignatures *self);
void flatpak_oci_signatures_add_signature (FlatpakOciSignatures *self,
GBytes *signature);
gboolean flatpak_oci_signatures_load_from_dfd (FlatpakOciSignatures *self,
int dfd,
GCancellable *cancellable,
GError **error);
gboolean flatpak_oci_signatures_save_to_dfd (FlatpakOciSignatures *self,
int dfd,
GCancellable *cancellable,
GError **error);
gboolean flatpak_oci_signatures_verify (FlatpakOciSignatures *self,
OstreeRepo *repo,
const char *remote_name,
const char *registry_url,
const char *repository_name,
const char *digest,
GError **error);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FlatpakOciSignatures, flatpak_oci_signatures_free)
#endif /* __FLATPAK_OCI_SIGNATURES_H__ */

View File

@@ -28,13 +28,26 @@
#include <gpgme.h>
#include "flatpak-oci-signatures-private.h"
#include "flatpak-uri-private.h"
#include "flatpak-utils-private.h"
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (gpgme_data_t, gpgme_data_release, NULL)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (gpgme_ctx_t, gpgme_release, NULL)
G_DEFINE_AUTO_CLEANUP_FREE_FUNC (gpgme_key_t, gpgme_key_unref, NULL)
gboolean
flatpak_remote_has_gpg_key (OstreeRepo *repo,
const char *remote_name)
{
g_autoptr(GFile) keyring_file = NULL;
g_autofree char *keyring_name = NULL;
keyring_name = g_strdup_printf ("%s.trustedkeys.gpg", remote_name);
keyring_file = g_file_get_child (ostree_repo_get_path (repo), keyring_name);
return g_file_query_exists (keyring_file, NULL);
}
static void
flatpak_gpgme_error_to_gio_error (gpgme_error_t gpg_error,
GError **error)
@@ -158,7 +171,11 @@ flatpak_gpgme_ctx_tmp_home_dir (gpgme_ctx_t gpgme_ctx,
return TRUE;
}
FlatpakOciSignature *
/* WARNING: This verifies that the data is signed with the correct key, but
* does not verify the payload matches what is being installed. The caller
* must do that.
*/
static FlatpakOciSignature *
flatpak_oci_verify_signature (OstreeRepo *repo,
const char *remote_name,
GBytes *signed_data,
@@ -241,3 +258,208 @@ flatpak_oci_verify_signature (OstreeRepo *repo,
return (FlatpakOciSignature *) g_steal_pointer (&json);
}
struct _FlatpakOciSignatures
{
GPtrArray *signatures;
};
FlatpakOciSignatures *
flatpak_oci_signatures_new (void)
{
FlatpakOciSignatures *self = g_new0 (FlatpakOciSignatures, 1);
self->signatures = g_ptr_array_new_with_free_func ((GDestroyNotify)g_bytes_unref);
return self;
}
void
flatpak_oci_signatures_free (FlatpakOciSignatures *self)
{
g_clear_pointer (&self->signatures, g_ptr_array_unref);
g_free (self);
}
void
flatpak_oci_signatures_add_signature (FlatpakOciSignatures *self,
GBytes *signature)
{
g_ptr_array_add (self->signatures, g_bytes_ref (signature));
}
gboolean
flatpak_oci_signatures_load_from_dfd (FlatpakOciSignatures *self,
int dfd,
GCancellable *cancellable,
GError **error)
{
for (guint i = 1; i < G_MAXUINT; i++)
{
g_autofree char *filename = g_strdup_printf ("signature-%u", i);
g_autoptr(GError) local_error = NULL;
GBytes *signature;
signature = flatpak_load_file_at (dfd, filename, cancellable, &local_error);
if (local_error)
{
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
break;
g_propagate_error (error, g_steal_pointer (&local_error));
return FALSE;
}
flatpak_oci_signatures_add_signature (self, g_steal_pointer (&signature));
}
return TRUE;
}
gboolean
flatpak_oci_signatures_save_to_dfd (FlatpakOciSignatures *self,
int dfd,
GCancellable *cancellable,
GError **error)
{
for (guint i = 0; i < self->signatures->len; i++)
{
g_autofree char *filename = g_strdup_printf ("signature-%u", i + 1);
GBytes *signature = g_ptr_array_index (self->signatures, i);
gconstpointer data;
gsize size;
data = g_bytes_get_data (signature, &size);
if (!glnx_file_replace_contents_at (dfd, filename, data, size,
0, cancellable, error))
return FALSE;
}
return TRUE;
}
/*
* Strip the tag and or digest off of a docker-reference. Regular expressions
* based off of:
*
* https://github.com/containers/image/tree/main/docker/reference
*
* For simplicity we don't do normalization - if the signature claims that it
* matches 'ubuntu', we don't match turn that into docker.io/library/ubuntu
* before matching against the domain and repository.
*/
#define TAG "[0-9A-Za-z_][0-9A-Za-z_-]{0,127}"
#define DIGEST "[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}"
#define STRIP_TAG_AND_DIGEST_RE "^(.*?)(?::" TAG ")?" "(?:@" DIGEST ")?$"
static char *
reference_strip_tag_and_digest (const char *str)
{
static gsize regex = 0;
g_autoptr(GMatchInfo) match_info = NULL;
gboolean matched;
if (g_once_init_enter (&regex))
{
g_autoptr(GRegex) compiled = g_regex_new (STRIP_TAG_AND_DIGEST_RE,
G_REGEX_DEFAULT,
G_REGEX_MATCH_DEFAULT,
NULL);
g_assert (compiled);
g_once_init_leave (&regex, (gsize) g_steal_pointer (&compiled));
}
matched = g_regex_match ((GRegex *) regex, str,
G_REGEX_MATCH_DEFAULT, &match_info);
if (!matched)
return NULL;
return g_match_info_fetch (match_info, 1);
}
gboolean
flatpak_oci_signatures_verify (FlatpakOciSignatures *self,
OstreeRepo *repo,
const char *remote_name,
const char *registry_url,
const char *repository_name,
const char *digest,
GError **error)
{
g_autoptr(GUri) uri = NULL;
int port;
g_autofree char *port_prefix = NULL;
g_autofree char *expected_identity = NULL;
if (!flatpak_remote_has_gpg_key (repo, remote_name))
{
g_info ("%s: no GPG key, skipping verification", remote_name);
return TRUE;
}
uri = g_uri_parse (registry_url, FLATPAK_HTTP_URI_FLAGS | G_URI_FLAGS_PARSE_RELAXED, error);
if (uri == NULL)
return FALSE;
port = g_uri_get_port (uri);
if (port != -1 && port != 80 && port != 443)
port_prefix = g_strdup_printf (":%d", g_uri_get_port (uri));
expected_identity = g_strdup_printf ("%s%s/%s",
g_uri_get_host (uri),
port_prefix ? port_prefix : "",
repository_name);
for (guint i = 0; i < self->signatures->len; i++)
{
g_autoptr(FlatpakOciSignature) signature = NULL;
g_autoptr(GError) local_error = NULL;
signature = flatpak_oci_verify_signature (repo, remote_name,
g_ptr_array_index (self->signatures, i),
&local_error);
if (signature == NULL)
{
g_info ("Couldn't verify signature: %s", local_error->message);
continue;
}
if (signature->critical.image.digest == NULL)
{
g_warning ("Signature is missing digest");
continue;
}
if (strcmp (signature->critical.image.digest, digest) != 0)
{
g_warning ("Digest in signature (%s) does not match %s",
signature->critical.image.digest, digest);
continue;
}
if (signature->critical.identity.reference != NULL)
{
g_autofree char *stripped = NULL;
stripped = reference_strip_tag_and_digest (signature->critical.identity.reference);
if (g_strcmp0 (expected_identity, stripped) != 0)
{
g_info ("Identity in signature (%s) does not match %s",
signature->critical.identity.reference,
expected_identity);
continue;
}
}
g_info ("%s: found valid signature for %s@%s",
remote_name, expected_identity, digest);
return TRUE;
}
return flatpak_fail_error (error, FLATPAK_ERROR_UNTRUSTED,
"%s: no valid signatures for %s@%s",
remote_name, expected_identity, digest);
}

View File

@@ -493,6 +493,7 @@ handle_deploy (FlatpakSystemHelper *object,
const char *image_source_digest;
const char *verified_digest;
g_autofree char *upstream_url = NULL;
g_autoptr(FlatpakImageSource) system_image_source = NULL;
ostree_repo_remote_get_url (flatpak_dir_get_repo (system),
arg_origin,
@@ -550,7 +551,21 @@ handle_deploy (FlatpakSystemHelper *object,
return G_DBUS_METHOD_INVOCATION_HANDLED;
}
checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source,
system_image_source =
flatpak_remote_state_fetch_image_source (state,
system,
arg_ref,
verified_digest,
NULL,
NULL, &error);
if (!system_image_source)
{
g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
"Can't fetch image source: %s", error->message);
return G_DBUS_METHOD_INVOCATION_HANDLED;
}
checksum = flatpak_pull_from_oci (flatpak_dir_get_repo (system), image_source, system_image_source,
arg_origin, arg_ref, FLATPAK_PULL_FLAGS_NONE, NULL, NULL, NULL, &error);
if (checksum == NULL)
{