summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMicah Anderson <micah@riseup.net>2008-06-18 23:35:20 -0400
committerMicah Anderson <micah@riseup.net>2008-06-18 23:35:20 -0400
commit308aa104f66a40f2426c13b96f48631937502f6b (patch)
tree55e1b1427ba23750b0c239e5d43fc3ffa49b0293 /src
parenta9a56853a27e1dbce3c48af327b0adff0e4c38e0 (diff)
parent9c94e937fbe8beb56956365cac07d6eff45215cd (diff)
Merge commit 'dkg/master'
Conflicts: doc/MonkeySpec
Diffstat (limited to 'src')
-rw-r--r--src/common566
-rw-r--r--src/keytrans/Makefile12
-rw-r--r--src/keytrans/gnutls-helpers.c446
-rw-r--r--src/keytrans/gnutls-helpers.h78
-rw-r--r--src/keytrans/openpgp2ssh.c437
-rwxr-xr-xsrc/monkeysphere214
-rwxr-xr-xsrc/monkeysphere-server271
-rwxr-xr-xsrc/monkeysphere-ssh-proxycommand56
-rwxr-xr-xsrc/seckey2sshagent25
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