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