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