Merge commit 'jrollins/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  import-subkey (i)                   import existing ssh key as gpg subkey
45    --keyfile (-f) FILE                 key file to import
46    --expire (-e) EXPIRE                date to expire
47  gen-subkey (g) [KEYID]              generate an authentication subkey
48    --length (-l) BITS                  key length in bits (2048)
49    --expire (-e) EXPIRE                date to expire
50  subkey-to-ssh-agent (s)             store authentication subkey in ssh-agent
51  version (v)                         show version number
52  help (h,?)                          this help
53
54 EOF
55 }
56
57 # import an existing ssh key as a gpg subkey
58 import_subkey() {
59     local keyFile="~/.ssh/id_rsa"
60     local keyExpire
61     local keyID
62     local gpgOut
63     local userID
64
65     # get options
66     while true ; do
67         case "$1" in
68             -f|--keyfile)
69                 keyFile="$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     log verbose "importing ssh key..."
87     fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
88     (umask 077 && mkfifo "$fifoDir/pass")
89     ssh2openpgp | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --import &
90
91     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
92
93     rm -rf "$fifoDir"
94     wait
95     log verbose "done."
96 }
97
98 # generate a subkey with the 'a' usage flags set
99 gen_subkey(){
100     local keyLength
101     local keyExpire
102     local keyID
103     local gpgOut
104     local userID
105
106     # get options
107     while true ; do
108         case "$1" in
109             -l|--length)
110                 keyLength="$2"
111                 shift 2
112                 ;;
113             -e|--expire)
114                 keyExpire="$2"
115                 shift 2
116                 ;;
117             *)
118                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
119                     failure "Unknown option '$1'.
120 Type '$PGRM help' for usage."
121                 fi
122                 break
123                 ;;
124         esac
125     done
126
127     case "$#" in
128         0)
129             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons 2>/dev/null | egrep '^sec:')
130             ;;
131         1)
132             gpgSecOut=$(gpg --quiet --fixed-list-mode --list-secret-keys --with-colons "$1" | egrep '^sec:') || failure
133             ;;
134         *)
135             failure "You must specify only a single primary key ID."
136             ;;
137     esac
138
139     # check that only a single secret key was found
140     case $(echo "$gpgSecOut" | grep -c '^sec:') in
141         0)
142             failure "No secret keys found.  Create an OpenPGP key with the following command:
143  gpg --gen-key"
144             ;;
145         1)
146             keyID=$(echo "$gpgSecOut" | cut -d: -f5)
147             ;;
148         *)
149             echo "Multiple primary secret keys found:"
150             echo "$gpgSecOut" | cut -d: -f5
151             failure "Please specify which primary key to use."
152             ;;
153     esac
154
155     # check that a valid authentication key does not already exist
156     IFS=$'\n'
157     for line in $(gpg --quiet --fixed-list-mode --list-keys --with-colons "$keyID") ; do
158         type=$(echo "$line" | cut -d: -f1)
159         validity=$(echo "$line" | cut -d: -f2)
160         usage=$(echo "$line" | cut -d: -f12)
161
162         # look at keys only
163         if [ "$type" != 'pub' -a "$type" != 'sub' ] ; then
164             continue
165         fi
166         # check for authentication capability
167         if ! check_capability "$usage" 'a' ; then
168             continue
169         fi
170         # if authentication key is valid, prompt to continue
171         if [ "$validity" = 'u' ] ; then
172             echo "A valid authentication key already exists for primary key '$keyID'."
173             read -p "Are you sure you would like to generate another one? (y/N) " OK; OK=${OK:N}
174             if [ "${OK/y/Y}" != 'Y' ] ; then
175                 failure "aborting."
176             fi
177             break
178         fi
179     done
180
181     # set subkey defaults
182     # prompt about key expiration if not specified
183     keyExpire=$(get_gpg_expiration "$keyExpire")
184
185     # generate the list of commands that will be passed to edit-key
186     editCommands=$(cat <<EOF
187 addkey
188 7
189 S
190 E
191 A
192 Q
193 $keyLength
194 $keyExpire
195 save
196 EOF
197 )
198
199     log verbose "generating subkey..."
200     fifoDir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
201     (umask 077 && mkfifo "$fifoDir/pass")
202     echo "$editCommands" | gpg --passphrase-fd 3 3< "$fifoDir/pass" --expert --command-fd 0 --edit-key "$keyID" &
203
204     # FIXME: this needs to fail more gracefully if the passphrase is incorrect
205     passphrase_prompt  "Please enter your passphrase for $keyID: " "$fifoDir/pass"
206
207     rm -rf "$fifoDir"
208     wait
209     log verbose "done."
210 }
211
212 subkey_to_ssh_agent() {
213     # try to add all authentication subkeys to the agent:
214
215     local sshaddresponse
216     local secretkeys
217     local authsubkeys
218     local workingdir
219     local keysuccess
220     local subkey
221     local publine
222     local kname
223
224     if ! test_gnu_dummy_s2k_extension ; then
225         failure "Your version of GnuTLS does not seem capable of using with gpg's exported subkeys.
226 You may want to consider patching or upgrading to GnuTLS 2.6 or later.
227
228 For more details, see:
229  http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html"
230     fi
231
232     # if there's no agent running, don't bother:
233     if [ -z "$SSH_AUTH_SOCK" ] || ! which ssh-add >/dev/null ; then
234         failure "No ssh-agent available."
235     fi
236
237     # and if it looks like it's running, but we can't actually talk to
238     # it, bail out:
239     ssh-add -l >/dev/null
240     sshaddresponse="$?"
241     if [ "$sshaddresponse" = "2" ]; then
242         failure "Could not connect to ssh-agent"
243     fi
244     
245     # get list of secret keys (to work around https://bugs.g10code.com/gnupg/issue945):
246     secretkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode --fingerprint | \
247         grep '^fpr:' | cut -f10 -d: | awk '{ print "0x" $1 "!" }')
248
249     if [ -z "$secretkeys" ]; then
250         failure "You have no secret keys in your keyring!
251 You might want to run 'gpg --gen-key'."
252     fi
253     
254     authsubkeys=$(gpg --list-secret-keys --with-colons --fixed-list-mode \
255         --fingerprint --fingerprint $secretkeys | \
256         cut -f1,5,10,12 -d: | grep -A1 '^ssb:[^:]*::[^:]*a[^:]*$' | \
257         grep '^fpr::' | cut -f3 -d: | sort -u)
258
259     if [ -z "$authsubkeys" ]; then
260         failure "no authentication-capable subkeys available.
261 You might want to 'monkeysphere gen-subkey'"
262     fi
263
264     workingdir=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
265     umask 077
266     mkfifo "$workingdir/passphrase"
267     keysuccess=1
268
269     # FIXME: we're currently allowing any other options to get passed
270     # through to ssh-add.  should we limit it to known ones?  For
271     # example: -d or -c and/or -t <lifetime> 
272
273     for subkey in $authsubkeys; do 
274         # choose a label by which this key will be known in the agent:
275         # we are labelling the key by User ID instead of by
276         # fingerprint, but filtering out all / characters to make sure
277         # the filename is legit.
278
279         primaryuid=$(gpg --with-colons --list-key "0x${subkey}!" | grep '^pub:' | cut -f10 -d: | tr -d /)
280
281         #kname="[monkeysphere] $primaryuid"
282         kname="$primaryuid"
283
284         if [ "$1" = '-d' ]; then
285             # we're removing the subkey:
286             gpg --export "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname"
287             (cd "$workingdir" && ssh-add -d "$kname")
288         else
289             # we're adding the subkey:
290             mkfifo "$workingdir/$kname"
291             gpg --quiet --passphrase-fd 3 3<"$workingdir/passphrase" \
292                 --export-options export-reset-subkey-passwd,export-minimal,no-export-attributes \
293                 --export-secret-subkeys "0x${subkey}!" | openpgp2ssh "$subkey" > "$workingdir/$kname" &
294             (cd "$workingdir" && DISPLAY=nosuchdisplay SSH_ASKPASS=/bin/false ssh-add "$@" "$kname" </dev/null )&
295
296             passphrase_prompt "Enter passphrase for key $kname: " "$workingdir/passphrase"
297             wait %2
298         fi
299         keysuccess="$?"
300
301         rm -f "$workingdir/$kname"
302     done
303
304     rm -rf "$workingdir"
305
306     # FIXME: sort out the return values: we're just returning the
307     # success or failure of the final authentication subkey in this
308     # case.  What if earlier ones failed?
309     exit "$keysuccess"
310 }
311
312 ########################################################################
313 # MAIN
314 ########################################################################
315
316 # unset variables that should be defined only in config file
317 unset KEYSERVER
318 unset CHECK_KEYSERVER
319 unset KNOWN_HOSTS
320 unset HASH_KNOWN_HOSTS
321 unset AUTHORIZED_KEYS
322
323 # load global config
324 [ -r "${SYSCONFIGDIR}/monkeysphere.conf" ] && . "${SYSCONFIGDIR}/monkeysphere.conf"
325
326 # set monkeysphere home directory
327 MONKEYSPHERE_HOME=${MONKEYSPHERE_HOME:="${HOME}/.monkeysphere"}
328 mkdir -p -m 0700 "$MONKEYSPHERE_HOME"
329
330 # load local config
331 [ -e ${MONKEYSPHERE_CONFIG:="${MONKEYSPHERE_HOME}/monkeysphere.conf"} ] && . "$MONKEYSPHERE_CONFIG"
332
333 # set empty config variables with ones from the environment, or from
334 # config file, or with defaults
335 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
336 GNUPGHOME=${MONKEYSPHERE_GNUPGHOME:=${GNUPGHOME:="${HOME}/.gnupg"}}
337 KEYSERVER=${MONKEYSPHERE_KEYSERVER:="$KEYSERVER"}
338 # if keyserver not specified in env or monkeysphere.conf,
339 # look in gpg.conf
340 if [ -z "$KEYSERVER" ] ; then
341     if [ -f "${GNUPGHOME}/gpg.conf" ] ; then
342         KEYSERVER=$(grep -e "^[[:space:]]*keyserver " "${GNUPGHOME}/gpg.conf" | tail -1 | awk '{ print $2 }')
343     fi
344 fi
345 # if it's still not specified, use the default
346 KEYSERVER=${KEYSERVER:="subkeys.pgp.net"}
347 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:=${CHECK_KEYSERVER:="true"}}
348 KNOWN_HOSTS=${MONKEYSPHERE_KNOWN_HOSTS:=${KNOWN_HOSTS:="${HOME}/.ssh/known_hosts"}}
349 HASH_KNOWN_HOSTS=${MONKEYSPHERE_HASH_KNOWN_HOSTS:=${HASH_KNOWN_HOSTS:="true"}}
350 AUTHORIZED_KEYS=${MONKEYSPHERE_AUTHORIZED_KEYS:=${AUTHORIZED_KEYS:="${HOME}/.ssh/authorized_keys"}}
351
352 # other variables not in config file
353 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:="${MONKEYSPHERE_HOME}/authorized_user_ids"}
354 REQUIRED_HOST_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_HOST_KEY_CAPABILITY:="a"}
355 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
356
357 # export GNUPGHOME and make sure gpg home exists with proper
358 # permissions
359 export GNUPGHOME
360 mkdir -p -m 0700 "$GNUPGHOME"
361 export LOG_LEVEL
362
363 # get subcommand
364 COMMAND="$1"
365 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
366 shift
367
368 case $COMMAND in
369     'update-known_hosts'|'update-known-hosts'|'k')
370         MODE='known_hosts'
371
372         # touch the known_hosts file so that the file permission check
373         # below won't fail upon not finding the file
374         (umask 0022 && touch "$KNOWN_HOSTS")
375
376         # check permissions on the known_hosts file path
377         check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
378
379         # if hosts are specified on the command line, process just
380         # those hosts
381         if [ "$1" ] ; then
382             update_known_hosts "$@"
383             RETURN="$?"
384
385         # otherwise, if no hosts are specified, process every host
386         # in the user's known_hosts file
387         else
388             # exit if the known_hosts file does not exist
389             if [ ! -e "$KNOWN_HOSTS" ] ; then
390                 log error "known_hosts file '$KNOWN_HOSTS' does not exist."
391                 exit
392             fi
393
394             process_known_hosts
395             RETURN="$?"
396         fi
397         ;;
398
399     'update-authorized_keys'|'update-authorized-keys'|'a')
400         MODE='authorized_keys'
401
402         # check permissions on the authorized_user_ids file path
403         check_key_file_permissions "$USER" "$AUTHORIZED_USER_IDS" || failure
404
405         # check permissions on the authorized_keys file path
406         check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
407
408         # exit if the authorized_user_ids file is empty
409         if [ ! -e "$AUTHORIZED_USER_IDS" ] ; then
410             log error "authorized_user_ids file '$AUTHORIZED_USER_IDS' does not exist."
411             exit
412         fi
413
414         # process authorized_user_ids file
415         process_authorized_user_ids "$AUTHORIZED_USER_IDS"
416         RETURN="$?"
417         ;;
418
419     'import-subkey'|'i')
420         import_key "$@"
421         ;;
422
423     'gen-subkey'|'g')
424         gen_subkey "$@"
425         ;;
426
427     'subkey-to-ssh-agent'|'s')
428         subkey_to_ssh_agent "$@"
429         ;;
430
431     'version'|'v')
432         echo "$VERSION"
433         ;;
434
435     '--help'|'help'|'-h'|'h'|'?')
436         usage
437         ;;
438
439     *)
440         failure "Unknown command: '$COMMAND'
441 Type '$PGRM help' for usage."
442         ;;
443 esac
444
445 exit "$RETURN"