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