Merge commit 'dkg/master'
[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     TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o l:e: -l length:,expire: -n "$PGRM" -- "$@") || failure "getopt failed!  Does your getopt support GNU-style long options?"
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 ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
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 to GnuTLS 2.6 or later.
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 ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
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 "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/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         # touch the known_hosts file so that the file permission check
322         # below won't fail upon not finding the file
323         (umask 0022 && touch "$KNOWN_HOSTS")
324
325         # check permissions on the known_hosts file path
326         check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
327
328         # if hosts are specified on the command line, process just
329         # those hosts
330         if [ "$1" ] ; then
331             update_known_hosts "$@"
332             RETURN="$?"
333
334         # otherwise, if no hosts are specified, process every host
335         # in the user's known_hosts file
336         else
337             # exit if the known_hosts file does not exist
338             if [ ! -e "$KNOWN_HOSTS" ] ; then
339                 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
340                 exit
341             fi
342
343             process_known_hosts
344             RETURN="$?"
345         fi
346         ;;
347
348     'update-authorized_keys'|'update-authorized-keys'|'a')
349         MODE='authorized_keys'
350
351         # check permissions on the authorized_user_ids file path
352         check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
353
354         # check permissions on the authorized_keys file path
355         check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
356
357         # exit if the authorized_user_ids file is empty
358         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
359             log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
360             exit
361         fi
362
363         # process authorized_user_ids file
364         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
365         RETURN="$?"
366         ;;
367
368     'gen-subkey'|'g')
369         gen_subkey "$@"
370         ;;
371
372     'subkey-to-ssh-agent'|'s')
373         subkey_to_ssh_agent "$@"
374         ;;
375
376     '--help'|'help'|'-h'|'h'|'?')
377         usage
378         ;;
379
380     *)
381         failure "Unknown command: '$COMMAND'
382 Type '$PGRM help' for usage."
383         ;;
384 esac
385
386 exit "$RETURN"