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