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