fix need for only single argument to gpg_sphere
[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 string
329     local tempfile
330
331     file="$1"
332     string="$2"
333
334     if [ -z "$file" -o -z "$string" ] ; then
335         return 1
336     fi
337
338     if [ ! -e "$file" ] ; then
339         return 1
340     fi
341
342     # if the string is in the file...
343     if grep -q -F "$string" "$file" 2>/dev/null ; then
344         tempfile=$(mktemp "${file}.XXXXXXX") || \
345             failure "Unable to make temp file '${file}.XXXXXXX'"
346         
347         # remove the line with the string, and return 0
348         grep -v -F "$string" "$file" >"$tempfile"
349         cat "$tempfile" > "$file"
350         rm "$tempfile"
351         return 0
352     # otherwise return 1
353     else
354         return 1
355     fi
356 }
357
358 # remove all lines with MonkeySphere strings in file
359 remove_monkeysphere_lines() {
360     local file
361     local tempfile
362
363     file="$1"
364
365     # return error if file does not exist
366     if [ ! -e "$file" ] ; then
367         return 1
368     fi
369
370     # just return ok if the file is empty, since there aren't any
371     # lines to remove
372     if [ ! -s "$file" ] ; then
373         return 0
374     fi
375
376     tempfile=$(mktemp "${file}.XXXXXXX") || \
377         failure "Could not make temporary file '${file}.XXXXXXX'."
378
379     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
380         "$file" >"$tempfile"
381     cat "$tempfile" > "$file"
382     rm "$tempfile"
383 }
384
385 # translate ssh-style path variables %h and %u
386 translate_ssh_variables() {
387     local uname
388     local home
389
390     uname="$1"
391     path="$2"
392
393     # get the user's home directory
394     userHome=$(get_homedir "$uname")
395
396     # translate '%u' to user name
397     path=${path/\%u/"$uname"}
398     # translate '%h' to user home directory
399     path=${path/\%h/"$userHome"}
400
401     echo "$path"
402 }
403
404 # test that a string to conforms to GPG's expiration format
405 test_gpg_expire() {
406     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
407 }
408
409 # check that a file is properly owned, and that all it's parent
410 # directories are not group/other writable
411 check_key_file_permissions() {
412     local uname
413     local path
414
415     uname="$1"
416     path="$2"
417
418     if [ "$STRICT_MODES" = 'false' ] ; then
419         log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
420         return 0
421     fi
422     log debug "checking path permission '$path'..."
423     "${SYSSHAREDIR}/checkperms" "$uname" "$path"
424 }
425
426 # return a list of all users on the system
427 list_users() {
428     if type getent &>/dev/null ; then
429         # for linux and FreeBSD systems
430         getent passwd | cut -d: -f1
431     elif type dscl &>/dev/null ; then
432         # for Darwin systems
433         dscl localhost -list /Search/Users
434     else
435         failure "Neither getent or dscl is in the path!  Could not determine list of users."
436     fi
437 }
438
439 # take one argument, a service name.  in response, print a series of
440 # lines, each with a unique numeric port number that might be
441 # associated with that service name.  (e.g. in: "https", out: "443")
442 # if nothing is found, print nothing, and return 0.
443
444 # return 1 if there was an error in the search somehow
445 get_port_for_service() {
446
447     [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
448         failure $(printf "This is not a valid service name: '%s'" "$1")
449     if type getent &>/dev/null ; then
450         # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
451         (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
452     elif [ -r /etc/services ] ; then
453         # fall back to /etc/services for systems that don't have getent (MacOS?)
454         # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
455         awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
456     else
457         return 1
458     fi
459 }
460
461 # return the path to the home directory of a user
462 get_homedir() {
463     local uname=${1:-`whoami`}
464     eval "echo ~${uname}"
465 }
466
467 # return the primary group of a user
468 get_primary_group() {
469     local uname=${1:-`whoami`}
470     groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
471 }
472
473 ### CONVERSION UTILITIES
474
475 # output the ssh key for a given key ID
476 gpg2ssh() {
477     local keyID
478     
479     keyID="$1"
480
481     gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
482 }
483
484 # output known_hosts line from ssh key
485 ssh2known_hosts() {
486     local host
487     local port
488     local key
489
490     # FIXME this does not properly deal with IPv6 hosts using the
491     # standard port (because it's unclear whether their final
492     # colon-delimited address section is a port number or an address
493     # string)
494     host=${1%:*}
495     port=${1##*:}
496     key="$2"
497
498     # specify the host and port properly for new ssh known_hosts
499     # format
500     if [ "$port" != "$host" ] ; then
501         host="[${host}]:${port}"
502     fi
503     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
504 }
505
506 # output authorized_keys line from ssh key
507 ssh2authorized_keys() {
508     local koptions="$1"
509     local userID="$2"
510     local key="$3"
511
512     if [[ -z "$koptions" ]]; then
513         printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
514     else
515         printf "%s %s MonkeySphere%s %s\n" "$koptions" "$key" "$DATE" "$userID"
516     fi
517 }
518
519 # convert key from gpg to ssh known_hosts format
520 gpg2known_hosts() {
521     local host
522     local keyID
523     local key
524
525     host="$1"
526     keyID="$2"
527
528     key=$(gpg2ssh "$keyID")
529
530     # NOTE: it seems that ssh-keygen -R removes all comment fields from
531     # all lines in the known_hosts file.  why?
532     # NOTE: just in case, the COMMENT can be matched with the
533     # following regexp:
534     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
535     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
536 }
537
538 # convert key from gpg to ssh authorized_keys format
539 gpg2authorized_keys() {
540     local userID
541     local keyID
542     local key
543
544     userID="$1"
545     keyID="$2"
546
547     key=$(gpg2ssh "$keyID")
548
549     # NOTE: just in case, the COMMENT can be matched with the
550     # following regexp:
551     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
552     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
553 }
554
555 ### GPG UTILITIES
556
557 # script to determine if gpg version is equal to or greater than specified version
558 is_gpg_version_greater_equal() {
559     local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
560     local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
561         | tr '.' ' ' | sort -g -k1 -k2 -k3 \
562         | tail -1 | tr ' ' '.')
563     [[ "$gpgVersion" == "$latest" ]]
564 }
565
566 # retrieve all keys with given user id from keyserver
567 # FIXME: need to figure out how to retrieve all matching keys
568 # (not just first N (5 in this case))
569 gpg_fetch_userid() {
570     local returnCode=0
571     local userID
572
573     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
574         return 0
575     fi
576
577     userID="$1"
578
579     log verbose " checking keyserver $KEYSERVER... "
580     echo 1,2,3,4,5 | \
581         gpg --quiet --batch --with-colons \
582         --command-fd 0 --keyserver "$KEYSERVER" \
583         --search ="$userID" &>/dev/null
584     returnCode="$?"
585
586     if [ "$returnCode" != 0 ] ; then
587         log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
588     fi
589
590     return "$returnCode"
591 }
592
593 ########################################################################
594 ### PROCESSING FUNCTIONS
595
596 # userid and key policy checking
597 # the following checks policy on the returned keys
598 # - checks that full key has appropriate valididy (u|f)
599 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
600 # - checks that requested user ID has appropriate validity
601 # (see /usr/share/doc/gnupg/DETAILS.gz)
602 # output is one line for every found key, in the following format:
603 #
604 # flag:sshKey
605 #
606 # "flag" is an acceptability flag, 0 = ok, 1 = bad
607 # "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
608 #
609 # all log output must go to stderr, as stdout is used to pass the
610 # flag:sshKey to the calling function.
611 process_user_id() {
612     local returnCode=0
613     local userID="$1"
614     local requiredCapability
615     local requiredPubCapability
616     local gpgOut
617     local type
618     local validity
619     local keyid
620     local uidfpr
621     local usage
622     local keyOK
623     local uidOK
624     local lastKey
625     local lastKeyOK
626     local fingerprint
627
628     # set the required key capability based on the mode
629     requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
630     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
631
632     # fetch the user ID if necessary/requested
633     gpg_fetch_userid "$userID"
634
635     # output gpg info for (exact) userid and store
636     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
637         --with-fingerprint --with-fingerprint \
638         ="$userID" 2>/dev/null) || returnCode="$?"
639
640     # if the gpg query return code is not 0, return 1
641     if [ "$returnCode" -ne 0 ] ; then
642         log verbose " no primary keys found."
643         return 1
644     fi
645
646     # loop over all lines in the gpg output and process.
647     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
648     while IFS=: read -r type validity keyid uidfpr usage ; do
649         # process based on record type
650         case $type in
651             'pub') # primary keys
652                 # new key, wipe the slate
653                 keyOK=
654                 uidOK=
655                 lastKey=pub
656                 lastKeyOK=
657                 fingerprint=
658
659                 log verbose " primary key found: $keyid"
660
661                 # if overall key is not valid, skip
662                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
663                     log debug "  - unacceptable primary key validity ($validity)."
664                     continue
665                 fi
666                 # if overall key is disabled, skip
667                 if check_capability "$usage" 'D' ; then
668                     log debug "  - key disabled."
669                     continue
670                 fi
671                 # if overall key capability is not ok, skip
672                 if ! check_capability "$usage" $requiredPubCapability ; then
673                     log debug "  - unacceptable primary key capability ($usage)."
674                     continue
675                 fi
676
677                 # mark overall key as ok
678                 keyOK=true
679
680                 # mark primary key as ok if capability is ok
681                 if check_capability "$usage" $requiredCapability ; then
682                     lastKeyOK=true
683                 fi
684                 ;;
685             'uid') # user ids
686                 if [ "$lastKey" != pub ] ; then
687                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
688                     continue
689                 fi
690                 # if an acceptable user ID was already found, skip
691                 if [ "$uidOK" = 'true' ] ; then
692                     continue
693                 fi
694                 # if the user ID does matches...
695                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
696                     # and the user ID validity is ok
697                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
698                         # mark user ID acceptable
699                         uidOK=true
700                     else
701                         log debug "  - unacceptable user ID validity ($validity)."
702                     fi
703                 else
704                     continue
705                 fi
706
707                 # output a line for the primary key
708                 # 0 = ok, 1 = bad
709                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
710                     log verbose "  * acceptable primary key."
711                     if [ -z "$sshKey" ] ; then
712                         log verbose "    ! primary key could not be translated (not RSA?)."
713                     else
714                         echo "0:${sshKey}"
715                     fi
716                 else
717                     log debug "  - unacceptable primary key."
718                     if [ -z "$sshKey" ] ; then
719                         log debug "    ! primary key could not be translated (not RSA?)."
720                     else
721                         echo "1:${sshKey}"
722                     fi
723                 fi
724                 ;;
725             'sub') # sub keys
726                 # unset acceptability of last key
727                 lastKey=sub
728                 lastKeyOK=
729                 fingerprint=
730                 
731                 # don't bother with sub keys if the primary key is not valid
732                 if [ "$keyOK" != true ] ; then
733                     continue
734                 fi
735
736                 # don't bother with sub keys if no user ID is acceptable:
737                 if [ "$uidOK" != true ] ; then
738                     continue
739                 fi
740                 
741                 # if sub key validity is not ok, skip
742                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
743                     log debug "  - unacceptable sub key validity ($validity)."
744                     continue
745                 fi
746                 # if sub key capability is not ok, skip
747                 if ! check_capability "$usage" $requiredCapability ; then
748                     log debug "  - unacceptable sub key capability ($usage)."
749                     continue
750                 fi
751
752                 # mark sub key as ok
753                 lastKeyOK=true
754                 ;;
755             'fpr') # key fingerprint
756                 fingerprint="$uidfpr"
757
758                 sshKey=$(gpg2ssh "$fingerprint")
759
760                 # if the last key was the pub key, skip
761                 if [ "$lastKey" = pub ] ; then
762                     continue
763                 fi
764
765                 # output a line for the sub key
766                 # 0 = ok, 1 = bad
767                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
768                     log verbose "  * acceptable sub key."
769                     if [ -z "$sshKey" ] ; then
770                         log error "    ! sub key could not be translated (not RSA?)."
771                     else
772                         echo "0:${sshKey}"
773                     fi
774                 else
775                     log debug "  - unacceptable sub key."
776                     if [ -z "$sshKey" ] ; then
777                         log debug "    ! sub key could not be translated (not RSA?)."
778                     else
779                         echo "1:${sshKey}"
780                     fi
781                 fi
782                 ;;
783         esac
784     done | sort -t: -k1 -n -r
785     # NOTE: this last sort is important so that the "good" keys (key
786     # flag '0') come last.  This is so that they take precedence when
787     # being processed in the key files over "bad" keys (key flag '1')
788 }
789
790 # output all valid keys for specified user ID literal
791 keys_for_userid() {
792     local userID
793     local noKey=
794     local nKeys
795     local nKeysOK
796     local ok
797     local sshKey
798     local tmpfile
799
800     userID="$1"
801
802     log verbose "processing: $userID"
803
804     nKeys=0
805     nKeysOK=0
806
807     IFS=$'\n'
808     for line in $(process_user_id "${userID}") ; do
809         # note that key was found
810         nKeys=$((nKeys+1))
811
812         ok=$(echo "$line" | cut -d: -f1)
813         sshKey=$(echo "$line" | cut -d: -f2)
814
815         if [ -z "$sshKey" ] ; then
816             continue
817         fi
818
819         # if key OK, output key to stdout
820         if [ "$ok" -eq '0' ] ; then
821             # note that key was found ok
822             nKeysOK=$((nKeysOK+1))
823
824             printf '%s\n' "$sshKey"
825         fi
826     done
827
828     # if at least one key was found...
829     if [ "$nKeys" -gt 0 ] ; then
830         # if ok keys were found, return 0
831         if [ "$nKeysOK" -gt 0 ] ; then
832             return 0
833         # else return 2
834         else
835             return 2
836         fi
837     # if no keys were found, return 1
838     else
839         return 1
840     fi
841 }
842
843 # process a single host in the known_host file
844 process_host_known_hosts() {
845     local host
846     local userID
847     local noKey=
848     local nKeys
849     local nKeysOK
850     local ok
851     local sshKey
852     local tmpfile
853
854     # set the key processing mode
855     export REQUIRED_KEY_CAPABILITY="$REQUIRED_HOST_KEY_CAPABILITY"
856
857     host="$1"
858     userID="ssh://${host}"
859
860     log verbose "processing: $host"
861
862     nKeys=0
863     nKeysOK=0
864
865     IFS=$'\n'
866     for line in $(process_user_id "${userID}") ; do
867         # note that key was found
868         nKeys=$((nKeys+1))
869
870         ok=$(echo "$line" | cut -d: -f1)
871         sshKey=$(echo "$line" | cut -d: -f2)
872
873         if [ -z "$sshKey" ] ; then
874             continue
875         fi
876
877         # remove any old host key line, and note if removed nothing is
878         # removed
879         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
880
881         # if key OK, add new host line
882         if [ "$ok" -eq '0' ] ; then
883             # note that key was found ok
884             nKeysOK=$((nKeysOK+1))
885
886             # hash if specified
887             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
888                 if (type ssh-keygen >/dev/null) ; then
889                 # FIXME: this is really hackish cause ssh-keygen won't
890                 # hash from stdin to stdout
891                     tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
892                     ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
893                     ssh-keygen -H -f "$tmpfile" 2>/dev/null
894                     cat "$tmpfile" >> "$KNOWN_HOSTS"
895                     rm -f "$tmpfile" "${tmpfile}.old"
896                 else
897        # FIXME: we could do this without needing ssh-keygen.  hashed
898        # known_hosts looks like: |1|X|Y where 1 means SHA1 (nothing
899        # else is defined in openssh sources), X is the salt (same
900        # length as the digest output), base64-encoded, and Y is the
901        # digested hostname (also base64-encoded).
902
903        # see hostfile.{c,h} in openssh sources.
904
905                     failure "Cannot hash known_hosts as requested"
906                 fi
907             else
908                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
909             fi
910
911             # log if this is a new key to the known_hosts file
912             if [ "$noKey" ] ; then
913                 log info "* new key for $host added to known_hosts file."
914             fi
915         fi
916     done
917
918     # if at least one key was found...
919     if [ "$nKeys" -gt 0 ] ; then
920         # if ok keys were found, return 0
921         if [ "$nKeysOK" -gt 0 ] ; then
922             return 0
923         # else return 2
924         else
925             return 2
926         fi
927     # if no keys were found, return 1
928     else
929         return 1
930     fi
931 }
932
933 # update the known_hosts file for a set of hosts listed on command
934 # line
935 update_known_hosts() {
936     local returnCode=0
937     local nHosts
938     local nHostsOK
939     local nHostsBAD
940     local fileCheck
941     local host
942     local newUmask
943
944     # the number of hosts specified on command line
945     nHosts="$#"
946
947     nHostsOK=0
948     nHostsBAD=0
949
950     # touch the known_hosts file so that the file permission check
951     # below won't fail upon not finding the file
952     if [ ! -f "$KNOWN_HOSTS" ]; then
953         # make sure to create any files or directories with the appropriate write bits turned off:
954         newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
955         [ -d $(dirname "$KNOWN_HOSTS") ] \
956             || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
957             || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
958         # make sure to create this file with the appropriate bits turned off:
959         (umask "$newUmask" && touch "$KNOWN_HOSTS") \
960             || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
961     fi
962
963     # check permissions on the known_hosts file path
964     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
965         || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
966
967     # create a lockfile on known_hosts:
968     lock create "$KNOWN_HOSTS"
969     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
970     trap "lock remove $KNOWN_HOSTS" EXIT
971
972     # note pre update file checksum
973     fileCheck=$(file_hash "$KNOWN_HOSTS")
974
975     for host ; do
976         # process the host
977         process_host_known_hosts "$host" || returnCode="$?"
978         # note the result
979         case "$returnCode" in
980             0)
981                 nHostsOK=$((nHostsOK+1))
982                 ;;
983             2)
984                 nHostsBAD=$((nHostsBAD+1))
985                 ;;
986         esac
987
988         # touch the lockfile, for good measure.
989         lock touch "$KNOWN_HOSTS"
990     done
991
992     # remove the lockfile and the trap
993     lock remove "$KNOWN_HOSTS"
994     trap - EXIT
995
996     # note if the known_hosts file was updated
997     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
998         log debug "known_hosts file updated."
999     fi
1000
1001     # if an acceptable host was found, return 0
1002     if [ "$nHostsOK" -gt 0 ] ; then
1003         return 0
1004     # else if no ok hosts were found...
1005     else
1006         # if no bad host were found then no hosts were found at all,
1007         # and return 1
1008         if [ "$nHostsBAD" -eq 0 ] ; then
1009             return 1
1010         # else if at least one bad host was found, return 2
1011         else
1012             return 2
1013         fi
1014     fi
1015 }
1016
1017 # process hosts from a known_hosts file
1018 process_known_hosts() {
1019     local hosts
1020
1021     # exit if the known_hosts file does not exist
1022     if [ ! -e "$KNOWN_HOSTS" ] ; then
1023         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
1024     fi
1025
1026     log debug "processing known_hosts file:"
1027     log debug " $KNOWN_HOSTS"
1028
1029     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
1030
1031     if [ -z "$hosts" ] ; then
1032         log debug "no hosts to process."
1033         return
1034     fi
1035
1036     # take all the hosts from the known_hosts file (first
1037     # field), grep out all the hashed hosts (lines starting
1038     # with '|')...
1039     update_known_hosts $hosts
1040 }
1041
1042 # process uids for the authorized_keys file
1043 process_uid_authorized_keys() {
1044     local userID
1045     local koptions
1046     local nKeys
1047     local nKeysOK
1048     local ok
1049     local sshKey
1050
1051     # set the key processing mode
1052     export REQUIRED_KEY_CAPABILITY="$REQUIRED_USER_KEY_CAPABILITY"
1053
1054     koptions="$1"
1055     userID="$2"
1056
1057     log verbose "processing: $userID"
1058
1059     nKeys=0
1060     nKeysOK=0
1061
1062     IFS=$'\n'
1063     for line in $(process_user_id "$userID") ; do
1064         # note that key was found
1065         nKeys=$((nKeys+1))
1066
1067         ok=$(echo "$line" | cut -d: -f1)
1068         sshKey=$(echo "$line" | cut -d: -f2)
1069
1070         if [ -z "$sshKey" ] ; then
1071             continue
1072         fi
1073
1074         # remove the old host key line
1075         remove_line "$AUTHORIZED_KEYS" "$sshKey"
1076
1077         # if key OK, add new host line
1078         if [ "$ok" -eq '0' ] ; then
1079             # note that key was found ok
1080             nKeysOK=$((nKeysOK+1))
1081
1082             ssh2authorized_keys "$koptions" "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
1083         fi
1084     done
1085
1086     # if at least one key was found...
1087     if [ "$nKeys" -gt 0 ] ; then
1088         # if ok keys were found, return 0
1089         if [ "$nKeysOK" -gt 0 ] ; then
1090             return 0
1091         # else return 2
1092         else
1093             return 2
1094         fi
1095     # if no keys were found, return 1
1096     else
1097         return 1
1098     fi
1099 }
1100
1101 # update the authorized_keys files from a list of user IDs on command
1102 # line
1103 update_authorized_keys() {
1104     local returnCode=0
1105     local userID
1106     local nIDs
1107     local nIDsOK
1108     local nIDsBAD
1109     local fileCheck
1110     local x koptions
1111     declare -i argtype
1112
1113     if (( $# % 2 )); then log error "Bad number of arguments; this should never happen."; return 1; fi
1114
1115     # the number of ids specified on command line
1116     (( nIDs=$#/2 ))
1117     (( argtype=0 ))
1118
1119     nIDsOK=0
1120     nIDsBAD=0
1121
1122     log debug "updating authorized_keys file:"
1123     log debug " $AUTHORIZED_KEYS"
1124
1125     # check permissions on the authorized_keys file path
1126     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1127
1128     # create a lockfile on authorized_keys
1129     lock create "$AUTHORIZED_KEYS"
1130     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1131     trap "lock remove $AUTHORIZED_KEYS" EXIT
1132
1133     # note pre update file checksum
1134     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1135
1136     # remove any monkeysphere lines from authorized_keys file
1137     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1138
1139     for x; do
1140         (( argtype++ ))
1141         if (( $argtype % 2 )); then
1142             koptions="$x"
1143         else
1144             userID="$x"
1145
1146             # process the user ID, change return code if key not found
1147             # for user ID
1148             process_uid_authorized_keys "$koptions" "$userID" || returnCode="$?"
1149
1150             # note the result
1151             case "$returnCode" in
1152                 0)
1153                     nIDsOK=$((nIDsOK+1))
1154                     ;;
1155                 2)
1156                     nIDsBAD=$((nIDsBAD+1))
1157                     ;;
1158             esac
1159
1160             # touch the lockfile, for good measure.
1161             lock touch "$AUTHORIZED_KEYS"
1162         fi
1163     done
1164
1165     # remove the lockfile and the trap
1166     lock remove "$AUTHORIZED_KEYS"
1167
1168     # remove the trap
1169     trap - EXIT
1170
1171     # note if the authorized_keys file was updated
1172     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1173         log debug "authorized_keys file updated."
1174     fi
1175
1176     # if an acceptable id was found, return 0
1177     if [ "$nIDsOK" -gt 0 ] ; then
1178         return 0
1179     # else if no ok ids were found...
1180     else
1181         # if no bad ids were found then no ids were found at all, and
1182         # return 1
1183         if [ "$nIDsBAD" -eq 0 ] ; then
1184             return 1
1185         # else if at least one bad id was found, return 2
1186         else
1187             return 2
1188         fi
1189     fi
1190 }
1191
1192 # process an authorized_user_ids file for authorized_keys
1193 process_authorized_user_ids() {
1194     local line
1195     declare -i nline
1196     declare -a userIDs
1197     declare -a koptions
1198     declare -a export_array
1199
1200     authorizedUserIDs="$1"
1201
1202     (( nline=0 ))
1203
1204     # exit if the authorized_user_ids file is empty
1205     if [ ! -e "$authorizedUserIDs" ] ; then
1206         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1207     fi
1208
1209     log debug "processing authorized_user_ids file:"
1210     log debug " $authorizedUserIDs"
1211
1212     # check permissions on the authorized_user_ids file path
1213     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1214
1215     if ! meat "$authorizedUserIDs" >/dev/null ; then
1216         log debug " no user IDs to process."
1217         return
1218     fi
1219
1220     nline=0
1221
1222     # extract user IDs from authorized_user_ids file
1223     IFS=$'\n'
1224     for line in $(meat "$authorizedUserIDs") ; do
1225         case "$line" in
1226             (" "*|$'\t'*)
1227                 if [[ -z ${koptions[${nline}]} ]]; then
1228                     koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
1229                 else
1230                     koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
1231                 fi
1232                 ;;
1233             (*)
1234                 ((nline++))
1235                 userIDs[${nline}]="$line"
1236                 unset koptions[${nline}] || true
1237                 ;;
1238         esac
1239     done
1240
1241     for i in $(seq 1 $nline); do
1242         export_array+=("${koptions[$i]}" "${userIDs[$i]}")
1243     done
1244
1245     update_authorized_keys "${export_array[@]}"
1246 }
1247
1248 # takes a gpg key or keys on stdin, and outputs a list of
1249 # fingerprints, one per line:
1250 list_primary_fingerprints() {
1251     local fake=$(msmktempdir)
1252     trap "rm -rf $fake" EXIT
1253     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
1254     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1255         awk -F: '/^fpr:/{ print $10 }'
1256     trap - EXIT
1257     rm -rf "$fake"
1258 }
1259
1260 # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
1261 # key identifier as $1, and outputs the gpg-formatted information for
1262 # the requested keys from the material on stdin
1263 get_cert_info() {
1264     local fake=$(msmktempdir)
1265     trap "rm -rf $fake" EXIT
1266     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
1267     GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
1268     trap - EXIT
1269     rm -rf "$fake"
1270 }
1271
1272
1273 check_cruft_file() {
1274     local loc="$1"
1275     local version="$2"
1276     
1277     if [ -e "$loc" ] ; then
1278         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
1279     fi
1280 }
1281
1282 check_upgrade_dir() {
1283     local loc="$1"
1284     local version="$2"
1285
1286     if [ -d "$loc" ] ; then
1287         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
1288     fi
1289 }
1290
1291 ## look for cruft from old versions of the monkeysphere, and notice if
1292 ## upgrades have not been run:
1293 report_cruft() {
1294     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1295     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1296
1297     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1298     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1299
1300     local found=
1301     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1302         if [ -d "$foo" ] ; then
1303             printf "! %s\n" "$foo" | log info
1304             found=true
1305         fi
1306     done
1307     if [ "$found" ] ; then
1308         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
1309     fi
1310 }