/* * 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 . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 .\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; }