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