9205b1d315bce9b59cdfecb85df952012425d38a
[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         # 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             RETURN="$?"
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'" || failure
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     echo "key found:"
342     gpg_authentication "--fingerprint $fingerprint"
343
344     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}
345     if [ "${OK/y/Y}" != 'Y' ] ; then
346         failure "aborting."
347     fi
348
349     # export the key to the host keyring
350     gpg_authentication "--export $keyID" | gpg_host --import
351
352     # default values for trust depth and domain
353     DOMAIN=${DOMAIN:-}
354     TRUST=${TRUST:-2}
355     DEPTH=${DEPTH:-1}
356
357     # ltsign command
358     # NOTE: *all* user IDs will be ltsigned
359     ltsignCommand=$(cat <<EOF
360 ltsign
361 y
362 $TRUST
363 $DEPTH
364 $DOMAIN
365 y
366 save
367 EOF
368 )
369
370     # ltsign the key
371     echo "$ltsignCommand" | gpg_host --quiet --command-fd 0 --edit-key "$fingerprint"
372
373     # update the trustdb for the authentication keyring
374     gpg_authentication "--check-trustdb"
375 }
376
377 # delete a certifiers key from the host keyring
378 remove_certifier() {
379     local keyID
380     local fingerprint
381
382     keyID="$1"
383
384     # delete the requested key (with prompting)
385     gpg_host --delete-key "$keyID"
386
387     # update the trustdb for the authentication keyring
388     gpg_authentication "--check-trustdb"
389 }
390
391 # list the host certifiers
392 list_certifiers() {
393     gpg_host --list-keys
394 }
395
396 ########################################################################
397 # MAIN
398 ########################################################################
399
400 # unset variables that should be defined only in config file
401 unset KEYSERVER
402 unset AUTHORIZED_USER_IDS
403 unset RAW_AUTHORIZED_KEYS
404 unset MONKEYSPHERE_USER
405
406 # load configuration file
407 [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${ETC}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
408
409 # set empty config variable with ones from the environment, or with
410 # defaults
411 KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="subkeys.pgp.net"}}
412 AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.config/monkeysphere/authorized_user_ids"}}
413 RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
414 MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
415
416 # other variables
417 CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
418 REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
419 GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${VARLIB}/gnupg-host"}
420 GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${VARLIB}/gnupg-authentication"}
421
422 # export variables needed in su invocation
423 export DATE
424 export MODE
425 export MONKEYSPHERE_USER
426 export KEYSERVER
427 export CHECK_KEYSERVER
428 export REQUIRED_USER_KEY_CAPABILITY
429 export GNUPGHOME_HOST
430 export GNUPGHOME_AUTHENTICATION
431 export GNUPGHOME
432
433 # get subcommand
434 COMMAND="$1"
435 [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
436 shift
437
438 # unset option variables
439 unset KEY_LENGTH
440 unset KEY_EXPIRE
441 unset REVOKER
442 unset DOMAIN
443 unset TRUST
444 unset DEPTH
445
446 # get options for key generation and add-certifier functions
447 TEMP=$(getopt -o l:e:r:n:t:d: -l length:,expire:,revoker:,domain:,trust:,depth: -n "$PGRM" -- "$@")
448
449 if [ $? != 0 ] ; then
450     usage
451     exit 1
452 fi
453
454 # Note the quotes around `$TEMP': they are essential!
455 eval set -- "$TEMP"
456
457 while true ; do
458     case "$1" in
459         -l|--length)
460             KEY_LENGTH="$2"
461             shift 2
462             ;;
463         -e|--expire)
464             KEY_EXPIRE="$2"
465             shift 2
466             ;;
467         -r|--revoker)
468             REVOKER="$2"
469             shift 2
470             ;;
471         -n|--domain)
472             DOMAIN="$2"
473             shift 2
474             ;;
475         -t|--trust)
476             TRUST="$2"
477             shift 2
478             ;;
479         -d|--depth)
480             DEPTH="$2"
481             shift 2
482             ;;
483         --)
484             shift
485             ;;
486         *)
487             break
488             ;;
489     esac
490 done
491
492 case $COMMAND in
493     'update-users'|'update-user'|'u')
494         update_users "$@"
495         ;;
496
497     'gen-key'|'g')
498         gen_key "$@"
499         ;;
500
501     'show-fingerprint'|'f')
502         fingerprint_server_key
503         ;;
504
505     'publish-key'|'p')
506         publish_server_key
507         ;;
508
509     'add-identity-certifier'|'add-certifier'|'a')
510         if [ -z "$1" ] ; then
511             failure "You must specify a key ID."
512         fi
513         add_certifier "$1"
514         ;;
515
516     'remove-identity-certifier'|'remove-certifier'|'r')
517         if [ -z "$1" ] ; then
518             failure "You must specify a key ID."
519         fi
520         remove_certifier "$1"
521         ;;
522
523     'list-identity-certifiers'|'list-certifiers'|'list-certifier'|'l')
524         list_certifiers "$@"
525         ;;
526
527     'help'|'h'|'?')
528         usage
529         ;;
530
531     *)
532         failure "Unknown command: '$COMMAND'
533 Type '$PGRM help' for usage."
534         ;;
535 esac
536
537 exit "$RETURN"