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