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