From: Jameson Graef Rollins Date: Tue, 10 Jun 2008 21:17:51 +0000 (-0400) Subject: New client/server components: X-Git-Tag: monkeysphere_0.1-1~62 X-Git-Url: https://codewiz.org/gitweb?a=commitdiff_plain;h=4793624c65673268128fb0146cd9bd1b3cfeb6c4;hp=6c335e70360c7502a2205d21e9f96d4bf2679cbd;p=monkeysphere.git New client/server components: - broke out all common functions to "common" file - put all client commands into "monkeysphere" script - put all server commands into "monkeysphere-server" script - moved all code into src directory to clean things up a bit - this effectively makes obsolete rhesus and howler - added proposed monkeysphere-ssh-proxycommand script that can be called to update known_hosts from ssh ProxyCommand - updated monkeysphere.conf to work as global client config - added monkeysphere-server.conf for server config --- diff --git a/.gitignore b/.gitignore index 80bf65d..0dc4f79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,2 @@ *~ *.[ao] -monkeysphere -gpg2ssh -ssh2gpg diff --git a/gpg2ssh/Makefile b/gpg2ssh/Makefile deleted file mode 100644 index a0b7241..0000000 --- a/gpg2ssh/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -all: monkeysphere gpg2ssh - -monkeysphere: main.c gnutls-helpers.o - gcc -g -Wall --pedantic -o monkeysphere main.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -gpg2ssh: gpg2ssh.c gnutls-helpers.o - gcc -g -Wall --pedantic -o gpg2ssh gpg2ssh.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -ssh2gpg: ssh2gpg.c gnutls-helpers.o - gcc -g -Wall --pedantic -o ssh2gpg ssh2gpg.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -%.o: %.c - gcc -g -Wall --pedantic -o $@ -c $< - -clean: - rm -f monkeysphere gpg2ssh *.o - -.PHONY: clean all diff --git a/gpg2ssh/gnutls-helpers.c b/gpg2ssh/gnutls-helpers.c deleted file mode 100644 index 6eae29e..0000000 --- a/gpg2ssh/gnutls-helpers.c +++ /dev/null @@ -1,364 +0,0 @@ -/* Author: Daniel Kahn Gillmor */ -/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ -/* License: GPL v3 or later */ - -#include "gnutls-helpers.h" -/* for htonl() */ -#include - -/* for setlocale() */ -#include - -/* for isalnum() */ -#include - -int loglevel = 0; - - -void err(const char* fmt, ...) { - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fflush(stderr); -} - -void logfunc(int level, const char* string) { - fprintf(stderr, "GnuTLS Logging (%d): %s\n", level, string); -} - -void init_keyid(gnutls_openpgp_keyid_t keyid) { - memset(keyid, 'x', sizeof(gnutls_openpgp_keyid_t)); -} - - - -void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid) -{ - static const char hex[16] = "0123456789ABCDEF"; - unsigned int kix = 0, outix = 0; - - while (kix < sizeof(gnutls_openpgp_keyid_t)) { - out[outix] = hex[(keyid[kix] >> 4) & 0x0f]; - out[outix + 1] = hex[keyid[kix] & 0x0f]; - kix++; - outix += 2; - } -} - - -int init_gnutls() { - const char* version = NULL; - const char* debug_string = NULL; - int ret; - - if (ret = gnutls_global_init(), ret) { - err("Failed to do gnutls_global_init() (error: %d)\n", ret); - return 1; - } - - version = gnutls_check_version(NULL); - - if (version) - err("gnutls version: %s\n", version); - else { - err("no version found!\n"); - return 1; - } - - if (debug_string = getenv("MONKEYSPHERE_DEBUG"), debug_string) { - loglevel = atoi(debug_string); - gnutls_global_set_log_function(logfunc); - - gnutls_global_set_log_level(loglevel); - err("set log level to %d\n", loglevel); - } - return 0; -} - -void init_datum(gnutls_datum_t* d) { - d->data = NULL; - d->size = 0; -} -void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src) { - dest->data = gnutls_realloc(dest->data, src->size); - dest->size = src->size; - memcpy(dest->data, src->data, src->size); -} -int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b) { - if (a->size > b->size) { - err("a is larger\n"); - return 1; - } - if (a->size < b->size) { - err("b is larger\n"); - return -1; - } - return memcmp(a->data, b->data, a->size); -} -void free_datum(gnutls_datum_t* d) { - gnutls_free(d->data); - d->data = NULL; - d->size = 0; -} - -/* read the passed-in string, store in a single datum */ -int set_datum_string(gnutls_datum_t* d, const char* s) { - unsigned int x = strlen(s)+1; - unsigned char* c = NULL; - - c = gnutls_realloc(d->data, x); - if (NULL == c) - return -1; - d->data = c; - d->size = x; - memcpy(d->data, s, x); - return 0; -} - -/* read the passed-in file descriptor until EOF, store in a single - datum */ -int set_datum_fd(gnutls_datum_t* d, int fd) { - unsigned int bufsize = 1024; - unsigned int len = 0; - - FILE* f = fdopen(fd, "r"); - if (bufsize > d->size) { - bufsize = 1024; - d->data = gnutls_realloc(d->data, bufsize); - if (d->data == NULL) { - err("out of memory!\n"); - return -1; - } - d->size = bufsize; - } else { - bufsize = d->size; - } - f = fdopen(fd, "r"); - if (NULL == f) { - err("could not fdopen FD %d\n", fd); - } - clearerr(f); - while (!feof(f) && !ferror(f)) { - if (len == bufsize) { - /* allocate more space by doubling: */ - bufsize *= 2; - d->data = gnutls_realloc(d->data, bufsize); - if (d->data == NULL) { - err("out of memory!\n"); - return -1; - }; - d->size = bufsize; - } - len += fread(d->data + len, 1, bufsize - len, f); - /* err("read %d bytes\n", len); */ - } - if (ferror(f)) { - err("Error reading from fd %d (error: %d) (error: %d '%s')\n", fd, ferror(f), errno, strerror(errno)); - return -1; - } - - /* touch up buffer size to match reality: */ - d->data = gnutls_realloc(d->data, len); - d->size = len; - return 0; -} - -/* read the file indicated (by name) in the fname parameter. store - its entire contents in a single datum. */ -int set_datum_file(gnutls_datum_t* d, const char* fname) { - struct stat sbuf; - unsigned char* c = NULL; - FILE* file = NULL; - size_t x = 0; - - if (0 != stat(fname, &sbuf)) { - err("failed to stat '%s'\n", fname); - return -1; - } - - c = gnutls_realloc(d->data, sbuf.st_size); - if (NULL == c) { - err("failed to allocate %d bytes for '%s'\n", sbuf.st_size, fname); - return -1; - } - - d->data = c; - d->size = sbuf.st_size; - file = fopen(fname, "r"); - if (NULL == file) { - err("failed to open '%s' for reading\n", fname); - return -1; - } - - x = fread(d->data, d->size, 1, file); - if (x != 1) { - err("tried to read %d bytes, read %d instead from '%s'\n", d->size, x, fname); - fclose(file); - return -1; - } - fclose(file); - return 0; -} - -int write_datum_fd(int fd, const gnutls_datum_t* d) { - if (d->size != write(fd, d->data, d->size)) { - err("failed to write body of datum.\n"); - return -1; - } - return 0; -} - - -int write_datum_fd_with_length(int fd, const gnutls_datum_t* d) { - uint32_t len; - int looks_negative = (d->data[0] & 0x80); - unsigned char zero = 0; - - /* if the first bit is 1, then the datum will appear negative in the - MPI encoding style used by OpenSSH. In that case, we'll increase - the length by one, and dump out one more byte */ - - if (looks_negative) { - len = htonl(d->size + 1); - } else { - len = htonl(d->size); - } - if (write(fd, &len, sizeof(len)) != sizeof(len)) { - err("failed to write size of datum.\n"); - return -2; - } - if (looks_negative) { - if (write(fd, &zero, 1) != 1) { - err("failed to write padding byte for MPI.\n"); - return -2; - } - } - return write_datum_fd(fd, d); -} - -int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num) { - unsigned int i; - int ret; - - for (i = 0; i < num; i++) - if (ret = write_datum_fd_with_length(fd, d[i]), ret != 0) - return ret; - - return 0; -} - - -int datum_from_string(gnutls_datum_t* d, const char* str) { - d->size = strlen(str); - d->data = gnutls_realloc(d->data, d->size); - if (d->data == 0) - return ENOMEM; - memcpy(d->data, str, d->size); - return 0; -} - - -int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]) { - int p[2]; - int ret; - - if (pid == NULL) { - err("bad pointer passed to create_writing_pipe()\n"); - return -1; - } - - if (ret = pipe(p), ret == -1) { - err("failed to create a pipe (error: %d \"%s\")\n", errno, strerror(errno)); - return -1; - } - - *pid = fork(); - if (*pid == -1) { - err("Failed to fork (error: %d \"%s\")\n", errno, strerror(errno)); - return -1; - } - if (*pid == 0) { /* this is the child */ - close(p[1]); /* close unused write end */ - - if (0 != dup2(p[0], 0)) { /* map the reading end into stdin */ - err("Failed to transfer reading file descriptor to stdin (error: %d \"%s\")\n", errno, strerror(errno)); - exit(1); - } - execv(path, argv); - err("exec %s failed (error: %d \"%s\")\n", path, errno, strerror(errno)); - /* close the open file descriptors */ - close(p[0]); - close(0); - - exit(1); - } else { /* this is the parent */ - close(p[0]); /* close unused read end */ - return p[1]; - } -} - -int validate_ssh_host_userid(const char* userid) { - char* oldlocale = setlocale(LC_ALL, "C"); - - /* choke if userid does not match the expected format - ("ssh://fully.qualified.domain.name") */ - if (strncmp("ssh://", userid, strlen("ssh://")) != 0) { - err("The user ID should start with ssh:// for a host key\n"); - goto fail; - } - /* so that isalnum will work properly */ - userid += strlen("ssh://"); - while (0 != (*userid)) { - if (!isalnum(*userid)) { - err("label did not start with a letter or a digit! (%s)\n", userid); - goto fail; - } - userid++; - while (isalnum(*userid) || ('-' == (*userid))) - userid++; - if (('.' == (*userid)) || (0 == (*userid))) { /* clean end of label: - check last char - isalnum */ - if (!isalnum(*(userid - 1))) { - err("label did not end with a letter or a digit!\n"); - goto fail; - } - if ('.' == (*userid)) /* advance to the start of the next label */ - userid++; - } else { - err("invalid character in domain name: %c\n", *userid); - goto fail; - } - } - /* ensure that the last character is valid: */ - if (!isalnum(*(userid - 1))) { - err("hostname did not end with a letter or a digit!\n"); - goto fail; - } - /* FIXME: fqdn's can be unicode now, thanks to RFC 3490 -- how do we - make sure that we've got an OK string? */ - - return 0; - - fail: - setlocale(LC_ALL, oldlocale); - return 1; -} - -/* http://tools.ietf.org/html/rfc4880#section-5.5.2 */ -size_t get_openpgp_mpi_size(gnutls_datum_t* d) { - return 2 + d->size; -} - -int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d) { - uint16_t x; - - x = d->size * 8; - x = htons(x); - - write(fd, &x, sizeof(x)); - write(fd, d->data, d->size); - - return 0; -} diff --git a/gpg2ssh/gnutls-helpers.h b/gpg2ssh/gnutls-helpers.h deleted file mode 100644 index 9ea22a3..0000000 --- a/gpg2ssh/gnutls-helpers.h +++ /dev/null @@ -1,72 +0,0 @@ -/* Author: Daniel Kahn Gillmor */ -/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ -/* License: GPL v3 or later */ - - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Functions to help dealing with GnuTLS for monkeysphere key - translation projects: */ - -/* set everything up, including logging levels. Return 0 on - success */ -int init_gnutls(); - -/* logging and output functions: */ - -void err(const char* fmt, ...); -void logfunc(int level, const char* string); - -/* basic datum manipulations: */ - -void init_datum(gnutls_datum_t* d); -void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src); -int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b); -void free_datum(gnutls_datum_t* d); -int write_datum_fd(int fd, const gnutls_datum_t* d); -int write_datum_fd_with_length(int fd, const gnutls_datum_t* d); -int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num); - -/* set up a datum from a null-terminated string */ -int datum_from_string(gnutls_datum_t* d, const char* str); - -/* keyid manipulations: */ -typedef unsigned char printable_keyid[16]; - -void init_keyid(gnutls_openpgp_keyid_t keyid); -void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid); - -/* functions to get data into datum objects: */ - -/* read the passed-in string, store in a single datum */ -int set_datum_string(gnutls_datum_t* d, const char* s); - -/* read the passed-in file descriptor until EOF, store in a single - datum */ -int set_datum_fd(gnutls_datum_t* d, int fd); - -/* read the file indicated (by name) in the fname parameter. store - its entire contents in a single datum. */ -int set_datum_file(gnutls_datum_t* d, const char* fname); - -/* set up file descriptor pipe for writing (child process pid gets - stored in pid, fd is returned)*/ -int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]); - -/* return 0 if userid matches the monkeysphere spec for ssh host user IDs */ -int validate_ssh_host_userid(const char* userid); - -/* how many bytes will it take to write out this datum in OpenPGP MPI form? */ -size_t get_openpgp_mpi_size(gnutls_datum_t* d); - -/* write the MPI stored in gnutls_datum_t to file descriptor fd: */ -int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d); diff --git a/gpg2ssh/gpg2ssh.c b/gpg2ssh/gpg2ssh.c deleted file mode 100644 index c99f03f..0000000 --- a/gpg2ssh/gpg2ssh.c +++ /dev/null @@ -1,293 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* for waitpid() */ -#include -#include - -/* - Author: Daniel Kahn Gillmor - Date: Tue, 08 Apr 2008 - License: GPL v3 or later - - monkeysphere public key translator: execute this with an GPG - certificate (public key(s) + userid(s)) on stdin. It currently - only works with RSA keys. - - It will spit out a version of the first key capable of being used - for authentication on stdout. The output format should be suitable - for appending a known_hosts file. - - Requirements: I've only built this so far with GnuTLS v2.3.4 -- - version 2.2.0 does not contain the appropriate pieces. - - */ - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_openpgp_crt_t openpgp_crt; - gnutls_openpgp_keyid_t keyid; - printable_keyid p_keyid; - unsigned int keyidx; - unsigned int usage, bits; - gnutls_pk_algorithm_t algo; - - gnutls_datum_t m, e, p, q, g, y; - gnutls_datum_t algolabel; - - char output_data[10240]; - char userid[10240]; - size_t uidsz = sizeof(userid); - - const gnutls_datum_t* all[5]; - int pipefd; - pid_t child_pid; - char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; - const char* algoname; - int mpicount; - int pipestatus; - - init_gnutls(); - - init_datum(&data); - - init_datum(&m); - init_datum(&e); - init_datum(&p); - init_datum(&q); - init_datum(&g); - init_datum(&y); - - init_datum(&algolabel); - - init_keyid(keyid); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - if (ret = gnutls_openpgp_crt_init(&openpgp_crt), ret) { - err("Failed to initialize OpenPGP certificate (error: %d)\n", ret); - return 1; - } - - /* format could be either: GNUTLS_OPENPGP_FMT_RAW, - GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, - otherwise, use BASE64: */ - - /* FIXME: we should be auto-detecting the input format, and - translating it as needed. */ - - if (getenv("MONKEYSPHERE_RAW")) { - err("assuming RAW formatted certificate\n"); - if (ret = gnutls_openpgp_crt_import(openpgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW), ret) { - err("failed to import the OpenPGP certificate in RAW format (error: %d)\n", ret); - return ret; - } - } else { - err("assuming BASE64 formatted certificate\n"); - if (ret = gnutls_openpgp_crt_import (openpgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64), ret) { - err("failed to import the OpenPGP certificate in BASE64 format (error: %d)\n", ret); - return ret; - } - } - - if (gnutls_openpgp_crt_get_revoked_status(openpgp_crt)) { - err("the primary key was revoked!\n"); - return 1; - } - - /* FIXME: We're currently looking at the primary key or maybe the - first authentication-capable subkey. - - Instead, we should be iterating through the primary key and all - subkeys: for each one with the authentication usage flag set of a - algorithm we can handle, we should output matching UserIDs and - the SSH version of the key. */ - - - if (ret = gnutls_openpgp_crt_get_key_usage(openpgp_crt, &usage), ret) { - err("failed to get the usage flags for the primary key (error: %d)\n", ret); - return ret; - } - if (usage & GNUTLS_KEY_KEY_AGREEMENT && - usage & GNUTLS_KEY_KEY_ENCIPHERMENT) { - err("the primary key can be used for authentication and communication encryption!\n"); - - algo = gnutls_openpgp_crt_get_pk_algorithm(openpgp_crt, &bits); - if (algo < 0) { - err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - - err("OpenPGP RSA certificate, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_pk_rsa_raw(openpgp_crt, &m, &e); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA Key, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_pk_dsa_raw(openpgp_crt, &p, &q, &g, &y); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - - } else { - err("primary key is not good for authentication and communication encryption. Trying subkeys...\n"); - - if (ret = gnutls_openpgp_crt_get_auth_subkey(openpgp_crt, keyid, 0), ret) { - err("failed to find a subkey capable of authentication and communication encryption (error: %d)\n", ret); - return ret; - } - make_keyid_printable(p_keyid, keyid); - err("found authentication subkey %.16s\n", p_keyid); - - ret = gnutls_openpgp_crt_get_subkey_idx(openpgp_crt, keyid); - if (ret < 0) { - err("could not get the index of subkey %.16s (error: %d)\n", ret); - return ret; - } - keyidx = ret; - - if (gnutls_openpgp_crt_get_subkey_revoked_status(openpgp_crt, keyidx)) { - err("The authentication subkey was revoked!\n"); - return 1; - } - - if (ret = gnutls_openpgp_crt_get_subkey_usage(openpgp_crt, keyidx, &usage), ret) { - err("could not figure out usage of subkey %.16s (error: %d)\n", p_keyid, ret); - return ret; - } - if ((usage & GNUTLS_KEY_KEY_AGREEMENT) == 0 && - usage & GNUTLS_KEY_KEY_ENCIPHERMENT) { - err("could not find a subkey with authentication and communication encryption.\n"); - return 1; - } - - /* switch, based on the algorithm in question, to extract the MPI - components: */ - - algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(openpgp_crt, keyidx, &bits); - if (algo < 0) { - err("failed to get the algorithm of the authentication subkey (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - - err("OpenPGP RSA subkey, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(openpgp_crt, keyidx, &m, &e); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA subkey parameters (error: %d)\n", ret); - return 1; - } - } else if (algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA subkey, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(openpgp_crt, keyidx, &p, &q, &g, &y); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA subkey parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP subkey was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - } - - /* make sure userid is NULL-terminated */ - userid[sizeof(userid) - 1] = 0; - uidsz--; - - /* FIXME: we're just choosing the first UserID from the certificate: - instead, we should be selecting every User ID that is adequately - signed and matches the spec, and aggregating them with commas for - known_hosts output */ - - if (ret = gnutls_openpgp_crt_get_name(openpgp_crt, 0, userid, &uidsz), ret) { - err("Failed to fetch the first UserID (error: %d)\n", ret); - return ret; - } - - if (ret = validate_ssh_host_userid(userid), ret) { - err("bad userid: not a valid ssh host.\n"); - return ret; - } - - /* remove ssh:// from the beginning of userid */ - memmove(userid, userid + strlen("ssh://"), 1 + strlen(userid) - strlen("ssh://")); - - - /* now we have algo, and the various MPI data are set. Can we - export them cleanly? */ - - /* for the moment, we'll just dump the info raw, and pipe it - externally through coreutils' /usr/bin/base64 */ - - if (algo == GNUTLS_PK_RSA) { - algoname = "ssh-rsa"; - mpicount = 3; - - all[0] = &algolabel; - all[1] = &e; - all[2] = &m; - } else if (algo == GNUTLS_PK_DSA) { - algoname = "ssh-dss"; - mpicount = 5; - - all[0] = &algolabel; - all[1] = &p; - all[2] = &q; - all[3] = &g; - all[4] = &y; - } else { - err("no idea what this algorithm is: %d\n", algo); - return 1; - } - - if (ret = datum_from_string(&algolabel, algoname), ret) { - err("couldn't label string (error: %d)\n", ret); - return ret; - } - - snprintf(output_data, sizeof(output_data), "%s %s ", userid, algoname); - - pipefd = create_writing_pipe(&child_pid, args[0], args); - if (pipefd < 0) { - err("failed to create a writing pipe (returned %d)\n", pipefd); - return pipefd; - } - - write(1, output_data, strlen(output_data)); - - if (0 != write_data_fd_with_length(pipefd, all, mpicount)) { - err("was not able to write out RSA key data\n"); - return 1; - } - close(pipefd); - if (child_pid != waitpid(child_pid, &pipestatus, 0)) { - err("could not wait for child process to return for some reason.\n"); - return 1; - } - if (pipestatus != 0) { - err("base64 pipe died with return code %d\n", pipestatus); - return pipestatus; - } - - write(1, "\n", 1); - - - - gnutls_openpgp_crt_deinit(openpgp_crt); - gnutls_global_deinit(); - return 0; -} diff --git a/gpg2ssh/main.c b/gpg2ssh/main.c deleted file mode 100644 index d6bac68..0000000 --- a/gpg2ssh/main.c +++ /dev/null @@ -1,271 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* - Author: Daniel Kahn Gillmor - Date: Tue, 01 Apr 2008 - License: GPL v3 or later - - monkeysphere private key translator: execute this with an GPG - secret key on stdin (at the moment, only passphraseless RSA keys - work). - - It will spit out a PEM-encoded version of the key on stdout, which - can be fed into ssh-add like this: - - gpg --export-secret-keys $KEYID | monkeysphere | ssh-add -c /dev/stdin - - Requirements: I've only built this so far with GnuTLS v2.3.4 -- - version 2.2.0 does not contain the appropriate pieces. - - Notes: gpgkey2ssh doesn't seem to provide the same public - keys. Mighty weird! - -0 wt215@squeak:~/monkeysphere$ gpg --export-secret-keys 1DCDF89F | ~dkg/src/monkeysphere/monkeysphere | ssh-add -c /dev/stdin -gnutls version: 2.3.4 -OpenPGP RSA Key, with 1024 bits -Identity added: /dev/stdin (/dev/stdin) -The user has to confirm each use of the key -0 wt215@squeak:~/monkeysphere$ ssh-add -L -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9gWQqfrnhQKDQnND/3eOexpddE64J+1zp9fcyCje7H5LKclb6DBV2HS6WgW32PJhIzvP+fYZM3dzXea3fpv14y1SicXiRBDgF9SnsNA1qWn2RyzkLcKy7PmM0PDYtU1oiLTcQj/xkWcqW2sLKHT/WW+vZP5XP7RMGN/yWNMfE2Q== /dev/stdin -0 wt215@squeak:~/monkeysphere$ gpgkey2ssh 1DCDF89F -ssh-rsa AAAAB3NzaC1yc2EAAACBAL2BZCp+ueFAoNCc0P/d457Gl10Trgn7XOn19zIKN7sfkspyVvoMFXYdLpaBbfY8mEjO8/59hkzd3Nd5rd+m/XjLVKJxeJEEOAX1Kew0DWpafZHLOQtwrLs+YzQ8Ni1TWiItNxCP/GRZypbawsodP9Zb69k/lc/tEwY3/JY0x8TZAAAAAwEAAQ== COMMENT -0 wt215@squeak:~/monkeysphere$ - - */ - - -int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) { - gnutls_openpgp_privkey_t pgp_privkey; - gnutls_datum_t m, e, d, p, q, u, g, y, x; - gnutls_pk_algorithm_t pgp_algo; - unsigned int pgp_bits; - int ret; - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - init_datum(&x); - - if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) { - err("Failed to initialized OpenPGP private key (error: %d)\n", ret); - return 1; - } - - - /* format could be either: GNUTLS_OPENPGP_FMT_RAW, - GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, - otherwise, use BASE64: */ - - if (getenv("MONKEYSPHERE_RAW")) { - err("assuming RAW formatted private keys\n"); - if (ret = gnutls_openpgp_privkey_import(pgp_privkey, input, GNUTLS_OPENPGP_FMT_RAW, NULL, 0), ret) - err("failed to import the OpenPGP private key in RAW format (error: %d)\n", ret); - } else { - err("assuming BASE64 formatted private keys\n"); - if (ret = gnutls_openpgp_privkey_import (pgp_privkey, input, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0), ret) - err("failed to import the OpenPGP private key in BASE64 format (error: %d)\n", ret); - } - - pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(pgp_privkey, &pgp_bits); - if (pgp_algo < 0) { - err("failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo); - return 1; - } - if (pgp_algo == GNUTLS_PK_RSA) { - err("OpenPGP RSA Key, with %d bits\n", pgp_bits); - ret = gnutls_openpgp_privkey_export_rsa_raw(pgp_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - - ret = gnutls_x509_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (pgp_algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA Key, with %d bits\n", pgp_bits); - ret = gnutls_openpgp_privkey_export_dsa_raw(pgp_privkey, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - - ret = gnutls_x509_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", pgp_algo); - return 1; - } - - ret = gnutls_x509_privkey_fix(*output); - if (ret != 0) { - err("failed to fix up the private key in X.509 format (error: %d)\n", ret); - return 1; - } - - gnutls_openpgp_privkey_deinit(pgp_privkey); - return 0; -} - -int convert_x509_to_pgp(gnutls_openpgp_privkey_t* output, gnutls_datum_t* input) { - gnutls_x509_privkey_t x509_privkey; - gnutls_datum_t m, e, d, p, q, u, g, y, x; - gnutls_pk_algorithm_t x509_algo; - int ret; - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - init_datum(&x); - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialized X.509 private key (error: %d)\n", ret); - return 1; - } - - - /* format could be either: GNUTLS_X509_FMT_DER, - GNUTLS_X509_FMT_PEM; if MONKEYSPHERE_DER is set, use DER, - otherwise, use PEM: */ - - if (getenv("MONKEYSPHERE_DER")) { - err("assuming DER formatted private keys\n"); - if (ret = gnutls_x509_privkey_import(x509_privkey, input, GNUTLS_X509_FMT_DER), ret) - err("failed to import the X.509 private key in DER format (error: %d)\n", ret); - } else { - err("assuming PEM formatted private keys\n"); - if (ret = gnutls_x509_privkey_import (x509_privkey, input, GNUTLS_X509_FMT_PEM), ret) - err("failed to import the X.509 private key in PEM format (error: %d)\n", ret); - } - - x509_algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); - if (x509_algo < 0) { - err("failed to get X.509 key algorithm (error: %d)\n", x509_algo); - return 1; - } - if (x509_algo == GNUTLS_PK_RSA) { - err("X.509 RSA Key\n"); - ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - - /* ret = gnutls_openpgp_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); */ - ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (x509_algo == GNUTLS_PK_DSA) { - err("X.509 DSA Key\n"); - ret = gnutls_x509_privkey_export_dsa_raw(x509_privkey, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - - /* ret = gnutls_openpgp_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); */ - ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", x509_algo); - return 1; - } - - gnutls_x509_privkey_deinit(x509_privkey); - return 0; -} - - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_x509_privkey_t x509_privkey; - - char output_data[10240]; - size_t ods = sizeof(output_data); - - init_gnutls(); - - init_datum(&data); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - - /* Or, instead, read in key from a file name: - if (ret = set_datum_file(&data, argv[1]), ret) { - err("didn't read file '%s'\n", argv[1]); - return 1; - } -*/ - - /* treat the passed file as an X.509 private key, and extract its - component values: */ - -/* if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { */ -/* err("Failed to import the X.509 key (error: %d)\n", ret); */ -/* return 1; */ -/* } */ -/* gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); */ - - /* try to print the PEM-encoded private key: */ -/* ret = gnutls_x509_privkey_export (x509_privkey, */ -/* GNUTLS_X509_FMT_PEM, */ -/* output_data, */ -/* &ods); */ -/* printf("ret: %u; ods: %u;\n", ret, ods); */ -/* if (ret == 0) { */ -/* write(0, output_data, ods); */ -/* } */ - - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialize X.509 private key (error: %d)\n", ret); - return 1; - } - - if (ret = convert_pgp_to_x509(&x509_privkey, &data), ret) { - return ret; - } - - ret = gnutls_x509_privkey_export (x509_privkey, - GNUTLS_X509_FMT_PEM, - output_data, - &ods); - printf("ret: %u; ods: %u;\n", ret, ods); - if (ret == 0) { - write(1, output_data, ods); - } - - - gnutls_x509_privkey_deinit(x509_privkey); - gnutls_global_deinit(); - return 0; -} diff --git a/gpg2ssh/ssh2gpg.c b/gpg2ssh/ssh2gpg.c deleted file mode 100644 index b14a540..0000000 --- a/gpg2ssh/ssh2gpg.c +++ /dev/null @@ -1,171 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* for waitpid() */ -#include -#include - -/* for time() */ -#include - -/* for htons() */ -#include - - -/* - Author: Daniel Kahn Gillmor - Date: Sun, 2008-04-20 - License: GPL v3 or later - - monkeysphere public key translator: execute this with an ssh - private key on stdin. It currently only works with RSA keys. - - it should eventually work with OpenSSH-style public keys instead of - the full private key, but it was easier to do this way. - - It shoud spit out a version of the public key suitable for acting - as an OpenPGP public sub key packet. - - */ - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_x509_privkey_t x509_privkey; - gnutls_openpgp_crt_t openpgp_crt; - gnutls_openpgp_keyid_t keyid; - printable_keyid p_keyid; - unsigned int keyidx; - unsigned int usage, bits; - gnutls_pk_algorithm_t algo; - - unsigned char packettag; - unsigned char openpgpversion; - time_t timestamp; - uint32_t clunkytime; - unsigned char openpgpalgo; - unsigned int packetlen; - uint16_t plen; - - gnutls_datum_t m, e, d, p, q, u, g, y; - gnutls_datum_t algolabel; - - char output_data[10240]; - char userid[10240]; - size_t uidsz = sizeof(userid); - - const gnutls_datum_t* all[5]; - int pipefd; - pid_t child_pid; - char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; - const char* algoname; - int mpicount; - int pipestatus; - - init_gnutls(); - - init_datum(&data); - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - - init_datum(&algolabel); - - init_keyid(keyid); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialize private key structure (error: %d)\n", ret); - return 1; - } - - err("assuming PEM formatted private key\n"); - if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { - err("failed to import the PEM-encoded private key (error: %d)\n", ret); - return ret; - } - - algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); - if (algo < 0) { - err("failed to get the algorithm of the PEM-encoded public key (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - err("RSA private key\n"); - ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - err("Modulus size %d, exponent size %d\n", m.size, e.size); - } else if (algo == GNUTLS_PK_DSA) { - err("DSA Key, not implemented!!\n", bits); - return 1; - } else { - err("Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - - /* now we have algo, and the various MPI data are set. Can we - export them as a public subkey packet? */ - - /* this packet should be tagged 14, and should contain: - - 1 octet: version (4) - 4 octets: time of generation (seconds since 1970) - 1 octet: algo (http://tools.ietf.org/html/rfc4880#section-5.5.2 implies 1 for RSA) - - MPI: modulus - MPI: exponent - */ - - packetlen = 1 + 4 + 1; - /* FIXME: this is RSA only. for DSA, there'll be more: */ - packetlen += get_openpgp_mpi_size(&m) + get_openpgp_mpi_size(&e); - - /* FIXME: we should generate this bound more cleanly -- i just - happen to know that 65535 is 2^16-1: */ - if (packetlen > 65535) { - err("packet length is too long (%d)\n", packetlen); - return 1; - } - - /* we're going to emit an old-style packet, with tag 14 (public - subkey), with a two-octet packet length */ - packettag = 0x80 | (14 << 2) | 1; - - write(1, &packettag, sizeof(packettag)); - plen = htons(packetlen); - write(1, &plen, sizeof(plen)); - - openpgpversion = 4; - write(1, &openpgpversion, 1); - - timestamp = time(NULL); - clunkytime = htonl(timestamp); - write(1, &clunkytime, 4); - - /* FIXME: handle things other than RSA */ - openpgpalgo = 1; - write(1, &openpgpalgo, 1); - - write_openpgp_mpi_to_fd(1, &m); - write_openpgp_mpi_to_fd(1, &e); - - gnutls_x509_privkey_deinit(x509_privkey); - gnutls_global_deinit(); - return 0; -} diff --git a/howler/howler b/howler/howler deleted file mode 100755 index 0b67c02..0000000 --- a/howler/howler +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/sh - -# howler: monkeysphere server gpg generator/publisher/maintainer -# -# Written by -# Jameson Rollins -# -# Copyright 2008, released under the GPL, version 3 or later - -PGRM=$(basename $0) - -######################################################################## -# FUNCTIONS -######################################################################## - -usage() { -cat <&2 - exit ${2:-'1'} -} - -# generate server gpg key -gen_key() { - KEY_TYPE=${KEY_TYPE:-RSA} - KEY_LENGTH=${KEY_LENGTH:-2048} - KEY_USAGE=${KEY_USAGE:-encrypt,auth} - SERVICE=${SERVICE:-ssh} - HOSTNAME_FQDN=${HOSTNAME_FQDN:-$(hostname -f)} - - USERID=${USERID:-"$SERVICE"://"$HOSTNAME_FQDN"} - - echo "key parameters:" - cat < /dev/null 2>&1 ; then - failure "key for '$USERID' already exists" - fi - - echo "generating server key..." - gpg --batch --gen-key < /dev/null | grep '^pub:' | cut -d: -f5) - - # dummy command so as not to publish fakes keys during testing - # eventually: - #gpg --send-keys --keyserver "$KEYSERVER" "$keyID" - echo "gpg --send-keys --keyserver $KEYSERVER $keyID" -} - -trust_key() { - for keyID ; do - # get the key from the key server - gpg --keyserver "$KEYSERVER" --recv-key "$keyID" || failure "could not retrieve key '$keyID'" - - # edit the key to change trust - # FIXME: need to figure out how to automate this, - # in a batch mode or something. - gpg --edit-key "$keyID" - done -} - -######################################################################## -# MAIN -######################################################################## - -# set ms home directory -MS_HOME=${MS_HOME:-/etc/monkeysphere} - -# load configuration file -MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} -[ -e "$MS_CONF" ] && . "$MS_CONF" - -GNUPGHOME=${GNUPGHOME:-"$MS_HOME"/gnupg} -export GNUPGHOME -KEYSERVER=${KEYSERVER:-subkeys.pgp.net} -export KEYSERVER - -COMMAND="$1" -[ "$COMMAND" ] || failure "Type '$PGRM help' for usage." -shift 1 - -case $COMMAND in - 'gen-key') - gen_key - ;; - 'publish-key') - publish_key - ;; - 'trust-key') - if [ -z "$1" ] ; then - failure "you must specify at least one key to trust." - fi - trust_key "$@" - ;; - 'help') - usage - exit - ;; - *) - failure "Unknown command: '$COMMAND' -Type '$PGRM help' for usage." - ;; -esac diff --git a/langur/README b/langur/README deleted file mode 100644 index ee60701..0000000 --- a/langur/README +++ /dev/null @@ -1,4 +0,0 @@ -Langur is the policy editor/viewer for the monkeysphere. - -Its goals are to provide a human-friendly interface to the simple and -intelligible policies monkeysphere supports. diff --git a/monkeysphere-server.conf b/monkeysphere-server.conf new file mode 100644 index 0000000..bed5c09 --- /dev/null +++ b/monkeysphere-server.conf @@ -0,0 +1,23 @@ +# MonkeySphere server configuration file. + +# GPG home directory for server +#GNUPGHOME=/etc/monkeysphere/gnupg + +# GPG keyserver to search for keys +#KEYSERVER=subkeys.pgp.net + +# Required key capabilities +# Must be quoted, lowercase, space-seperated list of the following: +# e = encrypt +# s = sign +# c = certify +# a = authentication +#REQUIRED_KEY_CAPABILITY="e a" + +# Whether to add user controlled authorized_keys file to +# monkeysphere-generated authorized_keys file. Should be path to file +# where '%h' will be substituted for the user's home directory. +#USER_CONTROLLED_AUTHORIZED_KEYS=%h/.ssh/authorized_keys + +# where to cache user authorized_keys lines +#STAGING_AREA=/var/lib/monkeysphere/stage diff --git a/monkeysphere.conf b/monkeysphere.conf index 6401203..385165a 100644 --- a/monkeysphere.conf +++ b/monkeysphere.conf @@ -1,30 +1,31 @@ -# monkeysphere system configuration file +# MonkeySphere system-wide client configuration file. -# This is particular configuration is meant to be sourced by the -# rhesus shell script when run in administrative mode to maintain -# authorized_keys files for users. +# authorized_user_ids file +#AUTHORIZED_USER_IDS=~/.config/monkeysphere/authorized_user_ids -AUTHORIZED_USER_IDS=/etc/monkeysphere/authorized_user_ids/"$USER" +# GPG home directory +#GNUPGHOME=~/.gnupg -STAGING_AREA=/var/lib/monkeysphere/stage/"$USER" +# GPG keyserver to search for keys +#KEYSERVER=subkeys.pgp.net -# gpg home directory for server -GNUPGHOME=/etc/monkeysphere/gnupg - -# gpg keyserver to search for keys -KEYSERVER=subkeys.pgp.net - -# required capabilities of keys -# must be quoted, lowercase, space-seperated list of the following: +# Required key capabilities +# Must be quoted, lowercase, space-seperated list of the following: # e = encrypt # s = sign # c = certify # a = authentication -REQUIRED_KEY_CAPABILITY="e a" +#REQUIRED_KEY_CAPABILITY="e a" # Path to user-controlled authorized_keys file to add to # Monkeysphere-generated authorized_keys file. If empty, then no -# user-controlled file will be added. To specify the user's home -# directory, use the string "~${USER}" -USER_CONTROLLED_AUTHORIZED_KEYS="~${USER}/.ssh/authorized_keys" +# user-controlled file will be added. +#USER_CONTROLLED_AUTHORIZED_KEYS=~/.ssh/authorized_keys + +# User known_hosts file +#USER_KNOWN_HOSTS=~/.ssh/known_hosts + +# Whether or not to hash the generated known_hosts lines +# (empty mean "no"). +#HASH_KNOWN_HOSTS= diff --git a/rhesus/README b/rhesus/README deleted file mode 100644 index 4d383d5..0000000 --- a/rhesus/README +++ /dev/null @@ -1,30 +0,0 @@ -rhesus is the monkeysphere authorized_keys/known_hosts generator. - -In authorized_keys mode, rhesus takes an auth_user_ids file, which -contains gpg user ids, uses gpg to fetch the keys of the specified -users, does a monkeysphere policy check on each id, and uses gpg2ssh -to generate authorized_keys lines for each verified id. The lines are -then combined with a user's traditional authorized_keys file to create -a new authorized_keys file. - -In known_hosts mode, rhesus takes an auth_host_ids file, which -contains gpg user ids of the form ssh://URL, uses gpg to fetch the -keys of the specified hosts, does a monkeysphere policy check on each -id, and uses gpg2ssh to generate a known_hosts lines for each verified -id. The lines are then combined with a user's traditional known_hosts -file to create a new known_hosts file. - -When run as a normal user, no special configuration is needed. - -When run as an administrator to update system-maintained -authorized_keys files for each user, the following environment -variables should be defined first: - - MS_CONF=/etc/monkeysphere/monkeysphere.conf - USER=foo - -For example, the command might be run like this: - - for USER in $(ls -1 /home) ; do - MS_CONF=/etc/monkeysphere/monkeysphere.conf rhesus --authorized_keys - done diff --git a/rhesus/rhesus b/rhesus/rhesus deleted file mode 100755 index f607f0b..0000000 --- a/rhesus/rhesus +++ /dev/null @@ -1,466 +0,0 @@ -#!/bin/sh - -# rhesus: monkeysphere authorized_keys/known_hosts generating script -# -# Written by -# Jameson Rollins -# -# Copyright 2008, released under the GPL, version 3 or later - -# all caps variables are meant to be user supplied (ie. from config -# file) and are considered global - -PGRM=$(basename $0) - -# date in UTF format if needed -DATE=$(date -u '+%FT%T') - -# unset some environment variables that could screw things up -GREP_OPTIONS= - -######################################################################## -# FUNCTIONS -######################################################################## - -usage() { -cat <&2 - exit ${2:-'1'} -} - -# write output to stdout -log() { - echo -n "ms: " - echo "$@" -} - -# write output to stderr -loge() { - echo -n "ms: " 1>&2 - echo "$@" 1>&2 -} - -# cut out all comments(#) and blank lines from standard input -meat() { - grep -v -e "^[[:space:]]*#" -e '^$' -} - -# cut a specified line from standard input -cutline() { - head --line="$1" | tail -1 -} - -# retrieve all keys with given user id from keyserver -# FIXME: need to figure out how to retrieve all matching keys -# (not just first 5) -gpg_fetch_keys() { - local id - id="$1" - echo 1,2,3,4,5 | \ - gpg --quiet --batch --command-fd 0 --with-colons \ - --keyserver "$KEYSERVER" \ - --search ="$id" >/dev/null 2>&1 -} - -# check that characters are in a string (in an AND fashion). -# used for checking key capability -# check_capability capability a [b...] -check_capability() { - local capability - local capcheck - - capability="$1" - shift 1 - - for capcheck ; do - if echo "$capability" | grep -q -v "$capcheck" ; then - return 1 - fi - done - return 0 -} - -# convert escaped characters from gpg output back into original -# character -# FIXME: undo all escape character translation in with-colons gpg output -unescape() { - echo "$1" | sed 's/\\x3a/:/' -} - -# stand in until we get dkg's gpg2ssh program -gpg2ssh_tmp() { - local mode - local keyID - local userID - local host - - mode="$1" - keyID="$2" - userID="$3" - - if [ "$mode" = 'authorized_keys' ] ; then - gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/" - - # NOTE: it seems that ssh-keygen -R removes all comment fields from - # all lines in the known_hosts file. why? - # NOTE: just in case, the COMMENT can be matched with the - # following regexp: - # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' - elif [ "$mode" = 'known_hosts' ] ; then - host=$(echo "$userID" | sed -e "s|ssh://||") - echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/" - fi -} - -# userid and key policy checking -# the following checks policy on the returned keys -# - checks that full key has appropriate valididy (u|f) -# - checks key has specified capability (REQUIRED_KEY_CAPABILITY) -# - checks that particular desired user id has appropriate validity -# see /usr/share/doc/gnupg/DETAILS.gz -# expects global variable: "mode" -process_user_id() { - local userID - local cacheDir - local requiredPubCapability - local gpgOut - local line - local type - local validity - local keyid - local uidfpr - local capability - local keyOK - local pubKeyID - local uidOK - local keyIDs - local userIDHash - local keyID - - userID="$1" - cacheDir="$2" - - requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]") - - # fetch keys from keyserver, return 1 if none found - gpg_fetch_keys "$userID" || return 1 - - # output gpg info for (exact) userid and store - gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \ - ="$userID" 2> /dev/null) - - # return 1 if there only "tru" lines are output from gpg - if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then - return 1 - fi - - # loop over all lines in the gpg output and process. - # need to do it this way (as opposed to "while read...") so that - # variables set in loop will be visible outside of loop - for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do - - # read the contents of the line - type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1) - validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2) - keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5) - uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10) - capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12) - - # process based on record type - case $type in - 'pub') # primary keys - # new key, wipe the slate - keyOK= - pubKeyID= - uidOK= - keyIDs= - - pubKeyID="$keyid" - - # check primary key validity - if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then - loge " unacceptable primary key validity ($validity)." - continue - fi - # check capability is not Disabled... - if check_capability "$capability" 'D' ; then - loge " key disabled." - continue - fi - # check overall key capability - # must be Encryption and Authentication - if ! check_capability "$capability" $requiredPubCapability ; then - loge " unacceptable primary key capability ($capability)." - continue - fi - - # mark if primary key is acceptable - keyOK=true - - # add primary key ID to key list if it has required capability - if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then - keyIDs[${#keyIDs[*]}]="$keyid" - fi - ;; - 'uid') # user ids - # check key ok and we have key fingerprint - if [ -z "$keyOK" ] ; then - continue - fi - # check key validity - if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then - continue - fi - # check the uid matches - if [ "$(unescape "$uidfpr")" != "$userID" ] ; then - continue - fi - - # mark if uid acceptable - uidOK=true - ;; - 'sub') # sub keys - # add sub key ID to key list if it has required capability - if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then - keyIDs[${#keyIDs[*]}]="$keyid" - fi - ;; - esac - done - - # hash userid for cache file name - userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }') - - # touch/clear key cache file - # (will be left empty if there are noacceptable keys) - > "$cacheDir"/"$userIDHash"."$pubKeyID" - - # for each acceptable key, write an ssh key line to the - # key cache file - if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then - for keyID in ${keyIDs[@]} ; do - # export the key with gpg2ssh - # FIXME: needs to apply extra options for authorized_keys - # lines if specified - gpg2ssh_tmp "$mode" "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID" - - # hash the cache file if specified - if [ "$mode" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then - ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1 - rm "$cacheDir"/"$userIDHash"."$pubKeyID".old - fi - done - fi - - # echo the path to the key cache file - echo "$cacheDir"/"$userIDHash"."$pubKeyID" -} - -# process a host for addition to a known_host file -process_host() { - local host - local cacheDir - local hostKeyCachePath - - host="$1" - cacheDir="$2" - - log "processing host: '$host'" - - hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir") - if [ $? = 0 ] ; then - ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS" - cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS" - fi -} - -# process known_hosts file -# go through line-by-line, extract each host, and process with the -# host processing function -process_known_hosts() { - local cacheDir - local userID - - cacheDir="$1" - - # take all the hosts from the known_hosts file (first field), - # grep out all the hashed hosts (lines starting with '|') - cut -d ' ' -f 1 "$USER_KNOWN_HOSTS" | \ - grep -v '^|.*$' | \ - while IFS=, read -r -a hosts ; do - # process each host - for host in ${hosts[*]} ; do - process_host "$host" "$cacheDir" - done - done -} - -# process an authorized_*_ids file -# go through line-by-line, extract each userid, and process -process_authorized_ids() { - local authorizedIDsFile - local cacheDir - local userID - local userKeyCachePath - - authorizedIDsFile="$1" - cacheDir="$2" - - # clean out keys file and remake keys directory - rm -rf "$cacheDir" - mkdir -p "$cacheDir" - - # loop through all user ids in file - # FIXME: needs to handle extra options if necessary - cat "$authorizedIDsFile" | meat | \ - while read -r userID ; do - # process the userid - log "processing userid: '$userID'" - userKeyCachePath=$(process_user_id "$userID" "$cacheDir") - if [ -s "$userKeyCachePath" ] ; then - loge " acceptable key/uid found." - fi - done -} - -######################################################################## -# MAIN -######################################################################## - -if [ -z "$1" ] ; then - usage - exit 1 -fi - -# mode given in first variable -mode="$1" -shift 1 - -# check user -if ! id -u "$USER" > /dev/null 2>&1 ; then - failure "invalid user '$USER'." -fi - -# set user home directory -HOME=$(getent passwd "$USER" | cut -d: -f6) - -# set ms home directory -MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere} - -# load configuration file -MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} -[ -e "$MS_CONF" ] && . "$MS_CONF" - -# set config variable defaults -STAGING_AREA=${STAGING_AREA:-"$MS_HOME"} -AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids} -GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg} -KEYSERVER=${KEYSERVER:-subkeys.pgp.net} -REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"} -USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"$HOME"/.ssh/authorized_keys} -USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts} -HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-} - -# export USER and GNUPGHOME variables, since they are used by gpg -export USER -export GNUPGHOME - -# stagging locations -hostKeysCacheDir="$STAGING_AREA"/host_keys -userKeysCacheDir="$STAGING_AREA"/user_keys -msKnownHosts="$STAGING_AREA"/known_hosts -msAuthorizedKeys="$STAGING_AREA"/authorized_keys - -# make sure gpg home exists with proper permissions -mkdir -p -m 0700 "$GNUPGHOME" - -## KNOWN_HOST MODE -if [ "$mode" = 'known_hosts' -o "$mode" = 'k' ] ; then - mode='known_hosts' - - cacheDir="$hostKeysCacheDir" - - log "user '$USER': monkeysphere known_hosts processing" - - # touch the known_hosts file to make sure it exists - touch "$USER_KNOWN_HOSTS" - - # if hosts are specified on the command line, process just - # those hosts - if [ "$1" ] ; then - for host ; do - process_host "$host" "$cacheDir" - done - - # otherwise, if no hosts are specified, process the user - # known_hosts file - else - if [ ! -s "$USER_KNOWN_HOSTS" ] ; then - failure "known_hosts file '$USER_KNOWN_HOSTS' is empty." - fi - process_known_hosts "$cacheDir" - fi - -## AUTHORIZED_KEYS MODE -elif [ "$mode" = 'authorized_keys' -o "$mode" = 'a' ] ; then - mode='authorized_keys' - - cacheDir="$userKeysCacheDir" - - # check auth ids file - if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then - log "authorized_user_ids file is empty or does not exist." - exit - fi - - log "user '$USER': monkeysphere authorized_keys processing" - - # if userids are specified on the command line, process just - # those userids - if [ "$1" ] ; then - for userID ; do - if ! grep -q "$userID" "$AUTHORIZED_USER_IDS" ; then - log "userid '$userID' not in authorized_user_ids file." - continue - fi - log "processing user id: '$userID'" - process_user_id "$userID" "$cacheDir" > /dev/null - done - - # otherwise, if no userids are specified, process the entire - # authorized_user_ids file - else - process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir" - fi - - # write output key file - log "writing monkeysphere authorized_keys file... " - touch "$msAuthorizedKeys" - if [ "$(ls "$cacheDir")" ] ; then - log -n "adding gpg keys... " - cat "$cacheDir"/* > "$msAuthorizedKeys" - echo "done." - else - log "no gpg keys to add." - fi - if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then - if [ -s "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then - log -n "adding user authorized_keys file... " - cat "$USER_CONTROLLED_AUTHORIZED_KEYS" >> "$msAuthorizedKeys" - echo "done." - fi - fi - log "monkeysphere authorized_keys file generated:" - log "$msAuthorizedKeys" - -else - failure "unknown command '$mode'." -fi diff --git a/src/common b/src/common new file mode 100755 index 0000000..8643080 --- /dev/null +++ b/src/common @@ -0,0 +1,353 @@ +# -*-shell-script-*- + +# Shared bash functions for the monkeysphere +# +# Written by +# Jameson Rollins +# +# Copyright 2008, released under the GPL, version 3 or later + +# all caps variables are meant to be user supplied (ie. from config +# file) and are considered global + +######################################################################## +# managed directories +ETC="/etc/monkeysphere" +export ETC +LIB="/var/lib/monkeysphere" +export LIB +######################################################################## + +failure() { + echo "$1" >&2 + exit ${2:-'1'} +} + +# write output to stdout +log() { + echo -n "ms: " + echo "$@" +} + +# write output to stderr +loge() { + echo -n "ms: " 1>&2 + echo "$@" 1>&2 +} + +# cut out all comments(#) and blank lines from standard input +meat() { + grep -v -e "^[[:space:]]*#" -e '^$' +} + +# cut a specified line from standard input +cutline() { + head --line="$1" | tail -1 +} + +# retrieve all keys with given user id from keyserver +# FIXME: need to figure out how to retrieve all matching keys +# (not just first 5) +gpg_fetch_keys() { + local id + id="$1" + echo 1,2,3,4,5 | \ + gpg --quiet --batch --command-fd 0 --with-colons \ + --keyserver "$KEYSERVER" \ + --search ="$id" >/dev/null 2>&1 +} + +# check that characters are in a string (in an AND fashion). +# used for checking key capability +# check_capability capability a [b...] +check_capability() { + local capability + local capcheck + + capability="$1" + shift 1 + + for capcheck ; do + if echo "$capability" | grep -q -v "$capcheck" ; then + return 1 + fi + done + return 0 +} + +# convert escaped characters from gpg output back into original +# character +# FIXME: undo all escape character translation in with-colons gpg output +unescape() { + echo "$1" | sed 's/\\x3a/:/' +} + +# stand in until we get dkg's gpg2ssh program +gpg2ssh_tmp() { + local keyID + local userID + local host + + keyID="$2" + userID="$3" + + if [ "$mode" = 'authorized_keys' ] ; then + gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/" + + # NOTE: it seems that ssh-keygen -R removes all comment fields from + # all lines in the known_hosts file. why? + # NOTE: just in case, the COMMENT can be matched with the + # following regexp: + # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' + elif [ "$MODE" = 'known_hosts' ] ; then + host=$(echo "$userID" | sed -e "s|ssh://||") + echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/" + fi +} + +# userid and key policy checking +# the following checks policy on the returned keys +# - checks that full key has appropriate valididy (u|f) +# - checks key has specified capability (REQUIRED_KEY_CAPABILITY) +# - checks that particular desired user id has appropriate validity +# see /usr/share/doc/gnupg/DETAILS.gz +# expects global variable: "MODE" +process_user_id() { + local userID + local cacheDir + local requiredPubCapability + local gpgOut + local line + local type + local validity + local keyid + local uidfpr + local capability + local keyOK + local pubKeyID + local uidOK + local keyIDs + local userIDHash + local keyID + + userID="$1" + cacheDir="$2" + + requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]") + + # fetch keys from keyserver, return 1 if none found + gpg_fetch_keys "$userID" || return 1 + + # output gpg info for (exact) userid and store + gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \ + ="$userID" 2> /dev/null) + + # return 1 if there only "tru" lines are output from gpg + if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then + loge " key not found." + return 1 + fi + + # loop over all lines in the gpg output and process. + # need to do it this way (as opposed to "while read...") so that + # variables set in loop will be visible outside of loop + for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do + + # read the contents of the line + type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1) + validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2) + keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5) + uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10) + capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12) + + # process based on record type + case $type in + 'pub') # primary keys + # new key, wipe the slate + keyOK= + pubKeyID= + uidOK= + keyIDs= + + pubKeyID="$keyid" + + # check primary key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + loge " unacceptable primary key validity ($validity)." + continue + fi + # check capability is not Disabled... + if check_capability "$capability" 'D' ; then + loge " key disabled." + continue + fi + # check overall key capability + # must be Encryption and Authentication + if ! check_capability "$capability" $requiredPubCapability ; then + loge " unacceptable primary key capability ($capability)." + continue + fi + + # mark if primary key is acceptable + keyOK=true + + # add primary key ID to key list if it has required capability + if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then + keyIDs[${#keyIDs[*]}]="$keyid" + fi + ;; + 'uid') # user ids + # check key ok and we have key fingerprint + if [ -z "$keyOK" ] ; then + continue + fi + # check key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + # check the uid matches + if [ "$(unescape "$uidfpr")" != "$userID" ] ; then + continue + fi + + # mark if uid acceptable + uidOK=true + ;; + 'sub') # sub keys + # add sub key ID to key list if it has required capability + if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then + keyIDs[${#keyIDs[*]}]="$keyid" + fi + ;; + esac + done + + # hash userid for cache file name + userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }') + + # touch/clear key cache file + # (will be left empty if there are noacceptable keys) + > "$cacheDir"/"$userIDHash"."$pubKeyID" + + # for each acceptable key, write an ssh key line to the + # key cache file + if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then + for keyID in ${keyIDs[@]} ; do + loge " acceptable key/uid found." + + # export the key with gpg2ssh + # FIXME: needs to apply extra options for authorized_keys + # lines if specified + gpg2ssh_tmp "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID" + + # hash the cache file if specified + if [ "$MODE" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then + ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1 + rm "$cacheDir"/"$userIDHash"."$pubKeyID".old + fi + done + fi + + # echo the path to the key cache file + echo "$cacheDir"/"$userIDHash"."$pubKeyID" +} + +# process a host for addition to a known_host file +process_host() { + local host + local cacheDir + local hostKeyCachePath + + host="$1" + cacheDir="$2" + + log "processing host: '$host'" + + hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir") + if [ $? = 0 ] ; then + ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS" + cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS" + fi +} + +# process known_hosts file +# go through line-by-line, extract each host, and process with the +# host processing function +process_known_hosts() { + local knownHosts + local cacheDir + local hosts + local host + + knownHosts="$1" + cacheDir="$2" + + # take all the hosts from the known_hosts file (first field), + # grep out all the hashed hosts (lines starting with '|') + cut -d ' ' -f 1 "$knownHosts" | \ + grep -v '^|.*$' | \ + while IFS=, read -r -a hosts ; do + # process each host + for host in ${hosts[*]} ; do + process_host "$host" "$cacheDir" + done + done +} + +# process authorized_keys file +# go through line-by-line, extract monkeysphere userids from comment +# fields, and process each userid +process_authorized_keys() { + local authorizedKeys + local cacheDir + local userID + + authorizedKeys="$1" + cacheDir="$2" + + # take all the monkeysphere userids from the authorized_keys file + # comment field (third field) that starts with "MonkeySphere uid:" + # FIXME: needs to handle authorized_keys options (field 0) + cat "$authorizedKeys" | \ + while read -r options keytype key comment ; do + # if the comment field is empty, assume the third field was + # the comment + if [ -z "$comment" ] ; then + comment="$key" + fi + if ! echo "$comment" | grep '^MonkeySphere userID:.*$' ; then + continue + fi + userID=$(echo "$comment" | sed -e "/^MonkeySphere userID://") + if [ -z "$userID" ] ; then + continue + fi + # process the userid + log "processing userid: '$userID'" + process_user_id "$userID" "$cacheDir" > /dev/null + done +} + +# process an authorized_*_ids file +# go through line-by-line, extract each userid, and process +process_authorized_ids() { + local authorizedIDs + local cacheDir + local userID + + authorizedIDs="$1" + cacheDir="$2" + + # clean out keys file and remake keys directory + rm -rf "$cacheDir" + mkdir -p "$cacheDir" + + # loop through all user ids in file + # FIXME: needs to handle authorized_keys options + cat "$authorizedIDs" | meat | \ + while read -r userID ; do + # process the userid + log "processing userid: '$userID'" + process_user_id "$userID" "$cacheDir" > /dev/null + done +} diff --git a/src/gpg2ssh/Makefile b/src/gpg2ssh/Makefile new file mode 100644 index 0000000..a0b7241 --- /dev/null +++ b/src/gpg2ssh/Makefile @@ -0,0 +1,18 @@ +all: monkeysphere gpg2ssh + +monkeysphere: main.c gnutls-helpers.o + gcc -g -Wall --pedantic -o monkeysphere main.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +gpg2ssh: gpg2ssh.c gnutls-helpers.o + gcc -g -Wall --pedantic -o gpg2ssh gpg2ssh.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +ssh2gpg: ssh2gpg.c gnutls-helpers.o + gcc -g -Wall --pedantic -o ssh2gpg ssh2gpg.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +%.o: %.c + gcc -g -Wall --pedantic -o $@ -c $< + +clean: + rm -f monkeysphere gpg2ssh *.o + +.PHONY: clean all diff --git a/src/gpg2ssh/gnutls-helpers.c b/src/gpg2ssh/gnutls-helpers.c new file mode 100644 index 0000000..6eae29e --- /dev/null +++ b/src/gpg2ssh/gnutls-helpers.c @@ -0,0 +1,364 @@ +/* Author: Daniel Kahn Gillmor */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + +#include "gnutls-helpers.h" +/* for htonl() */ +#include + +/* for setlocale() */ +#include + +/* for isalnum() */ +#include + +int loglevel = 0; + + +void err(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fflush(stderr); +} + +void logfunc(int level, const char* string) { + fprintf(stderr, "GnuTLS Logging (%d): %s\n", level, string); +} + +void init_keyid(gnutls_openpgp_keyid_t keyid) { + memset(keyid, 'x', sizeof(gnutls_openpgp_keyid_t)); +} + + + +void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid) +{ + static const char hex[16] = "0123456789ABCDEF"; + unsigned int kix = 0, outix = 0; + + while (kix < sizeof(gnutls_openpgp_keyid_t)) { + out[outix] = hex[(keyid[kix] >> 4) & 0x0f]; + out[outix + 1] = hex[keyid[kix] & 0x0f]; + kix++; + outix += 2; + } +} + + +int init_gnutls() { + const char* version = NULL; + const char* debug_string = NULL; + int ret; + + if (ret = gnutls_global_init(), ret) { + err("Failed to do gnutls_global_init() (error: %d)\n", ret); + return 1; + } + + version = gnutls_check_version(NULL); + + if (version) + err("gnutls version: %s\n", version); + else { + err("no version found!\n"); + return 1; + } + + if (debug_string = getenv("MONKEYSPHERE_DEBUG"), debug_string) { + loglevel = atoi(debug_string); + gnutls_global_set_log_function(logfunc); + + gnutls_global_set_log_level(loglevel); + err("set log level to %d\n", loglevel); + } + return 0; +} + +void init_datum(gnutls_datum_t* d) { + d->data = NULL; + d->size = 0; +} +void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src) { + dest->data = gnutls_realloc(dest->data, src->size); + dest->size = src->size; + memcpy(dest->data, src->data, src->size); +} +int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b) { + if (a->size > b->size) { + err("a is larger\n"); + return 1; + } + if (a->size < b->size) { + err("b is larger\n"); + return -1; + } + return memcmp(a->data, b->data, a->size); +} +void free_datum(gnutls_datum_t* d) { + gnutls_free(d->data); + d->data = NULL; + d->size = 0; +} + +/* read the passed-in string, store in a single datum */ +int set_datum_string(gnutls_datum_t* d, const char* s) { + unsigned int x = strlen(s)+1; + unsigned char* c = NULL; + + c = gnutls_realloc(d->data, x); + if (NULL == c) + return -1; + d->data = c; + d->size = x; + memcpy(d->data, s, x); + return 0; +} + +/* read the passed-in file descriptor until EOF, store in a single + datum */ +int set_datum_fd(gnutls_datum_t* d, int fd) { + unsigned int bufsize = 1024; + unsigned int len = 0; + + FILE* f = fdopen(fd, "r"); + if (bufsize > d->size) { + bufsize = 1024; + d->data = gnutls_realloc(d->data, bufsize); + if (d->data == NULL) { + err("out of memory!\n"); + return -1; + } + d->size = bufsize; + } else { + bufsize = d->size; + } + f = fdopen(fd, "r"); + if (NULL == f) { + err("could not fdopen FD %d\n", fd); + } + clearerr(f); + while (!feof(f) && !ferror(f)) { + if (len == bufsize) { + /* allocate more space by doubling: */ + bufsize *= 2; + d->data = gnutls_realloc(d->data, bufsize); + if (d->data == NULL) { + err("out of memory!\n"); + return -1; + }; + d->size = bufsize; + } + len += fread(d->data + len, 1, bufsize - len, f); + /* err("read %d bytes\n", len); */ + } + if (ferror(f)) { + err("Error reading from fd %d (error: %d) (error: %d '%s')\n", fd, ferror(f), errno, strerror(errno)); + return -1; + } + + /* touch up buffer size to match reality: */ + d->data = gnutls_realloc(d->data, len); + d->size = len; + return 0; +} + +/* read the file indicated (by name) in the fname parameter. store + its entire contents in a single datum. */ +int set_datum_file(gnutls_datum_t* d, const char* fname) { + struct stat sbuf; + unsigned char* c = NULL; + FILE* file = NULL; + size_t x = 0; + + if (0 != stat(fname, &sbuf)) { + err("failed to stat '%s'\n", fname); + return -1; + } + + c = gnutls_realloc(d->data, sbuf.st_size); + if (NULL == c) { + err("failed to allocate %d bytes for '%s'\n", sbuf.st_size, fname); + return -1; + } + + d->data = c; + d->size = sbuf.st_size; + file = fopen(fname, "r"); + if (NULL == file) { + err("failed to open '%s' for reading\n", fname); + return -1; + } + + x = fread(d->data, d->size, 1, file); + if (x != 1) { + err("tried to read %d bytes, read %d instead from '%s'\n", d->size, x, fname); + fclose(file); + return -1; + } + fclose(file); + return 0; +} + +int write_datum_fd(int fd, const gnutls_datum_t* d) { + if (d->size != write(fd, d->data, d->size)) { + err("failed to write body of datum.\n"); + return -1; + } + return 0; +} + + +int write_datum_fd_with_length(int fd, const gnutls_datum_t* d) { + uint32_t len; + int looks_negative = (d->data[0] & 0x80); + unsigned char zero = 0; + + /* if the first bit is 1, then the datum will appear negative in the + MPI encoding style used by OpenSSH. In that case, we'll increase + the length by one, and dump out one more byte */ + + if (looks_negative) { + len = htonl(d->size + 1); + } else { + len = htonl(d->size); + } + if (write(fd, &len, sizeof(len)) != sizeof(len)) { + err("failed to write size of datum.\n"); + return -2; + } + if (looks_negative) { + if (write(fd, &zero, 1) != 1) { + err("failed to write padding byte for MPI.\n"); + return -2; + } + } + return write_datum_fd(fd, d); +} + +int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num) { + unsigned int i; + int ret; + + for (i = 0; i < num; i++) + if (ret = write_datum_fd_with_length(fd, d[i]), ret != 0) + return ret; + + return 0; +} + + +int datum_from_string(gnutls_datum_t* d, const char* str) { + d->size = strlen(str); + d->data = gnutls_realloc(d->data, d->size); + if (d->data == 0) + return ENOMEM; + memcpy(d->data, str, d->size); + return 0; +} + + +int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]) { + int p[2]; + int ret; + + if (pid == NULL) { + err("bad pointer passed to create_writing_pipe()\n"); + return -1; + } + + if (ret = pipe(p), ret == -1) { + err("failed to create a pipe (error: %d \"%s\")\n", errno, strerror(errno)); + return -1; + } + + *pid = fork(); + if (*pid == -1) { + err("Failed to fork (error: %d \"%s\")\n", errno, strerror(errno)); + return -1; + } + if (*pid == 0) { /* this is the child */ + close(p[1]); /* close unused write end */ + + if (0 != dup2(p[0], 0)) { /* map the reading end into stdin */ + err("Failed to transfer reading file descriptor to stdin (error: %d \"%s\")\n", errno, strerror(errno)); + exit(1); + } + execv(path, argv); + err("exec %s failed (error: %d \"%s\")\n", path, errno, strerror(errno)); + /* close the open file descriptors */ + close(p[0]); + close(0); + + exit(1); + } else { /* this is the parent */ + close(p[0]); /* close unused read end */ + return p[1]; + } +} + +int validate_ssh_host_userid(const char* userid) { + char* oldlocale = setlocale(LC_ALL, "C"); + + /* choke if userid does not match the expected format + ("ssh://fully.qualified.domain.name") */ + if (strncmp("ssh://", userid, strlen("ssh://")) != 0) { + err("The user ID should start with ssh:// for a host key\n"); + goto fail; + } + /* so that isalnum will work properly */ + userid += strlen("ssh://"); + while (0 != (*userid)) { + if (!isalnum(*userid)) { + err("label did not start with a letter or a digit! (%s)\n", userid); + goto fail; + } + userid++; + while (isalnum(*userid) || ('-' == (*userid))) + userid++; + if (('.' == (*userid)) || (0 == (*userid))) { /* clean end of label: + check last char + isalnum */ + if (!isalnum(*(userid - 1))) { + err("label did not end with a letter or a digit!\n"); + goto fail; + } + if ('.' == (*userid)) /* advance to the start of the next label */ + userid++; + } else { + err("invalid character in domain name: %c\n", *userid); + goto fail; + } + } + /* ensure that the last character is valid: */ + if (!isalnum(*(userid - 1))) { + err("hostname did not end with a letter or a digit!\n"); + goto fail; + } + /* FIXME: fqdn's can be unicode now, thanks to RFC 3490 -- how do we + make sure that we've got an OK string? */ + + return 0; + + fail: + setlocale(LC_ALL, oldlocale); + return 1; +} + +/* http://tools.ietf.org/html/rfc4880#section-5.5.2 */ +size_t get_openpgp_mpi_size(gnutls_datum_t* d) { + return 2 + d->size; +} + +int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d) { + uint16_t x; + + x = d->size * 8; + x = htons(x); + + write(fd, &x, sizeof(x)); + write(fd, d->data, d->size); + + return 0; +} diff --git a/src/gpg2ssh/gnutls-helpers.h b/src/gpg2ssh/gnutls-helpers.h new file mode 100644 index 0000000..9ea22a3 --- /dev/null +++ b/src/gpg2ssh/gnutls-helpers.h @@ -0,0 +1,72 @@ +/* Author: Daniel Kahn Gillmor */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Functions to help dealing with GnuTLS for monkeysphere key + translation projects: */ + +/* set everything up, including logging levels. Return 0 on + success */ +int init_gnutls(); + +/* logging and output functions: */ + +void err(const char* fmt, ...); +void logfunc(int level, const char* string); + +/* basic datum manipulations: */ + +void init_datum(gnutls_datum_t* d); +void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src); +int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b); +void free_datum(gnutls_datum_t* d); +int write_datum_fd(int fd, const gnutls_datum_t* d); +int write_datum_fd_with_length(int fd, const gnutls_datum_t* d); +int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num); + +/* set up a datum from a null-terminated string */ +int datum_from_string(gnutls_datum_t* d, const char* str); + +/* keyid manipulations: */ +typedef unsigned char printable_keyid[16]; + +void init_keyid(gnutls_openpgp_keyid_t keyid); +void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid); + +/* functions to get data into datum objects: */ + +/* read the passed-in string, store in a single datum */ +int set_datum_string(gnutls_datum_t* d, const char* s); + +/* read the passed-in file descriptor until EOF, store in a single + datum */ +int set_datum_fd(gnutls_datum_t* d, int fd); + +/* read the file indicated (by name) in the fname parameter. store + its entire contents in a single datum. */ +int set_datum_file(gnutls_datum_t* d, const char* fname); + +/* set up file descriptor pipe for writing (child process pid gets + stored in pid, fd is returned)*/ +int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]); + +/* return 0 if userid matches the monkeysphere spec for ssh host user IDs */ +int validate_ssh_host_userid(const char* userid); + +/* how many bytes will it take to write out this datum in OpenPGP MPI form? */ +size_t get_openpgp_mpi_size(gnutls_datum_t* d); + +/* write the MPI stored in gnutls_datum_t to file descriptor fd: */ +int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d); diff --git a/src/gpg2ssh/gpg2ssh.c b/src/gpg2ssh/gpg2ssh.c new file mode 100644 index 0000000..c99f03f --- /dev/null +++ b/src/gpg2ssh/gpg2ssh.c @@ -0,0 +1,293 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* for waitpid() */ +#include +#include + +/* + Author: Daniel Kahn Gillmor + Date: Tue, 08 Apr 2008 + License: GPL v3 or later + + monkeysphere public key translator: execute this with an GPG + certificate (public key(s) + userid(s)) on stdin. It currently + only works with RSA keys. + + It will spit out a version of the first key capable of being used + for authentication on stdout. The output format should be suitable + for appending a known_hosts file. + + Requirements: I've only built this so far with GnuTLS v2.3.4 -- + version 2.2.0 does not contain the appropriate pieces. + + */ + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_openpgp_crt_t openpgp_crt; + gnutls_openpgp_keyid_t keyid; + printable_keyid p_keyid; + unsigned int keyidx; + unsigned int usage, bits; + gnutls_pk_algorithm_t algo; + + gnutls_datum_t m, e, p, q, g, y; + gnutls_datum_t algolabel; + + char output_data[10240]; + char userid[10240]; + size_t uidsz = sizeof(userid); + + const gnutls_datum_t* all[5]; + int pipefd; + pid_t child_pid; + char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; + const char* algoname; + int mpicount; + int pipestatus; + + init_gnutls(); + + init_datum(&data); + + init_datum(&m); + init_datum(&e); + init_datum(&p); + init_datum(&q); + init_datum(&g); + init_datum(&y); + + init_datum(&algolabel); + + init_keyid(keyid); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + if (ret = gnutls_openpgp_crt_init(&openpgp_crt), ret) { + err("Failed to initialize OpenPGP certificate (error: %d)\n", ret); + return 1; + } + + /* format could be either: GNUTLS_OPENPGP_FMT_RAW, + GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, + otherwise, use BASE64: */ + + /* FIXME: we should be auto-detecting the input format, and + translating it as needed. */ + + if (getenv("MONKEYSPHERE_RAW")) { + err("assuming RAW formatted certificate\n"); + if (ret = gnutls_openpgp_crt_import(openpgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW), ret) { + err("failed to import the OpenPGP certificate in RAW format (error: %d)\n", ret); + return ret; + } + } else { + err("assuming BASE64 formatted certificate\n"); + if (ret = gnutls_openpgp_crt_import (openpgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64), ret) { + err("failed to import the OpenPGP certificate in BASE64 format (error: %d)\n", ret); + return ret; + } + } + + if (gnutls_openpgp_crt_get_revoked_status(openpgp_crt)) { + err("the primary key was revoked!\n"); + return 1; + } + + /* FIXME: We're currently looking at the primary key or maybe the + first authentication-capable subkey. + + Instead, we should be iterating through the primary key and all + subkeys: for each one with the authentication usage flag set of a + algorithm we can handle, we should output matching UserIDs and + the SSH version of the key. */ + + + if (ret = gnutls_openpgp_crt_get_key_usage(openpgp_crt, &usage), ret) { + err("failed to get the usage flags for the primary key (error: %d)\n", ret); + return ret; + } + if (usage & GNUTLS_KEY_KEY_AGREEMENT && + usage & GNUTLS_KEY_KEY_ENCIPHERMENT) { + err("the primary key can be used for authentication and communication encryption!\n"); + + algo = gnutls_openpgp_crt_get_pk_algorithm(openpgp_crt, &bits); + if (algo < 0) { + err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + + err("OpenPGP RSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_rsa_raw(openpgp_crt, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA Key, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_dsa_raw(openpgp_crt, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + + } else { + err("primary key is not good for authentication and communication encryption. Trying subkeys...\n"); + + if (ret = gnutls_openpgp_crt_get_auth_subkey(openpgp_crt, keyid, 0), ret) { + err("failed to find a subkey capable of authentication and communication encryption (error: %d)\n", ret); + return ret; + } + make_keyid_printable(p_keyid, keyid); + err("found authentication subkey %.16s\n", p_keyid); + + ret = gnutls_openpgp_crt_get_subkey_idx(openpgp_crt, keyid); + if (ret < 0) { + err("could not get the index of subkey %.16s (error: %d)\n", ret); + return ret; + } + keyidx = ret; + + if (gnutls_openpgp_crt_get_subkey_revoked_status(openpgp_crt, keyidx)) { + err("The authentication subkey was revoked!\n"); + return 1; + } + + if (ret = gnutls_openpgp_crt_get_subkey_usage(openpgp_crt, keyidx, &usage), ret) { + err("could not figure out usage of subkey %.16s (error: %d)\n", p_keyid, ret); + return ret; + } + if ((usage & GNUTLS_KEY_KEY_AGREEMENT) == 0 && + usage & GNUTLS_KEY_KEY_ENCIPHERMENT) { + err("could not find a subkey with authentication and communication encryption.\n"); + return 1; + } + + /* switch, based on the algorithm in question, to extract the MPI + components: */ + + algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(openpgp_crt, keyidx, &bits); + if (algo < 0) { + err("failed to get the algorithm of the authentication subkey (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + + err("OpenPGP RSA subkey, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(openpgp_crt, keyidx, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA subkey parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA subkey, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(openpgp_crt, keyidx, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA subkey parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP subkey was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + } + + /* make sure userid is NULL-terminated */ + userid[sizeof(userid) - 1] = 0; + uidsz--; + + /* FIXME: we're just choosing the first UserID from the certificate: + instead, we should be selecting every User ID that is adequately + signed and matches the spec, and aggregating them with commas for + known_hosts output */ + + if (ret = gnutls_openpgp_crt_get_name(openpgp_crt, 0, userid, &uidsz), ret) { + err("Failed to fetch the first UserID (error: %d)\n", ret); + return ret; + } + + if (ret = validate_ssh_host_userid(userid), ret) { + err("bad userid: not a valid ssh host.\n"); + return ret; + } + + /* remove ssh:// from the beginning of userid */ + memmove(userid, userid + strlen("ssh://"), 1 + strlen(userid) - strlen("ssh://")); + + + /* now we have algo, and the various MPI data are set. Can we + export them cleanly? */ + + /* for the moment, we'll just dump the info raw, and pipe it + externally through coreutils' /usr/bin/base64 */ + + if (algo == GNUTLS_PK_RSA) { + algoname = "ssh-rsa"; + mpicount = 3; + + all[0] = &algolabel; + all[1] = &e; + all[2] = &m; + } else if (algo == GNUTLS_PK_DSA) { + algoname = "ssh-dss"; + mpicount = 5; + + all[0] = &algolabel; + all[1] = &p; + all[2] = &q; + all[3] = &g; + all[4] = &y; + } else { + err("no idea what this algorithm is: %d\n", algo); + return 1; + } + + if (ret = datum_from_string(&algolabel, algoname), ret) { + err("couldn't label string (error: %d)\n", ret); + return ret; + } + + snprintf(output_data, sizeof(output_data), "%s %s ", userid, algoname); + + pipefd = create_writing_pipe(&child_pid, args[0], args); + if (pipefd < 0) { + err("failed to create a writing pipe (returned %d)\n", pipefd); + return pipefd; + } + + write(1, output_data, strlen(output_data)); + + if (0 != write_data_fd_with_length(pipefd, all, mpicount)) { + err("was not able to write out RSA key data\n"); + return 1; + } + close(pipefd); + if (child_pid != waitpid(child_pid, &pipestatus, 0)) { + err("could not wait for child process to return for some reason.\n"); + return 1; + } + if (pipestatus != 0) { + err("base64 pipe died with return code %d\n", pipestatus); + return pipestatus; + } + + write(1, "\n", 1); + + + + gnutls_openpgp_crt_deinit(openpgp_crt); + gnutls_global_deinit(); + return 0; +} diff --git a/src/gpg2ssh/main.c b/src/gpg2ssh/main.c new file mode 100644 index 0000000..d6bac68 --- /dev/null +++ b/src/gpg2ssh/main.c @@ -0,0 +1,271 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* + Author: Daniel Kahn Gillmor + Date: Tue, 01 Apr 2008 + License: GPL v3 or later + + monkeysphere private key translator: execute this with an GPG + secret key on stdin (at the moment, only passphraseless RSA keys + work). + + It will spit out a PEM-encoded version of the key on stdout, which + can be fed into ssh-add like this: + + gpg --export-secret-keys $KEYID | monkeysphere | ssh-add -c /dev/stdin + + Requirements: I've only built this so far with GnuTLS v2.3.4 -- + version 2.2.0 does not contain the appropriate pieces. + + Notes: gpgkey2ssh doesn't seem to provide the same public + keys. Mighty weird! + +0 wt215@squeak:~/monkeysphere$ gpg --export-secret-keys 1DCDF89F | ~dkg/src/monkeysphere/monkeysphere | ssh-add -c /dev/stdin +gnutls version: 2.3.4 +OpenPGP RSA Key, with 1024 bits +Identity added: /dev/stdin (/dev/stdin) +The user has to confirm each use of the key +0 wt215@squeak:~/monkeysphere$ ssh-add -L +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9gWQqfrnhQKDQnND/3eOexpddE64J+1zp9fcyCje7H5LKclb6DBV2HS6WgW32PJhIzvP+fYZM3dzXea3fpv14y1SicXiRBDgF9SnsNA1qWn2RyzkLcKy7PmM0PDYtU1oiLTcQj/xkWcqW2sLKHT/WW+vZP5XP7RMGN/yWNMfE2Q== /dev/stdin +0 wt215@squeak:~/monkeysphere$ gpgkey2ssh 1DCDF89F +ssh-rsa AAAAB3NzaC1yc2EAAACBAL2BZCp+ueFAoNCc0P/d457Gl10Trgn7XOn19zIKN7sfkspyVvoMFXYdLpaBbfY8mEjO8/59hkzd3Nd5rd+m/XjLVKJxeJEEOAX1Kew0DWpafZHLOQtwrLs+YzQ8Ni1TWiItNxCP/GRZypbawsodP9Zb69k/lc/tEwY3/JY0x8TZAAAAAwEAAQ== COMMENT +0 wt215@squeak:~/monkeysphere$ + + */ + + +int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) { + gnutls_openpgp_privkey_t pgp_privkey; + gnutls_datum_t m, e, d, p, q, u, g, y, x; + gnutls_pk_algorithm_t pgp_algo; + unsigned int pgp_bits; + int ret; + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + init_datum(&x); + + if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) { + err("Failed to initialized OpenPGP private key (error: %d)\n", ret); + return 1; + } + + + /* format could be either: GNUTLS_OPENPGP_FMT_RAW, + GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, + otherwise, use BASE64: */ + + if (getenv("MONKEYSPHERE_RAW")) { + err("assuming RAW formatted private keys\n"); + if (ret = gnutls_openpgp_privkey_import(pgp_privkey, input, GNUTLS_OPENPGP_FMT_RAW, NULL, 0), ret) + err("failed to import the OpenPGP private key in RAW format (error: %d)\n", ret); + } else { + err("assuming BASE64 formatted private keys\n"); + if (ret = gnutls_openpgp_privkey_import (pgp_privkey, input, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0), ret) + err("failed to import the OpenPGP private key in BASE64 format (error: %d)\n", ret); + } + + pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(pgp_privkey, &pgp_bits); + if (pgp_algo < 0) { + err("failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo); + return 1; + } + if (pgp_algo == GNUTLS_PK_RSA) { + err("OpenPGP RSA Key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_rsa_raw(pgp_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + + ret = gnutls_x509_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (pgp_algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA Key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_dsa_raw(pgp_privkey, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + + ret = gnutls_x509_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", pgp_algo); + return 1; + } + + ret = gnutls_x509_privkey_fix(*output); + if (ret != 0) { + err("failed to fix up the private key in X.509 format (error: %d)\n", ret); + return 1; + } + + gnutls_openpgp_privkey_deinit(pgp_privkey); + return 0; +} + +int convert_x509_to_pgp(gnutls_openpgp_privkey_t* output, gnutls_datum_t* input) { + gnutls_x509_privkey_t x509_privkey; + gnutls_datum_t m, e, d, p, q, u, g, y, x; + gnutls_pk_algorithm_t x509_algo; + int ret; + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + init_datum(&x); + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialized X.509 private key (error: %d)\n", ret); + return 1; + } + + + /* format could be either: GNUTLS_X509_FMT_DER, + GNUTLS_X509_FMT_PEM; if MONKEYSPHERE_DER is set, use DER, + otherwise, use PEM: */ + + if (getenv("MONKEYSPHERE_DER")) { + err("assuming DER formatted private keys\n"); + if (ret = gnutls_x509_privkey_import(x509_privkey, input, GNUTLS_X509_FMT_DER), ret) + err("failed to import the X.509 private key in DER format (error: %d)\n", ret); + } else { + err("assuming PEM formatted private keys\n"); + if (ret = gnutls_x509_privkey_import (x509_privkey, input, GNUTLS_X509_FMT_PEM), ret) + err("failed to import the X.509 private key in PEM format (error: %d)\n", ret); + } + + x509_algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); + if (x509_algo < 0) { + err("failed to get X.509 key algorithm (error: %d)\n", x509_algo); + return 1; + } + if (x509_algo == GNUTLS_PK_RSA) { + err("X.509 RSA Key\n"); + ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + + /* ret = gnutls_openpgp_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); */ + ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (x509_algo == GNUTLS_PK_DSA) { + err("X.509 DSA Key\n"); + ret = gnutls_x509_privkey_export_dsa_raw(x509_privkey, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + + /* ret = gnutls_openpgp_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); */ + ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", x509_algo); + return 1; + } + + gnutls_x509_privkey_deinit(x509_privkey); + return 0; +} + + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_x509_privkey_t x509_privkey; + + char output_data[10240]; + size_t ods = sizeof(output_data); + + init_gnutls(); + + init_datum(&data); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + + /* Or, instead, read in key from a file name: + if (ret = set_datum_file(&data, argv[1]), ret) { + err("didn't read file '%s'\n", argv[1]); + return 1; + } +*/ + + /* treat the passed file as an X.509 private key, and extract its + component values: */ + +/* if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { */ +/* err("Failed to import the X.509 key (error: %d)\n", ret); */ +/* return 1; */ +/* } */ +/* gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); */ + + /* try to print the PEM-encoded private key: */ +/* ret = gnutls_x509_privkey_export (x509_privkey, */ +/* GNUTLS_X509_FMT_PEM, */ +/* output_data, */ +/* &ods); */ +/* printf("ret: %u; ods: %u;\n", ret, ods); */ +/* if (ret == 0) { */ +/* write(0, output_data, ods); */ +/* } */ + + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialize X.509 private key (error: %d)\n", ret); + return 1; + } + + if (ret = convert_pgp_to_x509(&x509_privkey, &data), ret) { + return ret; + } + + ret = gnutls_x509_privkey_export (x509_privkey, + GNUTLS_X509_FMT_PEM, + output_data, + &ods); + printf("ret: %u; ods: %u;\n", ret, ods); + if (ret == 0) { + write(1, output_data, ods); + } + + + gnutls_x509_privkey_deinit(x509_privkey); + gnutls_global_deinit(); + return 0; +} diff --git a/src/gpg2ssh/ssh2gpg.c b/src/gpg2ssh/ssh2gpg.c new file mode 100644 index 0000000..b14a540 --- /dev/null +++ b/src/gpg2ssh/ssh2gpg.c @@ -0,0 +1,171 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* for waitpid() */ +#include +#include + +/* for time() */ +#include + +/* for htons() */ +#include + + +/* + Author: Daniel Kahn Gillmor + Date: Sun, 2008-04-20 + License: GPL v3 or later + + monkeysphere public key translator: execute this with an ssh + private key on stdin. It currently only works with RSA keys. + + it should eventually work with OpenSSH-style public keys instead of + the full private key, but it was easier to do this way. + + It shoud spit out a version of the public key suitable for acting + as an OpenPGP public sub key packet. + + */ + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_x509_privkey_t x509_privkey; + gnutls_openpgp_crt_t openpgp_crt; + gnutls_openpgp_keyid_t keyid; + printable_keyid p_keyid; + unsigned int keyidx; + unsigned int usage, bits; + gnutls_pk_algorithm_t algo; + + unsigned char packettag; + unsigned char openpgpversion; + time_t timestamp; + uint32_t clunkytime; + unsigned char openpgpalgo; + unsigned int packetlen; + uint16_t plen; + + gnutls_datum_t m, e, d, p, q, u, g, y; + gnutls_datum_t algolabel; + + char output_data[10240]; + char userid[10240]; + size_t uidsz = sizeof(userid); + + const gnutls_datum_t* all[5]; + int pipefd; + pid_t child_pid; + char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; + const char* algoname; + int mpicount; + int pipestatus; + + init_gnutls(); + + init_datum(&data); + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + + init_datum(&algolabel); + + init_keyid(keyid); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialize private key structure (error: %d)\n", ret); + return 1; + } + + err("assuming PEM formatted private key\n"); + if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { + err("failed to import the PEM-encoded private key (error: %d)\n", ret); + return ret; + } + + algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); + if (algo < 0) { + err("failed to get the algorithm of the PEM-encoded public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + err("RSA private key\n"); + ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + err("Modulus size %d, exponent size %d\n", m.size, e.size); + } else if (algo == GNUTLS_PK_DSA) { + err("DSA Key, not implemented!!\n", bits); + return 1; + } else { + err("Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + + /* now we have algo, and the various MPI data are set. Can we + export them as a public subkey packet? */ + + /* this packet should be tagged 14, and should contain: + + 1 octet: version (4) + 4 octets: time of generation (seconds since 1970) + 1 octet: algo (http://tools.ietf.org/html/rfc4880#section-5.5.2 implies 1 for RSA) + + MPI: modulus + MPI: exponent + */ + + packetlen = 1 + 4 + 1; + /* FIXME: this is RSA only. for DSA, there'll be more: */ + packetlen += get_openpgp_mpi_size(&m) + get_openpgp_mpi_size(&e); + + /* FIXME: we should generate this bound more cleanly -- i just + happen to know that 65535 is 2^16-1: */ + if (packetlen > 65535) { + err("packet length is too long (%d)\n", packetlen); + return 1; + } + + /* we're going to emit an old-style packet, with tag 14 (public + subkey), with a two-octet packet length */ + packettag = 0x80 | (14 << 2) | 1; + + write(1, &packettag, sizeof(packettag)); + plen = htons(packetlen); + write(1, &plen, sizeof(plen)); + + openpgpversion = 4; + write(1, &openpgpversion, 1); + + timestamp = time(NULL); + clunkytime = htonl(timestamp); + write(1, &clunkytime, 4); + + /* FIXME: handle things other than RSA */ + openpgpalgo = 1; + write(1, &openpgpalgo, 1); + + write_openpgp_mpi_to_fd(1, &m); + write_openpgp_mpi_to_fd(1, &e); + + gnutls_x509_privkey_deinit(x509_privkey); + gnutls_global_deinit(); + return 0; +} diff --git a/src/howler/howler b/src/howler/howler new file mode 100755 index 0000000..0b67c02 --- /dev/null +++ b/src/howler/howler @@ -0,0 +1,134 @@ +#!/bin/sh + +# howler: monkeysphere server gpg generator/publisher/maintainer +# +# Written by +# Jameson Rollins +# +# Copyright 2008, released under the GPL, version 3 or later + +PGRM=$(basename $0) + +######################################################################## +# FUNCTIONS +######################################################################## + +usage() { +cat <&2 + exit ${2:-'1'} +} + +# generate server gpg key +gen_key() { + KEY_TYPE=${KEY_TYPE:-RSA} + KEY_LENGTH=${KEY_LENGTH:-2048} + KEY_USAGE=${KEY_USAGE:-encrypt,auth} + SERVICE=${SERVICE:-ssh} + HOSTNAME_FQDN=${HOSTNAME_FQDN:-$(hostname -f)} + + USERID=${USERID:-"$SERVICE"://"$HOSTNAME_FQDN"} + + echo "key parameters:" + cat < /dev/null 2>&1 ; then + failure "key for '$USERID' already exists" + fi + + echo "generating server key..." + gpg --batch --gen-key < /dev/null | grep '^pub:' | cut -d: -f5) + + # dummy command so as not to publish fakes keys during testing + # eventually: + #gpg --send-keys --keyserver "$KEYSERVER" "$keyID" + echo "gpg --send-keys --keyserver $KEYSERVER $keyID" +} + +trust_key() { + for keyID ; do + # get the key from the key server + gpg --keyserver "$KEYSERVER" --recv-key "$keyID" || failure "could not retrieve key '$keyID'" + + # edit the key to change trust + # FIXME: need to figure out how to automate this, + # in a batch mode or something. + gpg --edit-key "$keyID" + done +} + +######################################################################## +# MAIN +######################################################################## + +# set ms home directory +MS_HOME=${MS_HOME:-/etc/monkeysphere} + +# load configuration file +MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} +[ -e "$MS_CONF" ] && . "$MS_CONF" + +GNUPGHOME=${GNUPGHOME:-"$MS_HOME"/gnupg} +export GNUPGHOME +KEYSERVER=${KEYSERVER:-subkeys.pgp.net} +export KEYSERVER + +COMMAND="$1" +[ "$COMMAND" ] || failure "Type '$PGRM help' for usage." +shift 1 + +case $COMMAND in + 'gen-key') + gen_key + ;; + 'publish-key') + publish_key + ;; + 'trust-key') + if [ -z "$1" ] ; then + failure "you must specify at least one key to trust." + fi + trust_key "$@" + ;; + 'help') + usage + exit + ;; + *) + failure "Unknown command: '$COMMAND' +Type '$PGRM help' for usage." + ;; +esac diff --git a/src/monkeysphere b/src/monkeysphere new file mode 100755 index 0000000..f279d86 --- /dev/null +++ b/src/monkeysphere @@ -0,0 +1,154 @@ +#!/bin/sh + +######################################################################## +PGRM=$(basename $0) + +SHAREDIR=${SHAREDIR:-"/usr/share/monkeysphere"} +export SHAREDIR +. "${SHAREDIR}/common" + +GLOBAL_CONFIG=${GLOBAL_CONFIG:-"${ETC}"/monkeysphere.conf} +[ -r "$GLOBAL_CONFIG" ] && . "$GLOBAL_CONFIG" + +# date in UTF format if needed +DATE=$(date -u '+%FT%T') + +# unset some environment variables that could screw things up +GREP_OPTIONS= + +######################################################################## +# FUNCTIONS +######################################################################## + +usage() { +cat < [args] +Monkeysphere client tool. + +subcommands: + update-known-hosts (k) [HOST]... update known_hosts file + update-authorized-keys (a) update authorized_keys file + update-userid (u) [USERID]... add/update userid to + authorized_user_ids + help (h,?) this help + +EOF +} + +######################################################################## +# MAIN +######################################################################## + +COMMAND="$1" +[ "$COMMAND" ] || failure "Type '$PGRM help' for usage." +shift + +# set ms home directory +MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere} + +# load configuration file +MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} +[ -e "$MS_CONF" ] && . "$MS_CONF" + +# set empty config variable with defaults +AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids} +GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg} +KEYSERVER=${KEYSERVER:-subkeys.pgp.net} +REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"} +USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-%h/.ssh/authorized_keys} +USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts} +HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-} + +export GNUPGHOME + +# stagging locations +hostKeysCacheDir="$MS_HOME"/host_keys +userKeysCacheDir="$MS_HOME"/user_keys +msAuthorizedKeys="$MS_HOME"/authorized_keys + +# make sure gpg home exists with proper permissions +mkdir -p -m 0700 "$GNUPGHOME" + +case $COMMAND in + 'update-known-hosts'|'k') + MODE='known_hosts' + + # touch the known_hosts file to make sure it exists + touch "$USER_KNOWN_HOSTS" + + # if hosts are specified on the command line, process just + # those hosts + if [ "$1" ] ; then + for host ; do + process_host "$host" "$hostKeysCacheDir" + done + + # otherwise, if no hosts are specified, process the user + # known_hosts file + else + if [ ! -s "$USER_KNOWN_HOSTS" ] ; then + failure "known_hosts file '$USER_KNOWN_HOSTS' is empty." + fi + log "processing known_hosts file..." + process_known_hosts "$USER_KNOWN_HOSTS" "$hostKeysCacheDir" + fi + ;; + + 'update-authorized-keys'|'a') + MODE='authorized_keys' + + log "processing authorized_user_ids file..." + + # make sure authorized_user_ids file exists + if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then + log "authorized_user_ids file is empty or does not exist." + exit + fi + + process_authorized_ids "$AUTHORIZED_USER_IDS" "$userKeysCacheDir" + + # write output key file + log "writing monkeysphere authorized_keys file... " + touch "$msAuthorizedKeys" + if [ "$(ls "$userKeysCacheDir")" ] ; then + log -n "adding gpg keys... " + cat "$userKeysCacheDir"/* > "$msAuthorizedKeys" + echo "done." + else + log "no gpg keys to add." + fi + if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then + userAuthorizedKeys=${USER_CONTROLLED_AUTHORIZED_KEYS/\%h/"$HOME"} + if [ -s "$userAuthorizedKeys" ] ; then + log -n "adding user authorized_keys file... " + cat "$userAuthorizedKeys" >> "$msAuthorizedKeys" + echo "done." + fi + fi + log "monkeysphere authorized_keys file generated:" + log "$msAuthorizedKeys" + ;; + + 'update-userid'|'u') + if [ -z "$1" ] ; then + failure "you must specify at least one userid." + fi + for userID ; do + if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then + log "userid '$userID' not in authorized_user_ids file." + continue + fi + log "processing user id: '$userID'" + process_user_id "$userID" "$userKeysCacheDir" > /dev/null + done + ;; + + 'help'|'h'|'?') + usage + ;; + + *) + failure "Unknown command: '$COMMAND' +Type 'cereal-admin help' for usage." + ;; +esac diff --git a/src/monkeysphere-server b/src/monkeysphere-server new file mode 100755 index 0000000..f1b4892 --- /dev/null +++ b/src/monkeysphere-server @@ -0,0 +1,219 @@ +#!/bin/sh + +######################################################################## +PGRM=$(basename $0) + +SHAREDIR=${SHAREDIR:-"/usr/share/monkeysphere"} +export SHAREDIR +. "${SHAREDIR}/common" + +# date in UTF format if needed +DATE=$(date -u '+%FT%T') + +# unset some environment variables that could screw things up +GREP_OPTIONS= + +######################################################################## +# FUNCTIONS +######################################################################## + +usage() { +cat < [args] +Monkeysphere server admin tool. + +subcommands: + update-users (s) [USER]... update authorized_keys file + gen-key (g) generate gpg key for the host + publish-key (p) publish host gpg to keyserver + trust-key (t) KEYID [KEYID]... mark keyid as trusted + update-user-userid (u) USER UID [UID]... add/update userid for user + help (h,?) this help + +EOF +} + +# generate server gpg key +gen_key() { + KEY_TYPE=${KEY_TYPE:-RSA} + KEY_LENGTH=${KEY_LENGTH:-2048} + KEY_USAGE=${KEY_USAGE:-encrypt,auth} + SERVICE=${SERVICE:-ssh} + HOSTNAME_FQDN=${HOSTNAME_FQDN:-$(hostname -f)} + + USERID=${USERID:-"$SERVICE"://"$HOSTNAME_FQDN"} + + echo "key parameters:" + cat < /dev/null 2>&1 ; then + failure "key for '$USERID' already exists" + fi + + echo "generating server key..." + gpg --batch --gen-key < /dev/null | grep '^pub:' | cut -d: -f5) + + # dummy command so as not to publish fakes keys during testing + # eventually: + #gpg --send-keys --keyserver "$KEYSERVER" "$keyID" + echo "gpg --send-keys --keyserver $KEYSERVER $keyID" +} + +# trust key +trust_key() { + for keyID ; do + # get the key from the key server + gpg --keyserver "$KEYSERVER" --recv-key "$keyID" || failure "could not retrieve key '$keyID'" + + # edit the key to change trust + # FIXME: need to figure out how to automate this, + # in a batch mode or something. + gpg --edit-key "$keyID" + done +} + +######################################################################## +# MAIN +######################################################################## + +COMMAND="$1" +[ "$COMMAND" ] || failure "Type '$PGRM help' for usage." +shift + +# set ms home directory +MS_HOME=${MS_HOME:-"$ETC"} + +# load configuration file +MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere-server.conf} +[ -e "$MS_CONF" ] && . "$MS_CONF" + +# set empty config variable with defaults +GNUPGHOME=${GNUPGHOME:-"$MS_HOME"/gnupg} +KEYSERVER=${KEYSERVER:-subkeys.pgp.net} +REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"} +USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-%h/.ssh/authorized_keys} +STAGING_AREA=${STAGING_AREA:-"$LIB"/stage} + +export GNUPGHOME + +# make sure gpg home exists with proper permissions +mkdir -p -m 0700 "$GNUPGHOME" + +case $COMMAND in + 'update-users'|'s') + if [ "$1" ] ; then + unames="$@" + else + unames=$(ls -1 "$MS_HOME"/authorized_user_ids) + fi + + for uname in $unames ; do + MODE="authorized_keys" + authorizedUserIDs="$MS_HOME"/authorized_user_ids/"$uname" + cacheDir="$STAGING_AREA"/"$uname"/user_keys + msAuthorizedKeys="$STAGING_AREA"/"$uname"/authorized_keys + + # make sure authorized_user_ids file exists + if [ ! -s "$authorizedUserIDs" ] ; then + log "authorized_user_ids file for '$uname' is empty or does not exist." + continue + fi + + log "processing authorized_keys for user '$uname'..." + + process_authorized_ids "$authorizedUserIDs" "$cacheDir" + + # write output key file + log "writing monkeysphere authorized_keys file... " + touch "$msAuthorizedKeys" + if [ "$(ls "$cacheDir")" ] ; then + log -n "adding gpg keys... " + cat "$cacheDir"/* > "$msAuthorizedKeys" + echo "done." + else + log "no gpg keys to add." + fi + if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then + userHome=$(getent passwd "$uname" | cut -d: -f6) + userAuthorizedKeys=${USER_CONTROLLED_AUTHORIZED_KEYS/\%h/"$userHome"} + if [ -s "$userAuthorizedKeys" ] ; then + log -n "adding user authorized_keys file... " + cat "$userAuthorizedKeys" >> "$msAuthorizedKeys" + echo "done." + fi + fi + log "monkeysphere authorized_keys file generated:" + log "$msAuthorizedKeys" + done + ;; + + 'gen-key'|'g') + gen_key + ;; + + 'publish-key'|'p') + publish_key + ;; + + 'trust-key'|'t') + if [ -z "$1" ] ; then + failure "you must specify at least one key to trust." + fi + trust_key "$@" + ;; + + 'update-user-userid'|'u') + uname="$1" + shift + if [ -z "$uname" ] ; then + failure "you must specify user." + fi + if [ -z "$1" ] ; then + failure "you must specify at least one userid." + fi + for userID ; do + AUTHORIZED_USER_IDS="$MS_HOME"/authorized_user_ids/"$uname" + if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then + log "userid '$userID' not in authorized_user_ids file." + continue + fi + log "processing user id: '$userID'" + process_user_id "$userID" "$userKeysCacheDir" > /dev/null + done + ;; + + 'help'|'h'|'?') + usage + ;; + + *) + failure "Unknown command: '$COMMAND' +Type 'cereal-admin help' for usage." + ;; +esac diff --git a/src/monkeysphere-ssh-proxycommand b/src/monkeysphere-ssh-proxycommand new file mode 100755 index 0000000..1724966 --- /dev/null +++ b/src/monkeysphere-ssh-proxycommand @@ -0,0 +1,16 @@ +#!/bin/sh -e + +# MonkeySphere ssh ProxyCommand hook +# Proxy command script to initiate a monkeysphere known_hosts update +# before an ssh connection to host is established. +# Can be added to ~/.ssh/config as follows: +# ProxyCommand monkeysphere-ssh-proxycommand %h %p + +HOST="$1" +PORT="$2" + +# update the known_hosts file for the host +monkeysphere update-known-hosts "$HOST" + +# make a netcat connection to host for the ssh connection +exec nc "$HOST" "$PORT" diff --git a/src/rhesus/README b/src/rhesus/README new file mode 100644 index 0000000..4d383d5 --- /dev/null +++ b/src/rhesus/README @@ -0,0 +1,30 @@ +rhesus is the monkeysphere authorized_keys/known_hosts generator. + +In authorized_keys mode, rhesus takes an auth_user_ids file, which +contains gpg user ids, uses gpg to fetch the keys of the specified +users, does a monkeysphere policy check on each id, and uses gpg2ssh +to generate authorized_keys lines for each verified id. The lines are +then combined with a user's traditional authorized_keys file to create +a new authorized_keys file. + +In known_hosts mode, rhesus takes an auth_host_ids file, which +contains gpg user ids of the form ssh://URL, uses gpg to fetch the +keys of the specified hosts, does a monkeysphere policy check on each +id, and uses gpg2ssh to generate a known_hosts lines for each verified +id. The lines are then combined with a user's traditional known_hosts +file to create a new known_hosts file. + +When run as a normal user, no special configuration is needed. + +When run as an administrator to update system-maintained +authorized_keys files for each user, the following environment +variables should be defined first: + + MS_CONF=/etc/monkeysphere/monkeysphere.conf + USER=foo + +For example, the command might be run like this: + + for USER in $(ls -1 /home) ; do + MS_CONF=/etc/monkeysphere/monkeysphere.conf rhesus --authorized_keys + done diff --git a/src/rhesus/rhesus b/src/rhesus/rhesus new file mode 100755 index 0000000..f607f0b --- /dev/null +++ b/src/rhesus/rhesus @@ -0,0 +1,466 @@ +#!/bin/sh + +# rhesus: monkeysphere authorized_keys/known_hosts generating script +# +# Written by +# Jameson Rollins +# +# Copyright 2008, released under the GPL, version 3 or later + +# all caps variables are meant to be user supplied (ie. from config +# file) and are considered global + +PGRM=$(basename $0) + +# date in UTF format if needed +DATE=$(date -u '+%FT%T') + +# unset some environment variables that could screw things up +GREP_OPTIONS= + +######################################################################## +# FUNCTIONS +######################################################################## + +usage() { +cat <&2 + exit ${2:-'1'} +} + +# write output to stdout +log() { + echo -n "ms: " + echo "$@" +} + +# write output to stderr +loge() { + echo -n "ms: " 1>&2 + echo "$@" 1>&2 +} + +# cut out all comments(#) and blank lines from standard input +meat() { + grep -v -e "^[[:space:]]*#" -e '^$' +} + +# cut a specified line from standard input +cutline() { + head --line="$1" | tail -1 +} + +# retrieve all keys with given user id from keyserver +# FIXME: need to figure out how to retrieve all matching keys +# (not just first 5) +gpg_fetch_keys() { + local id + id="$1" + echo 1,2,3,4,5 | \ + gpg --quiet --batch --command-fd 0 --with-colons \ + --keyserver "$KEYSERVER" \ + --search ="$id" >/dev/null 2>&1 +} + +# check that characters are in a string (in an AND fashion). +# used for checking key capability +# check_capability capability a [b...] +check_capability() { + local capability + local capcheck + + capability="$1" + shift 1 + + for capcheck ; do + if echo "$capability" | grep -q -v "$capcheck" ; then + return 1 + fi + done + return 0 +} + +# convert escaped characters from gpg output back into original +# character +# FIXME: undo all escape character translation in with-colons gpg output +unescape() { + echo "$1" | sed 's/\\x3a/:/' +} + +# stand in until we get dkg's gpg2ssh program +gpg2ssh_tmp() { + local mode + local keyID + local userID + local host + + mode="$1" + keyID="$2" + userID="$3" + + if [ "$mode" = 'authorized_keys' ] ; then + gpgkey2ssh "$keyID" | sed -e "s/COMMENT/${userID}/" + + # NOTE: it seems that ssh-keygen -R removes all comment fields from + # all lines in the known_hosts file. why? + # NOTE: just in case, the COMMENT can be matched with the + # following regexp: + # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' + elif [ "$mode" = 'known_hosts' ] ; then + host=$(echo "$userID" | sed -e "s|ssh://||") + echo -n "$host "; gpgkey2ssh "$keyID" | sed -e "s/COMMENT/MonkeySphere${DATE}/" + fi +} + +# userid and key policy checking +# the following checks policy on the returned keys +# - checks that full key has appropriate valididy (u|f) +# - checks key has specified capability (REQUIRED_KEY_CAPABILITY) +# - checks that particular desired user id has appropriate validity +# see /usr/share/doc/gnupg/DETAILS.gz +# expects global variable: "mode" +process_user_id() { + local userID + local cacheDir + local requiredPubCapability + local gpgOut + local line + local type + local validity + local keyid + local uidfpr + local capability + local keyOK + local pubKeyID + local uidOK + local keyIDs + local userIDHash + local keyID + + userID="$1" + cacheDir="$2" + + requiredPubCapability=$(echo "$REQUIRED_KEY_CAPABILITY" | tr "[:lower:]" "[:upper:]") + + # fetch keys from keyserver, return 1 if none found + gpg_fetch_keys "$userID" || return 1 + + # output gpg info for (exact) userid and store + gpgOut=$(gpg --fixed-list-mode --list-key --with-colons \ + ="$userID" 2> /dev/null) + + # return 1 if there only "tru" lines are output from gpg + if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then + return 1 + fi + + # loop over all lines in the gpg output and process. + # need to do it this way (as opposed to "while read...") so that + # variables set in loop will be visible outside of loop + for line in $(seq 1 $(echo "$gpgOut" | wc -l)) ; do + + # read the contents of the line + type=$(echo "$gpgOut" | cutline "$line" | cut -d: -f1) + validity=$(echo "$gpgOut" | cutline "$line" | cut -d: -f2) + keyid=$(echo "$gpgOut" | cutline "$line" | cut -d: -f5) + uidfpr=$(echo "$gpgOut" | cutline "$line" | cut -d: -f10) + capability=$(echo "$gpgOut" | cutline "$line" | cut -d: -f12) + + # process based on record type + case $type in + 'pub') # primary keys + # new key, wipe the slate + keyOK= + pubKeyID= + uidOK= + keyIDs= + + pubKeyID="$keyid" + + # check primary key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + loge " unacceptable primary key validity ($validity)." + continue + fi + # check capability is not Disabled... + if check_capability "$capability" 'D' ; then + loge " key disabled." + continue + fi + # check overall key capability + # must be Encryption and Authentication + if ! check_capability "$capability" $requiredPubCapability ; then + loge " unacceptable primary key capability ($capability)." + continue + fi + + # mark if primary key is acceptable + keyOK=true + + # add primary key ID to key list if it has required capability + if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then + keyIDs[${#keyIDs[*]}]="$keyid" + fi + ;; + 'uid') # user ids + # check key ok and we have key fingerprint + if [ -z "$keyOK" ] ; then + continue + fi + # check key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + # check the uid matches + if [ "$(unescape "$uidfpr")" != "$userID" ] ; then + continue + fi + + # mark if uid acceptable + uidOK=true + ;; + 'sub') # sub keys + # add sub key ID to key list if it has required capability + if check_capability "$capability" $REQUIRED_KEY_CAPABILITY ; then + keyIDs[${#keyIDs[*]}]="$keyid" + fi + ;; + esac + done + + # hash userid for cache file name + userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }') + + # touch/clear key cache file + # (will be left empty if there are noacceptable keys) + > "$cacheDir"/"$userIDHash"."$pubKeyID" + + # for each acceptable key, write an ssh key line to the + # key cache file + if [ "$keyOK" -a "$uidOK" -a "${keyIDs[*]}" ] ; then + for keyID in ${keyIDs[@]} ; do + # export the key with gpg2ssh + # FIXME: needs to apply extra options for authorized_keys + # lines if specified + gpg2ssh_tmp "$mode" "$keyID" "$userID" >> "$cacheDir"/"$userIDHash"."$pubKeyID" + + # hash the cache file if specified + if [ "$mode" = 'known_hosts' -a "$HASH_KNOWN_HOSTS" ] ; then + ssh-keygen -H -f "$cacheDir"/"$userIDHash"."$pubKeyID" > /dev/null 2>&1 + rm "$cacheDir"/"$userIDHash"."$pubKeyID".old + fi + done + fi + + # echo the path to the key cache file + echo "$cacheDir"/"$userIDHash"."$pubKeyID" +} + +# process a host for addition to a known_host file +process_host() { + local host + local cacheDir + local hostKeyCachePath + + host="$1" + cacheDir="$2" + + log "processing host: '$host'" + + hostKeyCachePath=$(process_user_id "ssh://${host}" "$cacheDir") + if [ $? = 0 ] ; then + ssh-keygen -R "$host" -f "$USER_KNOWN_HOSTS" + cat "$hostKeyCachePath" >> "$USER_KNOWN_HOSTS" + fi +} + +# process known_hosts file +# go through line-by-line, extract each host, and process with the +# host processing function +process_known_hosts() { + local cacheDir + local userID + + cacheDir="$1" + + # take all the hosts from the known_hosts file (first field), + # grep out all the hashed hosts (lines starting with '|') + cut -d ' ' -f 1 "$USER_KNOWN_HOSTS" | \ + grep -v '^|.*$' | \ + while IFS=, read -r -a hosts ; do + # process each host + for host in ${hosts[*]} ; do + process_host "$host" "$cacheDir" + done + done +} + +# process an authorized_*_ids file +# go through line-by-line, extract each userid, and process +process_authorized_ids() { + local authorizedIDsFile + local cacheDir + local userID + local userKeyCachePath + + authorizedIDsFile="$1" + cacheDir="$2" + + # clean out keys file and remake keys directory + rm -rf "$cacheDir" + mkdir -p "$cacheDir" + + # loop through all user ids in file + # FIXME: needs to handle extra options if necessary + cat "$authorizedIDsFile" | meat | \ + while read -r userID ; do + # process the userid + log "processing userid: '$userID'" + userKeyCachePath=$(process_user_id "$userID" "$cacheDir") + if [ -s "$userKeyCachePath" ] ; then + loge " acceptable key/uid found." + fi + done +} + +######################################################################## +# MAIN +######################################################################## + +if [ -z "$1" ] ; then + usage + exit 1 +fi + +# mode given in first variable +mode="$1" +shift 1 + +# check user +if ! id -u "$USER" > /dev/null 2>&1 ; then + failure "invalid user '$USER'." +fi + +# set user home directory +HOME=$(getent passwd "$USER" | cut -d: -f6) + +# set ms home directory +MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere} + +# load configuration file +MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} +[ -e "$MS_CONF" ] && . "$MS_CONF" + +# set config variable defaults +STAGING_AREA=${STAGING_AREA:-"$MS_HOME"} +AUTHORIZED_USER_IDS=${AUTHORIZED_USER_IDS:-"$MS_HOME"/authorized_user_ids} +GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg} +KEYSERVER=${KEYSERVER:-subkeys.pgp.net} +REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-"e a"} +USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"$HOME"/.ssh/authorized_keys} +USER_KNOWN_HOSTS=${USER_KNOWN_HOSTS:-"$HOME"/.ssh/known_hosts} +HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-} + +# export USER and GNUPGHOME variables, since they are used by gpg +export USER +export GNUPGHOME + +# stagging locations +hostKeysCacheDir="$STAGING_AREA"/host_keys +userKeysCacheDir="$STAGING_AREA"/user_keys +msKnownHosts="$STAGING_AREA"/known_hosts +msAuthorizedKeys="$STAGING_AREA"/authorized_keys + +# make sure gpg home exists with proper permissions +mkdir -p -m 0700 "$GNUPGHOME" + +## KNOWN_HOST MODE +if [ "$mode" = 'known_hosts' -o "$mode" = 'k' ] ; then + mode='known_hosts' + + cacheDir="$hostKeysCacheDir" + + log "user '$USER': monkeysphere known_hosts processing" + + # touch the known_hosts file to make sure it exists + touch "$USER_KNOWN_HOSTS" + + # if hosts are specified on the command line, process just + # those hosts + if [ "$1" ] ; then + for host ; do + process_host "$host" "$cacheDir" + done + + # otherwise, if no hosts are specified, process the user + # known_hosts file + else + if [ ! -s "$USER_KNOWN_HOSTS" ] ; then + failure "known_hosts file '$USER_KNOWN_HOSTS' is empty." + fi + process_known_hosts "$cacheDir" + fi + +## AUTHORIZED_KEYS MODE +elif [ "$mode" = 'authorized_keys' -o "$mode" = 'a' ] ; then + mode='authorized_keys' + + cacheDir="$userKeysCacheDir" + + # check auth ids file + if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then + log "authorized_user_ids file is empty or does not exist." + exit + fi + + log "user '$USER': monkeysphere authorized_keys processing" + + # if userids are specified on the command line, process just + # those userids + if [ "$1" ] ; then + for userID ; do + if ! grep -q "$userID" "$AUTHORIZED_USER_IDS" ; then + log "userid '$userID' not in authorized_user_ids file." + continue + fi + log "processing user id: '$userID'" + process_user_id "$userID" "$cacheDir" > /dev/null + done + + # otherwise, if no userids are specified, process the entire + # authorized_user_ids file + else + process_authorized_ids "$AUTHORIZED_USER_IDS" "$cacheDir" + fi + + # write output key file + log "writing monkeysphere authorized_keys file... " + touch "$msAuthorizedKeys" + if [ "$(ls "$cacheDir")" ] ; then + log -n "adding gpg keys... " + cat "$cacheDir"/* > "$msAuthorizedKeys" + echo "done." + else + log "no gpg keys to add." + fi + if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then + if [ -s "$USER_CONTROLLED_AUTHORIZED_KEYS" ] ; then + log -n "adding user authorized_keys file... " + cat "$USER_CONTROLLED_AUTHORIZED_KEYS" >> "$msAuthorizedKeys" + echo "done." + fi + fi + log "monkeysphere authorized_keys file generated:" + log "$msAuthorizedKeys" + +else + failure "unknown command '$mode'." +fi