/* * 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 * . * * Author: Stef Walter , 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 #include #include #if WITH_SYSTEMD #include #endif #include 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, PROP_INTERACTION }; struct _GcrSshAgentService { GObject object; gchar *path; GcrSshAgentPreload *preload; GcrSshAgentProcess *process; /* for mocking */ GTlsInteraction *interaction; 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; case PROP_INTERACTION: self->interaction = 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_clear_object (&self->interaction); 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)); g_object_class_install_property (gobject_class, PROP_INTERACTION, g_param_spec_object ("interaction", "Interaction", "Interaction", G_TYPE_TLS_INTERACTION, 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"); if (self->interaction) { interaction = g_object_ref (self->interaction); } else { 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); } 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)); } else { add_key (self, key); } g_free (standard_error); g_hash_table_unref (fields); g_free (unique); gcr_ssh_agent_key_info_free (info); g_clear_error (&error); 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) { GError *error = NULL; #if WITH_SYSTEMD int ret; ret = sd_listen_fds (0); if (ret > 1) { g_warning ("too many file descriptors received"); return FALSE; } else if (ret == 1) { GSocket *socket; socket = g_socket_new_from_fd (SD_LISTEN_FDS_START, &error); if (!socket) { g_warning ("couldn't create a socket: %s", error->message); g_error_free (error); return FALSE; } if (!g_socket_listener_add_socket (self->listener, socket, G_OBJECT (self), &error)) { g_warning ("couldn't bind socket on %d: %s", g_socket_get_fd (socket), error->message); g_object_unref (socket); g_error_free (error); return FALSE; } } else #endif { gchar *path; path = g_strdup_printf ("%s/ssh", self->path); g_unlink (path); self->address = g_unix_socket_address_new (path); g_free (path); 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 */ };