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