Start modifications to output logging with LOG_LEVEL.
[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 >&2
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 info "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 info "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 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
284 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
285 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
286 # if keyserver not specified in env or monkeysphere.conf,
287 # look in gpg.conf
288 if [ -z "$KEYSERVER" ] ; then
289     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
290         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
291     fi
292 fi
293 # if it's still not specified, use the default
294 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
295 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
296 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
297 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
298 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
299
300 # other variables not in config file
301 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
302 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
303 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
304
305 # export GNUPGHOME and make sure gpg home exists with proper
306 # permissions
307 export GNUPGHOME
308 mkdir -p -m 0700 "$GNUPGHOME"
309 export LOG_LEVEL
310
311 # get subcommand
312 COMMAND="$1"
313 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
314 shift
315
316 case $COMMAND in
317     'update-known_hosts'|'update-known-hosts'|'k')
318         MODE='known_hosts'
319
320         # check permissions on the known_hosts file path
321         if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
322             failure "Improper permissions on known_hosts file path."
323         fi
324
325         # if hosts are specified on the command line, process just
326         # those hosts
327         if [ "$1" ] ; then
328             update_known_hosts "$@"
329             RETURN="$?"
330
331         # otherwise, if no hosts are specified, process every host
332         # in the user's known_hosts file
333         else
334             # exit if the known_hosts file does not exist
335             if [ ! -e "$KNOWN_HOSTS" ] ; then
336                 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
337                 exit
338             fi
339
340             process_known_hosts
341             RETURN="$?"
342         fi
343         ;;
344
345     'update-authorized_keys'|'update-authorized-keys'|'a')
346         MODE='authorized_keys'
347
348         # check permissions on the authorized_user_ids file path
349         if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
350             failure "Improper permissions on authorized_user_ids file path."
351         fi
352
353         # check permissions on the authorized_keys file path
354         if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
355             failure "Improper permissions on authorized_keys file path."
356         fi
357
358         # exit if the authorized_user_ids file is empty
359         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
360             log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
361             exit
362         fi
363
364         # process authorized_user_ids file
365         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
366         RETURN="$?"
367         ;;
368
369     'gen-subkey'|'g')
370         gen_subkey "$@"
371         ;;
372
373     'subkey-to-ssh-agent'|'s')
374         subkey_to_ssh_agent "$@"
375         ;;
376
377     '--help'|'help'|'-h'|'h'|'?')
378         usage
379         ;;
380
381     *)
382         failure "Unknown command: '$COMMAND'
383 Type '$PGRM help' for usage."
384         ;;
385 esac
386
387 exit "$RETURN"