proposed patch for issue #660, to properly specify host and port number in known_host...
[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 ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
151         if ! ( which 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 2>&1; 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     md5sum "$1" 2> /dev/null
256 }
257
258 # convert escaped characters in pipeline from gpg output back into
259 # original character
260 # FIXME: undo all escape character translation in with-colons gpg
261 # output
262 gpg_unescape() {
263     sed 's/\\x3a/:/g'
264 }
265
266 # convert nasty chars into gpg-friendly form in pipeline
267 # FIXME: escape everything, not just colons!
268 gpg_escape() {
269     sed 's/:/\\x3a/g'
270 }
271
272 # prompt for GPG-formatted expiration, and emit result on stdout
273 get_gpg_expiration() {
274     local keyExpire
275
276     keyExpire="$1"
277
278     if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
279         cat >&2 <<EOF
280 Please specify how long the key should be valid.
281          0 = key does not expire
282       <n>  = key expires in n days
283       <n>w = key expires in n weeks
284       <n>m = key expires in n months
285       <n>y = key expires in n years
286 EOF
287         while [ -z "$keyExpire" ] ; do
288             read -p "Key is valid for? (0) " keyExpire
289             if ! test_gpg_expire ${keyExpire:=0} ; then
290                 echo "invalid value" >&2
291                 unset keyExpire
292             fi
293         done
294     elif ! test_gpg_expire "$keyExpire" ; then
295         failure "invalid key expiration value '$keyExpire'."
296     fi
297         
298     echo "$keyExpire"
299 }
300
301 passphrase_prompt() {
302     local prompt="$1"
303     local fifo="$2"
304     local PASS
305
306     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
307         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
308     else
309         read -s -p "$prompt" PASS
310         # Uses the builtin echo, so should not put the passphrase into
311         # the process table.  I think. --dkg
312         echo "$PASS" > "$fifo"
313     fi
314 }
315
316 # remove all lines with specified string from specified file
317 remove_line() {
318     local file
319     local string
320     local tempfile
321
322     file="$1"
323     string="$2"
324
325     if [ -z "$file" -o -z "$string" ] ; then
326         return 1
327     fi
328
329     if [ ! -e "$file" ] ; then
330         return 1
331     fi
332
333     # if the string is in the file...
334     if grep -q -F "$string" "$file" 2> /dev/null ; then
335         tempfile=$(mktemp "${file}.XXXXXXX") || \
336             failure "Unable to make temp file '${file}.XXXXXXX'"
337         
338         # remove the line with the string, and return 0
339         grep -v -F "$string" "$file" >"$tempfile"
340         cat "$tempfile" > "$file"
341         rm "$tempfile"
342         return 0
343     # otherwise return 1
344     else
345         return 1
346     fi
347 }
348
349 # remove all lines with MonkeySphere strings in file
350 remove_monkeysphere_lines() {
351     local file
352     local tempfile
353
354     file="$1"
355
356     # return error if file does not exist
357     if [ ! -e "$file" ] ; then
358         return 1
359     fi
360
361     # just return ok if the file is empty, since there aren't any
362     # lines to remove
363     if [ ! -s "$file" ] ; then
364         return 0
365     fi
366
367     tempfile=$(mktemp "${file}.XXXXXXX") || \
368         failure "Could not make temporary file '${file}.XXXXXXX'."
369
370     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
371         "$file" >"$tempfile"
372     cat "$tempfile" > "$file"
373     rm "$tempfile"
374 }
375
376 # translate ssh-style path variables %h and %u
377 translate_ssh_variables() {
378     local uname
379     local home
380
381     uname="$1"
382     path="$2"
383
384     # get the user's home directory
385     userHome=$(getent passwd "$uname" | cut -d: -f6)
386
387     # translate '%u' to user name
388     path=${path/\%u/"$uname"}
389     # translate '%h' to user home directory
390     path=${path/\%h/"$userHome"}
391
392     echo "$path"
393 }
394
395 # test that a string to conforms to GPG's expiration format
396 test_gpg_expire() {
397     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
398 }
399
400 # check that a file is properly owned, and that all it's parent
401 # directories are not group/other writable
402 check_key_file_permissions() {
403     local uname
404     local path
405     local stat
406     local access
407     local gAccess
408     local oAccess
409
410     # function to check that the given permission corresponds to writability
411     is_write() {
412         [ "$1" = "w" ]
413     }
414
415     uname="$1"
416     path="$2"
417
418     log debug "checking path permission '$path'..."
419
420     # return 255 if cannot stat file
421     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
422         log error "could not stat path '$path'."
423         return 255
424     fi
425
426     owner=$(echo "$stat" | awk '{ print $3 }')
427     gAccess=$(echo "$stat" | cut -c6)
428     oAccess=$(echo "$stat" | cut -c9)
429
430     # return 1 if path has invalid owner
431     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
432         log error "improper ownership on path '$path':"
433         log error " $owner != ($uname|root)"
434         return 1
435     fi
436
437     # return 2 if path has group or other writability
438     if is_write "$gAccess" || is_write "$oAccess" ; then
439         log error "improper group or other writability on path '$path':"
440         log error " group: $gAccess, other: $oAcess"
441         return 2
442     fi
443
444     # return zero if all clear, or go to next path
445     if [ "$path" = '/' ] ; then
446         log debug "path ok."
447         return 0
448     else
449         check_key_file_permissions "$uname" $(dirname "$path")
450     fi
451 }
452
453 ### CONVERSION UTILITIES
454
455 # output the ssh key for a given key ID
456 gpg2ssh() {
457     local keyID
458     
459     keyID="$1"
460
461     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
462 }
463
464 # output known_hosts line from ssh key
465 ssh2known_hosts() {
466     local host
467     local port
468     local key
469
470     host=${1%%:*}
471     port=${1##*:}
472     key="$2"
473
474     # specify the host and port properly for new ssh known_hosts
475     # format
476     if [ "$port" != "$host" ] ; then
477         host="[${host}]:${port}"
478     fi
479     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
480 }
481
482 # output authorized_keys line from ssh key
483 ssh2authorized_keys() {
484     local userID
485     local key
486     
487     userID="$1"
488     key="$2"
489
490     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
491 }
492
493 # convert key from gpg to ssh known_hosts format
494 gpg2known_hosts() {
495     local host
496     local keyID
497     local key
498
499     host="$1"
500     keyID="$2"
501
502     key=$(gpg2ssh "$keyID")
503
504     # NOTE: it seems that ssh-keygen -R removes all comment fields from
505     # all lines in the known_hosts file.  why?
506     # NOTE: just in case, the COMMENT can be matched with the
507     # following regexp:
508     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
509     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
510 }
511
512 # convert key from gpg to ssh authorized_keys format
513 gpg2authorized_keys() {
514     local userID
515     local keyID
516     local key
517
518     userID="$1"
519     keyID="$2"
520
521     key=$(gpg2ssh "$keyID")
522
523     # NOTE: just in case, the COMMENT can be matched with the
524     # following regexp:
525     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
526     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
527 }
528
529 ### GPG UTILITIES
530
531 # retrieve all keys with given user id from keyserver
532 # FIXME: need to figure out how to retrieve all matching keys
533 # (not just first N (5 in this case))
534 gpg_fetch_userid() {
535     local returnCode=0
536     local userID
537
538     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
539         return 0
540     fi
541
542     userID="$1"
543
544     log verbose " checking keyserver $KEYSERVER... "
545     echo 1,2,3,4,5 | \
546         gpg --quiet --batch --with-colons \
547         --command-fd 0 --keyserver "$KEYSERVER" \
548         --search ="$userID" > /dev/null 2>&1
549     returnCode="$?"
550
551     return "$returnCode"
552 }
553
554 ########################################################################
555 ### PROCESSING FUNCTIONS
556
557 # userid and key policy checking
558 # the following checks policy on the returned keys
559 # - checks that full key has appropriate valididy (u|f)
560 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
561 # - checks that requested user ID has appropriate validity
562 # (see /usr/share/doc/gnupg/DETAILS.gz)
563 # output is one line for every found key, in the following format:
564 #
565 # flag:sshKey
566 #
567 # "flag" is an acceptability flag, 0 = ok, 1 = bad
568 # "sshKey" is the translated gpg key
569 #
570 # all log output must go to stderr, as stdout is used to pass the
571 # flag:sshKey to the calling function.
572 #
573 # expects global variable: "MODE"
574 process_user_id() {
575     local returnCode=0
576     local userID
577     local requiredCapability
578     local requiredPubCapability
579     local gpgOut
580     local type
581     local validity
582     local keyid
583     local uidfpr
584     local usage
585     local keyOK
586     local uidOK
587     local lastKey
588     local lastKeyOK
589     local fingerprint
590
591     userID="$1"
592
593     # set the required key capability based on the mode
594     if [ "$MODE" = 'known_hosts' ] ; then
595         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
596     elif [ "$MODE" = 'authorized_keys' ] ; then
597         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
598     fi
599     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
600
601     # fetch the user ID if necessary/requested
602     gpg_fetch_userid "$userID"
603
604     # output gpg info for (exact) userid and store
605     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
606         --with-fingerprint --with-fingerprint \
607         ="$userID" 2>/dev/null) || returnCode="$?"
608
609     # if the gpg query return code is not 0, return 1
610     if [ "$returnCode" -ne 0 ] ; then
611         log verbose " no primary keys found."
612         return 1
613     fi
614
615     # loop over all lines in the gpg output and process.
616     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
617     while IFS=: read -r type validity keyid uidfpr usage ; do
618         # process based on record type
619         case $type in
620             'pub') # primary keys
621                 # new key, wipe the slate
622                 keyOK=
623                 uidOK=
624                 lastKey=pub
625                 lastKeyOK=
626                 fingerprint=
627
628                 log verbose " primary key found: $keyid"
629
630                 # if overall key is not valid, skip
631                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
632                     log debug "  - unacceptable primary key validity ($validity)."
633                     continue
634                 fi
635                 # if overall key is disabled, skip
636                 if check_capability "$usage" 'D' ; then
637                     log debug "  - key disabled."
638                     continue
639                 fi
640                 # if overall key capability is not ok, skip
641                 if ! check_capability "$usage" $requiredPubCapability ; then
642                     log debug "  - unacceptable primary key capability ($usage)."
643                     continue
644                 fi
645
646                 # mark overall key as ok
647                 keyOK=true
648
649                 # mark primary key as ok if capability is ok
650                 if check_capability "$usage" $requiredCapability ; then
651                     lastKeyOK=true
652                 fi
653                 ;;
654             'uid') # user ids
655                 if [ "$lastKey" != pub ] ; then
656                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
657                     continue
658                 fi
659                 # if an acceptable user ID was already found, skip
660                 if [ "$uidOK" = 'true' ] ; then
661                     continue
662                 fi
663                 # if the user ID does matches...
664                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
665                     # and the user ID validity is ok
666                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
667                         # mark user ID acceptable
668                         uidOK=true
669                     else
670                         log debug "  - unacceptable user ID validity ($validity)."
671                     fi
672                 else
673                     continue
674                 fi
675
676                 # output a line for the primary key
677                 # 0 = ok, 1 = bad
678                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
679                     log verbose "  * acceptable primary key."
680                     if [ -z "$sshKey" ] ; then
681                         log error "    ! primary key could not be translated (not RSA?)."
682                     else
683                         echo "0:${sshKey}"
684                     fi
685                 else
686                     log debug "  - unacceptable primary key."
687                     if [ -z "$sshKey" ] ; then
688                         log debug "    ! primary key could not be translated (not RSA?)."
689                     else
690                         echo "1:${sshKey}"
691                     fi
692                 fi
693                 ;;
694             'sub') # sub keys
695                 # unset acceptability of last key
696                 lastKey=sub
697                 lastKeyOK=
698                 fingerprint=
699                 
700                 # don't bother with sub keys if the primary key is not valid
701                 if [ "$keyOK" != true ] ; then
702                     continue
703                 fi
704
705                 # don't bother with sub keys if no user ID is acceptable:
706                 if [ "$uidOK" != true ] ; then
707                     continue
708                 fi
709                 
710                 # if sub key validity is not ok, skip
711                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
712                     log debug "  - unacceptable sub key validity ($validity)."
713                     continue
714                 fi
715                 # if sub key capability is not ok, skip
716                 if ! check_capability "$usage" $requiredCapability ; then
717                     log debug "  - unacceptable sub key capability ($usage)."
718                     continue
719                 fi
720
721                 # mark sub key as ok
722                 lastKeyOK=true
723                 ;;
724             'fpr') # key fingerprint
725                 fingerprint="$uidfpr"
726
727                 sshKey=$(gpg2ssh "$fingerprint")
728
729                 # if the last key was the pub key, skip
730                 if [ "$lastKey" = pub ] ; then
731                     continue
732                 fi
733
734                 # output a line for the sub key
735                 # 0 = ok, 1 = bad
736                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
737                     log verbose "  * acceptable sub key."
738                     if [ -z "$sshKey" ] ; then
739                         log error "    ! sub key could not be translated (not RSA?)."
740                     else
741                         echo "0:${sshKey}"
742                     fi
743                 else
744                     log debug "  - unacceptable sub key."
745                     if [ -z "$sshKey" ] ; then
746                         log debug "    ! sub key could not be translated (not RSA?)."
747                     else
748                         echo "1:${sshKey}"
749                     fi
750                 fi
751                 ;;
752         esac
753     done | sort -t: -k1 -n -r
754     # NOTE: this last sort is important so that the "good" keys (key
755     # flag '0') come last.  This is so that they take precedence when
756     # being processed in the key files over "bad" keys (key flag '1')
757 }
758
759 # process a single host in the known_host file
760 process_host_known_hosts() {
761     local host
762     local userID
763     local noKey=
764     local nKeys
765     local nKeysOK
766     local ok
767     local sshKey
768     local tmpfile
769
770     # set the key processing mode
771     export MODE='known_hosts'
772
773     host="$1"
774     userID="ssh://${host}"
775
776     log verbose "processing: $host"
777
778     nKeys=0
779     nKeysOK=0
780
781     IFS=$'\n'
782     for line in $(process_user_id "${userID}") ; do
783         # note that key was found
784         nKeys=$((nKeys+1))
785
786         ok=$(echo "$line" | cut -d: -f1)
787         sshKey=$(echo "$line" | cut -d: -f2)
788
789         if [ -z "$sshKey" ] ; then
790             continue
791         fi
792
793         # remove any old host key line, and note if removed nothing is
794         # removed
795         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
796
797         # if key OK, add new host line
798         if [ "$ok" -eq '0' ] ; then
799             # note that key was found ok
800             nKeysOK=$((nKeysOK+1))
801
802             # hash if specified
803             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
804                 # FIXME: this is really hackish cause ssh-keygen won't
805                 # hash from stdin to stdout
806                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
807                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
808                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
809                 cat "$tmpfile" >> "$KNOWN_HOSTS"
810                 rm -f "$tmpfile" "${tmpfile}.old"
811             else
812                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
813             fi
814
815             # log if this is a new key to the known_hosts file
816             if [ "$noKey" ] ; then
817                 log info "* new key for $host added to known_hosts file."
818             fi
819         fi
820     done
821
822     # if at least one key was found...
823     if [ "$nKeys" -gt 0 ] ; then
824         # if ok keys were found, return 0
825         if [ "$nKeysOK" -gt 0 ] ; then
826             return 0
827         # else return 2
828         else
829             return 2
830         fi
831     # if no keys were found, return 1
832     else
833         return 1
834     fi
835 }
836
837 # update the known_hosts file for a set of hosts listed on command
838 # line
839 update_known_hosts() {
840     local returnCode=0
841     local nHosts
842     local nHostsOK
843     local nHostsBAD
844     local fileCheck
845     local host
846
847     # the number of hosts specified on command line
848     nHosts="$#"
849
850     nHostsOK=0
851     nHostsBAD=0
852
853     # touch the known_hosts file so that the file permission check
854     # below won't fail upon not finding the file
855     (umask 0022 && touch "$KNOWN_HOSTS")
856
857     # check permissions on the known_hosts file path
858     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" || failure
859
860     # create a lockfile on known_hosts:
861     lock create "$KNOWN_HOSTS"
862     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
863     trap "lock remove $KNOWN_HOSTS" EXIT
864
865     # note pre update file checksum
866     fileCheck="$(file_hash "$KNOWN_HOSTS")"
867
868     for host ; do
869         # process the host
870         process_host_known_hosts "$host" || returnCode="$?"
871         # note the result
872         case "$returnCode" in
873             0)
874                 nHostsOK=$((nHostsOK+1))
875                 ;;
876             2)
877                 nHostsBAD=$((nHostsBAD+1))
878                 ;;
879         esac
880
881         # touch the lockfile, for good measure.
882         lock touch "$KNOWN_HOSTS"
883     done
884
885     # remove the lockfile and the trap
886     lock remove "$KNOWN_HOSTS"
887     trap - EXIT
888
889     # note if the known_hosts file was updated
890     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
891         log debug "known_hosts file updated."
892     fi
893
894     # if an acceptable host was found, return 0
895     if [ "$nHostsOK" -gt 0 ] ; then
896         return 0
897     # else if no ok hosts were found...
898     else
899         # if no bad host were found then no hosts were found at all,
900         # and return 1
901         if [ "$nHostsBAD" -eq 0 ] ; then
902             return 1
903         # else if at least one bad host was found, return 2
904         else
905             return 2
906         fi
907     fi
908 }
909
910 # process hosts from a known_hosts file
911 process_known_hosts() {
912     local hosts
913
914     # exit if the known_hosts file does not exist
915     if [ ! -e "$KNOWN_HOSTS" ] ; then
916         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
917     fi
918
919     log debug "processing known_hosts file:"
920     log debug " $KNOWN_HOSTS"
921
922     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
923
924     if [ -z "$hosts" ] ; then
925         log debug "no hosts to process."
926         return
927     fi
928
929     # take all the hosts from the known_hosts file (first
930     # field), grep out all the hashed hosts (lines starting
931     # with '|')...
932     update_known_hosts $hosts
933 }
934
935 # process uids for the authorized_keys file
936 process_uid_authorized_keys() {
937     local userID
938     local nKeys
939     local nKeysOK
940     local ok
941     local sshKey
942
943     # set the key processing mode
944     export MODE='authorized_keys'
945
946     userID="$1"
947
948     log verbose "processing: $userID"
949
950     nKeys=0
951     nKeysOK=0
952
953     IFS=$'\n'
954     for line in $(process_user_id "$userID") ; do
955         # note that key was found
956         nKeys=$((nKeys+1))
957
958         ok=$(echo "$line" | cut -d: -f1)
959         sshKey=$(echo "$line" | cut -d: -f2)
960
961         if [ -z "$sshKey" ] ; then
962             continue
963         fi
964
965         # remove the old host key line
966         remove_line "$AUTHORIZED_KEYS" "$sshKey"
967
968         # if key OK, add new host line
969         if [ "$ok" -eq '0' ] ; then
970             # note that key was found ok
971             nKeysOK=$((nKeysOK+1))
972
973             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
974         fi
975     done
976
977     # if at least one key was found...
978     if [ "$nKeys" -gt 0 ] ; then
979         # if ok keys were found, return 0
980         if [ "$nKeysOK" -gt 0 ] ; then
981             return 0
982         # else return 2
983         else
984             return 2
985         fi
986     # if no keys were found, return 1
987     else
988         return 1
989     fi
990 }
991
992 # update the authorized_keys files from a list of user IDs on command
993 # line
994 update_authorized_keys() {
995     local returnCode=0
996     local userID
997     local nIDs
998     local nIDsOK
999     local nIDsBAD
1000     local fileCheck
1001
1002     # the number of ids specified on command line
1003     nIDs="$#"
1004
1005     nIDsOK=0
1006     nIDsBAD=0
1007
1008     log debug "updating authorized_keys file:"
1009     log debug " $AUTHORIZED_KEYS"
1010
1011     # check permissions on the authorized_keys file path
1012     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1013
1014     # create a lockfile on authorized_keys
1015     lock create "$AUTHORIZED_KEYS"
1016     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1017     trap "lock remove $AUTHORIZED_KEYS" EXIT
1018
1019     # note pre update file checksum
1020     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1021
1022     # remove any monkeysphere lines from authorized_keys file
1023     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1024
1025     for userID ; do
1026         # process the user ID, change return code if key not found for
1027         # user ID
1028         process_uid_authorized_keys "$userID" || returnCode="$?"
1029
1030         # note the result
1031         case "$returnCode" in
1032             0)
1033                 nIDsOK=$((nIDsOK+1))
1034                 ;;
1035             2)
1036                 nIDsBAD=$((nIDsBAD+1))
1037                 ;;
1038         esac
1039
1040         # touch the lockfile, for good measure.
1041         lock touch "$AUTHORIZED_KEYS"
1042     done
1043
1044     # remove the lockfile and the trap
1045     lock remove "$AUTHORIZED_KEYS"
1046
1047     # remove the trap
1048     trap - EXIT
1049
1050     # note if the authorized_keys file was updated
1051     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1052         log debug "authorized_keys file updated."
1053     fi
1054
1055     # if an acceptable id was found, return 0
1056     if [ "$nIDsOK" -gt 0 ] ; then
1057         return 0
1058     # else if no ok ids were found...
1059     else
1060         # if no bad ids were found then no ids were found at all, and
1061         # return 1
1062         if [ "$nIDsBAD" -eq 0 ] ; then
1063             return 1
1064         # else if at least one bad id was found, return 2
1065         else
1066             return 2
1067         fi
1068     fi
1069 }
1070
1071 # process an authorized_user_ids file for authorized_keys
1072 process_authorized_user_ids() {
1073     local line
1074     local nline
1075     local userIDs
1076
1077     authorizedUserIDs="$1"
1078
1079     # exit if the authorized_user_ids file is empty
1080     if [ ! -e "$authorizedUserIDs" ] ; then
1081         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1082     fi
1083
1084     log debug "processing authorized_user_ids file:"
1085     log debug " $authorizedUserIDs"
1086
1087     # check permissions on the authorized_user_ids file path
1088     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1089
1090     if ! meat "$authorizedUserIDs" > /dev/null ; then
1091         log debug " no user IDs to process."
1092         return
1093     fi
1094
1095     nline=0
1096
1097     # extract user IDs from authorized_user_ids file
1098     IFS=$'\n'
1099     for line in $(meat "$authorizedUserIDs") ; do
1100         userIDs["$nline"]="$line"
1101         nline=$((nline+1))
1102     done
1103
1104     update_authorized_keys "${userIDs[@]}"
1105 }
1106
1107 # takes a gpg key or keys on stdin, and outputs a list of
1108 # fingerprints, one per line:
1109 list_primary_fingerprints() {
1110     local fake=$(msmktempdir)
1111     GNUPGHOME="$fake" gpg --no-tty --quiet --import
1112     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1113         awk -F: '/^fpr:/{ print $10 }'
1114     rm -rf "$fake"
1115 }
1116
1117
1118 check_cruft_file() {
1119     local loc="$1"
1120     local version="$2"
1121     
1122     if [ -e "$loc" ] ; then
1123         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
1124     fi
1125 }
1126
1127 check_upgrade_dir() {
1128     local loc="$1"
1129     local version="$2"
1130
1131     if [ -d "$loc" ] ; then
1132         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
1133     fi
1134 }
1135
1136 ## look for cruft from old versions of the monkeysphere, and notice if
1137 ## upgrades have not been run:
1138 report_cruft() {
1139     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1140     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1141
1142     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1143     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1144
1145     local found=
1146     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1147         if [ -d "$foo" ] ; then
1148             printf "! %s\n" "$foo" | log info
1149             found=true
1150         fi
1151     done
1152     if [ "$found" ] ; then
1153         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
1154     fi
1155 }