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