made monkeysphere subkey-to-ssh-agent more user-friendly.
[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     if [ -z "$keyExpire" ] ; then
132         cat <<EOF
133 Please specify how long the key should be valid.
134          0 = key does not expire
135       <n>  = key expires in n days
136       <n>w = key expires in n weeks
137       <n>m = key expires in n months
138       <n>y = key expires in n years
139 EOF
140         while [ -z "$keyExpire" ] ; do
141             read -p "Key is valid for? (0) " keyExpire
142             if ! test_gpg_expire ${keyExpire:=0} ; then
143                 echo "invalid value"
144                 unset keyExpire
145             fi
146         done
147     elif ! test_gpg_expire "$keyExpire" ; then
148         failure "invalid key expiration value '$keyExpire'."
149     fi
150
151     # generate the list of commands that will be passed to edit-key
152     editCommands=$(cat <<EOF
153 addkey
154 7
155 S
156 E
157 A
158 Q
159 $keyLength
160 $keyExpire
161 save
162 EOF
163 )
164
165     log "generating subkey..."
166     fifoDir=$(mktemp -d)
167     (umask 077 && mkfifo "$fifoDir/pass")
168     echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
169
170     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
171
172     rm -rf "$fifoDir"
173     wait
174     log "done."
175 }
176
177 function subkey_to_ssh_agent() {
178     # try to add all authentication subkeys to the agent:
179
180     local authsubkeys
181     local secretkeys
182     local subkey
183     local workingdir
184     local kname
185     local sshaddresponse
186     local keysuccess
187
188     # if there's no agent running, don't bother:
189     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
190         failure "No ssh-agent available."
191     fi
192
193     # and if it looks like it's running, but we can't actually talk to
194     # it, bail out:
195     ssh-add -l >/dev/null
196     sshaddresponse="$?"
197     if [ "$sshaddresponse" = "2" ]; then
198         failure "Could not connect to ssh-agent"
199     fi
200     
201     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
202     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
203
204     if [ -z "$secretkeys" ]; then
205         failure "You have no secret keys in your keyring!
206 You might want to 'gpg --gen-key'"
207     fi
208     
209     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)
210
211     if [ -z "$authsubkeys" ]; then
212         failure "no authentication-capable subkeys available.
213 You might want to 'monkeysphere gen-subkey'"
214     fi
215
216     if ! test_gnu_dummy_s2k_extension ; then
217         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
218 You may want to consider patching or upgrading.
219
220 For more details, see:
221  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
222     fi
223
224     workingdir=$(mktemp -d)
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
243         if [ "$1" = '-d' ]; then
244             # we're removing the subkey:
245             gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
246             (cd "$workingdir" && ssh-add -d "$kname")
247         else
248             # we're adding the subkey:
249             mkfifo "$workingdir/$kname"
250             gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
251                 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
252                 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
253             (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
254
255             passphrase_prompt "Enter passphrase for key for $primaryuid: " "$workingdir/passphrase"
256             wait %2
257         fi
258         keysuccess="$?"
259
260         rm -f "$workingdir/$kname"
261     done
262
263     rm -rf "$workingdir"
264
265     # FIXME: sort out the return values: we're just returning the
266     # success or failure of the final authentication subkey in this
267     # case.  What if earlier ones failed?
268     exit "$keysuccess"
269 }
270
271 ########################################################################
272 # MAIN
273 ########################################################################
274
275 # unset variables that should be defined only in config file
276 unset KEYSERVER
277 unset CHECK_KEYSERVER
278 unset KNOWN_HOSTS
279 unset HASH_KNOWN_HOSTS
280 unset AUTHORIZED_KEYS
281
282 # load global config
283 [ -r "${ETC}/monkeysphere.conf" ] && . "${ETC}/monkeysphere.conf"
284
285 # set monkeysphere home directory
286 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.config/monkeysphere"}
287 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
288
289 # load local config
290 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
291
292 # set empty config variables with ones from the environment, or from
293 # config file, or with defaults
294 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
295 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
296 # if keyserver not specified in env or monkeysphere.conf,
297 # look in gpg.conf
298 if [ -z "$KEYSERVER" ] ; then
299     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
300         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
301     fi
302 fi
303 # if it's still not specified, use the default
304 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
305 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
306 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
307 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
308 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
309
310 # other variables not in config file
311 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
312 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
313 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
314
315 # export GNUPGHOME and make sure gpg home exists with proper
316 # permissions
317 export GNUPGHOME
318 mkdir -p -m 0700 "$GNUPGHOME"
319
320 # get subcommand
321 COMMAND="$1"
322 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
323 shift
324
325 case $COMMAND in
326     'update-known_hosts'|'update-known-hosts'|'k')
327         MODE='known_hosts'
328
329         # check permissions on the known_hosts file path
330         if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
331             failure "Improper permissions on known_hosts file path."
332         fi
333
334         # if hosts are specified on the command line, process just
335         # those hosts
336         if [ "$1" ] ; then
337             update_known_hosts "$@"
338             RETURN="$?"
339
340         # otherwise, if no hosts are specified, process every host
341         # in the user's known_hosts file
342         else
343             # exit if the known_hosts file does not exist
344             if [ ! -e "$KNOWN_HOSTS" ] ; then
345                 log "known_hosts file '$KNOWN_HOSTS' does not exist."
346                 exit
347             fi
348
349             process_known_hosts
350             RETURN="$?"
351         fi
352         ;;
353
354     'update-authorized_keys'|'update-authorized-keys'|'a')
355         MODE='authorized_keys'
356
357         # check permissions on the authorized_user_ids file path
358         if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
359             failure "Improper permissions on authorized_user_ids file path."
360         fi
361
362         # check permissions on the authorized_keys file path
363         if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
364             failure "Improper permissions on authorized_keys file path."
365         fi
366
367         # exit if the authorized_user_ids file is empty
368         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
369             log "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
370             exit
371         fi
372
373         # process authorized_user_ids file
374         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
375         RETURN="$?"
376         ;;
377
378     'gen-subkey'|'g')
379         gen_subkey "$@"
380         ;;
381
382     'subkey-to-ssh-agent'|'s')
383         subkey_to_ssh_agent "$@"
384         ;;
385
386     '--help'|'help'|'-h'|'h'|'?')
387         usage
388         ;;
389
390     *)
391         failure "Unknown command: '$COMMAND'
392 Type '$PGRM help' for usage."
393         ;;
394 esac
395
396 exit "$RETURN"