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