diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/common | 566 | ||||
-rw-r--r-- | src/keytrans/Makefile | 12 | ||||
-rw-r--r-- | src/keytrans/gnutls-helpers.c | 446 | ||||
-rw-r--r-- | src/keytrans/gnutls-helpers.h | 78 | ||||
-rw-r--r-- | src/keytrans/openpgp2ssh.c | 437 | ||||
-rwxr-xr-x | src/monkeysphere | 214 | ||||
-rwxr-xr-x | src/monkeysphere-server | 271 | ||||
-rwxr-xr-x | src/monkeysphere-ssh-proxycommand | 56 | ||||
-rwxr-xr-x | src/seckey2sshagent | 25 |
9 files changed, 2105 insertions, 0 deletions
diff --git a/src/common b/src/common new file mode 100644 index 0000000..7a90453 --- /dev/null +++ b/src/common @@ -0,0 +1,566 @@ +# -*-shell-script-*- + +# Shared sh functions for the monkeysphere +# +# Written by +# Jameson Rollins <jrollins@fifthhorseman.net> +# +# 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 + +######################################################################## +### COMMON VARIABLES + +# managed directories +ETC="/etc/monkeysphere" +export ETC +CACHE="/var/cache/monkeysphere" +export CACHE + +######################################################################## +### UTILITY FUNCTIONS + +failure() { + echo "$1" >&2 + exit ${2:-'1'} +} + +# write output to stderr +log() { + echo -n "ms: " 1>&2 + echo "$@" 1>&2 +} + +loge() { + 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 +} + +# check that characters are in a string (in an AND fashion). +# used for checking key capability +# check_capability capability a [b...] +check_capability() { + local usage + local capcheck + + usage="$1" + shift 1 + + for capcheck ; do + if echo "$usage" | 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/:/' +} + +# remove all lines with specified string from specified file +remove_file_line() { + local file + local string + + file="$1" + string="$2" + + if [ "$file" -a "$string" ] ; then + grep -v "$string" "$file" | sponge "$file" + fi +} + +### CONVERTION UTILITIES + +# output the ssh key for a given key ID +gpg2ssh() { + local keyID + + #keyID="$1" #TMP + # only use last 16 characters until openpgp2ssh can take all 40 #TMP + keyID=$(echo "$1" | cut -c 25-) #TMP + + gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null +} + +# output known_hosts line from ssh key +ssh2known_hosts() { + local host + local key + + host="$1" + key="$2" + + echo -n "$host " + echo -n "$key" | tr -d '\n' + echo " MonkeySphere${DATE}" +} + +# output authorized_keys line from ssh key +ssh2authorized_keys() { + local userID + local key + + userID="$1" + key="$2" + + echo -n "$key" | tr -d '\n' + echo " MonkeySphere${DATE}: ${userID}" +} + +# convert key from gpg to ssh known_hosts format +gpg2known_hosts() { + local host + local keyID + + host="$1" + keyID="$2" + + # 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}$' + echo -n "$host " + gpg2ssh "$keyID" | tr -d '\n' + echo " MonkeySphere${DATE}" +} + +# convert key from gpg to ssh authorized_keys format +gpg2authorized_keys() { + local userID + local keyID + + userID="$1" + keyID="$2" + + # 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}$' + gpg2ssh "$keyID" | tr -d '\n' + echo " MonkeySphere${DATE}: ${userID}" +} + +### GPG UTILITIES + +# retrieve all keys with given user id from keyserver +# FIXME: need to figure out how to retrieve all matching keys +# (not just first N (5 in this case)) +gpg_fetch_userid() { + local userID + + userID="$1" + + log -n " checking keyserver $KEYSERVER... " + echo 1,2,3,4,5 | \ + gpg --quiet --batch --with-colons \ + --command-fd 0 --keyserver "$KEYSERVER" \ + --search ="$userID" > /dev/null 2>&1 + loge "done." +} + +# get the full fingerprint of a key ID +get_key_fingerprint() { + local keyID + + keyID="$1" + + gpg --list-key --with-colons --fixed-list-mode \ + --with-fingerprint "$keyID" | grep "$keyID" | \ + grep '^fpr:' | cut -d: -f10 +} + +######################################################################## +### PROCESSING FUNCTIONS + +# 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 requested user ID has appropriate validity +# (see /usr/share/doc/gnupg/DETAILS.gz) +# output is one line for every found key, in the following format: +# +# flag fingerprint +# +# "flag" is an acceptability flag, 0 = ok, 1 = bad +# "fingerprint" is the fingerprint of the key +# +# expects global variable: "MODE" +process_user_id() { + local userID + local requiredCapability + local requiredPubCapability + local gpgOut + local type + local validity + local keyid + local uidfpr + local usage + local keyOK + local uidOK + local lastKey + local lastKeyOK + local fingerprint + + userID="$1" + + # set the required key capability based on the mode + if [ "$MODE" = 'known_hosts' ] ; then + requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY" + elif [ "$MODE" = 'authorized_keys' ] ; then + requiredCapability="$REQUIRED_USER_KEY_CAPABILITY" + fi + requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]") + + # if CHECK_KEYSERVER variable set, check the keyserver + # for the user ID + if [ "$CHECK_KEYSERVER" = "true" ] ; then + gpg_fetch_userid "$userID" + fi + + # output gpg info for (exact) userid and store + gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \ + --with-fingerprint --with-fingerprint \ + ="$userID" 2>/dev/null) + + # if the gpg query return code is not 0, return 1 + if [ "$?" -ne 0 ] ; then + log " - 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 + echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \ + while IFS=: read -r type validity keyid uidfpr usage ; do + # process based on record type + case $type in + 'pub') # primary keys + # new key, wipe the slate + keyOK= + uidOK= + lastKey=pub + lastKeyOK= + fingerprint= + + log " primary key found: $keyid" + + # if overall key is not valid, skip + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + log " - unacceptable primary key validity ($validity)." + continue + fi + # if overall key is disabled, skip + if check_capability "$usage" 'D' ; then + log " - key disabled." + continue + fi + # if overall key capability is not ok, skip + if ! check_capability "$usage" $requiredPubCapability ; then + log " - unacceptable primary key capability ($usage)." + continue + fi + + # mark overall key as ok + keyOK=true + + # mark primary key as ok if capability is ok + if check_capability "$usage" $requiredCapability ; then + lastKeyOK=true + fi + ;; + 'uid') # user ids + # if an acceptable user ID was already found, skip + if [ "$uidOK" ] ; then + continue + fi + # if the user ID does not match, skip + if [ "$(unescape "$uidfpr")" != "$userID" ] ; then + continue + fi + # if the user ID validity is not ok, skip + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + + # mark user ID acceptable + uidOK=true + + # output a line for the primary key + # 0 = ok, 1 = bad + if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then + log " * acceptable key found." + echo 0 "$fingerprint" + else + echo 1 "$fingerprint" + fi + ;; + 'sub') # sub keys + # unset acceptability of last key + lastKey=sub + lastKeyOK= + fingerprint= + + # if sub key validity is not ok, skip + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + # if sub key capability is not ok, skip + if ! check_capability "$usage" $requiredCapability ; then + continue + fi + + # mark sub key as ok + lastKeyOK=true + ;; + 'fpr') # key fingerprint + fingerprint="$uidfpr" + + # if the last key was the pub key, skip + if [ "$lastKey" = pub ] ; then + continue + fi + + # output a line for the last subkey + # 0 = ok, 1 = bad + if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then + log " * acceptable key found." + echo 0 "$fingerprint" + else + echo 1 "$fingerprint" + fi + ;; + esac + done +} + +# update the cache for userid, and prompt to add file to +# authorized_user_ids file if the userid is found in gpg +# and not already in file. +update_userid() { + local userID + + userID="$1" + + log "processing userid: '$userID'" + + # process the user ID to pull it from keyserver + process_user_id "$userID" | grep -q "^0 " + + # check if user ID is in the authorized_user_ids file + if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then + read -p "user ID not currently authorized. authorize? [Y|n]: " OK; OK=${OK:=Y} + if [ ${OK/y/Y} = 'Y' ] ; then + # add if specified + log -n " adding user ID to authorized_user_ids file... " + echo "$userID" >> "$AUTHORIZED_USER_IDS" + loge "done." + else + # else do nothing + log " authorized_user_ids file untouched." + fi + fi +} + +# remove a userid from the authorized_user_ids file +remove_userid() { + local userID + + userID="$1" + + log "processing userid: '$userID'" + + # check if user ID is in the authorized_user_ids file + if ! grep -q "^${userID}\$" "$AUTHORIZED_USER_IDS" ; then + log " user ID not currently authorized." + return 1 + fi + + # remove user ID from file + log -n " removing user ID '$userID'... " + remove_file_line "$AUTHORIZED_USER_IDS" "^${userID}$" + loge "done." +} + +# process a host in known_host file +process_host_known_hosts() { + local host + local userID + local ok + local keyid + local tmpfile + + host="$1" + userID="ssh://${host}" + + log "processing host: $host" + + process_user_id "ssh://${host}" | \ + while read -r ok keyid ; do + sshKey=$(gpg2ssh "$keyid") + # remove the old host key line + remove_file_line "$KNOWN_HOSTS" "$sshKey" + # if key OK, add new host line + if [ "$ok" -eq '0' ] ; then + # hash if specified + if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then + # FIXME: this is really hackish cause ssh-keygen won't + # hash from stdin to stdout + tmpfile=$(mktemp) + ssh2known_hosts "$host" "$sshKey" > "$tmpfile" + ssh-keygen -H -f "$tmpfile" 2> /dev/null + cat "$tmpfile" >> "$KNOWN_HOSTS" + rm -f "$tmpfile" "${tmpfile}.old" + else + ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS" + fi + fi + done +} + +# process a uid in an authorized_keys file +process_uid_authorized_keys() { + local userID + local ok + local keyid + + userID="$1" + + log "processing user ID: $userID" + + process_user_id "$userID" | \ + while read -r ok keyid ; do + sshKey=$(gpg2ssh "$keyid") + # remove the old host key line + remove_file_line "$AUTHORIZED_KEYS" "$sshKey" + # if key OK, add new host line + if [ "$ok" -eq '0' ] ; then + ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS" + fi + done +} + +# process known_hosts file +# go through line-by-line, extract each host, and process with the +# host processing function +process_known_hosts() { + local hosts + local host + + # take all the hosts from the known_hosts file (first field), + # grep out all the hashed hosts (lines starting with '|')... + cat "$KNOWN_HOSTS" | meat | \ + cut -d ' ' -f 1 | grep -v '^|.*$' | \ + while IFS=, read -r -a hosts ; do + # and process each host + for host in ${hosts[*]} ; do + process_host_known_hosts "$host" + done + done +} + +# process an authorized_user_ids file for authorized_keys +process_authorized_user_ids() { + local userid + + cat "$AUTHORIZED_USER_IDS" | meat | \ + while read -r userid ; do + process_uid_authorized_keys "$userid" + done +} + +# EXPERIMENTAL (unused) process userids found in authorized_keys file +# go through line-by-line, extract monkeysphere userids from comment +# fields, and process each userid +# NOT WORKING +process_authorized_keys() { + local authorizedKeys + local userID + + authorizedKeys="$1" + + # 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" | meat | \ + 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" | egrep -v -q '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}:' ; then + continue + fi + userID=$(echo "$comment" | awk "{ print $2 }") + if [ -z "$userID" ] ; then + continue + fi + + # process the userid + log "processing userid: '$userID'" + process_user_id "$userID" > /dev/null + done +} + +################################################## +### GPG HELPER FUNCTIONS + +# retrieve key from web of trust, and set owner trust to "full" +# if key is found. +trust_key() { + # get the key from the key server + if ! gpg --keyserver "$KEYSERVER" --recv-key "$keyID" ; then + log "could not retrieve key '$keyID'" + return 1 + fi + + # get key fingerprint + fingerprint=$(get_key_fingerprint "$keyID") + + # attach a "non-exportable" signature to the key + # this is required for the key to have any validity at all + # the 'y's on stdin indicates "yes, i really want to sign" + echo -e 'y\ny' | gpg --lsign-key --command-fd 0 "$fingerprint" + + # import "full" trust for fingerprint into gpg + echo ${fingerprint}:5: | gpg --import-ownertrust + if [ $? = 0 ] ; then + log "owner trust updated." + else + failure "there was a problem changing owner trust." + fi +} + +# publish server key to keyserver +publish_server_key() { + read -p "really publish key to $KEYSERVER? [y|N]: " OK; OK=${OK:=N} + if [ ${OK/y/Y} != 'Y' ] ; then + failure "aborting." + fi + + # publish host key + # FIXME: need to figure out better way to identify host key + # dummy command so as not to publish fakes keys during testing + # eventually: + #gpg --send-keys --keyserver "$KEYSERVER" $(hostname -f) + echo "NOT PUBLISHED: gpg --send-keys --keyserver $KEYSERVER $(hostname -f)" +} diff --git a/src/keytrans/Makefile b/src/keytrans/Makefile new file mode 100644 index 0000000..79602ef --- /dev/null +++ b/src/keytrans/Makefile @@ -0,0 +1,12 @@ +all: openpgp2ssh + +openpgp2ssh: openpgp2ssh.c gnutls-helpers.o + gcc -g -Wall --pedantic -o openpgp2ssh openpgp2ssh.c `libgnutls-config --libs --cflags` gnutls-helpers.o + +%.o: %.c + gcc -g -Wall --pedantic -o $@ -c $< + +clean: + rm -f openpgp2ssh *.o + +.PHONY: clean all diff --git a/src/keytrans/gnutls-helpers.c b/src/keytrans/gnutls-helpers.c new file mode 100644 index 0000000..5b4c46a --- /dev/null +++ b/src/keytrans/gnutls-helpers.c @@ -0,0 +1,446 @@ +/* Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net> */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + +#include "gnutls-helpers.h" +/* for htonl() */ +#include <arpa/inet.h> + +/* for setlocale() */ +#include <locale.h> + +/* for isalnum() */ +#include <ctype.h> + +/* for exit() */ +#include <unistd.h> + +#include <assert.h> + +/* higher levels allow more frivolous error messages through. + this is set with the MONKEYSPHERE_DEBUG variable */ +static int loglevel = 0; + +void err(int level, const char* fmt, ...) { + va_list ap; + if (level > loglevel) + return; + 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) +{ + assert(sizeof(out) >= 2*sizeof(keyid)); + hex_print_data((char*)out, (const char*)keyid, sizeof(keyid)); +} + +/* you must have twice as many bytes in the out buffer as in the in buffer */ +void hex_print_data(char* out, const char* in, size_t incount) +{ + static const char hex[16] = "0123456789ABCDEF"; + unsigned int inix = 0, outix = 0; + + while (inix < incount) { + out[outix] = hex[(in[inix] >> 4) & 0x0f]; + out[outix + 1] = hex[in[inix] & 0x0f]; + inix++; + outix += 2; + } +} + +unsigned char hex2bin(unsigned char x) { + if ((x >= '0') && (x <= '9')) + return x - '0'; + if ((x >= 'A') && (x <= 'F')) + return 10 + x - 'A'; + if ((x >= 'a') && (x <= 'f')) + return 10 + x - 'a'; + return 0xff; +} + +void collapse_printable_keyid(gnutls_openpgp_keyid_t out, printable_keyid in) { + unsigned int pkix = 0, outkix = 0; + + while (pkix < sizeof(printable_keyid)) { + unsigned hi = hex2bin(in[pkix]); + unsigned lo = hex2bin(in[pkix + 1]); + if (hi == 0xff) { + err(0, "character '%c' is not a hex char\n", in[pkix]); + exit(1); + } + if (lo == 0xff) { + err(0, "character '%c' is not a hex char\n", in[pkix + 1]); + exit(1); + } + out[outkix] = lo | (hi << 4); + + pkix += 2; + outkix++; + } +} + +int convert_string_to_keyid(gnutls_openpgp_keyid_t out, const char* str) { + printable_keyid p; + int ret; + + ret = convert_string_to_printable_keyid(p, str); + if (ret == 0) + collapse_printable_keyid(out, p); + return ret; +} +int convert_string_to_printable_keyid(printable_keyid pkeyid, const char* str) { + int arglen, x; + arglen = 0; + x = 0; + while ((arglen <= sizeof(printable_keyid)) && + (str[x] != '\0')) { + if (isxdigit(str[x])) { + if (arglen == sizeof(printable_keyid)) { + err(0, "There are more than %d hex digits in the keyid '%s'\n", sizeof(printable_keyid), str); + return 1; + } + pkeyid[arglen] = str[x]; + arglen++; + } + x++; + } + + if (arglen != sizeof(printable_keyid)) { + err(0, "Keyid '%s' is not %d hex digits in length\n", str, sizeof(printable_keyid)); + return 1; + } + return 0; +} + + + +int init_gnutls() { + const char* version = NULL; + const char* debug_string = NULL; + int ret; + + if (debug_string = getenv("MONKEYSPHERE_DEBUG"), debug_string) { + loglevel = atoi(debug_string); + } + + if (ret = gnutls_global_init(), ret) { + err(0, "Failed to do gnutls_global_init() (error: %d)\n", ret); + return 1; + } + + version = gnutls_check_version(NULL); + + if (version) + err(1, "gnutls version: %s\n", version); + else { + err(0, "no gnutls version found!\n"); + return 1; + } + + gnutls_global_set_log_function(logfunc); + + gnutls_global_set_log_level(loglevel); + err(1, "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(0,"a is larger\n"); + return 1; + } + if (a->size < b->size) { + err(0,"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(0,"out of memory!\n"); + return -1; + } + d->size = bufsize; + } else { + bufsize = d->size; + } + f = fdopen(fd, "r"); + if (NULL == f) { + err(0,"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(0,"out of memory!\n"); + return -1; + }; + d->size = bufsize; + } + len += fread(d->data + len, 1, bufsize - len, f); + /* err(0,"read %d bytes\n", len); */ + } + if (ferror(f)) { + err(0,"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(0,"failed to stat '%s'\n", fname); + return -1; + } + + c = gnutls_realloc(d->data, sbuf.st_size); + if (NULL == c) { + err(0,"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(0,"failed to open '%s' for reading\n", fname); + return -1; + } + + x = fread(d->data, d->size, 1, file); + if (x != 1) { + err(0,"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(0,"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(0,"failed to write size of datum.\n"); + return -2; + } + if (looks_negative) { + if (write(fd, &zero, 1) != 1) { + err(0,"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(0,"bad pointer passed to create_writing_pipe()\n"); + return -1; + } + + if (ret = pipe(p), ret == -1) { + err(0,"failed to create a pipe (error: %d \"%s\")\n", errno, strerror(errno)); + return -1; + } + + *pid = fork(); + if (*pid == -1) { + err(0,"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(0,"Failed to transfer reading file descriptor to stdin (error: %d \"%s\")\n", errno, strerror(errno)); + exit(1); + } + execv(path, argv); + err(0,"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(0,"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(0,"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(0,"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(0,"invalid character in domain name: %c\n", *userid); + goto fail; + } + } + /* ensure that the last character is valid: */ + if (!isalnum(*(userid - 1))) { + err(0,"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/keytrans/gnutls-helpers.h b/src/keytrans/gnutls-helpers.h new file mode 100644 index 0000000..f196456 --- /dev/null +++ b/src/keytrans/gnutls-helpers.h @@ -0,0 +1,78 @@ +/* Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net> */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + + +#include <gnutls/gnutls.h> +#include <gnutls/openpgp.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdarg.h> + +/* 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(int level, 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); +void collapse_printable_keyid(gnutls_openpgp_keyid_t out, printable_keyid in); +int convert_string_to_keyid(gnutls_openpgp_keyid_t out, const char* str); +int convert_string_to_printable_keyid(printable_keyid out, const char* str); + +/* you must have twice as many bytes in the out buffer as in the in buffer */ +void hex_print_data(char* out, const char* in, size_t incount); + +/* 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/keytrans/openpgp2ssh.c b/src/keytrans/openpgp2ssh.c new file mode 100644 index 0000000..511af71 --- /dev/null +++ b/src/keytrans/openpgp2ssh.c @@ -0,0 +1,437 @@ +#include "gnutls-helpers.h" + +#include <gnutls/openpgp.h> +#include <gnutls/x509.h> + +/* for waitpid() */ +#include <sys/types.h> +#include <sys/wait.h> + +/* + Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net> + Date: 2008-06-12 13:47:41-0400 + License: GPL v3 or later + + monkeysphere key translator: execute this with an OpenPGP key on + stdin, (please indicate the specific keyid that you want as the + first argument if there are subkeys). At the moment, only public + keys and passphraseless secret keys work. + + For secret keys, 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 | openpgp2ssh $KEYID | ssh-add -c /dev/stdin + + For public keys, it will spit out a single line of text that can + (with some massaging) be used in an openssh known_hosts or + authorized_keys file. For example: + + echo server.example.org $(gpg --export $KEYID | openpgp2ssh $KEYID) >> ~/.ssh/known_hosts + + Requirements: I've only built this so far with GnuTLS v2.3.x. + GnuTLS 2.2.x does not contain the appropriate functionality. + + */ + + +/* FIXME: keyid should be const as well */ +int convert_private_pgp_to_x509(gnutls_x509_privkey_t* output, const gnutls_openpgp_privkey_t* pgp_privkey, gnutls_openpgp_keyid_t* keyid) { + gnutls_datum_t m, e, d, p, q, u, g, y, x; + gnutls_pk_algorithm_t pgp_algo; + unsigned int pgp_bits; + int ret; + gnutls_openpgp_keyid_t curkeyid; + int subkeyidx; + int subkeycount; + int found = 0; + + 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); + + subkeycount = gnutls_openpgp_privkey_get_subkey_count(*pgp_privkey); + if (subkeycount < 0) { + err(0,"Could not determine subkey count (got value %d)\n", subkeycount); + return 1; + } + + if ((keyid == NULL) && + (subkeycount > 0)) { + err(0,"No keyid passed in, but there were %d keys to choose from\n", subkeycount + 1); + return 1; + } + + if (keyid != NULL) { + ret = gnutls_openpgp_privkey_get_key_id(*pgp_privkey, curkeyid); + if (ret) { + err(0,"Could not get keyid (error: %d)\n", ret); + return 1; + } + } + if ((keyid == NULL) || (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0)) { + /* we want to export the primary key: */ + err(0,"exporting primary key\n"); + + /* FIXME: this is almost identical to the block below for subkeys. + This clumsiness seems inherent in the gnutls OpenPGP API, + though. ugh. */ + pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(*pgp_privkey, &pgp_bits); + if (pgp_algo < 0) { + err(0, "failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo); + return 1; + } + if (pgp_algo == GNUTLS_PK_RSA) { + err(0,"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(0, "failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + + } else if (pgp_algo == GNUTLS_PK_DSA) { + err(0,"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(0,"failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + } + found = 1; + } else { + /* lets trawl through the subkeys until we find the one we want: */ + for (subkeyidx = 0; (subkeyidx < subkeycount) && !found; subkeyidx++) { + ret = gnutls_openpgp_privkey_get_subkey_id(*pgp_privkey, subkeyidx, curkeyid); + if (ret) { + err(0,"Could not get keyid of subkey with index %d (error: %d)\n", subkeyidx, ret); + return 1; + } + if (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0) { + err(0,"exporting subkey index %d\n", subkeyidx); + + /* FIXME: this is almost identical to the block above for the + primary key. */ + pgp_algo = gnutls_openpgp_privkey_get_subkey_pk_algorithm(*pgp_privkey, subkeyidx, &pgp_bits); + if (pgp_algo < 0) { + err(0,"failed to get the algorithm of the OpenPGP public key (error: %d)\n", pgp_algo); + return pgp_algo; + } else if (pgp_algo == GNUTLS_PK_RSA) { + err(0,"OpenPGP RSA key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_subkey_rsa_raw(*pgp_privkey, subkeyidx, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (pgp_algo == GNUTLS_PK_DSA) { + err(0,"OpenPGP DSA Key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_subkey_dsa_raw(*pgp_privkey, subkeyidx, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + } + found = 1; + } + } + } + + if (!found) { + err(0,"Could not find key in input\n"); + return 1; + } + + if (pgp_algo == GNUTLS_PK_RSA) { + ret = gnutls_x509_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err(0, "failed to import RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (pgp_algo == GNUTLS_PK_DSA) { + ret = gnutls_x509_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to import DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err(0,"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(0,"failed to fix up the private key in X.509 format (error: %d)\n", ret); + return 1; + } + + return 0; +} + +/* FIXME: keyid should be const also */ +int emit_public_openssh_from_pgp(const gnutls_openpgp_crt_t* pgp_crt, gnutls_openpgp_keyid_t* keyid) { + gnutls_openpgp_keyid_t curkeyid; + int ret; + int subkeyidx; + int subkeycount; + int found = 0; + gnutls_datum_t m, e, p, q, g, y, algolabel; + unsigned int bits; + gnutls_pk_algorithm_t algo; + const gnutls_datum_t* all[5]; + const char* algoname; + int mpicount; + /* output_data must be at least 2 chars longer than the maximum possible + algorithm name: */ + char output_data[20]; + + /* variables for the output conversion: */ + int pipestatus; + int pipefd, child_pid; + char* const b64args[] = {"/usr/bin/base64", "--wrap=0", NULL}; + + init_datum(&m); + init_datum(&e); + init_datum(&p); + init_datum(&q); + init_datum(&g); + init_datum(&algolabel); + + + /* figure out if we've got the right thing: */ + subkeycount = gnutls_openpgp_crt_get_subkey_count(*pgp_crt); + if (subkeycount < 0) { + err(0,"Could not determine subkey count (got value %d)\n", subkeycount); + return 1; + } + + if ((keyid == NULL) && + (subkeycount > 0)) { + err(0,"No keyid passed in, but there were %d keys to choose from\n", subkeycount + 1); + return 1; + } + + if (keyid != NULL) { + ret = gnutls_openpgp_crt_get_key_id(*pgp_crt, curkeyid); + if (ret) { + err(0,"Could not get keyid (error: %d)\n", ret); + return 1; + } + } + if ((keyid == NULL) || (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0)) { + /* we want to export the primary key: */ + err(0,"exporting primary key\n"); + + /* FIXME: this is almost identical to the block below for subkeys. + This clumsiness seems inherent in the gnutls OpenPGP API, + though. ugh. */ + algo = gnutls_openpgp_crt_get_pk_algorithm(*pgp_crt, &bits); + if (algo < 0) { + err(0,"failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + err(0,"OpenPGP RSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_rsa_raw(*pgp_crt, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export RSA certificate parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err(0,"OpenPGP DSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_dsa_raw(*pgp_crt, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export DSA certificate parameters (error: %d)\n", ret); + return 1; + } + } + found = 1; + + } else { + /* lets trawl through the subkeys until we find the one we want: */ + for (subkeyidx = 0; (subkeyidx < subkeycount) && !found; subkeyidx++) { + ret = gnutls_openpgp_crt_get_subkey_id(*pgp_crt, subkeyidx, curkeyid); + if (ret) { + err(0,"Could not get keyid of subkey with index %d (error: %d)\n", subkeyidx, ret); + return 1; + } + if (memcmp(*keyid, curkeyid, sizeof(gnutls_openpgp_keyid_t)) == 0) { + err(0,"exporting subkey index %d\n", subkeyidx); + + /* FIXME: this is almost identical to the block above for the + primary key. */ + algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(*pgp_crt, subkeyidx, &bits); + if (algo < 0) { + err(0,"failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + err(0,"OpenPGP RSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(*pgp_crt, subkeyidx, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export RSA certificate parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err(0,"OpenPGP DSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(*pgp_crt, subkeyidx, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err(0,"failed to export DSA certificate parameters (error: %d)\n", ret); + return 1; + } + } + found = 1; + + } + } + } + + if (!found) { + err(0,"Could not find key in input\n"); + return 1; + } + + /* if we made it this far, we've got MPIs, and we've got the + algorithm, so we just need to emit the info */ + 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(0,"Key algorithm was neither DSA nor RSA (it was %d). Can't deal. Sorry!\n", algo); + return 1; + } + + if (ret = datum_from_string(&algolabel, algoname), ret) { + err(0,"couldn't label string (error: %d)\n", ret); + return ret; + } + + snprintf(output_data, sizeof(output_data), "%s ", algoname); + + pipefd = create_writing_pipe(&child_pid, b64args[0], b64args); + if (pipefd < 0) { + err(0,"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(0,"was not able to write out RSA key data\n"); + return 1; + } + close(pipefd); + if (child_pid != waitpid(child_pid, &pipestatus, 0)) { + err(0,"could not wait for child process to return for some reason.\n"); + return 1; + } + if (pipestatus != 0) { + err(0,"base64 pipe died with return code %d\n", pipestatus); + return pipestatus; + } + + write(1, "\n", 1); + + return 0; +} + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_x509_privkey_t x509_privkey; + gnutls_openpgp_privkey_t pgp_privkey; + gnutls_openpgp_crt_t pgp_crt; + + char output_data[10240]; + size_t ods = sizeof(output_data); + + gnutls_openpgp_keyid_t keyid; + gnutls_openpgp_keyid_t* use_keyid; + + init_gnutls(); + + /* figure out what keyid we should be looking for: */ + use_keyid = NULL; + if (argv[1] != NULL) { + ret = convert_string_to_keyid(keyid, argv[1]); + if (ret != 0) + return ret; + use_keyid = &keyid; + } + + + init_datum(&data); + + /* slurp in the key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err(0,"didn't read file descriptor 0\n"); + return 1; + } + + + if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) { + err(0,"Failed to initialized OpenPGP private key (error: %d)\n", ret); + return 1; + } + /* check whether it's a private key or a public key, by trying them: */ + if ((gnutls_openpgp_privkey_import(pgp_privkey, &data, GNUTLS_OPENPGP_FMT_RAW, NULL, 0) == 0) || + (gnutls_openpgp_privkey_import(pgp_privkey, &data, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0) == 0)) { + /* we're dealing with a private key */ + err(0,"Translating private key\n"); + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err(0,"Failed to initialize X.509 private key for output (error: %d)\n", ret); + return 1; + } + + ret = convert_private_pgp_to_x509(&x509_privkey, &pgp_privkey, use_keyid); + + gnutls_openpgp_privkey_deinit(pgp_privkey); + if (ret) + return ret; + + ret = gnutls_x509_privkey_export (x509_privkey, + GNUTLS_X509_FMT_PEM, + output_data, + &ods); + if (ret == 0) { + write(1, output_data, ods); + } + gnutls_x509_privkey_deinit(x509_privkey); + + } else { + if (ret = gnutls_openpgp_crt_init(&pgp_crt), ret) { + err(0,"Failed to initialized OpenPGP certificate (error: %d)\n", ret); + return 1; + } + + if ((gnutls_openpgp_crt_import(pgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW) == 0) || + (gnutls_openpgp_crt_import(pgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64) == 0)) { + /* we're dealing with a public key */ + err(0,"Translating public key\n"); + + ret = emit_public_openssh_from_pgp(&pgp_crt, use_keyid); + + } else { + /* we have no idea what kind of key this is at all anyway! */ + err(0,"Input does contain any form of OpenPGP key I recognize.\n"); + return 1; + } + } + + gnutls_global_deinit(); + return 0; +} diff --git a/src/monkeysphere b/src/monkeysphere new file mode 100755 index 0000000..6853f58 --- /dev/null +++ b/src/monkeysphere @@ -0,0 +1,214 @@ +#!/bin/bash + +# monkeysphere: MonkeySphere client tool +# +# The monkeysphere scripts are written by: +# Jameson Rollins <jrollins@fifthhorseman.net> +# +# They are Copyright 2008, and are all released under the GPL, version 3 +# or later. + +######################################################################## +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 <<EOF +usage: $PGRM <subcommand> [args] +MonkeySphere client tool. + +subcommands: + update-known_hosts (k) [HOST]... update known_hosts file + update-userids (u) [USERID]... add/update user IDs + remove-userids (r) [USERID]... remove user IDs + update-authorized_keys (a) update authorized_keys file + gen-subkey (g) KEYID generate an 'a' capable subkey + help (h,?) this help + +EOF +} + +# generate a subkey with the 'a' usage flags set +# FIXME: this needs some tweaking to clean it up +gen_subkey(){ + local keyID + local gpgOut + local userID + + keyID="$1" + + gpgOut=$(gpg --quiet --fixed-list-mode --list-keys --with-colons \ + "$keyID" 2> /dev/null) + + # return 1 if there only "tru" lines are output from gpg + if [ -z "$(echo "$gpgOut" | grep -v '^tru:')" ] ; then + failure "Key ID '$keyID' not found." + fi + + # set subkey defaults + SUBKEY_TYPE=${SUBKEY_TYPE:-"RSA"} + #SUBKEY_LENGTH=${SUBKEY_LENGTH:-"2048"} + SUBKEY_USAGE=${SUBKEY_USAGE:-"auth"} + SUBKEY_EXPIRE=${SUBKEY_EXPIRE:-"0"} + cat <<EOF +Please specify how long the key should be valid. + 0 = key does not expire + <n> = key expires in n days + <n>w = key expires in n weeks + <n>m = key expires in n months + <n>y = key expires in n years +EOF + read -p "Key is valid for? ($SUBKEY_EXPIRE) " SUBKEY_EXPIRE; SUBKEY_EXPIRE=${SUBKEY_EXPIRE:-"0"} + + # generate the list of commands that will be passed to edit-key + editCommands=$(cat <<EOF +addkey +7 +S +E +A +Q +$SUBKEY_LENGTH +$SUBKEY_EXPIRE +save +EOF +) + + log "generating subkey..." + echo "$editCommands" | gpg --expert --command-fd 0 --edit-key "$keyID" + log "done." +} + +######################################################################## +# 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"} +CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"} +REQUIRED_HOST_KEY_CAPABILITY=${REQUIRED_HOST_KEY_CAPABILITY:-"e a"} +REQUIRED_USER_KEY_CAPABILITY=${REQUIRED_USER_KEY_CAPABILITY:-"a"} +KNOWN_HOSTS=${KNOWN_HOSTS:-"${HOME}/.ssh/known_hosts"} +AUTHORIZED_KEYS=${AUTHORIZED_KEYS:-"${HOME}/.ssh/authorized_keys"} +HASH_KNOWN_HOSTS=${HASH_KNOWN_HOSTS:-"true"} + +export GNUPGHOME + +# make sure gpg home exists with proper permissions +mkdir -p -m 0700 "$GNUPGHOME" + +# make sure the user monkeysphere home directory exists +mkdir -p -m 0700 "$MS_HOME" +touch "$AUTHORIZED_USER_IDS" +touch "$AUTHORIZED_KEYS" + +case $COMMAND in + 'update-known_hosts'|'update-known-hosts'|'k') + MODE='known_hosts' + + # touch the known_hosts file to make sure it exists + # ssh-keygen complains if it doesn't exist + touch "$KNOWN_HOSTS" + + # if hosts are specified on the command line, process just + # those hosts + if [ "$1" ] ; then + for host ; do + process_host_known_hosts "$host" + done + log "known_hosts file updated." + + # otherwise, if no hosts are specified, process every user + # in the user's known_hosts file + else + if [ ! -s "$KNOWN_HOSTS" ] ; then + failure "known_hosts file '$KNOWN_HOSTS' is empty." + fi + log "processing known_hosts file..." + process_known_hosts + log "known_hosts file updated." + fi + ;; + + 'update-userids'|'update-userid'|'u') + if [ -z "$1" ] ; then + failure "you must specify at least one userid." + fi + for userID ; do + update_userid "$userID" + done + log "Run the following to update your monkeysphere authorized_keys file:" + log "$PGRM update-authorized_keys" + ;; + + 'remove-userids'|'remove-userid'|'r') + if [ -z "$1" ] ; then + failure "you must specify at least one userid." + fi + for userID ; do + remove_userid "$userID" + done + log "Run the following to update your monkeysphere authorized_keys file:" + log "$PGRM update-authorized_keys" + ;; + + 'update-authorized_keys'|'update-authorized-keys'|'a') + MODE='authorized_keys' + + # fail if the authorized_user_ids file is empty + if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then + failure "$AUTHORIZED_USER_IDS is empty." + fi + + # process authorized_user_ids file + log "processing authorized_user_ids file..." + process_authorized_user_ids + log "authorized_keys file updated." + ;; + + 'gen-subkey'|'g') + keyID="$1" + if [ -z "$keyID" ] ; then + failure "You must specify the key ID of your primary key." + fi + gen_subkey "$keyID" + ;; + + 'help'|'h'|'?') + usage + ;; + + *) + failure "Unknown command: '$COMMAND' +Type '$PGRM help' for usage." + ;; +esac diff --git a/src/monkeysphere-server b/src/monkeysphere-server new file mode 100755 index 0000000..560d249 --- /dev/null +++ b/src/monkeysphere-server @@ -0,0 +1,271 @@ +#!/bin/bash + +# monkeysphere-server: MonkeySphere server admin tool +# +# The monkeysphere scripts are written by: +# Jameson Rollins <jrollins@fifthhorseman.net> +# +# They are Copyright 2008, and are all released under the GPL, version 3 +# or later. + +######################################################################## +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 <<EOF +usage: $PGRM <subcommand> [args] +MonkeySphere server admin tool. + +subcommands: + update-users (s) [USER]... update users authorized_keys files + gen-key (g) [HOSTNAME] generate gpg key for the server + publish-key (p) publish server key to keyserver + trust-keys (t) KEYID... mark keyids as trusted + update-user-userids (u) USER UID... add/update user IDs for a user + remove-user-userids (r) USER UID... remove user IDs for a user + help (h,?) this help + +EOF +} + +# generate server gpg key +gen_key() { + local hostName + + hostName=${1:-$(hostname --fqdn)} + + # set key defaults + KEY_TYPE=${KEY_TYPE:-"RSA"} + KEY_LENGTH=${KEY_LENGTH:-"2048"} + KEY_USAGE=${KEY_USAGE:-"auth,encrypt"} + cat <<EOF +Please specify how long the key should be valid. + 0 = key does not expire + <n> = key expires in n days + <n>w = key expires in n weeks + <n>m = key expires in n months + <n>y = key expires in n years +EOF + read -p "Key is valid for? ($EXPIRE) " EXPIRE; EXPIRE=${EXPIRE:-"0"} + + SERVICE=${SERVICE:-"ssh"} + USERID=${USERID:-"$SERVICE"://"$hostName"} + + # set key parameters + keyParameters=$(cat <<EOF +Key-Type: $KEY_TYPE +Key-Length: $KEY_LENGTH +Key-Usage: $KEY_USAGE +Name-Real: $USERID +Expire-Date: $EXPIRE +EOF +) + + # add the revoker field if requested + if [ "$REVOKER" ] ; then + keyParameters="${keyParameters}"$(cat <<EOF + +Revoker: 1:$REVOKER sensitive +EOF +) + fi + + echo "The following key parameters will be used:" + echo "$keyParameters" + + read -p "generate key? [Y|n]: " OK; OK=${OK:=Y} + if [ ${OK/y/Y} != 'Y' ] ; then + failure "aborting." + fi + + if gpg --list-key ="$USERID" > /dev/null 2>&1 ; then + failure "key for '$USERID' already exists" + fi + + # add commit command + keyParameters="${keyParameters}"$(cat <<EOF + +%commit +%echo done +EOF +) + + log -n "generating server key... " + echo "$keyParameters" | gpg --batch --gen-key + loge "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"} +CHECK_KEYSERVER=${CHECK_KEYSERVER:="true"} +REQUIRED_USER_KEY_CAPABILITY=${REQUIRED_USER_KEY_CAPABILITY:-"a"} +USER_CONTROLLED_AUTHORIZED_KEYS=${USER_CONTROLLED_AUTHORIZED_KEYS:-"%h/.ssh/authorized_keys"} + +export GNUPGHOME + +# make sure the monkeysphere home directory exists +mkdir -p "${MS_HOME}/authorized_user_ids" +# make sure gpg home exists with proper permissions +mkdir -p -m 0700 "$GNUPGHOME" +# make sure the authorized_keys directory exists +mkdir -p "${CACHE}/authorized_keys" + +case $COMMAND in + 'update-users'|'update-user'|'s') + if [ "$1" ] ; then + unames="$@" + else + unames=$(ls -1 "${MS_HOME}/authorized_user_ids") + fi + + for uname in $unames ; do + MODE="authorized_keys" + + log "----- user: $uname -----" + + # set variables for the user + AUTHORIZED_USER_IDS="${MS_HOME}/authorized_user_ids/${uname}" + # temporary authorized_keys file + AUTHORIZED_KEYS="${CACHE}/authorized_keys/${uname}.tmp" + + # make sure user's authorized_user_ids file exists + touch "$AUTHORIZED_USER_IDS" + # make sure the authorized_keys file exists and is clear + > "$AUTHORIZED_KEYS" + + # skip if the user's authorized_user_ids file is empty + if [ ! -s "$AUTHORIZED_USER_IDS" ] ; then + log "authorized_user_ids file for '$uname' is empty." + continue + fi + + # process authorized_user_ids file + log "processing authorized_user_ids file..." + process_authorized_user_ids + + # add user-controlled authorized_keys file path if specified + if [ "$USER_CONTROLLED_AUTHORIZED_KEYS" != '-' ] ; then + userHome=$(getent passwd "$uname" | cut -d: -f6) + userAuthorizedKeys=${USER_CONTROLLED_AUTHORIZED_KEYS/\%h/"$userHome"} + log -n "adding user's authorized_keys file... " + cat "$userAuthorizedKeys" >> "$AUTHORIZED_KEYS" + loge "done." + fi + + # move the temp authorized_keys file into place + mv -f "${CACHE}/authorized_keys/${uname}.tmp" "${CACHE}/authorized_keys/${uname}" + + log "authorized_keys file updated." + done + + log "----- done. -----" + ;; + + 'gen-key'|'g') + gen_key "$1" + ;; + + 'publish-key'|'p') + publish_server_key + ;; + + 'trust-keys'|'trust-key'|'t') + if [ -z "$1" ] ; then + failure "You must specify at least one key to trust." + fi + + # process key IDs + for keyID ; do + trust_key "$keyID" + done + ;; + + 'update-user-userids'|'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 user ID." + fi + + # set variables for the user + AUTHORIZED_USER_IDS="$MS_HOME"/authorized_user_ids/"$uname" + + # make sure user's authorized_user_ids file exists + touch "$AUTHORIZED_USER_IDS" + + # process the user IDs + for userID ; do + update_userid "$userID" + done + + log "Run the following to update user's authorized_keys file:" + log "$PGRM update-users $uname" + ;; + + 'remove-user-userids'|'remove-user-userid'|'r') + uname="$1" + shift + if [ -z "$uname" ] ; then + failure "You must specify user." + fi + if [ -z "$1" ] ; then + failure "You must specify at least one user ID." + fi + + # set variables for the user + AUTHORIZED_USER_IDS="$MS_HOME"/authorized_user_ids/"$uname" + + # make sure user's authorized_user_ids file exists + touch "$AUTHORIZED_USER_IDS" + + # process the user IDs + for userID ; do + remove_userid "$userID" + done + + log "Run the following to update user's authorized_keys file:" + log "$PGRM update-users $uname" + ;; + + 'help'|'h'|'?') + usage + ;; + + *) + failure "Unknown command: '$COMMAND' +Type '$PGRM help' for usage." + ;; +esac diff --git a/src/monkeysphere-ssh-proxycommand b/src/monkeysphere-ssh-proxycommand new file mode 100755 index 0000000..4b90a0d --- /dev/null +++ b/src/monkeysphere-ssh-proxycommand @@ -0,0 +1,56 @@ +#!/bin/sh -e + +# monkeysphere-ssh-proxycommand: MonkeySphere ssh ProxyCommand hook +# +# The monkeysphere scripts are written by: +# Jameson Rollins <jrollins@fifthhorseman.net> +# +# They are Copyright 2008, and are all released under the GPL, version 3 +# or later. + +# This is meant to be run as an ssh ProxyCommand 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" + +usage() { +cat <<EOF >&2 +usage: ssh -o ProxyCommand="$(basename $0) %h %p" ... +EOF +} + +log() { + echo "$@" >&2 +} + +if [ -z "$HOST" ] ; then + log "host must be specified." + usage + exit 1 +fi +if [ -z "$PORT" ] ; then + log "port must be specified." + usage + exit 1 +fi + +# check for the host key in the known_hosts file +hostKey=$(ssh-keygen -F "$HOST") + +# if the host key is found in the known_hosts file, +# don't check the keyserver +if [ "$hostKey" ] ; then + CHECK_KEYSERVER="false" +else + CHECK_KEYSERVER="true" +fi +export CHECK_KEYSERVER + +# update the known_hosts file for the host +monkeysphere update-known-hosts "$HOST" + +# exec a netcat passthrough to host for the ssh connection +exec nc "$HOST" "$PORT" diff --git a/src/seckey2sshagent b/src/seckey2sshagent new file mode 100755 index 0000000..0e8d695 --- /dev/null +++ b/src/seckey2sshagent @@ -0,0 +1,25 @@ +#!/bin/sh + +cleanup() { + echo -n "removing temp gpg home... " + rm -rf $FOO + echo "done." +} + +trap cleanup EXIT + +GPGID="$1" + +idchars=$(echo $GPGID | wc -m) +if [ "$idchars" -ne 17 ] ; then + echo "GPGID is not 16 characters ($idchars)." + exit 1 +fi + +FOO=$(mktemp -d) + +gpg --export-secret-key --export-options export-reset-subkey-passwd $GPGID | GNUPGHOME=$FOO gpg --import + +GNUPGHOME=$FOO gpg --edit-key $GPGID + +GNUPGHOME=$FOO gpg --export-secret-key $GPGID | openpgp2ssh $GPGID | ssh-add -c /dev/stdin |