f8ae9dfdb5bc1a0360fd1d1daf9a0521a7277d31
[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     if [ ! -f "$keyFile" ]; then
388         # make sure to create files and directories with the
389         # appropriate write bits turned off:
390         newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
391         [ -d $(dirname "$keyFile") ] \
392             || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$keyFile") ) \
393             || failure "Could not create path to $keyFile"
394         # make sure to create this file with the appropriate bits turned off:
395         (umask "$newUmask" && touch "$keyFile") \
396             || failure "Unable to create $keyFile"
397     fi
398 }
399
400 # check that a file is properly owned, and that all it's parent
401 # directories are not group/other writable
402 check_key_file_permissions() {
403     local uname
404     local path
405
406     uname="$1"
407     path="$2"
408
409     if [ "$STRICT_MODES" = 'false' ] ; then
410         log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
411         return 0
412     fi
413     log debug "checking path permission '$path'..."
414     "${SYSSHAREDIR}/checkperms" "$uname" "$path"
415 }
416
417 # return a list of all users on the system
418 list_users() {
419     if type getent &>/dev/null ; then
420         # for linux and FreeBSD systems
421         getent passwd | cut -d: -f1
422     elif type dscl &>/dev/null ; then
423         # for Darwin systems
424         dscl localhost -list /Search/Users
425     else
426         failure "Neither getent or dscl is in the path!  Could not determine list of users."
427     fi
428 }
429
430 # take one argument, a service name.  in response, print a series of
431 # lines, each with a unique numeric port number that might be
432 # associated with that service name.  (e.g. in: "https", out: "443")
433 # if nothing is found, print nothing, and return 0.
434
435 # return 1 if there was an error in the search somehow
436 get_port_for_service() {
437
438     [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
439         failure $(printf "This is not a valid service name: '%s'" "$1")
440     if type getent &>/dev/null ; then
441         # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
442         (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
443     elif [ -r /etc/services ] ; then
444         # fall back to /etc/services for systems that don't have getent (MacOS?)
445         # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
446         awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
447     else
448         return 1
449     fi
450 }
451
452 # return the path to the home directory of a user
453 get_homedir() {
454     local uname=${1:-`whoami`}
455     eval "echo ~${uname}"
456 }
457
458 # return the primary group of a user
459 get_primary_group() {
460     local uname=${1:-`whoami`}
461     groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
462 }
463
464 ### CONVERSION UTILITIES
465
466 # output the ssh key for a given key ID
467 gpg2ssh() {
468     local keyID
469     
470     keyID="$1"
471
472     gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
473 }
474
475 # output known_hosts line from ssh key
476 ssh2known_hosts() {
477     local host
478     local port
479     local key
480
481     # FIXME this does not properly deal with IPv6 hosts using the
482     # standard port (because it's unclear whether their final
483     # colon-delimited address section is a port number or an address
484     # string)
485     host=${1%:*}
486     port=${1##*:}
487     key="$2"
488
489     # specify the host and port properly for new ssh known_hosts
490     # format
491     if [ "$port" != "$host" ] ; then
492         host="[${host}]:${port}"
493     fi
494
495     # hash if specified
496     if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
497         if (type ssh-keygen >/dev/null) ; then
498             log verbose "hashing known_hosts line"
499             # FIXME: this is really hackish cause
500             # ssh-keygen won't hash from stdin to
501             # stdout
502             tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
503             printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE" \
504                 > "$tmpfile"
505             ssh-keygen -H -f "$tmpfile" 2>/dev/null
506             if [[ "$keyFile" == '-' ]] ; then
507                 cat "$tmpfile"
508             else
509                 cat "$tmpfile" >> "$keyFile"
510             fi
511             rm -f "$tmpfile" "${tmpfile}.old"
512             # FIXME: we could do this without needing ssh-keygen.
513             # hashed known_hosts looks like: |1|X|Y where 1 means SHA1
514             # (nothing else is defined in openssh sources), X is the
515             # salt (same length as the digest output), base64-encoded,
516             # and Y is the digested hostname (also base64-encoded).
517             # see hostfile.{c,h} in openssh sources.
518         else
519             log error "Cannot hash known_hosts line as requested."
520         fi
521     else
522         printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
523     fi
524 }
525
526 # output authorized_keys line from ssh key
527 ssh2authorized_keys() {
528     local userID="$1"
529     local key="$2"
530
531     if [[ "$AUTHORIZED_KEYS_OPTIONS" ]]; then
532         printf "%s %s MonkeySphere%s %s\n" "$AUTHORIZED_KEYS_OPTIONS" "$key" "$DATE" "$userID"
533     else
534         printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
535     fi
536 }
537
538 # convert key from gpg to ssh known_hosts format
539 gpg2known_hosts() {
540     local host
541     local keyID
542     local key
543
544     host="$1"
545     keyID="$2"
546
547     key=$(gpg2ssh "$keyID")
548
549     # NOTE: it seems that ssh-keygen -R removes all comment fields from
550     # all lines in the known_hosts file.  why?
551     # NOTE: just in case, the COMMENT can be matched with the
552     # following regexp:
553     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
554     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
555 }
556
557 # convert key from gpg to ssh authorized_keys format
558 gpg2authorized_keys() {
559     local userID
560     local keyID
561     local key
562
563     userID="$1"
564     keyID="$2"
565
566     key=$(gpg2ssh "$keyID")
567
568     # NOTE: just in case, the COMMENT can be matched with the
569     # following regexp:
570     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
571     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
572 }
573
574 ### GPG UTILITIES
575
576 # script to determine if gpg version is equal to or greater than specified version
577 is_gpg_version_greater_equal() {
578     local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
579     local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
580         | tr '.' ' ' | sort -g -k1 -k2 -k3 \
581         | tail -1 | tr ' ' '.')
582     [[ "$gpgVersion" == "$latest" ]]
583 }
584
585 # retrieve all keys with given user id from keyserver
586 # FIXME: need to figure out how to retrieve all matching keys
587 # (not just first N (5 in this case))
588 gpg_fetch_userid() {
589     local returnCode=0
590     local userID
591
592     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
593         return 0
594     fi
595
596     userID="$1"
597
598     log verbose " checking keyserver $KEYSERVER... "
599     echo 1,2,3,4,5 | \
600         gpg --quiet --batch --with-colons \
601         --command-fd 0 --keyserver "$KEYSERVER" \
602         --search ="$userID" &>/dev/null
603     returnCode="$?"
604
605     if [ "$returnCode" != 0 ] ; then
606         log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
607     fi
608
609     return "$returnCode"
610 }
611
612 ########################################################################
613 ### PROCESSING FUNCTIONS
614
615 # userid and key policy checking
616 # the following checks policy on the returned keys
617 # - checks that full key has appropriate valididy (u|f)
618 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
619 # - checks that requested user ID has appropriate validity
620 # (see /usr/share/doc/gnupg/DETAILS.gz)
621 # output is one line for every found key, in the following format:
622 #
623 # flag:sshKey
624 #
625 # "flag" is an acceptability flag, 0 = ok, 1 = bad
626 # "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
627 #
628 # all log output must go to stderr, as stdout is used to pass the
629 # flag:sshKey to the calling function.
630 process_user_id() {
631     local returnCode=0
632     local userID="$1"
633     local requiredCapability
634     local requiredPubCapability
635     local gpgOut
636     local type
637     local validity
638     local keyid
639     local uidfpr
640     local usage
641     local keyOK
642     local uidOK
643     local lastKey
644     local lastKeyOK
645     local fingerprint
646
647     # set the required key capability based on the mode
648     requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
649     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
650
651     # fetch the user ID if necessary/requested
652     gpg_fetch_userid "$userID"
653
654     # output gpg info for (exact) userid and store
655     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
656         --with-fingerprint --with-fingerprint \
657         ="$userID" 2>/dev/null) || returnCode="$?"
658
659     # if the gpg query return code is not 0, return 1
660     if [ "$returnCode" -ne 0 ] ; then
661         log verbose " no primary keys found."
662         return 1
663     fi
664
665     # loop over all lines in the gpg output and process.
666     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
667     while IFS=: read -r type validity keyid uidfpr usage ; do
668         # process based on record type
669         case $type in
670             'pub') # primary keys
671                 # new key, wipe the slate
672                 keyOK=
673                 uidOK=
674                 lastKey=pub
675                 lastKeyOK=
676                 fingerprint=
677
678                 log verbose " primary key found: $keyid"
679
680                 # if overall key is not valid, skip
681                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
682                     log debug "  - unacceptable primary key validity ($validity)."
683                     continue
684                 fi
685                 # if overall key is disabled, skip
686                 if check_capability "$usage" 'D' ; then
687                     log debug "  - key disabled."
688                     continue
689                 fi
690                 # if overall key capability is not ok, skip
691                 if ! check_capability "$usage" $requiredPubCapability ; then
692                     log debug "  - unacceptable primary key capability ($usage)."
693                     continue
694                 fi
695
696                 # mark overall key as ok
697                 keyOK=true
698
699                 # mark primary key as ok if capability is ok
700                 if check_capability "$usage" $requiredCapability ; then
701                     lastKeyOK=true
702                 fi
703                 ;;
704             'uid') # user ids
705                 if [ "$lastKey" != pub ] ; then
706                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
707                     continue
708                 fi
709                 # if an acceptable user ID was already found, skip
710                 if [ "$uidOK" = 'true' ] ; then
711                     continue
712                 fi
713                 # if the user ID does matches...
714                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
715                     # and the user ID validity is ok
716                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
717                         # mark user ID acceptable
718                         uidOK=true
719                     else
720                         log debug "  - unacceptable user ID validity ($validity)."
721                     fi
722                 else
723                     continue
724                 fi
725
726                 # output a line for the primary key
727                 # 0 = ok, 1 = bad
728                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
729                     log verbose "  * acceptable primary key."
730                     if [ -z "$sshKey" ] ; then
731                         log verbose "    ! primary key could not be translated (not RSA?)."
732                     else
733                         echo "0:${sshKey}"
734                     fi
735                 else
736                     log debug "  - unacceptable primary key."
737                     if [ -z "$sshKey" ] ; then
738                         log debug "    ! primary key could not be translated (not RSA?)."
739                     else
740                         echo "1:${sshKey}"
741                     fi
742                 fi
743                 ;;
744             'sub') # sub keys
745                 # unset acceptability of last key
746                 lastKey=sub
747                 lastKeyOK=
748                 fingerprint=
749                 
750                 # don't bother with sub keys if the primary key is not valid
751                 if [ "$keyOK" != true ] ; then
752                     continue
753                 fi
754
755                 # don't bother with sub keys if no user ID is acceptable:
756                 if [ "$uidOK" != true ] ; then
757                     continue
758                 fi
759                 
760                 # if sub key validity is not ok, skip
761                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
762                     log debug "  - unacceptable sub key validity ($validity)."
763                     continue
764                 fi
765                 # if sub key capability is not ok, skip
766                 if ! check_capability "$usage" $requiredCapability ; then
767                     log debug "  - unacceptable sub key capability ($usage)."
768                     continue
769                 fi
770
771                 # mark sub key as ok
772                 lastKeyOK=true
773                 ;;
774             'fpr') # key fingerprint
775                 fingerprint="$uidfpr"
776
777                 sshKey=$(gpg2ssh "$fingerprint")
778
779                 # if the last key was the pub key, skip
780                 if [ "$lastKey" = pub ] ; then
781                     continue
782                 fi
783
784                 # output a line for the sub key
785                 # 0 = ok, 1 = bad
786                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
787                     log verbose "  * acceptable sub key."
788                     if [ -z "$sshKey" ] ; then
789                         log error "    ! sub key could not be translated (not RSA?)."
790                     else
791                         echo "0:${sshKey}"
792                     fi
793                 else
794                     log debug "  - unacceptable sub key."
795                     if [ -z "$sshKey" ] ; then
796                         log debug "    ! sub key could not be translated (not RSA?)."
797                     else
798                         echo "1:${sshKey}"
799                     fi
800                 fi
801                 ;;
802         esac
803     done | sort -t: -k1 -n -r
804     # NOTE: this last sort is important so that the "good" keys (key
805     # flag '0') come last.  This is so that they take precedence when
806     # being processed in the key files over "bad" keys (key flag '1')
807 }
808
809 process_keys_for_file() {
810     local keyFile="$1"
811     local userID="$2"
812     local host
813     local ok
814     local sshKey
815     local keyLine
816
817     log verbose "processing: $userID"
818     log debug "key file: $keyFile"
819
820     IFS=$'\n'
821     for line in $(process_user_id "$userID") ; do
822         ok=${line%%:*}
823         sshKey=${line#*:}
824
825         if [ -z "$sshKey" ] ; then
826             continue
827         fi
828
829         # remove the old key line
830         if [[ "$keyFile" != '-' ]] ; then
831             case "$FILE_TYPE" in
832                 ('authorized_keys')
833                     remove_line "$keyFile" "$sshKey"
834                     ;;
835                 ('known_hosts')
836                     host=${userID#ssh://}
837                     remove_line "$keyFile" "$host" "$sshKey"
838                     ;;
839             esac
840         fi
841
842         # if key OK, add new key line
843         if [ "$ok" -eq '0' ] ; then
844             case "$FILE_TYPE" in
845                 ('raw')
846                     keyLine="$sshKey"
847                     ;;
848                 ('authorized_keys')
849                     keyLine=$(ssh2authorized_keys "$userID" "$sshKey")
850                     ;;
851                 ('known_hosts')
852                     host=${userID#ssh://}
853                     keyLine=$(ssh2known_hosts "$host" "$sshKey")
854                     ;;
855             esac
856
857             echo "$keyLine" | log debug
858             if [[ "$keyFile" == '-' ]] ; then
859                 echo "$keyLine"
860             else
861                 echo "$keyLine" >>"$keyFile"
862             fi
863         fi
864     done
865 }
866
867 # process an authorized_user_ids file on stdin for authorized_keys
868 process_authorized_user_ids() {
869     local authorizedKeys="$1"
870     declare -i nline=0
871     local line
872     declare -a userIDs
873     declare -a koptions
874
875     # extract user IDs from authorized_user_ids file
876     IFS=$'\n'
877     while read line ; do
878         case "$line" in
879             ("#"*)
880                 continue
881                 ;;
882             (" "*|$'\t'*)
883                 if [[ -z ${koptions[${nline}]} ]]; then
884                     koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
885                 else
886                     koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
887                 fi
888                 ;;
889             (*)
890                 ((++nline))
891                 userIDs[${nline}]="$line"
892                 unset koptions[${nline}] || true
893                 ;;
894         esac
895     done
896
897     for i in $(seq 1 $nline); do
898         AUTHORIZED_KEYS_OPTIONS="${koptions[$i]}" FILE_TYPE='authorized_keys' process_keys_for_file "$authorizedKeys" "${userIDs[$i]}" || returnCode="$?"
899     done
900 }
901
902 # takes a gpg key or keys on stdin, and outputs a list of
903 # fingerprints, one per line:
904 list_primary_fingerprints() {
905     local fake=$(msmktempdir)
906     trap "rm -rf $fake" EXIT
907     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
908     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
909         awk -F: '/^fpr:/{ print $10 }'
910     trap - EXIT
911     rm -rf "$fake"
912 }
913
914 # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
915 # key identifier as $1, and outputs the gpg-formatted information for
916 # the requested keys from the material on stdin
917 get_cert_info() {
918     local fake=$(msmktempdir)
919     trap "rm -rf $fake" EXIT
920     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
921     GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
922     trap - EXIT
923     rm -rf "$fake"
924 }
925
926
927 check_cruft_file() {
928     local loc="$1"
929     local version="$2"
930     
931     if [ -e "$loc" ] ; then
932         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
933     fi
934 }
935
936 check_upgrade_dir() {
937     local loc="$1"
938     local version="$2"
939
940     if [ -d "$loc" ] ; then
941         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
942     fi
943 }
944
945 ## look for cruft from old versions of the monkeysphere, and notice if
946 ## upgrades have not been run:
947 report_cruft() {
948     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
949     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
950
951     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
952     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
953
954     local found=
955     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
956         if [ -d "$foo" ] ; then
957             printf "! %s\n" "$foo" | log info
958             found=true
959         fi
960     done
961     if [ "$found" ] ; then
962         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
963     fi
964 }