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