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