From 6b83b50141e37e2926333dc1aa987bfb50317b5b Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Fri, 23 May 2008 19:01:50 -0400 Subject: major overhaul of rhesus: - much more sophisticated validity checking of keys/uids - broke out more functions - cleaned-up/simplified code - changed to new variable naming standard --- doc/MonkeySpec | 2 +- doc/README | 49 ++++++- doc/git init | 128 ------------------ monkeysphere.conf | 8 -- rhesus/rhesus | 389 +++++++++++++++++++++++++++++------------------------- 5 files changed, 257 insertions(+), 319 deletions(-) delete mode 100644 doc/git init diff --git a/doc/MonkeySpec b/doc/MonkeySpec index 7a19df0..c36e7de 100644 --- a/doc/MonkeySpec +++ b/doc/MonkeySpec @@ -102,4 +102,4 @@ Write manpage for gpgkey2ssh gpg private key (start with passwordless) to PEM encoded private key: perl libraries, libopencdk / gnutls, gpgme setup remote git repo think through / plan merging of known_hosts (& auth_keys?) -think about policies and their representation \ No newline at end of file +think about policies and their representation diff --git a/doc/README b/doc/README index 4c70d1d..9dc8753 100644 --- a/doc/README +++ b/doc/README @@ -1,5 +1,48 @@ - Monkeysphere - ------------ +Monkeysphere README +------------------- +Default file locations: -This is the README! +MS_HOME=~/.config/monkeysphere +STAGING_AREA=$MS_HOME +GNUPGHOME=~/.gnupg +$MS_HOME/monkeysphere.conf +$MS_HOME/auth_host_ids +$MS_HOME/auth_user_ids +$STAGING_AREA/host_keys/KEYHASH +$STAGING_AREA/known_hosts +$STAGING_AREA/user_keys/KEYHASH +$STAGING_AREA/authorized_keys + +For a user to update their ms known_hosts file: + +$ rhesus --known_hosts + +For a user to update their ms authorized_keys file: + +$ rhesus --authorized_keys + +A system can maintain ms authorized_keys files for it's users. Some +different variables need to be defined to help manage this. The way +this is done is by first defining a new MS_HOME: + +MS_HOME=/etc/monkeysphere + +This directory would then have a monkeysphere.conf which defines the +following variables: + +AUTH_USER_FILE="$MS_HOME"/auth_user_ids/"$USER" +STAGING_AREA=/var/lib/monkeysphere/stage/$USER +GNUPGHOME=$MS_HOME/gnupg + +To update the ms authorized_keys file for user "foo", the system would +then run the following: + +# USER=foo MS_HOME=/etc/monkeysphere rhesus --authorized_keys + +To update the ms authorized_keys file for all users on the the system: + +MS_HOME=/etc/monkeysphere +for USER in $(ls -1 /etc/monkeysphere/auth_user_ids) ; do + rhesus --authorized_keys +done diff --git a/doc/git init b/doc/git init deleted file mode 100644 index 7ba5071..0000000 --- a/doc/git init +++ /dev/null @@ -1,128 +0,0 @@ -remote$ mkdir public_html/git -(etch) -remote$ GIT_DIR=~/public_html/git/monkeysphere.git git init-db -remote$ cd ~/public_html/git/monkeysphere.git -remote$ chmod a+x hooks/post-update -# NOT SURE IF THIS IS NEEDED: remote$ git-update-server-info -fetch = +refs/heads/*:refs/remotes/dkg/* - -(newer) -remote$ mkdir -p public_html/git/monkey.git -remote$ cd public_html/git/monkey.git -remote$ git --bare init -remote$ chmod a+x hooks/post-update -remote$ git-update-server-info - -(new way! no origin/) -$ cd ~/src -$ mkdir monkeysphere -$ cd monkeysphere -$ git init -$ git remote add -f mlcastle http://git.mlcastle.net/monkeysphere.git/ -$ git remote add grunt grunt:/whatever -$ git config remote.grunt.push "+refs/heads/*" -$ git merge mlcastle/master -$ git push grunt - -(old way!) -(in ~/src or wherever) -local$ git clone http://git.mlcastle.net/monkeysphere.git/ monkeysphere -local$ cd monkeysphere - -.git/config: - -[core] - repositoryformatversion = 0 - filemode = true - bare = false - logallrefupdates = true - -## THIS ONE NEEDS TO BE CHANGED TO YOUR REMOTE URI -[remote "post"] - url = YOUR-REMOTE-URL/git/monkeysphere.git - push = +refs/heads/* -### THE ABOVE ONE NEEDS TO BE CHANGED - -[remote "mlcastle"] - url = http://git.mlcastle.net/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/mlcastle/* - -[remote "jrollins"] - url = http://lair.fifthhorseman.net/~jrollins/git/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/jrollins/* - -[remote "dkg"] - url = http://lair.fifthhorseman.net/~dkg/git/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/dkg/* - -[remote "mjgoins"] SEE: dkg, jrollins, etc. - -[remote "micah"] - url = http://micah.riseup.net/git/monkeysphere.git - fetch = +refs/heads/*:refs/remotes/micah/* - -[remote "enw"] - url = http://lair.fifthhorseman.net/~enw/git/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/enw/* - -[remote "rossg"] - url = http://lair.fifthhorseman.net/~rossg/git/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/rossg/* - -[remote "greg"] - url = http://lair.fifthhorseman.net/~greg/git/monkeysphere.git/ - fetch = +refs/heads/*:refs/remotes/greg/* - blood type = - ------------------ -[remote "upload"] - url = ssh://z.mlcastle.net/var/www/git/monkeysphere.git/ - push = +refs/heads/* - - -$ git fetch dkg -$ git checkout master -$ git merge remotes/dkg/master -$ git push post - - - - - - - -grunt's fingerprint: be:43:9c:03:9c:04:1a:97:7a:61:8a:fe:71:9d:6c:67 -(grunt is lair.fifthhorseman.net) - -for foo in $(git remote); do git fetch $foo; done - - - -set mainfont {Arial 12} -set textfont { Courier 12} -set uifont {Arial 10 bold} -set tabstop 8 -set findmergefiles 0 -set maxgraphpct 50 -set maxwidth 16 -set cmitmode patch -set wrapcomment none -set showneartags 1 -set showlocalchanges 1 -set datetimeformat {%Y-%m-%d %H:%M:%S} -set limitdiffs 1 -set bgcolor white -set fgcolor black -set colors {green red blue magenta darkgrey brown orange} -set diffcolors {red "#00a000" blue} -set diffcontext 3 -set selectbgcolor gray85 -set geometry(main) 1280x936+14+28 -set geometry(topwidth) 1278 -set geometry(topheight) 286 -set geometry(pwsash0) "638 1" -set geometry(pwsash1) "903 1" -set geometry(botwidth) 1001 -set geometry(botheight) 638 -set permviews {} - diff --git a/monkeysphere.conf b/monkeysphere.conf index a54b6bd..cd5e3b2 100644 --- a/monkeysphere.conf +++ b/monkeysphere.conf @@ -13,11 +13,3 @@ GNUPGHOME=/etc/monkeysphere/gnupg # gpg keyserver to search for keys KEYSERVER=subkeys.pgp.net - -# acceptable key capabilities for user keys -# can be any combination of: -# e = encrypt -# s = sign -# c = certify -# a = authentication -REQUIRED_KEY_CAPABILITY='sca' diff --git a/rhesus/rhesus b/rhesus/rhesus index fc2f2f5..2e05dfd 100755 --- a/rhesus/rhesus +++ b/rhesus/rhesus @@ -2,20 +2,6 @@ # rhesus: monkeysphere authorized_keys/known_hosts generating script # -# When run as a normal user, no special configuration is needed. -# -# When run as an administrator to update users' authorized_keys files, -# the following environment variables should be defined first: -# -# MS_CONF=/etc/monkeysphere/monkeysphere.conf -# USER=foo -# -# ie: -# -# for USER in $(ls -1 /home) ; do -# MS_CONF=/etc/monkeysphere/monkeysphere.conf rhesus --authorized_keys -# done -# # Written by # Jameson Rollins # @@ -23,6 +9,10 @@ CMD=$(basename $0) +######################################################################## +# FUNCTIONS +######################################################################## + usage() { cat </dev/null 2>&1 +} + +# convert escaped characters from gpg output back into original +# character +# FIXME: undo all escape character translation in with-colons gpg output +unescape() { + echo "$1" | sed 's/\\x3a/:/' +} + +# stand in until we get dkg's gpg2ssh program +gpg2ssh_tmp() { + local mode + local keyID + mode="$1" - keyid="$2" + keyID="$2" + userID="$3" + if [ "$mode" = '--authorized_keys' -o "$mode" = '-a' ] ; then - gpgkey2ssh "$keyid" | sed -e "s/COMMENT/$userid/" + gpgkey2ssh "$keyID" | sed -e "s/COMMENT/$userID/" elif [ "$mode" = '--known_hosts' -o "$mode" = '-k' ] ; then - echo -n "$userid "; gpgkey2ssh "$keyid" | sed -e 's/ COMMENT//' + echo -n "$userID "; gpgkey2ssh "$keyID" | sed -e 's/ COMMENT//' + fi +} + +# userid and key policy checking +# the following checks policy on the returned keys +# - checks that full key has appropriate valididy (u|f) +# - checks key has appropriate capability (E|A) +# - checks that particular desired user id has appropriate validity +# see /usr/share/doc/gnupg/DETAILS.gz +# FIXME: add some more status output +# expects global variable: "mode" +process_user_id() { + local userID + local cacheDir + local keyOK + local keyCapability + local keyFingerprint + local userIDHash + + userID="$1" + cacheDir="$2" + + # fetch all keys from keyserver + # if none found, break + if ! gpg_fetch_keys "$userID" ; then + echo " no keys found." + return fi + + # some crazy piping here that takes the output of gpg and + # pipes it into a "while read" loop that reads each line + # of standard input one-by-one. + gpg --fixed-list-mode --list-key --with-colons \ + --with-fingerprint ="$userID" 2> /dev/null | \ + cut -d : -f 1,2,5,10,12 | \ + while IFS=: read -r type validity keyid uidfpr capability ; do + # process based on record type + case $type in + 'pub') + # new key, wipe the slate + keyOK= + keyCapability= + keyFingerprint= + # check primary key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + # check capability is not Disabled... + if echo "$capability" | grep -q 'D' ; then + continue + fi + # check capability is Encryption and Authentication + # FIXME: make more flexible capability specification + # (ie. in conf file) + if echo "$capability" | grep -q -v 'E' ; then + if echo "$capability" | grep -q -v 'A' ; then + continue + fi + fi + keyCapability="$capability" + keyOK=true + keyID="$keyid" + ;; + 'fpr') + # if key ok, get fingerprint + if [ "$keyOK" ] ; then + keyFingerprint="$uidfpr" + fi + ;; + 'uid') + # check key ok and we have key fingerprint + if [ -z "$keyOK" -o -z "$keyFingerprint" ] ; then + continue + fi + # check key validity + if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + continue + fi + # check the uid matches + if [ "$(unescape "$uidfpr")" != "$userID" ] ; then + continue + fi + # convert the key + # FIXME: needs to apply extra options if specified + echo -n " valid key found; generating ssh key(s)... " + userIDHash=$(echo "$userID" | sha1sum | awk '{ print $1 }') + # export the key with gpg2ssh + #gpg --export "$keyFingerprint" | gpg2ssh "$mode" > "$cacheDir"/"$userIDHash"."$keyFingerprint" + # stand in until we get dkg's gpg2ssh program + gpg2ssh_tmp "$mode" "$keyID" "$userID" > "$cacheDir"/"$userIDHash"."$keyFingerprint" + if [ "$?" = 0 ] ; then + echo "done." + else + echo "error." + fi + ;; + esac + done } -# expects global variables -# mode REQUIRED_KEY_CAPABILITY ids_file key_dir -process_keys() { - local nlines - local n - local userid - local userid_hash - local return - local pub_info - local key_trust - local key_capability - local gen_key - unset gen_key +# process the auth_*_ids file +# go through line-by-line, extracting and processing each user id +# expects global variable: "mode" +process_auth_file() { + local authIDsFile + local cacheDir + local nLines + local line + local userID + + authIDsFile="$1" + cacheDir="$2" # find number of user ids in auth_user_ids file - nlines=$(meat "$ids_file" | wc -l) + nLines=$(meat <"$authIDsFile" | wc -l) # make sure gpg home exists with proper permissions mkdir -p -m 0700 "$GNUPGHOME" # clean out keys file and remake keys directory - rm -rf "$key_dir" - mkdir -p "$key_dir" - - # loop through all user ids, and generate ssh keys - for n in $(seq 1 $nlines) ; do - - # get id - userid=$(meat "$ids_file" | cutline "$n" ) - userid_hash=$(echo "$userid" | sha1sum | awk '{ print $1 }') - - # search for key on keyserver - log "validating: '$userid'" - return=$(echo 1 | gpg --quiet --batch --command-fd 0 --with-colons --keyserver "$KEYSERVER" --search ="$userid") - - # if the key was found... - if [ "$return" ] ; then - echo " key found." - - # checking key attributes - # see /usr/share/doc/gnupg/DETAILS.gz - - pub_info=$(gpg --fixed-list-mode --with-colons --list-keys --with-fingerprint ="$userid" | grep '^pub:') - if [ -z "$pub_info" ] ; then - echo " error getting pub info -> SKIPPING" - continue - fi - - # extract needed fields - key_trust=$(echo "$pub_info" | cut -d: -f2) - keyid=$(echo "$pub_info" | cut -d: -f5) - key_capability=$(echo "$pub_info" | cut -d: -f12) - - # check if key disabled - if echo "$key_capability" | grep -q '[D]' ; then - echo " key disabled -> SKIPPING" - continue - fi - - # check key capability - if echo "$key_capability" | grep -q '[$REQUIRED_KEY_CAPABILITY]' ; then - echo " key capability verified ('$key_capability')." - else - echo " unacceptable key capability ('$key_capability') -> SKIPPING" - continue - fi - - # if key is not fully trusted exit - # (this includes not revoked or expired) - # determine trust - echo -n " key " - case "$key_trust" in - 'i') - echo -n "invalid" ;; - 'r') - echo -n "revoked" ;; - 'e') - echo -n "expired" ;; - '-'|'q'|'n'|'m') - echo -n "has unacceptable trust" ;; - 'f'|'u') - echo -n "fully trusted" - gen_key=true - ;; - *) - echo -n "has unknown trust" ;; - esac - - if [ "$gen_key" ] ; then - # convert pgp key to ssh key, and write to cache file - echo -n " -> generating ssh key... " - gpg2ssh "$mode" "$keyid" > "$key_dir"/"$userid_hash" - echo "done." - else - echo ". -> SKIPPING" - fi - - else - echo " key not found." - fi + rm -rf "$cacheDir" + mkdir -p "$cacheDir" + + # loop through all user ids + for line in $(seq 1 $nLines) ; do + # get user id + # FIXME: needs to handle extra options if necessary + userID=$(meat <"$authIDsFile" | cutline "$line" ) + + # process the user id and extract keys + log "processing user id: '$userID'" + process_user_id "$userID" "$cacheDir" done } + ######################################################################## # MAIN ######################################################################## @@ -180,11 +225,13 @@ if ! id -u "$USER" > /dev/null 2>&1 ; then failure "invalid user '$USER'." fi +# set user home directory HOME=$(getent passwd "$USER" | cut -d: -f6) +# get ms home directory MS_HOME=${MS_HOME:-"$HOME"/.config/monkeysphere} -# load conf file +# load configuration file MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} [ -e "$MS_CONF" ] && . "$MS_CONF" @@ -194,78 +241,62 @@ AUTH_HOST_FILE=${AUTH_HOST_FILE:-"$MS_HOME"/auth_host_ids} AUTH_USER_FILE=${AUTH_USER_FILE:-"$MS_HOME"/auth_user_ids} GNUPGHOME=${GNUPGHOME:-"$HOME"/.gnupg} KEYSERVER=${KEYSERVER:-subkeys.pgp.net} -REQUIRED_KEY_CAPABILITY=${REQUIRED_KEY_CAPABILITY:-'a'} +USER_KNOW_HOSTS="$HOME"/.ssh/known_hosts +USER_AUTHORIZED_KEYS="$HOME"/.ssh/authorized_keys + +# export USER and GNUPGHOME variables, since they are used by gpg export USER export GNUPGHOME -host_keys_dir="$STAGING_AREA"/host_keys -user_keys_dir="$STAGING_AREA"/user_keys -known_hosts_stage_file="$STAGING_AREA"/known_hosts -authorized_keys_stage_file="$STAGING_AREA"/authorized_keys +# stagging locations +hostKeysCacheDir="$STAGING_AREA"/host_keys +userKeysCacheDir="$STAGING_AREA"/user_keys +msKnownHosts="$STAGING_AREA"/known_hosts +msAuthorizedKeys="$STAGING_AREA"/authorized_keys -# act on mode +# set mode variables if [ "$mode" = '--known_hosts' -o "$mode" = '-k' ] ; then + fileType=known_hosts + authIDsFile="$AUTH_HOST_FILE" + outFile="$msKnownHosts" + cacheDir="$hostKeysCacheDir" + userFile="$USER_KNOWN_HOSTS" +elif [ "$mode" = '--authorized_keys' -o "$mode" = '-a' ] ; then + fileType=authorized_keys + authIDsFile="$AUTH_USER_FILE" + outFile="$msAuthorizedKeys" + cacheDir="$userKeysCacheDir" + userFile="$USER_AUTHORIZED_KEYS" +else + failure "unknown command '$mode'." +fi - # set variables for process_keys command - ids_file="$AUTH_HOST_FILE" - log -n "[$USER] " - if [ ! -s "$ids_file" ] ; then - echo "auth_host_ids file is empty or does not exist." - exit - else - echo "updating known_hosts file..." - fi - key_dir="$host_keys_dir" - - # process the keys - process_keys - - # write known_hosts file - > "$known_hosts_stage_file" - if [ $(ls "$key_dir") ] ; then - log -n "writing known_hosts stage file..." - cat "$key_dir"/* > "$known_hosts_stage_file" - echo "done." - else - log "no gpg keys to add to known_hosts file." - fi - if [ -s "$HOME"/.ssh/known_hosts ] ; then - log -n "adding user known_hosts file... " - cat "$HOME"/.ssh/known_hosts >> "$known_hosts_stage_file" - echo "done." - fi - log "known_hosts file updated: $known_hosts_stage_file" +# check auth ids file +if [ ! -s "$authIDsFile" ] ; then + echo $(basename "$authIDsFile") "file is empty or does not exist." + exit +fi -elif [ "$mode" = '--authorized_keys' -o "$mode" = '-a' ] ; then +log "user '$USER': monkeysphere $fileType generation..." - # set variables for process_keys command - ids_file="$AUTH_USER_FILE" - log -n "[$USER] " - if [ ! -s "$ids_file" ] ; then - echo "auth_user_ids file is empty or does not exist." - exit - else - echo "updating authorized_keys file:" - fi - key_dir="$user_keys_dir" - - # process the keys - process_keys - - # write authorized_keys file - > "$authorized_keys_stage_file" - if [ $(ls "$key_dir") ] ; then - log -n "writing ms authorized_keys file... " - cat "$key_dir"/* > "$authorized_keys_stage_file" - echo "done." - else - log "no gpg keys to add to authorized_keys file." - fi - if [ -s "$HOME"/.ssh/authorized_keys ] ; then - log -n "adding user authorized_keys file... " - cat "$HOME"/.ssh/authorized_keys >> "$authorized_keys_stage_file" - echo "done." - fi - log "authorized_keys file updated: $authorized_keys_stage_file" +# process the auth file +process_auth_file "$authIDsFile" "$cacheDir" + +# write output key file +log "writing ms $fileType file... " +> "$outFile" +if [ "$(ls "$cacheDir")" ] ; then + log -n "adding gpg keys... " + cat "$cacheDir"/* > "$outFile" + echo "done." +else + log "no gpg keys to add." +fi +if [ -s "$userFile" ] ; then + log -n "adding user $fileType file... " + cat "$userFile" >> "$outFile" + echo "done." fi +log "ms $fileType file generated:" +log "$outFile" -- cgit v1.2.3 From 9c7796a6c4f3964c9255b3741fe92ed4ddd9a41d Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Sun, 25 May 2008 02:34:52 -0400 Subject: fix bashism, and correct bad error message --- rhesus/rhesus | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rhesus/rhesus b/rhesus/rhesus index 2e05dfd..4bef85e 100755 --- a/rhesus/rhesus +++ b/rhesus/rhesus @@ -44,7 +44,8 @@ cutline() { # FIXME: need to figure out how to retrieve all matching keys # (not just first 5) gpg_fetch_keys() { - local id="$1" + local id + id="$1" echo 1,2,3,4,5 | \ gpg --quiet --batch --command-fd 0 --with-colons \ --keyserver "$KEYSERVER" \ @@ -274,7 +275,7 @@ fi # check auth ids file if [ ! -s "$authIDsFile" ] ; then - echo $(basename "$authIDsFile") "file is empty or does not exist." + echo "'$authIDsFile' file is empty or does not exist." exit fi -- cgit v1.2.3 From b05a928cfe0738f733d8bc95289aacc562068e67 Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Sun, 25 May 2008 14:35:27 -0400 Subject: some updates to rhesus: - add ability to rhesus to just process specified userids. - removed '--' in front of process type specification at command line. - cleaned up some log output --- rhesus/rhesus | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/rhesus/rhesus b/rhesus/rhesus index 4bef85e..dec24a2 100755 --- a/rhesus/rhesus +++ b/rhesus/rhesus @@ -15,8 +15,11 @@ CMD=$(basename $0) usage() { cat < Date: Sun, 25 May 2008 14:43:39 -0400 Subject: update to MonkeySpec: - i took the liberty to reapportion some of the monkey names to new/different components. some components are no longer needed (eg. marmoset as originally defined is no longer needed since rhesus handles both ssh key file types). i also took the name "howler" and used it for a new component that generates and publishes server gpg keys. - fleshed out the Alice/Bob use case senario with some clarification and more detail. --- doc/MonkeySpec | 187 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 109 insertions(+), 78 deletions(-) diff --git a/doc/MonkeySpec b/doc/MonkeySpec index c36e7de..45d6cf6 100644 --- a/doc/MonkeySpec +++ b/doc/MonkeySpec @@ -7,99 +7,130 @@ AGENDA [ ] work [x] jrollins will talk and gesture - in progress +MONKEYNAMES +=========== + +rhesus, marmoset, howler, langur, tamarin, barbary + COMPONENTS ========== -* client-side componants -** "Marmoset": update known_hosts file with public key of server(s): -*** be responsible for removing keys from the file as key revocation happens -*** be responsible for updating a key in the file where there is a key replacement -*** must result in a file that is parsable by the existing ssh client without errors -*** manual management must be allowed without stomping on it -*** provide a simple, intelligible, clear policy for key acceptance -*** questions: should this query keyserver & update known host files? (we already - have awesome tool that queries keyservers and updates a web of trust (gpg) -** "Howler": simple script that could be placed as a trigger function (in your .ssh/config) -*** runs on connection to a certain host -*** triggers update to known_hosts file then makes connection -*** proxy-command | pre-hook script | wrapper script -** "Langur": policy-editor for viewing/editing policies - -* server-side componants -** "Rhesus" updates a per-user authorized_keys file, instead of updating a - known_hosts file from a public key by matching a specified user-id (for given - user: update authkeys file with public keys derived from authorized_uids - file) -*** Needs to operate with the same principles that Marmoset client-side does -** "Tamarin" triggers Rhesus during an attempt to initiate a connection or a scheduler (or both) -** "Barbary" - policy editor / viewer - -* common componants -** Create a ssh keypair from a openpgp keypair - -from ssh_config(5): - LocalCommand - Specifies a command to execute on the local machine after suc‐ - cessfully connecting to the server. The command string extends - to the end of the line, and is executed with /bin/sh. This - directive is ignored unless PermitLocalCommand has been enabled. +(names in "" are code names until we think of better ones.) + +common components +----------------- +* "rhesus": update known_hosts/authorized_keys files: + - be responsible for removing keys from the file as key revocation + happens + - be responsible for updating a key in the file where there is a key + replacement + - must result in a file that is parsable by the existing ssh client + without errors + - manual management must be allowed without stomping on it + - provide a simple, intelligible, clear policy for key acceptance + +* "langur": policy-editor for viewing/editing policies + +* gpg2ssh: utility to convert gpg keys to ssh + known_hosts/authorized_keys lines + +* ssh2gpg: create openpgp keypair from ssh keypair + +server-side components +---------------------- +* "howler": service gpg key generator/publisher + +* "tamarin": script to trigger rhesus during attempt to initiate + connection from client + +client-side components +---------------------- +* "marmoset": script to trigger rhesus during attempt to initiate + connection to server + - runs on connection to a certain host + - triggers update to known_hosts file then makes connection + - proxy-command | pre-hook script | wrapper script + - (ssh_config "LocalCommand" is only run *after* connection) + +USE CASE +======== + +Dramatis Personae: http://en.wikipedia.org/wiki/Alice_and_Bob +Backstory: http://www.conceptlabs.co.uk/alicebob.html + +Bob wants to sign on to the computer "mangabey" via monkeysphere +framework. He doesn't yet have access to the machine, but he knows +Alice, who is the admin of magabey. Alice and Bob, being the +contientious netizens that they are, have already published their +personal gpg keys to the web of trust, and being good friends, have +both signed each other's keys and marked each others keys with "full" +trust. + +Alice uses howler to publish a gpg key for magabey with the special +"ssh://magabey" URI userid. Alice signs magabey's gpg key and +publishes her signature. Alice then creates a user "bob" on magabey, +and puts Bob's userid in the auth_user_ids file for user bob on +magabey. tamarin triggers on magabey, which triggers rhesus, which +takes all userids in bob's auth_user_ids file, look on a keyserver to +find the public keys for each user, converts the gpg public keys into +ssh public keys if the key validity is acceptable, and finally insert +those keys into an authorized_keys file for bob. + +Bob now adds the "ssh://magabey" userid to the auth_host_ids file in +his account on his localhost. Bob now goes to connect to bob@magabey. +Bob's ssh client, which is monkeysphere enabled, triggers marmoset, +which triggers rhesus on Bob's computer, which takes all server +userids in his auth_host_ids file, looks on a keyserver to find the +public key for each server (based on the server's URI), converts the +gpg public keys into ssh public keys if the key validity is +acceptable, and finally insert those keys into Bob's known_hosts file. + +On Bob's side, since mangabey's key had "full" validity (since it was +signed by Alice whom he fully trusts), Bob's ssh client deems magabey +"known" and no further host key checking is required. + +On magabey's side, since Bob's key has "full" validity (since it had +also been signed by Alice whom magabey fully trusts (since Alice told +him to)), Bob is authenticated to log into bob@magabey. NOTES ===== + * Daniel and Elliot lie. -* We will use a distributed VCS, each developer will create their own git repository and publish it publically for others to pull from, mail out +* We will use a distributed VCS, each developer will create their own + git repository and publish it publicly for others to pull from, mail + out * public project page doesn't perhaps make sense yet -* approximate goal - using the web of trust to authenticate ppl for SSH +* approximate goal - using the web of trust to authenticate ppl for + SSH * outline of various components of monkeysphere -* M: what does it mean to be in the monkeysphere? not necessarily a great coder. -* J: interested in seeing project happen, not in actually doing it. anybody can contribute as much as they want. -* J: if we put the structure in place to work on monkeysphere then we don't have to do anything +* M: what does it mean to be in the monkeysphere? not necessarily a + great coder. +* J: interested in seeing project happen, not in actually doing it. + anybody can contribute as much as they want. +* J: if we put the structure in place to work on monkeysphere then we + don't have to do anything * D: we are not creating -* understand gpg's keyring better, understanding tools better, building scripts +* understand gpg's keyring better, understanding tools better, + building scripts * Some debian packages allow automated configuration of config files. - * GENERAL GOAL - use openpgp web-of-trust to authenticate ppl for SSH -* SPECIFIC GOAL - allow openssh to tie into pgp web-of-trust without modifying either openpgp and openssh -* DESIGN GOALS - authentication, use the existing generic OpenSSH client, the admin can make it default, although end-user should be decide to use monkeysphere or not -* DESIGN GOAL - use of monkeysphere should not radically change connecting-to-server experience +* SPECIFIC GOAL - allow openssh to tie into pgp web-of-trust without + modifying either openpgp and openssh +* DESIGN GOALS - authentication, use the existing generic OpenSSH + client, the admin can make it default, although end-user should be + decide to use monkeysphere or not +* DESIGN GOAL - use of monkeysphere should not radically change + connecting-to-server experience * GOAL - pick a monkey-related name for each component -Dramatis Personae: http://en.wikipedia.org/wiki/Alice_and_Bob -Backstory: http://www.conceptlabs.co.uk/alicebob.html +Host identity piece of monkeysphere could be used without buying into +the authorization component. -* Use Case: Bob wants to sign on to the computer "mangabey" via monkeysphere - framework. He doesn't have access to the machine, but he knows Alice, who is - the admin of magabey. Alice creates a user bob and puts bob's userid in the - auth_user_ids file for bob. Tamarin triggers which causes Rhesus to take all - the things in the auth_userids file, takes those users, look son a keyserver - finds the public keys for the users, converts the gpg public keys into ssh - public keys and inserts those into a user_authorized_keys file. Bob goes to - connect, bob's ssh client which is monkeysphere enbaled, howler is triggered - which triggers marmoset which looks out into the web of trust and find an - OpenPGP key that has a userid that matches the URI of magabey. Marmoset checks - to see if this key for mangabey has been signed by any keys that you trust - (based on your policy). Has this key been signed by somebody that you trust? - If yes, connect, if no: abort or fail-through or whatever. Alice has signed - this uid, so Marmoset says "OK, this server has been verified" it then - converts the gpg public key into a ssh public key and then adds this gpg key - to the known_host file. ssh says, "you" are about to connect to magabey and - you know this is magabey because alice says so and you trust alice". The gpg - private key of bob has to be converted (somehow, via agent or something) into - a ssh private_key. SSH connection happens. - -Host identity piece of monkeysphere could be used without buying into the -authorization component. - -Monkeysphere is authentication layer that allows the sysadmin to perform -authorization on user identities instead of on keys, it additionally allows the -sysadmin also to authenticate the server to the end-user. +Monkeysphere is authentication layer that allows the sysadmin to +perform authorization on user identities instead of on keys, it +additionally allows the sysadmin also to authenticate the server to +the end-user. git clone http://git.mlcastle.net/monkeysphere.git/ monkeysphere - -Fix gpgkey2ssh so that the entire key fingerprint will work, accept full fingerprint, or accept a pipe and do the conversion -Write manpage for gpgkey2ssh -gpg private key (start with passwordless) to PEM encoded private key: perl libraries, libopencdk / gnutls, gpgme -setup remote git repo -think through / plan merging of known_hosts (& auth_keys?) -think about policies and their representation -- cgit v1.2.3 From 03f7058885d83592f3fe4faefc2d3fb148615ec4 Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Sun, 25 May 2008 14:49:26 -0400 Subject: add new component "howler": small script to generate and publish a gpg key for a server --- howler/howler | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100755 howler/howler diff --git a/howler/howler b/howler/howler new file mode 100755 index 0000000..7e33471 --- /dev/null +++ b/howler/howler @@ -0,0 +1,78 @@ +#!/bin/sh + +# howler: server gpg key generator/publisher +# +# Written by +# Jameson Rollins +# +# Copyright 2008, released under the GPL, version 3 or later + +CMD=$(basename $0) + +######################################################################## +# FUNCTIONS +######################################################################## + +failure() { + echo "$1" >&2 + exit ${2:-'1'} +} + +######################################################################## +# MAIN +######################################################################## + +MS_HOME=${MS_HOME:-/etc/monkeysphere} + +. "$MS_HOME"/monkeysphere.conf + +export GNUPGHOME + +KEY_TYPE=${KEY_TYPE:-RSA} +KEY_LENGTH=${KEY_LENGTH:-2048} +KEY_USAGE=${KEY_USAGE:-encrypt,auth} +SERVICE=${SERVICE:-ssh} +HOSTNAME=${HOSTNAME:-$(hostname -f)} + +USERID=${USERID:-"$SERVICE"://"$HOSTNAME"} + +echo "key parameters:" +cat < /dev/null 2>&1 ; then + failure "key for '$USERID' already exists" +fi + +echo "generating server key..." +gpg --batch --gen-key < /dev/null | grep '^pub:' | cut -d: -f5) + +# dummy command so as not to publish fakes keys during testing +# eventually: +#gpg --send-keys --keyserver "$KEYSERVER" "$keyID" +echo "gpg --send-keys --keyserver $KEYSERVER $keyID" + +echo "done." -- cgit v1.2.3 From ad6dc98e4a8b38ed1ae3972f948723a205306a7d Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Sun, 25 May 2008 14:51:32 -0400 Subject: move gpg2ssh stuff into it's own subdirectory to reduce some cluter --- Makefile | 16 --- gnutls-helpers.c | 364 ----------------------------------------------- gnutls-helpers.h | 72 ---------- gpg2ssh.c | 291 ------------------------------------- gpg2ssh/Makefile | 16 +++ gpg2ssh/gnutls-helpers.c | 364 +++++++++++++++++++++++++++++++++++++++++++++++ gpg2ssh/gnutls-helpers.h | 72 ++++++++++ gpg2ssh/gpg2ssh.c | 291 +++++++++++++++++++++++++++++++++++++ gpg2ssh/main.c | 271 +++++++++++++++++++++++++++++++++++ gpg2ssh/ssh2gpg.c | 171 ++++++++++++++++++++++ main.c | 271 ----------------------------------- ssh2gpg.c | 171 ---------------------- 12 files changed, 1185 insertions(+), 1185 deletions(-) delete mode 100644 Makefile delete mode 100644 gnutls-helpers.c delete mode 100644 gnutls-helpers.h delete mode 100644 gpg2ssh.c create mode 100644 gpg2ssh/Makefile create mode 100644 gpg2ssh/gnutls-helpers.c create mode 100644 gpg2ssh/gnutls-helpers.h create mode 100644 gpg2ssh/gpg2ssh.c create mode 100644 gpg2ssh/main.c create mode 100644 gpg2ssh/ssh2gpg.c delete mode 100644 main.c delete mode 100644 ssh2gpg.c diff --git a/Makefile b/Makefile deleted file mode 100644 index aa18aaa..0000000 --- a/Makefile +++ /dev/null @@ -1,16 +0,0 @@ -monkeysphere: main.c gnutls-helpers.o - gcc -g -Wall --pedantic -o monkeysphere main.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -gpg2ssh: gpg2ssh.c gnutls-helpers.o - gcc -g -Wall --pedantic -o gpg2ssh gpg2ssh.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -ssh2gpg: ssh2gpg.c gnutls-helpers.o - gcc -g -Wall --pedantic -o ssh2gpg ssh2gpg.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o - -%.o: %.c - gcc -g -Wall --pedantic -o $@ -c $< - -clean: - rm -f monkeysphere *.o - -.PHONY: clean diff --git a/gnutls-helpers.c b/gnutls-helpers.c deleted file mode 100644 index 6eae29e..0000000 --- a/gnutls-helpers.c +++ /dev/null @@ -1,364 +0,0 @@ -/* Author: Daniel Kahn Gillmor */ -/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ -/* License: GPL v3 or later */ - -#include "gnutls-helpers.h" -/* for htonl() */ -#include - -/* for setlocale() */ -#include - -/* for isalnum() */ -#include - -int loglevel = 0; - - -void err(const char* fmt, ...) { - va_list ap; - va_start(ap, fmt); - vfprintf(stderr, fmt, ap); - va_end(ap); - fflush(stderr); -} - -void logfunc(int level, const char* string) { - fprintf(stderr, "GnuTLS Logging (%d): %s\n", level, string); -} - -void init_keyid(gnutls_openpgp_keyid_t keyid) { - memset(keyid, 'x', sizeof(gnutls_openpgp_keyid_t)); -} - - - -void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid) -{ - static const char hex[16] = "0123456789ABCDEF"; - unsigned int kix = 0, outix = 0; - - while (kix < sizeof(gnutls_openpgp_keyid_t)) { - out[outix] = hex[(keyid[kix] >> 4) & 0x0f]; - out[outix + 1] = hex[keyid[kix] & 0x0f]; - kix++; - outix += 2; - } -} - - -int init_gnutls() { - const char* version = NULL; - const char* debug_string = NULL; - int ret; - - if (ret = gnutls_global_init(), ret) { - err("Failed to do gnutls_global_init() (error: %d)\n", ret); - return 1; - } - - version = gnutls_check_version(NULL); - - if (version) - err("gnutls version: %s\n", version); - else { - err("no version found!\n"); - return 1; - } - - if (debug_string = getenv("MONKEYSPHERE_DEBUG"), debug_string) { - loglevel = atoi(debug_string); - gnutls_global_set_log_function(logfunc); - - gnutls_global_set_log_level(loglevel); - err("set log level to %d\n", loglevel); - } - return 0; -} - -void init_datum(gnutls_datum_t* d) { - d->data = NULL; - d->size = 0; -} -void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src) { - dest->data = gnutls_realloc(dest->data, src->size); - dest->size = src->size; - memcpy(dest->data, src->data, src->size); -} -int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b) { - if (a->size > b->size) { - err("a is larger\n"); - return 1; - } - if (a->size < b->size) { - err("b is larger\n"); - return -1; - } - return memcmp(a->data, b->data, a->size); -} -void free_datum(gnutls_datum_t* d) { - gnutls_free(d->data); - d->data = NULL; - d->size = 0; -} - -/* read the passed-in string, store in a single datum */ -int set_datum_string(gnutls_datum_t* d, const char* s) { - unsigned int x = strlen(s)+1; - unsigned char* c = NULL; - - c = gnutls_realloc(d->data, x); - if (NULL == c) - return -1; - d->data = c; - d->size = x; - memcpy(d->data, s, x); - return 0; -} - -/* read the passed-in file descriptor until EOF, store in a single - datum */ -int set_datum_fd(gnutls_datum_t* d, int fd) { - unsigned int bufsize = 1024; - unsigned int len = 0; - - FILE* f = fdopen(fd, "r"); - if (bufsize > d->size) { - bufsize = 1024; - d->data = gnutls_realloc(d->data, bufsize); - if (d->data == NULL) { - err("out of memory!\n"); - return -1; - } - d->size = bufsize; - } else { - bufsize = d->size; - } - f = fdopen(fd, "r"); - if (NULL == f) { - err("could not fdopen FD %d\n", fd); - } - clearerr(f); - while (!feof(f) && !ferror(f)) { - if (len == bufsize) { - /* allocate more space by doubling: */ - bufsize *= 2; - d->data = gnutls_realloc(d->data, bufsize); - if (d->data == NULL) { - err("out of memory!\n"); - return -1; - }; - d->size = bufsize; - } - len += fread(d->data + len, 1, bufsize - len, f); - /* err("read %d bytes\n", len); */ - } - if (ferror(f)) { - err("Error reading from fd %d (error: %d) (error: %d '%s')\n", fd, ferror(f), errno, strerror(errno)); - return -1; - } - - /* touch up buffer size to match reality: */ - d->data = gnutls_realloc(d->data, len); - d->size = len; - return 0; -} - -/* read the file indicated (by name) in the fname parameter. store - its entire contents in a single datum. */ -int set_datum_file(gnutls_datum_t* d, const char* fname) { - struct stat sbuf; - unsigned char* c = NULL; - FILE* file = NULL; - size_t x = 0; - - if (0 != stat(fname, &sbuf)) { - err("failed to stat '%s'\n", fname); - return -1; - } - - c = gnutls_realloc(d->data, sbuf.st_size); - if (NULL == c) { - err("failed to allocate %d bytes for '%s'\n", sbuf.st_size, fname); - return -1; - } - - d->data = c; - d->size = sbuf.st_size; - file = fopen(fname, "r"); - if (NULL == file) { - err("failed to open '%s' for reading\n", fname); - return -1; - } - - x = fread(d->data, d->size, 1, file); - if (x != 1) { - err("tried to read %d bytes, read %d instead from '%s'\n", d->size, x, fname); - fclose(file); - return -1; - } - fclose(file); - return 0; -} - -int write_datum_fd(int fd, const gnutls_datum_t* d) { - if (d->size != write(fd, d->data, d->size)) { - err("failed to write body of datum.\n"); - return -1; - } - return 0; -} - - -int write_datum_fd_with_length(int fd, const gnutls_datum_t* d) { - uint32_t len; - int looks_negative = (d->data[0] & 0x80); - unsigned char zero = 0; - - /* if the first bit is 1, then the datum will appear negative in the - MPI encoding style used by OpenSSH. In that case, we'll increase - the length by one, and dump out one more byte */ - - if (looks_negative) { - len = htonl(d->size + 1); - } else { - len = htonl(d->size); - } - if (write(fd, &len, sizeof(len)) != sizeof(len)) { - err("failed to write size of datum.\n"); - return -2; - } - if (looks_negative) { - if (write(fd, &zero, 1) != 1) { - err("failed to write padding byte for MPI.\n"); - return -2; - } - } - return write_datum_fd(fd, d); -} - -int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num) { - unsigned int i; - int ret; - - for (i = 0; i < num; i++) - if (ret = write_datum_fd_with_length(fd, d[i]), ret != 0) - return ret; - - return 0; -} - - -int datum_from_string(gnutls_datum_t* d, const char* str) { - d->size = strlen(str); - d->data = gnutls_realloc(d->data, d->size); - if (d->data == 0) - return ENOMEM; - memcpy(d->data, str, d->size); - return 0; -} - - -int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]) { - int p[2]; - int ret; - - if (pid == NULL) { - err("bad pointer passed to create_writing_pipe()\n"); - return -1; - } - - if (ret = pipe(p), ret == -1) { - err("failed to create a pipe (error: %d \"%s\")\n", errno, strerror(errno)); - return -1; - } - - *pid = fork(); - if (*pid == -1) { - err("Failed to fork (error: %d \"%s\")\n", errno, strerror(errno)); - return -1; - } - if (*pid == 0) { /* this is the child */ - close(p[1]); /* close unused write end */ - - if (0 != dup2(p[0], 0)) { /* map the reading end into stdin */ - err("Failed to transfer reading file descriptor to stdin (error: %d \"%s\")\n", errno, strerror(errno)); - exit(1); - } - execv(path, argv); - err("exec %s failed (error: %d \"%s\")\n", path, errno, strerror(errno)); - /* close the open file descriptors */ - close(p[0]); - close(0); - - exit(1); - } else { /* this is the parent */ - close(p[0]); /* close unused read end */ - return p[1]; - } -} - -int validate_ssh_host_userid(const char* userid) { - char* oldlocale = setlocale(LC_ALL, "C"); - - /* choke if userid does not match the expected format - ("ssh://fully.qualified.domain.name") */ - if (strncmp("ssh://", userid, strlen("ssh://")) != 0) { - err("The user ID should start with ssh:// for a host key\n"); - goto fail; - } - /* so that isalnum will work properly */ - userid += strlen("ssh://"); - while (0 != (*userid)) { - if (!isalnum(*userid)) { - err("label did not start with a letter or a digit! (%s)\n", userid); - goto fail; - } - userid++; - while (isalnum(*userid) || ('-' == (*userid))) - userid++; - if (('.' == (*userid)) || (0 == (*userid))) { /* clean end of label: - check last char - isalnum */ - if (!isalnum(*(userid - 1))) { - err("label did not end with a letter or a digit!\n"); - goto fail; - } - if ('.' == (*userid)) /* advance to the start of the next label */ - userid++; - } else { - err("invalid character in domain name: %c\n", *userid); - goto fail; - } - } - /* ensure that the last character is valid: */ - if (!isalnum(*(userid - 1))) { - err("hostname did not end with a letter or a digit!\n"); - goto fail; - } - /* FIXME: fqdn's can be unicode now, thanks to RFC 3490 -- how do we - make sure that we've got an OK string? */ - - return 0; - - fail: - setlocale(LC_ALL, oldlocale); - return 1; -} - -/* http://tools.ietf.org/html/rfc4880#section-5.5.2 */ -size_t get_openpgp_mpi_size(gnutls_datum_t* d) { - return 2 + d->size; -} - -int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d) { - uint16_t x; - - x = d->size * 8; - x = htons(x); - - write(fd, &x, sizeof(x)); - write(fd, d->data, d->size); - - return 0; -} diff --git a/gnutls-helpers.h b/gnutls-helpers.h deleted file mode 100644 index 9ea22a3..0000000 --- a/gnutls-helpers.h +++ /dev/null @@ -1,72 +0,0 @@ -/* Author: Daniel Kahn Gillmor */ -/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ -/* License: GPL v3 or later */ - - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Functions to help dealing with GnuTLS for monkeysphere key - translation projects: */ - -/* set everything up, including logging levels. Return 0 on - success */ -int init_gnutls(); - -/* logging and output functions: */ - -void err(const char* fmt, ...); -void logfunc(int level, const char* string); - -/* basic datum manipulations: */ - -void init_datum(gnutls_datum_t* d); -void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src); -int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b); -void free_datum(gnutls_datum_t* d); -int write_datum_fd(int fd, const gnutls_datum_t* d); -int write_datum_fd_with_length(int fd, const gnutls_datum_t* d); -int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num); - -/* set up a datum from a null-terminated string */ -int datum_from_string(gnutls_datum_t* d, const char* str); - -/* keyid manipulations: */ -typedef unsigned char printable_keyid[16]; - -void init_keyid(gnutls_openpgp_keyid_t keyid); -void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid); - -/* functions to get data into datum objects: */ - -/* read the passed-in string, store in a single datum */ -int set_datum_string(gnutls_datum_t* d, const char* s); - -/* read the passed-in file descriptor until EOF, store in a single - datum */ -int set_datum_fd(gnutls_datum_t* d, int fd); - -/* read the file indicated (by name) in the fname parameter. store - its entire contents in a single datum. */ -int set_datum_file(gnutls_datum_t* d, const char* fname); - -/* set up file descriptor pipe for writing (child process pid gets - stored in pid, fd is returned)*/ -int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]); - -/* return 0 if userid matches the monkeysphere spec for ssh host user IDs */ -int validate_ssh_host_userid(const char* userid); - -/* how many bytes will it take to write out this datum in OpenPGP MPI form? */ -size_t get_openpgp_mpi_size(gnutls_datum_t* d); - -/* write the MPI stored in gnutls_datum_t to file descriptor fd: */ -int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d); diff --git a/gpg2ssh.c b/gpg2ssh.c deleted file mode 100644 index a1e94df..0000000 --- a/gpg2ssh.c +++ /dev/null @@ -1,291 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* for waitpid() */ -#include -#include - -/* - Author: Daniel Kahn Gillmor - Date: Tue, 08 Apr 2008 - License: GPL v3 or later - - monkeysphere public key translator: execute this with an GPG - certificate (public key(s) + userid(s)) on stdin. It currently - only works with RSA keys. - - It will spit out a version of the first key capable of being used - for authentication on stdout. The output format should be suitable - for appending a known_hosts file. - - Requirements: I've only built this so far with GnuTLS v2.3.4 -- - version 2.2.0 does not contain the appropriate pieces. - - */ - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_openpgp_crt_t openpgp_crt; - gnutls_openpgp_keyid_t keyid; - printable_keyid p_keyid; - unsigned int keyidx; - unsigned int usage, bits; - gnutls_pk_algorithm_t algo; - - gnutls_datum_t m, e, p, q, g, y; - gnutls_datum_t algolabel; - - char output_data[10240]; - char userid[10240]; - size_t uidsz = sizeof(userid); - - const gnutls_datum_t* all[5]; - int pipefd; - pid_t child_pid; - char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; - const char* algoname; - int mpicount; - int pipestatus; - - init_gnutls(); - - init_datum(&data); - - init_datum(&m); - init_datum(&e); - init_datum(&p); - init_datum(&q); - init_datum(&g); - init_datum(&y); - - init_datum(&algolabel); - - init_keyid(keyid); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - if (ret = gnutls_openpgp_crt_init(&openpgp_crt), ret) { - err("Failed to initialize OpenPGP certificate (error: %d)\n", ret); - return 1; - } - - /* format could be either: GNUTLS_OPENPGP_FMT_RAW, - GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, - otherwise, use BASE64: */ - - /* FIXME: we should be auto-detecting the input format, and - translating it as needed. */ - - if (getenv("MONKEYSPHERE_RAW")) { - err("assuming RAW formatted certificate\n"); - if (ret = gnutls_openpgp_crt_import(openpgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW), ret) { - err("failed to import the OpenPGP certificate in RAW format (error: %d)\n", ret); - return ret; - } - } else { - err("assuming BASE64 formatted certificate\n"); - if (ret = gnutls_openpgp_crt_import (openpgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64), ret) { - err("failed to import the OpenPGP certificate in BASE64 format (error: %d)\n", ret); - return ret; - } - } - - if (gnutls_openpgp_crt_get_revoked_status(openpgp_crt)) { - err("the primary key was revoked!\n"); - return 1; - } - - /* FIXME: We're currently looking at the primary key or maybe the - first authentication-capable subkey. - - Instead, we should be iterating through the primary key and all - subkeys: for each one with the authentication usage flag set of a - algorithm we can handle, we should output matching UserIDs and - the SSH version of the key. */ - - - if (ret = gnutls_openpgp_crt_get_key_usage(openpgp_crt, &usage), ret) { - err("failed to get the usage flags for the primary key (error: %d)\n", ret); - return ret; - } - if (usage & GNUTLS_KEY_KEY_AGREEMENT) { - err("the primary key can be used for authentication\n"); - - algo = gnutls_openpgp_crt_get_pk_algorithm(openpgp_crt, &bits); - if (algo < 0) { - err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - - err("OpenPGP RSA certificate, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_pk_rsa_raw(openpgp_crt, &m, &e); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA Key, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_pk_dsa_raw(openpgp_crt, &p, &q, &g, &y); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - - } else { - err("primary key is only good for: 0x%08x. Trying subkeys...\n", usage); - - if (ret = gnutls_openpgp_crt_get_auth_subkey(openpgp_crt, keyid, 0), ret) { - err("failed to find a subkey capable of authentication (error: %d)\n", ret); - return ret; - } - make_keyid_printable(p_keyid, keyid); - err("found authentication subkey %.16s\n", p_keyid); - - ret = gnutls_openpgp_crt_get_subkey_idx(openpgp_crt, keyid); - if (ret < 0) { - err("could not get the index of subkey %.16s (error: %d)\n", ret); - return ret; - } - keyidx = ret; - - if (gnutls_openpgp_crt_get_subkey_revoked_status(openpgp_crt, keyidx)) { - err("The authentication subkey was revoked!\n"); - return 1; - } - - if (ret = gnutls_openpgp_crt_get_subkey_usage(openpgp_crt, keyidx, &usage), ret) { - err("could not figure out usage of subkey %.16s (error: %d)\n", p_keyid, ret); - return ret; - } - if ((usage & GNUTLS_KEY_KEY_AGREEMENT) == 0) { - err("could not find a subkey with authentication privileges.\n"); - return 1; - } - - /* switch, based on the algorithm in question, to extract the MPI - components: */ - - algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(openpgp_crt, keyidx, &bits); - if (algo < 0) { - err("failed to get the algorithm of the authentication subkey (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - - err("OpenPGP RSA subkey, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(openpgp_crt, keyidx, &m, &e); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA subkey parameters (error: %d)\n", ret); - return 1; - } - } else if (algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA subkey, with %d bits\n", bits); - ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(openpgp_crt, keyidx, &p, &q, &g, &y); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA subkey parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP subkey was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - } - - /* make sure userid is NULL-terminated */ - userid[sizeof(userid) - 1] = 0; - uidsz--; - - /* FIXME: we're just choosing the first UserID from the certificate: - instead, we should be selecting every User ID that is adequately - signed and matches the spec, and aggregating them with commas for - known_hosts output */ - - if (ret = gnutls_openpgp_crt_get_name(openpgp_crt, 0, userid, &uidsz), ret) { - err("Failed to fetch the first UserID (error: %d)\n", ret); - return ret; - } - - if (ret = validate_ssh_host_userid(userid), ret) { - err("bad userid: not a valid ssh host.\n"); - return ret; - } - - /* remove ssh:// from the beginning of userid */ - memmove(userid, userid + strlen("ssh://"), 1 + strlen(userid) - strlen("ssh://")); - - - /* now we have algo, and the various MPI data are set. Can we - export them cleanly? */ - - /* for the moment, we'll just dump the info raw, and pipe it - externally through coreutils' /usr/bin/base64 */ - - if (algo == GNUTLS_PK_RSA) { - algoname = "ssh-rsa"; - mpicount = 3; - - all[0] = &algolabel; - all[1] = &e; - all[2] = &m; - } else if (algo == GNUTLS_PK_DSA) { - algoname = "ssh-dss"; - mpicount = 5; - - all[0] = &algolabel; - all[1] = &p; - all[2] = &q; - all[3] = &g; - all[4] = &y; - } else { - err("no idea what this algorithm is: %d\n", algo); - return 1; - } - - if (ret = datum_from_string(&algolabel, algoname), ret) { - err("couldn't label string (error: %d)\n", ret); - return ret; - } - - snprintf(output_data, sizeof(output_data), "%s %s ", userid, algoname); - - pipefd = create_writing_pipe(&child_pid, args[0], args); - if (pipefd < 0) { - err("failed to create a writing pipe (returned %d)\n", pipefd); - return pipefd; - } - - write(1, output_data, strlen(output_data)); - - if (0 != write_data_fd_with_length(pipefd, all, mpicount)) { - err("was not able to write out RSA key data\n"); - return 1; - } - close(pipefd); - if (child_pid != waitpid(child_pid, &pipestatus, 0)) { - err("could not wait for child process to return for some reason.\n"); - return 1; - } - if (pipestatus != 0) { - err("base64 pipe died with return code %d\n", pipestatus); - return pipestatus; - } - - write(1, "\n", 1); - - - - gnutls_openpgp_crt_deinit(openpgp_crt); - gnutls_global_deinit(); - return 0; -} diff --git a/gpg2ssh/Makefile b/gpg2ssh/Makefile new file mode 100644 index 0000000..aa18aaa --- /dev/null +++ b/gpg2ssh/Makefile @@ -0,0 +1,16 @@ +monkeysphere: main.c gnutls-helpers.o + gcc -g -Wall --pedantic -o monkeysphere main.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +gpg2ssh: gpg2ssh.c gnutls-helpers.o + gcc -g -Wall --pedantic -o gpg2ssh gpg2ssh.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +ssh2gpg: ssh2gpg.c gnutls-helpers.o + gcc -g -Wall --pedantic -o ssh2gpg ssh2gpg.c `libgnutls-config --libs --cflags` -lgnutls-extra gnutls-helpers.o + +%.o: %.c + gcc -g -Wall --pedantic -o $@ -c $< + +clean: + rm -f monkeysphere *.o + +.PHONY: clean diff --git a/gpg2ssh/gnutls-helpers.c b/gpg2ssh/gnutls-helpers.c new file mode 100644 index 0000000..6eae29e --- /dev/null +++ b/gpg2ssh/gnutls-helpers.c @@ -0,0 +1,364 @@ +/* Author: Daniel Kahn Gillmor */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + +#include "gnutls-helpers.h" +/* for htonl() */ +#include + +/* for setlocale() */ +#include + +/* for isalnum() */ +#include + +int loglevel = 0; + + +void err(const char* fmt, ...) { + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fflush(stderr); +} + +void logfunc(int level, const char* string) { + fprintf(stderr, "GnuTLS Logging (%d): %s\n", level, string); +} + +void init_keyid(gnutls_openpgp_keyid_t keyid) { + memset(keyid, 'x', sizeof(gnutls_openpgp_keyid_t)); +} + + + +void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid) +{ + static const char hex[16] = "0123456789ABCDEF"; + unsigned int kix = 0, outix = 0; + + while (kix < sizeof(gnutls_openpgp_keyid_t)) { + out[outix] = hex[(keyid[kix] >> 4) & 0x0f]; + out[outix + 1] = hex[keyid[kix] & 0x0f]; + kix++; + outix += 2; + } +} + + +int init_gnutls() { + const char* version = NULL; + const char* debug_string = NULL; + int ret; + + if (ret = gnutls_global_init(), ret) { + err("Failed to do gnutls_global_init() (error: %d)\n", ret); + return 1; + } + + version = gnutls_check_version(NULL); + + if (version) + err("gnutls version: %s\n", version); + else { + err("no version found!\n"); + return 1; + } + + if (debug_string = getenv("MONKEYSPHERE_DEBUG"), debug_string) { + loglevel = atoi(debug_string); + gnutls_global_set_log_function(logfunc); + + gnutls_global_set_log_level(loglevel); + err("set log level to %d\n", loglevel); + } + return 0; +} + +void init_datum(gnutls_datum_t* d) { + d->data = NULL; + d->size = 0; +} +void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src) { + dest->data = gnutls_realloc(dest->data, src->size); + dest->size = src->size; + memcpy(dest->data, src->data, src->size); +} +int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b) { + if (a->size > b->size) { + err("a is larger\n"); + return 1; + } + if (a->size < b->size) { + err("b is larger\n"); + return -1; + } + return memcmp(a->data, b->data, a->size); +} +void free_datum(gnutls_datum_t* d) { + gnutls_free(d->data); + d->data = NULL; + d->size = 0; +} + +/* read the passed-in string, store in a single datum */ +int set_datum_string(gnutls_datum_t* d, const char* s) { + unsigned int x = strlen(s)+1; + unsigned char* c = NULL; + + c = gnutls_realloc(d->data, x); + if (NULL == c) + return -1; + d->data = c; + d->size = x; + memcpy(d->data, s, x); + return 0; +} + +/* read the passed-in file descriptor until EOF, store in a single + datum */ +int set_datum_fd(gnutls_datum_t* d, int fd) { + unsigned int bufsize = 1024; + unsigned int len = 0; + + FILE* f = fdopen(fd, "r"); + if (bufsize > d->size) { + bufsize = 1024; + d->data = gnutls_realloc(d->data, bufsize); + if (d->data == NULL) { + err("out of memory!\n"); + return -1; + } + d->size = bufsize; + } else { + bufsize = d->size; + } + f = fdopen(fd, "r"); + if (NULL == f) { + err("could not fdopen FD %d\n", fd); + } + clearerr(f); + while (!feof(f) && !ferror(f)) { + if (len == bufsize) { + /* allocate more space by doubling: */ + bufsize *= 2; + d->data = gnutls_realloc(d->data, bufsize); + if (d->data == NULL) { + err("out of memory!\n"); + return -1; + }; + d->size = bufsize; + } + len += fread(d->data + len, 1, bufsize - len, f); + /* err("read %d bytes\n", len); */ + } + if (ferror(f)) { + err("Error reading from fd %d (error: %d) (error: %d '%s')\n", fd, ferror(f), errno, strerror(errno)); + return -1; + } + + /* touch up buffer size to match reality: */ + d->data = gnutls_realloc(d->data, len); + d->size = len; + return 0; +} + +/* read the file indicated (by name) in the fname parameter. store + its entire contents in a single datum. */ +int set_datum_file(gnutls_datum_t* d, const char* fname) { + struct stat sbuf; + unsigned char* c = NULL; + FILE* file = NULL; + size_t x = 0; + + if (0 != stat(fname, &sbuf)) { + err("failed to stat '%s'\n", fname); + return -1; + } + + c = gnutls_realloc(d->data, sbuf.st_size); + if (NULL == c) { + err("failed to allocate %d bytes for '%s'\n", sbuf.st_size, fname); + return -1; + } + + d->data = c; + d->size = sbuf.st_size; + file = fopen(fname, "r"); + if (NULL == file) { + err("failed to open '%s' for reading\n", fname); + return -1; + } + + x = fread(d->data, d->size, 1, file); + if (x != 1) { + err("tried to read %d bytes, read %d instead from '%s'\n", d->size, x, fname); + fclose(file); + return -1; + } + fclose(file); + return 0; +} + +int write_datum_fd(int fd, const gnutls_datum_t* d) { + if (d->size != write(fd, d->data, d->size)) { + err("failed to write body of datum.\n"); + return -1; + } + return 0; +} + + +int write_datum_fd_with_length(int fd, const gnutls_datum_t* d) { + uint32_t len; + int looks_negative = (d->data[0] & 0x80); + unsigned char zero = 0; + + /* if the first bit is 1, then the datum will appear negative in the + MPI encoding style used by OpenSSH. In that case, we'll increase + the length by one, and dump out one more byte */ + + if (looks_negative) { + len = htonl(d->size + 1); + } else { + len = htonl(d->size); + } + if (write(fd, &len, sizeof(len)) != sizeof(len)) { + err("failed to write size of datum.\n"); + return -2; + } + if (looks_negative) { + if (write(fd, &zero, 1) != 1) { + err("failed to write padding byte for MPI.\n"); + return -2; + } + } + return write_datum_fd(fd, d); +} + +int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num) { + unsigned int i; + int ret; + + for (i = 0; i < num; i++) + if (ret = write_datum_fd_with_length(fd, d[i]), ret != 0) + return ret; + + return 0; +} + + +int datum_from_string(gnutls_datum_t* d, const char* str) { + d->size = strlen(str); + d->data = gnutls_realloc(d->data, d->size); + if (d->data == 0) + return ENOMEM; + memcpy(d->data, str, d->size); + return 0; +} + + +int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]) { + int p[2]; + int ret; + + if (pid == NULL) { + err("bad pointer passed to create_writing_pipe()\n"); + return -1; + } + + if (ret = pipe(p), ret == -1) { + err("failed to create a pipe (error: %d \"%s\")\n", errno, strerror(errno)); + return -1; + } + + *pid = fork(); + if (*pid == -1) { + err("Failed to fork (error: %d \"%s\")\n", errno, strerror(errno)); + return -1; + } + if (*pid == 0) { /* this is the child */ + close(p[1]); /* close unused write end */ + + if (0 != dup2(p[0], 0)) { /* map the reading end into stdin */ + err("Failed to transfer reading file descriptor to stdin (error: %d \"%s\")\n", errno, strerror(errno)); + exit(1); + } + execv(path, argv); + err("exec %s failed (error: %d \"%s\")\n", path, errno, strerror(errno)); + /* close the open file descriptors */ + close(p[0]); + close(0); + + exit(1); + } else { /* this is the parent */ + close(p[0]); /* close unused read end */ + return p[1]; + } +} + +int validate_ssh_host_userid(const char* userid) { + char* oldlocale = setlocale(LC_ALL, "C"); + + /* choke if userid does not match the expected format + ("ssh://fully.qualified.domain.name") */ + if (strncmp("ssh://", userid, strlen("ssh://")) != 0) { + err("The user ID should start with ssh:// for a host key\n"); + goto fail; + } + /* so that isalnum will work properly */ + userid += strlen("ssh://"); + while (0 != (*userid)) { + if (!isalnum(*userid)) { + err("label did not start with a letter or a digit! (%s)\n", userid); + goto fail; + } + userid++; + while (isalnum(*userid) || ('-' == (*userid))) + userid++; + if (('.' == (*userid)) || (0 == (*userid))) { /* clean end of label: + check last char + isalnum */ + if (!isalnum(*(userid - 1))) { + err("label did not end with a letter or a digit!\n"); + goto fail; + } + if ('.' == (*userid)) /* advance to the start of the next label */ + userid++; + } else { + err("invalid character in domain name: %c\n", *userid); + goto fail; + } + } + /* ensure that the last character is valid: */ + if (!isalnum(*(userid - 1))) { + err("hostname did not end with a letter or a digit!\n"); + goto fail; + } + /* FIXME: fqdn's can be unicode now, thanks to RFC 3490 -- how do we + make sure that we've got an OK string? */ + + return 0; + + fail: + setlocale(LC_ALL, oldlocale); + return 1; +} + +/* http://tools.ietf.org/html/rfc4880#section-5.5.2 */ +size_t get_openpgp_mpi_size(gnutls_datum_t* d) { + return 2 + d->size; +} + +int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d) { + uint16_t x; + + x = d->size * 8; + x = htons(x); + + write(fd, &x, sizeof(x)); + write(fd, d->data, d->size); + + return 0; +} diff --git a/gpg2ssh/gnutls-helpers.h b/gpg2ssh/gnutls-helpers.h new file mode 100644 index 0000000..9ea22a3 --- /dev/null +++ b/gpg2ssh/gnutls-helpers.h @@ -0,0 +1,72 @@ +/* Author: Daniel Kahn Gillmor */ +/* Date: Fri, 04 Apr 2008 19:31:16 -0400 */ +/* License: GPL v3 or later */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Functions to help dealing with GnuTLS for monkeysphere key + translation projects: */ + +/* set everything up, including logging levels. Return 0 on + success */ +int init_gnutls(); + +/* logging and output functions: */ + +void err(const char* fmt, ...); +void logfunc(int level, const char* string); + +/* basic datum manipulations: */ + +void init_datum(gnutls_datum_t* d); +void copy_datum(gnutls_datum_t* dest, const gnutls_datum_t* src); +int compare_data(const gnutls_datum_t* a, const gnutls_datum_t* b); +void free_datum(gnutls_datum_t* d); +int write_datum_fd(int fd, const gnutls_datum_t* d); +int write_datum_fd_with_length(int fd, const gnutls_datum_t* d); +int write_data_fd_with_length(int fd, const gnutls_datum_t** d, unsigned int num); + +/* set up a datum from a null-terminated string */ +int datum_from_string(gnutls_datum_t* d, const char* str); + +/* keyid manipulations: */ +typedef unsigned char printable_keyid[16]; + +void init_keyid(gnutls_openpgp_keyid_t keyid); +void make_keyid_printable(printable_keyid out, gnutls_openpgp_keyid_t keyid); + +/* functions to get data into datum objects: */ + +/* read the passed-in string, store in a single datum */ +int set_datum_string(gnutls_datum_t* d, const char* s); + +/* read the passed-in file descriptor until EOF, store in a single + datum */ +int set_datum_fd(gnutls_datum_t* d, int fd); + +/* read the file indicated (by name) in the fname parameter. store + its entire contents in a single datum. */ +int set_datum_file(gnutls_datum_t* d, const char* fname); + +/* set up file descriptor pipe for writing (child process pid gets + stored in pid, fd is returned)*/ +int create_writing_pipe(pid_t* pid, const char* path, char* const argv[]); + +/* return 0 if userid matches the monkeysphere spec for ssh host user IDs */ +int validate_ssh_host_userid(const char* userid); + +/* how many bytes will it take to write out this datum in OpenPGP MPI form? */ +size_t get_openpgp_mpi_size(gnutls_datum_t* d); + +/* write the MPI stored in gnutls_datum_t to file descriptor fd: */ +int write_openpgp_mpi_to_fd(int fd, gnutls_datum_t* d); diff --git a/gpg2ssh/gpg2ssh.c b/gpg2ssh/gpg2ssh.c new file mode 100644 index 0000000..a1e94df --- /dev/null +++ b/gpg2ssh/gpg2ssh.c @@ -0,0 +1,291 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* for waitpid() */ +#include +#include + +/* + Author: Daniel Kahn Gillmor + Date: Tue, 08 Apr 2008 + License: GPL v3 or later + + monkeysphere public key translator: execute this with an GPG + certificate (public key(s) + userid(s)) on stdin. It currently + only works with RSA keys. + + It will spit out a version of the first key capable of being used + for authentication on stdout. The output format should be suitable + for appending a known_hosts file. + + Requirements: I've only built this so far with GnuTLS v2.3.4 -- + version 2.2.0 does not contain the appropriate pieces. + + */ + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_openpgp_crt_t openpgp_crt; + gnutls_openpgp_keyid_t keyid; + printable_keyid p_keyid; + unsigned int keyidx; + unsigned int usage, bits; + gnutls_pk_algorithm_t algo; + + gnutls_datum_t m, e, p, q, g, y; + gnutls_datum_t algolabel; + + char output_data[10240]; + char userid[10240]; + size_t uidsz = sizeof(userid); + + const gnutls_datum_t* all[5]; + int pipefd; + pid_t child_pid; + char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; + const char* algoname; + int mpicount; + int pipestatus; + + init_gnutls(); + + init_datum(&data); + + init_datum(&m); + init_datum(&e); + init_datum(&p); + init_datum(&q); + init_datum(&g); + init_datum(&y); + + init_datum(&algolabel); + + init_keyid(keyid); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + if (ret = gnutls_openpgp_crt_init(&openpgp_crt), ret) { + err("Failed to initialize OpenPGP certificate (error: %d)\n", ret); + return 1; + } + + /* format could be either: GNUTLS_OPENPGP_FMT_RAW, + GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, + otherwise, use BASE64: */ + + /* FIXME: we should be auto-detecting the input format, and + translating it as needed. */ + + if (getenv("MONKEYSPHERE_RAW")) { + err("assuming RAW formatted certificate\n"); + if (ret = gnutls_openpgp_crt_import(openpgp_crt, &data, GNUTLS_OPENPGP_FMT_RAW), ret) { + err("failed to import the OpenPGP certificate in RAW format (error: %d)\n", ret); + return ret; + } + } else { + err("assuming BASE64 formatted certificate\n"); + if (ret = gnutls_openpgp_crt_import (openpgp_crt, &data, GNUTLS_OPENPGP_FMT_BASE64), ret) { + err("failed to import the OpenPGP certificate in BASE64 format (error: %d)\n", ret); + return ret; + } + } + + if (gnutls_openpgp_crt_get_revoked_status(openpgp_crt)) { + err("the primary key was revoked!\n"); + return 1; + } + + /* FIXME: We're currently looking at the primary key or maybe the + first authentication-capable subkey. + + Instead, we should be iterating through the primary key and all + subkeys: for each one with the authentication usage flag set of a + algorithm we can handle, we should output matching UserIDs and + the SSH version of the key. */ + + + if (ret = gnutls_openpgp_crt_get_key_usage(openpgp_crt, &usage), ret) { + err("failed to get the usage flags for the primary key (error: %d)\n", ret); + return ret; + } + if (usage & GNUTLS_KEY_KEY_AGREEMENT) { + err("the primary key can be used for authentication\n"); + + algo = gnutls_openpgp_crt_get_pk_algorithm(openpgp_crt, &bits); + if (algo < 0) { + err("failed to get the algorithm of the OpenPGP public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + + err("OpenPGP RSA certificate, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_rsa_raw(openpgp_crt, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA Key, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_pk_dsa_raw(openpgp_crt, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + + } else { + err("primary key is only good for: 0x%08x. Trying subkeys...\n", usage); + + if (ret = gnutls_openpgp_crt_get_auth_subkey(openpgp_crt, keyid, 0), ret) { + err("failed to find a subkey capable of authentication (error: %d)\n", ret); + return ret; + } + make_keyid_printable(p_keyid, keyid); + err("found authentication subkey %.16s\n", p_keyid); + + ret = gnutls_openpgp_crt_get_subkey_idx(openpgp_crt, keyid); + if (ret < 0) { + err("could not get the index of subkey %.16s (error: %d)\n", ret); + return ret; + } + keyidx = ret; + + if (gnutls_openpgp_crt_get_subkey_revoked_status(openpgp_crt, keyidx)) { + err("The authentication subkey was revoked!\n"); + return 1; + } + + if (ret = gnutls_openpgp_crt_get_subkey_usage(openpgp_crt, keyidx, &usage), ret) { + err("could not figure out usage of subkey %.16s (error: %d)\n", p_keyid, ret); + return ret; + } + if ((usage & GNUTLS_KEY_KEY_AGREEMENT) == 0) { + err("could not find a subkey with authentication privileges.\n"); + return 1; + } + + /* switch, based on the algorithm in question, to extract the MPI + components: */ + + algo = gnutls_openpgp_crt_get_subkey_pk_algorithm(openpgp_crt, keyidx, &bits); + if (algo < 0) { + err("failed to get the algorithm of the authentication subkey (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + + err("OpenPGP RSA subkey, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_rsa_raw(openpgp_crt, keyidx, &m, &e); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA subkey parameters (error: %d)\n", ret); + return 1; + } + } else if (algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA subkey, with %d bits\n", bits); + ret = gnutls_openpgp_crt_get_subkey_pk_dsa_raw(openpgp_crt, keyidx, &p, &q, &g, &y); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA subkey parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP subkey was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + } + + /* make sure userid is NULL-terminated */ + userid[sizeof(userid) - 1] = 0; + uidsz--; + + /* FIXME: we're just choosing the first UserID from the certificate: + instead, we should be selecting every User ID that is adequately + signed and matches the spec, and aggregating them with commas for + known_hosts output */ + + if (ret = gnutls_openpgp_crt_get_name(openpgp_crt, 0, userid, &uidsz), ret) { + err("Failed to fetch the first UserID (error: %d)\n", ret); + return ret; + } + + if (ret = validate_ssh_host_userid(userid), ret) { + err("bad userid: not a valid ssh host.\n"); + return ret; + } + + /* remove ssh:// from the beginning of userid */ + memmove(userid, userid + strlen("ssh://"), 1 + strlen(userid) - strlen("ssh://")); + + + /* now we have algo, and the various MPI data are set. Can we + export them cleanly? */ + + /* for the moment, we'll just dump the info raw, and pipe it + externally through coreutils' /usr/bin/base64 */ + + if (algo == GNUTLS_PK_RSA) { + algoname = "ssh-rsa"; + mpicount = 3; + + all[0] = &algolabel; + all[1] = &e; + all[2] = &m; + } else if (algo == GNUTLS_PK_DSA) { + algoname = "ssh-dss"; + mpicount = 5; + + all[0] = &algolabel; + all[1] = &p; + all[2] = &q; + all[3] = &g; + all[4] = &y; + } else { + err("no idea what this algorithm is: %d\n", algo); + return 1; + } + + if (ret = datum_from_string(&algolabel, algoname), ret) { + err("couldn't label string (error: %d)\n", ret); + return ret; + } + + snprintf(output_data, sizeof(output_data), "%s %s ", userid, algoname); + + pipefd = create_writing_pipe(&child_pid, args[0], args); + if (pipefd < 0) { + err("failed to create a writing pipe (returned %d)\n", pipefd); + return pipefd; + } + + write(1, output_data, strlen(output_data)); + + if (0 != write_data_fd_with_length(pipefd, all, mpicount)) { + err("was not able to write out RSA key data\n"); + return 1; + } + close(pipefd); + if (child_pid != waitpid(child_pid, &pipestatus, 0)) { + err("could not wait for child process to return for some reason.\n"); + return 1; + } + if (pipestatus != 0) { + err("base64 pipe died with return code %d\n", pipestatus); + return pipestatus; + } + + write(1, "\n", 1); + + + + gnutls_openpgp_crt_deinit(openpgp_crt); + gnutls_global_deinit(); + return 0; +} diff --git a/gpg2ssh/main.c b/gpg2ssh/main.c new file mode 100644 index 0000000..d6bac68 --- /dev/null +++ b/gpg2ssh/main.c @@ -0,0 +1,271 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* + Author: Daniel Kahn Gillmor + Date: Tue, 01 Apr 2008 + License: GPL v3 or later + + monkeysphere private key translator: execute this with an GPG + secret key on stdin (at the moment, only passphraseless RSA keys + work). + + It will spit out a PEM-encoded version of the key on stdout, which + can be fed into ssh-add like this: + + gpg --export-secret-keys $KEYID | monkeysphere | ssh-add -c /dev/stdin + + Requirements: I've only built this so far with GnuTLS v2.3.4 -- + version 2.2.0 does not contain the appropriate pieces. + + Notes: gpgkey2ssh doesn't seem to provide the same public + keys. Mighty weird! + +0 wt215@squeak:~/monkeysphere$ gpg --export-secret-keys 1DCDF89F | ~dkg/src/monkeysphere/monkeysphere | ssh-add -c /dev/stdin +gnutls version: 2.3.4 +OpenPGP RSA Key, with 1024 bits +Identity added: /dev/stdin (/dev/stdin) +The user has to confirm each use of the key +0 wt215@squeak:~/monkeysphere$ ssh-add -L +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9gWQqfrnhQKDQnND/3eOexpddE64J+1zp9fcyCje7H5LKclb6DBV2HS6WgW32PJhIzvP+fYZM3dzXea3fpv14y1SicXiRBDgF9SnsNA1qWn2RyzkLcKy7PmM0PDYtU1oiLTcQj/xkWcqW2sLKHT/WW+vZP5XP7RMGN/yWNMfE2Q== /dev/stdin +0 wt215@squeak:~/monkeysphere$ gpgkey2ssh 1DCDF89F +ssh-rsa AAAAB3NzaC1yc2EAAACBAL2BZCp+ueFAoNCc0P/d457Gl10Trgn7XOn19zIKN7sfkspyVvoMFXYdLpaBbfY8mEjO8/59hkzd3Nd5rd+m/XjLVKJxeJEEOAX1Kew0DWpafZHLOQtwrLs+YzQ8Ni1TWiItNxCP/GRZypbawsodP9Zb69k/lc/tEwY3/JY0x8TZAAAAAwEAAQ== COMMENT +0 wt215@squeak:~/monkeysphere$ + + */ + + +int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) { + gnutls_openpgp_privkey_t pgp_privkey; + gnutls_datum_t m, e, d, p, q, u, g, y, x; + gnutls_pk_algorithm_t pgp_algo; + unsigned int pgp_bits; + int ret; + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + init_datum(&x); + + if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) { + err("Failed to initialized OpenPGP private key (error: %d)\n", ret); + return 1; + } + + + /* format could be either: GNUTLS_OPENPGP_FMT_RAW, + GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, + otherwise, use BASE64: */ + + if (getenv("MONKEYSPHERE_RAW")) { + err("assuming RAW formatted private keys\n"); + if (ret = gnutls_openpgp_privkey_import(pgp_privkey, input, GNUTLS_OPENPGP_FMT_RAW, NULL, 0), ret) + err("failed to import the OpenPGP private key in RAW format (error: %d)\n", ret); + } else { + err("assuming BASE64 formatted private keys\n"); + if (ret = gnutls_openpgp_privkey_import (pgp_privkey, input, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0), ret) + err("failed to import the OpenPGP private key in BASE64 format (error: %d)\n", ret); + } + + pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(pgp_privkey, &pgp_bits); + if (pgp_algo < 0) { + err("failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo); + return 1; + } + if (pgp_algo == GNUTLS_PK_RSA) { + err("OpenPGP RSA Key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_rsa_raw(pgp_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + + ret = gnutls_x509_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (pgp_algo == GNUTLS_PK_DSA) { + err("OpenPGP DSA Key, with %d bits\n", pgp_bits); + ret = gnutls_openpgp_privkey_export_dsa_raw(pgp_privkey, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + + ret = gnutls_x509_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", pgp_algo); + return 1; + } + + ret = gnutls_x509_privkey_fix(*output); + if (ret != 0) { + err("failed to fix up the private key in X.509 format (error: %d)\n", ret); + return 1; + } + + gnutls_openpgp_privkey_deinit(pgp_privkey); + return 0; +} + +int convert_x509_to_pgp(gnutls_openpgp_privkey_t* output, gnutls_datum_t* input) { + gnutls_x509_privkey_t x509_privkey; + gnutls_datum_t m, e, d, p, q, u, g, y, x; + gnutls_pk_algorithm_t x509_algo; + int ret; + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + init_datum(&x); + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialized X.509 private key (error: %d)\n", ret); + return 1; + } + + + /* format could be either: GNUTLS_X509_FMT_DER, + GNUTLS_X509_FMT_PEM; if MONKEYSPHERE_DER is set, use DER, + otherwise, use PEM: */ + + if (getenv("MONKEYSPHERE_DER")) { + err("assuming DER formatted private keys\n"); + if (ret = gnutls_x509_privkey_import(x509_privkey, input, GNUTLS_X509_FMT_DER), ret) + err("failed to import the X.509 private key in DER format (error: %d)\n", ret); + } else { + err("assuming PEM formatted private keys\n"); + if (ret = gnutls_x509_privkey_import (x509_privkey, input, GNUTLS_X509_FMT_PEM), ret) + err("failed to import the X.509 private key in PEM format (error: %d)\n", ret); + } + + x509_algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); + if (x509_algo < 0) { + err("failed to get X.509 key algorithm (error: %d)\n", x509_algo); + return 1; + } + if (x509_algo == GNUTLS_PK_RSA) { + err("X.509 RSA Key\n"); + ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + + /* ret = gnutls_openpgp_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); */ + ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import RSA key parameters (error: %d)\n", ret); + return 1; + } + } else if (x509_algo == GNUTLS_PK_DSA) { + err("X.509 DSA Key\n"); + ret = gnutls_x509_privkey_export_dsa_raw(x509_privkey, &p, &q, &g, &y, &x); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export DSA key parameters (error: %d)\n", ret); + return 1; + } + + /* ret = gnutls_openpgp_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); */ + ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to import DSA key parameters (error: %d)\n", ret); + return 1; + } + } else { + err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", x509_algo); + return 1; + } + + gnutls_x509_privkey_deinit(x509_privkey); + return 0; +} + + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_x509_privkey_t x509_privkey; + + char output_data[10240]; + size_t ods = sizeof(output_data); + + init_gnutls(); + + init_datum(&data); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + + /* Or, instead, read in key from a file name: + if (ret = set_datum_file(&data, argv[1]), ret) { + err("didn't read file '%s'\n", argv[1]); + return 1; + } +*/ + + /* treat the passed file as an X.509 private key, and extract its + component values: */ + +/* if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { */ +/* err("Failed to import the X.509 key (error: %d)\n", ret); */ +/* return 1; */ +/* } */ +/* gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); */ + + /* try to print the PEM-encoded private key: */ +/* ret = gnutls_x509_privkey_export (x509_privkey, */ +/* GNUTLS_X509_FMT_PEM, */ +/* output_data, */ +/* &ods); */ +/* printf("ret: %u; ods: %u;\n", ret, ods); */ +/* if (ret == 0) { */ +/* write(0, output_data, ods); */ +/* } */ + + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialize X.509 private key (error: %d)\n", ret); + return 1; + } + + if (ret = convert_pgp_to_x509(&x509_privkey, &data), ret) { + return ret; + } + + ret = gnutls_x509_privkey_export (x509_privkey, + GNUTLS_X509_FMT_PEM, + output_data, + &ods); + printf("ret: %u; ods: %u;\n", ret, ods); + if (ret == 0) { + write(1, output_data, ods); + } + + + gnutls_x509_privkey_deinit(x509_privkey); + gnutls_global_deinit(); + return 0; +} diff --git a/gpg2ssh/ssh2gpg.c b/gpg2ssh/ssh2gpg.c new file mode 100644 index 0000000..b14a540 --- /dev/null +++ b/gpg2ssh/ssh2gpg.c @@ -0,0 +1,171 @@ +#include "gnutls-helpers.h" + +#include +#include + +/* for waitpid() */ +#include +#include + +/* for time() */ +#include + +/* for htons() */ +#include + + +/* + Author: Daniel Kahn Gillmor + Date: Sun, 2008-04-20 + License: GPL v3 or later + + monkeysphere public key translator: execute this with an ssh + private key on stdin. It currently only works with RSA keys. + + it should eventually work with OpenSSH-style public keys instead of + the full private key, but it was easier to do this way. + + It shoud spit out a version of the public key suitable for acting + as an OpenPGP public sub key packet. + + */ + +int main(int argc, char* argv[]) { + gnutls_datum_t data; + int ret; + gnutls_x509_privkey_t x509_privkey; + gnutls_openpgp_crt_t openpgp_crt; + gnutls_openpgp_keyid_t keyid; + printable_keyid p_keyid; + unsigned int keyidx; + unsigned int usage, bits; + gnutls_pk_algorithm_t algo; + + unsigned char packettag; + unsigned char openpgpversion; + time_t timestamp; + uint32_t clunkytime; + unsigned char openpgpalgo; + unsigned int packetlen; + uint16_t plen; + + gnutls_datum_t m, e, d, p, q, u, g, y; + gnutls_datum_t algolabel; + + char output_data[10240]; + char userid[10240]; + size_t uidsz = sizeof(userid); + + const gnutls_datum_t* all[5]; + int pipefd; + pid_t child_pid; + char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; + const char* algoname; + int mpicount; + int pipestatus; + + init_gnutls(); + + init_datum(&data); + + init_datum(&m); + init_datum(&e); + init_datum(&d); + init_datum(&p); + init_datum(&q); + init_datum(&u); + init_datum(&g); + init_datum(&y); + + init_datum(&algolabel); + + init_keyid(keyid); + + /* slurp in the private key from stdin */ + if (ret = set_datum_fd(&data, 0), ret) { + err("didn't read file descriptor 0\n"); + return 1; + } + + + if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { + err("Failed to initialize private key structure (error: %d)\n", ret); + return 1; + } + + err("assuming PEM formatted private key\n"); + if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { + err("failed to import the PEM-encoded private key (error: %d)\n", ret); + return ret; + } + + algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); + if (algo < 0) { + err("failed to get the algorithm of the PEM-encoded public key (error: %d)\n", algo); + return algo; + } else if (algo == GNUTLS_PK_RSA) { + err("RSA private key\n"); + ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); + if (GNUTLS_E_SUCCESS != ret) { + err ("failed to export RSA key parameters (error: %d)\n", ret); + return 1; + } + err("Modulus size %d, exponent size %d\n", m.size, e.size); + } else if (algo == GNUTLS_PK_DSA) { + err("DSA Key, not implemented!!\n", bits); + return 1; + } else { + err("Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); + return 1; + } + + /* now we have algo, and the various MPI data are set. Can we + export them as a public subkey packet? */ + + /* this packet should be tagged 14, and should contain: + + 1 octet: version (4) + 4 octets: time of generation (seconds since 1970) + 1 octet: algo (http://tools.ietf.org/html/rfc4880#section-5.5.2 implies 1 for RSA) + + MPI: modulus + MPI: exponent + */ + + packetlen = 1 + 4 + 1; + /* FIXME: this is RSA only. for DSA, there'll be more: */ + packetlen += get_openpgp_mpi_size(&m) + get_openpgp_mpi_size(&e); + + /* FIXME: we should generate this bound more cleanly -- i just + happen to know that 65535 is 2^16-1: */ + if (packetlen > 65535) { + err("packet length is too long (%d)\n", packetlen); + return 1; + } + + /* we're going to emit an old-style packet, with tag 14 (public + subkey), with a two-octet packet length */ + packettag = 0x80 | (14 << 2) | 1; + + write(1, &packettag, sizeof(packettag)); + plen = htons(packetlen); + write(1, &plen, sizeof(plen)); + + openpgpversion = 4; + write(1, &openpgpversion, 1); + + timestamp = time(NULL); + clunkytime = htonl(timestamp); + write(1, &clunkytime, 4); + + /* FIXME: handle things other than RSA */ + openpgpalgo = 1; + write(1, &openpgpalgo, 1); + + write_openpgp_mpi_to_fd(1, &m); + write_openpgp_mpi_to_fd(1, &e); + + gnutls_x509_privkey_deinit(x509_privkey); + gnutls_global_deinit(); + return 0; +} diff --git a/main.c b/main.c deleted file mode 100644 index d6bac68..0000000 --- a/main.c +++ /dev/null @@ -1,271 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* - Author: Daniel Kahn Gillmor - Date: Tue, 01 Apr 2008 - License: GPL v3 or later - - monkeysphere private key translator: execute this with an GPG - secret key on stdin (at the moment, only passphraseless RSA keys - work). - - It will spit out a PEM-encoded version of the key on stdout, which - can be fed into ssh-add like this: - - gpg --export-secret-keys $KEYID | monkeysphere | ssh-add -c /dev/stdin - - Requirements: I've only built this so far with GnuTLS v2.3.4 -- - version 2.2.0 does not contain the appropriate pieces. - - Notes: gpgkey2ssh doesn't seem to provide the same public - keys. Mighty weird! - -0 wt215@squeak:~/monkeysphere$ gpg --export-secret-keys 1DCDF89F | ~dkg/src/monkeysphere/monkeysphere | ssh-add -c /dev/stdin -gnutls version: 2.3.4 -OpenPGP RSA Key, with 1024 bits -Identity added: /dev/stdin (/dev/stdin) -The user has to confirm each use of the key -0 wt215@squeak:~/monkeysphere$ ssh-add -L -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC9gWQqfrnhQKDQnND/3eOexpddE64J+1zp9fcyCje7H5LKclb6DBV2HS6WgW32PJhIzvP+fYZM3dzXea3fpv14y1SicXiRBDgF9SnsNA1qWn2RyzkLcKy7PmM0PDYtU1oiLTcQj/xkWcqW2sLKHT/WW+vZP5XP7RMGN/yWNMfE2Q== /dev/stdin -0 wt215@squeak:~/monkeysphere$ gpgkey2ssh 1DCDF89F -ssh-rsa AAAAB3NzaC1yc2EAAACBAL2BZCp+ueFAoNCc0P/d457Gl10Trgn7XOn19zIKN7sfkspyVvoMFXYdLpaBbfY8mEjO8/59hkzd3Nd5rd+m/XjLVKJxeJEEOAX1Kew0DWpafZHLOQtwrLs+YzQ8Ni1TWiItNxCP/GRZypbawsodP9Zb69k/lc/tEwY3/JY0x8TZAAAAAwEAAQ== COMMENT -0 wt215@squeak:~/monkeysphere$ - - */ - - -int convert_pgp_to_x509(gnutls_x509_privkey_t* output, gnutls_datum_t* input) { - gnutls_openpgp_privkey_t pgp_privkey; - gnutls_datum_t m, e, d, p, q, u, g, y, x; - gnutls_pk_algorithm_t pgp_algo; - unsigned int pgp_bits; - int ret; - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - init_datum(&x); - - if (ret = gnutls_openpgp_privkey_init(&pgp_privkey), ret) { - err("Failed to initialized OpenPGP private key (error: %d)\n", ret); - return 1; - } - - - /* format could be either: GNUTLS_OPENPGP_FMT_RAW, - GNUTLS_OPENPGP_FMT_BASE64; if MONKEYSPHERE_RAW is set, use RAW, - otherwise, use BASE64: */ - - if (getenv("MONKEYSPHERE_RAW")) { - err("assuming RAW formatted private keys\n"); - if (ret = gnutls_openpgp_privkey_import(pgp_privkey, input, GNUTLS_OPENPGP_FMT_RAW, NULL, 0), ret) - err("failed to import the OpenPGP private key in RAW format (error: %d)\n", ret); - } else { - err("assuming BASE64 formatted private keys\n"); - if (ret = gnutls_openpgp_privkey_import (pgp_privkey, input, GNUTLS_OPENPGP_FMT_BASE64, NULL, 0), ret) - err("failed to import the OpenPGP private key in BASE64 format (error: %d)\n", ret); - } - - pgp_algo = gnutls_openpgp_privkey_get_pk_algorithm(pgp_privkey, &pgp_bits); - if (pgp_algo < 0) { - err("failed to get OpenPGP key algorithm (error: %d)\n", pgp_algo); - return 1; - } - if (pgp_algo == GNUTLS_PK_RSA) { - err("OpenPGP RSA Key, with %d bits\n", pgp_bits); - ret = gnutls_openpgp_privkey_export_rsa_raw(pgp_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - - ret = gnutls_x509_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (pgp_algo == GNUTLS_PK_DSA) { - err("OpenPGP DSA Key, with %d bits\n", pgp_bits); - ret = gnutls_openpgp_privkey_export_dsa_raw(pgp_privkey, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - - ret = gnutls_x509_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", pgp_algo); - return 1; - } - - ret = gnutls_x509_privkey_fix(*output); - if (ret != 0) { - err("failed to fix up the private key in X.509 format (error: %d)\n", ret); - return 1; - } - - gnutls_openpgp_privkey_deinit(pgp_privkey); - return 0; -} - -int convert_x509_to_pgp(gnutls_openpgp_privkey_t* output, gnutls_datum_t* input) { - gnutls_x509_privkey_t x509_privkey; - gnutls_datum_t m, e, d, p, q, u, g, y, x; - gnutls_pk_algorithm_t x509_algo; - int ret; - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - init_datum(&x); - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialized X.509 private key (error: %d)\n", ret); - return 1; - } - - - /* format could be either: GNUTLS_X509_FMT_DER, - GNUTLS_X509_FMT_PEM; if MONKEYSPHERE_DER is set, use DER, - otherwise, use PEM: */ - - if (getenv("MONKEYSPHERE_DER")) { - err("assuming DER formatted private keys\n"); - if (ret = gnutls_x509_privkey_import(x509_privkey, input, GNUTLS_X509_FMT_DER), ret) - err("failed to import the X.509 private key in DER format (error: %d)\n", ret); - } else { - err("assuming PEM formatted private keys\n"); - if (ret = gnutls_x509_privkey_import (x509_privkey, input, GNUTLS_X509_FMT_PEM), ret) - err("failed to import the X.509 private key in PEM format (error: %d)\n", ret); - } - - x509_algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); - if (x509_algo < 0) { - err("failed to get X.509 key algorithm (error: %d)\n", x509_algo); - return 1; - } - if (x509_algo == GNUTLS_PK_RSA) { - err("X.509 RSA Key\n"); - ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - - /* ret = gnutls_openpgp_privkey_import_rsa_raw (*output, &m, &e, &d, &p, &q, &u); */ - ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import RSA key parameters (error: %d)\n", ret); - return 1; - } - } else if (x509_algo == GNUTLS_PK_DSA) { - err("X.509 DSA Key\n"); - ret = gnutls_x509_privkey_export_dsa_raw(x509_privkey, &p, &q, &g, &y, &x); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export DSA key parameters (error: %d)\n", ret); - return 1; - } - - /* ret = gnutls_openpgp_privkey_import_dsa_raw (*output, &p, &q, &g, &y, &x); */ - ret = GNUTLS_E_UNIMPLEMENTED_FEATURE; - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to import DSA key parameters (error: %d)\n", ret); - return 1; - } - } else { - err("OpenPGP Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", x509_algo); - return 1; - } - - gnutls_x509_privkey_deinit(x509_privkey); - return 0; -} - - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_x509_privkey_t x509_privkey; - - char output_data[10240]; - size_t ods = sizeof(output_data); - - init_gnutls(); - - init_datum(&data); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - - /* Or, instead, read in key from a file name: - if (ret = set_datum_file(&data, argv[1]), ret) { - err("didn't read file '%s'\n", argv[1]); - return 1; - } -*/ - - /* treat the passed file as an X.509 private key, and extract its - component values: */ - -/* if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { */ -/* err("Failed to import the X.509 key (error: %d)\n", ret); */ -/* return 1; */ -/* } */ -/* gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); */ - - /* try to print the PEM-encoded private key: */ -/* ret = gnutls_x509_privkey_export (x509_privkey, */ -/* GNUTLS_X509_FMT_PEM, */ -/* output_data, */ -/* &ods); */ -/* printf("ret: %u; ods: %u;\n", ret, ods); */ -/* if (ret == 0) { */ -/* write(0, output_data, ods); */ -/* } */ - - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialize X.509 private key (error: %d)\n", ret); - return 1; - } - - if (ret = convert_pgp_to_x509(&x509_privkey, &data), ret) { - return ret; - } - - ret = gnutls_x509_privkey_export (x509_privkey, - GNUTLS_X509_FMT_PEM, - output_data, - &ods); - printf("ret: %u; ods: %u;\n", ret, ods); - if (ret == 0) { - write(1, output_data, ods); - } - - - gnutls_x509_privkey_deinit(x509_privkey); - gnutls_global_deinit(); - return 0; -} diff --git a/ssh2gpg.c b/ssh2gpg.c deleted file mode 100644 index b14a540..0000000 --- a/ssh2gpg.c +++ /dev/null @@ -1,171 +0,0 @@ -#include "gnutls-helpers.h" - -#include -#include - -/* for waitpid() */ -#include -#include - -/* for time() */ -#include - -/* for htons() */ -#include - - -/* - Author: Daniel Kahn Gillmor - Date: Sun, 2008-04-20 - License: GPL v3 or later - - monkeysphere public key translator: execute this with an ssh - private key on stdin. It currently only works with RSA keys. - - it should eventually work with OpenSSH-style public keys instead of - the full private key, but it was easier to do this way. - - It shoud spit out a version of the public key suitable for acting - as an OpenPGP public sub key packet. - - */ - -int main(int argc, char* argv[]) { - gnutls_datum_t data; - int ret; - gnutls_x509_privkey_t x509_privkey; - gnutls_openpgp_crt_t openpgp_crt; - gnutls_openpgp_keyid_t keyid; - printable_keyid p_keyid; - unsigned int keyidx; - unsigned int usage, bits; - gnutls_pk_algorithm_t algo; - - unsigned char packettag; - unsigned char openpgpversion; - time_t timestamp; - uint32_t clunkytime; - unsigned char openpgpalgo; - unsigned int packetlen; - uint16_t plen; - - gnutls_datum_t m, e, d, p, q, u, g, y; - gnutls_datum_t algolabel; - - char output_data[10240]; - char userid[10240]; - size_t uidsz = sizeof(userid); - - const gnutls_datum_t* all[5]; - int pipefd; - pid_t child_pid; - char* const args[] = {"/usr/bin/base64", "--wrap=0", NULL}; - const char* algoname; - int mpicount; - int pipestatus; - - init_gnutls(); - - init_datum(&data); - - init_datum(&m); - init_datum(&e); - init_datum(&d); - init_datum(&p); - init_datum(&q); - init_datum(&u); - init_datum(&g); - init_datum(&y); - - init_datum(&algolabel); - - init_keyid(keyid); - - /* slurp in the private key from stdin */ - if (ret = set_datum_fd(&data, 0), ret) { - err("didn't read file descriptor 0\n"); - return 1; - } - - - if (ret = gnutls_x509_privkey_init(&x509_privkey), ret) { - err("Failed to initialize private key structure (error: %d)\n", ret); - return 1; - } - - err("assuming PEM formatted private key\n"); - if (ret = gnutls_x509_privkey_import(x509_privkey, &data, GNUTLS_X509_FMT_PEM), ret) { - err("failed to import the PEM-encoded private key (error: %d)\n", ret); - return ret; - } - - algo = gnutls_x509_privkey_get_pk_algorithm(x509_privkey); - if (algo < 0) { - err("failed to get the algorithm of the PEM-encoded public key (error: %d)\n", algo); - return algo; - } else if (algo == GNUTLS_PK_RSA) { - err("RSA private key\n"); - ret = gnutls_x509_privkey_export_rsa_raw(x509_privkey, &m, &e, &d, &p, &q, &u); - if (GNUTLS_E_SUCCESS != ret) { - err ("failed to export RSA key parameters (error: %d)\n", ret); - return 1; - } - err("Modulus size %d, exponent size %d\n", m.size, e.size); - } else if (algo == GNUTLS_PK_DSA) { - err("DSA Key, not implemented!!\n", bits); - return 1; - } else { - err("Key was not RSA or DSA -- can't deal! (actual algorithm was: %d)\n", algo); - return 1; - } - - /* now we have algo, and the various MPI data are set. Can we - export them as a public subkey packet? */ - - /* this packet should be tagged 14, and should contain: - - 1 octet: version (4) - 4 octets: time of generation (seconds since 1970) - 1 octet: algo (http://tools.ietf.org/html/rfc4880#section-5.5.2 implies 1 for RSA) - - MPI: modulus - MPI: exponent - */ - - packetlen = 1 + 4 + 1; - /* FIXME: this is RSA only. for DSA, there'll be more: */ - packetlen += get_openpgp_mpi_size(&m) + get_openpgp_mpi_size(&e); - - /* FIXME: we should generate this bound more cleanly -- i just - happen to know that 65535 is 2^16-1: */ - if (packetlen > 65535) { - err("packet length is too long (%d)\n", packetlen); - return 1; - } - - /* we're going to emit an old-style packet, with tag 14 (public - subkey), with a two-octet packet length */ - packettag = 0x80 | (14 << 2) | 1; - - write(1, &packettag, sizeof(packettag)); - plen = htons(packetlen); - write(1, &plen, sizeof(plen)); - - openpgpversion = 4; - write(1, &openpgpversion, 1); - - timestamp = time(NULL); - clunkytime = htonl(timestamp); - write(1, &clunkytime, 4); - - /* FIXME: handle things other than RSA */ - openpgpalgo = 1; - write(1, &openpgpalgo, 1); - - write_openpgp_mpi_to_fd(1, &m); - write_openpgp_mpi_to_fd(1, &e); - - gnutls_x509_privkey_deinit(x509_privkey); - gnutls_global_deinit(); - return 0; -} -- cgit v1.2.3 From 4eba4e7e66fc7febb1e7255a649f6b6ad240d653 Mon Sep 17 00:00:00 2001 From: Jameson Graef Rollins Date: Sun, 25 May 2008 15:59:54 -0400 Subject: expand howler to handle general gpg maintenence tasks for server - add "gen-key", "publish-key", and "trust-uids" functions small tweak to rhesus. update README and MonkeySpec --- doc/MonkeySpec | 5 ++- doc/README | 50 ++++++++++++++++++---- howler/howler | 130 +++++++++++++++++++++++++++++++++++++++------------------ rhesus/rhesus | 8 ++-- 4 files changed, 140 insertions(+), 53 deletions(-) diff --git a/doc/MonkeySpec b/doc/MonkeySpec index 45d6cf6..3b565db 100644 --- a/doc/MonkeySpec +++ b/doc/MonkeySpec @@ -38,7 +38,10 @@ common components server-side components ---------------------- -* "howler": service gpg key generator/publisher +* "howler": server gpg maintainer + - generates gpg keys for the server + - publishes server gpg keys + - used to specify userids to trust for user authentication * "tamarin": script to trigger rhesus during attempt to initiate connection from client diff --git a/doc/README b/doc/README index 9dc8753..9034519 100644 --- a/doc/README +++ b/doc/README @@ -1,19 +1,22 @@ Monkeysphere README -------------------- +=================== -Default file locations: +Default files locations (by variable): MS_HOME=~/.config/monkeysphere -STAGING_AREA=$MS_HOME +MS_CONF=$MS_HOME/monkeysphere.conf +AUTH_HOST_FILE=$MS_HOME/auth_host_ids +AUTH_USER_FILE=$MS_HOME/auth_user_ids GNUPGHOME=~/.gnupg -$MS_HOME/monkeysphere.conf -$MS_HOME/auth_host_ids -$MS_HOME/auth_user_ids +STAGING_AREA=$MS_HOME + $STAGING_AREA/host_keys/KEYHASH $STAGING_AREA/known_hosts $STAGING_AREA/user_keys/KEYHASH $STAGING_AREA/authorized_keys +user usage +---------- For a user to update their ms known_hosts file: $ rhesus --known_hosts @@ -22,6 +25,23 @@ For a user to update their ms authorized_keys file: $ rhesus --authorized_keys +server service publication +-------------------------- +To publish a server host key use the "howler" component: + +# howler gen-key +# howler publish-key + +This will generate the key for server with the service URI +(ssh://server.hostname). The server admin should now sign the server +key so that people in the admin's web of trust can authenticate the +server without manual host key checking: + +$ gpg --search ='ssh://server.hostname' +$ gpg --sign-key 'ssh://server.hostname' + +server authorized_keys maintenance +---------------------------------- A system can maintain ms authorized_keys files for it's users. Some different variables need to be defined to help manage this. The way this is done is by first defining a new MS_HOME: @@ -35,10 +55,24 @@ AUTH_USER_FILE="$MS_HOME"/auth_user_ids/"$USER" STAGING_AREA=/var/lib/monkeysphere/stage/$USER GNUPGHOME=$MS_HOME/gnupg -To update the ms authorized_keys file for user "foo", the system would +For each user account on the server, the userids of people authorized +to log into that account would be placed in the AUTH_USER_FILE for +that user. However, in order for users to become authenticated, the +server must determine that the user keys have "full" validity. This +means that the server must fully trust at least one person whose +signature on the connecting users key would validate the user. This +would generally be the server admin. If the server admin's userid is + +"Alice " + +then the server would run: + +# howler trust-uids "Alice " + +To update the ms authorized_keys file for user "bob", the system would then run the following: -# USER=foo MS_HOME=/etc/monkeysphere rhesus --authorized_keys +# USER=bob MS_HOME=/etc/monkeysphere rhesus --authorized_keys To update the ms authorized_keys file for all users on the the system: diff --git a/howler/howler b/howler/howler index 7e33471..d0bb13d 100755 --- a/howler/howler +++ b/howler/howler @@ -1,78 +1,128 @@ #!/bin/sh -# howler: server gpg key generator/publisher +# howler: monkeysphere server gpg generator/publisher/maintainer # # Written by # Jameson Rollins # # Copyright 2008, released under the GPL, version 3 or later -CMD=$(basename $0) +PGRM=$(basename $0) ######################################################################## # FUNCTIONS ######################################################################## +usage() { +cat <&2 exit ${2:-'1'} } -######################################################################## -# MAIN -######################################################################## - -MS_HOME=${MS_HOME:-/etc/monkeysphere} - -. "$MS_HOME"/monkeysphere.conf - -export GNUPGHOME - -KEY_TYPE=${KEY_TYPE:-RSA} -KEY_LENGTH=${KEY_LENGTH:-2048} -KEY_USAGE=${KEY_USAGE:-encrypt,auth} -SERVICE=${SERVICE:-ssh} -HOSTNAME=${HOSTNAME:-$(hostname -f)} +# generate server gpg key +gen_key() { + KEY_TYPE=${KEY_TYPE:-RSA} + KEY_LENGTH=${KEY_LENGTH:-2048} + KEY_USAGE=${KEY_USAGE:-encrypt,auth} + SERVICE=${SERVICE:-ssh} + HOSTNAME_FQDN=${HOSTNAME_FQDN:-$(hostname -f)} -USERID=${USERID:-"$SERVICE"://"$HOSTNAME"} + USERID=${USERID:-"$SERVICE"://"$HOSTNAME_FQDN"} -echo "key parameters:" -cat < /dev/null 2>&1 ; then - failure "key for '$USERID' already exists" -fi + if gpg --list-key ="$USERID" > /dev/null 2>&1 ; then + failure "key for '$USERID' already exists" + fi -echo "generating server key..." -gpg --batch --gen-key < /dev/null | grep '^pub:' | cut -d: -f5) + keyID=$(gpg --list-key --with-colons ="$USERID" 2> /dev/null | grep '^pub:' | cut -d: -f5) -# dummy command so as not to publish fakes keys during testing -# eventually: -#gpg --send-keys --keyserver "$KEYSERVER" "$keyID" -echo "gpg --send-keys --keyserver $KEYSERVER $keyID" + # dummy command so as not to publish fakes keys during testing + # eventually: + #gpg --send-keys --keyserver "$KEYSERVER" "$keyID" + echo "gpg --send-keys --keyserver $KEYSERVER $keyID" +} -echo "done." +# FIXME: need to figure out how to automate this, in a batch mode +# or something. +trust_uids() { + for userID ; do + gpg --keyserver "$KEYSERVER" --search ="$userID" + gpg --edit-key "$userID" + done +} + +######################################################################## +# MAIN +######################################################################## + +# set ms home directory +MS_HOME=${MS_HOME:-/etc/monkeysphere} + +# load configuration file +MS_CONF=${MS_CONF:-"$MS_HOME"/monkeysphere.conf} +[ -e "$MS_CONF" ] && . "$MS_CONF" + +GNUPGHOME=${GNUPGHOME:-"$MS_HOME"/gnupg} +export GNUPGHOME +KEYSERVER=${KEYSERVER:-subkeys.pgp.net} +export KEYSERVER + +COMMAND="$1" +[ "$COMMAND" ] || failure "Type '$PGRM help' for usage." +shift 1 + +case $COMMAND in + 'gen-key') + gen_key + ;; + 'publish-key') + publish_key + ;; + 'trust-uids') + trust_uids "$@" + ;; + 'help') + usage + exit + ;; + *) + failure "Unknown command: '$COMMAND' +Type '$PGRM help' for usage." + ;; +esac diff --git a/rhesus/rhesus b/rhesus/rhesus index dec24a2..7a43fca 100755 --- a/rhesus/rhesus +++ b/rhesus/rhesus @@ -7,7 +7,7 @@ # # Copyright 2008, released under the GPL, version 3 or later -CMD=$(basename $0) +PGRM=$(basename $0) ######################################################################## # FUNCTIONS @@ -15,8 +15,8 @@ CMD=$(basename $0) usage() { cat <