make sure that the FreeBSD package is using GNU-style getopt calls.
[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 ETC="/etc/monkeysphere"
20 export ETC
21
22 ########################################################################
23 ### UTILITY FUNCTIONS
24
25 # failure function.  exits with code 255, unless specified otherwise.
26 failure() {
27     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 user
397     local path
398     local access
399     local gAccess
400     local oAccess
401
402     # function to check that an octal corresponds to writability
403     is_write() {
404         [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
405     }
406
407     user="$1"
408     path="$2"
409
410     # return 0 is path does not exist
411     [ -e "$path" ] || return 0
412
413     owner=$(stat --format '%U' "$path")
414     access=$(stat --format '%a' "$path")
415     gAccess=$(echo "$access" | cut -c2)
416     oAccess=$(echo "$access" | cut -c3)
417
418     # check owner
419     if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
420         return 1
421     fi
422
423     # check group/other writability
424     if is_write "$gAccess" || is_write "$oAccess" ; then
425         return 2
426     fi
427
428     if [ "$path" = '/' ] ; then
429         return 0
430     else
431         check_key_file_permissions $(dirname "$path")
432     fi
433 }
434
435 ### CONVERSION UTILITIES
436
437 # output the ssh key for a given key ID
438 gpg2ssh() {
439     local keyID
440     
441     keyID="$1"
442
443     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
444 }
445
446 # output known_hosts line from ssh key
447 ssh2known_hosts() {
448     local host
449     local key
450
451     host="$1"
452     key="$2"
453
454     echo -n "$host "
455     echo -n "$key" | tr -d '\n'
456     echo " MonkeySphere${DATE}"
457 }
458
459 # output authorized_keys line from ssh key
460 ssh2authorized_keys() {
461     local userID
462     local key
463     
464     userID="$1"
465     key="$2"
466
467     echo -n "$key" | tr -d '\n'
468     echo " MonkeySphere${DATE} ${userID}"
469 }
470
471 # convert key from gpg to ssh known_hosts format
472 gpg2known_hosts() {
473     local host
474     local keyID
475
476     host="$1"
477     keyID="$2"
478
479     # NOTE: it seems that ssh-keygen -R removes all comment fields from
480     # all lines in the known_hosts file.  why?
481     # NOTE: just in case, the COMMENT can be matched with the
482     # following regexp:
483     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
484     echo -n "$host "
485     gpg2ssh "$keyID" | tr -d '\n'
486     echo " MonkeySphere${DATE}"
487 }
488
489 # convert key from gpg to ssh authorized_keys format
490 gpg2authorized_keys() {
491     local userID
492     local keyID
493
494     userID="$1"
495     keyID="$2"
496
497     # NOTE: just in case, the COMMENT can be matched with the
498     # following regexp:
499     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
500     gpg2ssh "$keyID" | tr -d '\n'
501     echo " MonkeySphere${DATE} ${userID}"
502 }
503
504 ### GPG UTILITIES
505
506 # retrieve all keys with given user id from keyserver
507 # FIXME: need to figure out how to retrieve all matching keys
508 # (not just first N (5 in this case))
509 gpg_fetch_userid() {
510     local userID
511     local returnCode
512
513     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
514         return 0
515     fi
516
517     userID="$1"
518
519     log info " checking keyserver $KEYSERVER... "
520     echo 1,2,3,4,5 | \
521         gpg --quiet --batch --with-colons \
522         --command-fd 0 --keyserver "$KEYSERVER" \
523         --search ="$userID" > /dev/null 2>&1
524     returnCode="$?"
525
526     # if the user is the monkeysphere user, then update the
527     # monkeysphere user's trustdb
528     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
529         gpg_authentication "--check-trustdb" > /dev/null 2>&1
530     fi
531
532     return "$returnCode"
533 }
534
535 ########################################################################
536 ### PROCESSING FUNCTIONS
537
538 # userid and key policy checking
539 # the following checks policy on the returned keys
540 # - checks that full key has appropriate valididy (u|f)
541 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
542 # - checks that requested user ID has appropriate validity
543 # (see /usr/share/doc/gnupg/DETAILS.gz)
544 # output is one line for every found key, in the following format:
545 #
546 # flag:sshKey
547 #
548 # "flag" is an acceptability flag, 0 = ok, 1 = bad
549 # "sshKey" is the translated gpg key
550 #
551 # all log output must go to stderr, as stdout is used to pass the
552 # flag:sshKey to the calling function.
553 #
554 # expects global variable: "MODE"
555 process_user_id() {
556     local userID
557     local requiredCapability
558     local requiredPubCapability
559     local gpgOut
560     local type
561     local validity
562     local keyid
563     local uidfpr
564     local usage
565     local keyOK
566     local uidOK
567     local lastKey
568     local lastKeyOK
569     local fingerprint
570
571     userID="$1"
572
573     # set the required key capability based on the mode
574     if [ "$MODE" = 'known_hosts' ] ; then
575         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
576     elif [ "$MODE" = 'authorized_keys' ] ; then
577         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
578     fi
579     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
580
581     # fetch the user ID if necessary/requested
582     gpg_fetch_userid "$userID"
583
584     # output gpg info for (exact) userid and store
585     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
586         --with-fingerprint --with-fingerprint \
587         ="$userID" 2>/dev/null)
588
589     # if the gpg query return code is not 0, return 1
590     if [ "$?" -ne 0 ] ; then
591         log verbose " no primary keys found."
592         return 1
593     fi
594
595     # loop over all lines in the gpg output and process.
596     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
597     while IFS=: read -r type validity keyid uidfpr usage ; do
598         # process based on record type
599         case $type in
600             'pub') # primary keys
601                 # new key, wipe the slate
602                 keyOK=
603                 uidOK=
604                 lastKey=pub
605                 lastKeyOK=
606                 fingerprint=
607
608                 log verbose " primary key found: $keyid"
609
610                 # if overall key is not valid, skip
611                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
612                     log error "  - unacceptable primary key validity ($validity)."
613                     continue
614                 fi
615                 # if overall key is disabled, skip
616                 if check_capability "$usage" 'D' ; then
617                     log error "  - key disabled."
618                     continue
619                 fi
620                 # if overall key capability is not ok, skip
621                 if ! check_capability "$usage" $requiredPubCapability ; then
622                     log error "  - unacceptable primary key capability ($usage)."
623                     continue
624                 fi
625
626                 # mark overall key as ok
627                 keyOK=true
628
629                 # mark primary key as ok if capability is ok
630                 if check_capability "$usage" $requiredCapability ; then
631                     lastKeyOK=true
632                 fi
633                 ;;
634             'uid') # user ids
635                 if [ "$lastKey" != pub ] ; then
636                     log error " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
637                     continue
638                 fi
639                 # if an acceptable user ID was already found, skip
640                 if [ "$uidOK" = 'true' ] ; then
641                     continue
642                 fi
643                 # if the user ID does matches...
644                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
645                     # and the user ID validity is ok
646                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
647                         # mark user ID acceptable
648                         uidOK=true
649                     fi
650                 else
651                     continue
652                 fi
653
654                 # output a line for the primary key
655                 # 0 = ok, 1 = bad
656                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
657                     log verbose "  * acceptable primary key."
658                     if [ -z "$sshKey" ] ; then
659                         log error "    ! primary key could not be translated (not RSA or DSA?)."
660                     else
661                         echo "0:${sshKey}"
662                     fi
663                 else
664                     log error "  - unacceptable primary key."
665                     if [ -z "$sshKey" ] ; then
666                         log error "   ! primary key could not be translated (not RSA or DSA?)."
667                     else
668                         echo "1:${sshKey}"
669                     fi
670                 fi
671                 ;;
672             'sub') # sub keys
673                 # unset acceptability of last key
674                 lastKey=sub
675                 lastKeyOK=
676                 fingerprint=
677                 
678                 # don't bother with sub keys if the primary key is not valid
679                 if [ "$keyOK" != true ] ; then
680                     continue
681                 fi
682
683                 # don't bother with sub keys if no user ID is acceptable:
684                 if [ "$uidOK" != true ] ; then
685                     continue
686                 fi
687                 
688                 # if sub key validity is not ok, skip
689                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
690                     continue
691                 fi
692                 # if sub key capability is not ok, skip
693                 if ! check_capability "$usage" $requiredCapability ; then
694                     continue
695                 fi
696
697                 # mark sub key as ok
698                 lastKeyOK=true
699                 ;;
700             'fpr') # key fingerprint
701                 fingerprint="$uidfpr"
702
703                 sshKey=$(gpg2ssh "$fingerprint")
704
705                 # if the last key was the pub key, skip
706                 if [ "$lastKey" = pub ] ; then
707                     continue
708                 fi
709
710                 # output a line for the sub key
711                 # 0 = ok, 1 = bad
712                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
713                     log verbose "  * acceptable sub key."
714                     if [ -z "$sshKey" ] ; then
715                         log error "    ! sub key could not be translated (not RSA or DSA?)."
716                     else
717                         echo "0:${sshKey}"
718                     fi
719                 else
720                     log error "  - unacceptable sub key."
721                     if [ -z "$sshKey" ] ; then
722                         log error "    ! sub key could not be translated (not RSA or DSA?)."
723                     else
724                         echo "1:${sshKey}"
725                     fi
726                 fi
727                 ;;
728         esac
729     done | sort -t: -k1 -n -r
730     # NOTE: this last sort is important so that the "good" keys (key
731     # flag '0') come last.  This is so that they take precedence when
732     # being processed in the key files over "bad" keys (key flag '1')
733 }
734
735 # process a single host in the known_host file
736 process_host_known_hosts() {
737     local host
738     local userID
739     local nKeys
740     local nKeysOK
741     local ok
742     local sshKey
743     local tmpfile
744
745     host="$1"
746     userID="ssh://${host}"
747
748     log verbose "processing: $host"
749
750     nKeys=0
751     nKeysOK=0
752
753     IFS=$'\n'
754     for line in $(process_user_id "${userID}") ; do
755         # note that key was found
756         nKeys=$((nKeys+1))
757
758         ok=$(echo "$line" | cut -d: -f1)
759         sshKey=$(echo "$line" | cut -d: -f2)
760
761         if [ -z "$sshKey" ] ; then
762             continue
763         fi
764
765         # remove the old host key line, and note if removed
766         remove_line "$KNOWN_HOSTS" "$sshKey"
767
768         # if key OK, add new host line
769         if [ "$ok" -eq '0' ] ; then
770             # note that key was found ok
771             nKeysOK=$((nKeysOK+1))
772
773             # hash if specified
774             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
775                 # FIXME: this is really hackish cause ssh-keygen won't
776                 # hash from stdin to stdout
777                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
778                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
779                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
780                 cat "$tmpfile" >> "$KNOWN_HOSTS"
781                 rm -f "$tmpfile" "${tmpfile}.old"
782             else
783                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
784             fi
785         fi
786     done
787
788     # if at least one key was found...
789     if [ "$nKeys" -gt 0 ] ; then
790         # if ok keys were found, return 0
791         if [ "$nKeysOK" -gt 0 ] ; then
792             return 0
793         # else return 2
794         else
795             return 2
796         fi
797     # if no keys were found, return 1
798     else
799         return 1
800     fi
801 }
802
803 # update the known_hosts file for a set of hosts listed on command
804 # line
805 update_known_hosts() {
806     local nHosts
807     local nHostsOK
808     local nHostsBAD
809     local fileCheck
810     local host
811
812     # the number of hosts specified on command line
813     nHosts="$#"
814
815     nHostsOK=0
816     nHostsBAD=0
817
818     # create a lockfile on known_hosts:
819     lock create "$KNOWN_HOSTS"
820     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
821     trap "lock remove $KNOWN_HOSTS" EXIT
822
823     # note pre update file checksum
824     fileCheck="$(file_hash "$KNOWN_HOSTS")"
825
826     for host ; do
827         # process the host
828         process_host_known_hosts "$host"
829         # note the result
830         case "$?" in
831             0)
832                 nHostsOK=$((nHostsOK+1))
833                 ;;
834             2)
835                 nHostsBAD=$((nHostsBAD+1))
836                 ;;
837         esac
838
839         # touch the lockfile, for good measure.
840         lock touch "$KNOWN_HOSTS"
841     done
842
843     # remove the lockfile and the trap
844     lock remove "$KNOWN_HOSTS"
845     trap - EXIT
846
847     # note if the known_hosts file was updated
848     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
849         log verbose "known_hosts file updated."
850     fi
851
852     # if an acceptable host was found, return 0
853     if [ "$nHostsOK" -gt 0 ] ; then
854         return 0
855     # else if no ok hosts were found...
856     else
857         # if no bad host were found then no hosts were found at all,
858         # and return 1
859         if [ "$nHostsBAD" -eq 0 ] ; then
860             return 1
861         # else if at least one bad host was found, return 2
862         else
863             return 2
864         fi
865     fi
866 }
867
868 # process hosts from a known_hosts file
869 process_known_hosts() {
870     local hosts
871
872     log verbose "processing known_hosts file..."
873
874     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
875
876     if [ -z "$hosts" ] ; then
877         log error "no hosts to process."
878         return
879     fi
880
881     # take all the hosts from the known_hosts file (first
882     # field), grep out all the hashed hosts (lines starting
883     # with '|')...
884     update_known_hosts $hosts
885 }
886
887 # process uids for the authorized_keys file
888 process_uid_authorized_keys() {
889     local userID
890     local nKeys
891     local nKeysOK
892     local ok
893     local sshKey
894
895     userID="$1"
896
897     log verbose "processing: $userID"
898
899     nKeys=0
900     nKeysOK=0
901
902     IFS=$'\n'
903     for line in $(process_user_id "$userID") ; do
904         # note that key was found
905         nKeys=$((nKeys+1))
906
907         ok=$(echo "$line" | cut -d: -f1)
908         sshKey=$(echo "$line" | cut -d: -f2)
909
910         if [ -z "$sshKey" ] ; then
911             continue
912         fi
913
914         # remove the old host key line
915         remove_line "$AUTHORIZED_KEYS" "$sshKey"
916
917         # if key OK, add new host line
918         if [ "$ok" -eq '0' ] ; then
919             # note that key was found ok
920             nKeysOK=$((nKeysOK+1))
921
922             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
923         fi
924     done
925
926     # if at least one key was found...
927     if [ "$nKeys" -gt 0 ] ; then
928         # if ok keys were found, return 0
929         if [ "$nKeysOK" -gt 0 ] ; then
930             return 0
931         # else return 2
932         else
933             return 2
934         fi
935     # if no keys were found, return 1
936     else
937         return 1
938     fi
939 }
940
941 # update the authorized_keys files from a list of user IDs on command
942 # line
943 update_authorized_keys() {
944     local userID
945     local nIDs
946     local nIDsOK
947     local nIDsBAD
948     local fileCheck
949
950     # the number of ids specified on command line
951     nIDs="$#"
952
953     nIDsOK=0
954     nIDsBAD=0
955
956     # create a lockfile on authorized_keys
957     lock create "$AUTHORIZED_KEYS"
958     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
959     trap "lock remove $AUTHORIZED_KEYS" EXIT
960
961     # note pre update file checksum
962     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
963
964     # remove any monkeysphere lines from authorized_keys file
965     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
966
967     for userID ; do
968         # process the user ID, change return code if key not found for
969         # user ID
970         process_uid_authorized_keys "$userID"
971
972         # note the result
973         case "$?" in
974             0)
975                 nIDsOK=$((nIDsOK+1))
976                 ;;
977             2)
978                 nIDsBAD=$((nIDsBAD+1))
979                 ;;
980         esac
981
982         # touch the lockfile, for good measure.
983         lock touch "$AUTHORIZED_KEYS"
984     done
985
986     # remove the lockfile and the trap
987     lock remove "$AUTHORIZED_KEYS"
988     trap - EXIT
989
990     # note if the authorized_keys file was updated
991     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
992         log verbose "authorized_keys file updated."
993     fi
994
995     # if an acceptable id was found, return 0
996     if [ "$nIDsOK" -gt 0 ] ; then
997         return 0
998     # else if no ok ids were found...
999     else
1000         # if no bad ids were found then no ids were found at all, and
1001         # return 1
1002         if [ "$nIDsBAD" -eq 0 ] ; then
1003             return 1
1004         # else if at least one bad id was found, return 2
1005         else
1006             return 2
1007         fi
1008     fi
1009 }
1010
1011 # process an authorized_user_ids file for authorized_keys
1012 process_authorized_user_ids() {
1013     local line
1014     local nline
1015     local userIDs
1016
1017     authorizedUserIDs="$1"
1018
1019     log verbose "processing authorized_user_ids file..."
1020
1021     if ! meat "$authorizedUserIDs" > /dev/null ; then
1022         log error "no user IDs to process."
1023         return
1024     fi
1025
1026     nline=0
1027
1028     # extract user IDs from authorized_user_ids file
1029     IFS=$'\n'
1030     for line in $(meat "$authorizedUserIDs") ; do
1031         userIDs["$nline"]="$line"
1032         nline=$((nline+1))
1033     done
1034
1035     update_authorized_keys "${userIDs[@]}"
1036 }