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