resyn implementation of m-s gen-key with the help output
[monkeysphere.git] / src / monkeysphere-server
1 #!/usr/bin/env bash
2
3 # monkeysphere-server: MonkeySphere server admin tool
4 #
5 # The monkeysphere scripts are written by:
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 # Jamie McClelland <jm@mayfirst.org>
8 # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
9 #
10 # They are Copyright 2008, and are all released under the GPL, version 3
11 # or later.
12
13 ########################################################################
14 PGRM=$(basename $0)
15
16 SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
17 export SYSSHAREDIR
18 . "${SYSSHAREDIR}/common" || exit 1
19
20 SYSDATADIR=${MONKEYSPHERE_SYSDATADIR:-"/var/lib/monkeysphere"}
21 export SYSDATADIR
22
23 # 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             -l|--length)
410                 keyLength="$2"
411                 shift 2
412                 ;;
413             -e|--expire)
414                 keyExpire="$2"
415                 shift 2
416                 ;;
417             -r|--revoker)
418                 revoker="$2"
419                 shift 2
420                 ;;
421             *)
422                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
423                     failure "Unknown option '$1'.
424 Type '$PGRM help' for usage."
425                 fi
426                 hostName="$1"
427                 break
428                 ;;
429         esac
430     done
431
432     userID="ssh://${hostName}"
433
434     # prompt about key expiration if not specified
435     keyExpire=$(get_gpg_expiration "$keyExpire")
436
437     # set key parameters
438     keyParameters=\
439 "Key-Type: $keyType
440 Key-Length: $keyLength
441 Key-Usage: $keyUsage
442 Name-Real: $userID
443 Expire-Date: $keyExpire"
444
445     # add the revoker field if specified
446     # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key.
447     # FIXME: key is marked "sensitive"?  is this appropriate?
448     if [ "$revoker" ] ; then
449         keyParameters=\
450 "${keyParameters}
451 Revoker: 1:${revoker} sensitive"
452     fi
453
454     echo "The following key parameters will be used for the host private key:"
455     echo "$keyParameters"
456
457     read -p "Generate key? (Y/n) " OK; OK=${OK:=Y}
458     if [ ${OK/y/Y} != 'Y' ] ; then
459         failure "aborting."
460     fi
461
462     # add commit command
463     # must include blank line!
464     keyParameters=\
465 "${keyParameters}
466
467 %commit
468 %echo done"
469
470     log verbose "generating host key..."
471     echo "$keyParameters" | gpg_host --batch --gen-key
472
473     # find the key fingerprint of the newly generated key
474     fingerprint=$(fingerprint_server_key)
475
476     # export host ownertrust to authentication keyring
477     log verbose "setting ultimate owner trust for host key..."
478     echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust"
479
480     # translate the private key to ssh format, and export to a file
481     # for sshs usage.
482     # NOTE: assumes that the primary key is the proper key to use
483     (umask 077 && \
484         gpg_host --export-secret-key "$fingerprint" | \
485         openpgp2ssh "$fingerprint" > "${SYSDATADIR}/ssh_host_rsa_key")
486     log info "SSH host private key output to file: ${SYSDATADIR}/ssh_host_rsa_key"
487     ssh-keygen -y -f "${SYSDATADIR}/ssh_host_rsa_key" > "${SYSDATADIR}/ssh_host_rsa_key.pub"
488     log info "SSH host public key output to file: ${SYSDATADIR}/ssh_host_rsa_key.pub"
489     gpg_authentication "--export-options export-minimal --armor --export 0x${fingerprint}\!" > "${SYSDATADIR}/ssh_host_rsa_key.pub.gpg"
490     log info "SSH host public key in OpenPGP form: ${SYSDATADIR}/ssh_host_rsa_key.pub.gpg"
491
492     # show info about new key
493     show_server_key
494 }
495
496 # extend the lifetime of a host key:
497 extend_key() {
498     local fpr=$(fingerprint_server_key)
499     local extendTo="$1"
500
501     # get the new expiration date
502     extendTo=$(get_gpg_expiration "$extendTo")
503
504     gpg_host --quiet --command-fd 0 --edit-key "$fpr" <<EOF 
505 expire
506 $extendTo
507 save
508 EOF
509
510     echo
511     echo "NOTE: Host key expiration date adjusted, but not yet published."
512     echo "Run '$PGRM publish-key' to publish the new expiration date."
513 }
514
515 # add hostname user ID to server key
516 add_hostname() {
517     local userID
518     local fingerprint
519     local tmpuidMatch
520     local line
521     local adduidCommand
522
523     if [ -z "$1" ] ; then
524         failure "You must specify a hostname to add."
525     fi
526
527     userID="ssh://${1}"
528
529     fingerprint=$(fingerprint_server_key)
530
531     # match to only ultimately trusted user IDs
532     tmpuidMatch="u:$(echo $userID | gpg_escape)"
533
534     # find the index of the requsted user ID
535     # NOTE: this is based on circumstantial evidence that the order of
536     # this output is the appropriate index
537     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
538         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
539         failure "Host userID '$userID' already exists."
540     fi
541
542     echo "The following user ID will be added to the host key:"
543     echo "  $userID"
544     read -p "Are you sure you would like to add this user ID? (y/N) " OK; OK=${OK:=N}
545     if [ ${OK/y/Y} != 'Y' ] ; then
546         failure "User ID not added."
547     fi
548
549     # edit-key script command to add user ID
550     adduidCommand=$(cat <<EOF
551 adduid
552 $userID
553
554
555 save
556 EOF
557 )
558
559     # execute edit-key script
560     if echo "$adduidCommand" | \
561         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
562
563         # update the trustdb for the authentication keyring
564         gpg_authentication "--check-trustdb"
565
566         show_server_key
567
568         echo
569         echo "NOTE: User ID added to key, but key not published."
570         echo "Run '$PGRM publish-key' to publish the new user ID."
571     else
572         failure "Problem adding user ID."
573     fi
574 }
575
576 # revoke hostname user ID to server key
577 revoke_hostname() {
578     local userID
579     local fingerprint
580     local tmpuidMatch
581     local line
582     local uidIndex
583     local message
584     local revuidCommand
585
586     if [ -z "$1" ] ; then
587         failure "You must specify a hostname to revoke."
588     fi
589
590     echo "WARNING: There is a known bug in this function."
591     echo "This function has been known to occasionally revoke the wrong user ID."
592     echo "Please see the following bug report for more information:"
593     echo "http://web.monkeysphere.info/bugs/revoke-hostname-revoking-wrong-userid/"
594     read -p "Are you sure you would like to proceed? (y/N) " OK; OK=${OK:=N}
595     if [ ${OK/y/Y} != 'Y' ] ; then
596         failure "aborting."
597     fi
598
599     userID="ssh://${1}"
600
601     fingerprint=$(fingerprint_server_key)
602
603     # match to only ultimately trusted user IDs
604     tmpuidMatch="u:$(echo $userID | gpg_escape)"
605
606     # find the index of the requsted user ID
607     # NOTE: this is based on circumstantial evidence that the order of
608     # this output is the appropriate index
609     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
610         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
611         uidIndex=${line%%:*}
612     else
613         failure "No non-revoked user ID '$userID' is found."
614     fi
615
616     echo "The following host key user ID will be revoked:"
617     echo "  $userID"
618     read -p "Are you sure you would like to revoke this user ID? (y/N) " OK; OK=${OK:=N}
619     if [ ${OK/y/Y} != 'Y' ] ; then
620         failure "User ID not revoked."
621     fi
622
623     message="Hostname removed by monkeysphere-server $DATE"
624
625     # edit-key script command to revoke user ID
626     revuidCommand=$(cat <<EOF
627 $uidIndex
628 revuid
629 y
630 4
631 $message
632
633 y
634 save
635 EOF
636         )       
637
638     # execute edit-key script
639     if echo "$revuidCommand" | \
640         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
641
642         # update the trustdb for the authentication keyring
643         gpg_authentication "--check-trustdb"
644
645         show_server_key
646
647         echo
648         echo "NOTE: User ID revoked, but revocation not published."
649         echo "Run '$PGRM publish-key' to publish the revocation."
650     else
651         failure "Problem revoking user ID."
652     fi
653 }
654
655 # add a revoker to the host key
656 add_revoker() {
657     # FIXME: implement!
658     failure "not implemented yet!"
659 }
660
661 # revoke the host key
662 revoke_key() {
663     # FIXME: implement!
664     failure "not implemented yet!"
665 }
666
667 # publish server key to keyserver
668 publish_server_key() {
669     read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
670     if [ ${OK/y/Y} != 'Y' ] ; then
671         failure "key not published."
672     fi
673
674     # find the key fingerprint
675     fingerprint=$(fingerprint_server_key)
676
677     # publish host key
678     gpg_authentication "--keyserver $KEYSERVER --send-keys '0x${fingerprint}!'"
679 }
680
681
682 diagnostics() {
683 #  * check on the status and validity of the key and public certificates
684     local seckey
685     local keysfound
686     local curdate
687     local warnwindow
688     local warndate
689     local create
690     local expire
691     local uid
692     local fingerprint
693     local badhostkeys
694     local sshd_config
695     local problemsfound=0
696
697     # FIXME: what's the correct, cross-platform answer?
698     sshd_config=/etc/ssh/sshd_config
699     seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
700     keysfound=$(echo "$seckey" | grep -c ^sec:)
701     curdate=$(date +%s)
702     # warn when anything is 2 months away from expiration
703     warnwindow='2 months'
704     warndate=$(advance_date $warnwindow +%s)
705
706     if ! id monkeysphere >/dev/null ; then
707         echo "! No monkeysphere user found!  Please create a monkeysphere system user with bash as its shell."
708         problemsfound=$(($problemsfound+1))
709     fi
710
711     if ! [ -d "$SYSDATADIR" ] ; then
712         echo "! no $SYSDATADIR directory found.  Please create it."
713         problemsfound=$(($problemsfound+1))
714     fi
715
716     echo "Checking host GPG key..."
717     if (( "$keysfound" < 1 )); then
718         echo "! No host key found."
719         echo " - Recommendation: run 'monkeysphere-server gen-key'"
720         problemsfound=$(($problemsfound+1))
721     elif (( "$keysfound" > 1 )); then
722         echo "! More than one host key found?"
723         # FIXME: recommend a way to resolve this
724         problemsfound=$(($problemsfound+1))
725     else
726         create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
727         expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
728         fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
729         # check for key expiration:
730         if [ "$expire" ]; then
731             if (( "$expire"  < "$curdate" )); then
732                 echo "! Host key is expired."
733                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
734                 problemsfound=$(($problemsfound+1))
735             elif (( "$expire" < "$warndate" )); then
736                 echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
737                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
738                 problemsfound=$(($problemsfound+1))
739             fi
740         fi
741
742         # and weirdnesses:
743         if [ "$create" ] && (( "$create" > "$curdate" )); then
744             echo "! Host key was created in the future(?!). Is your clock correct?"
745             echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
746             problemsfound=$(($problemsfound+1))
747         fi
748
749         # check for UserID expiration:
750         echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
751         while IFS=: read create expire uid ; do
752             # FIXME: should we be doing any checking on the form
753             # of the User ID?  Should we be unmangling it somehow?
754
755             if [ "$create" ] && (( "$create" > "$curdate" )); then
756                 echo "! User ID '$uid' was created in the future(?!).  Is your clock correct?"
757                 echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
758                 problemsfound=$(($problemsfound+1))
759             fi
760             if [ "$expire" ] ; then
761                 if (( "$expire" < "$curdate" )); then
762                     echo "! User ID '$uid' is expired."
763                     # FIXME: recommend a way to resolve this
764                     problemsfound=$(($problemsfound+1))
765                 elif (( "$expire" < "$warndate" )); then
766                     echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)             
767                     # FIXME: recommend a way to resolve this
768                     problemsfound=$(($problemsfound+1))
769                 fi
770             fi
771         done
772             
773 # FIXME: verify that the host key is properly published to the
774 #   keyservers (do this with the non-privileged user)
775
776 # FIXME: check that there are valid, non-expired certifying signatures
777 #   attached to the host key after fetching from the public keyserver
778 #   (do this with the non-privileged user as well)
779
780 # FIXME: propose adding a revoker to the host key if none exist (do we
781 #   have a way to do that after key generation?)
782
783         # Ensure that the ssh_host_rsa_key file is present and non-empty:
784         echo
785         echo "Checking host SSH key..."
786         if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
787             echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
788             problemsfound=$(($problemsfound+1))
789         else
790             if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
791                 echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
792                 problemsfound=$(($problemsfound+1))
793             fi
794
795             # propose changes needed for sshd_config (if any)
796             if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
797                 echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
798                 echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
799                 problemsfound=$(($problemsfound+1))
800             fi
801             if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
802                 echo "! $sshd_config refers to some non-monkeysphere host keys:"
803                 echo "$badhostkeys"
804                 echo " - Recommendation: remove the above HostKey lines from $sshd_config"
805                 problemsfound=$(($problemsfound+1))
806             fi
807
808         # FIXME: test (with ssh-keyscan?) that the running ssh
809         # daemon is actually offering the monkeysphere host key.
810
811         fi
812     fi
813
814 # FIXME: look at the ownership/privileges of the various keyrings,
815 #    directories housing them, etc (what should those values be?  can
816 #    we make them as minimal as possible?)
817
818 # FIXME: look to see that the ownertrust rules are set properly on the
819 #    authentication keyring
820
821 # FIXME: make sure that at least one identity certifier exists
822
823 # FIXME: look at the timestamps on the monkeysphere-generated
824 # authorized_keys files -- warn if they seem out-of-date.
825
826 # FIXME: check for a cronjob that updates monkeysphere-generated
827 # authorized_keys?
828
829     echo
830     echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
831     # Ensure that User ID authentication is enabled:
832     if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
833         echo "! $sshd_config does not point to monkeysphere authorized keys."
834         echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
835         problemsfound=$(($problemsfound+1))
836     fi
837     if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
838         echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
839         echo "$badauthorizedkeys"
840         echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
841         problemsfound=$(($problemsfound+1))
842     fi
843
844     if [ "$problemsfound" -gt 0 ]; then
845         echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
846         echo "  monkeysphere-server diagnostics"
847     else
848         echo "Everything seems to be in order!"
849     fi
850 }
851
852 # retrieve key from web of trust, import it into the host keyring, and
853 # ltsign the key in the host keyring so that it may certify other keys
854 add_certifier() {
855     local domain
856     local trust
857     local depth
858     local keyID
859     local fingerprint
860     local ltsignCommand
861     local trustval
862
863     # set default values for trust depth and domain
864     domain=
865     trust=full
866     depth=1
867
868     # get options
869     while true ; do
870         case "$1" in
871             -n|--domain)
872                 domain="$2"
873                 shift 2
874                 ;;
875             -t|--trust)
876                 trust="$2"
877                 shift 2
878                 ;;
879             -d|--depth)
880                 depth="$2"
881                 shift 2
882                 ;;
883             *)
884                 if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
885                     failure "Unknown option '$1'.
886 Type '$PGRM help' for usage."
887                 fi
888                 break
889                 ;;
890         esac
891     done
892
893     keyID="$1"
894     if [ -z "$keyID" ] ; then
895         failure "You must specify the key ID of a key to add, or specify a file to read the key from."
896     fi
897     if [ -f "$keyID" ] ; then
898         echo "Reading key from file '$keyID':"
899         importinfo=$(gpg_authentication "--import" < "$keyID" 2>&1) || failure "could not read key from '$keyID'"
900         # FIXME: if this is tried when the key database is not
901         # up-to-date, i got these errors (using set -x):
902
903 # ++ su -m monkeysphere -c '\''gpg --import'\''
904 # Warning: using insecure memory!
905 # gpg: key D21739E9: public key "Daniel Kahn Gillmor <dkg@fifthhorseman.net>" imported
906 # gpg: Total number processed: 1
907 # gpg:               imported: 1  (RSA: 1)
908 # gpg: can'\''t create `/var/monkeysphere/gnupg-host/pubring.gpg.tmp'\'': Permission denied
909 # gpg: failed to rebuild keyring cache: Permission denied
910 # gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
911 # gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
912 # gpg: next trustdb check due at 2009-01-10'
913 # + failure 'could not read key from '\''/root/dkg.gpg'\'''
914 # + echo 'could not read key from '\''/root/dkg.gpg'\'''
915
916         keyID=$(echo "$importinfo" | grep '^gpg: key ' | cut -f2 -d: | cut -f3 -d\ )
917         if [ -z "$keyID" ] || [ $(echo "$keyID" | wc -l) -ne 1 ] ; then
918             failure "Expected there to be a single gpg key in the file."
919         fi
920     else
921         # get the key from the key server
922         gpg_authentication "--keyserver $KEYSERVER --recv-key '0x${keyID}!'" || failure "Could not receive a key with this ID from the '$KEYSERVER' keyserver."
923     fi
924
925     export keyID
926
927
928     # get the full fingerprint of a key ID
929     fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint 0x${keyID}!" | \
930         grep '^fpr:' | grep "$keyID" | cut -d: -f10)
931
932     if [ -z "$fingerprint" ] ; then
933         failure "Key '$keyID' not found."
934     fi
935
936     echo
937     echo "key found:"
938     gpg_authentication "--fingerprint 0x${fingerprint}!"
939
940     echo "Are you sure you want to add the above key as a"
941     read -p "certifier of users on this system? (y/N) " OK; OK=${OK:-N}
942     if [ "${OK/y/Y}" != 'Y' ] ; then
943         failure "Identity certifier not added."
944     fi
945
946     # export the key to the host keyring
947     gpg_authentication "--export 0x${fingerprint}!" | gpg_host --import
948
949     if [ "$trust" = marginal ]; then
950         trustval=1
951     elif [ "$trust" = full ]; then
952         trustval=2
953     else
954         failure "Trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)."
955     fi
956
957     # ltsign command
958     # NOTE: *all* user IDs will be ltsigned
959     ltsignCommand=$(cat <<EOF
960 ltsign
961 y
962 $trustval
963 $depth
964 $domain
965 y
966 save
967 EOF
968         )
969
970     # ltsign the key
971     if echo "$ltsignCommand" | \
972         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
973
974         # update the trustdb for the authentication keyring
975         gpg_authentication "--check-trustdb"
976
977         echo
978         echo "Identity certifier added."
979     else
980         failure "Problem adding identify certifier."
981     fi
982 }
983
984 # delete a certifiers key from the host keyring
985 remove_certifier() {
986     local keyID
987     local fingerprint
988
989     keyID="$1"
990     if [ -z "$keyID" ] ; then
991         failure "You must specify the key ID of a key to remove."
992     fi
993
994     if gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key 0x${keyID}!" ; then
995         read -p "Really remove above listed identity certifier? (y/N) " OK; OK=${OK:-N}
996         if [ "${OK/y/Y}" != 'Y' ] ; then
997             failure "Identity certifier not removed."
998         fi
999     else
1000         failure
1001     fi
1002
1003     # delete the requested key
1004     if gpg_authentication "--delete-key --batch --yes 0x${keyID}!" ; then
1005         # delete key from host keyring as well
1006         gpg_host --delete-key --batch --yes "0x${keyID}!"
1007
1008         # update the trustdb for the authentication keyring
1009         gpg_authentication "--check-trustdb"
1010
1011         echo
1012         echo "Identity certifier removed."
1013     else
1014         failure "Problem removing identity certifier."
1015     fi
1016 }
1017
1018 # list the host certifiers
1019 list_certifiers() {
1020     local keys
1021     local key
1022
1023     # find trusted keys in authentication keychain
1024     keys=$(gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-keys --with-colons --fingerprint" | \
1025         grep ^pub: | cut -d: -f2,5 | egrep '^(u|f):' | cut -d: -f2)
1026
1027     # output keys
1028     for key in $keys ; do
1029         gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key --fingerprint $key"
1030     done
1031 }
1032
1033 # issue command to gpg-authentication keyring
1034 gpg_authentication_cmd() {
1035     gpg_authentication "$@"
1036 }
1037
1038 ########################################################################
1039 # MAIN
1040 ########################################################################
1041
1042 # unset variables that should be defined only in config file
1043 unset KEYSERVER
1044 unset AUTHORIZED_USER_IDS
1045 unset RAW_AUTHORIZED_KEYS
1046 unset MONKEYSPHERE_USER
1047
1048 # load configuration file
1049 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
1050
1051 # set empty config variable with ones from the environment, or with
1052 # defaults
1053 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
1054 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
1055 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
1056 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
1057 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
1058
1059 # other variables
1060 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
1061 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
1062 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
1063 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}
1064
1065 # export variables needed in su invocation
1066 export DATE
1067 export MODE
1068 export MONKEYSPHERE_USER
1069 export LOG_LEVEL
1070 export KEYSERVER
1071 export CHECK_KEYSERVER
1072 export REQUIRED_USER_KEY_CAPABILITY
1073 export GNUPGHOME_HOST
1074 export GNUPGHOME_AUTHENTICATION
1075 export GNUPGHOME
1076
1077 # get subcommand
1078 COMMAND="$1"
1079 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
1080 shift
1081
1082 case $COMMAND in
1083     'update-users'|'update-user'|'u')
1084         check_host_keyring
1085         update_users "$@"
1086         ;;
1087
1088     'import-key'|'i')
1089         import_key "$@"
1090         ;;
1091
1092     'gen-key'|'g')
1093         gen_key "$@"
1094         ;;
1095
1096     'extend-key'|'e')
1097         check_host_keyring
1098         extend_key "$@"
1099         ;;
1100
1101     'add-hostname'|'add-name'|'n+')
1102         check_host_keyring
1103         add_hostname "$@"
1104         ;;
1105
1106     'revoke-hostname'|'revoke-name'|'n-')
1107         check_host_keyring
1108         revoke_hostname "$@"
1109         ;;
1110
1111     'add-revoker'|'o')
1112         check_host_keyring
1113         add_revoker "$@"
1114         ;;
1115
1116     'revoke-key'|'r')
1117         check_host_keyring
1118         revoke_key "$@"
1119         ;;
1120
1121     'show-key'|'show'|'s')
1122         show_server_key
1123         ;;
1124
1125     'publish-key'|'publish'|'p')
1126         check_host_keyring
1127         publish_server_key
1128         ;;
1129
1130     'diagnostics'|'d')
1131         diagnostics
1132         ;;
1133
1134     'add-identity-certifier'|'add-id-certifier'|'add-certifier'|'c+')
1135         check_host_keyring
1136         add_certifier "$@"
1137         ;;
1138
1139     'remove-identity-certifier'|'remove-id-certifier'|'remove-certifier'|'c-')
1140         check_host_keyring
1141         remove_certifier "$@"
1142         ;;
1143
1144     'list-identity-certifiers'|'list-id-certifiers'|'list-certifiers'|'list-certifier'|'c')
1145         check_host_keyring
1146         list_certifiers "$@"
1147         ;;
1148
1149     'gpg-authentication-cmd')
1150         gpg_authentication_cmd "$@"
1151         ;;
1152
1153     'version'|'v')
1154         echo "$VERSION"
1155         ;;
1156
1157     '--help'|'help'|'-h'|'h'|'?')
1158         usage
1159         ;;
1160
1161     *)
1162         failure "Unknown command: '$COMMAND'
1163 Type '$PGRM help' for usage."
1164         ;;
1165 esac
1166
1167 exit "$RETURN"