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