diff options
author | Daiki Ueno <dueno@src.gnome.org> | 2020-09-26 11:34:34 +0200 |
---|---|---|
committer | Daiki Ueno <dueno@src.gnome.org> | 2021-03-27 09:49:47 +0100 |
commit | 0932602d1af98c35bd2030629c517c173b480381 (patch) | |
tree | 00b954c5c70524f4230c088216d648b8524acb5c /gcr/gcr-ssh-agent-service.c | |
parent | 9761517998b59d684f03a14911de9fb3c8d67c76 (diff) | |
download | gcr-0932602d1af98c35bd2030629c517c173b480381.tar.gz |
gcr-ssh-agent: Port from gnome-keyring
This port the ssh-agent support provided as a sub-daemon in
gnome-keyring, as a standalone binary, so that it can easily be
managed through systemd.
Diffstat (limited to 'gcr/gcr-ssh-agent-service.c')
-rw-r--r-- | gcr/gcr-ssh-agent-service.c | 663 |
1 files changed, 663 insertions, 0 deletions
diff --git a/gcr/gcr-ssh-agent-service.c b/gcr/gcr-ssh-agent-service.c new file mode 100644 index 0000000..f0133fb --- /dev/null +++ b/gcr/gcr-ssh-agent-service.c @@ -0,0 +1,663 @@ +/* + * gnome-keyring + * + * Copyright (C) 2007 Stefan Walter + * Copyright (C) 2018 Red Hat, Inc. + * + * This program 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.1 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, see + * <http://www.gnu.org/licenses/>. + * + * Author: Stef Walter <stef@thewalter.net>, Daiki Ueno + */ + +#include "config.h" + +#include "gcr-ssh-agent-service.h" + +#include "gcr-ssh-agent-interaction.h" +#include "gcr-ssh-agent-preload.h" +#include "gcr-ssh-agent-private.h" +#include "gcr-ssh-agent-process.h" +#include "gcr-ssh-agent-util.h" + +#include "egg/egg-buffer.h" +#include "egg/egg-error.h" +#include "egg/egg-secure-memory.h" + +#include <gio/gunixsocketaddress.h> +#include <glib/gstdio.h> +#include <gcr/gcr-base.h> + +#include <glib/gi18n-lib.h> + +EGG_SECURE_DECLARE (ssh_agent); + +typedef gboolean (*GcrSshAgentOperation) (GcrSshAgentService *agent, GSocketConnection *connection, EggBuffer *req, EggBuffer *resp, GCancellable *cancellable, GError **error); +static const GcrSshAgentOperation operations[GCR_SSH_OP_MAX]; + +enum { + PROP_0, + PROP_PATH, + PROP_PRELOAD +}; + +struct _GcrSshAgentService +{ + GObject object; + gchar *path; + GcrSshAgentPreload *preload; + GcrSshAgentProcess *process; + GSocketAddress *address; + GSocketListener *listener; + GHashTable *keys; + GMutex lock; + GCancellable *cancellable; +}; + +G_DEFINE_TYPE (GcrSshAgentService, gcr_ssh_agent_service, G_TYPE_OBJECT); + +static void +gcr_ssh_agent_service_init (GcrSshAgentService *self) +{ + self->keys = g_hash_table_new_full (g_bytes_hash, g_bytes_equal, + (GDestroyNotify)g_bytes_unref, NULL); + g_mutex_init (&self->lock); +} + +static void +gcr_ssh_agent_service_constructed (GObject *object) +{ + GcrSshAgentService *self = GCR_SSH_AGENT_SERVICE (object); + gchar *path; + + path = g_strdup_printf ("%s/.ssh", self->path); + self->process = gcr_ssh_agent_process_new (path); + g_free (path); + + self->listener = G_SOCKET_LISTENER (g_threaded_socket_service_new (-1)); + self->cancellable = g_cancellable_new (); + + G_OBJECT_CLASS (gcr_ssh_agent_service_parent_class)->constructed (object); +} + +static void +gcr_ssh_agent_service_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcrSshAgentService *self = GCR_SSH_AGENT_SERVICE (object); + + switch (prop_id) { + case PROP_PATH: + self->path = g_value_dup_string (value); + break; + case PROP_PRELOAD: + self->preload = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gcr_ssh_agent_service_finalize (GObject *object) +{ + GcrSshAgentService *self = GCR_SSH_AGENT_SERVICE (object); + + g_free (self->path); + g_object_unref (self->preload); + + g_object_unref (self->process); + g_object_unref (self->listener); + g_clear_object (&self->address); + g_mutex_clear (&self->lock); + g_hash_table_unref (self->keys); + g_object_unref (self->cancellable); + + G_OBJECT_CLASS (gcr_ssh_agent_service_parent_class)->finalize (object); +} + +static void +gcr_ssh_agent_service_class_init (GcrSshAgentServiceClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + gobject_class->constructed = gcr_ssh_agent_service_constructed; + gobject_class->set_property = gcr_ssh_agent_service_set_property; + gobject_class->finalize = gcr_ssh_agent_service_finalize; + g_object_class_install_property (gobject_class, PROP_PATH, + g_param_spec_string ("path", "Path", "Path", + "", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); + g_object_class_install_property (gobject_class, PROP_PRELOAD, + g_param_spec_object ("preload", "Preload", "Preload", + GCR_TYPE_SSH_AGENT_PRELOAD, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE)); +} + +static gboolean +relay_request (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + return _gcr_ssh_agent_call (connection, req, resp, cancellable, error); +} + +static gboolean +handle_request (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + GcrSshAgentOperation func; + guchar op; + + egg_buffer_reset (resp); + egg_buffer_add_uint32 (resp, 0); + + /* Decode the operation; on failure, just pass through */ + if (egg_buffer_get_byte (req, 4, NULL, &op) && + op <= GCR_SSH_OP_MAX && operations[op] != NULL) + func = operations[op]; + else + func = relay_request; + + return func (self, connection, req, resp, cancellable, error); +} + +static void +add_key (GcrSshAgentService *self, + GBytes *key) +{ + g_mutex_lock (&self->lock); + g_hash_table_add (self->keys, g_bytes_ref (key)); + g_mutex_unlock (&self->lock); +} + +static void +remove_key (GcrSshAgentService *self, + GBytes *key) +{ + g_mutex_lock (&self->lock); + g_hash_table_remove (self->keys, key); + g_mutex_unlock (&self->lock); +} + +static void +clear_keys (GcrSshAgentService *self) +{ + g_mutex_lock (&self->lock); + g_hash_table_remove_all (self->keys); + g_mutex_unlock (&self->lock); +} + +static void +ensure_key (GcrSshAgentService *self, + GBytes *key) +{ + GcrSshAskpass *askpass; + GError *error = NULL; + gint status; + GcrSshAgentKeyInfo *info; + gchar *unique; + const gchar *label; + GHashTable *fields; + GTlsInteraction *interaction; + gchar *standard_error; + + gchar *argv[] = { + SSH_ADD_EXECUTABLE, + NULL, + NULL + }; + + if (gcr_ssh_agent_service_lookup_key (self, key)) + return; + + info = gcr_ssh_agent_preload_lookup_by_public_key (self->preload, key); + if (!info) + return; + + argv[1] = info->filename; + + fields = g_hash_table_new (g_str_hash, g_str_equal); + unique = g_strdup_printf ("ssh-store:%s", info->filename); + g_hash_table_insert (fields, "unique", unique); + + label = info->comment[0] != '\0' ? info->comment : _("Unnamed"); + + interaction = gcr_ssh_agent_interaction_new (NULL, label, fields); + askpass = gcr_ssh_askpass_new (interaction); + g_object_unref (interaction); + + if (!g_spawn_sync (NULL, argv, NULL, + G_SPAWN_STDOUT_TO_DEV_NULL, + gcr_ssh_askpass_child_setup, askpass, + NULL, &standard_error, &status, &error)) { + g_warning ("couldn't run %s: %s", argv[0], error->message); + g_error_free (error); + } else if (!g_spawn_check_exit_status (status, &error)) { + g_message ("the %s command failed: %s", argv[0], error->message); + g_printerr ("%s", _gcr_ssh_agent_canon_error (standard_error)); + g_free (standard_error); + } else { + add_key (self, key); + } + + g_hash_table_unref (fields); + g_free (unique); + gcr_ssh_agent_key_info_free (info); + g_object_unref (askpass); +} + +static gboolean +on_run (GThreadedSocketService *service, + GSocketConnection *connection, + GObject *source_object, + gpointer user_data) +{ + GcrSshAgentService *self = g_object_ref (GCR_SSH_AGENT_SERVICE (user_data)); + EggBuffer req; + EggBuffer resp; + GError *error; + GSocketConnection *agent_connection; + gboolean ret; + + egg_buffer_init_full (&req, 128, egg_secure_realloc); + egg_buffer_init_full (&resp, 128, (EggBufferAllocator)g_realloc); + + error = NULL; + agent_connection = gcr_ssh_agent_process_connect (self->process, self->cancellable, &error); + if (!agent_connection) { + g_warning ("couldn't connect to ssh-agent: %s", error->message); + g_error_free (error); + goto out; + } + + while (TRUE) { + /* Read in the request */ + error = NULL; + if (!_gcr_ssh_agent_read_packet (connection, &req, self->cancellable, &error)) { + if (error->code != G_IO_ERROR_CANCELLED && + error->code != G_IO_ERROR_CONNECTION_CLOSED) + g_message ("couldn't read from client: %s", error->message); + g_error_free (error); + break; + } + + /* Handle the request */ + error = NULL; + while (!(ret = handle_request (self, agent_connection, &req, &resp, self->cancellable, &error))) { + if (gcr_ssh_agent_process_get_pid (self->process) != 0) { + if (error->code != G_IO_ERROR_CANCELLED) + g_message ("couldn't handle client request: %s", error->message); + g_error_free (error); + goto out; + } + + /* Reconnect to the ssh-agent */ + g_clear_object (&agent_connection); + g_clear_error (&error); + agent_connection = gcr_ssh_agent_process_connect (self->process, self->cancellable, &error); + if (!agent_connection) { + if (error->code != G_IO_ERROR_CANCELLED) + g_message ("couldn't connect to ssh-agent: %s", error->message); + g_error_free (error); + goto out; + } + } + + /* Write the reply back out */ + error = NULL; + if (!_gcr_ssh_agent_write_packet (connection, &resp, self->cancellable, &error)) { + if (error->code != G_IO_ERROR_CANCELLED) + g_message ("couldn't write to client: %s", error->message); + g_error_free (error); + break; + } + } + + out: + egg_buffer_uninit (&req); + egg_buffer_uninit (&resp); + + g_object_unref (agent_connection); + g_object_unref (self); + + return TRUE; +} + +static void +on_closed (GcrSshAgentProcess *process, + gpointer user_data) +{ + GcrSshAgentService *self = GCR_SSH_AGENT_SERVICE (user_data); + clear_keys (self); +} + +gboolean +gcr_ssh_agent_service_start (GcrSshAgentService *self) +{ + gchar *path; + GError *error; + + path = g_strdup_printf ("%s/ssh", self->path); + g_unlink (path); + self->address = g_unix_socket_address_new (path); + g_free (path); + + error = NULL; + if (!g_socket_listener_add_address (self->listener, + self->address, + G_SOCKET_TYPE_STREAM, + G_SOCKET_PROTOCOL_DEFAULT, + NULL, + NULL, + &error)) { + g_warning ("couldn't listen on %s: %s", + g_unix_socket_address_get_path (G_UNIX_SOCKET_ADDRESS (self->address)), + error->message); + g_error_free (error); + return FALSE; + } + + /* For the ssh-add command called upon SSH_AGENTC_SIGN_REQUEST */ + g_setenv ("SSH_AUTH_SOCK", g_unix_socket_address_get_path (G_UNIX_SOCKET_ADDRESS (self->address)), TRUE); + + g_signal_connect (self->listener, "run", G_CALLBACK (on_run), self); + g_signal_connect (self->process, "closed", G_CALLBACK (on_closed), self); + + g_socket_service_start (G_SOCKET_SERVICE (self->listener)); + + return TRUE; +} + +void +gcr_ssh_agent_service_stop (GcrSshAgentService *self) +{ + if (self->address) + g_unlink (g_unix_socket_address_get_path (G_UNIX_SOCKET_ADDRESS (self->address))); + + g_cancellable_cancel (self->cancellable); + g_socket_service_stop (G_SOCKET_SERVICE (self->listener)); +} + +GcrSshAgentService * +gcr_ssh_agent_service_new (const gchar *path, + GcrSshAgentPreload *preload) +{ + g_return_val_if_fail (path, NULL); + g_return_val_if_fail (preload, NULL); + + return g_object_new (GCR_TYPE_SSH_AGENT_SERVICE, + "path", path, + "preload", preload, + NULL); +} + +GcrSshAgentPreload * +gcr_ssh_agent_service_get_preload (GcrSshAgentService *self) +{ + return self->preload; +} + +GcrSshAgentProcess * +gcr_ssh_agent_service_get_process (GcrSshAgentService *self) +{ + return self->process; +} + +gboolean +gcr_ssh_agent_service_lookup_key (GcrSshAgentService *self, + GBytes *key) +{ + gboolean ret; + g_mutex_lock (&self->lock); + ret = g_hash_table_contains (self->keys, key); + g_mutex_unlock (&self->lock); + return ret; +} + +/* ---------------------------------------------------------------------------- */ + +static gboolean +op_add_identity (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + const guchar *blob; + gsize offset = 5; + gsize length; + GBytes *key = NULL; + gboolean ret; + + /* If parsing the request fails, just pass through */ + ret = egg_buffer_get_byte_array (req, offset, &offset, &blob, &length); + if (ret) + key = g_bytes_new (blob, length); + else + g_message ("got unparseable add identity request for ssh-agent"); + + ret = relay_request (self, connection, req, resp, cancellable, error); + if (key) { + if (ret) + add_key (self, key); + g_bytes_unref (key); + } + + return ret; +} + +static GHashTable * +parse_identities_answer (EggBuffer *resp) +{ + GHashTable *answer; + const guchar *blob; + gchar *comment; + gsize length; + gsize offset = 4; + guint32 count; + guchar op; + guint32 i; + + if (!egg_buffer_get_byte (resp, offset, &offset, &op) || + op != GCR_SSH_RES_IDENTITIES_ANSWER || + !egg_buffer_get_uint32 (resp, offset, &offset, &count)) { + g_message ("got unexpected response back from ssh-agent when requesting identities"); + return NULL; + } + + answer = g_hash_table_new_full (g_bytes_hash, g_bytes_equal, (GDestroyNotify)g_bytes_unref, g_free); + + for (i = 0; i < count; i++) { + if (!egg_buffer_get_byte_array (resp, offset, &offset, &blob, &length) || + !egg_buffer_get_string (resp, offset, &offset, &comment, g_realloc)) { + g_message ("got unparseable response back from ssh-agent when requesting identities"); + g_hash_table_unref (answer); + return NULL; + } + g_hash_table_insert (answer, g_bytes_new (blob, length), comment); + } + + return answer; +} + + +static gboolean +op_request_identities (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + GHashTable *answer; + GHashTableIter iter; + gsize length; + guint32 added; + GBytes *key; + GList *keys; + GList *l; + GcrSshAgentPreload *preload; + + if (!relay_request (self, connection, req, resp, cancellable, error)) + return FALSE; + + /* Parse all the keys, and if it fails, just fall through */ + answer = parse_identities_answer (resp); + if (!answer) + return TRUE; + + g_hash_table_iter_init (&iter, answer); + while (g_hash_table_iter_next (&iter, (gpointer *)&key, NULL)) + add_key (self, key); + + added = 0; + + /* Add any preloaded keys not already in answer */ + preload = gcr_ssh_agent_service_get_preload (self); + keys = gcr_ssh_agent_preload_get_keys (preload); + for (l = keys; l != NULL; l = g_list_next (l)) { + GcrSshAgentKeyInfo *info = l->data; + if (!g_hash_table_contains (answer, info->public_key)) { + const guchar *blob = g_bytes_get_data (info->public_key, &length); + egg_buffer_add_byte_array (resp, blob, length); + egg_buffer_add_string (resp, info->comment); + added++; + } + } + + g_list_free_full (keys, (GDestroyNotify)gcr_ssh_agent_key_info_free); + + /* Set the correct amount of keys including the ones we added */ + egg_buffer_set_uint32 (resp, 5, added + g_hash_table_size (answer)); + g_hash_table_unref (answer); + + /* Set the correct total size of the payload */ + egg_buffer_set_uint32 (resp, 0, resp->len - 4); + + return TRUE; +} + +static gboolean +op_sign_request (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + const guchar *blob; + gsize length; + gsize offset = 5; + GBytes *key; + + /* If parsing the request fails, just pass through */ + if (egg_buffer_get_byte_array (req, offset, &offset, &blob, &length)) { + key = g_bytes_new (blob, length); + ensure_key (self, key); + g_bytes_unref (key); + } else { + g_message ("got unparseable sign request for ssh-agent"); + } + + return relay_request (self, connection, req, resp, cancellable, error); +} + +static gboolean +op_remove_identity (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + const guchar *blob; + gsize length; + gsize offset = 5; + GBytes *key = NULL; + gboolean ret; + + /* If parsing the request fails, just pass through */ + ret = egg_buffer_get_byte_array (req, offset, &offset, &blob, &length); + if (ret) + key = g_bytes_new (blob, length); + else + g_message ("got unparseable remove request for ssh-agent"); + + /* Call out ssh-agent anyway to make sure that the key is removed */ + ret = relay_request (self, connection, req, resp, cancellable, error); + if (key) { + if (ret) + remove_key (self, key); + g_bytes_unref (key); + } + return ret; +} + +static gboolean +op_remove_all_identities (GcrSshAgentService *self, + GSocketConnection *connection, + EggBuffer *req, + EggBuffer *resp, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + + ret = relay_request (self, connection, req, resp, cancellable, error); + if (ret) + clear_keys (self); + + return ret; +} + +static const GcrSshAgentOperation operations[GCR_SSH_OP_MAX] = { + NULL, /* 0 */ + NULL, /* GCR_SSH_OP_REQUEST_RSA_IDENTITIES */ + NULL, /* 2 */ + NULL, /* GCR_SSH_OP_RSA_CHALLENGE */ + NULL, /* 4 */ + NULL, /* 5 */ + NULL, /* 6 */ + NULL, /* GCR_SSH_OP_ADD_RSA_IDENTITY */ + NULL, /* GCR_SSH_OP_REMOVE_RSA_IDENTITY */ + NULL, /* GCR_SSH_OP_REMOVE_ALL_RSA_IDENTITIES */ + NULL, /* 10 */ + op_request_identities, /* GCR_SSH_OP_REQUEST_IDENTITIES */ + NULL, /* 12 */ + op_sign_request, /* GCR_SSH_OP_SIGN_REQUEST */ + NULL, /* 14 */ + NULL, /* 15 */ + NULL, /* 16 */ + op_add_identity, /* GCR_SSH_OP_ADD_IDENTITY */ + op_remove_identity, /* GCR_SSH_OP_REMOVE_IDENTITY */ + op_remove_all_identities, /* GCR_SSH_OP_REMOVE_ALL_IDENTITIES */ + NULL, /* GCR_SSH_OP_ADD_SMARTCARD_KEY */ + NULL, /* GCR_SSH_OP_REMOVE_SMARTCARD_KEY */ + NULL, /* GCR_SSH_OP_LOCK */ + NULL, /* GCR_SSH_OP_UNLOCK */ + NULL, /* GCR_SSH_OP_ADD_RSA_ID_CONSTRAINED */ + op_add_identity, /* GCR_SSH_OP_ADD_ID_CONSTRAINED */ + NULL, /* GCR_SSH_OP_ADD_SMARTCARD_KEY_CONSTRAINED */ +}; |