c88857e44adabca90d57bb02358bcce92d16f67d
[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     log debug "checking path permission '$path'..."
428
429     # rewrite path if it points to a symlink
430     if [ -h "$path" ] ; then
431         path=$(readlink -f "$path")
432         log debug "checking path symlink '$path'..."
433     fi
434
435     # return 255 if cannot stat file
436     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
437         log error "could not stat path '$path'."
438         return 255
439     fi
440
441     owner=$(echo "$stat" | awk '{ print $3 }')
442     gAccess=$(echo "$stat" | cut -c6)
443     oAccess=$(echo "$stat" | cut -c9)
444
445     # return 1 if path has invalid owner
446     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
447         log error "improper ownership on path '$path':"
448         log error " $owner != ($uname|root)"
449         return 1
450     fi
451
452     # return 2 if path has group or other writability
453     if is_write "$gAccess" || is_write "$oAccess" ; then
454         log error "improper group or other writability on path '$path':"
455         log error " group: $gAccess, other: $oAccess"
456         return 2
457     fi
458
459     # return zero if all clear, or go to next path
460     if [ "$path" = '/' ] ; then
461         log debug "path ok."
462         return 0
463     else
464         check_key_file_permissions "$uname" $(dirname "$path")
465     fi
466 }
467
468 # return a list of all users on the system
469 list_users() {
470     if type getent &>/dev/null ; then
471         # for linux and FreeBSD systems
472         getent passwd | cut -d: -f1
473     elif type dscl &>/dev/null ; then
474         # for Darwin systems
475         dscl localhost -list /Search/Users
476     else
477         failure "Neither getent or dscl is in the path!  Could not determine list of users."
478     fi
479 }
480
481 # return the path to the home directory of a user
482 get_homedir() {
483     local uname=${1:-`whoami`}
484     eval "echo ~${uname}"
485 }
486
487 ### CONVERSION UTILITIES
488
489 # output the ssh key for a given key ID
490 gpg2ssh() {
491     local keyID
492     
493     keyID="$1"
494
495     gpg --export "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
496 }
497
498 # output known_hosts line from ssh key
499 ssh2known_hosts() {
500     local host
501     local port
502     local key
503
504     # FIXME this does not properly deal with IPv6 hosts using the
505     # standard port (because it's unclear whether their final
506     # colon-delimited address section is a port number or an address
507     # string)
508     host=${1%:*}
509     port=${1##*:}
510     key="$2"
511
512     # specify the host and port properly for new ssh known_hosts
513     # format
514     if [ "$port" != "$host" ] ; then
515         host="[${host}]:${port}"
516     fi
517     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
518 }
519
520 # output authorized_keys line from ssh key
521 ssh2authorized_keys() {
522     local userID
523     local key
524     
525     userID="$1"
526     key="$2"
527
528     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
529 }
530
531 # convert key from gpg to ssh known_hosts format
532 gpg2known_hosts() {
533     local host
534     local keyID
535     local key
536
537     host="$1"
538     keyID="$2"
539
540     key=$(gpg2ssh "$keyID")
541
542     # NOTE: it seems that ssh-keygen -R removes all comment fields from
543     # all lines in the known_hosts file.  why?
544     # NOTE: just in case, the COMMENT can be matched with the
545     # following regexp:
546     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
547     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
548 }
549
550 # convert key from gpg to ssh authorized_keys format
551 gpg2authorized_keys() {
552     local userID
553     local keyID
554     local key
555
556     userID="$1"
557     keyID="$2"
558
559     key=$(gpg2ssh "$keyID")
560
561     # NOTE: just in case, the COMMENT can be matched with the
562     # following regexp:
563     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
564     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
565 }
566
567 ### GPG UTILITIES
568
569 # retrieve all keys with given user id from keyserver
570 # FIXME: need to figure out how to retrieve all matching keys
571 # (not just first N (5 in this case))
572 gpg_fetch_userid() {
573     local returnCode=0
574     local userID
575
576     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
577         return 0
578     fi
579
580     userID="$1"
581
582     log verbose " checking keyserver $KEYSERVER... "
583     echo 1,2,3,4,5 | \
584         gpg --quiet --batch --with-colons \
585         --command-fd 0 --keyserver "$KEYSERVER" \
586         --search ="$userID" &>/dev/null
587     returnCode="$?"
588
589     return "$returnCode"
590 }
591
592 ########################################################################
593 ### PROCESSING FUNCTIONS
594
595 # userid and key policy checking
596 # the following checks policy on the returned keys
597 # - checks that full key has appropriate valididy (u|f)
598 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
599 # - checks that requested user ID has appropriate validity
600 # (see /usr/share/doc/gnupg/DETAILS.gz)
601 # output is one line for every found key, in the following format:
602 #
603 # flag:sshKey
604 #
605 # "flag" is an acceptability flag, 0 = ok, 1 = bad
606 # "sshKey" is the translated gpg key
607 #
608 # all log output must go to stderr, as stdout is used to pass the
609 # flag:sshKey to the calling function.
610 #
611 # expects global variable: "MODE"
612 process_user_id() {
613     local returnCode=0
614     local userID
615     local requiredCapability
616     local requiredPubCapability
617     local gpgOut
618     local type
619     local validity
620     local keyid
621     local uidfpr
622     local usage
623     local keyOK
624     local uidOK
625     local lastKey
626     local lastKeyOK
627     local fingerprint
628
629     userID="$1"
630
631     # set the required key capability based on the mode
632     if [ "$MODE" = 'known_hosts' ] ; then
633         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
634     elif [ "$MODE" = 'authorized_keys' ] ; then
635         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
636     fi
637     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
638
639     # fetch the user ID if necessary/requested
640     gpg_fetch_userid "$userID"
641
642     # output gpg info for (exact) userid and store
643     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
644         --with-fingerprint --with-fingerprint \
645         ="$userID" 2>/dev/null) || returnCode="$?"
646
647     # if the gpg query return code is not 0, return 1
648     if [ "$returnCode" -ne 0 ] ; then
649         log verbose " no primary keys found."
650         return 1
651     fi
652
653     # loop over all lines in the gpg output and process.
654     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
655     while IFS=: read -r type validity keyid uidfpr usage ; do
656         # process based on record type
657         case $type in
658             'pub') # primary keys
659                 # new key, wipe the slate
660                 keyOK=
661                 uidOK=
662                 lastKey=pub
663                 lastKeyOK=
664                 fingerprint=
665
666                 log verbose " primary key found: $keyid"
667
668                 # if overall key is not valid, skip
669                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
670                     log debug "  - unacceptable primary key validity ($validity)."
671                     continue
672                 fi
673                 # if overall key is disabled, skip
674                 if check_capability "$usage" 'D' ; then
675                     log debug "  - key disabled."
676                     continue
677                 fi
678                 # if overall key capability is not ok, skip
679                 if ! check_capability "$usage" $requiredPubCapability ; then
680                     log debug "  - unacceptable primary key capability ($usage)."
681                     continue
682                 fi
683
684                 # mark overall key as ok
685                 keyOK=true
686
687                 # mark primary key as ok if capability is ok
688                 if check_capability "$usage" $requiredCapability ; then
689                     lastKeyOK=true
690                 fi
691                 ;;
692             'uid') # user ids
693                 if [ "$lastKey" != pub ] ; then
694                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
695                     continue
696                 fi
697                 # if an acceptable user ID was already found, skip
698                 if [ "$uidOK" = 'true' ] ; then
699                     continue
700                 fi
701                 # if the user ID does matches...
702                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
703                     # and the user ID validity is ok
704                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
705                         # mark user ID acceptable
706                         uidOK=true
707                     else
708                         log debug "  - unacceptable user ID validity ($validity)."
709                     fi
710                 else
711                     continue
712                 fi
713
714                 # output a line for the primary key
715                 # 0 = ok, 1 = bad
716                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
717                     log verbose "  * acceptable primary key."
718                     if [ -z "$sshKey" ] ; then
719                         log error "    ! primary key could not be translated (not RSA?)."
720                     else
721                         echo "0:${sshKey}"
722                     fi
723                 else
724                     log debug "  - unacceptable primary key."
725                     if [ -z "$sshKey" ] ; then
726                         log debug "    ! primary key could not be translated (not RSA?)."
727                     else
728                         echo "1:${sshKey}"
729                     fi
730                 fi
731                 ;;
732             'sub') # sub keys
733                 # unset acceptability of last key
734                 lastKey=sub
735                 lastKeyOK=
736                 fingerprint=
737                 
738                 # don't bother with sub keys if the primary key is not valid
739                 if [ "$keyOK" != true ] ; then
740                     continue
741                 fi
742
743                 # don't bother with sub keys if no user ID is acceptable:
744                 if [ "$uidOK" != true ] ; then
745                     continue
746                 fi
747                 
748                 # if sub key validity is not ok, skip
749                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
750                     log debug "  - unacceptable sub key validity ($validity)."
751                     continue
752                 fi
753                 # if sub key capability is not ok, skip
754                 if ! check_capability "$usage" $requiredCapability ; then
755                     log debug "  - unacceptable sub key capability ($usage)."
756                     continue
757                 fi
758
759                 # mark sub key as ok
760                 lastKeyOK=true
761                 ;;
762             'fpr') # key fingerprint
763                 fingerprint="$uidfpr"
764
765                 sshKey=$(gpg2ssh "$fingerprint")
766
767                 # if the last key was the pub key, skip
768                 if [ "$lastKey" = pub ] ; then
769                     continue
770                 fi
771
772                 # output a line for the sub key
773                 # 0 = ok, 1 = bad
774                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
775                     log verbose "  * acceptable sub key."
776                     if [ -z "$sshKey" ] ; then
777                         log error "    ! sub key could not be translated (not RSA?)."
778                     else
779                         echo "0:${sshKey}"
780                     fi
781                 else
782                     log debug "  - unacceptable sub key."
783                     if [ -z "$sshKey" ] ; then
784                         log debug "    ! sub key could not be translated (not RSA?)."
785                     else
786                         echo "1:${sshKey}"
787                     fi
788                 fi
789                 ;;
790         esac
791     done | sort -t: -k1 -n -r
792     # NOTE: this last sort is important so that the "good" keys (key
793     # flag '0') come last.  This is so that they take precedence when
794     # being processed in the key files over "bad" keys (key flag '1')
795 }
796
797 # process a single host in the known_host file
798 process_host_known_hosts() {
799     local host
800     local userID
801     local noKey=
802     local nKeys
803     local nKeysOK
804     local ok
805     local sshKey
806     local tmpfile
807
808     # set the key processing mode
809     export MODE='known_hosts'
810
811     host="$1"
812     userID="ssh://${host}"
813
814     log verbose "processing: $host"
815
816     nKeys=0
817     nKeysOK=0
818
819     IFS=$'\n'
820     for line in $(process_user_id "${userID}") ; do
821         # note that key was found
822         nKeys=$((nKeys+1))
823
824         ok=$(echo "$line" | cut -d: -f1)
825         sshKey=$(echo "$line" | cut -d: -f2)
826
827         if [ -z "$sshKey" ] ; then
828             continue
829         fi
830
831         # remove any old host key line, and note if removed nothing is
832         # removed
833         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
834
835         # if key OK, add new host line
836         if [ "$ok" -eq '0' ] ; then
837             # note that key was found ok
838             nKeysOK=$((nKeysOK+1))
839
840             # hash if specified
841             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
842                 # FIXME: this is really hackish cause ssh-keygen won't
843                 # hash from stdin to stdout
844                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
845                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
846                 ssh-keygen -H -f "$tmpfile" 2>/dev/null
847                 cat "$tmpfile" >> "$KNOWN_HOSTS"
848                 rm -f "$tmpfile" "${tmpfile}.old"
849             else
850                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
851             fi
852
853             # log if this is a new key to the known_hosts file
854             if [ "$noKey" ] ; then
855                 log info "* new key for $host added to known_hosts file."
856             fi
857         fi
858     done
859
860     # if at least one key was found...
861     if [ "$nKeys" -gt 0 ] ; then
862         # if ok keys were found, return 0
863         if [ "$nKeysOK" -gt 0 ] ; then
864             return 0
865         # else return 2
866         else
867             return 2
868         fi
869     # if no keys were found, return 1
870     else
871         return 1
872     fi
873 }
874
875 # update the known_hosts file for a set of hosts listed on command
876 # line
877 update_known_hosts() {
878     local returnCode=0
879     local nHosts
880     local nHostsOK
881     local nHostsBAD
882     local fileCheck
883     local host
884
885     # the number of hosts specified on command line
886     nHosts="$#"
887
888     nHostsOK=0
889     nHostsBAD=0
890
891     # touch the known_hosts file so that the file permission check
892     # below won't fail upon not finding the file
893     (umask 0022 && touch "$KNOWN_HOSTS")
894
895     # check permissions on the known_hosts file path
896     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" || failure
897
898     # create a lockfile on known_hosts:
899     lock create "$KNOWN_HOSTS"
900     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
901     trap "lock remove $KNOWN_HOSTS" EXIT
902
903     # note pre update file checksum
904     fileCheck="$(file_hash "$KNOWN_HOSTS")"
905
906     for host ; do
907         # process the host
908         process_host_known_hosts "$host" || returnCode="$?"
909         # note the result
910         case "$returnCode" in
911             0)
912                 nHostsOK=$((nHostsOK+1))
913                 ;;
914             2)
915                 nHostsBAD=$((nHostsBAD+1))
916                 ;;
917         esac
918
919         # touch the lockfile, for good measure.
920         lock touch "$KNOWN_HOSTS"
921     done
922
923     # remove the lockfile and the trap
924     lock remove "$KNOWN_HOSTS"
925     trap - EXIT
926
927     # note if the known_hosts file was updated
928     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
929         log debug "known_hosts file updated."
930     fi
931
932     # if an acceptable host was found, return 0
933     if [ "$nHostsOK" -gt 0 ] ; then
934         return 0
935     # else if no ok hosts were found...
936     else
937         # if no bad host were found then no hosts were found at all,
938         # and return 1
939         if [ "$nHostsBAD" -eq 0 ] ; then
940             return 1
941         # else if at least one bad host was found, return 2
942         else
943             return 2
944         fi
945     fi
946 }
947
948 # process hosts from a known_hosts file
949 process_known_hosts() {
950     local hosts
951
952     # exit if the known_hosts file does not exist
953     if [ ! -e "$KNOWN_HOSTS" ] ; then
954         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
955     fi
956
957     log debug "processing known_hosts file:"
958     log debug " $KNOWN_HOSTS"
959
960     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
961
962     if [ -z "$hosts" ] ; then
963         log debug "no hosts to process."
964         return
965     fi
966
967     # take all the hosts from the known_hosts file (first
968     # field), grep out all the hashed hosts (lines starting
969     # with '|')...
970     update_known_hosts $hosts
971 }
972
973 # process uids for the authorized_keys file
974 process_uid_authorized_keys() {
975     local userID
976     local nKeys
977     local nKeysOK
978     local ok
979     local sshKey
980
981     # set the key processing mode
982     export MODE='authorized_keys'
983
984     userID="$1"
985
986     log verbose "processing: $userID"
987
988     nKeys=0
989     nKeysOK=0
990
991     IFS=$'\n'
992     for line in $(process_user_id "$userID") ; do
993         # note that key was found
994         nKeys=$((nKeys+1))
995
996         ok=$(echo "$line" | cut -d: -f1)
997         sshKey=$(echo "$line" | cut -d: -f2)
998
999         if [ -z "$sshKey" ] ; then
1000             continue
1001         fi
1002
1003         # remove the old host key line
1004         remove_line "$AUTHORIZED_KEYS" "$sshKey"
1005
1006         # if key OK, add new host line
1007         if [ "$ok" -eq '0' ] ; then
1008             # note that key was found ok
1009             nKeysOK=$((nKeysOK+1))
1010
1011             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
1012         fi
1013     done
1014
1015     # if at least one key was found...
1016     if [ "$nKeys" -gt 0 ] ; then
1017         # if ok keys were found, return 0
1018         if [ "$nKeysOK" -gt 0 ] ; then
1019             return 0
1020         # else return 2
1021         else
1022             return 2
1023         fi
1024     # if no keys were found, return 1
1025     else
1026         return 1
1027     fi
1028 }
1029
1030 # update the authorized_keys files from a list of user IDs on command
1031 # line
1032 update_authorized_keys() {
1033     local returnCode=0
1034     local userID
1035     local nIDs
1036     local nIDsOK
1037     local nIDsBAD
1038     local fileCheck
1039
1040     # the number of ids specified on command line
1041     nIDs="$#"
1042
1043     nIDsOK=0
1044     nIDsBAD=0
1045
1046     log debug "updating authorized_keys file:"
1047     log debug " $AUTHORIZED_KEYS"
1048
1049     # check permissions on the authorized_keys file path
1050     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1051
1052     # create a lockfile on authorized_keys
1053     lock create "$AUTHORIZED_KEYS"
1054     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1055     trap "lock remove $AUTHORIZED_KEYS" EXIT
1056
1057     # note pre update file checksum
1058     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1059
1060     # remove any monkeysphere lines from authorized_keys file
1061     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1062
1063     for userID ; do
1064         # process the user ID, change return code if key not found for
1065         # user ID
1066         process_uid_authorized_keys "$userID" || returnCode="$?"
1067
1068         # note the result
1069         case "$returnCode" in
1070             0)
1071                 nIDsOK=$((nIDsOK+1))
1072                 ;;
1073             2)
1074                 nIDsBAD=$((nIDsBAD+1))
1075                 ;;
1076         esac
1077
1078         # touch the lockfile, for good measure.
1079         lock touch "$AUTHORIZED_KEYS"
1080     done
1081
1082     # remove the lockfile and the trap
1083     lock remove "$AUTHORIZED_KEYS"
1084
1085     # remove the trap
1086     trap - EXIT
1087
1088     # note if the authorized_keys file was updated
1089     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1090         log debug "authorized_keys file updated."
1091     fi
1092
1093     # if an acceptable id was found, return 0
1094     if [ "$nIDsOK" -gt 0 ] ; then
1095         return 0
1096     # else if no ok ids were found...
1097     else
1098         # if no bad ids were found then no ids were found at all, and
1099         # return 1
1100         if [ "$nIDsBAD" -eq 0 ] ; then
1101             return 1
1102         # else if at least one bad id was found, return 2
1103         else
1104             return 2
1105         fi
1106     fi
1107 }
1108
1109 # process an authorized_user_ids file for authorized_keys
1110 process_authorized_user_ids() {
1111     local line
1112     local nline
1113     local userIDs
1114
1115     authorizedUserIDs="$1"
1116
1117     # exit if the authorized_user_ids file is empty
1118     if [ ! -e "$authorizedUserIDs" ] ; then
1119         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1120     fi
1121
1122     log debug "processing authorized_user_ids file:"
1123     log debug " $authorizedUserIDs"
1124
1125     # check permissions on the authorized_user_ids file path
1126     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1127
1128     if ! meat "$authorizedUserIDs" >/dev/null ; then
1129         log debug " no user IDs to process."
1130         return
1131     fi
1132
1133     nline=0
1134
1135     # extract user IDs from authorized_user_ids file
1136     IFS=$'\n'
1137     for line in $(meat "$authorizedUserIDs") ; do
1138         userIDs["$nline"]="$line"
1139         nline=$((nline+1))
1140     done
1141
1142     update_authorized_keys "${userIDs[@]}"
1143 }
1144
1145 # takes a gpg key or keys on stdin, and outputs a list of
1146 # fingerprints, one per line:
1147 list_primary_fingerprints() {
1148     local fake=$(msmktempdir)
1149     GNUPGHOME="$fake" gpg --no-tty --quiet --import
1150     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1151         awk -F: '/^fpr:/{ print $10 }'
1152     rm -rf "$fake"
1153 }
1154
1155
1156 check_cruft_file() {
1157     local loc="$1"
1158     local version="$2"
1159     
1160     if [ -e "$loc" ] ; then
1161         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
1162     fi
1163 }
1164
1165 check_upgrade_dir() {
1166     local loc="$1"
1167     local version="$2"
1168
1169     if [ -d "$loc" ] ; then
1170         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
1171     fi
1172 }
1173
1174 ## look for cruft from old versions of the monkeysphere, and notice if
1175 ## upgrades have not been run:
1176 report_cruft() {
1177     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1178     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1179
1180     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1181     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1182
1183     local found=
1184     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1185         if [ -d "$foo" ] ; then
1186             printf "! %s\n" "$foo" | log info
1187             found=true
1188         fi
1189     done
1190     if [ "$found" ] ; then
1191         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
1192     fi
1193 }