move config directory to ~/.monkeysphere
[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 # 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 SHARE=${MONKEYSPHERE_SHARE:-"/usr/share/monkeysphere"}
17 export SHARE
18 . "${SHARE}/common" || exit 1
19
20 # date in UTF 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     TEMP=$(getopt -o l:e: -l length:,expire: -n "$PGRM" -- "$@")
67
68     if [ $? != 0 ] ; then
69         exit 1
70     fi
71
72     # Note the quotes around `$TEMP': they are essential!
73     eval set -- "$TEMP"
74
75     while true ; do
76         case "$1" in
77             -l|--length)
78                 keyLength="$2"
79                 shift 2
80                 ;;
81             -e|--expire)
82                 keyExpire="$2"
83                 shift 2
84                 ;;
85             --)
86                 shift
87                 ;;
88             *)
89                 break
90                 ;;
91         esac
92     done
93
94     if [ -z "$1" ] ; then
95         # find all secret keys
96         keyID=$(gpg --with-colons --list-secret-keys | grep ^sec | cut -f5 -d: | sort -u)
97         # if multiple sec keys exist, fail
98         if (( $(echo "$keyID" | wc -l) > 1 )) ; then
99             echo "Multiple secret keys found:"
100             echo "$keyID"
101             failure "Please specify which primary key to use."
102         fi
103     else
104         keyID="$1"
105     fi
106     if [ -z "$keyID" ] ; then
107         failure "You have no secret key available.  You should create an OpenPGP
108 key before joining the monkeysphere. You can do this with:
109    gpg --gen-key"
110     fi
111
112     # get key output, and fail if not found
113     gpgOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons \
114         "$keyID") || failure
115
116     # fail if multiple sec lines are returned, which means the id
117     # given is not unique
118     if [ $(echo "$gpgOut" | grep -c '^sec:') -gt '1' ] ; then
119         failure "Key ID '$keyID' is not unique."
120     fi
121
122     # prompt if an authentication subkey already exists
123     if echo "$gpgOut" | egrep "^(sec|ssb):" | cut -d: -f 12 | grep -q a ; then
124         echo "An authentication subkey already exists for key '$keyID'."
125         read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
126         if [ "${OK/y/Y}" != 'Y' ] ; then
127             failure "aborting."
128         fi
129     fi
130
131     # set subkey defaults
132     # prompt about key expiration if not specified
133     keyExpire=$(get_gpg_expiration "$keyExpire")
134
135     # generate the list of commands that will be passed to edit-key
136     editCommands=$(cat <<EOF
137 addkey
138 7
139 S
140 E
141 A
142 Q
143 $keyLength
144 $keyExpire
145 save
146 EOF
147 )
148
149     log verbose "generating subkey..."
150     fifoDir=$(mktemp -d)
151     (umask 077 && mkfifo "$fifoDir/pass")
152     echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
153
154     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
155
156     rm -rf "$fifoDir"
157     wait
158     log verbose "done."
159 }
160
161 function subkey_to_ssh_agent() {
162     # try to add all authentication subkeys to the agent:
163
164     local sshaddresponse
165     local secretkeys
166     local authsubkeys
167     local workingdir
168     local keysuccess
169     local subkey
170     local publine
171     local kname
172
173     if ! test_gnu_dummy_s2k_extension ; then
174         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
175 You may want to consider patching or upgrading.
176
177 For more details, see:
178  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
179     fi
180
181     # if there's no agent running, don't bother:
182     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
183         failure "No ssh-agent available."
184     fi
185
186     # and if it looks like it's running, but we can't actually talk to
187     # it, bail out:
188     ssh-add -l >/dev/null
189     sshaddresponse="$?"
190     if [ "$sshaddresponse" = "2" ]; then
191         failure "Could not connect to ssh-agent"
192     fi
193     
194     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
195     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
196         grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
197
198     if [ -z "$secretkeys" ]; then
199         failure "You have no secret keys in your keyring!
200 You might want to run 'gpg --gen-key'."
201     fi
202     
203     authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
204         --fingerprint --fingerprint $secretkeys | \
205         cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
206         grep '^fpr::' | cut -f3 -d: | sort -u)
207
208     if [ -z "$authsubkeys" ]; then
209         failure "no authentication-capable subkeys available.
210 You might want to 'monkeysphere gen-subkey'"
211     fi
212
213     workingdir=$(mktemp -d)
214     umask 077
215     mkfifo "$workingdir/passphrase"
216     keysuccess=1
217
218     # FIXME: we're currently allowing any other options to get passed
219     # through to ssh-add.  should we limit it to known ones?  For
220     # example: -d or -c and/or -t <lifetime> 
221
222     for subkey in $authsubkeys; do 
223         # choose a label by which this key will be known in the agent:
224         # we are labelling the key by User ID instead of by
225         # fingerprint, but filtering out all / characters to make sure
226         # the filename is legit.
227
228         primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
229
230         #kname="[monkeysphere] $primaryuid"
231         kname="$primaryuid"
232
233         if [ "$1" = '-d' ]; then
234             # we're removing the subkey:
235             gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
236             (cd "$workingdir" && ssh-add -d "$kname")
237         else
238             # we're adding the subkey:
239             mkfifo "$workingdir/$kname"
240             gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
241                 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
242                 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
243             (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
244
245             passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
246             wait %2
247         fi
248         keysuccess="$?"
249
250         rm -f "$workingdir/$kname"
251     done
252
253     rm -rf "$workingdir"
254
255     # FIXME: sort out the return values: we're just returning the
256     # success or failure of the final authentication subkey in this
257     # case.  What if earlier ones failed?
258     exit "$keysuccess"
259 }
260
261 ########################################################################
262 # MAIN
263 ########################################################################
264
265 # unset variables that should be defined only in config file
266 unset KEYSERVER
267 unset CHECK_KEYSERVER
268 unset KNOWN_HOSTS
269 unset HASH_KNOWN_HOSTS
270 unset AUTHORIZED_KEYS
271
272 # load global config
273 [ -r "${ETC}/monkeysphere.conf" ] && . "${ETC}/monkeysphere.conf"
274
275 # set monkeysphere home directory
276 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
277 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
278
279 # load local config
280 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
281
282 # set empty config variables with ones from the environment, or from
283 # config file, or with defaults
284 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
285 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
286 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
287 # if keyserver not specified in env or monkeysphere.conf,
288 # look in gpg.conf
289 if [ -z "$KEYSERVER" ] ; then
290     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
291         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
292     fi
293 fi
294 # if it's still not specified, use the default
295 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
296 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
297 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
298 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
299 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
300
301 # other variables not in config file
302 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
303 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
304 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
305
306 # export GNUPGHOME and make sure gpg home exists with proper
307 # permissions
308 export GNUPGHOME
309 mkdir -p -m 0700 "$GNUPGHOME"
310 export LOG_LEVEL
311
312 # get subcommand
313 COMMAND="$1"
314 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
315 shift
316
317 case $COMMAND in
318     'update-known_hosts'|'update-known-hosts'|'k')
319         MODE='known_hosts'
320
321         # check permissions on the known_hosts file path
322         if ! check_key_file_permissions "$USER" "$KNOWN_HOSTS" ; then
323             failure "Improper permissions on known_hosts file path."
324         fi
325
326         # if hosts are specified on the command line, process just
327         # those hosts
328         if [ "$1" ] ; then
329             update_known_hosts "$@"
330             RETURN="$?"
331
332         # otherwise, if no hosts are specified, process every host
333         # in the user's known_hosts file
334         else
335             # exit if the known_hosts file does not exist
336             if [ ! -e "$KNOWN_HOSTS" ] ; then
337                 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
338                 exit
339             fi
340
341             process_known_hosts
342             RETURN="$?"
343         fi
344         ;;
345
346     'update-authorized_keys'|'update-authorized-keys'|'a')
347         MODE='authorized_keys'
348
349         # check permissions on the authorized_user_ids file path
350         if ! check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" ; then
351             failure "Improper permissions on authorized_user_ids file path."
352         fi
353
354         # check permissions on the authorized_keys file path
355         if ! check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" ; then
356             failure "Improper permissions on authorized_keys file path."
357         fi
358
359         # exit if the authorized_user_ids file is empty
360         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
361             log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
362             exit
363         fi
364
365         # process authorized_user_ids file
366         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
367         RETURN="$?"
368         ;;
369
370     'gen-subkey'|'g')
371         gen_subkey "$@"
372         ;;
373
374     'subkey-to-ssh-agent'|'s')
375         subkey_to_ssh_agent "$@"
376         ;;
377
378     '--help'|'help'|'-h'|'h'|'?')
379         usage
380         ;;
381
382     *)
383         failure "Unknown command: '$COMMAND'
384 Type '$PGRM help' for usage."
385         ;;
386 esac
387
388 exit "$RETURN"