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