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