3 # monkeysphere: MonkeySphere client tool
5 # The monkeysphere scripts are written by:
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 # Jamie McClelland <jm@mayfirst.org>
8 # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
10 # They are Copyright 2008, and are all released under the GPL, version 3
13 ########################################################################
16 SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
18 . "${SYSSHAREDIR}/common" || exit 1
20 # UTC date in ISO 8601 format if needed
21 DATE=$(date -u '+%FT%T')
23 # unset some environment variables that could screw things up
29 # set the file creation mask to be only owner rw
32 ########################################################################
34 ########################################################################
38 usage: $PGRM <subcommand> [options] [args]
39 Monkeysphere client tool.
42 update-known_hosts (k) [HOST]... update known_hosts file
43 update-authorized_keys (a) update authorized_keys file
44 import-subkey (i) import existing ssh key as gpg subkey
45 --keyfile (-f) FILE key file to import
46 --expire (-e) EXPIRE date to expire
47 gen-subkey (g) [KEYID] generate an authentication subkey
48 --length (-l) BITS key length in bits (2048)
49 --expire (-e) EXPIRE date to expire
50 ssh-proxycommand ssh proxycommand
51 subkey-to-ssh-agent (s) store authentication subkey in ssh-agent
52 version (v) show version number
58 # import an existing ssh key as a gpg subkey
60 local keyFile="~/.ssh/id_rsa"
78 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
79 failure "Unknown option '$1'.
80 Type '$PGRM help' for usage."
87 log verbose "importing ssh key..."
88 fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
89 (umask 077 && mkfifo "$fifoDir/pass")
90 ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import &
92 passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
99 # generate a subkey with the 'a' usage flags set
119 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
120 failure "Unknown option '$1'.
121 Type '$PGRM help' for usage."
130 gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
133 gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
136 failure "You must specify only a single primary key ID."
140 # check that only a single secret key was found
141 case $(echo "$gpgSecOut" | grep -c '^sec:') in
143 failure "No secret keys found. Create an OpenPGP key with the following command:
147 keyID=$(echo "$gpgSecOut" | cut -d: -f5)
150 echo "Multiple primary secret keys found:"
151 echo "$gpgSecOut" | cut -d: -f5
152 failure "Please specify which primary key to use."
156 # check that a valid authentication key does not already exist
158 for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do
159 type=$(echo "$line" | cut -d: -f1)
160 validity=$(echo "$line" | cut -d: -f2)
161 usage=$(echo "$line" | cut -d: -f12)
164 if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
167 # check for authentication capability
168 if ! check_capability "$usage" 'a' ; then
171 # if authentication key is valid, prompt to continue
172 if [ "$validity" = 'u' ] ; then
173 echo "A valid authentication key already exists for primary key '$keyID'."
174 read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
175 if [ "${OK/y/Y}" != 'Y' ] ; then
182 # set subkey defaults
183 # prompt about key expiration if not specified
184 keyExpire=$(get_gpg_expiration "$keyExpire")
186 # generate the list of commands that will be passed to edit-key
187 editCommands=$(cat <<EOF
200 log verbose "generating subkey..."
201 fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
202 (umask 077 && mkfifo "$fifoDir/pass")
203 echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
205 # FIXME: this needs to fail more gracefully if the passphrase is incorrect
206 passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
213 subkey_to_ssh_agent() {
214 # try to add all authentication subkeys to the agent:
225 if ! test_gnu_dummy_s2k_extension ; then
226 failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
227 You may want to consider patching or upgrading to GnuTLS 2.6 or later.
229 For more details, see:
230 http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
233 # if there's no agent running, don't bother:
234 if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
235 failure "No ssh-agent available."
238 # and if it looks like it's running, but we can't actually talk to
240 ssh-add -l >/dev/null
242 if [ "$sshaddresponse" = "2" ]; then
243 failure "Could not connect to ssh-agent"
246 # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
247 secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
248 grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
250 if [ -z "$secretkeys" ]; then
251 failure "You have no secret keys in your keyring!
252 You might want to run 'gpg --gen-key'."
255 authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
256 --fingerprint --fingerprint $secretkeys | \
257 cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
258 grep '^fpr::' | cut -f3 -d: | sort -u)
260 if [ -z "$authsubkeys" ]; then
261 failure "no authentication-capable subkeys available.
262 You might want to 'monkeysphere gen-subkey'"
265 workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
267 mkfifo "$workingdir/passphrase"
270 # FIXME: we're currently allowing any other options to get passed
271 # through to ssh-add. should we limit it to known ones? For
272 # example: -d or -c and/or -t <lifetime>
274 for subkey in $authsubkeys; do
275 # choose a label by which this key will be known in the agent:
276 # we are labelling the key by User ID instead of by
277 # fingerprint, but filtering out all / characters to make sure
278 # the filename is legit.
280 primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
282 #kname="[monkeysphere] $primaryuid"
285 if [ "$1" = '-d' ]; then
286 # we're removing the subkey:
287 gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
288 (cd "$workingdir" && ssh-add -d "$kname")
290 # we're adding the subkey:
291 mkfifo "$workingdir/$kname"
292 gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
293 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
294 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
295 (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
297 passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
302 rm -f "$workingdir/$kname"
307 # FIXME: sort out the return values: we're just returning the
308 # success or failure of the final authentication subkey in this
309 # case. What if earlier ones failed?
313 ########################################################################
315 ########################################################################
317 # unset variables that should be defined only in config file
319 unset CHECK_KEYSERVER
321 unset HASH_KNOWN_HOSTS
322 unset AUTHORIZED_KEYS
325 [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
327 # set monkeysphere home directory
328 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
329 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
332 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
334 # set empty config variables with ones from the environment, or from
335 # config file, or with defaults
336 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
337 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
338 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
339 # if keyserver not specified in env or monkeysphere.conf,
341 if [ -z "$KEYSERVER" ] ; then
342 if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
343 KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
346 # if it's still not specified, use the default
347 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
348 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
349 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
350 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
351 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
353 # other variables not in config file
354 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
355 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
356 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
358 # export GNUPGHOME and make sure gpg home exists with proper
361 mkdir -p -m 0700 "$GNUPGHOME"
366 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
370 'update-known_hosts'|'update-known-hosts'|'k')
373 # touch the known_hosts file so that the file permission check
374 # below won't fail upon not finding the file
375 (umask 0022 && touch "$KNOWN_HOSTS")
377 # check permissions on the known_hosts file path
378 check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
380 # if hosts are specified on the command line, process just
383 update_known_hosts "$@"
386 # otherwise, if no hosts are specified, process every host
387 # in the user's known_hosts file
389 # exit if the known_hosts file does not exist
390 if [ ! -e "$KNOWN_HOSTS" ] ; then
391 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
400 'update-authorized_keys'|'update-authorized-keys'|'a')
401 MODE='authorized_keys'
403 # check permissions on the authorized_user_ids file path
404 check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
406 # check permissions on the authorized_keys file path
407 check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
409 # exit if the authorized_user_ids file is empty
410 if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
411 log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
415 # process authorized_user_ids file
416 process_authorized_user_ids "$AUTHORIZED_USER_IDS"
428 'ssh-proxycommand'|'p')
429 ssh-proxycommand "$@"
432 'subkey-to-ssh-agent'|'s')
433 subkey_to_ssh_agent "$@"
440 '--help'|'help'|'-h'|'h'|'?')
445 failure "Unknown command: '$COMMAND'
446 Type '$PGRM help' for usage."