diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/common | 22 | ||||
-rwxr-xr-x | src/keytrans/pem2openpgp | 469 | ||||
-rwxr-xr-x | src/monkeysphere | 161 | ||||
-rwxr-xr-x | src/monkeysphere-server | 322 | ||||
-rwxr-xr-x | src/monkeysphere-ssh-proxycommand | 153 |
5 files changed, 983 insertions, 144 deletions
@@ -19,6 +19,9 @@ SYSCONFIGDIR=${MONKEYSPHERE_SYSCONFIGDIR:-"/etc/monkeysphere"} export SYSCONFIGDIR +# monkeysphere version +VERSION=__VERSION__ + ######################################################################## ### UTILITY FUNCTIONS @@ -147,7 +150,7 @@ advance_date() { local shortunits # try things the GNU way first - if date -d "$number $longunits" "$format" >&/dev/null ; then + if date -d "$number $longunits" "$format" >/dev/null 2>&1; then date -d "$number $longunits" "$format" else # otherwise, convert to (a limited version of) BSD date syntax: @@ -639,7 +642,7 @@ process_user_id() { ;; 'uid') # user ids if [ "$lastKey" != pub ] ; then - log verbose " - got a user ID after a sub key?! user IDs should only follow primary keys!" + log verbose " ! got a user ID after a sub key?! user IDs should only follow primary keys!" continue fi # if an acceptable user ID was already found, skip @@ -652,6 +655,8 @@ process_user_id() { if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then # mark user ID acceptable uidOK=true + else + log debug " - unacceptable user ID validity ($validity)." fi else continue @@ -693,10 +698,12 @@ process_user_id() { # if sub key validity is not ok, skip if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then + log debug " - unacceptable sub key validity ($validity)." continue fi # if sub key capability is not ok, skip if ! check_capability "$usage" $requiredCapability ; then + log debug " - unacceptable sub key capability ($usage)." continue fi @@ -742,6 +749,7 @@ process_user_id() { process_host_known_hosts() { local host local userID + local noKey= local nKeys local nKeysOK local ok @@ -768,8 +776,9 @@ process_host_known_hosts() { continue fi - # remove the old host key line, and note if removed - remove_line "$KNOWN_HOSTS" "$sshKey" + # remove any old host key line, and note if removed nothing is + # removed + remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true # if key OK, add new host line if [ "$ok" -eq '0' ] ; then @@ -788,6 +797,11 @@ process_host_known_hosts() { else ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS" fi + + # log if this is a new key to the known_hosts file + if [ "$noKey" ] ; then + log info "* new key for $host added to known_hosts file." + fi fi done diff --git a/src/keytrans/pem2openpgp b/src/keytrans/pem2openpgp new file mode 100755 index 0000000..3d9f6f8 --- /dev/null +++ b/src/keytrans/pem2openpgp @@ -0,0 +1,469 @@ +#!/usr/bin/perl -w -T + +# pem2openpgp: take a PEM-encoded RSA private-key on standard input, a +# User ID as the first argument, and generate an OpenPGP secret key +# and certificate from it. + +# WARNING: the secret key material *will* appear on stdout (albeit in +# OpenPGP form) -- if you redirect stdout to a file, make sure the +# permissions on that file are appropriately locked down! + +# Usage: + +# pem2openpgp 'ssh://'$(hostname -f) < /etc/ssh/ssh_host_rsa_key | gpg --import + +# Authors: +# Jameson Rollins <jrollins@finestructure.net> +# Daniel Kahn Gillmor <dkg@fifthhorseman.net> + +# Started on: 2009-01-07 02:01:19-0500 + +# License: GPL v3 or later (we may need to adjust this given that this +# connects to OpenSSL via perl) + +use strict; +use warnings; +use Crypt::OpenSSL::RSA; +use Crypt::OpenSSL::Bignum; +use Crypt::OpenSSL::Bignum::CTX; +use Digest::SHA1; +use MIME::Base64; + +## make sure all length() and substr() calls use bytes only: +use bytes; + +my $uid = shift; + +# FIXME: fail if there is no given user ID; or should we default to +# hostname_long() from Sys::Hostname::Long ? + + + +# see RFC 4880 section 9.1 (ignoring deprecated algorithms for now) +my $asym_algos = { rsa => 1, + elgamal => 16, + dsa => 17, + }; + +# see RFC 4880 section 9.2 +my $ciphers = { plaintext => 0, + idea => 1, + tripledes => 2, + cast5 => 3, + blowfish => 4, + aes128 => 7, + aes192 => 8, + aes256 => 9, + twofish => 10, + }; + +# see RFC 4880 section 9.3 +my $zips = { uncompressed => 0, + zip => 1, + zlib => 2, + bzip2 => 3, + }; + +# see RFC 4880 section 9.4 +my $digests = { md5 => 1, + sha1 => 2, + ripemd160 => 3, + sha256 => 8, + sha384 => 9, + sha512 => 10, + sha224 => 11, + }; + +# see RFC 4880 section 5.2.3.21 +my $usage_flags = { certify => 0x01, + sign => 0x02, + encrypt_comms => 0x04, + encrypt_storage => 0x08, + encrypt => 0x0c, ## both comms and storage + split => 0x10, # the private key is split via secret sharing + authenticate => 0x20, + shared => 0x80, # more than one person holds the entire private key + }; + +# see RFC 4880 section 4.3 +my $packet_types = { pubkey_enc_session => 1, + sig => 2, + symkey_enc_session => 3, + onepass_sig => 4, + seckey => 5, + pubkey => 6, + sec_subkey => 7, + compressed_data => 8, + symenc_data => 9, + marker => 10, + literal => 11, + trust => 12, + uid => 13, + pub_subkey => 14, + uat => 17, + symenc_w_integrity => 18, + mdc => 19, + }; + +# see RFC 4880 section 5.2.1 +my $sig_types = { binary_doc => 0x00, + text_doc => 0x01, + standalone => 0x02, + generic_certification => 0x10, + persona_certification => 0x11, + casual_certification => 0x12, + positive_certification => 0x13, + subkey_binding => 0x18, + primary_key_binding => 0x19, + key_signature => 0x1f, + key_revocation => 0x20, + subkey_revocation => 0x28, + certification_revocation => 0x30, + timestamp => 0x40, + thirdparty => 0x50, + }; + + +# see RFC 4880 section 5.2.3.1 +my $subpacket_types = { sig_creation_time => 2, + sig_expiration_time => 3, + exportable => 4, + trust_sig => 5, + regex => 6, + revocable => 7, + key_expiration_time => 9, + preferred_cipher => 11, + revocation_key => 12, + issuer => 16, + notation => 20, + preferred_digest => 21, + preferred_compression => 22, + keyserver_prefs => 23, + preferred_keyserver => 24, + primary_uid => 25, + policy_uri => 26, + usage_flags => 27, + signers_uid => 28, + revocation_reason => 29, + features => 30, + signature_target => 31, + embedded_signature => 32, + }; + +# bitstring (see RFC 4880 section 5.2.3.24) +my $features = { mdc => 0x01 + }; + +# bitstring (see RFC 4880 5.2.3.17) +my $keyserver_prefs = { nomodify => 0x80 + }; + +###### end lookup tables ###### + +# FIXME: if we want to be able to interpret openpgp data as well as +# produce it, we need to produce key/value-swapped lookup tables as well. + + +########### Math/Utility Functions ############## + + +# see the bottom of page 43 of RFC 4880 +sub simple_checksum { + my $bytes = shift; + + return unpack("%32W*",$bytes) % 65536; +} + +# calculate the multiplicative inverse of a mod b this is euclid's +# extended algorithm. For more information see: +# http://en.wikipedia.org/wiki/Extended_Euclidean_algorithm the +# arguments here should be Crypt::OpenSSL::Bignum objects. $a should +# be the larger of the two values, and the two values should be +# coprime. + +sub modular_multi_inverse { + my $a = shift; + my $b = shift; + + my $ctx = Crypt::OpenSSL::Bignum::CTX->new(); + my $x = Crypt::OpenSSL::Bignum->zero(); + my $y = Crypt::OpenSSL::Bignum->one(); + my $lastx = Crypt::OpenSSL::Bignum->one(); + my $lasty = Crypt::OpenSSL::Bignum->zero(); + + while (! $b->is_zero()) { + my ($quotient, $remainder) = $a->div($b, $ctx); + + $a = $b; + $b = $remainder; + + my $temp = $x; + $x = $lastx->sub($quotient->mul($x, $ctx)); + $lastx = $temp; + + $temp = $y; + $y = $lasty->sub($quotient->mul($y, $ctx)); + $lasty = $temp; + } + + if (!$a->is_one()) { + die "did this math wrong.\n"; + } + + return $lastx; +} + + +############ OpenPGP formatting functions ############ + +# make an old-style packet out of the given packet type and body. +# old-style (see RFC 4880 section 4.2) +sub make_packet { + my $type = shift; + my $body = shift; + + my $len = length($body); + + my $lenbytes; + my $lencode; + + if ($len < 2**8) { + $lenbytes = 0; + $lencode = 'C'; + } elsif ($len < 2**16) { + $lenbytes = 1; + $lencode = 'n'; + } elsif ($len < 2**31) { + ## not testing against full 32 bits because i don't want to deal + ## with potential overflow. + $lenbytes = 2; + $lencode = 'N'; + } else { + ## what the hell do we do here? + $lenbytes = 3; + $lencode = ''; + } + + return pack('C'.$lencode, 0x80 + ($type * 4) + $lenbytes, $len). + $body; +} + + +# takes a Crypt::OpenSSL::Bignum, returns it formatted as OpenPGP MPI +# (RFC 4880 section 3.2) +sub mpi_pack { + my $num = shift; + + my $val = $num->to_bin(); + my $mpilen = length($val)*8; + +# this is a kludgy way to get the number of significant bits in the +# first byte: + my $bitsinfirstbyte = length(sprintf("%b", ord($val))); + + $mpilen -= (8 - $bitsinfirstbyte); + + return pack('n', $mpilen).$val; +} + +# FIXME: genericize these to accept either RSA or DSA keys: +sub make_rsa_pub_key_body { + my $key = shift; + my $timestamp = shift; + + my ($n, $e) = $key->get_key_parameters(); + + return + pack('CN', 4, $timestamp). + pack('C', $asym_algos->{rsa}). + mpi_pack($n). + mpi_pack($e); +} + +sub make_rsa_sec_key_body { + my $key = shift; + my $timestamp = shift; + + # we're not using $a and $b, but we need them to get to $c. + my ($n, $e, $d, $p, $q) = $key->get_key_parameters(); + + my $secret_material = mpi_pack($d). + mpi_pack($p). + mpi_pack($q). + mpi_pack(modular_multi_inverse($p, $q)); + + # according to Crypt::OpenSSL::RSA, the closest value we can get out + # of get_key_parameters is 1/q mod p; but according to sec 5.5.3 of + # RFC 4880, we're actually looking for u, the multiplicative inverse + # of p, mod q. This is why we're calculating the value directly + # with modular_multi_inverse. + + return + pack('CN', 4, $timestamp). + pack('C', $asym_algos->{rsa}). + mpi_pack($n). + mpi_pack($e). + pack('C', 0). # seckey material is not encrypted -- see RFC 4880 sec 5.5.3 + $secret_material. + pack('n', simple_checksum($secret_material)); +} + +# expects an RSA key (public or private) and a timestamp +sub fingerprint { + my $key = shift; + my $timestamp = shift; + + my $rsabody = make_rsa_pub_key_body($key, $timestamp); + + return Digest::SHA1::sha1(pack('Cn', 0x99, length($rsabody)).$rsabody); +} + +# we're just not dealing with newline business right now. slurp in +# the whole file. +undef $/; +my $buf = <STDIN>; + + +my $rsa = Crypt::OpenSSL::RSA->new_private_key($buf); + +$rsa->use_sha1_hash(); + +# see page 22 of RFC 4880 for why i think this is the right padding +# choice to use: +$rsa->use_pkcs1_padding(); + +if (! $rsa->check_key()) { + die "key does not check"; +} + +my $version = pack('C', 4); +# strong assertion of identity: +my $sigtype = pack('C', $sig_types->{positive_certification}); +# RSA +my $pubkey_algo = pack('C', $asym_algos->{rsa}); +# SHA1 +my $hash_algo = pack('C', $digests->{sha1}); + +# FIXME: i'm worried about generating a bazillion new OpenPGP +# certificates from the same key, which could easily happen if you run +# this script more than once against the same key (because the +# timestamps will differ). How can we prevent this? + +# could an environment variable (if set) override the current time, to +# be able to create a standard key? If we read the key from a file +# instead of stdin, should we use the creation time on the file? +my $timestamp = time(); + +my $creation_time_packet = pack('CCN', 5, $subpacket_types->{sig_creation_time}, $timestamp); + + +# FIXME: HARDCODED: what if someone wants to select a different set of +# usage flags? For now, we do only authentication because that's what +# monkeysphere needs. +my $usage_packet = pack('CCC', 2, $subpacket_types->{usage_flags}, $usage_flags->{authenticate}); + + +# FIXME: HARDCODED: how should we determine how far off to set the +# expiration date? default is to expire in 2 days, which is insanely +# short (but good for testing). The user ought to be able to decide +# this directly, rather than having to do "monkeysphere-server +# extend-key". +my $expires_in = 86400*2; +my $expiration_packet = pack('CCN', 5, $subpacket_types->{key_expiration_time}, $expires_in); + + +# prefer AES-256, AES-192, AES-128, CAST5, 3DES: +my $pref_sym_algos = pack('CCCCCCC', 6, $subpacket_types->{preferred_cipher}, + $ciphers->{aes256}, + $ciphers->{aes192}, + $ciphers->{aes128}, + $ciphers->{cast5}, + $ciphers->{tripledes} + ); + +# prefer SHA-1, SHA-256, RIPE-MD/160 +my $pref_hash_algos = pack('CCCCC', 4, $subpacket_types->{preferred_digest}, + $digests->{sha1}, + $digests->{sha256}, + $digests->{ripemd160} + ); + +# prefer ZLIB, BZip2, ZIP +my $pref_zip_algos = pack('CCCCC', 4, $subpacket_types->{preferred_compression}, + $zips->{zlib}, + $zips->{bzip2}, + $zips->{zip} + ); + +# we support the MDC feature: +my $feature_subpacket = pack('CCC', 2, $subpacket_types->{features}, + $features->{mdc}); + +# keyserver preference: only owner modify (???): +my $keyserver_pref = pack('CCC', 2, $subpacket_types->{keyserver_prefs}, + $keyserver_prefs->{nomodify}); + +my $subpackets_to_be_hashed = + $creation_time_packet. + $usage_packet. + $expiration_packet. + $pref_sym_algos. + $pref_hash_algos. + $pref_zip_algos. + $feature_subpacket. + $keyserver_pref; + +my $subpacket_octets = pack('n', length($subpackets_to_be_hashed)); + +my $sig_data_to_be_hashed = + $version. + $sigtype. + $pubkey_algo. + $hash_algo. + $subpacket_octets. + $subpackets_to_be_hashed; + +my $pubkey = make_rsa_pub_key_body($rsa, $timestamp); +my $seckey = make_rsa_sec_key_body($rsa, $timestamp); + +my $key_data = make_packet($packet_types->{pubkey}, $pubkey); + +# take the last 8 bytes of the fingerprint as the keyid: +my $keyid = substr(fingerprint($rsa, $timestamp), 20 - 8, 8); + +# the v4 signature trailer is: + +# version number, literal 0xff, and then a 4-byte count of the +# signature data itself. +my $trailer = pack('CCN', 4, 0xff, length($sig_data_to_be_hashed)); + +my $uid_data = + pack('CN', 0xb4, length($uid)). + $uid; + +my $datatosign = + $key_data. + $uid_data. + $sig_data_to_be_hashed. + $trailer; + +my $data_hash = Digest::SHA1::sha1_hex($datatosign); + + +my $issuer_packet = pack('CCa8', 9, $subpacket_types->{issuer}, $keyid); + +my $sig = Crypt::OpenSSL::Bignum->new_from_bin($rsa->sign($datatosign)); + +my $sig_body = + $sig_data_to_be_hashed. + pack('n', length($issuer_packet)). + $issuer_packet. + pack('n', hex(substr($data_hash, 0, 4))). + mpi_pack($sig); + +print + make_packet($packet_types->{seckey}, $seckey). + make_packet($packet_types->{uid}, $uid). + make_packet($packet_types->{sig}, $sig_body); + + diff --git a/src/monkeysphere b/src/monkeysphere index 7e800cc..463a1b1 100755 --- a/src/monkeysphere +++ b/src/monkeysphere @@ -41,37 +41,69 @@ Monkeysphere client tool. subcommands: update-known_hosts (k) [HOST]... update known_hosts file update-authorized_keys (a) update authorized_keys file + import-subkey (i) import existing ssh key as gpg subkey + --keyfile (-f) FILE key file to import + --expire (-e) EXPIRE date to expire gen-subkey (g) [KEYID] generate an authentication subkey --length (-l) BITS key length in bits (2048) --expire (-e) EXPIRE date to expire subkey-to-ssh-agent (s) store authentication subkey in ssh-agent + version (v) show version number help (h,?) this help EOF } -# generate a subkey with the 'a' usage flags set -gen_subkey(){ - local keyLength +# import an existing ssh key as a gpg subkey +import_subkey() { + local keyFile="~/.ssh/id_rsa" local keyExpire local keyID local gpgOut local userID - # set default key parameter values - keyLength= - keyExpire= - # get options - TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o l:e: -l length:,expire: -n "$PGRM" -- "$@") || failure "getopt failed! Does your getopt support GNU-style long options?" + while true ; do + case "$1" in + -f|--keyfile) + keyFile="$2" + shift 2 + ;; + -e|--expire) + keyExpire="$2" + shift 2 + ;; + *) + if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then + failure "Unknown option '$1'. +Type '$PGRM help' for usage." + fi + break + ;; + esac + done - if [ $? != 0 ] ; then - exit 1 - fi + log verbose "importing ssh key..." + fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX) + (umask 077 && mkfifo "$fifoDir/pass") + ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import & + + passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass" + + rm -rf "$fifoDir" + wait + log verbose "done." +} - # Note the quotes around `$TEMP': they are essential! - eval set -- "$TEMP" +# generate a subkey with the 'a' usage flags set +gen_subkey(){ + local keyLength + local keyExpire + local keyID + local gpgOut + local userID + # get options while true ; do case "$1" in -l|--length) @@ -82,51 +114,69 @@ gen_subkey(){ keyExpire="$2" shift 2 ;; - --) - shift - ;; - *) + *) + if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then + failure "Unknown option '$1'. +Type '$PGRM help' for usage." + fi break ;; esac done - if [ -z "$1" ] ; then - # find all secret keys - keyID=$(gpg --with-colons --list-secret-keys | grep ^sec | cut -f5 -d: | sort -u) - # if multiple sec keys exist, fail - if (( $(echo "$keyID" | wc -l) > 1 )) ; then - echo "Multiple secret keys found:" - echo "$keyID" + case "$#" in + 0) + gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:') + ;; + 1) + gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure + ;; + *) + failure "You must specify only a single primary key ID." + ;; + esac + + # check that only a single secret key was found + case $(echo "$gpgSecOut" | grep -c '^sec:') in + 0) + failure "No secret keys found. Create an OpenPGP key with the following command: + gpg --gen-key" + ;; + 1) + keyID=$(echo "$gpgSecOut" | cut -d: -f5) + ;; + *) + echo "Multiple primary secret keys found:" + echo "$gpgSecOut" | cut -d: -f5 failure "Please specify which primary key to use." + ;; + esac + + # check that a valid authentication key does not already exist + IFS=$'\n' + for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do + type=$(echo "$line" | cut -d: -f1) + validity=$(echo "$line" | cut -d: -f2) + usage=$(echo "$line" | cut -d: -f12) + + # look at keys only + if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then + continue fi - else - keyID="$1" - fi - if [ -z "$keyID" ] ; then - failure "You have no secret key available. You should create an OpenPGP -key before joining the monkeysphere. You can do this with: - gpg --gen-key" - fi - - # get key output, and fail if not found - gpgOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons \ - "$keyID") || failure - - # fail if multiple sec lines are returned, which means the id - # given is not unique - if [ $(echo "$gpgOut" | grep -c '^sec:') -gt '1' ] ; then - failure "Key ID '$keyID' is not unique." - fi - - # prompt if an authentication subkey already exists - if echo "$gpgOut" | egrep "^(sec|ssb):" | cut -d: -f 12 | grep -q a ; then - echo "An authentication subkey already exists for key '$keyID'." - read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N} - if [ "${OK/y/Y}" != 'Y' ] ; then - failure "aborting." + # check for authentication capability + if ! check_capability "$usage" 'a' ; then + continue fi - fi + # if authentication key is valid, prompt to continue + if [ "$validity" = 'u' ] ; then + echo "A valid authentication key already exists for primary key '$keyID'." + read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N} + if [ "${OK/y/Y}" != 'Y' ] ; then + failure "aborting." + fi + break + fi + done # set subkey defaults # prompt about key expiration if not specified @@ -151,6 +201,7 @@ EOF (umask 077 && mkfifo "$fifoDir/pass") echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" & + # FIXME: this needs to fail more gracefully if the passphrase is incorrect passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass" rm -rf "$fifoDir" @@ -158,7 +209,7 @@ EOF log verbose "done." } -function subkey_to_ssh_agent() { +subkey_to_ssh_agent() { # try to add all authentication subkeys to the agent: local sshaddresponse @@ -365,6 +416,10 @@ case $COMMAND in RETURN="$?" ;; + 'import-subkey'|'i') + import_key "$@" + ;; + 'gen-subkey'|'g') gen_subkey "$@" ;; @@ -373,6 +428,10 @@ case $COMMAND in subkey_to_ssh_agent "$@" ;; + 'version'|'v') + echo "$VERSION" + ;; + '--help'|'help'|'-h'|'h'|'?') usage ;; diff --git a/src/monkeysphere-server b/src/monkeysphere-server index e78903b..96f5b56 100755 --- a/src/monkeysphere-server +++ b/src/monkeysphere-server @@ -46,13 +46,20 @@ Monkeysphere server admin tool. subcommands: update-users (u) [USER]... update user authorized_keys files - gen-key (g) [NAME[:PORT]] generate gpg key for the server + import-key (i) import existing ssh key to gpg + --hostname (-h) NAME[:PORT] hostname for key user ID + --keyfile (-f) FILE key file to import + --expire (-e) EXPIRE date to expire + gen-key (g) generate gpg key for the host + --hostname (-h) NAME[:PORT] hostname for key user ID --length (-l) BITS key length in bits (2048) --expire (-e) EXPIRE date to expire --revoker (-r) FINGERPRINT add a revoker - extend-key (e) EXPIRE extend expiration to EXPIRE - add-hostname (n+) NAME[:PORT] add hostname user ID to server key + extend-key (e) EXPIRE extend host key expiration + add-hostname (n+) NAME[:PORT] add hostname user ID to host key revoke-hostname (n-) NAME[:PORT] revoke hostname user ID + add-revoker (o) FINGERPRINT add a revoker to the host key + revoke-key (r) revoke host key show-key (s) output all server host key information publish-key (p) publish server host key to keyserver diagnostics (d) report on server monkeysphere status @@ -64,8 +71,10 @@ subcommands: remove-id-certifier (c-) KEYID remove a certification key list-id-certifiers (c) list certification keys - gpg-authentication-cmd CMD gnupg-authentication command + gpg-authentication-cmd CMD give a gpg command to the + authentication keyring + version (v) show version number help (h,?) this help EOF @@ -117,30 +126,59 @@ gpg_authentication() { su_monkeysphere_user "gpg $@" } +# check if user is root +is_root() { + [ $(id -u 2>/dev/null) = '0' ] +} + +# check that user is root, for functions that require root access +check_user() { + is_root || failure "You must be root to run this command." +} + # output just key fingerprint fingerprint_server_key() { + # set the pipefail option so functions fails if can't read sec key + set -o pipefail + gpg_host --list-secret-keys --fingerprint \ --with-colons --fixed-list-mode 2> /dev/null | \ - grep '^fpr:' | head -1 | cut -d: -f10 + grep '^fpr:' | head -1 | cut -d: -f10 2>/dev/null +} + +# function to check for host secret key +check_host_keyring() { + fingerprint_server_key >/dev/null \ + || failure "You don't appear to have a Monkeysphere host key on this server. Please run 'monkeysphere-server gen-key' first." } # output key information show_server_key() { - local fingerprint - local tmpkey + local fingerprintPGP + local fingerprintSSH + local ret=0 + + # FIXME: you shouldn't have to be root to see the host key fingerprint + if is_root ; then + check_host_keyring + fingerprintPGP=$(fingerprint_server_key) + gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprintPGP" 2>/dev/null + echo "OpenPGP fingerprint: $fingerprintPGP" + else + log info "You must be root to see host OpenPGP fingerprint." + ret='1' + fi - fingerprint=$(fingerprint_server_key) - gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprint" - - # dumping to a file named ' ' so that the ssh-keygen output - # doesn't claim any potentially bogus hostname(s): - tmpkey=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX) || failure "Could not create temporary directory!" - gpg_authentication "--export $fingerprint" | openpgp2ssh "$fingerprint" 2>/dev/null > "$tmpkey/ " - echo -n "ssh fingerprint: " - (cd "$tmpkey" && ssh-keygen -l -f ' ' | awk '{ print $2 }') - rm -rf "$tmpkey" - echo -n "OpenPGP fingerprint: " - echo "$fingerprint" + if [ -f "${SYSDATADIR}/ssh_host_rsa_key.pub" ] ; then + fingerprintSSH=$(ssh-keygen -l -f "${SYSDATADIR}/ssh_host_rsa_key.pub" | \ + awk '{ print $1, $2, $4 }') + echo "ssh fingerprint: $fingerprintSSH" + else + log info "SSH host key not found." + ret='1' + fi + + return $ret } # update authorized_keys for users @@ -281,37 +319,107 @@ update_users() { done } +# import an existing ssh key to a gpg key +import_key() { + local hostName=$(hostname -f) + local keyFile="/etc/ssh/ssh_host_rsa_key" + local keyExpire + local userID + + # check for presense of secret key + # FIXME: is this the proper test to be doing here? + fingerprint_server_key >/dev/null \ + && failure "An OpenPGP host key already exists." + + # get options + while true ; do + case "$1" in + -h|--hostname) + hostName="$2" + shift 2 + ;; + -f|--keyfile) + keyFile="$2" + shift 2 + ;; + -e|--expire) + keyExpire="$2" + shift 2 + ;; + *) + if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then + failure "Unknown option '$1'. +Type '$PGRM help' for usage." + fi + break + ;; + esac + done + + if [ ! -f "$keyFile" ] ; then + failure "SSH secret key file '$keyFile' not found." + fi + + userID="ssh://${hostName}" + + # prompt about key expiration if not specified + keyExpire=$(get_gpg_expiration "$keyExpire") + + echo "The following key parameters will be used for the host private key:" + echo "Import: $keyFile" + echo "Name-Real: $userID" + echo "Expire-Date: $keyExpire" + + read -p "Import key? (Y/n) " OK; OK=${OK:=Y} + if [ ${OK/y/Y} != 'Y' ] ; then + failure "aborting." + fi + + log verbose "importing ssh key..." + # translate ssh key to a private key + (umask 077 && \ + pem2openpgp "$userID" "$keyExpire" < "$sshKey" | gpg_host --import) + + # find the key fingerprint of the newly converted key + fingerprint=$(fingerprint_server_key) + + # export host ownertrust to authentication keyring + log verbose "setting ultimate owner trust for host key..." + echo "${fingerprint}:6:" | gpg_host "--import-ownertrust" + echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust" + + # export public key to file + gpg_authentication "--export-options export-minimal --armor --export 0x${fingerprint}\!" > "${SYSDATADIR}/ssh_host_rsa_key.pub.gpg" + log info "SSH host public key in OpenPGP form: ${SYSDATADIR}/ssh_host_rsa_key.pub.gpg" + + # show info about new key + show_server_key +} + # generate server gpg key gen_key() { - local keyType - local keyLength - local keyUsage + local keyType="RSA" + local keyLength="2048" + local keyUsage="auth" local keyExpire local revoker - local hostName + local hostName=$(hostname -f) local userID local keyParameters local fingerprint - # set default key parameter values - keyType="RSA" - keyLength="2048" - keyUsage="auth" - keyExpire= - revoker= + # check for presense of secret key + # FIXME: is this the proper test to be doing here? + fingerprint_server_key >/dev/null \ + && failure "An OpenPGP host key already exists." # get options - TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o e:l:r -l expire:,length:,revoker: -n "$PGRM" -- "$@") || failure "getopt failed! Does your getopt support GNU-style long options?" - - if [ $? != 0 ] ; then - exit 1 - fi - - # Note the quotes around `$TEMP': they are essential! - eval set -- "$TEMP" - while true ; do case "$1" in + -h|--hostname) + hostName="$2" + shift 2 + ;; -l|--length) keyLength="$2" shift 2 @@ -324,44 +432,36 @@ gen_key() { revoker="$2" shift 2 ;; - --) - shift - ;; - *) + *) + if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then + failure "Unknown option '$1'. +Type '$PGRM help' for usage." + fi break ;; esac done - hostName=${1:-$(hostname -f)} userID="ssh://${hostName}" - # check for presense of key with user ID - if gpg_host --list-key ="$userID" > /dev/null 2>&1 ; then - failure "Key for '$userID' already exists" - fi - # prompt about key expiration if not specified keyExpire=$(get_gpg_expiration "$keyExpire") # set key parameters - keyParameters=$(cat <<EOF -Key-Type: $keyType + keyParameters=\ +"Key-Type: $keyType Key-Length: $keyLength Key-Usage: $keyUsage Name-Real: $userID -Expire-Date: $keyExpire -EOF -) +Expire-Date: $keyExpire" # add the revoker field if specified # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key. # FIXME: key is marked "sensitive"? is this appropriate? if [ "$revoker" ] ; then - keyParameters="${keyParameters}"$(cat <<EOF -Revoker: 1:$revoker sensitive -EOF -) + keyParameters=\ +"${keyParameters} +Revoker: 1:${revoker} sensitive" fi echo "The following key parameters will be used for the host private key:" @@ -373,24 +473,21 @@ EOF fi # add commit command - keyParameters="${keyParameters}"$(cat <<EOF + # must include blank line! + keyParameters=\ +"${keyParameters} %commit -%echo done -EOF -) +%echo done" - log verbose "generating server key..." + log verbose "generating host key..." echo "$keyParameters" | gpg_host --batch --gen-key - # output the server fingerprint - fingerprint_server_key "=${userID}" - # find the key fingerprint of the newly generated key fingerprint=$(fingerprint_server_key) # export host ownertrust to authentication keyring - log verbose "setting ultimate owner trust for server key..." + log verbose "setting ultimate owner trust for host key..." echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust" # translate the private key to ssh format, and export to a file @@ -399,7 +496,14 @@ EOF (umask 077 && \ gpg_host --export-secret-key "$fingerprint" | \ openpgp2ssh "$fingerprint" > "${SYSDATADIR}/ssh_host_rsa_key") - log info "private SSH host key output to file: ${SYSDATADIR}/ssh_host_rsa_key" + log info "SSH host private key output to file: ${SYSDATADIR}/ssh_host_rsa_key" + ssh-keygen -y -f "${SYSDATADIR}/ssh_host_rsa_key" > "${SYSDATADIR}/ssh_host_rsa_key.pub" + log info "SSH host public key output to file: ${SYSDATADIR}/ssh_host_rsa_key.pub" + gpg_authentication "--export-options export-minimal --armor --export 0x${fingerprint}\!" > "${SYSDATADIR}/ssh_host_rsa_key.pub.gpg" + log info "SSH host public key in OpenPGP form: ${SYSDATADIR}/ssh_host_rsa_key.pub.gpg" + + # show info about new key + show_server_key } # extend the lifetime of a host key: @@ -407,10 +511,6 @@ extend_key() { local fpr=$(fingerprint_server_key) local extendTo="$1" - if [ -z "$fpr" ] ; then - failure "You don't appear to have a MonkeySphere host key on this server. Try 'monkeysphere-server gen-key' first." - fi - # get the new expiration date extendTo=$(get_gpg_expiration "$extendTo") @@ -467,7 +567,7 @@ $userID save EOF - ) +) # execute edit-key script if echo "$adduidCommand" | \ @@ -565,6 +665,18 @@ EOF fi } +# add a revoker to the host key +add_revoker() { + # FIXME: implement! + failure "not implemented yet!" +} + +# revoke the host key +revoke_key() { + # FIXME: implement! + failure "not implemented yet!" +} + # publish server key to keyserver publish_server_key() { read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N} @@ -705,6 +817,10 @@ diagnostics() { echo " - Recommendation: remove the above HostKey lines from $sshd_config" problemsfound=$(($problemsfound+1)) fi + + # FIXME: test (with ssh-keyscan?) that the running ssh + # daemon is actually offering the monkeysphere host key. + fi fi @@ -763,15 +879,6 @@ add_certifier() { depth=1 # get options - TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o n:t:d: -l domain:,trust:,depth: -n "$PGRM" -- "$@") || failure "getopt failed! Does your getopt support GNU-style long options?" - - if [ $? != 0 ] ; then - exit 1 - fi - - # Note the quotes around `$TEMP': they are essential! - eval set -- "$TEMP" - while true ; do case "$1" in -n|--domain) @@ -786,10 +893,11 @@ add_certifier() { depth="$2" shift 2 ;; - --) - shift - ;; - *) + *) + if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then + failure "Unknown option '$1'. +Type '$PGRM help' for usage." + fi break ;; esac @@ -851,9 +959,9 @@ add_certifier() { # export the key to the host keyring gpg_authentication "--export 0x${fingerprint}!" | gpg_host --import - if [ "$trust" == marginal ]; then + if [ "$trust" = marginal ]; then trustval=1 - elif [ "$trust" == full ]; then + elif [ "$trust" = full ]; then trustval=2 else failure "Trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)." @@ -986,53 +1094,93 @@ shift case $COMMAND in 'update-users'|'update-user'|'u') + check_user + check_host_keyring update_users "$@" ;; + 'import-key'|'i') + check_user + import_key "$@" + ;; + 'gen-key'|'g') + check_user gen_key "$@" ;; 'extend-key'|'e') + check_user + check_host_keyring extend_key "$@" ;; 'add-hostname'|'add-name'|'n+') + check_user + check_host_keyring add_hostname "$@" ;; 'revoke-hostname'|'revoke-name'|'n-') + check_user + check_host_keyring revoke_hostname "$@" ;; + 'add-revoker'|'o') + check_user + check_host_keyring + add_revoker "$@" + ;; + + 'revoke-key'|'r') + check_user + check_host_keyring + revoke_key "$@" + ;; + 'show-key'|'show'|'s') show_server_key ;; 'publish-key'|'publish'|'p') + check_user + check_host_keyring publish_server_key ;; 'diagnostics'|'d') + check_user diagnostics ;; 'add-identity-certifier'|'add-id-certifier'|'add-certifier'|'c+') + check_user + check_host_keyring add_certifier "$@" ;; 'remove-identity-certifier'|'remove-id-certifier'|'remove-certifier'|'c-') + check_user + check_host_keyring remove_certifier "$@" ;; 'list-identity-certifiers'|'list-id-certifiers'|'list-certifiers'|'list-certifier'|'c') + check_user + check_host_keyring list_certifiers "$@" ;; 'gpg-authentication-cmd') + check_user gpg_authentication_cmd "$@" ;; + 'version'|'v') + echo "$VERSION" + ;; + '--help'|'help'|'-h'|'h'|'?') usage ;; diff --git a/src/monkeysphere-ssh-proxycommand b/src/monkeysphere-ssh-proxycommand index 6276092..a609199 100755 --- a/src/monkeysphere-ssh-proxycommand +++ b/src/monkeysphere-ssh-proxycommand @@ -14,13 +14,129 @@ # ProxyCommand monkeysphere-ssh-proxycommand %h %p ######################################################################## +PGRM=$(basename $0) + +SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"} +export SYSSHAREDIR +. "${SYSSHAREDIR}/common" || exit 1 + +######################################################################## +# FUNCTIONS +######################################################################## usage() { -cat <<EOF >&2 + cat <<EOF >&2 usage: ssh -o ProxyCommand="$(basename $0) %h %p" ... EOF } +log() { + echo "$@" >&2 +} + +output_no_valid_key() { + local sshKeyOffered + local userID + local type + local validity + local keyid + local uidfpr + local usage + local sshKeyGPG + local tmpkey + local sshFingerprint + local gpgSigOut + + userID="ssh://${HOSTP}" + + log "-------------------- Monkeysphere warning -------------------" + log "Monkeysphere found OpenPGP keys for this hostname, but none had full validity." + + # retrieve the actual ssh key + sshKeyOffered=$(ssh-keyscan -t rsa -p "$PORT" "$HOST" 2>/dev/null | awk '{ print $2, $3 }') + # FIXME: should we do any checks for failed keyscans, eg. host not + # found? + + # get the gpg info for userid + gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \ + --with-fingerprint --with-fingerprint \ + ="$userID" 2>/dev/null) + + # find all 'pub' and 'sub' lines in the gpg output, which each + # represent a retrieved key for the user ID + echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \ + while IFS=: read -r type validity keyid uidfpr usage ; do + case $type in + 'pub'|'sub') + # get the ssh key of the gpg key + sshKeyGPG=$(gpg2ssh "$keyid") + + # if one of keys found matches the one offered by the + # host, then output info + if [ "$sshKeyGPG" = "$sshKeyOffered" ] ; then + log "An OpenPGP key matching the ssh key offered by the host was found:" + log + + # do some crazy "Here Strings" redirection to get the key to + # ssh-keygen, since it doesn't read from stdin cleanly + sshFingerprint=$(ssh-keygen -l -f /dev/stdin \ + <<<$(echo "$sshKeyGPG") | \ + awk '{ print $2 }') + + # get the sigs for the matching key + gpgSigOut=$(gpg --check-sigs \ + --list-options show-uid-validity \ + "$keyid") + + # output the sigs, but only those on the user ID + # we are looking for + echo "$gpgSigOut" | awk ' +{ +if (match($0,"^pub")) { print; } +if (match($0,"^uid")) { ok=0; } +if (match($0,"^uid.*'$userID'$")) { ok=1; print; } +if (ok) { if (match($0,"^sig")) { print; } } +} +' >&2 + log + + # output the other user IDs for reference + if (echo "$gpgSigOut" | grep "^uid" | grep -v -q "$userID") ; then + log "Other user IDs on this key:" + echo "$gpgSigOut" | grep "^uid" | grep -v "$userID" >&2 + log + fi + + # output ssh fingerprint + log "RSA key fingerprint is ${sshFingerprint}." + + # this whole process is in a "while read" + # subshell. the only way to get information out + # of the subshell is to change the return code. + # therefore we return 1 here to indicate that a + # matching gpg key was found for the ssh key + # offered by the host + return 1 + fi + ;; + esac + done + + # if no key match was made (and the "while read" subshell returned + # 1) output how many keys were found + if (($? != 1)) ; then + log "None of the found keys matched the key offered by the host." + log "Run the following command for more info about the found keys:" + log "gpg --check-sigs --list-options show-uid-validity =${userID}" + # FIXME: should we do anything extra here if the retrieved + # host key is actually in the known_hosts file and the ssh + # connection will succeed? Should the user be warned? + # prompted? + fi + + log "-------------------- ssh continues below --------------------" +} + ######################################################################## # export the monkeysphere log level @@ -35,7 +151,7 @@ HOST="$1" PORT="$2" if [ -z "$HOST" ] ; then - echo "Host not specified." >&2 + log "Host not specified." usage exit 255 fi @@ -88,6 +204,39 @@ export MONKEYSPHERE_CHECK_KEYSERVER # update the known_hosts file for the host monkeysphere update-known_hosts "$HOSTP" +# output on depending on the return of the update-known_hosts +# subcommand, which is (ultimately) the return code of the +# update_known_hosts function in common +case $? in + 0) + # acceptable host key found so continue to ssh + true + ;; + 1) + # no hosts at all found so also continue (drop through to + # regular ssh host verification) + true + ;; + 2) + # at least one *bad* host key (and no good host keys) was + # found, so output some usefull information + output_no_valid_key + ;; + *) + # anything else drop through + true + ;; +esac + +# FIXME: what about the case where monkeysphere successfully finds a +# valid key for the host and adds it to the known_hosts file, but a +# different non-monkeysphere key for the host already exists in the +# known_hosts, and it is this non-ms key that is offered by the host? +# monkeysphere will succeed, and the ssh connection will succeed, and +# the user will be left with the impression that they are dealing with +# a OpenPGP/PKI host key when in fact they are not. should we use +# ssh-keyscan to compare the keys first? + # exec a netcat passthrough to host for the ssh connection if [ -z "$NO_CONNECT" ] ; then if (which nc 2>/dev/null >/dev/null); then |