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