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