removed test_gnu_dummy_s2k_extension(); no longer necessary
[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 ### COMMON VARIABLES
18
19 # managed directories
20 SYSCONFIGDIR=${MONKEYSPHERE_SYSCONFIGDIR:-"/etc/monkeysphere"}
21 export SYSCONFIGDIR
22
23 # default log level
24 LOG_LEVEL="INFO"
25
26 # default keyserver
27 KEYSERVER="pool.sks-keyservers.net"
28
29 # whether or not to check keyservers by defaul
30 CHECK_KEYSERVER="true"
31
32 # default monkeysphere user
33 MONKEYSPHERE_USER="monkeysphere"
34
35 # default about whether or not to prompt
36 PROMPT="true"
37
38 ########################################################################
39 ### UTILITY FUNCTIONS
40
41 # output version info
42 version() {
43     cat "${SYSSHAREDIR}/VERSION"
44 }
45
46 # failure function.  exits with code 255, unless specified otherwise.
47 failure() {
48     [ "$1" ] && echo "$1" >&2
49     exit ${2:-'255'}
50 }
51
52 # write output to stderr based on specified LOG_LEVEL the first
53 # parameter is the priority of the output, and everything else is what
54 # is echoed to stderr.  If there is nothing else, then output comes
55 # from stdin, and is not prefaced by log prefix.
56 log() {
57     local priority
58     local level
59     local output
60     local alllevels
61     local found=
62
63     # don't include SILENT in alllevels: it's handled separately
64     # list in decreasing verbosity (all caps).
65     # separate with $IFS explicitly, since we do some fancy footwork
66     # elsewhere.
67     alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
68
69     # translate lowers to uppers in global log level
70     LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
71
72     # just go ahead and return if the log level is silent
73     if [ "$LOG_LEVEL" = 'SILENT' ] ; then
74         return
75     fi
76
77     for level in $alllevels ; do 
78         if [ "$LOG_LEVEL" = "$level" ] ; then
79             found=true
80         fi
81     done
82     if [ -z "$found" ] ; then
83         # default to INFO:
84         LOG_LEVEL=INFO
85     fi
86
87     # get priority from first parameter, translating all lower to
88     # uppers
89     priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
90     shift
91
92     # scan over available levels
93     for level in $alllevels ; do
94         # output if the log level matches, set output to true
95         # this will output for all subsequent loops as well.
96         if [ "$LOG_LEVEL" = "$level" ] ; then
97             output=true
98         fi
99         if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
100             if [ "$1" ] ; then
101                 echo -n "ms: " >&2
102                 echo "$@" >&2
103             else
104                 cat >&2
105             fi
106         fi
107     done
108 }
109
110 # run command as monkeysphere user
111 su_monkeysphere_user() {
112     # our main goal here is to run the given command as the the
113     # monkeysphere user, but without prompting for any sort of
114     # authentication.  If this is not possible, we should just fail.
115
116     # FIXME: our current implementation is overly restrictive, because
117     # there may be some su PAM configurations that would allow su
118     # "$MONKEYSPHERE_USER" -c "$@" to Just Work without prompting,
119     # allowing specific users to invoke commands which make use of
120     # this user.
121
122     # chpst (from runit) would be nice to use, but we don't want to
123     # introduce an extra dependency just for this.  This may be a
124     # candidate for re-factoring if we switch implementation languages.
125
126     case $(id -un) in
127         # if monkeysphere user, run the command under bash
128         "$MONKEYSPHERE_USER")
129             bash -c "$@"
130             ;;
131
132          # if root, su command as monkeysphere user
133         'root')
134             su "$MONKEYSPHERE_USER" -c "$@"
135             ;;
136
137         # otherwise, fail
138         *)
139             log error "non-privileged user."
140             ;;
141     esac
142 }
143
144 # cut out all comments(#) and blank lines from standard input
145 meat() {
146     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
147 }
148
149 # cut a specified line from standard input
150 cutline() {
151     head --line="$1" "$2" | tail -1
152 }
153
154 # make a temporary directory
155 msmktempdir() {
156     mktemp -d ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
157 }
158
159 # make a temporary file
160 msmktempfile() {
161     mktemp ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
162 }
163
164 # this is a wrapper for doing lock functions.
165 #
166 # it lets us depend on either lockfile-progs (preferred) or procmail's
167 # lockfile, and should
168 lock() {
169     local use_lockfileprogs=true
170     local action="$1"
171     local file="$2"
172
173     if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
174         if ! ( which lockfile >/dev/null ); then
175             failure "Neither lockfile-create nor lockfile are in the path!"
176         fi
177         use_lockfileprogs=
178     fi
179     
180     case "$action" in
181         create)
182             if [ -n "$use_lockfileprogs" ] ; then
183                 lockfile-create "$file" || failure "unable to lock '$file'"
184             else
185                 lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
186             fi
187             log debug "lock created on '$file'."
188             ;;
189         touch)  
190             if [ -n "$use_lockfileprogs" ] ; then
191                 lockfile-touch --oneshot "$file"
192             else
193                 : Nothing to do here
194             fi
195             log debug "lock touched on '$file'."
196             ;;
197         remove)
198             if [ -n "$use_lockfileprogs" ] ; then
199                 lockfile-remove "$file"
200             else
201                 rm -f "${file}.lock"
202             fi
203             log debug "lock removed on '$file'."
204             ;;
205         *)
206             failure "bad argument for lock subfunction '$action'"
207     esac
208 }
209
210
211 # for portability, between gnu date and BSD date.
212 # arguments should be:  number longunits format
213
214 # e.g. advance_date 20 seconds +%F
215 advance_date() {
216     local gnutry
217     local number="$1"
218     local longunits="$2"
219     local format="$3"
220     local shortunits
221
222     # try things the GNU way first 
223     if date -d "$number $longunits" "$format" >/dev/null 2>&1; then
224         date -d "$number $longunits" "$format"
225     else
226         # otherwise, convert to (a limited version of) BSD date syntax:
227         case "$longunits" in
228             years)
229                 shortunits=y
230                 ;;
231             months)
232                 shortunits=m
233                 ;;
234             weeks)
235                 shortunits=w
236                 ;;
237             days)
238                 shortunits=d
239                 ;;
240             hours)
241                 shortunits=H
242                 ;;
243             minutes)
244                 shortunits=M
245                 ;;
246             seconds)
247                 shortunits=S
248                 ;;
249             *)
250                 # this is a longshot, and will likely fail; oh well.
251                 shortunits="$longunits"
252         esac
253         date "-v+${number}${shortunits}" "$format"
254     fi
255 }
256
257
258 # check that characters are in a string (in an AND fashion).
259 # used for checking key capability
260 # check_capability capability a [b...]
261 check_capability() {
262     local usage
263     local capcheck
264
265     usage="$1"
266     shift 1
267
268     for capcheck ; do
269         if echo "$usage" | grep -q -v "$capcheck" ; then
270             return 1
271         fi
272     done
273     return 0
274 }
275
276 # hash of a file
277 file_hash() {
278     md5sum "$1" 2> /dev/null
279 }
280
281 # convert escaped characters in pipeline from gpg output back into
282 # original character
283 # FIXME: undo all escape character translation in with-colons gpg
284 # output
285 gpg_unescape() {
286     sed 's/\\x3a/:/g'
287 }
288
289 # convert nasty chars into gpg-friendly form in pipeline
290 # FIXME: escape everything, not just colons!
291 gpg_escape() {
292     sed 's/:/\\x3a/g'
293 }
294
295 # prompt for GPG-formatted expiration, and emit result on stdout
296 get_gpg_expiration() {
297     local keyExpire
298
299     keyExpire="$1"
300
301     if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
302         cat >&2 <<EOF
303 Please specify how long the key should be valid.
304          0 = key does not expire
305       <n>  = key expires in n days
306       <n>w = key expires in n weeks
307       <n>m = key expires in n months
308       <n>y = key expires in n years
309 EOF
310         while [ -z "$keyExpire" ] ; do
311             read -p "Key is valid for? (0) " keyExpire
312             if ! test_gpg_expire ${keyExpire:=0} ; then
313                 echo "invalid value" >&2
314                 unset keyExpire
315             fi
316         done
317     elif ! test_gpg_expire "$keyExpire" ; then
318         failure "invalid key expiration value '$keyExpire'."
319     fi
320         
321     echo "$keyExpire"
322 }
323
324 passphrase_prompt() {
325     local prompt="$1"
326     local fifo="$2"
327     local PASS
328
329     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
330         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
331     else
332         read -s -p "$prompt" PASS
333         # Uses the builtin echo, so should not put the passphrase into
334         # the process table.  I think. --dkg
335         echo "$PASS" > "$fifo"
336     fi
337 }
338
339 # remove all lines with specified string from specified file
340 remove_line() {
341     local file
342     local string
343     local tempfile
344
345     file="$1"
346     string="$2"
347
348     if [ -z "$file" -o -z "$string" ] ; then
349         return 1
350     fi
351
352     if [ ! -e "$file" ] ; then
353         return 1
354     fi
355
356     # if the string is in the file...
357     if grep -q -F "$string" "$file" 2> /dev/null ; then
358         tempfile=$(mktemp "${file}.XXXXXXX") || \
359             failure "Unable to make temp file '${file}.XXXXXXX'"
360         
361         # remove the line with the string, and return 0
362         grep -v -F "$string" "$file" >"$tempfile"
363         cat "$tempfile" > "$file"
364         rm "$tempfile"
365         return 0
366     # otherwise return 1
367     else
368         return 1
369     fi
370 }
371
372 # remove all lines with MonkeySphere strings in file
373 remove_monkeysphere_lines() {
374     local file
375     local tempfile
376
377     file="$1"
378
379     if [ -z "$file" ] ; then
380         return 1
381     fi
382
383     if [ ! -e "$file" ] ; then
384         return 1
385     fi
386
387     tempfile=$(mktemp "${file}.XXXXXXX") || \
388         failure "Could not make temporary file '${file}.XXXXXXX'."
389
390     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
391         "$file" >"$tempfile"
392     cat "$tempfile" > "$file"
393     rm "$tempfile"
394 }
395
396 # translate ssh-style path variables %h and %u
397 translate_ssh_variables() {
398     local uname
399     local home
400
401     uname="$1"
402     path="$2"
403
404     # get the user's home directory
405     userHome=$(getent passwd "$uname" | cut -d: -f6)
406
407     # translate '%u' to user name
408     path=${path/\%u/"$uname"}
409     # translate '%h' to user home directory
410     path=${path/\%h/"$userHome"}
411
412     echo "$path"
413 }
414
415 # test that a string to conforms to GPG's expiration format
416 test_gpg_expire() {
417     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
418 }
419
420 # check that a file is properly owned, and that all it's parent
421 # directories are not group/other writable
422 check_key_file_permissions() {
423     local uname
424     local path
425     local stat
426     local access
427     local gAccess
428     local oAccess
429
430     # function to check that the given permission corresponds to writability
431     is_write() {
432         [ "$1" = "w" ]
433     }
434
435     uname="$1"
436     path="$2"
437
438     log debug "checking path permission '$path'..."
439
440     # return 255 if cannot stat file
441     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
442         log error "could not stat path '$path'."
443         return 255
444     fi
445
446     owner=$(echo "$stat" | awk '{ print $3 }')
447     gAccess=$(echo "$stat" | cut -c6)
448     oAccess=$(echo "$stat" | cut -c9)
449
450     # return 1 if path has invalid owner
451     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
452         log error "improper ownership on path '$path'."
453         return 1
454     fi
455
456     # return 2 if path has group or other writability
457     if is_write "$gAccess" || is_write "$oAccess" ; then
458         log error "improper group or other writability on path '$path'."
459         return 2
460     fi
461
462     # return zero if all clear, or go to next path
463     if [ "$path" = '/' ] ; then
464         return 0
465     else
466         check_key_file_permissions "$uname" $(dirname "$path")
467     fi
468 }
469
470 ### CONVERSION UTILITIES
471
472 # output the ssh key for a given key ID
473 gpg2ssh() {
474     local keyID
475     
476     keyID="$1"
477
478     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
479 }
480
481 # output known_hosts line from ssh key
482 ssh2known_hosts() {
483     local host
484     local key
485
486     host="$1"
487     key="$2"
488
489     echo -n "$host "
490     echo -n "$key" | tr -d '\n'
491     echo " MonkeySphere${DATE}"
492 }
493
494 # output authorized_keys line from ssh key
495 ssh2authorized_keys() {
496     local userID
497     local key
498     
499     userID="$1"
500     key="$2"
501
502     echo -n "$key" | tr -d '\n'
503     echo " MonkeySphere${DATE} ${userID}"
504 }
505
506 # convert key from gpg to ssh known_hosts format
507 gpg2known_hosts() {
508     local host
509     local keyID
510
511     host="$1"
512     keyID="$2"
513
514     # NOTE: it seems that ssh-keygen -R removes all comment fields from
515     # all lines in the known_hosts file.  why?
516     # NOTE: just in case, the COMMENT can be matched with the
517     # following regexp:
518     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
519     echo -n "$host "
520     gpg2ssh "$keyID" | tr -d '\n'
521     echo " MonkeySphere${DATE}"
522 }
523
524 # convert key from gpg to ssh authorized_keys format
525 gpg2authorized_keys() {
526     local userID
527     local keyID
528
529     userID="$1"
530     keyID="$2"
531
532     # NOTE: just in case, the COMMENT can be matched with the
533     # following regexp:
534     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
535     gpg2ssh "$keyID" | tr -d '\n'
536     echo " MonkeySphere${DATE} ${userID}"
537 }
538
539 ### GPG UTILITIES
540
541 # retrieve all keys with given user id from keyserver
542 # FIXME: need to figure out how to retrieve all matching keys
543 # (not just first N (5 in this case))
544 gpg_fetch_userid() {
545     local returnCode=0
546     local userID
547
548     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
549         return 0
550     fi
551
552     userID="$1"
553
554     log verbose " checking keyserver $KEYSERVER... "
555     echo 1,2,3,4,5 | \
556         gpg --quiet --batch --with-colons \
557         --command-fd 0 --keyserver "$KEYSERVER" \
558         --search ="$userID" > /dev/null 2>&1
559     returnCode="$?"
560
561     return "$returnCode"
562 }
563
564 ########################################################################
565 ### PROCESSING FUNCTIONS
566
567 # userid and key policy checking
568 # the following checks policy on the returned keys
569 # - checks that full key has appropriate valididy (u|f)
570 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
571 # - checks that requested user ID has appropriate validity
572 # (see /usr/share/doc/gnupg/DETAILS.gz)
573 # output is one line for every found key, in the following format:
574 #
575 # flag:sshKey
576 #
577 # "flag" is an acceptability flag, 0 = ok, 1 = bad
578 # "sshKey" is the translated gpg key
579 #
580 # all log output must go to stderr, as stdout is used to pass the
581 # flag:sshKey to the calling function.
582 #
583 # expects global variable: "MODE"
584 process_user_id() {
585     local returnCode=0
586     local userID
587     local requiredCapability
588     local requiredPubCapability
589     local gpgOut
590     local type
591     local validity
592     local keyid
593     local uidfpr
594     local usage
595     local keyOK
596     local uidOK
597     local lastKey
598     local lastKeyOK
599     local fingerprint
600
601     userID="$1"
602
603     # set the required key capability based on the mode
604     if [ "$MODE" = 'known_hosts' ] ; then
605         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
606     elif [ "$MODE" = 'authorized_keys' ] ; then
607         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
608     fi
609     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
610
611     # fetch the user ID if necessary/requested
612     gpg_fetch_userid "$userID"
613
614     # output gpg info for (exact) userid and store
615     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
616         --with-fingerprint --with-fingerprint \
617         ="$userID" 2>/dev/null) || returnCode="$?"
618
619     # if the gpg query return code is not 0, return 1
620     if [ "$returnCode" -ne 0 ] ; then
621         log verbose " no primary keys found."
622         return 1
623     fi
624
625     # loop over all lines in the gpg output and process.
626     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
627     while IFS=: read -r type validity keyid uidfpr usage ; do
628         # process based on record type
629         case $type in
630             'pub') # primary keys
631                 # new key, wipe the slate
632                 keyOK=
633                 uidOK=
634                 lastKey=pub
635                 lastKeyOK=
636                 fingerprint=
637
638                 log verbose " primary key found: $keyid"
639
640                 # if overall key is not valid, skip
641                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
642                     log debug "  - unacceptable primary key validity ($validity)."
643                     continue
644                 fi
645                 # if overall key is disabled, skip
646                 if check_capability "$usage" 'D' ; then
647                     log debug "  - key disabled."
648                     continue
649                 fi
650                 # if overall key capability is not ok, skip
651                 if ! check_capability "$usage" $requiredPubCapability ; then
652                     log debug "  - unacceptable primary key capability ($usage)."
653                     continue
654                 fi
655
656                 # mark overall key as ok
657                 keyOK=true
658
659                 # mark primary key as ok if capability is ok
660                 if check_capability "$usage" $requiredCapability ; then
661                     lastKeyOK=true
662                 fi
663                 ;;
664             'uid') # user ids
665                 if [ "$lastKey" != pub ] ; then
666                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
667                     continue
668                 fi
669                 # if an acceptable user ID was already found, skip
670                 if [ "$uidOK" = 'true' ] ; then
671                     continue
672                 fi
673                 # if the user ID does matches...
674                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
675                     # and the user ID validity is ok
676                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
677                         # mark user ID acceptable
678                         uidOK=true
679                     else
680                         log debug "  - unacceptable user ID validity ($validity)."
681                     fi
682                 else
683                     continue
684                 fi
685
686                 # output a line for the primary key
687                 # 0 = ok, 1 = bad
688                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
689                     log verbose "  * acceptable primary key."
690                     if [ -z "$sshKey" ] ; then
691                         log error "    ! primary key could not be translated (not RSA or DSA?)."
692                     else
693                         echo "0:${sshKey}"
694                     fi
695                 else
696                     log debug "  - unacceptable primary key."
697                     if [ -z "$sshKey" ] ; then
698                         log debug "    ! primary key could not be translated (not RSA or DSA?)."
699                     else
700                         echo "1:${sshKey}"
701                     fi
702                 fi
703                 ;;
704             'sub') # sub keys
705                 # unset acceptability of last key
706                 lastKey=sub
707                 lastKeyOK=
708                 fingerprint=
709                 
710                 # don't bother with sub keys if the primary key is not valid
711                 if [ "$keyOK" != true ] ; then
712                     continue
713                 fi
714
715                 # don't bother with sub keys if no user ID is acceptable:
716                 if [ "$uidOK" != true ] ; then
717                     continue
718                 fi
719                 
720                 # if sub key validity is not ok, skip
721                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
722                     log debug "  - unacceptable sub key validity ($validity)."
723                     continue
724                 fi
725                 # if sub key capability is not ok, skip
726                 if ! check_capability "$usage" $requiredCapability ; then
727                     log debug "  - unacceptable sub key capability ($usage)."
728                     continue
729                 fi
730
731                 # mark sub key as ok
732                 lastKeyOK=true
733                 ;;
734             'fpr') # key fingerprint
735                 fingerprint="$uidfpr"
736
737                 sshKey=$(gpg2ssh "$fingerprint")
738
739                 # if the last key was the pub key, skip
740                 if [ "$lastKey" = pub ] ; then
741                     continue
742                 fi
743
744                 # output a line for the sub key
745                 # 0 = ok, 1 = bad
746                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
747                     log verbose "  * acceptable sub key."
748                     if [ -z "$sshKey" ] ; then
749                         log error "    ! sub key could not be translated (not RSA or DSA?)."
750                     else
751                         echo "0:${sshKey}"
752                     fi
753                 else
754                     log debug "  - unacceptable sub key."
755                     if [ -z "$sshKey" ] ; then
756                         log debug "    ! sub key could not be translated (not RSA or DSA?)."
757                     else
758                         echo "1:${sshKey}"
759                     fi
760                 fi
761                 ;;
762         esac
763     done | sort -t: -k1 -n -r
764     # NOTE: this last sort is important so that the "good" keys (key
765     # flag '0') come last.  This is so that they take precedence when
766     # being processed in the key files over "bad" keys (key flag '1')
767 }
768
769 # process a single host in the known_host file
770 process_host_known_hosts() {
771     local host
772     local userID
773     local noKey=
774     local nKeys
775     local nKeysOK
776     local ok
777     local sshKey
778     local tmpfile
779
780     # set the key processing mode
781     export MODE='known_hosts'
782
783     host="$1"
784     userID="ssh://${host}"
785
786     log verbose "processing: $host"
787
788     nKeys=0
789     nKeysOK=0
790
791     IFS=$'\n'
792     for line in $(process_user_id "${userID}") ; do
793         # note that key was found
794         nKeys=$((nKeys+1))
795
796         ok=$(echo "$line" | cut -d: -f1)
797         sshKey=$(echo "$line" | cut -d: -f2)
798
799         if [ -z "$sshKey" ] ; then
800             continue
801         fi
802
803         # remove any old host key line, and note if removed nothing is
804         # removed
805         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
806
807         # if key OK, add new host line
808         if [ "$ok" -eq '0' ] ; then
809             # note that key was found ok
810             nKeysOK=$((nKeysOK+1))
811
812             # hash if specified
813             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
814                 # FIXME: this is really hackish cause ssh-keygen won't
815                 # hash from stdin to stdout
816                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
817                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
818                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
819                 cat "$tmpfile" >> "$KNOWN_HOSTS"
820                 rm -f "$tmpfile" "${tmpfile}.old"
821             else
822                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
823             fi
824
825             # log if this is a new key to the known_hosts file
826             if [ "$noKey" ] ; then
827                 log info "* new key for $host added to known_hosts file."
828             fi
829         fi
830     done
831
832     # if at least one key was found...
833     if [ "$nKeys" -gt 0 ] ; then
834         # if ok keys were found, return 0
835         if [ "$nKeysOK" -gt 0 ] ; then
836             return 0
837         # else return 2
838         else
839             return 2
840         fi
841     # if no keys were found, return 1
842     else
843         return 1
844     fi
845 }
846
847 # update the known_hosts file for a set of hosts listed on command
848 # line
849 update_known_hosts() {
850     local returnCode=0
851     local nHosts
852     local nHostsOK
853     local nHostsBAD
854     local fileCheck
855     local host
856
857     # the number of hosts specified on command line
858     nHosts="$#"
859
860     nHostsOK=0
861     nHostsBAD=0
862
863     # touch the known_hosts file so that the file permission check
864     # below won't fail upon not finding the file
865     (umask 0022 && touch "$KNOWN_HOSTS")
866
867     # check permissions on the known_hosts file path
868     check_key_file_permissions "$USER" "$KNOWN_HOSTS" || failure
869
870     # create a lockfile on known_hosts:
871     lock create "$KNOWN_HOSTS"
872     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
873     trap "lock remove $KNOWN_HOSTS" EXIT
874
875     # note pre update file checksum
876     fileCheck="$(file_hash "$KNOWN_HOSTS")"
877
878     for host ; do
879         # process the host
880         process_host_known_hosts "$host" || returnCode="$?"
881         # note the result
882         case "$returnCode" in
883             0)
884                 nHostsOK=$((nHostsOK+1))
885                 ;;
886             2)
887                 nHostsBAD=$((nHostsBAD+1))
888                 ;;
889         esac
890
891         # touch the lockfile, for good measure.
892         lock touch "$KNOWN_HOSTS"
893     done
894
895     # remove the lockfile and the trap
896     lock remove "$KNOWN_HOSTS"
897     trap - EXIT
898
899     # note if the known_hosts file was updated
900     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
901         log debug "known_hosts file updated."
902     fi
903
904     # if an acceptable host was found, return 0
905     if [ "$nHostsOK" -gt 0 ] ; then
906         return 0
907     # else if no ok hosts were found...
908     else
909         # if no bad host were found then no hosts were found at all,
910         # and return 1
911         if [ "$nHostsBAD" -eq 0 ] ; then
912             return 1
913         # else if at least one bad host was found, return 2
914         else
915             return 2
916         fi
917     fi
918 }
919
920 # process hosts from a known_hosts file
921 process_known_hosts() {
922     local hosts
923
924     # exit if the known_hosts file does not exist
925     if [ ! -e "$KNOWN_HOSTS" ] ; then
926         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
927     fi
928
929     log debug "processing known_hosts file..."
930
931     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
932
933     if [ -z "$hosts" ] ; then
934         log debug "no hosts to process."
935         return
936     fi
937
938     # take all the hosts from the known_hosts file (first
939     # field), grep out all the hashed hosts (lines starting
940     # with '|')...
941     update_known_hosts $hosts
942 }
943
944 # process uids for the authorized_keys file
945 process_uid_authorized_keys() {
946     local userID
947     local nKeys
948     local nKeysOK
949     local ok
950     local sshKey
951
952     # set the key processing mode
953     export MODE='authorized_keys'
954
955     userID="$1"
956
957     log verbose "processing: $userID"
958
959     nKeys=0
960     nKeysOK=0
961
962     IFS=$'\n'
963     for line in $(process_user_id "$userID") ; do
964         # note that key was found
965         nKeys=$((nKeys+1))
966
967         ok=$(echo "$line" | cut -d: -f1)
968         sshKey=$(echo "$line" | cut -d: -f2)
969
970         if [ -z "$sshKey" ] ; then
971             continue
972         fi
973
974         # remove the old host key line
975         remove_line "$AUTHORIZED_KEYS" "$sshKey"
976
977         # if key OK, add new host line
978         if [ "$ok" -eq '0' ] ; then
979             # note that key was found ok
980             nKeysOK=$((nKeysOK+1))
981
982             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
983         fi
984     done
985
986     # if at least one key was found...
987     if [ "$nKeys" -gt 0 ] ; then
988         # if ok keys were found, return 0
989         if [ "$nKeysOK" -gt 0 ] ; then
990             return 0
991         # else return 2
992         else
993             return 2
994         fi
995     # if no keys were found, return 1
996     else
997         return 1
998     fi
999 }
1000
1001 # update the authorized_keys files from a list of user IDs on command
1002 # line
1003 update_authorized_keys() {
1004     local returnCode=0
1005     local userID
1006     local nIDs
1007     local nIDsOK
1008     local nIDsBAD
1009     local fileCheck
1010
1011     # the number of ids specified on command line
1012     nIDs="$#"
1013
1014     nIDsOK=0
1015     nIDsBAD=0
1016
1017     # check permissions on the authorized_keys file path
1018     check_key_file_permissions "$USER" "$AUTHORIZED_KEYS" || failure
1019
1020     # create a lockfile on authorized_keys
1021     lock create "$AUTHORIZED_KEYS"
1022     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1023     trap "lock remove $AUTHORIZED_KEYS" EXIT
1024
1025     # note pre update file checksum
1026     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1027
1028     # remove any monkeysphere lines from authorized_keys file
1029     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1030
1031     for userID ; do
1032         # process the user ID, change return code if key not found for
1033         # user ID
1034         process_uid_authorized_keys "$userID" || returnCode="$?"
1035
1036         # note the result
1037         case "$returnCode" in
1038             0)
1039                 nIDsOK=$((nIDsOK+1))
1040                 ;;
1041             2)
1042                 nIDsBAD=$((nIDsBAD+1))
1043                 ;;
1044         esac
1045
1046         # touch the lockfile, for good measure.
1047         lock touch "$AUTHORIZED_KEYS"
1048     done
1049
1050     # remove the lockfile and the trap
1051     lock remove "$AUTHORIZED_KEYS"
1052
1053     # remove the trap
1054     trap - EXIT
1055
1056     # note if the authorized_keys file was updated
1057     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1058         log debug "authorized_keys file updated."
1059     fi
1060
1061     # if an acceptable id was found, return 0
1062     if [ "$nIDsOK" -gt 0 ] ; then
1063         return 0
1064     # else if no ok ids were found...
1065     else
1066         # if no bad ids were found then no ids were found at all, and
1067         # return 1
1068         if [ "$nIDsBAD" -eq 0 ] ; then
1069             return 1
1070         # else if at least one bad id was found, return 2
1071         else
1072             return 2
1073         fi
1074     fi
1075 }
1076
1077 # process an authorized_user_ids file for authorized_keys
1078 process_authorized_user_ids() {
1079     local line
1080     local nline
1081     local userIDs
1082
1083     authorizedUserIDs="$1"
1084
1085     # exit if the authorized_user_ids file is empty
1086     if [ ! -e "$authorizedUserIDs" ] ; then
1087         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1088     fi
1089
1090     # check permissions on the authorized_user_ids file path
1091     check_key_file_permissions "$USER" "$authorizedUserIDs" || failure
1092
1093     log debug "processing authorized_user_ids file..."
1094
1095     if ! meat "$authorizedUserIDs" > /dev/null ; then
1096         log debug " no user IDs to process."
1097         return
1098     fi
1099
1100     nline=0
1101
1102     # extract user IDs from authorized_user_ids file
1103     IFS=$'\n'
1104     for line in $(meat "$authorizedUserIDs") ; do
1105         userIDs["$nline"]="$line"
1106         nline=$((nline+1))
1107     done
1108
1109     update_authorized_keys "${userIDs[@]}"
1110 }
1111
1112 # takes a gpg key or keys on stdin, and outputs a list of
1113 # fingerprints, one per line:
1114 list_primary_fingerprints() {
1115     local fake=$(msmktempdir)
1116     GNUPGHOME="$fake" gpg --no-tty --quiet --import
1117     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1118         awk -F: '/^fpr:/{ print $10 }'
1119     rm -rf "$fake"
1120 }
1121
1122
1123 check_cruft_file() {
1124     local loc="$1"
1125     local version="$2"
1126     
1127     if [ -e "$loc" ] ; then
1128         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
1129     fi
1130 }
1131
1132 check_upgrade_dir() {
1133     local loc="$1"
1134     local version="$2"
1135
1136     if [ -d "$loc" ] ; then
1137         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
1138     fi
1139 }
1140
1141 ## look for cruft from old versions of the monkeysphere, and notice if
1142 ## upgrades have not been run:
1143 report_cruft() {
1144     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1145     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1146
1147     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1148     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1149
1150     local found=
1151     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1152         if [ -d "$foo" ] ; then
1153             printf "! %s\n" "$foo" | log info
1154             found=true
1155         fi
1156     done
1157     if [ "$found" ] ; then
1158         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
1159     fi
1160 }