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