added MONKEYSPHERE_STRICT_MODES environment option to disable permissions checking.
[monkeysphere.git] / src / share / common
1 # -*-shell-script-*-
2 # This should be sourced by bash (though we welcome changes to make it POSIX sh compliant)
3
4 # Shared sh functions for the monkeysphere
5 #
6 # Written by
7 # Jameson Rollins <jrollins@finestructure.net>
8 # Jamie McClelland <jm@mayfirst.org>
9 # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
10 #
11 # Copyright 2008-2009, released under the GPL, version 3 or later
12
13 # all-caps variables are meant to be user supplied (ie. from config
14 # file) and are considered global
15
16 ########################################################################
17 ### UTILITY FUNCTIONS
18
19 # output version info
20 version() {
21     cat "${SYSSHAREDIR}/VERSION"
22 }
23
24 # failure function.  exits with code 255, unless specified otherwise.
25 failure() {
26     [ "$1" ] && echo "$1" >&2
27     exit ${2:-'255'}
28 }
29
30 # write output to stderr based on specified LOG_LEVEL the first
31 # parameter is the priority of the output, and everything else is what
32 # is echoed to stderr.  If there is nothing else, then output comes
33 # from stdin, and is not prefaced by log prefix.
34 log() {
35     local priority
36     local level
37     local output
38     local alllevels
39     local found=
40
41     # don't include SILENT in alllevels: it's handled separately
42     # list in decreasing verbosity (all caps).
43     # separate with $IFS explicitly, since we do some fancy footwork
44     # elsewhere.
45     alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
46
47     # translate lowers to uppers in global log level
48     LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
49
50     # just go ahead and return if the log level is silent
51     if [ "$LOG_LEVEL" = 'SILENT' ] ; then
52         return
53     fi
54
55     for level in $alllevels ; do 
56         if [ "$LOG_LEVEL" = "$level" ] ; then
57             found=true
58         fi
59     done
60     if [ -z "$found" ] ; then
61         # default to INFO:
62         LOG_LEVEL=INFO
63     fi
64
65     # get priority from first parameter, translating all lower to
66     # uppers
67     priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
68     shift
69
70     # scan over available levels
71     for level in $alllevels ; do
72         # output if the log level matches, set output to true
73         # this will output for all subsequent loops as well.
74         if [ "$LOG_LEVEL" = "$level" ] ; then
75             output=true
76         fi
77         if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
78             if [ "$1" ] ; then
79                 echo "$@"
80             else
81                 cat
82             fi | sed 's/^/'"${LOG_PREFIX}"'/' >&2
83         fi
84     done
85 }
86
87 # run command as monkeysphere user
88 su_monkeysphere_user() {
89     # our main goal here is to run the given command as the the
90     # monkeysphere user, but without prompting for any sort of
91     # authentication.  If this is not possible, we should just fail.
92
93     # FIXME: our current implementation is overly restrictive, because
94     # there may be some su PAM configurations that would allow su
95     # "$MONKEYSPHERE_USER" -c "$@" to Just Work without prompting,
96     # allowing specific users to invoke commands which make use of
97     # this user.
98
99     # chpst (from runit) would be nice to use, but we don't want to
100     # introduce an extra dependency just for this.  This may be a
101     # candidate for re-factoring if we switch implementation languages.
102
103     case $(id -un) in
104         # if monkeysphere user, run the command under bash
105         "$MONKEYSPHERE_USER")
106             bash -c "$@"
107             ;;
108
109          # if root, su command as monkeysphere user
110         'root')
111             su "$MONKEYSPHERE_USER" -c "$@"
112             ;;
113
114         # otherwise, fail
115         *)
116             log error "non-privileged user."
117             ;;
118     esac
119 }
120
121 # cut out all comments(#) and blank lines from standard input
122 meat() {
123     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
124 }
125
126 # cut a specified line from standard input
127 cutline() {
128     head --line="$1" "$2" | tail -1
129 }
130
131 # make a temporary directory
132 msmktempdir() {
133     mktemp -d ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
134 }
135
136 # make a temporary file
137 msmktempfile() {
138     mktemp ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
139 }
140
141 # this is a wrapper for doing lock functions.
142 #
143 # it lets us depend on either lockfile-progs (preferred) or procmail's
144 # lockfile, and should
145 lock() {
146     local use_lockfileprogs=true
147     local action="$1"
148     local file="$2"
149
150     if ! ( type lockfile-create &>/dev/null ) ; then
151         if ! ( type lockfile &>/dev/null ); then
152             failure "Neither lockfile-create nor lockfile are in the path!"
153         fi
154         use_lockfileprogs=
155     fi
156     
157     case "$action" in
158         create)
159             if [ -n "$use_lockfileprogs" ] ; then
160                 lockfile-create "$file" || failure "unable to lock '$file'"
161             else
162                 lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
163             fi
164             log debug "lock created on '$file'."
165             ;;
166         touch)  
167             if [ -n "$use_lockfileprogs" ] ; then
168                 lockfile-touch --oneshot "$file"
169             else
170                 : Nothing to do here
171             fi
172             log debug "lock touched on '$file'."
173             ;;
174         remove)
175             if [ -n "$use_lockfileprogs" ] ; then
176                 lockfile-remove "$file"
177             else
178                 rm -f "${file}.lock"
179             fi
180             log debug "lock removed on '$file'."
181             ;;
182         *)
183             failure "bad argument for lock subfunction '$action'"
184     esac
185 }
186
187
188 # for portability, between gnu date and BSD date.
189 # arguments should be:  number longunits format
190
191 # e.g. advance_date 20 seconds +%F
192 advance_date() {
193     local gnutry
194     local number="$1"
195     local longunits="$2"
196     local format="$3"
197     local shortunits
198
199     # try things the GNU way first 
200     if date -d "$number $longunits" "$format" &>/dev/null; then
201         date -d "$number $longunits" "$format"
202     else
203         # otherwise, convert to (a limited version of) BSD date syntax:
204         case "$longunits" in
205             years)
206                 shortunits=y
207                 ;;
208             months)
209                 shortunits=m
210                 ;;
211             weeks)
212                 shortunits=w
213                 ;;
214             days)
215                 shortunits=d
216                 ;;
217             hours)
218                 shortunits=H
219                 ;;
220             minutes)
221                 shortunits=M
222                 ;;
223             seconds)
224                 shortunits=S
225                 ;;
226             *)
227                 # this is a longshot, and will likely fail; oh well.
228                 shortunits="$longunits"
229         esac
230         date "-v+${number}${shortunits}" "$format"
231     fi
232 }
233
234
235 # check that characters are in a string (in an AND fashion).
236 # used for checking key capability
237 # check_capability capability a [b...]
238 check_capability() {
239     local usage
240     local capcheck
241
242     usage="$1"
243     shift 1
244
245     for capcheck ; do
246         if echo "$usage" | grep -q -v "$capcheck" ; then
247             return 1
248         fi
249     done
250     return 0
251 }
252
253 # hash of a file
254 file_hash() {
255     if type md5sum &>/dev/null ; then
256         md5sum "$1"
257     elif type md5 &>/dev/null ; then
258         md5 "$1"
259     else
260         failure "Neither md5sum nor md5 are in the path!"
261     fi
262 }
263
264 # convert escaped characters in pipeline from gpg output back into
265 # original character
266 # FIXME: undo all escape character translation in with-colons gpg
267 # output
268 gpg_unescape() {
269     sed 's/\\x3a/:/g'
270 }
271
272 # convert nasty chars into gpg-friendly form in pipeline
273 # FIXME: escape everything, not just colons!
274 gpg_escape() {
275     sed 's/:/\\x3a/g'
276 }
277
278 # prompt for GPG-formatted expiration, and emit result on stdout
279 get_gpg_expiration() {
280     local keyExpire
281
282     keyExpire="$1"
283
284     if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
285         cat >&2 <<EOF
286 Please specify how long the key should be valid.
287          0 = key does not expire
288       <n>  = key expires in n days
289       <n>w = key expires in n weeks
290       <n>m = key expires in n months
291       <n>y = key expires in n years
292 EOF
293         while [ -z "$keyExpire" ] ; do
294             printf "Key is valid for? (0) " >&2
295             read keyExpire
296             if ! test_gpg_expire ${keyExpire:=0} ; then
297                 echo "invalid value" >&2
298                 unset keyExpire
299             fi
300         done
301     elif ! test_gpg_expire "$keyExpire" ; then
302         failure "invalid key expiration value '$keyExpire'."
303     fi
304         
305     echo "$keyExpire"
306 }
307
308 passphrase_prompt() {
309     local prompt="$1"
310     local fifo="$2"
311     local PASS
312
313     if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
314         printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
315         printf '(with prompt "%s")\n' "$prompt" | log debug
316         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
317     else
318         read -s -p "$prompt" PASS
319         # Uses the builtin echo, so should not put the passphrase into
320         # the process table.  I think. --dkg
321         echo "$PASS" > "$fifo"
322     fi
323 }
324
325 # remove all lines with specified string from specified file
326 remove_line() {
327     local file
328     local string
329     local tempfile
330
331     file="$1"
332     string="$2"
333
334     if [ -z "$file" -o -z "$string" ] ; then
335         return 1
336     fi
337
338     if [ ! -e "$file" ] ; then
339         return 1
340     fi
341
342     # if the string is in the file...
343     if grep -q -F "$string" "$file" 2>/dev/null ; then
344         tempfile=$(mktemp "${file}.XXXXXXX") || \
345             failure "Unable to make temp file '${file}.XXXXXXX'"
346         
347         # remove the line with the string, and return 0
348         grep -v -F "$string" "$file" >"$tempfile"
349         cat "$tempfile" > "$file"
350         rm "$tempfile"
351         return 0
352     # otherwise return 1
353     else
354         return 1
355     fi
356 }
357
358 # remove all lines with MonkeySphere strings in file
359 remove_monkeysphere_lines() {
360     local file
361     local tempfile
362
363     file="$1"
364
365     # return error if file does not exist
366     if [ ! -e "$file" ] ; then
367         return 1
368     fi
369
370     # just return ok if the file is empty, since there aren't any
371     # lines to remove
372     if [ ! -s "$file" ] ; then
373         return 0
374     fi
375
376     tempfile=$(mktemp "${file}.XXXXXXX") || \
377         failure "Could not make temporary file '${file}.XXXXXXX'."
378
379     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
380         "$file" >"$tempfile"
381     cat "$tempfile" > "$file"
382     rm "$tempfile"
383 }
384
385 # translate ssh-style path variables %h and %u
386 translate_ssh_variables() {
387     local uname
388     local home
389
390     uname="$1"
391     path="$2"
392
393     # get the user's home directory
394     userHome=$(get_homedir "$uname")
395
396     # translate '%u' to user name
397     path=${path/\%u/"$uname"}
398     # translate '%h' to user home directory
399     path=${path/\%h/"$userHome"}
400
401     echo "$path"
402 }
403
404 # test that a string to conforms to GPG's expiration format
405 test_gpg_expire() {
406     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
407 }
408
409 # check that a file is properly owned, and that all it's parent
410 # directories are not group/other writable
411 check_key_file_permissions() {
412     local uname
413     local path
414     local stat
415     local access
416     local gAccess
417     local oAccess
418
419     # function to check that the given permission corresponds to writability
420     is_write() {
421         [ "$1" = "w" ]
422     }
423
424     uname="$1"
425     path="$2"
426
427     if [ "$STRICT_MODES" = 'false' ] ; then
428         log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
429         return 0
430     fi
431     log debug "checking path permission '$path'..."
432
433     # rewrite path if it points to a symlink
434     if [ -h "$path" ] ; then
435         path=$(readlink -f "$path")
436         log debug "checking path symlink '$path'..."
437     fi
438
439     # return 255 if cannot stat file
440     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
441         log error "could not stat path '$path'."
442         return 255
443     fi
444
445     owner=$(echo "$stat" | awk '{ print $3 }')
446     gAccess=$(echo "$stat" | cut -c6)
447     oAccess=$(echo "$stat" | cut -c9)
448
449     # return 1 if path has invalid owner
450     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
451         log error "improper ownership on path '$path':"
452         log error " $owner != ($uname|root)"
453         return 1
454     fi
455
456     # return 2 if path has group or other writability
457     if is_write "$gAccess" || is_write "$oAccess" ; then
458         log error "improper group or other writability on path '$path':"
459         log error " group: $gAccess, other: $oAccess"
460         return 2
461     fi
462
463     # return zero if all clear, or go to next path
464     if [ "$path" = '/' ] ; then
465         log debug "path ok."
466         return 0
467     else
468         check_key_file_permissions "$uname" $(dirname "$path")
469     fi
470 }
471
472 # return a list of all users on the system
473 list_users() {
474     if type getent &>/dev/null ; then
475         # for linux and FreeBSD systems
476         getent passwd | cut -d: -f1
477     elif type dscl &>/dev/null ; then
478         # for Darwin systems
479         dscl localhost -list /Search/Users
480     else
481         failure "Neither getent or dscl is in the path!  Could not determine list of users."
482     fi
483 }
484
485 # return the path to the home directory of a user
486 get_homedir() {
487     local uname=${1:-`whoami`}
488     eval "echo ~${uname}"
489 }
490
491 # return the primary group of a user
492 get_primary_group() {
493     local uname=${1:-`whoami`}
494     groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
495 }
496
497 ### CONVERSION UTILITIES
498
499 # output the ssh key for a given key ID
500 gpg2ssh() {
501     local keyID
502     
503     keyID="$1"
504
505     gpg --export "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
506 }
507
508 # output known_hosts line from ssh key
509 ssh2known_hosts() {
510     local host
511     local port
512     local key
513
514     # FIXME this does not properly deal with IPv6 hosts using the
515     # standard port (because it's unclear whether their final
516     # colon-delimited address section is a port number or an address
517     # string)
518     host=${1%:*}
519     port=${1##*:}
520     key="$2"
521
522     # specify the host and port properly for new ssh known_hosts
523     # format
524     if [ "$port" != "$host" ] ; then
525         host="[${host}]:${port}"
526     fi
527     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
528 }
529
530 # output authorized_keys line from ssh key
531 ssh2authorized_keys() {
532     local userID
533     local key
534     
535     userID="$1"
536     key="$2"
537
538     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
539 }
540
541 # convert key from gpg to ssh known_hosts format
542 gpg2known_hosts() {
543     local host
544     local keyID
545     local key
546
547     host="$1"
548     keyID="$2"
549
550     key=$(gpg2ssh "$keyID")
551
552     # NOTE: it seems that ssh-keygen -R removes all comment fields from
553     # all lines in the known_hosts file.  why?
554     # NOTE: just in case, the COMMENT can be matched with the
555     # following regexp:
556     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
557     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
558 }
559
560 # convert key from gpg to ssh authorized_keys format
561 gpg2authorized_keys() {
562     local userID
563     local keyID
564     local key
565
566     userID="$1"
567     keyID="$2"
568
569     key=$(gpg2ssh "$keyID")
570
571     # NOTE: just in case, the COMMENT can be matched with the
572     # following regexp:
573     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
574     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
575 }
576
577 ### GPG UTILITIES
578
579 # retrieve all keys with given user id from keyserver
580 # FIXME: need to figure out how to retrieve all matching keys
581 # (not just first N (5 in this case))
582 gpg_fetch_userid() {
583     local returnCode=0
584     local userID
585
586     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
587         return 0
588     fi
589
590     userID="$1"
591
592     log verbose " checking keyserver $KEYSERVER... "
593     echo 1,2,3,4,5 | \
594         gpg --quiet --batch --with-colons \
595         --command-fd 0 --keyserver "$KEYSERVER" \
596         --search ="$userID" &>/dev/null
597     returnCode="$?"
598
599     return "$returnCode"
600 }
601
602 ########################################################################
603 ### PROCESSING FUNCTIONS
604
605 # userid and key policy checking
606 # the following checks policy on the returned keys
607 # - checks that full key has appropriate valididy (u|f)
608 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
609 # - checks that requested user ID has appropriate validity
610 # (see /usr/share/doc/gnupg/DETAILS.gz)
611 # output is one line for every found key, in the following format:
612 #
613 # flag:sshKey
614 #
615 # "flag" is an acceptability flag, 0 = ok, 1 = bad
616 # "sshKey" is the translated gpg key
617 #
618 # all log output must go to stderr, as stdout is used to pass the
619 # flag:sshKey to the calling function.
620 #
621 # expects global variable: "MODE"
622 process_user_id() {
623     local returnCode=0
624     local userID
625     local requiredCapability
626     local requiredPubCapability
627     local gpgOut
628     local type
629     local validity
630     local keyid
631     local uidfpr
632     local usage
633     local keyOK
634     local uidOK
635     local lastKey
636     local lastKeyOK
637     local fingerprint
638
639     userID="$1"
640
641     # set the required key capability based on the mode
642     if [ "$MODE" = 'known_hosts' ] ; then
643         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
644     elif [ "$MODE" = 'authorized_keys' ] ; then
645         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
646     fi
647     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
648
649     # fetch the user ID if necessary/requested
650     gpg_fetch_userid "$userID"
651
652     # output gpg info for (exact) userid and store
653     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
654         --with-fingerprint --with-fingerprint \
655         ="$userID" 2>/dev/null) || returnCode="$?"
656
657     # if the gpg query return code is not 0, return 1
658     if [ "$returnCode" -ne 0 ] ; then
659         log verbose " no primary keys found."
660         return 1
661     fi
662
663     # loop over all lines in the gpg output and process.
664     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
665     while IFS=: read -r type validity keyid uidfpr usage ; do
666         # process based on record type
667         case $type in
668             'pub') # primary keys
669                 # new key, wipe the slate
670                 keyOK=
671                 uidOK=
672                 lastKey=pub
673                 lastKeyOK=
674                 fingerprint=
675
676                 log verbose " primary key found: $keyid"
677
678                 # if overall key is not valid, skip
679                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
680                     log debug "  - unacceptable primary key validity ($validity)."
681                     continue
682                 fi
683                 # if overall key is disabled, skip
684                 if check_capability "$usage" 'D' ; then
685                     log debug "  - key disabled."
686                     continue
687                 fi
688                 # if overall key capability is not ok, skip
689                 if ! check_capability "$usage" $requiredPubCapability ; then
690                     log debug "  - unacceptable primary key capability ($usage)."
691                     continue
692                 fi
693
694                 # mark overall key as ok
695                 keyOK=true
696
697                 # mark primary key as ok if capability is ok
698                 if check_capability "$usage" $requiredCapability ; then
699                     lastKeyOK=true
700                 fi
701                 ;;
702             'uid') # user ids
703                 if [ "$lastKey" != pub ] ; then
704                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
705                     continue
706                 fi
707                 # if an acceptable user ID was already found, skip
708                 if [ "$uidOK" = 'true' ] ; then
709                     continue
710                 fi
711                 # if the user ID does matches...
712                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
713                     # and the user ID validity is ok
714                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
715                         # mark user ID acceptable
716                         uidOK=true
717                     else
718                         log debug "  - unacceptable user ID validity ($validity)."
719                     fi
720                 else
721                     continue
722                 fi
723
724                 # output a line for the primary key
725                 # 0 = ok, 1 = bad
726                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
727                     log verbose "  * acceptable primary key."
728                     if [ -z "$sshKey" ] ; then
729                         log error "    ! primary key could not be translated (not RSA?)."
730                     else
731                         echo "0:${sshKey}"
732                     fi
733                 else
734                     log debug "  - unacceptable primary key."
735                     if [ -z "$sshKey" ] ; then
736                         log debug "    ! primary key could not be translated (not RSA?)."
737                     else
738                         echo "1:${sshKey}"
739                     fi
740                 fi
741                 ;;
742             'sub') # sub keys
743                 # unset acceptability of last key
744                 lastKey=sub
745                 lastKeyOK=
746                 fingerprint=
747                 
748                 # don't bother with sub keys if the primary key is not valid
749                 if [ "$keyOK" != true ] ; then
750                     continue
751                 fi
752
753                 # don't bother with sub keys if no user ID is acceptable:
754                 if [ "$uidOK" != true ] ; then
755                     continue
756                 fi
757                 
758                 # if sub key validity is not ok, skip
759                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
760                     log debug "  - unacceptable sub key validity ($validity)."
761                     continue
762                 fi
763                 # if sub key capability is not ok, skip
764                 if ! check_capability "$usage" $requiredCapability ; then
765                     log debug "  - unacceptable sub key capability ($usage)."
766                     continue
767                 fi
768
769                 # mark sub key as ok
770                 lastKeyOK=true
771                 ;;
772             'fpr') # key fingerprint
773                 fingerprint="$uidfpr"
774
775                 sshKey=$(gpg2ssh "$fingerprint")
776
777                 # if the last key was the pub key, skip
778                 if [ "$lastKey" = pub ] ; then
779                     continue
780                 fi
781
782                 # output a line for the sub key
783                 # 0 = ok, 1 = bad
784                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
785                     log verbose "  * acceptable sub key."
786                     if [ -z "$sshKey" ] ; then
787                         log error "    ! sub key could not be translated (not RSA?)."
788                     else
789                         echo "0:${sshKey}"
790                     fi
791                 else
792                     log debug "  - unacceptable sub key."
793                     if [ -z "$sshKey" ] ; then
794                         log debug "    ! sub key could not be translated (not RSA?)."
795                     else
796                         echo "1:${sshKey}"
797                     fi
798                 fi
799                 ;;
800         esac
801     done | sort -t: -k1 -n -r
802     # NOTE: this last sort is important so that the "good" keys (key
803     # flag '0') come last.  This is so that they take precedence when
804     # being processed in the key files over "bad" keys (key flag '1')
805 }
806
807 # process a single host in the known_host file
808 process_host_known_hosts() {
809     local host
810     local userID
811     local noKey=
812     local nKeys
813     local nKeysOK
814     local ok
815     local sshKey
816     local tmpfile
817
818     # set the key processing mode
819     export MODE='known_hosts'
820
821     host="$1"
822     userID="ssh://${host}"
823
824     log verbose "processing: $host"
825
826     nKeys=0
827     nKeysOK=0
828
829     IFS=$'\n'
830     for line in $(process_user_id "${userID}") ; do
831         # note that key was found
832         nKeys=$((nKeys+1))
833
834         ok=$(echo "$line" | cut -d: -f1)
835         sshKey=$(echo "$line" | cut -d: -f2)
836
837         if [ -z "$sshKey" ] ; then
838             continue
839         fi
840
841         # remove any old host key line, and note if removed nothing is
842         # removed
843         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
844
845         # if key OK, add new host line
846         if [ "$ok" -eq '0' ] ; then
847             # note that key was found ok
848             nKeysOK=$((nKeysOK+1))
849
850             # hash if specified
851             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
852                 # FIXME: this is really hackish cause ssh-keygen won't
853                 # hash from stdin to stdout
854                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
855                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
856                 ssh-keygen -H -f "$tmpfile" 2>/dev/null
857                 cat "$tmpfile" >> "$KNOWN_HOSTS"
858                 rm -f "$tmpfile" "${tmpfile}.old"
859             else
860                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
861             fi
862
863             # log if this is a new key to the known_hosts file
864             if [ "$noKey" ] ; then
865                 log info "* new key for $host added to known_hosts file."
866             fi
867         fi
868     done
869
870     # if at least one key was found...
871     if [ "$nKeys" -gt 0 ] ; then
872         # if ok keys were found, return 0
873         if [ "$nKeysOK" -gt 0 ] ; then
874             return 0
875         # else return 2
876         else
877             return 2
878         fi
879     # if no keys were found, return 1
880     else
881         return 1
882     fi
883 }
884
885 # update the known_hosts file for a set of hosts listed on command
886 # line
887 update_known_hosts() {
888     local returnCode=0
889     local nHosts
890     local nHostsOK
891     local nHostsBAD
892     local fileCheck
893     local host
894     local newUmask
895
896     # the number of hosts specified on command line
897     nHosts="$#"
898
899     nHostsOK=0
900     nHostsBAD=0
901
902     # touch the known_hosts file so that the file permission check
903     # below won't fail upon not finding the file
904     if [ ! -f "$KNOWN_HOSTS" ]; then
905         # make sure to create any files or directories with the appropriate write bits turned off:
906         newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
907         [ -d $(dirname "$KNOWN_HOSTS") ] \
908             || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
909             || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
910         # make sure to create this file with the appropriate bits turned off:
911         (umask "$newUmask" && touch "$KNOWN_HOSTS") \
912             || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
913     fi
914
915     # check permissions on the known_hosts file path
916     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
917         || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
918
919     # create a lockfile on known_hosts:
920     lock create "$KNOWN_HOSTS"
921     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
922     trap "lock remove $KNOWN_HOSTS" EXIT
923
924     # note pre update file checksum
925     fileCheck=$(file_hash "$KNOWN_HOSTS")
926
927     for host ; do
928         # process the host
929         process_host_known_hosts "$host" || returnCode="$?"
930         # note the result
931         case "$returnCode" in
932             0)
933                 nHostsOK=$((nHostsOK+1))
934                 ;;
935             2)
936                 nHostsBAD=$((nHostsBAD+1))
937                 ;;
938         esac
939
940         # touch the lockfile, for good measure.
941         lock touch "$KNOWN_HOSTS"
942     done
943
944     # remove the lockfile and the trap
945     lock remove "$KNOWN_HOSTS"
946     trap - EXIT
947
948     # note if the known_hosts file was updated
949     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
950         log debug "known_hosts file updated."
951     fi
952
953     # if an acceptable host was found, return 0
954     if [ "$nHostsOK" -gt 0 ] ; then
955         return 0
956     # else if no ok hosts were found...
957     else
958         # if no bad host were found then no hosts were found at all,
959         # and return 1
960         if [ "$nHostsBAD" -eq 0 ] ; then
961             return 1
962         # else if at least one bad host was found, return 2
963         else
964             return 2
965         fi
966     fi
967 }
968
969 # process hosts from a known_hosts file
970 process_known_hosts() {
971     local hosts
972
973     # exit if the known_hosts file does not exist
974     if [ ! -e "$KNOWN_HOSTS" ] ; then
975         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
976     fi
977
978     log debug "processing known_hosts file:"
979     log debug " $KNOWN_HOSTS"
980
981     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
982
983     if [ -z "$hosts" ] ; then
984         log debug "no hosts to process."
985         return
986     fi
987
988     # take all the hosts from the known_hosts file (first
989     # field), grep out all the hashed hosts (lines starting
990     # with '|')...
991     update_known_hosts $hosts
992 }
993
994 # process uids for the authorized_keys file
995 process_uid_authorized_keys() {
996     local userID
997     local nKeys
998     local nKeysOK
999     local ok
1000     local sshKey
1001
1002     # set the key processing mode
1003     export MODE='authorized_keys'
1004
1005     userID="$1"
1006
1007     log verbose "processing: $userID"
1008
1009     nKeys=0
1010     nKeysOK=0
1011
1012     IFS=$'\n'
1013     for line in $(process_user_id "$userID") ; do
1014         # note that key was found
1015         nKeys=$((nKeys+1))
1016
1017         ok=$(echo "$line" | cut -d: -f1)
1018         sshKey=$(echo "$line" | cut -d: -f2)
1019
1020         if [ -z "$sshKey" ] ; then
1021             continue
1022         fi
1023
1024         # remove the old host key line
1025         remove_line "$AUTHORIZED_KEYS" "$sshKey"
1026
1027         # if key OK, add new host line
1028         if [ "$ok" -eq '0' ] ; then
1029             # note that key was found ok
1030             nKeysOK=$((nKeysOK+1))
1031
1032             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
1033         fi
1034     done
1035
1036     # if at least one key was found...
1037     if [ "$nKeys" -gt 0 ] ; then
1038         # if ok keys were found, return 0
1039         if [ "$nKeysOK" -gt 0 ] ; then
1040             return 0
1041         # else return 2
1042         else
1043             return 2
1044         fi
1045     # if no keys were found, return 1
1046     else
1047         return 1
1048     fi
1049 }
1050
1051 # update the authorized_keys files from a list of user IDs on command
1052 # line
1053 update_authorized_keys() {
1054     local returnCode=0
1055     local userID
1056     local nIDs
1057     local nIDsOK
1058     local nIDsBAD
1059     local fileCheck
1060
1061     # the number of ids specified on command line
1062     nIDs="$#"
1063
1064     nIDsOK=0
1065     nIDsBAD=0
1066
1067     log debug "updating authorized_keys file:"
1068     log debug " $AUTHORIZED_KEYS"
1069
1070     # check permissions on the authorized_keys file path
1071     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1072
1073     # create a lockfile on authorized_keys
1074     lock create "$AUTHORIZED_KEYS"
1075     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1076     trap "lock remove $AUTHORIZED_KEYS" EXIT
1077
1078     # note pre update file checksum
1079     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1080
1081     # remove any monkeysphere lines from authorized_keys file
1082     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1083
1084     for userID ; do
1085         # process the user ID, change return code if key not found for
1086         # user ID
1087         process_uid_authorized_keys "$userID" || returnCode="$?"
1088
1089         # note the result
1090         case "$returnCode" in
1091             0)
1092                 nIDsOK=$((nIDsOK+1))
1093                 ;;
1094             2)
1095                 nIDsBAD=$((nIDsBAD+1))
1096                 ;;
1097         esac
1098
1099         # touch the lockfile, for good measure.
1100         lock touch "$AUTHORIZED_KEYS"
1101     done
1102
1103     # remove the lockfile and the trap
1104     lock remove "$AUTHORIZED_KEYS"
1105
1106     # remove the trap
1107     trap - EXIT
1108
1109     # note if the authorized_keys file was updated
1110     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1111         log debug "authorized_keys file updated."
1112     fi
1113
1114     # if an acceptable id was found, return 0
1115     if [ "$nIDsOK" -gt 0 ] ; then
1116         return 0
1117     # else if no ok ids were found...
1118     else
1119         # if no bad ids were found then no ids were found at all, and
1120         # return 1
1121         if [ "$nIDsBAD" -eq 0 ] ; then
1122             return 1
1123         # else if at least one bad id was found, return 2
1124         else
1125             return 2
1126         fi
1127     fi
1128 }
1129
1130 # process an authorized_user_ids file for authorized_keys
1131 process_authorized_user_ids() {
1132     local line
1133     local nline
1134     local userIDs
1135
1136     authorizedUserIDs="$1"
1137
1138     # exit if the authorized_user_ids file is empty
1139     if [ ! -e "$authorizedUserIDs" ] ; then
1140         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1141     fi
1142
1143     log debug "processing authorized_user_ids file:"
1144     log debug " $authorizedUserIDs"
1145
1146     # check permissions on the authorized_user_ids file path
1147     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1148
1149     if ! meat "$authorizedUserIDs" >/dev/null ; then
1150         log debug " no user IDs to process."
1151         return
1152     fi
1153
1154     nline=0
1155
1156     # extract user IDs from authorized_user_ids file
1157     IFS=$'\n'
1158     for line in $(meat "$authorizedUserIDs") ; do
1159         userIDs["$nline"]="$line"
1160         nline=$((nline+1))
1161     done
1162
1163     update_authorized_keys "${userIDs[@]}"
1164 }
1165
1166 # takes a gpg key or keys on stdin, and outputs a list of
1167 # fingerprints, one per line:
1168 list_primary_fingerprints() {
1169     local fake=$(msmktempdir)
1170     GNUPGHOME="$fake" gpg --no-tty --quiet --import
1171     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1172         awk -F: '/^fpr:/{ print $10 }'
1173     rm -rf "$fake"
1174 }
1175
1176
1177 check_cruft_file() {
1178     local loc="$1"
1179     local version="$2"
1180     
1181     if [ -e "$loc" ] ; then
1182         printf "! The file '%s' is no longer used by\n  monkeysphere (as of version %s), and can be removed.\n\n" "$loc" "$version" | log info
1183     fi
1184 }
1185
1186 check_upgrade_dir() {
1187     local loc="$1"
1188     local version="$2"
1189
1190     if [ -d "$loc" ] ; then
1191         printf "The presence of directory '%s' indicates that you have\nnot yet completed a monkeysphere upgrade.\nYou should probably run the following script:\n  %s/transitions/%s\n\n" "$loc" "$SYSSHAREDIR" "$version" | log info
1192     fi
1193 }
1194
1195 ## look for cruft from old versions of the monkeysphere, and notice if
1196 ## upgrades have not been run:
1197 report_cruft() {
1198     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1199     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1200
1201     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1202     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1203
1204     local found=
1205     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1206         if [ -d "$foo" ] ; then
1207             printf "! %s\n" "$foo" | log info
1208             found=true
1209         fi
1210     done
1211     if [ "$found" ] ; then
1212         printf "The directories above are backups left over from a monkeysphere transition.\nThey may contain copies of sensitive data (host keys, certifier lists), but\nthey are no longer needed by monkeysphere.\nYou may remove them at any time.\n\n" | log info
1213     fi
1214 }