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