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