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