break out import-key and gen-key from monkeysphere-host
[monkeysphere.git] / src / monkeysphere-host
1 #!/usr/bin/env bash
2
3 # monkeysphere-host: Monkeysphere host 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/host"}
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 host admin tool.
45
46 subcommands:
47  show-key (s)                        output all host key information
48  extend-key (e) EXPIRE               extend host key expiration
49  add-hostname (n+) NAME[:PORT]       add hostname user ID to host key
50  revoke-hostname (n-) NAME[:PORT]    revoke hostname user ID
51  add-revoker (o) FINGERPRINT         add a revoker to the host key
52  revoke-key (r)                      revoke host key
53  publish-key (p)                     publish server host key to keyserver
54
55  expert
56   import-key (i)                     import existing ssh key to gpg
57    --hostname (-h) NAME[:PORT]         hostname for key user ID
58    --keyfile (-f) FILE                 key file to import
59    --expire (-e) EXPIRE                date to expire
60   gen-key (g)                        generate gpg key for the host
61    --hostname (-h) NAME[:PORT]         hostname for key user ID
62    --length (-l) BITS                  key length in bits (2048)
63    --expire (-e) EXPIRE                date to expire
64    --revoker (-r) FINGERPRINT          add a revoker
65   diagnostics (d)                    monkeysphere host status
66
67  version (v)                         show version number
68  help (h,?)                          this help
69
70 EOF
71 }
72
73 # function to run command as monkeysphere user
74 su_monkeysphere_user() {
75     # if the current user is the monkeysphere user, then just eval
76     # command
77     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
78         eval "$@"
79
80     # otherwise su command as monkeysphere user
81     else
82         su "$MONKEYSPHERE_USER" -c "$@"
83     fi
84 }
85
86 # function to interact with the host gnupg keyring
87 gpg_host() {
88     local returnCode
89
90     GNUPGHOME="$GNUPGHOME_HOST"
91     export GNUPGHOME
92
93     # NOTE: we supress this warning because we need the monkeysphere
94     # user to be able to read the host pubring.  we realize this might
95     # be problematic, but it's the simplest solution, without too much
96     # loss of security.
97     gpg --no-permission-warning "$@"
98     returnCode="$?"
99
100     # always reset the permissions on the host pubring so that the
101     # monkeysphere user can read the trust signatures
102     chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
103     chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
104     
105     return "$returnCode"
106 }
107
108 # check if user is root
109 is_root() {
110     [ $(id -u 2>/dev/null) = '0' ]
111 }
112
113 # check that user is root, for functions that require root access
114 check_user() {
115     is_root || failure "You must be root to run this command."
116 }
117
118 # output just key fingerprint
119 fingerprint_server_key() {
120     # set the pipefail option so functions fails if can't read sec key
121     set -o pipefail
122
123     gpg_host --list-secret-keys --fingerprint \
124         --with-colons --fixed-list-mode 2> /dev/null | \
125         grep '^fpr:' | head -1 | cut -d: -f10 2>/dev/null
126 }
127
128 # function to check for host secret key
129 check_host_keyring() {
130     fingerprint_server_key >/dev/null \
131         || failure "You don't appear to have a Monkeysphere host key on this server.  Please run 'monkeysphere-server gen-key' first."
132 }
133
134 # output key information
135 show_server_key() {
136     local fingerprintPGP
137     local fingerprintSSH
138     local ret=0
139
140     # FIXME: you shouldn't have to be root to see the host key fingerprint
141     if is_root ; then
142         check_host_keyring
143         fingerprintPGP=$(fingerprint_server_key)
144         gpg_authentication "--fingerprint --list-key --list-options show-unusable-uids $fingerprintPGP" 2>/dev/null
145         echo "OpenPGP fingerprint: $fingerprintPGP"
146     else
147         log info "You must be root to see host OpenPGP fingerprint."
148         ret='1'
149     fi
150
151     if [ -f "${SYSDATADIR}/ssh_host_rsa_key.pub" ] ; then
152         fingerprintSSH=$(ssh-keygen -l -f "${SYSDATADIR}/ssh_host_rsa_key.pub" | \
153             awk '{ print $1, $2, $4 }')
154         echo "ssh fingerprint: $fingerprintSSH"
155     else
156         log info "SSH host key not found."
157         ret='1'
158     fi
159
160     return $ret
161 }
162
163 # extend the lifetime of a host key:
164 extend_key() {
165     local fpr=$(fingerprint_server_key)
166     local extendTo="$1"
167
168     # get the new expiration date
169     extendTo=$(get_gpg_expiration "$extendTo")
170
171     gpg_host --quiet --command-fd 0 --edit-key "$fpr" <<EOF 
172 expire
173 $extendTo
174 save
175 EOF
176
177     echo
178     echo "NOTE: Host key expiration date adjusted, but not yet published."
179     echo "Run '$PGRM publish-key' to publish the new expiration date."
180 }
181
182 # add hostname user ID to server key
183 add_hostname() {
184     local userID
185     local fingerprint
186     local tmpuidMatch
187     local line
188     local adduidCommand
189
190     if [ -z "$1" ] ; then
191         failure "You must specify a hostname to add."
192     fi
193
194     userID="ssh://${1}"
195
196     fingerprint=$(fingerprint_server_key)
197
198     # match to only ultimately trusted user IDs
199     tmpuidMatch="u:$(echo $userID | gpg_escape)"
200
201     # find the index of the requsted user ID
202     # NOTE: this is based on circumstantial evidence that the order of
203     # this output is the appropriate index
204     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
205         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
206         failure "Host userID '$userID' already exists."
207     fi
208
209     echo "The following user ID will be added to the host key:"
210     echo "  $userID"
211     read -p "Are you sure you would like to add this user ID? (y/N) " OK; OK=${OK:=N}
212     if [ ${OK/y/Y} != 'Y' ] ; then
213         failure "User ID not added."
214     fi
215
216     # edit-key script command to add user ID
217     adduidCommand=$(cat <<EOF
218 adduid
219 $userID
220
221
222 save
223 EOF
224 )
225
226     # execute edit-key script
227     if echo "$adduidCommand" | \
228         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
229
230         # update the trustdb for the authentication keyring
231         gpg_authentication "--check-trustdb"
232
233         show_server_key
234
235         echo
236         echo "NOTE: User ID added to key, but key not published."
237         echo "Run '$PGRM publish-key' to publish the new user ID."
238     else
239         failure "Problem adding user ID."
240     fi
241 }
242
243 # revoke hostname user ID to server key
244 revoke_hostname() {
245     local userID
246     local fingerprint
247     local tmpuidMatch
248     local line
249     local uidIndex
250     local message
251     local revuidCommand
252
253     if [ -z "$1" ] ; then
254         failure "You must specify a hostname to revoke."
255     fi
256
257     echo "WARNING: There is a known bug in this function."
258     echo "This function has been known to occasionally revoke the wrong user ID."
259     echo "Please see the following bug report for more information:"
260     echo "http://web.monkeysphere.info/bugs/revoke-hostname-revoking-wrong-userid/"
261     read -p "Are you sure you would like to proceed? (y/N) " OK; OK=${OK:=N}
262     if [ ${OK/y/Y} != 'Y' ] ; then
263         failure "aborting."
264     fi
265
266     userID="ssh://${1}"
267
268     fingerprint=$(fingerprint_server_key)
269
270     # match to only ultimately trusted user IDs
271     tmpuidMatch="u:$(echo $userID | gpg_escape)"
272
273     # find the index of the requsted user ID
274     # NOTE: this is based on circumstantial evidence that the order of
275     # this output is the appropriate index
276     if line=$(gpg_host --list-keys --with-colons --fixed-list-mode "0x${fingerprint}!" \
277         | egrep '^(uid|uat):' | cut -f2,10 -d: | grep -n -x -F "$tmpuidMatch") ; then
278         uidIndex=${line%%:*}
279     else
280         failure "No non-revoked user ID '$userID' is found."
281     fi
282
283     echo "The following host key user ID will be revoked:"
284     echo "  $userID"
285     read -p "Are you sure you would like to revoke this user ID? (y/N) " OK; OK=${OK:=N}
286     if [ ${OK/y/Y} != 'Y' ] ; then
287         failure "User ID not revoked."
288     fi
289
290     message="Hostname removed by monkeysphere-server $DATE"
291
292     # edit-key script command to revoke user ID
293     revuidCommand=$(cat <<EOF
294 $uidIndex
295 revuid
296 y
297 4
298 $message
299
300 y
301 save
302 EOF
303         )       
304
305     # execute edit-key script
306     if echo "$revuidCommand" | \
307         gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
308
309         # update the trustdb for the authentication keyring
310         gpg_authentication "--check-trustdb"
311
312         show_server_key
313
314         echo
315         echo "NOTE: User ID revoked, but revocation not published."
316         echo "Run '$PGRM publish-key' to publish the revocation."
317     else
318         failure "Problem revoking user ID."
319     fi
320 }
321
322 # add a revoker to the host key
323 add_revoker() {
324     # FIXME: implement!
325     failure "not implemented yet!"
326 }
327
328 # revoke the host key
329 revoke_key() {
330     # FIXME: implement!
331     failure "not implemented yet!"
332 }
333
334 # publish server key to keyserver
335 publish_server_key() {
336     read -p "Really publish host key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
337     if [ ${OK/y/Y} != 'Y' ] ; then
338         failure "key not published."
339     fi
340
341     # find the key fingerprint
342     fingerprint=$(fingerprint_server_key)
343
344     # publish host key
345     gpg_authentication "--keyserver $KEYSERVER --send-keys '0x${fingerprint}!'"
346 }
347
348 diagnostics() {
349 #  * check on the status and validity of the key and public certificates
350     local seckey
351     local keysfound
352     local curdate
353     local warnwindow
354     local warndate
355     local create
356     local expire
357     local uid
358     local fingerprint
359     local badhostkeys
360     local sshd_config
361     local problemsfound=0
362
363     # FIXME: what's the correct, cross-platform answer?
364     sshd_config=/etc/ssh/sshd_config
365     seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
366     keysfound=$(echo "$seckey" | grep -c ^sec:)
367     curdate=$(date +%s)
368     # warn when anything is 2 months away from expiration
369     warnwindow='2 months'
370     warndate=$(advance_date $warnwindow +%s)
371
372     if ! id monkeysphere >/dev/null ; then
373         echo "! No monkeysphere user found!  Please create a monkeysphere system user with bash as its shell."
374         problemsfound=$(($problemsfound+1))
375     fi
376
377     if ! [ -d "$SYSDATADIR" ] ; then
378         echo "! no $SYSDATADIR directory found.  Please create it."
379         problemsfound=$(($problemsfound+1))
380     fi
381
382     echo "Checking host GPG key..."
383     if (( "$keysfound" < 1 )); then
384         echo "! No host key found."
385         echo " - Recommendation: run 'monkeysphere-server gen-key'"
386         problemsfound=$(($problemsfound+1))
387     elif (( "$keysfound" > 1 )); then
388         echo "! More than one host key found?"
389         # FIXME: recommend a way to resolve this
390         problemsfound=$(($problemsfound+1))
391     else
392         create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
393         expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
394         fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
395         # check for key expiration:
396         if [ "$expire" ]; then
397             if (( "$expire"  < "$curdate" )); then
398                 echo "! Host key is expired."
399                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
400                 problemsfound=$(($problemsfound+1))
401             elif (( "$expire" < "$warndate" )); then
402                 echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
403                 echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
404                 problemsfound=$(($problemsfound+1))
405             fi
406         fi
407
408         # and weirdnesses:
409         if [ "$create" ] && (( "$create" > "$curdate" )); then
410             echo "! Host key was created in the future(?!). Is your clock correct?"
411             echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
412             problemsfound=$(($problemsfound+1))
413         fi
414
415         # check for UserID expiration:
416         echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
417         while IFS=: read create expire uid ; do
418             # FIXME: should we be doing any checking on the form
419             # of the User ID?  Should we be unmangling it somehow?
420
421             if [ "$create" ] && (( "$create" > "$curdate" )); then
422                 echo "! User ID '$uid' was created in the future(?!).  Is your clock correct?"
423                 echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
424                 problemsfound=$(($problemsfound+1))
425             fi
426             if [ "$expire" ] ; then
427                 if (( "$expire" < "$curdate" )); then
428                     echo "! User ID '$uid' is expired."
429                     # FIXME: recommend a way to resolve this
430                     problemsfound=$(($problemsfound+1))
431                 elif (( "$expire" < "$warndate" )); then
432                     echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)             
433                     # FIXME: recommend a way to resolve this
434                     problemsfound=$(($problemsfound+1))
435                 fi
436             fi
437         done
438             
439 # FIXME: verify that the host key is properly published to the
440 #   keyservers (do this with the non-privileged user)
441
442 # FIXME: check that there are valid, non-expired certifying signatures
443 #   attached to the host key after fetching from the public keyserver
444 #   (do this with the non-privileged user as well)
445
446 # FIXME: propose adding a revoker to the host key if none exist (do we
447 #   have a way to do that after key generation?)
448
449         # Ensure that the ssh_host_rsa_key file is present and non-empty:
450         echo
451         echo "Checking host SSH key..."
452         if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
453             echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
454             problemsfound=$(($problemsfound+1))
455         else
456             if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
457                 echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
458                 problemsfound=$(($problemsfound+1))
459             fi
460
461             # propose changes needed for sshd_config (if any)
462             if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
463                 echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
464                 echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
465                 problemsfound=$(($problemsfound+1))
466             fi
467             if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
468                 echo "! $sshd_config refers to some non-monkeysphere host keys:"
469                 echo "$badhostkeys"
470                 echo " - Recommendation: remove the above HostKey lines from $sshd_config"
471                 problemsfound=$(($problemsfound+1))
472             fi
473
474         # FIXME: test (with ssh-keyscan?) that the running ssh
475         # daemon is actually offering the monkeysphere host key.
476
477         fi
478     fi
479
480 # FIXME: look at the ownership/privileges of the various keyrings,
481 #    directories housing them, etc (what should those values be?  can
482 #    we make them as minimal as possible?)
483
484 # FIXME: look to see that the ownertrust rules are set properly on the
485 #    authentication keyring
486
487 # FIXME: make sure that at least one identity certifier exists
488
489 # FIXME: look at the timestamps on the monkeysphere-generated
490 # authorized_keys files -- warn if they seem out-of-date.
491
492 # FIXME: check for a cronjob that updates monkeysphere-generated
493 # authorized_keys?
494
495     echo
496     echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
497     # Ensure that User ID authentication is enabled:
498     if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
499         echo "! $sshd_config does not point to monkeysphere authorized keys."
500         echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
501         problemsfound=$(($problemsfound+1))
502     fi
503     if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
504         echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
505         echo "$badauthorizedkeys"
506         echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
507         problemsfound=$(($problemsfound+1))
508     fi
509
510     if [ "$problemsfound" -gt 0 ]; then
511         echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
512         echo "  monkeysphere-server diagnostics"
513     else
514         echo "Everything seems to be in order!"
515     fi
516 }
517
518 ########################################################################
519 # MAIN
520 ########################################################################
521
522 # unset variables that should be defined only in config file
523 unset KEYSERVER
524 unset AUTHORIZED_USER_IDS
525 unset RAW_AUTHORIZED_KEYS
526 unset MONKEYSPHERE_USER
527
528 # load configuration file
529 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
530
531 # set empty config variable with ones from the environment, or with
532 # defaults
533 LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
534 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
535 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
536 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
537 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
538
539 # other variables
540 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
541 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
542 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
543 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}
544
545 # export variables needed in su invocation
546 export DATE
547 export MODE
548 export MONKEYSPHERE_USER
549 export LOG_LEVEL
550 export KEYSERVER
551 export CHECK_KEYSERVER
552 export REQUIRED_USER_KEY_CAPABILITY
553 export GNUPGHOME_HOST
554 export GNUPGHOME_AUTHENTICATION
555 export GNUPGHOME
556
557 # get subcommand
558 COMMAND="$1"
559 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
560 shift
561
562 case $COMMAND in
563     'show-key'|'show'|'s')
564         show_server_key
565         ;;
566
567     'extend-key'|'e')
568         check_user
569         check_host_keyring
570         extend_key "$@"
571         ;;
572
573     'add-hostname'|'add-name'|'n+')
574         check_user
575         check_host_keyring
576         add_hostname "$@"
577         ;;
578
579     'revoke-hostname'|'revoke-name'|'n-')
580         check_user
581         check_host_keyring
582         revoke_hostname "$@"
583         ;;
584
585     'add-revoker'|'o')
586         check_user
587         check_host_keyring
588         add_revoker "$@"
589         ;;
590
591     'revoke-key'|'r')
592         check_user
593         check_host_keyring
594         revoke_key "$@"
595         ;;
596
597     'publish-key'|'publish'|'p')
598         check_user
599         check_host_keyring
600         publish_server_key
601         ;;
602
603     'expert'|'e')
604         check_user
605         SUBCOMMAND="$1"
606         shift
607         case "$SUBCOMMAND" in
608             'import-key'|'i')
609                 import_key "$@"
610                 ;;
611
612             'gen-key'|'g')
613                 gen_key "$@"
614                 ;;
615
616             'diagnostics'|'d')
617                 diagnostics
618                 ;;
619
620             *)
621                 failure "Unknown expert subcommand: '$COMMAND'
622 Type '$PGRM help' for usage."
623                 ;;
624         esac
625         ;;
626
627     'version'|'v')
628         echo "$VERSION"
629         ;;
630
631     '--help'|'help'|'-h'|'h'|'?')
632         usage
633         ;;
634
635     *)
636         failure "Unknown command: '$COMMAND'
637 Type '$PGRM help' for usage."
638         ;;
639 esac
640
641 exit "$RETURN"