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 subkey-to-ssh-agent (s) store authentication subkey in ssh-agent
51 version (v) show version number
57 # import an existing ssh key as a gpg subkey
59 local keyFile="~/.ssh/id_rsa"
77 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
78 failure "Unknown option '$1'.
79 Type '$PGRM help' for usage."
86 log verbose "importing ssh key..."
87 fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
88 (umask 077 && mkfifo "$fifoDir/pass")
89 ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import &
91 passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
98 # generate a subkey with the 'a' usage flags set
118 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
119 failure "Unknown option '$1'.
120 Type '$PGRM help' for usage."
129 gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
132 gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
135 failure "You must specify only a single primary key ID."
139 # check that only a single secret key was found
140 case $(echo "$gpgSecOut" | grep -c '^sec:') in
142 failure "No secret keys found. Create an OpenPGP key with the following command:
146 keyID=$(echo "$gpgSecOut" | cut -d: -f5)
149 echo "Multiple primary secret keys found:"
150 echo "$gpgSecOut" | cut -d: -f5
151 failure "Please specify which primary key to use."
155 # check that a valid authentication key does not already exist
157 for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do
158 type=$(echo "$line" | cut -d: -f1)
159 validity=$(echo "$line" | cut -d: -f2)
160 usage=$(echo "$line" | cut -d: -f12)
163 if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
166 # check for authentication capability
167 if ! check_capability "$usage" 'a' ; then
170 # if authentication key is valid, prompt to continue
171 if [ "$validity" = 'u' ] ; then
172 echo "A valid authentication key already exists for primary key '$keyID'."
173 read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
174 if [ "${OK/y/Y}" != 'Y' ] ; then
181 # set subkey defaults
182 # prompt about key expiration if not specified
183 keyExpire=$(get_gpg_expiration "$keyExpire")
185 # generate the list of commands that will be passed to edit-key
186 editCommands=$(cat <<EOF
199 log verbose "generating subkey..."
200 fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
201 (umask 077 && mkfifo "$fifoDir/pass")
202 echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
204 # FIXME: this needs to fail more gracefully if the passphrase is incorrect
205 passphrase_prompt "Please enter your passphrase for $keyID: " "$fifoDir/pass"
212 subkey_to_ssh_agent() {
213 # try to add all authentication subkeys to the agent:
224 if ! test_gnu_dummy_s2k_extension ; then
225 failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
226 You may want to consider patching or upgrading to GnuTLS 2.6 or later.
228 For more details, see:
229 http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
232 # if there's no agent running, don't bother:
233 if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
234 failure "No ssh-agent available."
237 # and if it looks like it's running, but we can't actually talk to
239 ssh-add -l >/dev/null
241 if [ "$sshaddresponse" = "2" ]; then
242 failure "Could not connect to ssh-agent"
245 # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
246 secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
247 grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
249 if [ -z "$secretkeys" ]; then
250 failure "You have no secret keys in your keyring!
251 You might want to run 'gpg --gen-key'."
254 authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
255 --fingerprint --fingerprint $secretkeys | \
256 cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
257 grep '^fpr::' | cut -f3 -d: | sort -u)
259 if [ -z "$authsubkeys" ]; then
260 failure "no authentication-capable subkeys available.
261 You might want to 'monkeysphere gen-subkey'"
264 workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
266 mkfifo "$workingdir/passphrase"
269 # FIXME: we're currently allowing any other options to get passed
270 # through to ssh-add. should we limit it to known ones? For
271 # example: -d or -c and/or -t <lifetime>
273 for subkey in $authsubkeys; do
274 # choose a label by which this key will be known in the agent:
275 # we are labelling the key by User ID instead of by
276 # fingerprint, but filtering out all / characters to make sure
277 # the filename is legit.
279 primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
281 #kname="[monkeysphere] $primaryuid"
284 if [ "$1" = '-d' ]; then
285 # we're removing the subkey:
286 gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
287 (cd "$workingdir" && ssh-add -d "$kname")
289 # we're adding the subkey:
290 mkfifo "$workingdir/$kname"
291 gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
292 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
293 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
294 (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
296 passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
301 rm -f "$workingdir/$kname"
306 # FIXME: sort out the return values: we're just returning the
307 # success or failure of the final authentication subkey in this
308 # case. What if earlier ones failed?
312 ########################################################################
314 ########################################################################
316 # unset variables that should be defined only in config file
318 unset CHECK_KEYSERVER
320 unset HASH_KNOWN_HOSTS
321 unset AUTHORIZED_KEYS
324 [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
326 # set monkeysphere home directory
327 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
328 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
331 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
333 # set empty config variables with ones from the environment, or from
334 # config file, or with defaults
335 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
336 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
337 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
338 # if keyserver not specified in env or monkeysphere.conf,
340 if [ -z "$KEYSERVER" ] ; then
341 if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
342 KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
345 # if it's still not specified, use the default
346 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
347 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
348 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
349 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
350 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
352 # other variables not in config file
353 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
354 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
355 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
357 # export GNUPGHOME and make sure gpg home exists with proper
360 mkdir -p -m 0700 "$GNUPGHOME"
365 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
369 'update-known_hosts'|'update-known-hosts'|'k')
372 # touch the known_hosts file so that the file permission check
373 # below won't fail upon not finding the file
374 (umask 0022 && touch "$KNOWN_HOSTS")
376 # check permissions on the known_hosts file path
377 check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
379 # if hosts are specified on the command line, process just
382 update_known_hosts "$@"
385 # otherwise, if no hosts are specified, process every host
386 # in the user's known_hosts file
388 # exit if the known_hosts file does not exist
389 if [ ! -e "$KNOWN_HOSTS" ] ; then
390 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
399 'update-authorized_keys'|'update-authorized-keys'|'a')
400 MODE='authorized_keys'
402 # check permissions on the authorized_user_ids file path
403 check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
405 # check permissions on the authorized_keys file path
406 check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
408 # exit if the authorized_user_ids file is empty
409 if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
410 log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
414 # process authorized_user_ids file
415 process_authorized_user_ids "$AUTHORIZED_USER_IDS"
427 'subkey-to-ssh-agent'|'s')
428 subkey_to_ssh_agent "$@"
435 '--help'|'help'|'-h'|'h'|'?')
440 failure "Unknown command: '$COMMAND'
441 Type '$PGRM help' for usage."