resyn implementation of m-s gen-key with the help output
[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 # monkeysphere version
23 VERSION=__VERSION__
24
25 ########################################################################
26 ### UTILITY FUNCTIONS
27
28 # failure function.  exits with code 255, unless specified otherwise.
29 failure() {
30     [ "$1" ] && echo "$1" >&2
31     exit ${2:-'255'}
32 }
33
34 # write output to stderr based on specified LOG_LEVEL the first
35 # parameter is the priority of the output, and everything else is what
36 # is echoed to stderr
37 log() {
38     local priority
39     local level
40     local output
41     local alllevels
42     local found=
43
44     # don't include SILENT in alllevels: it's handled separately
45     # list in decreasing verbosity (all caps).
46     # separate with $IFS explicitly, since we do some fancy footwork
47     # elsewhere.
48     alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
49
50     # translate lowers to uppers in global log level
51     LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
52
53     # just go ahead and return if the log level is silent
54     if [ "$LOG_LEVEL" = 'SILENT' ] ; then
55         return
56     fi
57
58     for level in $alllevels ; do 
59         if [ "$LOG_LEVEL" = "$level" ] ; then
60             found=true
61         fi
62     done
63     if [ -z "$found" ] ; then
64         # default to INFO:
65         LOG_LEVEL=INFO
66     fi
67
68     # get priority from first parameter, translating all lower to
69     # uppers
70     priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
71     shift
72
73     # scan over available levels
74     for level in $alllevels ; do
75         # output if the log level matches, set output to true
76         # this will output for all subsequent loops as well.
77         if [ "$LOG_LEVEL" = "$level" ] ; then
78             output=true
79         fi
80         if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
81             echo -n "ms: " >&2
82             echo "$@" >&2
83         fi
84     done
85 }
86
87 # cut out all comments(#) and blank lines from standard input
88 meat() {
89     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
90 }
91
92 # cut a specified line from standard input
93 cutline() {
94     head --line="$1" "$2" | tail -1
95 }
96
97 # this is a wrapper for doing lock functions.
98 #
99 # it lets us depend on either lockfile-progs (preferred) or procmail's
100 # lockfile, and should
101 lock() {
102     local use_lockfileprogs=true
103     local action="$1"
104     local file="$2"
105
106     if ! ( which lockfile-create >/dev/null 2>/dev/null ) ; then
107         if ! ( which lockfile >/dev/null ); then
108             failure "Neither lockfile-create nor lockfile are in the path!"
109         fi
110         use_lockfileprogs=
111     fi
112     
113     case "$action" in
114         create)
115             if [ -n "$use_lockfileprogs" ] ; then
116                 lockfile-create "$file" || failure "unable to lock '$file'"
117             else
118                 lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
119             fi
120             ;;
121         touch)  
122             if [ -n "$use_lockfileprogs" ] ; then
123                 lockfile-touch --oneshot "$file"
124             else
125                 : Nothing to do here
126             fi
127             ;;
128         remove)
129             if [ -n "$use_lockfileprogs" ] ; then
130                 lockfile-remove "$file"
131             else
132                 rm -f "${file}.lock"
133             fi
134             ;;
135         *)
136             failure "bad argument for lock subfunction '$action'"
137     esac
138 }
139
140
141 # for portability, between gnu date and BSD date.
142 # arguments should be:  number longunits format
143
144 # e.g. advance_date 20 seconds +%F
145 advance_date() {
146     local gnutry
147     local number="$1"
148     local longunits="$2"
149     local format="$3"
150     local shortunits
151
152     # try things the GNU way first 
153     if date -d "$number $longunits" "$format" >/dev/null 2>&1; then
154         date -d "$number $longunits" "$format"
155     else
156         # otherwise, convert to (a limited version of) BSD date syntax:
157         case "$longunits" in
158             years)
159                 shortunits=y
160                 ;;
161             months)
162                 shortunits=m
163                 ;;
164             weeks)
165                 shortunits=w
166                 ;;
167             days)
168                 shortunits=d
169                 ;;
170             hours)
171                 shortunits=H
172                 ;;
173             minutes)
174                 shortunits=M
175                 ;;
176             seconds)
177                 shortunits=S
178                 ;;
179             *)
180                 # this is a longshot, and will likely fail; oh well.
181                 shortunits="$longunits"
182         esac
183         date "-v+${number}${shortunits}" "$format"
184     fi
185 }
186
187
188 # check that characters are in a string (in an AND fashion).
189 # used for checking key capability
190 # check_capability capability a [b...]
191 check_capability() {
192     local usage
193     local capcheck
194
195     usage="$1"
196     shift 1
197
198     for capcheck ; do
199         if echo "$usage" | grep -q -v "$capcheck" ; then
200             return 1
201         fi
202     done
203     return 0
204 }
205
206 # hash of a file
207 file_hash() {
208     md5sum "$1" 2> /dev/null
209 }
210
211 # convert escaped characters in pipeline from gpg output back into
212 # original character
213 # FIXME: undo all escape character translation in with-colons gpg
214 # output
215 gpg_unescape() {
216     sed 's/\\x3a/:/g'
217 }
218
219 # convert nasty chars into gpg-friendly form in pipeline
220 # FIXME: escape everything, not just colons!
221 gpg_escape() {
222     sed 's/:/\\x3a/g'
223 }
224
225 # prompt for GPG-formatted expiration, and emit result on stdout
226 get_gpg_expiration() {
227     local keyExpire
228
229     keyExpire="$1"
230
231     if [ -z "$keyExpire" ]; then
232         cat >&2 <<EOF
233 Please specify how long the key should be valid.
234          0 = key does not expire
235       <n>  = key expires in n days
236       <n>w = key expires in n weeks
237       <n>m = key expires in n months
238       <n>y = key expires in n years
239 EOF
240         while [ -z "$keyExpire" ] ; do
241             read -p "Key is valid for? (0) " keyExpire
242             if ! test_gpg_expire ${keyExpire:=0} ; then
243                 echo "invalid value" >&2
244                 unset keyExpire
245             fi
246         done
247     elif ! test_gpg_expire "$keyExpire" ; then
248         failure "invalid key expiration value '$keyExpire'."
249     fi
250         
251     echo "$keyExpire"
252 }
253
254 passphrase_prompt() {
255     local prompt="$1"
256     local fifo="$2"
257     local PASS
258
259     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
260         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
261     else
262         read -s -p "$prompt" PASS
263         # Uses the builtin echo, so should not put the passphrase into
264         # the process table.  I think. --dkg
265         echo "$PASS" > "$fifo"
266     fi
267 }
268
269 test_gnu_dummy_s2k_extension() {
270
271 # this block contains a demonstration private key that has had the
272 # primary key stripped out using the GNU S2K extension known as
273 # "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
274 # present in cleartext, however.
275
276 # openpgp2ssh will be able to deal with this based on whether the
277 # local copy of GnuTLS contains read_s2k support that can handle it.
278
279 # read up on that here:
280
281 # http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
282
283 echo "
284 -----BEGIN PGP PRIVATE KEY BLOCK-----
285 Version: GnuPG v1.4.9 (GNU/Linux)
286
287 lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
288 xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
289 G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
290 /gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
291 biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
292 AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
293 ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
294 aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
295 g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
296 mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
297 8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
298 +bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
299 RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
300 4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
301 7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
302 ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
303 kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
304 Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
305 SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
306 vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
307 vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
308 vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
309 =CR5w
310 -----END PGP PRIVATE KEY BLOCK-----
311 " | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
312
313 }
314
315 # remove all lines with specified string from specified file
316 remove_line() {
317     local file
318     local string
319     local tempfile
320
321     file="$1"
322     string="$2"
323
324     if [ -z "$file" -o -z "$string" ] ; then
325         return 1
326     fi
327
328     if [ ! -e "$file" ] ; then
329         return 1
330     fi
331
332     # if the string is in the file...
333     if grep -q -F "$string" "$file" 2> /dev/null ; then
334         tempfile=$(mktemp "${file}.XXXXXXX") || \
335             failure "Unable to make temp file '${file}.XXXXXXX'"
336         
337         # remove the line with the string, and return 0
338         grep -v -F "$string" "$file" >"$tempfile"
339         cat "$tempfile" > "$file"
340         rm "$tempfile"
341         return 0
342     # otherwise return 1
343     else
344         return 1
345     fi
346 }
347
348 # remove all lines with MonkeySphere strings in file
349 remove_monkeysphere_lines() {
350     local file
351     local tempfile
352
353     file="$1"
354
355     if [ -z "$file" ] ; then
356         return 1
357     fi
358
359     if [ ! -e "$file" ] ; then
360         return 1
361     fi
362
363     tempfile=$(mktemp "${file}.XXXXXXX") || \
364         failure "Could not make temporary file '${file}.XXXXXXX'."
365
366     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
367         "$file" >"$tempfile"
368     cat "$tempfile" > "$file"
369     rm "$tempfile"
370 }
371
372 # translate ssh-style path variables %h and %u
373 translate_ssh_variables() {
374     local uname
375     local home
376
377     uname="$1"
378     path="$2"
379
380     # get the user's home directory
381     userHome=$(getent passwd "$uname" | cut -d: -f6)
382
383     # translate '%u' to user name
384     path=${path/\%u/"$uname"}
385     # translate '%h' to user home directory
386     path=${path/\%h/"$userHome"}
387
388     echo "$path"
389 }
390
391 # test that a string to conforms to GPG's expiration format
392 test_gpg_expire() {
393     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
394 }
395
396 # check that a file is properly owned, and that all it's parent
397 # directories are not group/other writable
398 check_key_file_permissions() {
399     local uname
400     local path
401     local stat
402     local access
403     local gAccess
404     local oAccess
405
406     # function to check that the given permission corresponds to writability
407     is_write() {
408         [ "$1" = "w" ]
409     }
410
411     uname="$1"
412     path="$2"
413
414     # return 255 if cannot stat file
415     if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
416         log error "could not stat path '$path'."
417         return 255
418     fi
419
420     owner=$(echo "$stat" | awk '{ print $3 }')
421     gAccess=$(echo "$stat" | cut -c6)
422     oAccess=$(echo "$stat" | cut -c9)
423
424     # return 1 if path has invalid owner
425     if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
426         log error "improper ownership on path '$path'."
427         return 1
428     fi
429
430     # return 2 if path has group or other writability
431     if is_write "$gAccess" || is_write "$oAccess" ; then
432         log error "improper group or other writability on path '$path'."
433         return 2
434     fi
435
436     # return zero if all clear, or go to next path
437     if [ "$path" = '/' ] ; then
438         return 0
439     else
440         check_key_file_permissions "$uname" $(dirname "$path")
441     fi
442 }
443
444 ### CONVERSION UTILITIES
445
446 # output the ssh key for a given key ID
447 gpg2ssh() {
448     local keyID
449     
450     keyID="$1"
451
452     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
453 }
454
455 # output known_hosts line from ssh key
456 ssh2known_hosts() {
457     local host
458     local key
459
460     host="$1"
461     key="$2"
462
463     echo -n "$host "
464     echo -n "$key" | tr -d '\n'
465     echo " MonkeySphere${DATE}"
466 }
467
468 # output authorized_keys line from ssh key
469 ssh2authorized_keys() {
470     local userID
471     local key
472     
473     userID="$1"
474     key="$2"
475
476     echo -n "$key" | tr -d '\n'
477     echo " MonkeySphere${DATE} ${userID}"
478 }
479
480 # convert key from gpg to ssh known_hosts format
481 gpg2known_hosts() {
482     local host
483     local keyID
484
485     host="$1"
486     keyID="$2"
487
488     # NOTE: it seems that ssh-keygen -R removes all comment fields from
489     # all lines in the known_hosts file.  why?
490     # NOTE: just in case, the COMMENT can be matched with the
491     # following regexp:
492     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
493     echo -n "$host "
494     gpg2ssh "$keyID" | tr -d '\n'
495     echo " MonkeySphere${DATE}"
496 }
497
498 # convert key from gpg to ssh authorized_keys format
499 gpg2authorized_keys() {
500     local userID
501     local keyID
502
503     userID="$1"
504     keyID="$2"
505
506     # NOTE: just in case, the COMMENT can be matched with the
507     # following regexp:
508     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
509     gpg2ssh "$keyID" | tr -d '\n'
510     echo " MonkeySphere${DATE} ${userID}"
511 }
512
513 ### GPG UTILITIES
514
515 # retrieve all keys with given user id from keyserver
516 # FIXME: need to figure out how to retrieve all matching keys
517 # (not just first N (5 in this case))
518 gpg_fetch_userid() {
519     local userID
520     local returnCode
521
522     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
523         return 0
524     fi
525
526     userID="$1"
527
528     log verbose " checking keyserver $KEYSERVER... "
529     echo 1,2,3,4,5 | \
530         gpg --quiet --batch --with-colons \
531         --command-fd 0 --keyserver "$KEYSERVER" \
532         --search ="$userID" > /dev/null 2>&1
533     returnCode="$?"
534
535     # if the user is the monkeysphere user, then update the
536     # monkeysphere user's trustdb
537     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
538         gpg_authentication "--check-trustdb" > /dev/null 2>&1
539     fi
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 }