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