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