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