baf4270c0b4b749b488fd78ec9b3203a03963a7e
[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-2009, 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 ### UTILITY FUNCTIONS
18
19 # output version info
20 version() {
21     cat "${SYSSHAREDIR}/VERSION"
22 }
23
24 # failure function.  exits with code 255, unless specified otherwise.
25 failure() {
26     [ "$1" ] && echo "$1" >&2
27     exit ${2:-'255'}
28 }
29
30 # write output to stderr based on specified LOG_LEVEL the first
31 # parameter is the priority of the output, and everything else is what
32 # is echoed to stderr.  If there is nothing else, then output comes
33 # from stdin, and is not prefaced by log prefix.
34 log() {
35     local priority
36     local level
37     local output
38     local alllevels
39     local found=
40
41     # don't include SILENT in alllevels: it's handled separately
42     # list in decreasing verbosity (all caps).
43     # separate with $IFS explicitly, since we do some fancy footwork
44     # elsewhere.
45     alllevels="DEBUG${IFS}VERBOSE${IFS}INFO${IFS}ERROR"
46
47     # translate lowers to uppers in global log level
48     LOG_LEVEL=$(echo "$LOG_LEVEL" | tr "[:lower:]" "[:upper:]")
49
50     # just go ahead and return if the log level is silent
51     if [ "$LOG_LEVEL" = 'SILENT' ] ; then
52         return
53     fi
54
55     for level in $alllevels ; do 
56         if [ "$LOG_LEVEL" = "$level" ] ; then
57             found=true
58         fi
59     done
60     if [ -z "$found" ] ; then
61         # default to INFO:
62         LOG_LEVEL=INFO
63     fi
64
65     # get priority from first parameter, translating all lower to
66     # uppers
67     priority=$(echo "$1" | tr "[:lower:]" "[:upper:]")
68     shift
69
70     # scan over available levels
71     for level in $alllevels ; do
72         # output if the log level matches, set output to true
73         # this will output for all subsequent loops as well.
74         if [ "$LOG_LEVEL" = "$level" ] ; then
75             output=true
76         fi
77         if [ "$priority" = "$level" -a "$output" = 'true' ] ; then
78             if [ "$1" ] ; then
79                 echo "$@"
80             else
81                 cat
82             fi | sed 's/^/'"${LOG_PREFIX}"'/' >&2
83         fi
84     done
85 }
86
87 # run command as monkeysphere user
88 su_monkeysphere_user() {
89     # our main goal here is to run the given command as the the
90     # monkeysphere user, but without prompting for any sort of
91     # authentication.  If this is not possible, we should just fail.
92
93     # FIXME: our current implementation is overly restrictive, because
94     # there may be some su PAM configurations that would allow su
95     # "$MONKEYSPHERE_USER" -c "$@" to Just Work without prompting,
96     # allowing specific users to invoke commands which make use of
97     # this user.
98
99     # chpst (from runit) would be nice to use, but we don't want to
100     # introduce an extra dependency just for this.  This may be a
101     # candidate for re-factoring if we switch implementation languages.
102
103     case $(id -un) in
104         # if monkeysphere user, run the command under bash
105         "$MONKEYSPHERE_USER")
106             bash -c "$*"
107             ;;
108
109          # if root, su command as monkeysphere user
110         'root')
111             su "$MONKEYSPHERE_USER" -c "$*"
112             ;;
113
114         # otherwise, fail
115         *)
116             log error "non-privileged user."
117             ;;
118     esac
119 }
120
121 # cut out all comments(#) and blank lines from standard input
122 meat() {
123     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
124 }
125
126 # cut a specified line from standard input
127 cutline() {
128     head --line="$1" "$2" | tail -1
129 }
130
131 # make a temporary directory
132 msmktempdir() {
133     mktemp -d ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
134 }
135
136 # make a temporary file
137 msmktempfile() {
138     mktemp ${TMPDIR:-/tmp}/monkeysphere.XXXXXXXXXX
139 }
140
141 # this is a wrapper for doing lock functions.
142 #
143 # it lets us depend on either lockfile-progs (preferred) or procmail's
144 # lockfile, and should
145 lock() {
146     local use_lockfileprogs=true
147     local action="$1"
148     local file="$2"
149
150     if ! ( type lockfile-create &>/dev/null ) ; then
151         if ! ( type lockfile &>/dev/null ); then
152             failure "Neither lockfile-create nor lockfile are in the path!"
153         fi
154         use_lockfileprogs=
155     fi
156     
157     case "$action" in
158         create)
159             if [ -n "$use_lockfileprogs" ] ; then
160                 lockfile-create "$file" || failure "unable to lock '$file'"
161             else
162                 lockfile -r 20 "${file}.lock" || failure "unable to lock '$file'"
163             fi
164             log debug "lock created on '$file'."
165             ;;
166         touch)  
167             if [ -n "$use_lockfileprogs" ] ; then
168                 lockfile-touch --oneshot "$file"
169             else
170                 : Nothing to do here
171             fi
172             log debug "lock touched on '$file'."
173             ;;
174         remove)
175             if [ -n "$use_lockfileprogs" ] ; then
176                 lockfile-remove "$file"
177             else
178                 rm -f "${file}.lock"
179             fi
180             log debug "lock removed on '$file'."
181             ;;
182         *)
183             failure "bad argument for lock subfunction '$action'"
184     esac
185 }
186
187
188 # for portability, between gnu date and BSD date.
189 # arguments should be:  number longunits format
190
191 # e.g. advance_date 20 seconds +%F
192 advance_date() {
193     local gnutry
194     local number="$1"
195     local longunits="$2"
196     local format="$3"
197     local shortunits
198
199     # try things the GNU way first 
200     if date -d "$number $longunits" "$format" &>/dev/null; then
201         date -d "$number $longunits" "$format"
202     else
203         # otherwise, convert to (a limited version of) BSD date syntax:
204         case "$longunits" in
205             years)
206                 shortunits=y
207                 ;;
208             months)
209                 shortunits=m
210                 ;;
211             weeks)
212                 shortunits=w
213                 ;;
214             days)
215                 shortunits=d
216                 ;;
217             hours)
218                 shortunits=H
219                 ;;
220             minutes)
221                 shortunits=M
222                 ;;
223             seconds)
224                 shortunits=S
225                 ;;
226             *)
227                 # this is a longshot, and will likely fail; oh well.
228                 shortunits="$longunits"
229         esac
230         date "-v+${number}${shortunits}" "$format"
231     fi
232 }
233
234
235 # check that characters are in a string (in an AND fashion).
236 # used for checking key capability
237 # check_capability capability a [b...]
238 check_capability() {
239     local usage
240     local capcheck
241
242     usage="$1"
243     shift 1
244
245     for capcheck ; do
246         if echo "$usage" | grep -q -v "$capcheck" ; then
247             return 1
248         fi
249     done
250     return 0
251 }
252
253 # hash of a file
254 file_hash() {
255     if type md5sum &>/dev/null ; then
256         md5sum "$1"
257     elif type md5 &>/dev/null ; then
258         md5 "$1"
259     else
260         failure "Neither md5sum nor md5 are in the path!"
261     fi
262 }
263
264 # convert escaped characters in pipeline from gpg output back into
265 # original character
266 # FIXME: undo all escape character translation in with-colons gpg
267 # output
268 gpg_unescape() {
269     sed 's/\\x3a/:/g'
270 }
271
272 # convert nasty chars into gpg-friendly form in pipeline
273 # FIXME: escape everything, not just colons!
274 gpg_escape() {
275     sed 's/:/\\x3a/g'
276 }
277
278 # prompt for GPG-formatted expiration, and emit result on stdout
279 get_gpg_expiration() {
280     local keyExpire
281
282     keyExpire="$1"
283
284     if [ -z "$keyExpire" -a "$PROMPT" != 'false' ]; then
285         cat >&2 <<EOF
286 Please specify how long the key should be valid.
287          0 = key does not expire
288       <n>  = key expires in n days
289       <n>w = key expires in n weeks
290       <n>m = key expires in n months
291       <n>y = key expires in n years
292 EOF
293         while [ -z "$keyExpire" ] ; do
294             printf "Key is valid for? (0) " >&2
295             read keyExpire
296             if ! test_gpg_expire ${keyExpire:=0} ; then
297                 echo "invalid value" >&2
298                 unset keyExpire
299             fi
300         done
301     elif ! test_gpg_expire "$keyExpire" ; then
302         failure "invalid key expiration value '$keyExpire'."
303     fi
304         
305     echo "$keyExpire"
306 }
307
308 passphrase_prompt() {
309     local prompt="$1"
310     local fifo="$2"
311     local PASS
312
313     if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
314         printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
315         printf '(with prompt "%s")\n' "$prompt" | log debug
316         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
317     else
318         read -s -p "$prompt" PASS
319         # Uses the builtin echo, so should not put the passphrase into
320         # the process table.  I think. --dkg
321         echo "$PASS" > "$fifo"
322     fi
323 }
324
325 # remove all lines with specified string from specified file
326 remove_line() {
327     local file
328     local lines
329     local tempfile
330
331     file="$1"
332     shift
333
334     if [ ! -e "$file" ] ; then
335         return 1
336     fi
337
338     if (($# == 1)) ; then
339         lines=$(grep -F "$1" "$file") || true
340     else
341         lines=$(grep -F "$1" "$file" | grep -F "$2") || true
342     fi
343
344     # if the string was found, remove it
345     if [ "$lines" ] ; then
346         log debug "removing matching key lines..."
347         tempfile=$(mktemp "${file}.XXXXXXX") || \
348             failure "Unable to make temp file '${file}.XXXXXXX'"
349         grep -v -x -F "$lines" "$file" >"$tempfile" || :
350         mv -f "$tempfile" "$file"
351     fi
352 }
353
354 # remove all lines with MonkeySphere strings from stdin
355 remove_monkeysphere_lines() {
356     egrep -v ' MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2} '
357 }
358
359 # translate ssh-style path variables %h and %u
360 translate_ssh_variables() {
361     local uname
362     local home
363
364     uname="$1"
365     path="$2"
366
367     # get the user's home directory
368     userHome=$(get_homedir "$uname")
369
370     # translate '%u' to user name
371     path=${path/\%u/"$uname"}
372     # translate '%h' to user home directory
373     path=${path/\%h/"$userHome"}
374
375     echo "$path"
376 }
377
378 # test that a string to conforms to GPG's expiration format
379 test_gpg_expire() {
380     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
381 }
382
383 # touch a key file if it doesn't exist, including creating needed
384 # directories with correct permissions
385 touch_key_file_or_fail() {
386     local keyFile="$1"
387     local newUmask
388
389     if [ ! -f "$keyFile" ]; then
390         # make sure to create files and directories with the
391         # appropriate write bits turned off:
392         newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
393         [ -d $(dirname "$keyFile") ] \
394             || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$keyFile") ) \
395             || failure "Could not create path to $keyFile"
396         # make sure to create this file with the appropriate bits turned off:
397         (umask "$newUmask" && touch "$keyFile") \
398             || failure "Unable to create $keyFile"
399     fi
400 }
401
402 # check that a file is properly owned, and that all it's parent
403 # directories are not group/other writable
404 check_key_file_permissions() {
405     local uname
406     local path
407
408     uname="$1"
409     path="$2"
410
411     if [ "$STRICT_MODES" = 'false' ] ; then
412         log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
413         return 0
414     fi
415     log debug "checking path permission '$path'..."
416     "${SYSSHAREDIR}/checkperms" "$uname" "$path"
417 }
418
419 # return a list of all users on the system
420 list_users() {
421     if type getent &>/dev/null ; then
422         # for linux and FreeBSD systems
423         getent passwd | cut -d: -f1
424     elif type dscl &>/dev/null ; then
425         # for Darwin systems
426         dscl localhost -list /Search/Users
427     else
428         failure "Neither getent or dscl is in the path!  Could not determine list of users."
429     fi
430 }
431
432 # take one argument, a service name.  in response, print a series of
433 # lines, each with a unique numeric port number that might be
434 # associated with that service name.  (e.g. in: "https", out: "443")
435 # if nothing is found, print nothing, and return 0.
436
437 # return 1 if there was an error in the search somehow
438 get_port_for_service() {
439
440     [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
441         failure $(printf "This is not a valid service name: '%s'" "$1")
442     if type getent &>/dev/null ; then
443         # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
444         (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
445     elif [ -r /etc/services ] ; then
446         # fall back to /etc/services for systems that don't have getent (MacOS?)
447         # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
448         awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
449     else
450         return 1
451     fi
452 }
453
454 # return the path to the home directory of a user
455 get_homedir() {
456     local uname=${1:-`whoami`}
457     eval "echo ~${uname}"
458 }
459
460 # return the primary group of a user
461 get_primary_group() {
462     local uname=${1:-`whoami`}
463     groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
464 }
465
466 ### CONVERSION UTILITIES
467
468 # output the ssh key for a given key ID
469 gpg2ssh() {
470     local keyID
471     
472     keyID="$1"
473
474     gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
475 }
476
477 # output known_hosts line from ssh key
478 ssh2known_hosts() {
479     local host
480     local port
481     local key
482
483     # FIXME this does not properly deal with IPv6 hosts using the
484     # standard port (because it's unclear whether their final
485     # colon-delimited address section is a port number or an address
486     # string)
487     host=${1%:*}
488     port=${1##*:}
489     key="$2"
490
491     # specify the host and port properly for new ssh known_hosts
492     # format
493     if [ "$port" != "$host" ] ; then
494         host="[${host}]:${port}"
495     fi
496
497     # hash if specified
498     if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
499         if (type ssh-keygen >/dev/null) ; then
500             log verbose "hashing known_hosts line"
501             # FIXME: this is really hackish cause
502             # ssh-keygen won't hash from stdin to
503             # stdout
504             tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
505             printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE" \
506                 > "$tmpfile"
507             ssh-keygen -H -f "$tmpfile" 2>/dev/null
508             if [[ "$keyFile" == '-' ]] ; then
509                 cat "$tmpfile"
510             else
511                 cat "$tmpfile" >> "$keyFile"
512             fi
513             rm -f "$tmpfile" "${tmpfile}.old"
514             # FIXME: we could do this without needing ssh-keygen.
515             # hashed known_hosts looks like: |1|X|Y where 1 means SHA1
516             # (nothing else is defined in openssh sources), X is the
517             # salt (same length as the digest output), base64-encoded,
518             # and Y is the digested hostname (also base64-encoded).
519             # see hostfile.{c,h} in openssh sources.
520         else
521             log error "Cannot hash known_hosts line as requested."
522         fi
523     else
524         printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
525     fi
526 }
527
528 # output authorized_keys line from ssh key
529 ssh2authorized_keys() {
530     local userID="$1"
531     local key="$2"
532
533     if [[ "$AUTHORIZED_KEYS_OPTIONS" ]]; then
534         printf "%s %s MonkeySphere%s %s\n" "$AUTHORIZED_KEYS_OPTIONS" "$key" "$DATE" "$userID"
535     else
536         printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
537     fi
538 }
539
540 # convert key from gpg to ssh known_hosts format
541 gpg2known_hosts() {
542     local host
543     local keyID
544     local key
545
546     host="$1"
547     keyID="$2"
548
549     key=$(gpg2ssh "$keyID")
550
551     # NOTE: it seems that ssh-keygen -R removes all comment fields from
552     # all lines in the known_hosts file.  why?
553     # NOTE: just in case, the COMMENT can be matched with the
554     # following regexp:
555     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
556     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
557 }
558
559 # convert key from gpg to ssh authorized_keys format
560 gpg2authorized_keys() {
561     local userID
562     local keyID
563     local key
564
565     userID="$1"
566     keyID="$2"
567
568     key=$(gpg2ssh "$keyID")
569
570     # NOTE: just in case, the COMMENT can be matched with the
571     # following regexp:
572     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
573     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
574 }
575
576 ### GPG UTILITIES
577
578 # script to determine if gpg version is equal to or greater than specified version
579 is_gpg_version_greater_equal() {
580     local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
581     local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
582         | tr '.' ' ' | sort -g -k1 -k2 -k3 \
583         | tail -1 | tr ' ' '.')
584     [[ "$gpgVersion" == "$latest" ]]
585 }
586
587 # retrieve all keys with given user id from keyserver
588 # FIXME: need to figure out how to retrieve all matching keys
589 # (not just first N (5 in this case))
590 gpg_fetch_userid() {
591     local returnCode=0
592     local userID
593
594     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
595         return 0
596     fi
597
598     userID="$1"
599
600     log verbose " checking keyserver $KEYSERVER... "
601     echo 1,2,3,4,5 | \
602         gpg --quiet --batch --with-colons \
603         --command-fd 0 --keyserver "$KEYSERVER" \
604         --search ="$userID" &>/dev/null
605     returnCode="$?"
606
607     if [ "$returnCode" != 0 ] ; then
608         log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
609     fi
610
611     return "$returnCode"
612 }
613
614 ########################################################################
615 ### PROCESSING FUNCTIONS
616
617 # userid and key policy checking
618 # the following checks policy on the returned keys
619 # - checks that full key has appropriate valididy (u|f)
620 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
621 # - checks that requested user ID has appropriate validity
622 # (see /usr/share/doc/gnupg/DETAILS.gz)
623 # output is one line for every found key, in the following format:
624 #
625 # flag:sshKey
626 #
627 # "flag" is an acceptability flag, 0 = ok, 1 = bad
628 # "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
629 #
630 # all log output must go to stderr, as stdout is used to pass the
631 # flag:sshKey to the calling function.
632 process_user_id() {
633     local returnCode=0
634     local userID="$1"
635     local requiredCapability
636     local requiredPubCapability
637     local gpgOut
638     local type
639     local validity
640     local keyid
641     local uidfpr
642     local usage
643     local keyOK
644     local uidOK
645     local lastKey
646     local lastKeyOK
647     local fingerprint
648
649     # set the required key capability based on the mode
650     requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
651     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
652
653     # fetch the user ID if necessary/requested
654     gpg_fetch_userid "$userID"
655
656     # output gpg info for (exact) userid and store
657     gpgOut=$(gpg --list-key --fixed-list-mode --with-colons \
658         --with-fingerprint --with-fingerprint \
659         ="$userID" 2>/dev/null) || returnCode="$?"
660
661     # if the gpg query return code is not 0, return 1
662     if [ "$returnCode" -ne 0 ] ; then
663         log verbose " no primary keys found."
664         return 1
665     fi
666
667     # loop over all lines in the gpg output and process.
668     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
669     while IFS=: read -r type validity keyid uidfpr usage ; do
670         # process based on record type
671         case $type in
672             'pub') # primary keys
673                 # new key, wipe the slate
674                 keyOK=
675                 uidOK=
676                 lastKey=pub
677                 lastKeyOK=
678                 fingerprint=
679
680                 log verbose " primary key found: $keyid"
681
682                 # if overall key is not valid, skip
683                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
684                     log debug "  - unacceptable primary key validity ($validity)."
685                     continue
686                 fi
687                 # if overall key is disabled, skip
688                 if check_capability "$usage" 'D' ; then
689                     log debug "  - key disabled."
690                     continue
691                 fi
692                 # if overall key capability is not ok, skip
693                 if ! check_capability "$usage" $requiredPubCapability ; then
694                     log debug "  - unacceptable primary key capability ($usage)."
695                     continue
696                 fi
697
698                 # mark overall key as ok
699                 keyOK=true
700
701                 # mark primary key as ok if capability is ok
702                 if check_capability "$usage" $requiredCapability ; then
703                     lastKeyOK=true
704                 fi
705                 ;;
706             'uid') # user ids
707                 if [ "$lastKey" != pub ] ; then
708                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
709                     continue
710                 fi
711                 # if an acceptable user ID was already found, skip
712                 if [ "$uidOK" = 'true' ] ; then
713                     continue
714                 fi
715                 # if the user ID does matches...
716                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
717                     # and the user ID validity is ok
718                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
719                         # mark user ID acceptable
720                         uidOK=true
721                     else
722                         log debug "  - unacceptable user ID validity ($validity)."
723                     fi
724                 else
725                     continue
726                 fi
727
728                 # output a line for the primary key
729                 # 0 = ok, 1 = bad
730                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
731                     log verbose "  * acceptable primary key."
732                     if [ -z "$sshKey" ] ; then
733                         log verbose "    ! primary key could not be translated (not RSA?)."
734                     else
735                         echo "0:${sshKey}"
736                     fi
737                 else
738                     log debug "  - unacceptable primary key."
739                     if [ -z "$sshKey" ] ; then
740                         log debug "    ! primary key could not be translated (not RSA?)."
741                     else
742                         echo "1:${sshKey}"
743                     fi
744                 fi
745                 ;;
746             'sub') # sub keys
747                 # unset acceptability of last key
748                 lastKey=sub
749                 lastKeyOK=
750                 fingerprint=
751                 
752                 # don't bother with sub keys if the primary key is not valid
753                 if [ "$keyOK" != true ] ; then
754                     continue
755                 fi
756
757                 # don't bother with sub keys if no user ID is acceptable:
758                 if [ "$uidOK" != true ] ; then
759                     continue
760                 fi
761                 
762                 # if sub key validity is not ok, skip
763                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
764                     log debug "  - unacceptable sub key validity ($validity)."
765                     continue
766                 fi
767                 # if sub key capability is not ok, skip
768                 if ! check_capability "$usage" $requiredCapability ; then
769                     log debug "  - unacceptable sub key capability ($usage)."
770                     continue
771                 fi
772
773                 # mark sub key as ok
774                 lastKeyOK=true
775                 ;;
776             'fpr') # key fingerprint
777                 fingerprint="$uidfpr"
778
779                 sshKey=$(gpg2ssh "$fingerprint")
780
781                 # if the last key was the pub key, skip
782                 if [ "$lastKey" = pub ] ; then
783                     continue
784                 fi
785
786                 # output a line for the sub key
787                 # 0 = ok, 1 = bad
788                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
789                     log verbose "  * acceptable sub key."
790                     if [ -z "$sshKey" ] ; then
791                         log error "    ! sub key could not be translated (not RSA?)."
792                     else
793                         echo "0:${sshKey}"
794                     fi
795                 else
796                     log debug "  - unacceptable sub key."
797                     if [ -z "$sshKey" ] ; then
798                         log debug "    ! sub key could not be translated (not RSA?)."
799                     else
800                         echo "1:${sshKey}"
801                     fi
802                 fi
803                 ;;
804         esac
805     done | sort -t: -k1 -n -r
806     # NOTE: this last sort is important so that the "good" keys (key
807     # flag '0') come last.  This is so that they take precedence when
808     # being processed in the key files over "bad" keys (key flag '1')
809 }
810
811 process_keys_for_file() {
812     local keyFile="$1"
813     local userID="$2"
814     local host
815     local ok
816     local sshKey
817     local keyLine
818
819     log verbose "processing: $userID"
820     log debug "key file: $keyFile"
821
822     IFS=$'\n'
823     for line in $(process_user_id "$userID") ; do
824         ok=${line%%:*}
825         sshKey=${line#*:}
826
827         if [ -z "$sshKey" ] ; then
828             continue
829         fi
830
831         # remove the old key line
832         if [[ "$keyFile" != '-' ]] ; then
833             case "$FILE_TYPE" in
834                 ('authorized_keys')
835                     remove_line "$keyFile" "$sshKey"
836                     ;;
837                 ('known_hosts')
838                     host=${userID#ssh://}
839                     remove_line "$keyFile" "$host" "$sshKey"
840                     ;;
841             esac
842         fi
843
844         ((++KEYS_PROCESSED))
845
846         # if key OK, add new key line
847         if [ "$ok" -eq '0' ] ; then
848             case "$FILE_TYPE" in
849                 ('raw')
850                     keyLine="$sshKey"
851                     ;;
852                 ('authorized_keys')
853                     keyLine=$(ssh2authorized_keys "$userID" "$sshKey")
854                     ;;
855                 ('known_hosts')
856                     host=${userID#ssh://}
857                     keyLine=$(ssh2known_hosts "$host" "$sshKey")
858                     ;;
859             esac
860
861             echo "key line: $keyLine" | log debug
862             if [[ "$keyFile" == '-' ]] ; then
863                 echo "$keyLine"
864             else
865                 log debug "adding key line to file..."
866                 echo "$keyLine" >>"$keyFile"
867             fi
868
869             ((++KEYS_VALID))
870         fi
871     done
872
873     log debug "KEYS_PROCESSED=$KEYS_PROCESSED"
874     log debug "KEYS_VALID=$KEYS_VALID"
875 }
876
877 # process an authorized_user_ids file on stdin for authorized_keys
878 process_authorized_user_ids() {
879     local authorizedKeys="$1"
880     declare -i nline=0
881     local line
882     declare -a userIDs
883     declare -a koptions
884
885     # extract user IDs from authorized_user_ids file
886     IFS=$'\n'
887     while read line ; do
888         case "$line" in
889             ("#"*)
890                 continue
891                 ;;
892             (" "*|$'\t'*)
893                 if [[ -z ${koptions[${nline}]} ]]; then
894                     koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
895                 else
896                     koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
897                 fi
898                 ;;
899             (*)
900                 ((++nline))
901                 userIDs[${nline}]="$line"
902                 unset koptions[${nline}] || true
903                 ;;
904         esac
905     done
906
907     for i in $(seq 1 $nline); do
908         AUTHORIZED_KEYS_OPTIONS="${koptions[$i]}" FILE_TYPE='authorized_keys' process_keys_for_file "$authorizedKeys" "${userIDs[$i]}" || returnCode="$?"
909     done
910 }
911
912 # takes a gpg key or keys on stdin, and outputs a list of
913 # fingerprints, one per line:
914 list_primary_fingerprints() {
915     local fake=$(msmktempdir)
916     trap "rm -rf $fake" EXIT
917     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
918     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
919         awk -F: '/^fpr:/{ print $10 }'
920     trap - EXIT
921     rm -rf "$fake"
922 }
923
924 # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
925 # key identifier as $1, and outputs the gpg-formatted information for
926 # the requested keys from the material on stdin
927 get_cert_info() {
928     local fake=$(msmktempdir)
929     trap "rm -rf $fake" EXIT
930     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
931     GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
932     trap - EXIT
933     rm -rf "$fake"
934 }
935
936
937 check_cruft_file() {
938     local loc="$1"
939     local version="$2"
940     
941     if [ -e "$loc" ] ; then
942         printf "! The file '%s' is no longer used by\n  monkeysphere (as of version %s), and can be removed.\n\n" "$loc" "$version" | log info
943     fi
944 }
945
946 check_upgrade_dir() {
947     local loc="$1"
948     local version="$2"
949
950     if [ -d "$loc" ] ; then
951         printf "The presence of directory '%s' indicates that you have\nnot yet completed a monkeysphere upgrade.\nYou should probably run the following script:\n  %s/transitions/%s\n\n" "$loc" "$SYSSHAREDIR" "$version" | log info
952     fi
953 }
954
955 ## look for cruft from old versions of the monkeysphere, and notice if
956 ## upgrades have not been run:
957 report_cruft() {
958     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
959     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
960
961     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
962     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
963
964     local found=
965     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
966         if [ -d "$foo" ] ; then
967             printf "! %s\n" "$foo" | log info
968             found=true
969         fi
970     done
971     if [ "$found" ] ; then
972         printf "The directories above are backups left over from a monkeysphere transition.\nThey may contain copies of sensitive data (host keys, certifier lists), but\nthey are no longer needed by monkeysphere.\nYou may remove them at any time.\n\n" | log info
973     fi
974 }