Added file permission check function, and fixed bug in key writing for
[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 users 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
49   add-identity-certifier (a) KEYID      import and tsign a certification key
50     -n|--domain DOMAIN                    domain of certifier ()
51     -t|--trust TRUST                      trust level of certifier ('full')
52     -d|--depth DEPTH                      trust depth for certifier (1)
53   remove-identity-certifier (r) KEYID   remove a certification key
54   list-identity-certifiers (l)          list certification keys
55
56   help (h,?)                            this help
57
58 EOF
59 }
60
61 su_monkeysphere_user() {
62     su --preserve-environment "$MONKEYSPHERE_USER" -- -c "$@"
63 }
64
65 # function to interact with the host gnupg keyring
66 gpg_host() {
67     local returnCode
68
69     GNUPGHOME="$GNUPGHOME_HOST"
70     export GNUPGHOME
71
72     # NOTE: we supress this warning because we need the monkeysphere
73     # user to be able to read the host pubring.  we realize this might
74     # be problematic, but it's the simplest solution, without too much
75     # loss of security.
76     gpg --no-permission-warning "$@"
77     returnCode="$?"
78
79     # always reset the permissions on the host pubring so that the
80     # monkeysphere user can read the trust signatures
81     chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
82     chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
83     
84     return "$returnCode"
85 }
86
87 # function to interact with the authentication gnupg keyring
88 # FIXME: this function requires basically accepts only a single
89 # argument because of problems with quote expansion.  this needs to be
90 # fixed/improved.
91 gpg_authentication() {
92     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
93     export GNUPGHOME
94
95     su_monkeysphere_user "gpg $@"
96 }
97
98 # update authorized_keys for users
99 update_users() {
100     if [ "$1" ] ; then
101         # get users from command line
102         unames="$@"
103     else
104         # or just look at all users if none specified
105         unames=$(getent passwd | cut -d: -f1)
106     fi
107
108     # set mode
109     MODE="authorized_keys"
110
111     # set gnupg home
112     GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
113
114     # check to see if the gpg trust database has been initialized
115     if [ ! -s "${GNUPGHOME}/trustdb.gpg" ] ; then
116         failure "GNUPG trust database uninitialized.  Please see MONKEYSPHERE-SERVER(8)."
117     fi
118
119     # make sure the authorized_keys directory exists
120     mkdir -p "${VARLIB}/authorized_keys"
121
122     # loop over users
123     for uname in $unames ; do
124         # check all specified users exist
125         if ! getent passwd "$uname" >/dev/null ; then
126             log "----- unknown user '$uname' -----"
127             continue
128         fi
129
130         # set authorized_user_ids and raw authorized_keys variables,
131         # translating ssh-style path variables
132         authorizedUserIDs=$(translate_ssh_variables "$uname" "$AUTHORIZED_USER_IDS")
133         rawAuthorizedKeys=$(translate_ssh_variables "$uname" "$RAW_AUTHORIZED_KEYS")
134
135         # if neither is found, skip user
136         if [ ! -s "$authorizedUserIDs" ] ; then
137             if [ "$rawAuthorizedKeys" = '-' -o ! -s "$rawAuthorizedKeys" ] ; then
138                 continue
139             fi
140         fi
141
142         log "----- user: $uname -----"
143
144         if ! check_key_file_permissions "$uname" "$AUTHORIZED_USER_IDS" ; then
145             log "Improper permissions on authorized_user_ids file."
146             continue
147         fi
148
149         if ! check_key_file_permissions "$uname" "$RAW_AUTHORIZED_KEYS" ; then
150             log "Improper permissions on authorized_keys file."
151             continue
152         fi
153
154         # make temporary directory
155         TMPDIR=$(mktemp -d)
156
157         # trap to delete temporary directory on exit
158         trap "rm -rf $TMPDIR" EXIT
159
160         # create temporary authorized_user_ids file
161         TMP_AUTHORIZED_USER_IDS="${TMPDIR}/authorized_user_ids"
162         touch "$TMP_AUTHORIZED_USER_IDS"
163
164         # create temporary authorized_keys file
165         AUTHORIZED_KEYS="${TMPDIR}/authorized_keys"
166         touch "$AUTHORIZED_KEYS"
167
168         # set restrictive permissions on the temporary files
169         # FIXME: is there a better way to do this?
170         chmod 0700 "$TMPDIR"
171         chmod 0600 "$AUTHORIZED_KEYS"
172         chmod 0600 "$TMP_AUTHORIZED_USER_IDS"
173         chown -R "$MONKEYSPHERE_USER" "$TMPDIR"
174
175         # if the authorized_user_ids file exists...
176         if [ -s "$authorizedUserIDs" ] ; then
177             # copy user authorized_user_ids file to temporary
178             # location
179             cat "$authorizedUserIDs" > "$TMP_AUTHORIZED_USER_IDS"
180
181             # export needed variables
182             export AUTHORIZED_KEYS
183             export TMP_AUTHORIZED_USER_IDS
184
185             # process authorized_user_ids file, as monkeysphere
186             # user
187             su_monkeysphere_user \
188                 ". ${SHARE}/common; process_authorized_user_ids $TMP_AUTHORIZED_USER_IDS"
189             RETURN="$?"
190         fi
191
192         # add user-controlled authorized_keys file path if specified
193         if [ "$rawAuthorizedKeys" != '-' -a -s "$rawAuthorizedKeys" ] ; then
194             log -n "adding raw authorized_keys file... "
195             cat "$rawAuthorizedKeys" >> "$AUTHORIZED_KEYS"
196             loge "done."
197         fi
198
199         # openssh appears to check the contents of the
200         # authorized_keys file as the user in question, so the
201         # file must be readable by that user at least.
202         # FIXME: is there a better way to do this?
203         chown root "$AUTHORIZED_KEYS"
204         chgrp $(getent passwd "$uname" | cut -f4 -d:) "$AUTHORIZED_KEYS"
205         chmod g+r "$AUTHORIZED_KEYS"
206
207         # if the resulting authorized_keys file is not empty, move
208         # it into place
209         mv -f "$AUTHORIZED_KEYS" "${VARLIB}/authorized_keys/${uname}"
210
211         # destroy temporary directory
212         rm -rf "$TMPDIR"
213     done
214 }
215
216 # generate server gpg key
217 gen_key() {
218     local hostName
219     local userID
220     local keyParameters
221     local fingerprint
222
223     hostName=${1:-$(hostname --fqdn)}
224     userID="ssh://${hostName}"
225
226     # check for presense of key with user ID
227     if gpg_host --list-key ="$userID" > /dev/null 2>&1 ; then
228         failure "Key for '$userID' already exists"
229     fi
230
231     # set key variables
232     KEY_TYPE="RSA"
233     KEY_LENGTH=${KEY_LENGTH:="2048"}
234     KEY_USAGE="auth"
235     # prompt about key expiration if not specified
236     if [ -z "$KEY_EXPIRE" ] ; then
237         cat <<EOF
238 Please specify how long the key should be valid.
239          0 = key does not expire
240       <n>  = key expires in n days
241       <n>w = key expires in n weeks
242       <n>m = key expires in n months
243       <n>y = key expires in n years
244 EOF
245         while [ -z "$KEY_EXPIRE" ] ; do
246             read -p "Key is valid for? (0) " KEY_EXPIRE
247             if ! test_gpg_expire ${KEY_EXPIRE:=0} ; then
248                 echo "invalid value"
249                 unset KEY_EXPIRE
250             fi
251         done
252     elif ! test_gpg_expire "$KEY_EXPIRE" ; then
253         failure "invalid key expiration value '$KEY_EXPIRE'."
254     fi
255
256     # set key parameters
257     keyParameters=$(cat <<EOF
258 Key-Type: $KEY_TYPE
259 Key-Length: $KEY_LENGTH
260 Key-Usage: $KEY_USAGE
261 Name-Real: $userID
262 Expire-Date: $KEY_EXPIRE
263 EOF
264 )
265
266     # add the revoker field if specified
267     # FIXME: the "1:" below assumes that $REVOKER's key is an RSA key.
268     # FIXME: key is marked "sensitive"?  is this appropriate?
269     if [ "$REVOKER" ] ; then
270         keyParameters="${keyParameters}"$(cat <<EOF
271 Revoker: 1:$REVOKER sensitive
272 EOF
273 )
274     fi
275
276     echo "The following key parameters will be used for the host private key:"
277     echo "$keyParameters"
278
279     read -p "Generate key? (Y/n) " OK; OK=${OK:=Y}
280     if [ ${OK/y/Y} != 'Y' ] ; then
281         failure "aborting."
282     fi
283
284     # add commit command
285     keyParameters="${keyParameters}"$(cat <<EOF
286
287 %commit
288 %echo done
289 EOF
290 )
291
292     log "generating server key..."
293     echo "$keyParameters" | gpg_host --batch --gen-key
294
295     # output the server fingerprint
296     fingerprint_server_key "=${userID}"
297
298     # find the key fingerprint of the server primary key
299     fingerprint=$(gpg_host --list-key --with-colons --with-fingerprint "=${userID}" | \
300         grep '^fpr:' | head -1 | cut -d: -f10)
301
302     # translate the private key to ssh format, and export to a file
303     # for sshs usage.
304     # NOTE: assumes that the primary key is the proper key to use
305     (umask 077 && \
306         gpg_host --export-secret-key "$fingerprint" | \
307         openpgp2ssh "$fingerprint" > "${VARLIB}/ssh_host_rsa_key")
308     log "Private SSH host key output to file: ${VARLIB}/ssh_host_rsa_key"
309 }
310
311 # gpg output key fingerprint
312 fingerprint_server_key() {
313     gpg_host --fingerprint --list-secret-keys
314 }
315
316 # publish server key to keyserver
317 publish_server_key() {
318     read -p "really publish key to $KEYSERVER? (y/N) " OK; OK=${OK:=N}
319     if [ ${OK/y/Y} != 'Y' ] ; then
320         failure "aborting."
321     fi
322
323     # publish host key
324     # FIXME: need to figure out better way to identify host key
325     # dummy command so as not to publish fakes keys during testing
326     # eventually:
327     #gpg_authentication "--keyring $GNUPGHOME_HOST/pubring.gpg --keyserver $KEYSERVER --send-keys $(hostname -f)"
328     failure "NOT PUBLISHED (to avoid permanent publication errors during monkeysphere development)."
329 }
330
331 # retrieve key from web of trust, import it into the host keyring, and
332 # ltsign the key in the host keyring so that it may certify other keys
333 add_certifier() {
334     local keyID
335     local fingerprint
336     local ltsignCommand
337
338     keyID="$1"
339     export keyID
340
341     # export host ownertrust to authentication keyring
342     gpg_host --export-ownertrust | gpg_authentication "--import-ownertrust"
343
344     # get the key from the key server
345     gpg_authentication "--keyserver $KEYSERVER --recv-key '$keyID'" || failure
346
347     # get the full fingerprint of a key ID
348     fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint $keyID" | \
349         grep '^fpr:' | grep "$keyID" | cut -d: -f10)
350
351     echo "key found:"
352     gpg_authentication "--fingerprint $fingerprint"
353
354     read -p "Are you sure you want to add this key as a certifier of users on this system? (y/N) " OK; OK=${OK:-N}
355     if [ "${OK/y/Y}" != 'Y' ] ; then
356         failure "aborting."
357     fi
358
359     # export the key to the host keyring
360     gpg_authentication "--export $keyID" | gpg_host --import
361
362     # default values for trust depth and domain
363     DOMAIN=${DOMAIN:-}
364     TRUST=${TRUST:-2}
365     DEPTH=${DEPTH:-1}
366
367     # ltsign command
368     # NOTE: *all* user IDs will be ltsigned
369     ltsignCommand=$(cat <<EOF
370 ltsign
371 y
372 $TRUST
373 $DEPTH
374 $DOMAIN
375 y
376 save
377 EOF
378 )
379
380     # ltsign the key
381     echo "$ltsignCommand" | gpg_host --quiet --command-fd 0 --edit-key "$fingerprint"
382
383     # update the trustdb for the authentication keyring
384     gpg_authentication "--check-trustdb"
385 }
386
387 # delete a certifiers key from the host keyring
388 remove_certifier() {
389     local keyID
390     local fingerprint
391
392     keyID="$1"
393
394     # delete the requested key (with prompting)
395     gpg_host --delete-key "$keyID"
396
397     # update the trustdb for the authentication keyring
398     gpg_authentication "--check-trustdb"
399 }
400
401 # list the host certifiers
402 list_certifiers() {
403     gpg_host --list-keys
404 }
405
406 ########################################################################
407 # MAIN
408 ########################################################################
409
410 # unset variables that should be defined only in config file
411 unset KEYSERVER
412 unset AUTHORIZED_USER_IDS
413 unset RAW_AUTHORIZED_KEYS
414 unset MONKEYSPHERE_USER
415
416 # load configuration file
417 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${ETC}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
418
419 # set empty config variable with ones from the environment, or with
420 # defaults
421 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="subkeys.pgp.net"}}
422 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.config/monkeysphere/authorized_user_ids"}}
423 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
424 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
425
426 # other variables
427 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
428 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
429 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${VARLIB}/gnupg-host"}
430 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${VARLIB}/gnupg-authentication"}
431
432 # export variables needed in su invocation
433 export DATE
434 export MODE
435 export MONKEYSPHERE_USER
436 export KEYSERVER
437 export CHECK_KEYSERVER
438 export REQUIRED_USER_KEY_CAPABILITY
439 export GNUPGHOME_HOST
440 export GNUPGHOME_AUTHENTICATION
441 export GNUPGHOME
442
443 # get subcommand
444 COMMAND="$1"
445 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
446 shift
447
448 # unset option variables
449 unset KEY_LENGTH
450 unset KEY_EXPIRE
451 unset REVOKER
452 unset DOMAIN
453 unset TRUST
454 unset DEPTH
455
456 # get options for key generation and add-certifier functions
457 TEMP=$(getopt -o l:e:r:n:t:d: -l length:,expire:,revoker:,domain:,trust:,depth: -n "$PGRM" -- "$@")
458
459 if [ $? != 0 ] ; then
460     usage
461     exit 1
462 fi
463
464 # Note the quotes around `$TEMP': they are essential!
465 eval set -- "$TEMP"
466
467 while true ; do
468     case "$1" in
469         -l|--length)
470             KEY_LENGTH="$2"
471             shift 2
472             ;;
473         -e|--expire)
474             KEY_EXPIRE="$2"
475             shift 2
476             ;;
477         -r|--revoker)
478             REVOKER="$2"
479             shift 2
480             ;;
481         -n|--domain)
482             DOMAIN="$2"
483             shift 2
484             ;;
485         -t|--trust)
486             TRUST="$2"
487             shift 2
488             ;;
489         -d|--depth)
490             DEPTH="$2"
491             shift 2
492             ;;
493         --)
494             shift
495             ;;
496         *)
497             break
498             ;;
499     esac
500 done
501
502 case $COMMAND in
503     'update-users'|'update-user'|'u')
504         update_users "$@"
505         ;;
506
507     'gen-key'|'g')
508         gen_key "$@"
509         ;;
510
511     'show-fingerprint'|'f')
512         fingerprint_server_key
513         ;;
514
515     'publish-key'|'p')
516         publish_server_key
517         ;;
518
519     'add-identity-certifier'|'add-certifier'|'a')
520         if [ -z "$1" ] ; then
521             failure "You must specify a key ID."
522         fi
523         add_certifier "$1"
524         ;;
525
526     'remove-identity-certifier'|'remove-certifier'|'r')
527         if [ -z "$1" ] ; then
528             failure "You must specify a key ID."
529         fi
530         remove_certifier "$1"
531         ;;
532
533     'list-identity-certifiers'|'list-certifiers'|'list-certifier'|'l')
534         list_certifiers "$@"
535         ;;
536
537     'help'|'h'|'?')
538         usage
539         ;;
540
541     *)
542         failure "Unknown command: '$COMMAND'
543 Type '$PGRM help' for usage."
544         ;;
545 esac
546
547 exit "$RETURN"