Commit 661767af authored by Stef Walter's avatar Stef Walter Committed by Daiki Ueno

gcr: Implement GcrSshAskpass API

This allows calling ssh programs like ssh-add or ssh itself, and
handling the password prompts.

https://bugzilla.gnome.org/show_bug.cgi?id=735873
parent 3548ba13
......@@ -76,6 +76,7 @@ AM_CPPFLAGS = \
-DSRCDIR="\"@abs_srcdir@\"" \
-DBUILDDIR="\"@abs_builddir@\"" \
-DLOCALEDIR=\""$(datadir)/locale"\" \
-DLIBEXECDIR="\"$(libexecdir)\"" \
$(GLIB_CFLAGS)
LDADD = \
......@@ -87,6 +88,7 @@ TESTS =
check_PROGRAMS =
check_LTLIBRARIES =
lib_LTLIBRARIES =
libexec_PROGRAMS =
noinst_LTLIBRARIES =
noinst_PROGRAMS = $(check_PROGRAMS)
......
......@@ -54,6 +54,7 @@
<xi:include href="xml/gcr-prompt-dialog.xml"/>
<xi:include href="xml/gcr-system-prompt.xml"/>
<xi:include href="xml/gcr-system-prompter.xml"/>
<xi:include href="xml/gcr-ssh-askpass.xml"/>
</part>
<part id="storage">
......
......@@ -695,6 +695,23 @@ gcr_secure_memory_free
gcr_secure_memory_is_secure
</SECTION>
<SECTION>
<FILE>gcr-ssh-askpass</FILE>
GcrSshAskpass
gcr_ssh_askpass_new
gcr_ssh_askpass_get_interaction
gcr_ssh_askpass_child_setup
<SUBSECTION Standard>
GCR_IS_SSH_ASKPASS
GCR_IS_SSH_ASKPASS_CLASS
GCR_SSH_ASKPASS
GCR_SSH_ASKPASS_CLASS
GCR_SSH_ASKPASS_GET_CLASS
GCR_TYPE_SSH_ASKPASS
GcrSshAskpassClass
gcr_ssh_askpass_get_type
</SECTION>
<SECTION>
<FILE>gcr-private</FILE>
<SUBSECTION Private>
......
......@@ -25,6 +25,7 @@ gcr_HEADER_FILES = \
gcr/gcr-secure-memory.h \
gcr/gcr-simple-certificate.h \
gcr/gcr-simple-collection.h \
gcr/gcr-ssh-askpass.h \
gcr/gcr-system-prompt.h \
gcr/gcr-system-prompter.h \
gcr/gcr-trust.h \
......@@ -74,6 +75,7 @@ gcr_PUBLIC_FILES = \
gcr/gcr-secure-memory.c gcr/gcr-secure-memory.h \
gcr/gcr-simple-certificate.c gcr/gcr-simple-certificate.h \
gcr/gcr-simple-collection.c gcr/gcr-simple-collection.h \
gcr/gcr-ssh-askpass.c gcr/gcr-ssh-askpass.h \
gcr/gcr-system-prompt.c gcr/gcr-system-prompt.h \
gcr/gcr-system-prompter.c gcr/gcr-system-prompter.h \
gcr/gcr-types.h \
......@@ -167,6 +169,19 @@ gcr/gcr-dbus-generated.c: $(DBUS_XML_DEFINITIONS)
gcr/gcr-dbus-generated.h: gcr/gcr-dbus-generated.c
libexec_PROGRAMS += gcr-ssh-askpass
gcr_ssh_askpass_SOURCES = gcr/gcr-ssh-askpass.c
gcr_ssh_askpass_CFLAGS = \
-DGCR_SSH_ASKPASS_TOOL \
-DGCR_COMPILATION \
$(GLIB_CFLAGS)
gcr_ssh_askpass_LDADD = \
libegg.la \
$(GLIB_LIBS)
pkgconfig_DATA += \
gcr-base-$(GCR_MAJOR).pc
......@@ -293,7 +308,8 @@ gcr_TESTS = \
test-gnupg-key \
test-gnupg-collection \
test-gnupg-process \
test-system-prompt
test-system-prompt \
test-ssh-askpass
test_certificate_SOURCES = gcr/test-certificate.c
test_certificate_CFLAGS = $(gcr_CFLAGS)
......@@ -359,6 +375,10 @@ test_simple_certificate_SOURCES = gcr/test-simple-certificate.c
test_simple_certificate_CFLAGS = $(gcr_CFLAGS)
test_simple_certificate_LDADD = $(gcr_LIBS)
test_ssh_askpass_SOURCES = gcr/test-ssh-askpass.c
test_ssh_askpass_CFLAGS = $(gcr_CFLAGS)
test_ssh_askpass_LDADD = libegg-test.la $(gcr_LIBS)
test_subject_public_key_SOURCES = gcr/test-subject-public-key.c
test_subject_public_key_CFLAGS = $(gcr_CFLAGS)
test_subject_public_key_LDADD = $(gcr_LIBS)
......
......@@ -52,6 +52,7 @@
#include <gcr/gcr-secure-memory.h>
#include <gcr/gcr-simple-certificate.h>
#include <gcr/gcr-simple-collection.h>
#include <gcr/gcr-ssh-askpass.h>
#include <gcr/gcr-system-prompt.h>
#include <gcr/gcr-system-prompter.h>
#include <gcr/gcr-trust.h>
......
......@@ -218,6 +218,11 @@ gcr_simple_collection_contains
gcr_simple_collection_get_type
gcr_simple_collection_new
gcr_simple_collection_remove
gcr_ssh_askpass_child_setup
gcr_ssh_askpass_executable
gcr_ssh_askpass_get_interaction
gcr_ssh_askpass_get_type
gcr_ssh_askpass_new
gcr_system_prompter_get_mode
gcr_system_prompter_get_prompt_type
gcr_system_prompter_get_prompting
......
/*
* Copyright (C) 2014 Stefan Walter
*
* 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/>.
*
* Auther: Stef Walter <stefw@gnome.org>
*/
#include "config.h"
#include "gcr-ssh-askpass.h"
#include <glib-unix.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <unistd.h>
/* Used from tests to override location */
const char *gcr_ssh_askpass_executable = LIBEXECDIR "/gcr-ssh-askpass";
/**
* SECTION:gcr-ssh-askpass
* @title: GcrSshAskpass
* @short_description: Allows an ssh command to callback for a password
*
* When used as the setup function while spawning an ssh command like ssh-add
* or ssh, this allows callbacks for passwords on the provided interaction.
*/
/**
* GcrSshAskpass:
*
* An object containing the password prompting state.
*/
/**
* GcrSshAskpassClass:
*
* The class for #GcrSshAskpass
*/
enum {
PROP_0,
PROP_INTERACTION
};
struct _GcrSshAskpass {
GObject parent;
GTlsInteraction *interaction;
gchar *directory;
gchar *socket;
guint source;
gint fd;
GCancellable *cancellable;
GMainContext *context;
};
struct _GcrSshAskpassClass {
GObjectClass parent;
};
G_DEFINE_TYPE (GcrSshAskpass, gcr_ssh_askpass, G_TYPE_OBJECT);
static void
gcr_ssh_askpass_init (GcrSshAskpass *self)
{
self->cancellable = g_cancellable_new ();
self->context = g_main_context_ref_thread_default ();
}
static void
gcr_ssh_askpass_set_property (GObject *obj,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GcrSshAskpass *self = GCR_SSH_ASKPASS (obj);
switch (prop_id) {
case PROP_INTERACTION:
self->interaction = g_value_dup_object (value);
g_return_if_fail (self->interaction != NULL);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static void
gcr_ssh_askpass_get_property (GObject *obj,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GcrSshAskpass *self = GCR_SSH_ASKPASS (obj);
switch (prop_id) {
case PROP_INTERACTION:
g_value_set_object (value, gcr_ssh_askpass_get_interaction (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
break;
}
}
static gboolean
write_all (gint fd,
const guchar *buf,
gsize len)
{
guint all = len;
int res;
while (len > 0) {
res = write (fd, buf, len);
if (res <= 0) {
if (errno == EAGAIN || errno == EINTR)
continue;
if (errno != EPIPE)
g_warning ("couldn't write %u bytes to client: %s", all,
res < 0 ? g_strerror (errno) : "");
return FALSE;
} else {
len -= res;
buf += res;
}
}
return TRUE;
}
static GString *
read_all_into_string (gint fd)
{
GString *input = g_string_new ("");
gsize len;
gssize ret;
for (;;) {
len = input->len;
g_string_set_size (input, len + 256);
ret = read (fd, input->str + len, 256);
if (ret < 0) {
if (errno != EINTR && errno != EAGAIN) {
g_critical ("couldn't read from gcr-ssh-askpass: %s", g_strerror (errno));
g_string_free (input, TRUE);
return NULL;
}
} else if (ret == 0) {
return input;
} else {
input->len = len + ret;
input->str[input->len] = '\0';
}
}
}
typedef struct {
gint fd;
GTlsInteraction *interaction;
GCancellable *cancellable;
} AskpassContext;
static gpointer
askpass_thread (gpointer data)
{
AskpassContext *ctx = data;
gboolean success = FALSE;
GTlsPassword *password = NULL;
GTlsInteractionResult res;
GError *error = NULL;
const guchar *value;
GString *input;
gsize length;
input = read_all_into_string (ctx->fd);
if (!input)
goto out;
if (input->len == 0)
g_string_append (input, _("Enter your OpenSSH passphrase"));
g_debug ("asking for ssh-askpass password: %s", input->str);
password = g_tls_password_new (G_TLS_PASSWORD_NONE, input->str);
res = g_tls_interaction_invoke_ask_password (ctx->interaction, password, ctx->cancellable, &error);
g_debug ("ask password returned %d", res);
success = FALSE;
if (res == G_TLS_INTERACTION_HANDLED) {
value = g_tls_password_get_value (password, &length);
if (write_all (ctx->fd, (const guchar *)value, length))
g_debug ("password written to gcr-ssh-askpass");
else
g_message ("failed to write password to gcr-ssh-askpass");
success = TRUE;
} else if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
g_warning ("couldn't prompt for password: %s", error->message);
} else {
g_debug ("unhandled or cancelled ask password");
}
out:
if (!success) {
g_debug ("writing failure to gcr-ssh-askpass");
write_all (ctx->fd, (const guchar *)"\xff", 1);
}
if (password)
g_object_unref (password);
if (input)
g_string_free (input, TRUE);
g_clear_error (&error);
g_close (ctx->fd, NULL);
g_object_unref (ctx->interaction);
g_object_unref (ctx->cancellable);
g_free (ctx);
return NULL;
}
static gboolean
askpass_accept (gint fd,
GIOCondition cond,
gpointer user_data)
{
GcrSshAskpass *self = user_data;
AskpassContext *ctx;
struct sockaddr_un addr;
socklen_t addrlen;
GThread *thread;
gint new_fd;
addrlen = sizeof (addr);
new_fd = accept (fd, (struct sockaddr *) &addr, &addrlen);
if (new_fd < 0) {
if (errno != EAGAIN && errno != EINTR)
g_warning ("couldn't accept new control request: %s", g_strerror (errno));
return TRUE;
}
g_debug ("accepted new connection from gcr-ssh-askpass");
ctx = g_new0 (AskpassContext, 1);
ctx->fd = new_fd;
ctx->interaction = g_object_ref (self->interaction);
ctx->cancellable = g_object_ref (self->cancellable);
thread = g_thread_new ("ssh-askpass", askpass_thread, ctx);
g_thread_unref (thread);
return TRUE;
}
static void
gcr_ssh_askpass_constructed (GObject *obj)
{
GcrSshAskpass *self = GCR_SSH_ASKPASS (obj);
struct sockaddr_un addr;
G_OBJECT_CLASS (gcr_ssh_askpass_parent_class)->constructed (obj);
self->directory = g_build_filename (g_get_user_runtime_dir (), "ssh-askpass.XXXXXX", NULL);
if (!g_mkdtemp_full (self->directory, 0700)) {
g_warning ("couldn't create temporary directory: %s: %s", self->directory, g_strerror (errno));
return;
}
self->socket = g_build_filename (self->directory, "socket", NULL);
self->fd = socket (AF_UNIX, SOCK_STREAM, 0);
if (self->fd < 0) {
g_warning ("couldn't open socket: %s", g_strerror (errno));
return;
}
if (!g_unix_set_fd_nonblocking (self->fd, TRUE, NULL))
g_return_if_reached ();
memset (&addr, 0, sizeof (addr));
addr.sun_family = AF_UNIX;
g_strlcpy (addr.sun_path, self->socket, sizeof (addr.sun_path));
if (bind (self->fd, (struct sockaddr*) &addr, sizeof (addr)) < 0) {
g_warning ("couldn't bind to askpass socket: %s: %s", self->socket, g_strerror (errno));
return;
}
if (listen (self->fd, 128) < 0) {
g_warning ("couldn't listen on askpass socket: %s: %s", self->socket, g_strerror (errno));
return;
}
g_debug ("listening for gcr-ssh-askpass at: %s", self->socket);
self->source = g_unix_fd_add (self->fd, G_IO_IN, askpass_accept, self);
}
static void
gcr_ssh_askpass_dispose (GObject *obj)
{
GcrSshAskpass *self = GCR_SSH_ASKPASS (obj);
g_cancellable_cancel (self->cancellable);
if (self->source) {
g_source_remove (self->source);
self->source = 0;
}
if (self->fd >= 0) {
g_close (self->fd, NULL);
self->fd = -1;
}
if (self->socket) {
g_unlink (self->socket);
g_free (self->socket);
self->socket = NULL;
}
if (self->directory) {
g_rmdir (self->directory);
g_free (self->directory);
self->directory = NULL;
}
if (self->interaction) {
g_object_unref (self->interaction);
self->interaction = NULL;
}
G_OBJECT_CLASS (gcr_ssh_askpass_parent_class)->dispose (obj);
}
static void
gcr_ssh_askpass_finalize (GObject *obj)
{
GcrSshAskpass *self = GCR_SSH_ASKPASS (obj);
g_object_unref (self->cancellable);
g_main_context_unref (self->context);
G_OBJECT_CLASS (gcr_ssh_askpass_parent_class)->finalize (obj);
}
/**
* gcr_ssh_askpass_new:
* @interaction: the interaction to use for prompting paswords
*
* Create a new GcrSshAskpass object which can be used to spawn an
* ssh command and prompt for any necessary passwords.
*
* Use the gcr_ssh_askpass_child_setup() function as a callback with
* g_spawn_sync(), g_spawn_async() or g_spawn_async_with_pipes().
*
* Returns: (transfer full): A new #GcrSshAskpass object
*/
GcrSshAskpass *
gcr_ssh_askpass_new (GTlsInteraction *interaction)
{
g_return_val_if_fail (G_IS_TLS_INTERACTION (interaction), NULL);
return g_object_new (GCR_TYPE_SSH_ASKPASS,
"interaction", interaction,
NULL);
}
/**
* gcr_ssh_askpass_get_interaction:
* @self: a #GcrSshAskpass object
*
* Get the interaction associated with this object.
*
* Returns: (transfer none): the interaction
*/
GTlsInteraction *
gcr_ssh_askpass_get_interaction (GcrSshAskpass *self)
{
g_return_val_if_fail (GCR_IS_SSH_ASKPASS (self), NULL);
return self->interaction;
}
/**
* gcr_ssh_askpass_child_setup:
* @askpass: a #GcrSshAskpass object
*
* Use this function as a callback setup function passed to g_spawn_sync(),
* g_spawn_async(), g_spawn_async_with_pipes().
*/
void
gcr_ssh_askpass_child_setup (gpointer askpass)
{
GcrSshAskpass *self = askpass;
g_setenv ("SSH_ASKPASS", gcr_ssh_askpass_executable, TRUE);
/* ssh wants DISPLAY set in order to use SSH_ASKPASS */
if (!g_getenv ("DISPLAY"))
g_setenv ("DISPLAY", "x", TRUE);
/* For communicating back with ourselves */
if (self->socket)
g_setenv ("GCR_SSH_ASKPASS_SOCKET", self->socket, TRUE);
/* Close the control terminal */
setsid ();
}
static void
gcr_ssh_askpass_class_init (GcrSshAskpassClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->get_property = gcr_ssh_askpass_get_property;
gobject_class->set_property = gcr_ssh_askpass_set_property;
gobject_class->constructed = gcr_ssh_askpass_constructed;
gobject_class->dispose = gcr_ssh_askpass_dispose;
gobject_class->finalize = gcr_ssh_askpass_finalize;
/**
* GcrSshAskpass:interaction:
*
* The interaction used to prompt for passwords.
*/
g_object_class_install_property (gobject_class, PROP_INTERACTION,
g_param_spec_object ("interaction", "Interaction", "Interaction",
G_TYPE_TLS_INTERACTION,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}
#ifdef GCR_SSH_ASKPASS_TOOL
#include "egg/egg-secure-memory.h"
EGG_SECURE_DEFINE_GLIB_GLOBALS ();
EGG_SECURE_DECLARE ("ssh-askpass");
int
main (int argc,
char *argv[])
{
GString *message;
struct sockaddr_un addr;
const gchar *path;
guchar *buf;
gint count;
gint i;
int ret;
int fd;
path = g_getenv ("GCR_SSH_ASKPASS_SOCKET");
if (path == NULL) {
g_printerr ("gcr-ssh-askpass: this program is not meant to be run directly");
return 2;
}
fd = socket (AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) {
g_warning ("couldn't open socket: %s", g_strerror (errno));
return -1;
}
memset (&addr, 0, sizeof (addr));
addr.sun_family = AF_UNIX;
g_strlcpy (addr.sun_path, path, sizeof (addr.sun_path));
if (connect (fd, (struct sockaddr*) &addr, sizeof (addr)) < 0) {
g_warning ("couldn't connect to askpass socket: %s: %s", path, g_strerror (errno));
return -1;
}
message = g_string_new ("");
if (argc > 1) {
for (i = 1; i < argc; i++) {
if (i == 1)
g_string_append_c (message, ' ');
g_string_append (message, argv[i]);
}
}
if (!write_all (fd, (const guchar *)message->str, message->len)) {
g_string_free (message, TRUE);
return -1;
}
g_string_free (message, TRUE);
if (shutdown (fd, SHUT_WR) < 0) {
g_warning ("couldn't shutdown socket: %s", g_strerror (errno));
return -1;
}
count = 0;
buf = egg_secure_alloc (128);
for (;;) {
ret = read (fd, buf, 128);
if (ret < 0) {
if (errno != EINTR && errno != EAGAIN) {
if (errno != ECONNRESET) {
g_critical ("couldn't read from ssh-askpass socket: %s",
g_strerror (errno));
}
egg_secure_free (buf);
return -1;
}
ret = 0;
} else if (ret == 0) {
break;
} else if (!write_all (1, buf, ret)) {
egg_secure_free (buf);
return -1;
}
count += ret;
}
if (count == 1 && buf[0] == 0xff) {
egg_secure_free (buf);
return -1;
}
egg_secure_free (buf);
return 0;
}
#endif /* GCR_SSH_ASKPASS_TOOL */
/*
* Copyright (C) 2014 Stefan Walter
*
* 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 <stefw@gnome.org>
*/
#if !defined (__GCR_INSIDE_HEADER__) && !defined (GCR_COMPILATION)
#error "Only <gcr/gcr.h> or <gcr/gcr-base.h> can be included directly."
#endif
#ifndef __GCR_SSH_ASKPASS_H__
#define __GCR_SSH_ASKPASS_H__
#include <glib-object.h>
#include <gio/gio.h>
G_BEGIN_DECLS
#define GCR_TYPE_SSH_ASKPASS (gcr_ssh_askpass_get_type ())
#define GCR_SSH_ASKPASS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GCR_TYPE_SSH_ASKPASS, GcrSshAskpass))
#define GCR_SSH_ASKPASS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GCR_TYPE_SSH_ASKPASS, GcrSshAskpassClass))
#define GCR_IS_SSH_ASKPASS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GCR_TYPE_SSH_ASKPASS))
#define GCR_IS_SSH_ASKPASS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GCR_TYPE_SSH_ASKPASS))
#define GCR_SSH_ASKPASS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GCR_TYPE_SSH_ASKPASS, GcrSshAskpassClass))
typedef struct _GcrSshAskpass GcrSshAskpass;
typedef struct _GcrSshAskpassClass GcrSshAskpassClass;
GType gcr_ssh_askpass_get_type (void);
GcrSshAskpass * gcr_ssh_askpass_new (GTlsInteraction *interaction);
GTlsInteraction * gcr_ssh_askpass_get_interaction (GcrSshAskpass *self);
void gcr_ssh_askpass_child_setup (gpointer askpass);
G_END_DECLS
#endif /* __GCR_SSH_ASKPASS_H__ */
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
Copyright (C) 2014 Stefan Walter
The Gnome Keyring Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of the
License, or (at your option) any later version.
The Gnome Keyring 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with the Gnome Library; see the file COPYING.LIB. If not,
see <http://www.gnu.org/licenses/>.
Author: Stef Walter <stefw@gnome.org>
*/
#include "config.h"
#include "gcr/gcr-base.h"
#include "egg/egg-testing.h"
#include "egg/mock-interaction.h"
#include <glib/gstdio.h>
extern const char *gcr_ssh_askpass_executable;
typedef struct {