gpgme/src/engine-assuan.c
Werner Koch 0a64c7d0c6
Add API gpgme_op_random_bytes.
* src/genrandom.c: New.
* src/Makefile.am: Add new file.
* src/engine-backend.h (struct engine_ops): Add func ptr getdirect.
Adjust all engine_ops.
* src/engine-gpg.c (gpg_getdirect): New.
(_gpgme_engine_ops_gpg): Connect new handler.
* src/gpgme.h.in (gpgme_random_mode_t): New.
(GPGME_RANDOM_MODE_NORMAL): New.
(GPGME_RANDOM_MODE_ZBASE32): New.
(gpgme_op_random_bytes): New public function
* src/libgpgme.vers: Add function.
* src/gpgme.def: Add function.

* tests/run-genrandom.c: New.
* tests/Makefile.am: Add new file.
--

This is a first take on this the mode parameter allows to extend the
function if ever needed.  Due to the gpg calling and fd setup overhead
this function is not yet very fast but its purpose is to get
"approved" random bytes.  We might eventually extend it to keep a
small internal cache of random numbers and get for example 128 random
bytes directly from gpg and deliver only the few required.

GnuPG-bug-id: 6694
2025-02-26 14:11:20 +01:00

857 lines
22 KiB
C
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* engine-assuan.c - Low-level Assuan protocol engine
* Copyright (C) 2009 g10 Code GmbH
*
* This file is part of GPGME.
*
* GPGME 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.
*
* GPGME 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 <https://gnu.org/licenses/>.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
/*
Note: This engine requires a modern Assuan server which uses
gpg-error codes. In particular there is no backward compatible
mapping of old Assuan error codes implemented.
*/
#if HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_SYS_TYPES_H
# include <sys/types.h>
#endif
#include <assert.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#ifdef HAVE_LOCALE_H
#include <locale.h>
#endif
#include <errno.h>
#include "gpgme.h"
#include "util.h"
#include "ops.h"
#include "wait.h"
#include "priv-io.h"
#include "sema.h"
#include "assuan.h"
#include "debug.h"
#include "engine-backend.h"
typedef struct
{
int fd; /* FD we talk about. */
int server_fd;/* Server FD for this connection. */
int dir; /* Inbound/Outbound, maybe given implicit? */
void *data; /* Handler-specific data. */
void *tag; /* ID from the user for gpgme_remove_io_callback. */
} iocb_data_t;
/* Engine instance data. */
struct engine_llass
{
assuan_context_t assuan_ctx;
int lc_ctype_set;
int lc_messages_set;
iocb_data_t status_cb;
struct gpgme_io_cbs io_cbs;
/* Hack for old opassuan.c interface, see there the result struct. */
gpg_error_t last_op_err;
/* User provided callbacks. */
struct {
gpgme_assuan_data_cb_t data_cb;
void *data_cb_value;
gpgme_assuan_inquire_cb_t inq_cb;
void *inq_cb_value;
gpgme_assuan_status_cb_t status_cb;
void *status_cb_value;
} user;
/* Option flags. */
struct {
int gpg_agent:1; /* Assume this is a gpg-agent connection. */
} opt;
char request_origin[10]; /* Copy from the CTX. */
};
typedef struct engine_llass *engine_llass_t;
gpg_error_t _gpgme_engine_assuan_last_op_err (void *engine)
{
engine_llass_t llass = engine;
return llass->last_op_err;
}
/* Prototypes. */
static void llass_io_event (void *engine,
gpgme_event_io_t type, void *type_data);
/* return the default home directory. */
static const char *
llass_get_home_dir (void)
{
/* For this engine the home directory is not a filename but a string
used to convey options. The exclamation mark is a marker to show
that this is not a directory name. Options are strings delimited
by a space. The only option defined for now is GPG_AGENT to
enable GPG_AGENT specific commands to send to the server at
connection startup. */
return "!GPG_AGENT";
}
static char *
llass_get_version (const char *file_name)
{
(void)file_name;
return NULL;
}
static const char *
llass_get_req_version (void)
{
return NULL;
}
static void
close_notify_handler (int fd, void *opaque)
{
engine_llass_t llass = opaque;
assert (fd != -1);
if (llass->status_cb.fd == fd)
{
if (llass->status_cb.tag)
llass->io_cbs.remove (llass->status_cb.tag);
llass->status_cb.fd = -1;
llass->status_cb.tag = NULL;
}
}
static gpgme_error_t
llass_cancel (void *engine)
{
engine_llass_t llass = engine;
if (!llass)
return gpg_error (GPG_ERR_INV_VALUE);
if (llass->status_cb.fd != -1)
_gpgme_io_close (llass->status_cb.fd);
if (llass->assuan_ctx)
{
assuan_release (llass->assuan_ctx);
llass->assuan_ctx = NULL;
}
return 0;
}
static gpgme_error_t
llass_cancel_op (void *engine)
{
engine_llass_t llass = engine;
if (!llass)
return gpg_error (GPG_ERR_INV_VALUE);
if (llass->status_cb.fd != -1)
_gpgme_io_close (llass->status_cb.fd);
return 0;
}
static void
llass_release (void *engine)
{
engine_llass_t llass = engine;
if (!llass)
return;
llass_cancel (engine);
free (llass);
}
/* Create a new instance. If HOME_DIR is NULL standard options for use
with gpg-agent are issued. */
static gpgme_error_t
llass_new (void **engine, const char *file_name, const char *home_dir,
const char *version)
{
gpgme_error_t err = 0;
engine_llass_t llass;
char *optstr;
char *env_tty = NULL;
(void)version; /* Not yet used. */
llass = calloc (1, sizeof *llass);
if (!llass)
return gpg_error_from_syserror ();
llass->status_cb.fd = -1;
llass->status_cb.dir = 1;
llass->status_cb.tag = 0;
llass->status_cb.data = llass;
/* Parse_options. */
if (home_dir && *home_dir == '!')
{
home_dir++;
/* Very simple parser only working for the one option we support. */
/* Note that wk promised to write a regression test if this
parser will be extended. */
if (!strncmp (home_dir, "GPG_AGENT", 9)
&& (!home_dir[9] || home_dir[9] == ' '))
llass->opt.gpg_agent = 1;
}
err = assuan_new_ext (&llass->assuan_ctx, GPG_ERR_SOURCE_GPGME,
&_gpgme_assuan_malloc_hooks, _gpgme_assuan_log_cb,
NULL);
if (err)
goto leave;
assuan_ctx_set_system_hooks (llass->assuan_ctx, &_gpgme_assuan_system_hooks);
assuan_set_flag (llass->assuan_ctx, ASSUAN_CONVEY_COMMENTS, 1);
err = assuan_socket_connect (llass->assuan_ctx, file_name, 0, 0);
if (err)
goto leave;
if (llass->opt.gpg_agent)
{
char *dft_display = NULL;
err = _gpgme_getenv ("DISPLAY", &dft_display);
if (err)
goto leave;
if (dft_display)
{
if (gpgrt_asprintf (&optstr, "OPTION display=%s", dft_display) < 0)
{
err = gpg_error_from_syserror ();
free (dft_display);
goto leave;
}
free (dft_display);
err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL, NULL,
NULL, NULL, NULL);
gpgrt_free (optstr);
if (err)
goto leave;
}
}
if (llass->opt.gpg_agent)
err = _gpgme_getenv ("GPG_TTY", &env_tty);
if (llass->opt.gpg_agent && (isatty (1) || env_tty || err))
{
int rc = 0;
char dft_ttyname[64];
char *dft_ttytype = NULL;
if (err)
goto leave;
else if (env_tty)
{
snprintf (dft_ttyname, sizeof (dft_ttyname), "%s", env_tty);
free (env_tty);
}
else
rc = ttyname_r (1, dft_ttyname, sizeof (dft_ttyname));
/* Even though isatty() returns 1, ttyname_r() may fail in many
ways, e.g., when /dev/pts is not accessible under chroot. */
if (!rc)
{
if (gpgrt_asprintf (&optstr, "OPTION ttyname=%s", dft_ttyname) < 0)
{
err = gpg_error_from_syserror ();
goto leave;
}
err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL, NULL,
NULL, NULL, NULL);
gpgrt_free (optstr);
if (err)
goto leave;
err = _gpgme_getenv ("TERM", &dft_ttytype);
if (err)
goto leave;
if (dft_ttytype)
{
if (gpgrt_asprintf (&optstr, "OPTION ttytype=%s", dft_ttytype)< 0)
{
err = gpg_error_from_syserror ();
free (dft_ttytype);
goto leave;
}
free (dft_ttytype);
err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL,
NULL, NULL, NULL, NULL);
gpgrt_free (optstr);
if (err)
goto leave;
}
}
}
#ifdef HAVE_W32_SYSTEM
/* Under Windows we need to use AllowSetForegroundWindow. Tell
llass to tell us when it needs it. */
if (!err && llass->opt.gpg_agent)
{
err = assuan_transact (llass->assuan_ctx, "OPTION allow-pinentry-notify",
NULL, NULL, NULL, NULL, NULL, NULL);
if (gpg_err_code (err) == GPG_ERR_UNKNOWN_OPTION)
err = 0; /* This work only with recent gpg-agents. */
}
#endif /*HAVE_W32_SYSTEM*/
leave:
/* Close the server ends of the pipes (because of this, we must use
the stored server_fd_str in the function start). Our ends are
closed in llass_release(). */
if (err)
llass_release (llass);
else
*engine = llass;
return err;
}
/* Copy flags from CTX into the engine object. */
static void
llass_set_engine_flags (void *engine, const gpgme_ctx_t ctx)
{
engine_llass_t llass = engine;
if (ctx->request_origin)
{
if (strlen (ctx->request_origin) + 1 > sizeof llass->request_origin)
strcpy (llass->request_origin, "xxx"); /* Too long - force error */
else
strcpy (llass->request_origin, ctx->request_origin);
}
else
*llass->request_origin = 0;
}
static gpgme_error_t
llass_set_locale (void *engine, int category, const char *value)
{
gpgme_error_t err;
engine_llass_t llass = engine;
char *optstr;
const char *catstr;
if (!llass->opt.gpg_agent)
return 0;
/* FIXME: If value is NULL, we need to reset the option to default.
But we can't do this. So we error out here. gpg-agent needs
support for this. */
if (0)
;
#ifdef LC_CTYPE
else if (category == LC_CTYPE)
{
catstr = "lc-ctype";
if (!value && llass->lc_ctype_set)
return gpg_error (GPG_ERR_INV_VALUE);
if (value)
llass->lc_ctype_set = 1;
}
#endif
#ifdef LC_MESSAGES
else if (category == LC_MESSAGES)
{
catstr = "lc-messages";
if (!value && llass->lc_messages_set)
return gpg_error (GPG_ERR_INV_VALUE);
if (value)
llass->lc_messages_set = 1;
}
#endif /* LC_MESSAGES */
else
return gpg_error (GPG_ERR_INV_VALUE);
/* FIXME: Reset value to default. */
if (!value)
return 0;
if (gpgrt_asprintf (&optstr, "OPTION %s=%s", catstr, value) < 0)
err = gpg_error_from_syserror ();
else
{
err = assuan_transact (llass->assuan_ctx, optstr, NULL, NULL,
NULL, NULL, NULL, NULL);
gpgrt_free (optstr);
}
return err;
}
/* This is the inquiry callback. It handles stuff which ee need to
handle here and passes everything on to the user callback. */
static gpgme_error_t
inquire_cb (engine_llass_t llass, const char *keyword, const char *args)
{
gpg_error_t err, err2;
gpgme_data_t data = NULL;
char buf[1024];
gpgme_ssize_t n;
if (llass->opt.gpg_agent && !strcmp (keyword, "PINENTRY_LAUNCHED"))
{
_gpgme_allow_set_foreground_window ((pid_t)strtoul (args, NULL, 10));
}
if (llass->user.inq_cb)
{
err = llass->user.inq_cb (llass->user.inq_cb_value,
keyword, args, &data);
if (!err && data)
{
while ((n = gpgme_data_read (data, buf, sizeof buf)) > 0)
{
err = assuan_send_data (llass->assuan_ctx, buf, n);
if (err)
break;
}
/* Tell the caller that we are finished with the data
* object. The error code from assuan_send_data has
* priority over the one from the cleanup function. */
err2 = llass->user.inq_cb (llass->user.inq_cb_value,
NULL, NULL, &data);
if (!err)
err = err2;
}
}
else
err = 0;
return err;
}
static gpgme_error_t
llass_status_handler (void *opaque, int fd)
{
struct io_cb_data *data = (struct io_cb_data *) opaque;
engine_llass_t llass = (engine_llass_t) data->handler_value;
gpgme_error_t err = 0;
char *line;
size_t linelen;
do
{
err = assuan_read_line (llass->assuan_ctx, &line, &linelen);
if (err)
{
/* Reading a full line may not be possible when
communicating over a socket in nonblocking mode. In this
case, we are done for now. */
if (gpg_err_code (err) == GPG_ERR_EAGAIN)
{
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: EAGAIN reading assuan line (ignored)", fd);
err = 0;
continue;
}
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: error reading assuan line: %s",
fd, gpg_strerror (err));
}
else if (linelen >= 2 && line[0] == 'D' && line[1] == ' ')
{
char *src = line + 2;
char *end = line + linelen;
char *dst = src;
linelen = 0;
while (src < end)
{
if (*src == '%' && src + 2 < end)
{
/* Handle escaped characters. */
++src;
*dst++ = _gpgme_hextobyte (src);
src += 2;
}
else
*dst++ = *src++;
linelen++;
}
src = line + 2;
if (linelen && llass->user.data_cb)
err = llass->user.data_cb (llass->user.data_cb_value,
src, linelen);
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: D inlinedata; status from cb: %s",
fd, (llass->user.data_cb ?
(err? gpg_strerror (err):"ok"):"no callback"));
}
else if (linelen >= 3
&& line[0] == 'E' && line[1] == 'N' && line[2] == 'D'
&& (line[3] == '\0' || line[3] == ' '))
{
/* END received. Tell the data callback. */
if (llass->user.data_cb)
err = llass->user.data_cb (llass->user.data_cb_value, NULL, 0);
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: END line; status from cb: %s",
fd, (llass->user.data_cb ?
(err? gpg_strerror (err):"ok"):"no callback"));
}
else if (linelen > 2 && line[0] == 'S' && line[1] == ' ')
{
char *args;
char *src;
for (src=line+2; *src == ' '; src++)
;
args = strchr (src, ' ');
if (!args)
args = line + linelen; /* Let it point to an empty string. */
else
*(args++) = 0;
while (*args == ' ')
args++;
if (llass->user.status_cb)
err = llass->user.status_cb (llass->user.status_cb_value,
src, args);
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: S line (%s) - status from cb: %s",
fd, line+2, (llass->user.status_cb ?
(err? gpg_strerror (err):"ok"):"no callback"));
}
else if (linelen >= 7
&& line[0] == 'I' && line[1] == 'N' && line[2] == 'Q'
&& line[3] == 'U' && line[4] == 'I' && line[5] == 'R'
&& line[6] == 'E'
&& (line[7] == '\0' || line[7] == ' '))
{
char *src;
char *args;
for (src=line+7; *src == ' '; src++)
;
args = strchr (src, ' ');
if (!args)
args = line + linelen; /* Let it point to an empty string. */
else
*(args++) = 0;
while (*args == ' ')
args++;
err = inquire_cb (llass, src, args);
if (!err)
{
/* Flush and send END. */
err = assuan_send_data (llass->assuan_ctx, NULL, 0);
}
else if (gpg_err_code (err) == GPG_ERR_ASS_CANCELED)
{
/* Flush and send CANcel. */
err = assuan_send_data (llass->assuan_ctx, NULL, 1);
}
}
else if (linelen >= 3
&& line[0] == 'E' && line[1] == 'R' && line[2] == 'R'
&& (line[3] == '\0' || line[3] == ' '))
{
if (line[3] == ' ')
err = atoi (line+4);
else
err = gpg_error (GPG_ERR_GENERAL);
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: ERR line: %s",
fd, err ? gpg_strerror (err) : "ok");
/* Command execution errors are not fatal, as we use
a session based protocol. */
data->op_err = err;
llass->last_op_err = err;
/* The caller will do the rest (namely, call cancel_op,
which closes status_fd). */
return 0;
}
else if (linelen >= 2
&& line[0] == 'O' && line[1] == 'K'
&& (line[2] == '\0' || line[2] == ' '))
{
TRACE (DEBUG_CTX, "gpgme:llass_status_handler", llass,
"fd 0x%x: OK line", fd);
llass->last_op_err = 0;
_gpgme_io_close (llass->status_cb.fd);
return 0;
}
else
{
/* Comment line or invalid line. */
}
}
while (!err && assuan_pending_line (llass->assuan_ctx));
return err;
}
static gpgme_error_t
add_io_cb (engine_llass_t llass, iocb_data_t *iocbd, gpgme_io_cb_t handler)
{
gpgme_error_t err;
TRACE_BEG (DEBUG_ENGINE, "engine-assuan:add_io_cb", llass,
"fd=%d, dir %d", iocbd->fd, iocbd->dir);
err = (*llass->io_cbs.add) (llass->io_cbs.add_priv,
iocbd->fd, iocbd->dir,
handler, iocbd->data, &iocbd->tag);
if (err)
return TRACE_ERR (err);
if (!iocbd->dir)
/* FIXME Kludge around poll() problem. */
err = _gpgme_io_set_nonblocking (iocbd->fd);
return TRACE_ERR (err);
}
static gpgme_error_t
start (engine_llass_t llass, const char *command)
{
gpgme_error_t err;
assuan_fd_t afdlist[5];
int fdlist[5];
int nfds;
int i;
if (*llass->request_origin && llass->opt.gpg_agent)
{
char *cmd;
cmd = _gpgme_strconcat ("OPTION pretend-request-origin=",
llass->request_origin, NULL);
if (!cmd)
return gpg_error_from_syserror ();
err = assuan_transact (llass->assuan_ctx, cmd, NULL, NULL, NULL,
NULL, NULL, NULL);
free (cmd);
if (err && gpg_err_code (err) != GPG_ERR_UNKNOWN_OPTION)
return err;
}
/* We need to know the fd used by assuan for reads. We do this by
using the assumption that the first returned fd from
assuan_get_active_fds() is always this one. */
nfds = assuan_get_active_fds (llass->assuan_ctx, 0 /* read fds */,
afdlist, DIM (afdlist));
if (nfds < 1)
return gpg_error (GPG_ERR_GENERAL); /* FIXME */
/* For now... */
for (i = 0; i < nfds; i++)
fdlist[i] = (int) afdlist[i];
/* We "duplicate" the file descriptor, so we can close it here (we
can't close fdlist[0], as that is closed by libassuan, and
closing it here might cause libassuan to close some unrelated FD
later). Alternatively, we could special case status_fd and
register/unregister it manually as needed, but this increases
code duplication and is more complicated as we can not use the
close notifications etc. A third alternative would be to let
Assuan know that we closed the FD, but that complicates the
Assuan interface. */
llass->status_cb.fd = _gpgme_io_dup (fdlist[0]);
if (llass->status_cb.fd < 0)
return gpg_error_from_syserror ();
if (_gpgme_io_set_close_notify (llass->status_cb.fd,
close_notify_handler, llass))
{
_gpgme_io_close (llass->status_cb.fd);
llass->status_cb.fd = -1;
return gpg_error (GPG_ERR_GENERAL);
}
err = add_io_cb (llass, &llass->status_cb, llass_status_handler);
if (!err)
err = assuan_write_line (llass->assuan_ctx, command);
/* FIXME: If *command == '#' no answer is expected. */
if (!err)
llass_io_event (llass, GPGME_EVENT_START, NULL);
return err;
}
static gpgme_error_t
llass_transact (void *engine,
const char *command,
gpgme_assuan_data_cb_t data_cb,
void *data_cb_value,
gpgme_assuan_inquire_cb_t inq_cb,
void *inq_cb_value,
gpgme_assuan_status_cb_t status_cb,
void *status_cb_value)
{
engine_llass_t llass = engine;
gpgme_error_t err;
if (!llass || !command || !*command)
return gpg_error (GPG_ERR_INV_VALUE);
llass->user.data_cb = data_cb;
llass->user.data_cb_value = data_cb_value;
llass->user.inq_cb = inq_cb;
llass->user.inq_cb_value = inq_cb_value;
llass->user.status_cb = status_cb;
llass->user.status_cb_value = status_cb_value;
err = start (llass, command);
return err;
}
static void
llass_set_io_cbs (void *engine, gpgme_io_cbs_t io_cbs)
{
engine_llass_t llass = engine;
llass->io_cbs = *io_cbs;
}
static void
llass_io_event (void *engine, gpgme_event_io_t type, void *type_data)
{
engine_llass_t llass = engine;
TRACE (DEBUG_ENGINE, "gpgme:llass_io_event", llass,
"event %p, type %d, type_data %p",
llass->io_cbs.event, type, type_data);
if (llass->io_cbs.event)
(*llass->io_cbs.event) (llass->io_cbs.event_priv, type, type_data);
}
struct engine_ops _gpgme_engine_ops_assuan =
{
/* Static functions. */
_gpgme_get_default_agent_socket,
llass_get_home_dir,
llass_get_version,
llass_get_req_version,
llass_new,
/* Member functions. */
llass_release,
NULL, /* reset */
NULL, /* set_status_cb */
NULL, /* set_status_handler */
NULL, /* set_command_handler */
NULL, /* set_colon_line_handler */
llass_set_locale,
NULL, /* set_protocol */
llass_set_engine_flags,
NULL, /* decrypt */
NULL, /* delete */
NULL, /* edit */
NULL, /* encrypt */
NULL, /* encrypt_sign */
NULL, /* export */
NULL, /* export_ext */
NULL, /* genkey */
NULL, /* import */
NULL, /* keylist */
NULL, /* keylist_ext */
NULL, /* keylist_data */
NULL, /* keysign */
NULL, /* revsig */
NULL, /* tofu_policy */
NULL, /* sign */
NULL, /* verify */
NULL, /* getauditlog */
NULL, /* setexpire */
NULL, /* setownertrust */
llass_transact, /* opassuan_transact */
NULL, /* getdirect */
NULL, /* conf_load */
NULL, /* conf_save */
NULL, /* conf_dir */
NULL, /* query_swdb */
llass_set_io_cbs,
llass_io_event,
llass_cancel,
llass_cancel_op,
NULL, /* passwd */
NULL, /* set_pinentry_mode */
NULL /* opspawn */
};