Some tools to play with GnuPG’s smartcard daemon and OpenPGP smartcards.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
scdtools/src/scdrand.c

459 lines
12 KiB

/*
* scdrand - Extract random numbers from a smart card
* Copyright (C) 2014 Damien Goutte-Gattat
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <getopt.h>
#include <err.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <pwd.h>
#include <linux/random.h>
#include <assuan.h>
#include <xmem.h>
#define MAX_RANDOM_BYTES 256
#define DEFAULT_RANDOM_BYTES 8
#define DEFAULT_LOOP_ITERATIONS 1
#define DEFAULT_LOOP_INTERVAL 10
static void
usage(int status)
{
printf("Usage: scdrand [options] [BYTES]\n\
Read the specified number of random bytes from a smartcard\n\
and feed them to the kernel entropy pool. If BYTES is not\n\
specified, the default is %d bytes.\n\n", DEFAULT_RANDOM_BYTES);
puts("Options:\n\
-h, --help Display this help message.\n\
-v, --version Display the version message.\n\
");
printf("\
-l, --loop Loop indefinitely.\n\
-L, --max-loop N Loop N times (default: %d).\n\
-i, --interval N Sleep N seconds between iterations\n\
(default: %d seconds).\n\
\n", DEFAULT_LOOP_ITERATIONS, DEFAULT_LOOP_INTERVAL);
puts("\
-t, --threshold N Do nothing if there is already N bits\n\
of entropy available in the kernel pool.\n\
Set to 0 (default) to always add entropy.\n\
");
printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
exit(status);
}
static void
info(void)
{
printf("\
scdrand %s\n\
Copyright (C) 2014 Damien Goutte-Gattat\n\
\n\
This program is released under the GNU General Public License.\n\
See the COPYING file or <http://www.gnu.org/licenses/gpl.html>.\n\
", VERSION);
exit(EXIT_SUCCESS);
}
/*
* Obtain the pathname of a running GPG Agent's listening socket
* from the environment. Terminate the program if the socket
* cannnot be found.
*
* @return
* A newly allocated string containing the socket's pathname,
* to be freed by the caller.
*/
static char *
get_gpg_agent_socket_name(void)
{
char *env_info, *socket_name, *colon_index;
struct passwd *pwd;
if ( (env_info = getenv("GPG_AGENT_INFO")) ) {
socket_name = xstrdup(env_info);
if ( (colon_index = strchr(socket_name, ':')) )
*colon_index = '\0';
}
else if ( (env_info = getenv("GNUPGHOME")) )
asprintf(&socket_name, "%s/S.gpg-agent", env_info);
else if ( (env_info = getenv("HOME")) )
asprintf(&socket_name, "%s/.gnupg/S.gpg-agent", env_info);
else if ( (pwd = getpwuid(getuid())) )
asprintf(&socket_name, "%s/.gnupg/S.gpg-agent", pwd->pw_dir);
else
errx(EXIT_FAILURE, "Cannot locate GPG Agent socket");
return socket_name;
}
/*
* Data callback for the below function.
*/
gpg_error_t
scd_running_data_cb(void *arg, const void *line, size_t len)
{
*((char **)arg) = xstrdup(line);
return 0;
}
/*
* Obtain the pathname of a running scdaemon's listening socket.
* Terminate the program if the socket cannot be found.
*
* @return
* A newly allocated string containing the socket's pathname,
* to be freed by the caller.
*/
static char *
get_scd_socket_name(void)
{
char *gpg_agent_socket_name, *scd_socket_name;
assuan_context_t ctx;
gpg_error_t ge;
gpg_agent_socket_name = get_gpg_agent_socket_name();
if ( (ge = assuan_new(&ctx)) )
errx(EXIT_FAILURE, "Cannot create Assuan context: %s",
gpg_strerror(ge));
if ( (ge = assuan_socket_connect(ctx, gpg_agent_socket_name,
ASSUAN_INVALID_PID, 0)) )
errx(EXIT_FAILURE, "Cannot connect to GPG Agent: %s",
gpg_strerror(ge));
if ( (ge = assuan_transact(ctx, "GETINFO scd_running",
NULL, NULL, NULL, NULL, NULL, NULL)) )
errx(EXIT_FAILURE, "No running scdaemon found");
if ( (ge = assuan_transact(ctx, "SCD GETINFO socket_name",
scd_running_data_cb, &scd_socket_name,
NULL, NULL, NULL, NULL)) )
errx(EXIT_FAILURE, "Cannot obtain scdaemon's socket name: %s",
gpg_strerror(ge));
assuan_release(ctx);
free(gpg_agent_socket_name);
return scd_socket_name;
}
/*
* Represents a response to a GET CHALLENGE command.
*/
struct challenge
{
size_t len;
unsigned char *data;
};
/*
* Data callback for the below function.
*/
gpg_error_t
get_challenge_data_cb(void *arg, const void *line, size_t len)
{
struct challenge *c;
int i;
c = (struct challenge *)arg;
for ( i = 0; i < len && i < c->len; i++ )
c->data[i] = ((const unsigned char *)line)[i];
c->len = len;
return 0;
}
/*
* Get random data ("challenge") from the smartcard.
*
* @param ctx Assuan context connected to a running scdaemon.
* @param buffer The buffer to store the returned data into.
* @param len Number of bytes of random data to retrieve. The buffer
* must be big enough to store the requested amount.
*
* @return
* The number of bytes of random data actually retrieved and stored
* in the provided buffer.
*/
static int
get_challenge(assuan_context_t ctx, unsigned char *buffer, size_t len)
{
char command[12];
gpg_error_t ge;
struct challenge c;
snprintf(command, sizeof(command), "RANDOM %d", len);
c.len = len;
c.data = buffer;
if ( (ge = assuan_transact(ctx, command, get_challenge_data_cb, &c,
NULL, NULL, NULL, NULL)) )
errx(EXIT_FAILURE, "Cannot get challenge from card: %s",
gpg_strerror(ge));
return c.len;
}
/*
* Connect to a running scdaemon.
*
* @return
* An Assuan context connected to the scdaemon.
*/
static assuan_context_t
connect_to_scdaemon(void)
{
char *socket_name;
assuan_context_t ctx;
gpg_error_t ge;
socket_name = get_scd_socket_name();
if ( (ge = assuan_new(&ctx)) )
errx(EXIT_FAILURE, "Cannot create Assuan context: %s",
gpg_strerror(ge));
if ( (ge = assuan_socket_connect(ctx, socket_name, ASSUAN_INVALID_PID, 0)) )
errx(EXIT_FAILURE, "Cannot connect to scdaemon: %s", gpg_strerror(ge));
free(socket_name);
return ctx;
}
/*
* Wait until the available entropy falls below the specified threshold.
* Available entropy is checked regularly at the specified interval.
*/
static void
wait_for_threshold(unsigned threshold, unsigned interval)
{
int entropy, random_fd, loop = 1;
if ( (random_fd = open("/dev/random", O_RDONLY)) == -1 )
err(EXIT_FAILURE, "Cannot open /dev/random");
while ( loop ) {
if ( ioctl(random_fd, RNDGETENTCNT, &entropy) == -1 )
err(EXIT_FAILURE, "Cannot get available entropy");
if ( entropy < threshold )
loop = 0;
else
sleep(interval);
}
close(random_fd);
}
/*
* Fork a new process solely charged with the task of adding entropy
* to the kernel pool (the only task that requires root privileges).
* The new process will listen to a pipe opened by its parent and
* write the data it receives to the entropy pool.
*
* The function only returns in the parent process; the child process
* remains in a loop until it terminates.
*
* @return
* A file descriptor for the pipe connecting the parent process to
* its child.
*/
static int
start_privileged_process(void)
{
int fd[2];
pid_t pid;
if ( pipe(fd) == -1 )
err(EXIT_FAILURE, "Cannot create pipe");
if ( (pid = fork()) == 0 ) {
int random_fd, n;
char buffer[MAX_RANDOM_BYTES];
struct rand_pool_info *rpi;
close(fd[1]);
if ( (random_fd = open("/dev/random", O_RDONLY)) == -1 )
err(EXIT_FAILURE, "Cannot open /dev/random");
rpi = xmalloc(sizeof(struct rand_pool_info) + MAX_RANDOM_BYTES);
while ( 1 ) {
n = read(fd[0], buffer, sizeof(buffer));
if ( n == -1 )
err(EXIT_FAILURE, "Cannot read from pipe");
if ( n == 0 ) /* EOF, parent process must have closed its end. */
exit(EXIT_SUCCESS);
rpi->entropy_count = n * 8;
rpi->buf_size = n;
memcpy(&(rpi->buf[0]), buffer, n);
if ( ioctl(random_fd, RNDADDENTROPY, rpi) == -1 )
warn("Cannot add entropy");
}
}
else if ( pid > 0 ) {
close(fd[0]);
/* Drop privileges, then return to caller. */
if ( setgid(getgid()) == -1 || setuid(getuid()) == -1 )
err(EXIT_FAILURE, "Cannot drop privileges");
return fd[1];
}
else
err(EXIT_FAILURE, "Cannot fork");
return 0; /* Not reached. */
}
static int
get_uinteger_or_die(const char *arg)
{
unsigned val;
char *endptr;
errno = 0;
val = strtoul(arg, &endptr, 10);
if ( errno != 0 || endptr == arg )
errx(EXIT_FAILURE, "Invalid argument, unsigned integer expected: %s", arg);
return val;
}
int
main(int argc, char **argv)
{
int c, fd, n, loop;
unsigned nbytes, interval, threshold;
assuan_context_t ctx;
unsigned char random_buffer[MAX_RANDOM_BYTES];
struct option options[] = {
{ "help", 0, NULL, 'h' },
{ "version", 0, NULL, 'v' },
{ "loop", 0, NULL, 'l' },
{ "max-loop", 1, NULL, 'L' },
{ "interval", 1, NULL, 'i' },
{ "threshold", 1, NULL, 't' },
{ NULL, 0, NULL, 0 }
};
setprogname(argv[0]);
nbytes = DEFAULT_RANDOM_BYTES;
loop = DEFAULT_LOOP_ITERATIONS;
interval = DEFAULT_LOOP_INTERVAL;
threshold = 0;
while ( (c = getopt_long(argc, argv, "hvlL:i:t:", options, NULL)) != -1 ) {
switch ( c ) {
case 'h':
usage(EXIT_SUCCESS);
break;
case '?':
usage(EXIT_FAILURE);
break;
case 'v':
info();
break;
case 'l':
loop = -1;
break;
case 'L':
loop = get_uinteger_or_die(optarg);
break;
case 'i':
interval = get_uinteger_or_die(optarg);
break;
case 't':
threshold = get_uinteger_or_die(optarg);
break;
}
}
fd = start_privileged_process();
if ( optind < argc ) {
nbytes = get_uinteger_or_die(argv[optind++]);
if ( nbytes == 0 ) /* Funny guy? */
exit(EXIT_FAILURE);
if ( nbytes > MAX_RANDOM_BYTES )
nbytes = MAX_RANDOM_BYTES;
}
#ifndef GPG_ERR_INITIALIZED
gpg_err_init();
#endif
assuan_set_gpg_err_source(GPG_ERR_SOURCE_USER_1);
ctx = connect_to_scdaemon();
while ( loop == -1 || loop-- > 0 ) {
if ( threshold != 0 )
wait_for_threshold(threshold, interval);
n = get_challenge(ctx, random_buffer, nbytes);
write(fd, random_buffer, n);
if ( loop != 0 )
sleep(interval);
}
assuan_release(ctx);
close(fd);
return EXIT_SUCCESS;
}