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