Merge commit 'jrollins/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 #
10 # They are Copyright 2008, and are all released under the GPL, version 3
11 # or later.
12
13 ########################################################################
14 PGRM=$(basename $0)
15
16 SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
17 export SYSSHAREDIR
18 . "${SYSSHAREDIR}/common" || exit 1
19
20 # UTC date in ISO 8601 format if needed
21 DATE=$(date -u '+%FT%T')
22
23 # unset some environment variables that could screw things up
24 unset GREP_OPTIONS
25
26 # default return code
27 RETURN=0
28
29 # set the file creation mask to be only owner rw
30 umask 077
31
32 ########################################################################
33 # FUNCTIONS
34 ########################################################################
35
36 usage() {
37     cat <<EOF >&2
38 usage: $PGRM <subcommand> [options] [args]
39 Monkeysphere client tool.
40
41 subcommands:
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
53  help (h,?)                          this help
54
55 EOF
56 }
57
58 # import an existing ssh key as a gpg subkey
59 import_subkey() {
60     local keyFile="~/.ssh/id_rsa"
61     local keyExpire
62     local keyID
63     local gpgOut
64     local userID
65
66     # get options
67     while true ; do
68         case "$1" in
69             -f|--keyfile)
70                 keyFile="$2"
71                 shift 2
72                 ;;
73             -e|--expire)
74                 keyExpire="$2"
75                 shift 2
76                 ;;
77             *)
78                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
79                     failure "Unknown option '$1'.
80 Type '$PGRM help' for usage."
81                 fi
82                 break
83                 ;;
84         esac
85     done
86
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 &
91
92     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
93
94     rm -rf "$fifoDir"
95     wait
96     log verbose "done."
97 }
98
99 # generate a subkey with the 'a' usage flags set
100 gen_subkey(){
101     local keyLength
102     local keyExpire
103     local keyID
104     local gpgOut
105     local userID
106
107     # get options
108     while true ; do
109         case "$1" in
110             -l|--length)
111                 keyLength="$2"
112                 shift 2
113                 ;;
114             -e|--expire)
115                 keyExpire="$2"
116                 shift 2
117                 ;;
118             *)
119                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
120                     failure "Unknown option '$1'.
121 Type '$PGRM help' for usage."
122                 fi
123                 break
124                 ;;
125         esac
126     done
127
128     case "$#" in
129         0)
130             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
131             ;;
132         1)
133             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
134             ;;
135         *)
136             failure "You must specify only a single primary key ID."
137             ;;
138     esac
139
140     # check that only a single secret key was found
141     case $(echo "$gpgSecOut" | grep -c '^sec:') in
142         0)
143             failure "No secret keys found.  Create an OpenPGP key with the following command:
144  gpg --gen-key"
145             ;;
146         1)
147             keyID=$(echo "$gpgSecOut" | cut -d: -f5)
148             ;;
149         *)
150             echo "Multiple primary secret keys found:"
151             echo "$gpgSecOut" | cut -d: -f5
152             failure "Please specify which primary key to use."
153             ;;
154     esac
155
156     # check that a valid authentication key does not already exist
157     IFS=$'\n'
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)
162
163         # look at keys only
164         if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
165             continue
166         fi
167         # check for authentication capability
168         if ! check_capability "$usage" 'a' ; then
169             continue
170         fi
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
176                 failure "aborting."
177             fi
178             break
179         fi
180     done
181
182     # set subkey defaults
183     # prompt about key expiration if not specified
184     keyExpire=$(get_gpg_expiration "$keyExpire")
185
186     # generate the list of commands that will be passed to edit-key
187     editCommands=$(cat <<EOF
188 addkey
189 7
190 S
191 E
192 A
193 Q
194 $keyLength
195 $keyExpire
196 save
197 EOF
198 )
199
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" &
204
205     # FIXME: this needs to fail more gracefully if the passphrase is incorrect
206     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
207
208     rm -rf "$fifoDir"
209     wait
210     log verbose "done."
211 }
212
213 subkey_to_ssh_agent() {
214     # try to add all authentication subkeys to the agent:
215
216     local sshaddresponse
217     local secretkeys
218     local authsubkeys
219     local workingdir
220     local keysuccess
221     local subkey
222     local publine
223     local kname
224
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.
228
229 For more details, see:
230  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
231     fi
232
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."
236     fi
237
238     # and if it looks like it's running, but we can't actually talk to
239     # it, bail out:
240     ssh-add -l >/dev/null
241     sshaddresponse="$?"
242     if [ "$sshaddresponse" = "2" ]; then
243         failure "Could not connect to ssh-agent"
244     fi
245     
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 "!" }')
249
250     if [ -z "$secretkeys" ]; then
251         failure "You have no secret keys in your keyring!
252 You might want to run 'gpg --gen-key'."
253     fi
254     
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)
259
260     if [ -z "$authsubkeys" ]; then
261         failure "no authentication-capable subkeys available.
262 You might want to 'monkeysphere gen-subkey'"
263     fi
264
265     workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
266     umask 077
267     mkfifo "$workingdir/passphrase"
268     keysuccess=1
269
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> 
273
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.
279
280         primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
281
282         #kname="[monkeysphere] $primaryuid"
283         kname="$primaryuid"
284
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")
289         else
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 )&
296
297             passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
298             wait %2
299         fi
300         keysuccess="$?"
301
302         rm -f "$workingdir/$kname"
303     done
304
305     rm -rf "$workingdir"
306
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?
310     exit "$keysuccess"
311 }
312
313 ########################################################################
314 # MAIN
315 ########################################################################
316
317 # unset variables that should be defined only in config file
318 unset KEYSERVER
319 unset CHECK_KEYSERVER
320 unset KNOWN_HOSTS
321 unset HASH_KNOWN_HOSTS
322 unset AUTHORIZED_KEYS
323
324 # load global config
325 [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
326
327 # set monkeysphere home directory
328 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
329 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
330
331 # load local config
332 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
333
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,
340 # look in gpg.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 }')
344     fi
345 fi
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"}}
352
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"}
357
358 # export GNUPGHOME and make sure gpg home exists with proper
359 # permissions
360 export GNUPGHOME
361 mkdir -p -m 0700 "$GNUPGHOME"
362 export LOG_LEVEL
363
364 # get subcommand
365 COMMAND="$1"
366 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
367 shift
368
369 case $COMMAND in
370     'update-known_hosts'|'update-known-hosts'|'k')
371         MODE='known_hosts'
372
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")
376
377         # check permissions on the known_hosts file path
378         check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
379
380         # if hosts are specified on the command line, process just
381         # those hosts
382         if [ "$1" ] ; then
383             update_known_hosts "$@"
384             RETURN="$?"
385
386         # otherwise, if no hosts are specified, process every host
387         # in the user's known_hosts file
388         else
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."
392                 exit
393             fi
394
395             process_known_hosts
396             RETURN="$?"
397         fi
398         ;;
399
400     'update-authorized_keys'|'update-authorized-keys'|'a')
401         MODE='authorized_keys'
402
403         # check permissions on the authorized_user_ids file path
404         check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
405
406         # check permissions on the authorized_keys file path
407         check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
408
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."
412             exit
413         fi
414
415         # process authorized_user_ids file
416         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
417         RETURN="$?"
418         ;;
419
420     'import-subkey'|'i')
421         import_key "$@"
422         ;;
423
424     'gen-subkey'|'g')
425         gen_subkey "$@"
426         ;;
427
428     'ssh-proxycommand'|'p')
429         ssh-proxycommand "$@"
430         ;;
431
432     'subkey-to-ssh-agent'|'s')
433         subkey_to_ssh_agent "$@"
434         ;;
435
436     'version'|'v')
437         echo "$VERSION"
438         ;;
439
440     '--help'|'help'|'-h'|'h'|'?')
441         usage
442         ;;
443
444     *)
445         failure "Unknown command: '$COMMAND'
446 Type '$PGRM help' for usage."
447         ;;
448 esac
449
450 exit "$RETURN"