add more debugging output.
[monkeysphere.git] / src / monkeysphere-server
1 #!/usr/bin/env bash
2
3 # monkeysphere-server: MonkeySphere server admin 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 SYSDATADIR=${MONKEYSPHERE_SYSDATADIR:-"/var/lib/monkeysphere"}
21 export SYSDATADIR
22
23 # UTC date in ISO 8601 format if needed
24 DATE=$(date -u '+%FT%T')
25
26 # unset some environment variables that could screw things up
27 unset GREP_OPTIONS
28
29 # default return code
30 RETURN=0
31
32 ########################################################################
33 # FUNCTIONS
34 ########################################################################
35
36 usage() {
37     cat <<EOF >&2
38 usage: $PGRM <subcommand> [options] [args]
39 Monkeysphere server admin tool.
40
41 subcommands:
42  update-users (u) [USER]...          update user authorized_keys files
43
44  gen-key (g) [NAME[:PORT]]           generate gpg key for the server
45    --length (-l) BITS                  key length in bits (2048)
46    --expire (-e) EXPIRE                date to expire
47    --revoker (-r) FINGERPRINT          add a revoker
48  extend-key (e) EXPIRE               extend expiration to EXPIRE
49  add-hostname (n+) NAME[:PORT]       add hostname user ID to server key
50  revoke-hostname (n-) NAME[:PORT]    revoke hostname user ID
51  show-key (s)                        output all server host key information
52  publish-key (p)                     publish server host key to keyserver
53  diagnostics (d)                     report on server monkeysphere status
54
55  add-id-certifier (c+) KEYID         import and tsign a certification key
56    --domain (-n) DOMAIN                limit ID certifications to DOMAIN
57    --trust (-t) TRUST                  trust level of certifier (full)
58    --depth (-d) DEPTH                  trust depth for certifier (1)
59  remove-id-certifier (c-) KEYID      remove a certification key
60  list-id-certifiers (c)              list certification keys
61
62  gpg-authentication-cmd CMD          gnupg-authentication command
63
64  help (h,?)                          this help
65
66 EOF
67 }
68
69 # function to run command as monkeysphere user
70 su_monkeysphere_user() {
71     # if the current user is the monkeysphere user, then just eval
72     # command
73     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
74         eval "$@"
75
76     # otherwise su command as monkeysphere user
77     else
78         su "$MONKEYSPHERE_USER" -c "$@"
79     fi
80 }
81
82 # function to interact with the host gnupg keyring
83 gpg_host() {
84     local returnCode
85
86     GNUPGHOME="$GNUPGHOME_HOST"
87     export GNUPGHOME
88
89     # NOTE: we supress this warning because we need the monkeysphere
90     # user to be able to read the host pubring.  we realize this might
91     # be problematic, but it's the simplest solution, without too much
92     # loss of security.
93     gpg --no-permission-warning "$@"
94     returnCode="$?"
95
96     # always reset the permissions on the host pubring so that the
97     # monkeysphere user can read the trust signatures
98     chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
99     chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
100     
101     return "$returnCode"
102 }
103
104 # function to interact with the authentication gnupg keyring
105 # FIXME: this function requires basically accepts only a single
106 # argument because of problems with quote expansion.  this needs to be
107 # fixed/improved.
108 gpg_authentication() {
109     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
110     export GNUPGHOME
111
112     su_monkeysphere_user "gpg $@"
113 }
114
115 # output just key fingerprint
116 fingerprint_server_key() {
117     gpg_host --list-secret-keys --fingerprint \
118         --with-colons --fixed-list-mode 2> /dev/null | \
119         grep '^fpr:' | head -1 | cut -d: -f10
120 }
121
122 # output key information
123 show_server_key() {
124     local fingerprint
125     local tmpkey
126
127     fingerprint=$(fingerprint_server_key)
128     gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprint"
129
130     # dumping to a file named ' ' so that the ssh-keygen output
131     # doesn't claim any potentially bogus hostname(s):
132     tmpkey=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
133     gpg_authentication "--export $fingerprint" | openpgp2ssh "$fingerprint" 2>/dev/null > "$tmpkey/ "
134     echo -n "ssh fingerprint: "
135     (cd "$tmpkey" && ssh-keygen -l -f ' ' | awk '{ print $2 }')
136     rm -rf "$tmpkey"
137     echo -n "OpenPGP fingerprint: "
138     echo "$fingerprint"
139 }
140
141 # update authorized_keys for users
142 update_users() {
143     if [ "$1" ] ; then
144         # get users from command line
145         unames="$@"
146     else
147         # or just look at all users if none specified
148         unames=$(getent passwd | cut -d: -f1)
149     fi
150
151     # set mode
152     MODE="authorized_keys"
153
154     # set gnupg home
155     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
156
157     # check to see if the gpg trust database has been initialized
158     if [ ! -s "${GNUPGHOME}/trustdb.gpg" ] ; then
159         failure "GNUPG trust database uninitialized.  Please see MONKEYSPHERE-SERVER(8)."
160     fi
161
162     # make sure the authorized_keys directory exists
163     mkdir -p "${SYSDATADIR}/authorized_keys"
164
165     # loop over users
166     for uname in $unames ; do
167         # check all specified users exist
168         if ! getent passwd "$uname" >/dev/null ; then
169             log error "----- unknown user '$uname' -----"
170             continue
171         fi
172
173         log verbose "----- user: $uname -----"
174
175         # make temporary directory
176         TMPLOC=$(mktemp -d ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
177
178         # trap to delete temporary directory on exit
179         trap "rm -rf $TMPLOC" EXIT
180
181         # create temporary authorized_user_ids file
182         TMP_AUTHORIZED_USER_IDS="${TMPLOC}/authorized_user_ids"
183         touch "$TMP_AUTHORIZED_USER_IDS"
184
185         # create temporary authorized_keys file
186         AUTHORIZED_KEYS="${TMPLOC}/authorized_keys"
187         touch "$AUTHORIZED_KEYS"
188
189         # set restrictive permissions on the temporary files
190         # FIXME: is there a better way to do this?
191         chmod 0700 "$TMPLOC"
192         chmod 0600 "$AUTHORIZED_KEYS"
193         chmod 0600 "$TMP_AUTHORIZED_USER_IDS"
194         chown -R "$MONKEYSPHERE_USER" "$TMPLOC"
195
196         # process authorized_user_ids file
197         log debug "checking for authorized_user_ids..."
198         # translating ssh-style path variables
199         authorizedUserIDs=$(translate_ssh_variables "$uname" "$AUTHORIZED_USER_IDS")
200         if [ -s "$authorizedUserIDs" ] ; then
201             # check permissions on the authorized_user_ids file path
202             if check_key_file_permissions "$uname" "$authorizedUserIDs" ; then
203                 # copy user authorized_user_ids file to temporary
204                 # location
205                 cat "$authorizedUserIDs" > "$TMP_AUTHORIZED_USER_IDS"
206
207                 # export needed variables
208                 export AUTHORIZED_KEYS
209                 export TMP_AUTHORIZED_USER_IDS
210
211                 # process authorized_user_ids file, as monkeysphere
212                 # user
213                 su_monkeysphere_user \
214                     ". ${SYSSHAREDIR}/common; process_authorized_user_ids $TMP_AUTHORIZED_USER_IDS"
215                 RETURN="$?"
216             else
217                 log debug "not processing authorized_user_ids."
218             fi
219         else
220             log debug "empty or absent authorized_user_ids file."
221         fi
222
223         # add user-controlled authorized_keys file if specified
224         # translate ssh-style path variables
225         rawAuthorizedKeys=$(translate_ssh_variables "$uname" "$RAW_AUTHORIZED_KEYS")
226         if [ "$rawAuthorizedKeys" ] ; then
227             log debug "checking for raw authorized_keys..."
228             if [ -s "$rawAuthorizedKeys" ] ; then
229                 # check permissions on the authorized_keys file path
230                 if check_key_file_permissions "$uname" "$rawAuthorizedKeys" ; then
231                     log verbose "adding raw authorized_keys file... "
232                     cat "$rawAuthorizedKeys" >> "$AUTHORIZED_KEYS"
233                 else
234                     log debug "not adding raw authorized_keys file."            
235                 fi
236             else
237                 log debug "empty or absent authorized_keys file."
238             fi
239         fi
240
241         # move the new authorized_keys file into place
242         if [ -s "$AUTHORIZED_KEYS" ] ; then
243             # openssh appears to check the contents of the
244             # authorized_keys file as the user in question, so the
245             # file must be readable by that user at least.
246             # FIXME: is there a better way to do this?
247             chown root "$AUTHORIZED_KEYS"
248             chgrp $(getent passwd "$uname" | cut -f4 -d:) "$AUTHORIZED_KEYS"
249             chmod g+r "$AUTHORIZED_KEYS"
250
251             mv -f "$AUTHORIZED_KEYS" "${SYSDATADIR}/authorized_keys/${uname}"
252         else
253             rm -f "${SYSDATADIR}/authorized_keys/${uname}"
254         fi
255
256         # unset the trap
257         trap - EXIT
258
259         # destroy temporary directory
260         rm -rf "$TMPLOC"
261     done
262 }
263
264 # generate server gpg key
265 gen_key() {
266     local keyType
267     local keyLength
268     local keyUsage
269     local keyExpire
270     local revoker
271     local hostName
272     local userID
273     local keyParameters
274     local fingerprint
275
276     # set default key parameter values
277     keyType="RSA"
278     keyLength="2048"
279     keyUsage="auth"
280     keyExpire=
281     revoker=
282
283     # get options
284     TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o e:l:r -l expire:,length:,revoker: -n "$PGRM" -- "$@") || failure "getopt failed!  Does your getopt support GNU-style long options?"
285
286     if [ $? != 0 ] ; then
287         exit 1
288     fi
289
290     # Note the quotes around `$TEMP': they are essential!
291     eval set -- "$TEMP"
292
293     while true ; do
294         case "$1" in
295             -l|--length)
296                 keyLength="$2"
297                 shift 2
298                 ;;
299             -e|--expire)
300                 keyExpire="$2"
301                 shift 2
302                 ;;
303             -r|--revoker)
304                 revoker="$2"
305                 shift 2
306                 ;;
307             --)
308                 shift
309                 ;;
310             *)
311                 break
312                 ;;
313         esac
314     done
315
316     hostName=${1:-$(hostname -f)}
317     userID="ssh://${hostName}"
318
319     # check for presense of key with user ID
320     if gpg_host --list-key ="$userID" > /dev/null 2>&1 ; then
321         failure "Key for '$userID' already exists"
322     fi
323
324     # prompt about key expiration if not specified
325     keyExpire=$(get_gpg_expiration "$keyExpire")
326
327     # set key parameters
328     keyParameters=$(cat <<EOF
329 Key-Type: $keyType
330 Key-Length: $keyLength
331 Key-Usage: $keyUsage
332 Name-Real: $userID
333 Expire-Date: $keyExpire
334 EOF
335 )
336
337     # add the revoker field if specified
338     # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key.
339     # FIXME: key is marked "sensitive"?  is this appropriate?
340     if [ "$revoker" ] ; then
341         keyParameters="${keyParameters}"$(cat <<EOF
342 Revoker: 1:$revoker sensitive
343 EOF
344 )
345     fi
346
347     echo "The following key parameters will be used for the host private key:"
348     echo "$keyParameters"
349
350     read -p "Generate key? (Y/n) " OK; OK=${OK:=Y}
351     if [ ${OK/y/Y} != 'Y' ] ; then
352         failure "aborting."
353     fi
354
355     # add commit command
356     keyParameters="${keyParameters}"$(cat <<EOF
357
358 %commit
359 %echo done
360 EOF
361 )
362
363     log verbose "generating server key..."
364     echo "$keyParameters" | gpg_host --batch --gen-key
365
366     # output the server fingerprint
367     fingerprint_server_key "=${userID}"
368
369     # find the key fingerprint of the newly generated key
370     fingerprint=$(fingerprint_server_key)
371
372     # export host ownertrust to authentication keyring
373     log verbose "setting ultimate owner trust for server key..."
374     echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust"
375
376     # translate the private key to ssh format, and export to a file
377     # for sshs usage.
378     # NOTE: assumes that the primary key is the proper key to use
379     (umask 077 && \
380         gpg_host --export-secret-key "$fingerprint" | \
381         openpgp2ssh "$fingerprint" > "${SYSDATADIR}/ssh_host_rsa_key")
382     log info "private SSH host key output to file: ${SYSDATADIR}/ssh_host_rsa_key"
383 }
384
385 # extend the lifetime of a host key:
386 extend_key() {
387     local fpr=$(fingerprint_server_key)
388     local extendTo="$1"
389
390     if [ -z "$fpr" ] ; then
391         failure "You don't appear to have a MonkeySphere host key on this server.  Try 'monkeysphere-server gen-key' first."
392     fi
393
394     # get the new expiration date
395     extendTo=$(get_gpg_expiration "$extendTo")
396
397     gpg_host --quiet --command-fd 0 --edit-key "$fpr" <<EOF 
398 expire
399 $extendTo
400 save
401 EOF
402
403     echo
404     echo "NOTE: Host key expiration date adjusted, but not yet published."
405     echo "Run '$PGRM publish-key' to publish the new expiration date."
406 }
407
408 # add hostname user ID to server key
409 add_hostname() {
410     local userID
411     local fingerprint
412     local tmpuidMatch
413     local line
414     local adduidCommand
415
416     if [ -z "$1" ] ; then
417         failure "You must specify a hostname to add."
418     fi
419
420     userID="ssh://${1}"
421
422     fingerprint=$(fingerprint_server_key)
423
424     # match to only ultimately trusted user IDs
425     tmpuidMatch="u:$(echo $userID | gpg_escape)"
426
427     # find the index of the requsted user ID
428     # NOTE: this is based on circumstantial evidence that the order of
429     # this output is the appropriate index
430     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
431         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
432         failure "Host userID '$userID' already exists."
433     fi
434
435     echo "The following user ID will be added to the host key:"
436     echo "  $userID"
437     read -p "Are you sure you would like to add this user ID? (y/N) " OK; OK=${OK:=N}
438     if [ ${OK/y/Y} != 'Y' ] ; then
439         failure "User ID not added."
440     fi
441
442     # edit-key script command to add user ID
443     adduidCommand=$(cat <<EOF
444 adduid
445 $userID
446
447
448 save
449 EOF
450         )
451
452     # execute edit-key script
453     if echo "$adduidCommand" | \
454         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
455
456         # update the trustdb for the authentication keyring
457         gpg_authentication "--check-trustdb"
458
459         show_server_key
460
461         echo
462         echo "NOTE: User ID added to key, but key not published."
463         echo "Run '$PGRM publish-key' to publish the new user ID."
464     else
465         failure "Problem adding user ID."
466     fi
467 }
468
469 # revoke hostname user ID to server key
470 revoke_hostname() {
471     local userID
472     local fingerprint
473     local tmpuidMatch
474     local line
475     local uidIndex
476     local message
477     local revuidCommand
478
479     if [ -z "$1" ] ; then
480         failure "You must specify a hostname to revoke."
481     fi
482
483     echo "WARNING: There is a known bug in this function."
484     echo "This function has been known to occasionally revoke the wrong user ID."
485     echo "Please see the following bug report for more information:"
486     echo "http://web.monkeysphere.info/bugs/revoke-hostname-revoking-wrong-userid/"
487     read -p "Are you sure you would like to proceed? (y/N) " OK; OK=${OK:=N}
488     if [ ${OK/y/Y} != 'Y' ] ; then
489         failure "aborting."
490     fi
491
492     userID="ssh://${1}"
493
494     fingerprint=$(fingerprint_server_key)
495
496     # match to only ultimately trusted user IDs
497     tmpuidMatch="u:$(echo $userID | gpg_escape)"
498
499     # find the index of the requsted user ID
500     # NOTE: this is based on circumstantial evidence that the order of
501     # this output is the appropriate index
502     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
503         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
504         uidIndex=${line%%:*}
505     else
506         failure "No non-revoked user ID '$userID' is found."
507     fi
508
509     echo "The following host key user ID will be revoked:"
510     echo "  $userID"
511     read -p "Are you sure you would like to revoke this user ID? (y/N) " OK; OK=${OK:=N}
512     if [ ${OK/y/Y} != 'Y' ] ; then
513         failure "User ID not revoked."
514     fi
515
516     message="Hostname removed by monkeysphere-server $DATE"
517
518     # edit-key script command to revoke user ID
519     revuidCommand=$(cat <<EOF
520 $uidIndex
521 revuid
522 y
523 4
524 $message
525
526 y
527 save
528 EOF
529         )       
530
531     # execute edit-key script
532     if echo "$revuidCommand" | \
533         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
534
535         # update the trustdb for the authentication keyring
536         gpg_authentication "--check-trustdb"
537
538         show_server_key
539
540         echo
541         echo "NOTE: User ID revoked, but revocation not published."
542         echo "Run '$PGRM publish-key' to publish the revocation."
543     else
544         failure "Problem revoking user ID."
545     fi
546 }
547
548 # publish server key to keyserver
549 publish_server_key() {
550     read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
551     if [ ${OK/y/Y} != 'Y' ] ; then
552         failure "key not published."
553     fi
554
555     # find the key fingerprint
556     fingerprint=$(fingerprint_server_key)
557
558     # publish host key
559     gpg_authentication "--keyserver $KEYSERVER --send-keys '0x${fingerprint}!'"
560 }
561
562
563 diagnostics() {
564 #  * check on the status and validity of the key and public certificates
565     local seckey
566     local keysfound
567     local curdate
568     local warnwindow
569     local warndate
570     local create
571     local expire
572     local uid
573     local fingerprint
574     local badhostkeys
575     local sshd_config
576     local problemsfound=0
577
578     # FIXME: what's the correct, cross-platform answer?
579     sshd_config=/etc/ssh/sshd_config
580     seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
581     keysfound=$(echo "$seckey" | grep -c ^sec:)
582     curdate=$(date +%s)
583     # warn when anything is 2 months away from expiration
584     warnwindow='2 months'
585     warndate=$(advance_date $warnwindow +%s)
586
587     if ! id monkeysphere >/dev/null ; then
588         echo "! No monkeysphere user found!  Please create a monkeysphere system user with bash as its shell."
589         problemsfound=$(($problemsfound+1))
590     fi
591
592     if ! [ -d "$SYSDATADIR" ] ; then
593         echo "! no $SYSDATADIR directory found.  Please create it."
594         problemsfound=$(($problemsfound+1))
595     fi
596
597     echo "Checking host GPG key..."
598     if (( "$keysfound" < 1 )); then
599         echo "! No host key found."
600         echo " - Recommendation: run 'monkeysphere-server gen-key'"
601         problemsfound=$(($problemsfound+1))
602     elif (( "$keysfound" > 1 )); then
603         echo "! More than one host key found?"
604         # FIXME: recommend a way to resolve this
605         problemsfound=$(($problemsfound+1))
606     else
607         create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
608         expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
609         fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
610         # check for key expiration:
611         if [ "$expire" ]; then
612             if (( "$expire"  < "$curdate" )); then
613                 echo "! Host key is expired."
614                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
615                 problemsfound=$(($problemsfound+1))
616             elif (( "$expire" < "$warndate" )); then
617                 echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
618                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
619                 problemsfound=$(($problemsfound+1))
620             fi
621         fi
622
623         # and weirdnesses:
624         if [ "$create" ] && (( "$create" > "$curdate" )); then
625             echo "! Host key was created in the future(?!). Is your clock correct?"
626             echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
627             problemsfound=$(($problemsfound+1))
628         fi
629
630         # check for UserID expiration:
631         echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
632         while IFS=: read create expire uid ; do
633             # FIXME: should we be doing any checking on the form
634             # of the User ID?  Should we be unmangling it somehow?
635
636             if [ "$create" ] && (( "$create" > "$curdate" )); then
637                 echo "! User ID '$uid' was created in the future(?!).  Is your clock correct?"
638                 echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
639                 problemsfound=$(($problemsfound+1))
640             fi
641             if [ "$expire" ] ; then
642                 if (( "$expire" < "$curdate" )); then
643                     echo "! User ID '$uid' is expired."
644                     # FIXME: recommend a way to resolve this
645                     problemsfound=$(($problemsfound+1))
646                 elif (( "$expire" < "$warndate" )); then
647                     echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)             
648                     # FIXME: recommend a way to resolve this
649                     problemsfound=$(($problemsfound+1))
650                 fi
651             fi
652         done
653             
654 # FIXME: verify that the host key is properly published to the
655 #   keyservers (do this with the non-privileged user)
656
657 # FIXME: check that there are valid, non-expired certifying signatures
658 #   attached to the host key after fetching from the public keyserver
659 #   (do this with the non-privileged user as well)
660
661 # FIXME: propose adding a revoker to the host key if none exist (do we
662 #   have a way to do that after key generation?)
663
664         # Ensure that the ssh_host_rsa_key file is present and non-empty:
665         echo
666         echo "Checking host SSH key..."
667         if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
668             echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
669             problemsfound=$(($problemsfound+1))
670         else
671             if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
672                 echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
673                 problemsfound=$(($problemsfound+1))
674             fi
675
676             # propose changes needed for sshd_config (if any)
677             if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
678                 echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
679                 echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
680                 problemsfound=$(($problemsfound+1))
681             fi
682             if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
683                 echo "! $sshd_config refers to some non-monkeysphere host keys:"
684                 echo "$badhostkeys"
685                 echo " - Recommendation: remove the above HostKey lines from $sshd_config"
686                 problemsfound=$(($problemsfound+1))
687             fi
688         fi
689     fi
690
691 # FIXME: look at the ownership/privileges of the various keyrings,
692 #    directories housing them, etc (what should those values be?  can
693 #    we make them as minimal as possible?)
694
695 # FIXME: look to see that the ownertrust rules are set properly on the
696 #    authentication keyring
697
698 # FIXME: make sure that at least one identity certifier exists
699
700 # FIXME: look at the timestamps on the monkeysphere-generated
701 # authorized_keys files -- warn if they seem out-of-date.
702
703 # FIXME: check for a cronjob that updates monkeysphere-generated
704 # authorized_keys?
705
706     echo
707     echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
708     # Ensure that User ID authentication is enabled:
709     if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
710         echo "! $sshd_config does not point to monkeysphere authorized keys."
711         echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
712         problemsfound=$(($problemsfound+1))
713     fi
714     if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
715         echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
716         echo "$badauthorizedkeys"
717         echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
718         problemsfound=$(($problemsfound+1))
719     fi
720
721     if [ "$problemsfound" -gt 0 ]; then
722         echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
723         echo "  monkeysphere-server diagnostics"
724     else
725         echo "Everything seems to be in order!"
726     fi
727 }
728
729 # retrieve key from web of trust, import it into the host keyring, and
730 # ltsign the key in the host keyring so that it may certify other keys
731 add_certifier() {
732     local domain
733     local trust
734     local depth
735     local keyID
736     local fingerprint
737     local ltsignCommand
738     local trustval
739
740     # set default values for trust depth and domain
741     domain=
742     trust=full
743     depth=1
744
745     # get options
746     TEMP=$(PATH="/usr/local/bin:$PATH" getopt -o n:t:d: -l domain:,trust:,depth: -n "$PGRM" -- "$@") || failure "getopt failed!  Does your getopt support GNU-style long options?"
747
748     if [ $? != 0 ] ; then
749         exit 1
750     fi
751
752     # Note the quotes around `$TEMP': they are essential!
753     eval set -- "$TEMP"
754
755     while true ; do
756         case "$1" in
757             -n|--domain)
758                 domain="$2"
759                 shift 2
760                 ;;
761             -t|--trust)
762                 trust="$2"
763                 shift 2
764                 ;;
765             -d|--depth)
766                 depth="$2"
767                 shift 2
768                 ;;
769             --)
770                 shift
771                 ;;
772             *)
773                 break
774                 ;;
775         esac
776     done
777
778     keyID="$1"
779     if [ -z "$keyID" ] ; then
780         failure "You must specify the key ID of a key to add, or specify a file to read the key from."
781     fi
782     if [ -f "$keyID" ] ; then
783         echo "Reading key from file '$keyID':"
784         importinfo=$(gpg_authentication "--import" < "$keyID" 2>&1) || failure "could not read key from '$keyID'"
785         # FIXME: if this is tried when the key database is not
786         # up-to-date, i got these errors (using set -x):
787
788 # ++ su -m monkeysphere -c '\''gpg --import'\''
789 # Warning: using insecure memory!
790 # gpg: key D21739E9: public key "Daniel Kahn Gillmor <dkg@fifthhorseman.net>" imported
791 # gpg: Total number processed: 1
792 # gpg:               imported: 1  (RSA: 1)
793 # gpg: can'\''t create `/var/monkeysphere/gnupg-host/pubring.gpg.tmp'\'': Permission denied
794 # gpg: failed to rebuild keyring cache: Permission denied
795 # gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
796 # gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
797 # gpg: next trustdb check due at 2009-01-10'
798 # + failure 'could not read key from '\''/root/dkg.gpg'\'''
799 # + echo 'could not read key from '\''/root/dkg.gpg'\'''
800
801         keyID=$(echo "$importinfo" | grep '^gpg: key ' | cut -f2 -d: | cut -f3 -d\ )
802         if [ -z "$keyID" ] || [ $(echo "$keyID" | wc -l) -ne 1 ] ; then
803             failure "Expected there to be a single gpg key in the file."
804         fi
805     else
806         # get the key from the key server
807         gpg_authentication "--keyserver $KEYSERVER --recv-key '0x${keyID}!'" || failure "Could not receive a key with this ID from the '$KEYSERVER' keyserver."
808     fi
809
810     export keyID
811
812
813     # get the full fingerprint of a key ID
814     fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint 0x${keyID}!" | \
815         grep '^fpr:' | grep "$keyID" | cut -d: -f10)
816
817     if [ -z "$fingerprint" ] ; then
818         failure "Key '$keyID' not found."
819     fi
820
821     echo
822     echo "key found:"
823     gpg_authentication "--fingerprint 0x${fingerprint}!"
824
825     echo "Are you sure you want to add the above key as a"
826     read -p "certifier of users on this system? (y/N) " OK; OK=${OK:-N}
827     if [ "${OK/y/Y}" != 'Y' ] ; then
828         failure "Identity certifier not added."
829     fi
830
831     # export the key to the host keyring
832     gpg_authentication "--export 0x${fingerprint}!" | gpg_host --import
833
834     if [ "$trust" == marginal ]; then
835         trustval=1
836     elif [ "$trust" == full ]; then
837         trustval=2
838     else
839         failure "Trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)."
840     fi
841
842     # ltsign command
843     # NOTE: *all* user IDs will be ltsigned
844     ltsignCommand=$(cat <<EOF
845 ltsign
846 y
847 $trustval
848 $depth
849 $domain
850 y
851 save
852 EOF
853         )
854
855     # ltsign the key
856     if echo "$ltsignCommand" | \
857         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
858
859         # update the trustdb for the authentication keyring
860         gpg_authentication "--check-trustdb"
861
862         echo
863         echo "Identity certifier added."
864     else
865         failure "Problem adding identify certifier."
866     fi
867 }
868
869 # delete a certifiers key from the host keyring
870 remove_certifier() {
871     local keyID
872     local fingerprint
873
874     keyID="$1"
875     if [ -z "$keyID" ] ; then
876         failure "You must specify the key ID of a key to remove."
877     fi
878
879     if gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key 0x${keyID}!" ; then
880         read -p "Really remove above listed identity certifier? (y/N) " OK; OK=${OK:-N}
881         if [ "${OK/y/Y}" != 'Y' ] ; then
882             failure "Identity certifier not removed."
883         fi
884     else
885         failure
886     fi
887
888     # delete the requested key
889     if gpg_authentication "--delete-key --batch --yes 0x${keyID}!" ; then
890         # delete key from host keyring as well
891         gpg_host --delete-key --batch --yes "0x${keyID}!"
892
893         # update the trustdb for the authentication keyring
894         gpg_authentication "--check-trustdb"
895
896         echo
897         echo "Identity certifier removed."
898     else
899         failure "Problem removing identity certifier."
900     fi
901 }
902
903 # list the host certifiers
904 list_certifiers() {
905     local keys
906     local key
907
908     # find trusted keys in authentication keychain
909     keys=$(gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-keys --with-colons --fingerprint" | \
910         grep ^pub: | cut -d: -f2,5 | egrep '^(u|f):' | cut -d: -f2)
911
912     # output keys
913     for key in $keys ; do
914         gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key --fingerprint $key"
915     done
916 }
917
918 # issue command to gpg-authentication keyring
919 gpg_authentication_cmd() {
920     gpg_authentication "$@"
921 }
922
923 ########################################################################
924 # MAIN
925 ########################################################################
926
927 # unset variables that should be defined only in config file
928 unset KEYSERVER
929 unset AUTHORIZED_USER_IDS
930 unset RAW_AUTHORIZED_KEYS
931 unset MONKEYSPHERE_USER
932
933 # load configuration file
934 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
935
936 # set empty config variable with ones from the environment, or with
937 # defaults
938 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
939 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
940 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
941 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
942 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
943
944 # other variables
945 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
946 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
947 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
948 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}
949
950 # export variables needed in su invocation
951 export DATE
952 export MODE
953 export MONKEYSPHERE_USER
954 export LOG_LEVEL
955 export KEYSERVER
956 export CHECK_KEYSERVER
957 export REQUIRED_USER_KEY_CAPABILITY
958 export GNUPGHOME_HOST
959 export GNUPGHOME_AUTHENTICATION
960 export GNUPGHOME
961
962 # get subcommand
963 COMMAND="$1"
964 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
965 shift
966
967 case $COMMAND in
968     'update-users'|'update-user'|'u')
969         update_users "$@"
970         ;;
971
972     'gen-key'|'g')
973         gen_key "$@"
974         ;;
975
976     'extend-key'|'e')
977         extend_key "$@"
978         ;;
979
980     'add-hostname'|'add-name'|'n+')
981         add_hostname "$@"
982         ;;
983
984     'revoke-hostname'|'revoke-name'|'n-')
985         revoke_hostname "$@"
986         ;;
987
988     'show-key'|'show'|'s')
989         show_server_key
990         ;;
991
992     'publish-key'|'publish'|'p')
993         publish_server_key
994         ;;
995
996     'diagnostics'|'d')
997         diagnostics
998         ;;
999
1000     'add-identity-certifier'|'add-id-certifier'|'add-certifier'|'c+')
1001         add_certifier "$@"
1002         ;;
1003
1004     'remove-identity-certifier'|'remove-id-certifier'|'remove-certifier'|'c-')
1005         remove_certifier "$@"
1006         ;;
1007
1008     'list-identity-certifiers'|'list-id-certifiers'|'list-certifiers'|'list-certifier'|'c')
1009         list_certifiers "$@"
1010         ;;
1011
1012     'gpg-authentication-cmd')
1013         gpg_authentication_cmd "$@"
1014         ;;
1015
1016     '--help'|'help'|'-h'|'h'|'?')
1017         usage
1018         ;;
1019
1020     *)
1021         failure "Unknown command: '$COMMAND'
1022 Type '$PGRM help' for usage."
1023         ;;
1024 esac
1025
1026 exit "$RETURN"