Merge commit 'dkg/master'
[monkeysphere.git] / src / monkeysphere
1 #!/usr/bin/env bash
2
3 # monkeysphere: MonkeySphere client tool
4 #
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>
9 # Micah Anderson <micah@riseup.net>
10 #
11 # They are Copyright 2008-2009, and are all released under the GPL, version 3
12 # or later.
13
14 ########################################################################
15 PGRM=$(basename $0)
16
17 SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
18 export SYSSHAREDIR
19 . "${SYSSHAREDIR}/common" || exit 1
20
21 # UTC date in ISO 8601 format if needed
22 DATE=$(date -u '+%FT%T')
23
24 # unset some environment variables that could screw things up
25 unset GREP_OPTIONS
26
27 # default return code
28 RETURN=0
29
30 # set the file creation mask to be only owner rw
31 umask 077
32
33 ########################################################################
34 # FUNCTIONS
35 ########################################################################
36
37 usage() {
38     cat <<EOF >&2
39 usage: $PGRM <subcommand> [options] [args]
40 Monkeysphere client tool.
41
42 subcommands:
43  update-known_hosts (k) [HOST]...    update known_hosts file
44  update-authorized_keys (a)          update authorized_keys file
45  import-subkey (i)                   import existing ssh key as gpg subkey
46    --keyfile (-f) FILE                 key file to import
47    --expire (-e) EXPIRE                date to expire
48  gen-subkey (g) [KEYID]              generate an authentication subkey
49    --length (-l) BITS                  key length in bits (2048)
50    --expire (-e) EXPIRE                date to expire
51  ssh-proxycommand                    ssh proxycommand
52  subkey-to-ssh-agent (s)             store authentication subkey in ssh-agent
53  version (v)                         show version number
54  help (h,?)                          this help
55
56 EOF
57 }
58
59 # import an existing ssh key as a gpg subkey
60 import_subkey() {
61     local keyFile="~/.ssh/id_rsa"
62     local keyExpire
63     local keyID
64     local gpgOut
65     local userID
66
67     # get options
68     while true ; do
69         case "$1" in
70             -f|--keyfile)
71                 keyFile="$2"
72                 shift 2
73                 ;;
74             -e|--expire)
75                 keyExpire="$2"
76                 shift 2
77                 ;;
78             *)
79                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
80                     failure "Unknown option '$1'.
81 Type '$PGRM help' for usage."
82                 fi
83                 break
84                 ;;
85         esac
86     done
87
88     log verbose "importing ssh key..."
89     fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
90     (umask 077 && mkfifo "$fifoDir/pass")
91     ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import &
92
93     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
94
95     rm -rf "$fifoDir"
96     wait
97     log verbose "done."
98 }
99
100 # generate a subkey with the 'a' usage flags set
101 gen_subkey(){
102     local keyLength
103     local keyExpire
104     local keyID
105     local gpgOut
106     local userID
107
108     # get options
109     while true ; do
110         case "$1" in
111             -l|--length)
112                 keyLength="$2"
113                 shift 2
114                 ;;
115             -e|--expire)
116                 keyExpire="$2"
117                 shift 2
118                 ;;
119             *)
120                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
121                     failure "Unknown option '$1'.
122 Type '$PGRM help' for usage."
123                 fi
124                 break
125                 ;;
126         esac
127     done
128
129     case "$#" in
130         0)
131             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
132             ;;
133         1)
134             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
135             ;;
136         *)
137             failure "You must specify only a single primary key ID."
138             ;;
139     esac
140
141     # check that only a single secret key was found
142     case $(echo "$gpgSecOut" | grep -c '^sec:') in
143         0)
144             failure "No secret keys found.  Create an OpenPGP key with the following command:
145  gpg --gen-key"
146             ;;
147         1)
148             keyID=$(echo "$gpgSecOut" | cut -d: -f5)
149             ;;
150         *)
151             echo "Multiple primary secret keys found:"
152             echo "$gpgSecOut" | cut -d: -f5
153             failure "Please specify which primary key to use."
154             ;;
155     esac
156
157     # check that a valid authentication key does not already exist
158     IFS=$'\n'
159     for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do
160         type=$(echo "$line" | cut -d: -f1)
161         validity=$(echo "$line" | cut -d: -f2)
162         usage=$(echo "$line" | cut -d: -f12)
163
164         # look at keys only
165         if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
166             continue
167         fi
168         # check for authentication capability
169         if ! check_capability "$usage" 'a' ; then
170             continue
171         fi
172         # if authentication key is valid, prompt to continue
173         if [ "$validity" = 'u' ] ; then
174             echo "A valid authentication key already exists for primary key '$keyID'."
175             read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
176             if [ "${OK/y/Y}" != 'Y' ] ; then
177                 failure "aborting."
178             fi
179             break
180         fi
181     done
182
183     # set subkey defaults
184     # prompt about key expiration if not specified
185     keyExpire=$(get_gpg_expiration "$keyExpire")
186
187     # generate the list of commands that will be passed to edit-key
188     editCommands=$(cat <<EOF
189 addkey
190 7
191 S
192 E
193 A
194 Q
195 $keyLength
196 $keyExpire
197 save
198 EOF
199 )
200
201     log verbose "generating subkey..."
202     fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
203     (umask 077 && mkfifo "$fifoDir/pass")
204     echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
205
206     # FIXME: this needs to fail more gracefully if the passphrase is incorrect
207     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
208
209     rm -rf "$fifoDir"
210     wait
211     log verbose "done."
212 }
213
214 subkey_to_ssh_agent() {
215     # try to add all authentication subkeys to the agent:
216
217     local sshaddresponse
218     local secretkeys
219     local authsubkeys
220     local workingdir
221     local keysuccess
222     local subkey
223     local publine
224     local kname
225
226     if ! test_gnu_dummy_s2k_extension ; then
227         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
228 You may want to consider patching or upgrading to GnuTLS 2.6 or later.
229
230 For more details, see:
231  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
232     fi
233
234     # if there's no agent running, don't bother:
235     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
236         failure "No ssh-agent available."
237     fi
238
239     # and if it looks like it's running, but we can't actually talk to
240     # it, bail out:
241     ssh-add -l >/dev/null
242     sshaddresponse="$?"
243     if [ "$sshaddresponse" = "2" ]; then
244         failure "Could not connect to ssh-agent"
245     fi
246     
247     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
248     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
249         grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
250
251     if [ -z "$secretkeys" ]; then
252         failure "You have no secret keys in your keyring!
253 You might want to run 'gpg --gen-key'."
254     fi
255     
256     authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
257         --fingerprint --fingerprint $secretkeys | \
258         cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
259         grep '^fpr::' | cut -f3 -d: | sort -u)
260
261     if [ -z "$authsubkeys" ]; then
262         failure "no authentication-capable subkeys available.
263 You might want to 'monkeysphere gen-subkey'"
264     fi
265
266     workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
267     umask 077
268     mkfifo "$workingdir/passphrase"
269     keysuccess=1
270
271     # FIXME: we're currently allowing any other options to get passed
272     # through to ssh-add.  should we limit it to known ones?  For
273     # example: -d or -c and/or -t <lifetime> 
274
275     for subkey in $authsubkeys; do 
276         # choose a label by which this key will be known in the agent:
277         # we are labelling the key by User ID instead of by
278         # fingerprint, but filtering out all / characters to make sure
279         # the filename is legit.
280
281         primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
282
283         #kname="[monkeysphere] $primaryuid"
284         kname="$primaryuid"
285
286         if [ "$1" = '-d' ]; then
287             # we're removing the subkey:
288             gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
289             (cd "$workingdir" && ssh-add -d "$kname")
290         else
291             # we're adding the subkey:
292             mkfifo "$workingdir/$kname"
293             gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
294                 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
295                 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
296             (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
297
298             passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
299             wait %2
300         fi
301         keysuccess="$?"
302
303         rm -f "$workingdir/$kname"
304     done
305
306     rm -rf "$workingdir"
307
308     # FIXME: sort out the return values: we're just returning the
309     # success or failure of the final authentication subkey in this
310     # case.  What if earlier ones failed?
311     exit "$keysuccess"
312 }
313
314 ########################################################################
315 # MAIN
316 ########################################################################
317
318 # unset variables that should be defined only in config file
319 unset KEYSERVER
320 unset CHECK_KEYSERVER
321 unset KNOWN_HOSTS
322 unset HASH_KNOWN_HOSTS
323 unset AUTHORIZED_KEYS
324
325 # load global config
326 [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
327
328 # set monkeysphere home directory
329 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
330 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
331
332 # load local config
333 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
334
335 # set empty config variables with ones from the environment, or from
336 # config file, or with defaults
337 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
338 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
339 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
340 # if keyserver not specified in env or monkeysphere.conf,
341 # look in gpg.conf
342 if [ -z "$KEYSERVER" ] ; then
343     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
344         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
345     fi
346 fi
347 # if it's still not specified, use the default
348 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
349 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
350 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
351 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
352 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
353
354 # other variables not in config file
355 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
356 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
357 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
358
359 # export GNUPGHOME and make sure gpg home exists with proper
360 # permissions
361 export GNUPGHOME
362 mkdir -p -m 0700 "$GNUPGHOME"
363 export LOG_LEVEL
364
365 # get subcommand
366 COMMAND="$1"
367 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
368 shift
369
370 case $COMMAND in
371     'update-known_hosts'|'update-known-hosts'|'k')
372         MODE='known_hosts'
373
374         # touch the known_hosts file so that the file permission check
375         # below won't fail upon not finding the file
376         (umask 0022 && touch "$KNOWN_HOSTS")
377
378         # check permissions on the known_hosts file path
379         check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
380
381         # if hosts are specified on the command line, process just
382         # those hosts
383         if [ "$1" ] ; then
384             update_known_hosts "$@"
385             RETURN="$?"
386
387         # otherwise, if no hosts are specified, process every host
388         # in the user's known_hosts file
389         else
390             # exit if the known_hosts file does not exist
391             if [ ! -e "$KNOWN_HOSTS" ] ; then
392                 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
393                 exit
394             fi
395
396             process_known_hosts
397             RETURN="$?"
398         fi
399         ;;
400
401     'update-authorized_keys'|'update-authorized-keys'|'a')
402         MODE='authorized_keys'
403
404         # check permissions on the authorized_user_ids file path
405         check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
406
407         # check permissions on the authorized_keys file path
408         check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
409
410         # exit if the authorized_user_ids file is empty
411         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
412             log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
413             exit
414         fi
415
416         # process authorized_user_ids file
417         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
418         RETURN="$?"
419         ;;
420
421     'import-subkey'|'i')
422         import_key "$@"
423         ;;
424
425     'gen-subkey'|'g')
426         gen_subkey "$@"
427         ;;
428
429     'ssh-proxycommand'|'p')
430         ssh-proxycommand "$@"
431         ;;
432
433     'subkey-to-ssh-agent'|'s')
434         subkey_to_ssh_agent "$@"
435         ;;
436
437     'version'|'v')
438         echo "$VERSION"
439         ;;
440
441     '--help'|'help'|'-h'|'h'|'?')
442         usage
443         ;;
444
445     *)
446         failure "Unknown command: '$COMMAND'
447 Type '$PGRM help' for usage."
448         ;;
449 esac
450
451 exit "$RETURN"