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