some small changes to subkey-to-ssh-agent.
[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 ! test_gnu_dummy_s2k_extension ; then
189         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
190 You may want to consider patching or upgrading.
191
192 For more details, see:
193  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
194     fi
195
196     # if there's no agent running, don't bother:
197     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
198         failure "No ssh-agent available."
199     fi
200
201     # and if it looks like it's running, but we can't actually talk to
202     # it, bail out:
203     ssh-add -l >/dev/null
204     sshaddresponse="$?"
205     if [ "$sshaddresponse" = "2" ]; then
206         failure "Could not connect to ssh-agent"
207     fi
208     
209     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
210     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
211
212     if [ -z "$secretkeys" ]; then
213         failure "You have no secret keys in your keyring!
214 You might want to run 'gpg --gen-key'."
215     fi
216     
217     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)
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)
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 for $primaryuid: " "$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 "${ETC}/monkeysphere.conf" ] && . "${ETC}/monkeysphere.conf"
285
286 # set monkeysphere home directory
287 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.config/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 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
296 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
297 # if keyserver not specified in env or monkeysphere.conf,
298 # look in gpg.conf
299 if [ -z "$KEYSERVER" ] ; then
300     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
301         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
302     fi
303 fi
304 # if it's still not specified, use the default
305 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
306 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
307 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
308 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
309 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
310
311 # other variables not in config file
312 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
313 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
314 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
315
316 # export GNUPGHOME and make sure gpg home exists with proper
317 # permissions
318 export GNUPGHOME
319 mkdir -p -m 0700 "$GNUPGHOME"
320
321 # get subcommand
322 COMMAND="$1"
323 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
324 shift
325
326 case $COMMAND in
327     'update-known_hosts'|'update-known-hosts'|'k')
328         MODE='known_hosts'
329
330         # check permissions on the known_hosts file path
331         if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
332             failure "Improper permissions on known_hosts file path."
333         fi
334
335         # if hosts are specified on the command line, process just
336         # those hosts
337         if [ "$1" ] ; then
338             update_known_hosts "$@"
339             RETURN="$?"
340
341         # otherwise, if no hosts are specified, process every host
342         # in the user's known_hosts file
343         else
344             # exit if the known_hosts file does not exist
345             if [ ! -e "$KNOWN_HOSTS" ] ; then
346                 log "known_hosts file '$KNOWN_HOSTS' does not exist."
347                 exit
348             fi
349
350             process_known_hosts
351             RETURN="$?"
352         fi
353         ;;
354
355     'update-authorized_keys'|'update-authorized-keys'|'a')
356         MODE='authorized_keys'
357
358         # check permissions on the authorized_user_ids file path
359         if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
360             failure "Improper permissions on authorized_user_ids file path."
361         fi
362
363         # check permissions on the authorized_keys file path
364         if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
365             failure "Improper permissions on authorized_keys file path."
366         fi
367
368         # exit if the authorized_user_ids file is empty
369         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
370             log "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     '--help'|'help'|'-h'|'h'|'?')
388         usage
389         ;;
390
391     *)
392         failure "Unknown command: '$COMMAND'
393 Type '$PGRM help' for usage."
394         ;;
395 esac
396
397 exit "$RETURN"