98b60c035b89f4bf98e7a2aca1b9cc56fcea2fc3
[monkeysphere.git] / src / monkeysphere-server
1 #!/bin/bash
2
3 # monkeysphere-server: MonkeySphere server admin tool
4 #
5 # The monkeysphere scripts are written by:
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 #
8 # They are Copyright 2008, and are all released under the GPL, version 3
9 # or later.
10
11 ########################################################################
12 PGRM=$(basename $0)
13
14 SHARE=${MONKEYSPHERE_SHARE:="/usr/share/monkeysphere"}
15 export SHARE
16 . "${SHARE}/common" || exit 1
17
18 VARLIB="/var/lib/monkeysphere"
19 export VARLIB
20
21 # date in UTF format if needed
22 DATE=$(date -u '+%FT%T')
23
24 # unset some environment variables that could screw things up
25 unset GREP_OPTIONS
26
27 # default return code
28 RETURN=0
29
30 ########################################################################
31 # FUNCTIONS
32 ########################################################################
33
34 usage() {
35 cat <<EOF
36 usage: $PGRM <subcommand> [options] [args]
37 MonkeySphere server admin tool.
38
39 subcommands:
40   update-users (u) [USER]...            update user authorized_keys files
41
42   gen-key (g) [HOSTNAME]                generate gpg key for the server
43     -l|--length BITS                      key length in bits (2048)
44     -e|--expire EXPIRE                    date to expire
45     -r|--revoker FINGERPRINT              add a revoker
46   show-fingerprint (f)                  show server's host key fingerprint
47   publish-key (p)                       publish server's host key to keyserver
48   diagnostics (d)                       report on the server's monkeysphere status
49
50   add-identity-certifier (a) KEYID      import and tsign a certification key
51     -n|--domain DOMAIN                    limit ID certifications to IDs in DOMAIN ()
52     -t|--trust TRUST                      trust level of certifier (full)
53     -d|--depth DEPTH                      trust depth for certifier (1)
54   remove-identity-certifier (r) KEYID   remove a certification key
55   list-identity-certifiers (l)          list certification keys
56
57   gpg-authentication-cmd CMD            gnupg-authentication command
58
59   help (h,?)                            this help
60
61 EOF
62 }
63
64 su_monkeysphere_user() {
65     su --preserve-environment "$MONKEYSPHERE_USER" -- -c "$@"
66 }
67
68 # function to interact with the host gnupg keyring
69 gpg_host() {
70     local returnCode
71
72     GNUPGHOME="$GNUPGHOME_HOST"
73     export GNUPGHOME
74
75     # NOTE: we supress this warning because we need the monkeysphere
76     # user to be able to read the host pubring.  we realize this might
77     # be problematic, but it's the simplest solution, without too much
78     # loss of security.
79     gpg --no-permission-warning "$@"
80     returnCode="$?"
81
82     # always reset the permissions on the host pubring so that the
83     # monkeysphere user can read the trust signatures
84     chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
85     chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
86     
87     return "$returnCode"
88 }
89
90 # function to interact with the authentication gnupg keyring
91 # FIXME: this function requires basically accepts only a single
92 # argument because of problems with quote expansion.  this needs to be
93 # fixed/improved.
94 gpg_authentication() {
95     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
96     export GNUPGHOME
97
98     su_monkeysphere_user "gpg $@"
99 }
100
101 # update authorized_keys for users
102 update_users() {
103     if [ "$1" ] ; then
104         # get users from command line
105         unames="$@"
106     else
107         # or just look at all users if none specified
108         unames=$(getent passwd | cut -d: -f1)
109     fi
110
111     # set mode
112     MODE="authorized_keys"
113
114     # set gnupg home
115     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
116
117     # check to see if the gpg trust database has been initialized
118     if [ ! -s "${GNUPGHOME}/trustdb.gpg" ] ; then
119         failure "GNUPG trust database uninitialized.  Please see MONKEYSPHERE-SERVER(8)."
120     fi
121
122     # make sure the authorized_keys directory exists
123     mkdir -p "${VARLIB}/authorized_keys"
124
125     # loop over users
126     for uname in $unames ; do
127         # check all specified users exist
128         if ! getent passwd "$uname" >/dev/null ; then
129             log "----- unknown user '$uname' -----"
130             continue
131         fi
132
133         # set authorized_user_ids and raw authorized_keys variables,
134         # translating ssh-style path variables
135         authorizedUserIDs=$(translate_ssh_variables "$uname" "$AUTHORIZED_USER_IDS")
136         rawAuthorizedKeys=$(translate_ssh_variables "$uname" "$RAW_AUTHORIZED_KEYS")
137
138         # if neither is found, skip user
139         if [ ! -s "$authorizedUserIDs" ] ; then
140             if [ "$rawAuthorizedKeys" = '-' -o ! -s "$rawAuthorizedKeys" ] ; then
141                 continue
142             fi
143         fi
144
145         log "----- user: $uname -----"
146
147         # exit if the authorized_user_ids file is empty
148         if ! check_key_file_permissions "$uname" "$AUTHORIZED_USER_IDS" ; then
149             log "Improper permissions on authorized_user_ids file path."
150             continue
151         fi
152
153         # check permissions on the authorized_keys file path
154         if ! check_key_file_permissions "$uname" "$RAW_AUTHORIZED_KEYS" ; then
155             log "Improper permissions on authorized_keys file path path."
156             continue
157         fi
158
159         # make temporary directory
160         TMPDIR=$(mktemp -d)
161
162         # trap to delete temporary directory on exit
163         trap "rm -rf $TMPDIR" EXIT
164
165         # create temporary authorized_user_ids file
166         TMP_AUTHORIZED_USER_IDS="${TMPDIR}/authorized_user_ids"
167         touch "$TMP_AUTHORIZED_USER_IDS"
168
169         # create temporary authorized_keys file
170         AUTHORIZED_KEYS="${TMPDIR}/authorized_keys"
171         touch "$AUTHORIZED_KEYS"
172
173         # set restrictive permissions on the temporary files
174         # FIXME: is there a better way to do this?
175         chmod 0700 "$TMPDIR"
176         chmod 0600 "$AUTHORIZED_KEYS"
177         chmod 0600 "$TMP_AUTHORIZED_USER_IDS"
178         chown -R "$MONKEYSPHERE_USER" "$TMPDIR"
179
180         # if the authorized_user_ids file exists...
181         if [ -s "$authorizedUserIDs" ] ; then
182             # copy user authorized_user_ids file to temporary
183             # location
184             cat "$authorizedUserIDs" > "$TMP_AUTHORIZED_USER_IDS"
185
186             # export needed variables
187             export AUTHORIZED_KEYS
188             export TMP_AUTHORIZED_USER_IDS
189
190             # process authorized_user_ids file, as monkeysphere
191             # user
192             su_monkeysphere_user \
193                 ". ${SHARE}/common; process_authorized_user_ids $TMP_AUTHORIZED_USER_IDS"
194             RETURN="$?"
195         fi
196
197         # add user-controlled authorized_keys file path if specified
198         if [ "$rawAuthorizedKeys" != '-' -a -s "$rawAuthorizedKeys" ] ; then
199             log -n "adding raw authorized_keys file... "
200             cat "$rawAuthorizedKeys" >> "$AUTHORIZED_KEYS"
201             loge "done."
202         fi
203
204         # openssh appears to check the contents of the
205         # authorized_keys file as the user in question, so the
206         # file must be readable by that user at least.
207         # FIXME: is there a better way to do this?
208         chown root "$AUTHORIZED_KEYS"
209         chgrp $(getent passwd "$uname" | cut -f4 -d:) "$AUTHORIZED_KEYS"
210         chmod g+r "$AUTHORIZED_KEYS"
211
212         # move the resulting authorized_keys file into place
213         mv -f "$AUTHORIZED_KEYS" "${VARLIB}/authorized_keys/${uname}"
214
215         # destroy temporary directory
216         rm -rf "$TMPDIR"
217     done
218 }
219
220 # generate server gpg key
221 gen_key() {
222     local keyType
223     local keyLength
224     local keyUsage
225     local keyExpire
226     local revoker
227     local hostName
228     local userID
229     local keyParameters
230     local fingerprint
231
232     # set default key parameter values
233     keyType="RSA"
234     keyLength="2048"
235     keyUsage="auth"
236     keyExpire=
237     revoker=
238
239     # get options
240     TEMP=$(getopt -o l:e:r: -l length:,expire:,revoker: -n "$PGRM" -- "$@")
241
242     if [ $? != 0 ] ; then
243         exit 1
244     fi
245
246     # Note the quotes around `$TEMP': they are essential!
247     eval set -- "$TEMP"
248
249     while true ; do
250         case "$1" in
251             -l|--length)
252                 keyLength="$2"
253                 shift 2
254                 ;;
255             -e|--expire)
256                 keyExpire="$2"
257                 shift 2
258                 ;;
259             -r|--revoker)
260                 revoker="$2"
261                 shift 2
262                 ;;
263             --)
264                 shift
265                 ;;
266             *)
267                 break
268                 ;;
269         esac
270     done
271
272     hostName=${1:-$(hostname --fqdn)}
273     userID="ssh://${hostName}"
274
275     # check for presense of key with user ID
276     if gpg_host --list-key ="$userID" > /dev/null 2>&1 ; then
277         failure "Key for '$userID' already exists"
278     fi
279
280     # prompt about key expiration if not specified
281     if [ -z "$keyExpire" ] ; then
282         cat <<EOF
283 Please specify how long the key should be valid.
284          0 = key does not expire
285       <n>  = key expires in n days
286       <n>w = key expires in n weeks
287       <n>m = key expires in n months
288       <n>y = key expires in n years
289 EOF
290         while [ -z "$keyExpire" ] ; do
291             read -p "Key is valid for? (0) " keyExpire
292             if ! test_gpg_expire ${keyExpire:=0} ; then
293                 echo "invalid value"
294                 unset keyExpire
295             fi
296         done
297     elif ! test_gpg_expire "$keyExpire" ; then
298         failure "invalid key expiration value '$keyExpire'."
299     fi
300
301     # set key parameters
302     keyParameters=$(cat <<EOF
303 Key-Type: $keyType
304 Key-Length: $keyLength
305 Key-Usage: $keyUsage
306 Name-Real: $userID
307 Expire-Date: $keyExpire
308 EOF
309 )
310
311     # add the revoker field if specified
312     # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key.
313     # FIXME: key is marked "sensitive"?  is this appropriate?
314     if [ "$revoker" ] ; then
315         keyParameters="${keyParameters}"$(cat <<EOF
316 Revoker: 1:$revoker sensitive
317 EOF
318 )
319     fi
320
321     echo "The following key parameters will be used for the host private key:"
322     echo "$keyParameters"
323
324     read -p "Generate key? (Y/n) " OK; OK=${OK:=Y}
325     if [ ${OK/y/Y} != 'Y' ] ; then
326         failure "aborting."
327     fi
328
329     # add commit command
330     keyParameters="${keyParameters}"$(cat <<EOF
331
332 %commit
333 %echo done
334 EOF
335 )
336
337     log "generating server key..."
338     echo "$keyParameters" | gpg_host --batch --gen-key
339
340     # output the server fingerprint
341     fingerprint_server_key "=${userID}"
342
343     # find the key fingerprint of the server primary key
344     fingerprint=$(gpg_host --list-key --with-colons --with-fingerprint "=${userID}" | \
345         grep '^fpr:' | head -1 | cut -d: -f10)
346
347     # export host ownertrust to authentication keyring
348     log "setting ultimate owner trust for server key..."
349     echo "${fingerprint}:6:" | gpg_authentication "--import-ownertrust"
350
351     # translate the private key to ssh format, and export to a file
352     # for sshs usage.
353     # NOTE: assumes that the primary key is the proper key to use
354     (umask 077 && \
355         gpg_host --export-secret-key "$fingerprint" | \
356         openpgp2ssh "$fingerprint" > "${VARLIB}/ssh_host_rsa_key")
357     log "Private SSH host key output to file: ${VARLIB}/ssh_host_rsa_key"
358 }
359
360 # gpg output key fingerprint
361 fingerprint_server_key() {
362     gpg_host --fingerprint --list-secret-keys
363 }
364
365 # publish server key to keyserver
366 publish_server_key() {
367     read -p "Really publish key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
368     if [ ${OK/y/Y} != 'Y' ] ; then
369         failure "aborting."
370     fi
371
372     # publish host key
373     # FIXME: need to figure out better way to identify host key
374     # dummy command so as not to publish fakes keys during testing
375     # eventually:
376     #gpg_authentication "--keyserver $KEYSERVER --send-keys $(hostname -f)"
377     echo "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development)."
378     echo "The following command should publish the key:"
379     echo "monkeysphere-server gpg-authentication-cmd '--keyserver $KEYSERVER --send-keys $(hostname -f)'"
380     exit 255
381 }
382
383 diagnostics() {
384 #  * check on the status and validity of the key and public certificates
385     local seckey
386     local keysfound
387     local keyexp
388     local curdate
389     local warnwindow
390     local warndate
391
392     seckey=$(gpg_host --list-secret-keys --with-colons --fixed-list-mode)
393     keysfound=$(echo "$seckey" | grep -c ^sec:)
394     curdate=$(date +%s)
395     # warn when anything is 2 months away from expiration
396     warnwindow='2 months'
397     warndate=$(date +%s -d "$warnwindow")
398
399     if (( "$keysfound" < 1 )); then
400         echo "No host key found!"
401         echo "Recommendation: run 'monkeysphere-server gen-key'"
402     else
403         if (( "$keysfound" > 1 )); then
404             echo "more than one host key found?"
405         else
406         # check for key expiration:
407             keyexp=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
408             if (( "$keyexp"  < "$curdate" )); then
409                 echo "Host key is expired!"
410                 # FIXME: recommend a way to resolve this other than re-keying?
411             elif (( "$keyexp" < "$warndate" )); then
412                 echo "Host key expires in less than $warnwindow"
413                 # FIXME: recommend a way to resolve this?
414             fi
415         # and weirdnesses:
416             if (( $(echo "$seckey" | grep ^sec: | cut -f6 -d:) > "$curdate" )); then
417                 echo "Host key was created in the future(?!). Is your clock correct?"
418                 echo "Recommendation: Check clock ($(date +%F_%T)); use NTP?"
419             fi
420
421         # check for UserID expiration:
422             echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
423             while IFS=: read create expire uid ; do
424                 # FIXME: should we be doing any checking on the form
425                 # of the User ID?  Should we be unmangling it somehow?
426                 if [ "$create" ] && (( "$create" > "$curdate" )); then
427                     echo "User ID '$uid' was created in the future(?!).  Is your clock correct?"
428                     echo "Recommendation: Check clock ($(date +%F_%T)); use NTP?"
429                 fi
430                 if [ "$expire" ] ; then
431                     if (( "$expire" < "$curdate" )); then
432                         echo "User ID '$uid' is expired!"
433                         # FIXME: recommend a way to resolve this
434                     elif (( "$expire" < "$warndate" )); then
435                         echo "User ID '$uid' expires in less than $warnwindow"
436                         # FIXME: recommend a way to resolve this
437                     fi
438                 fi
439             done
440             
441 # FIXME: verify that the host key is properly published to the
442 #   keyservers
443
444 # FIXME: check that there are valid, non-expired certifying signatures
445 #   attached to the host key
446
447 # FIXME: propose adding a revoker to the host key if none exist (do we
448 #   have a way to do that after key generation?)
449
450 # Ensure that the ssh_host_rsa_key file is present and non-empty:
451             if [ ! -s "${VARLIB}/ssh_host_rsa_key" ] ; then
452                 echo "The host key as prepared for SSH (${VARLIB}/ssh_host_rsa_key) is missing or empty!"
453             else
454                 if [ $(stat -c "${VARLIB}/ssh_host_rsa_key") != 600 ] ; then
455                     echo "Permissions seem wrong for ${VARLIB}/ssh_host_rsa_key -- should be 0600 !"
456                 fi
457
458                 # propose changes needed for sshd_config (if any)
459                 if ! grep -q "^HostKey ${VARLIB}/ssh_host_rsa_key$" /etc/ssh/sshd_config; then
460                     echo "/etc/ssh/sshd_config does not point to the monkeysphere host key (${VARLIB}/ssh_host_rsa_key)."
461                     echo "Recommendation: add a line to /etc/ssh/sshd_config: 'HostKey ${VARLIB}/ssh_host_rsa_key'"
462                 fi
463             fi
464         fi
465     fi
466
467 # FIXME: look at the ownership/privileges of the various keyrings,
468 #    directories housing them, etc (what should those values be?  can
469 #    we make them as minimal as possible?)
470
471 # FIXME: look to see that the ownertrust rules are set properly on the
472 #    authentication keyring
473
474 # FIXME:  make sure that at least one identity certifier exists
475
476 }
477
478 # retrieve key from web of trust, import it into the host keyring, and
479 # ltsign the key in the host keyring so that it may certify other keys
480 add_certifier() {
481     local domain
482     local trust
483     local depth
484     local keyID
485     local fingerprint
486     local ltsignCommand
487     local trustval
488
489     # set default values for trust depth and domain
490     domain=
491     trust=full
492     depth=1
493
494     # get options
495     TEMP=$(getopt -o n:t:d: -l domain:,trust:,depth: -n "$PGRM" -- "$@")
496
497     if [ $? != 0 ] ; then
498         exit 1
499     fi
500
501     # Note the quotes around `$TEMP': they are essential!
502     eval set -- "$TEMP"
503
504     while true ; do
505         case "$1" in
506             -n|--domain)
507                 domain="$2"
508                 shift 2
509                 ;;
510             -t|--trust)
511                 trust="$2"
512                 shift 2
513                 ;;
514             -d|--depth)
515                 depth="$2"
516                 shift 2
517                 ;;
518             --)
519                 shift
520                 ;;
521             *)
522                 break
523                 ;;
524         esac
525     done
526
527     keyID="$1"
528     if [ -z "$keyID" ] ; then
529         failure "You must specify the key ID of a key to add."
530     fi
531     export keyID
532
533     # get the key from the key server
534     gpg_authentication "--keyserver $KEYSERVER --recv-key '$keyID'"
535
536     # get the full fingerprint of a key ID
537     fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint $keyID" | \
538         grep '^fpr:' | grep "$keyID" | cut -d: -f10)
539
540     echo "key found:"
541     gpg_authentication "--fingerprint $fingerprint"
542
543     echo "Are you sure you want to add this key as a certifier of"
544     read -p "users on this system? (y/N) " OK; OK=${OK:-N}
545     if [ "${OK/y/Y}" != 'Y' ] ; then
546         failure "aborting."
547     fi
548
549     # export the key to the host keyring
550     gpg_authentication "--export $keyID" | gpg_host --import
551
552     if [ "$trust" == marginal ]; then
553         trustval=1
554     elif [ "$trust" == full ]; then
555         trustval=2
556     else
557         failure "trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)"
558     fi
559
560     # ltsign command
561     # NOTE: *all* user IDs will be ltsigned
562     ltsignCommand=$(cat <<EOF
563 ltsign
564 y
565 $trustval
566 $depth
567 $domain
568 y
569 save
570 EOF
571         )
572
573     # ltsign the key
574     echo "$ltsignCommand" | gpg_host --quiet --command-fd 0 --edit-key "$fingerprint"
575
576     # update the trustdb for the authentication keyring
577     gpg_authentication "--check-trustdb"
578 }
579
580 # delete a certifiers key from the host keyring
581 remove_certifier() {
582     local keyID
583     local fingerprint
584
585     keyID="$1"
586     if [ -z "$keyID" ] ; then
587         failure "You must specify the key ID of a key to remove."
588     fi
589
590     # delete the requested key (with prompting)
591     gpg_host --delete-key "$keyID"
592
593     # update the trustdb for the authentication keyring
594     gpg_authentication "--check-trustdb"
595 }
596
597 # list the host certifiers
598 list_certifiers() {
599     gpg_host --list-keys
600 }
601
602 # issue command to gpg-authentication keyring
603 gpg_authentication_cmd() {
604     gpg_authentication "$@"
605 }
606
607 ########################################################################
608 # MAIN
609 ########################################################################
610
611 # unset variables that should be defined only in config file
612 unset KEYSERVER
613 unset AUTHORIZED_USER_IDS
614 unset RAW_AUTHORIZED_KEYS
615 unset MONKEYSPHERE_USER
616
617 # load configuration file
618 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${ETC}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
619
620 # set empty config variable with ones from the environment, or with
621 # defaults
622 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="subkeys.pgp.net"}}
623 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.config/monkeysphere/authorized_user_ids"}}
624 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
625 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
626
627 # other variables
628 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
629 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
630 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${VARLIB}/gnupg-host"}
631 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${VARLIB}/gnupg-authentication"}
632
633 # export variables needed in su invocation
634 export DATE
635 export MODE
636 export MONKEYSPHERE_USER
637 export KEYSERVER
638 export CHECK_KEYSERVER
639 export REQUIRED_USER_KEY_CAPABILITY
640 export GNUPGHOME_HOST
641 export GNUPGHOME_AUTHENTICATION
642 export GNUPGHOME
643
644 # get subcommand
645 COMMAND="$1"
646 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
647 shift
648
649 case $COMMAND in
650     'update-users'|'update-user'|'u')
651         update_users "$@"
652         ;;
653
654     'gen-key'|'g')
655         gen_key "$@"
656         ;;
657
658     'show-fingerprint'|'f')
659         fingerprint_server_key
660         ;;
661
662     'publish-key'|'p')
663         publish_server_key
664         ;;
665
666     'diagnostics'|'d')
667         diagnostics
668         ;;
669
670     'add-identity-certifier'|'add-certifier'|'a')
671         add_certifier "$1"
672         ;;
673
674     'remove-identity-certifier'|'remove-certifier'|'r')
675         remove_certifier "$1"
676         ;;
677
678     'list-identity-certifiers'|'list-certifiers'|'list-certifier'|'l')
679         list_certifiers "$@"
680         ;;
681
682     'gpg-authentication-cmd')
683         gpg_authentication_cmd "$@"
684         ;;
685
686     'help'|'h'|'?')
687         usage
688         ;;
689
690     *)
691         failure "Unknown command: '$COMMAND'
692 Type '$PGRM help' for usage."
693         ;;
694 esac
695
696 exit "$RETURN"