tweaked the key expiration checking function, and replied to SJJ's bug
[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 authsubkeys
163     local secretkeys
164     local subkey
165     local workingdir
166     local kname
167     local sshaddresponse
168     local keysuccess
169
170     if ! test_gnu_dummy_s2k_extension ; then
171         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
172 You may want to consider patching or upgrading.
173
174 For more details, see:
175  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
176     fi
177
178     # if there's no agent running, don't bother:
179     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
180         failure "No ssh-agent available."
181     fi
182
183     # and if it looks like it's running, but we can't actually talk to
184     # it, bail out:
185     ssh-add -l >/dev/null
186     sshaddresponse="$?"
187     if [ "$sshaddresponse" = "2" ]; then
188         failure "Could not connect to ssh-agent"
189     fi
190     
191     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
192     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
193
194     if [ -z "$secretkeys" ]; then
195         failure "You have no secret keys in your keyring!
196 You might want to run 'gpg --gen-key'."
197     fi
198     
199     authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint --fingerprint $secretkeys | cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | grep '^fpr::' | cut -f3 -d: | sort -u)
200
201     if [ -z "$authsubkeys" ]; then
202         failure "no authentication-capable subkeys available.
203 You might want to 'monkeysphere gen-subkey'"
204     fi
205
206     workingdir=$(mktemp -d)
207     umask 077
208     mkfifo "$workingdir/passphrase"
209     keysuccess=1
210
211     # FIXME: we're currently allowing any other options to get passed
212     # through to ssh-add.  should we limit it to known ones?  For
213     # example: -d or -c and/or -t <lifetime> 
214
215     for subkey in $authsubkeys; do 
216         # choose a label by which this key will be known in the agent:
217         # we are labelling the key by User ID instead of by
218         # fingerprint, but filtering out all / characters to make sure
219         # the filename is legit.
220
221         primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
222
223         #kname="[monkeysphere] $primaryuid"
224         kname="'$primaryuid'"
225
226         if [ "$1" = '-d' ]; then
227             # we're removing the subkey:
228             gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
229             (cd "$workingdir" && ssh-add -d "$kname")
230         else
231             # we're adding the subkey:
232             mkfifo "$workingdir/$kname"
233             gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
234                 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
235                 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
236             (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
237
238             passphrase_prompt "Enter passphrase for key for $primaryuid: " "$workingdir/passphrase"
239             wait %2
240         fi
241         keysuccess="$?"
242
243         rm -f "$workingdir/$kname"
244     done
245
246     rm -rf "$workingdir"
247
248     # FIXME: sort out the return values: we're just returning the
249     # success or failure of the final authentication subkey in this
250     # case.  What if earlier ones failed?
251     exit "$keysuccess"
252 }
253
254 ########################################################################
255 # MAIN
256 ########################################################################
257
258 # unset variables that should be defined only in config file
259 unset KEYSERVER
260 unset CHECK_KEYSERVER
261 unset KNOWN_HOSTS
262 unset HASH_KNOWN_HOSTS
263 unset AUTHORIZED_KEYS
264
265 # load global config
266 [ -r "${ETC}/monkeysphere.conf" ] && . "${ETC}/monkeysphere.conf"
267
268 # set monkeysphere home directory
269 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.config/monkeysphere"}
270 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
271
272 # load local config
273 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
274
275 # set empty config variables with ones from the environment, or from
276 # config file, or with defaults
277 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
278 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
279 # if keyserver not specified in env or monkeysphere.conf,
280 # look in gpg.conf
281 if [ -z "$KEYSERVER" ] ; then
282     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
283         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
284     fi
285 fi
286 # if it's still not specified, use the default
287 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
288 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
289 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
290 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
291 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
292
293 # other variables not in config file
294 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
295 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
296 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
297
298 # export GNUPGHOME and make sure gpg home exists with proper
299 # permissions
300 export GNUPGHOME
301 mkdir -p -m 0700 "$GNUPGHOME"
302
303 # get subcommand
304 COMMAND="$1"
305 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
306 shift
307
308 case $COMMAND in
309     'update-known_hosts'|'update-known-hosts'|'k')
310         MODE='known_hosts'
311
312         # check permissions on the known_hosts file path
313         if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
314             failure "Improper permissions on known_hosts file path."
315         fi
316
317         # if hosts are specified on the command line, process just
318         # those hosts
319         if [ "$1" ] ; then
320             update_known_hosts "$@"
321             RETURN="$?"
322
323         # otherwise, if no hosts are specified, process every host
324         # in the user's known_hosts file
325         else
326             # exit if the known_hosts file does not exist
327             if [ ! -e "$KNOWN_HOSTS" ] ; then
328                 log "known_hosts file '$KNOWN_HOSTS' does not exist."
329                 exit
330             fi
331
332             process_known_hosts
333             RETURN="$?"
334         fi
335         ;;
336
337     'update-authorized_keys'|'update-authorized-keys'|'a')
338         MODE='authorized_keys'
339
340         # check permissions on the authorized_user_ids file path
341         if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
342             failure "Improper permissions on authorized_user_ids file path."
343         fi
344
345         # check permissions on the authorized_keys file path
346         if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
347             failure "Improper permissions on authorized_keys file path."
348         fi
349
350         # exit if the authorized_user_ids file is empty
351         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
352             log "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
353             exit
354         fi
355
356         # process authorized_user_ids file
357         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
358         RETURN="$?"
359         ;;
360
361     'gen-subkey'|'g')
362         gen_subkey "$@"
363         ;;
364
365     'subkey-to-ssh-agent'|'s')
366         subkey_to_ssh_agent "$@"
367         ;;
368
369     '--help'|'help'|'-h'|'h'|'?')
370         usage
371         ;;
372
373     *)
374         failure "Unknown command: '$COMMAND'
375 Type '$PGRM help' for usage."
376         ;;
377 esac
378
379 exit "$RETURN"