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