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