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