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