dump gpg --import error spew to /dev/null during hackish uses of gpg
[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 "$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 userID
509     local key
510     
511     userID="$1"
512     key="$2"
513
514     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
515 }
516
517 # convert key from gpg to ssh known_hosts format
518 gpg2known_hosts() {
519     local host
520     local keyID
521     local key
522
523     host="$1"
524     keyID="$2"
525
526     key=$(gpg2ssh "$keyID")
527
528     # NOTE: it seems that ssh-keygen -R removes all comment fields from
529     # all lines in the known_hosts file.  why?
530     # NOTE: just in case, the COMMENT can be matched with the
531     # following regexp:
532     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
533     printf "%s %s MonkeySphere%s\n" "$host" "$key" "$DATE"
534 }
535
536 # convert key from gpg to ssh authorized_keys format
537 gpg2authorized_keys() {
538     local userID
539     local keyID
540     local key
541
542     userID="$1"
543     keyID="$2"
544
545     key=$(gpg2ssh "$keyID")
546
547     # NOTE: just in case, the COMMENT can be matched with the
548     # following regexp:
549     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
550     printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
551 }
552
553 ### GPG UTILITIES
554
555 # script to determine if gpg version is equal to or greater than specified version
556 is_gpg_version_greater_equal() {
557     local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
558     local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
559         | tr '.' ' ' | sort -g -k1 -k2 -k3 \
560         | tail -1 | tr ' ' '.')
561     [[ "$gpgVersion" == "$latest" ]]
562 }
563
564 # retrieve all keys with given user id from keyserver
565 # FIXME: need to figure out how to retrieve all matching keys
566 # (not just first N (5 in this case))
567 gpg_fetch_userid() {
568     local returnCode=0
569     local userID
570
571     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
572         return 0
573     fi
574
575     userID="$1"
576
577     log verbose " checking keyserver $KEYSERVER... "
578     echo 1,2,3,4,5 | \
579         gpg --quiet --batch --with-colons \
580         --command-fd 0 --keyserver "$KEYSERVER" \
581         --search ="$userID" &>/dev/null
582     returnCode="$?"
583
584     return "$returnCode"
585 }
586
587 ########################################################################
588 ### PROCESSING FUNCTIONS
589
590 # userid and key policy checking
591 # the following checks policy on the returned keys
592 # - checks that full key has appropriate valididy (u|f)
593 # - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
594 # - checks that requested user ID has appropriate validity
595 # (see /usr/share/doc/gnupg/DETAILS.gz)
596 # output is one line for every found key, in the following format:
597 #
598 # flag:sshKey
599 #
600 # "flag" is an acceptability flag, 0 = ok, 1 = bad
601 # "sshKey" is the translated gpg key
602 #
603 # all log output must go to stderr, as stdout is used to pass the
604 # flag:sshKey to the calling function.
605 process_user_id() {
606     local returnCode=0
607     local userID
608     local requiredCapability
609     local requiredPubCapability
610     local gpgOut
611     local type
612     local validity
613     local keyid
614     local uidfpr
615     local usage
616     local keyOK
617     local uidOK
618     local lastKey
619     local lastKeyOK
620     local fingerprint
621
622     userID="$1"
623
624     # set the required key capability based on the mode
625     requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
626     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
627
628     # fetch the user ID if necessary/requested
629     gpg_fetch_userid "$userID"
630
631     # output gpg info for (exact) userid and store
632     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
633         --with-fingerprint --with-fingerprint \
634         ="$userID" 2>/dev/null) || returnCode="$?"
635
636     # if the gpg query return code is not 0, return 1
637     if [ "$returnCode" -ne 0 ] ; then
638         log verbose " no primary keys found."
639         return 1
640     fi
641
642     # loop over all lines in the gpg output and process.
643     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
644     while IFS=: read -r type validity keyid uidfpr usage ; do
645         # process based on record type
646         case $type in
647             'pub') # primary keys
648                 # new key, wipe the slate
649                 keyOK=
650                 uidOK=
651                 lastKey=pub
652                 lastKeyOK=
653                 fingerprint=
654
655                 log verbose " primary key found: $keyid"
656
657                 # if overall key is not valid, skip
658                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
659                     log debug "  - unacceptable primary key validity ($validity)."
660                     continue
661                 fi
662                 # if overall key is disabled, skip
663                 if check_capability "$usage" 'D' ; then
664                     log debug "  - key disabled."
665                     continue
666                 fi
667                 # if overall key capability is not ok, skip
668                 if ! check_capability "$usage" $requiredPubCapability ; then
669                     log debug "  - unacceptable primary key capability ($usage)."
670                     continue
671                 fi
672
673                 # mark overall key as ok
674                 keyOK=true
675
676                 # mark primary key as ok if capability is ok
677                 if check_capability "$usage" $requiredCapability ; then
678                     lastKeyOK=true
679                 fi
680                 ;;
681             'uid') # user ids
682                 if [ "$lastKey" != pub ] ; then
683                     log verbose " ! got a user ID after a sub key?!  user IDs should only follow primary keys!"
684                     continue
685                 fi
686                 # if an acceptable user ID was already found, skip
687                 if [ "$uidOK" = 'true' ] ; then
688                     continue
689                 fi
690                 # if the user ID does matches...
691                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
692                     # and the user ID validity is ok
693                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
694                         # mark user ID acceptable
695                         uidOK=true
696                     else
697                         log debug "  - unacceptable user ID validity ($validity)."
698                     fi
699                 else
700                     continue
701                 fi
702
703                 # output a line for the primary key
704                 # 0 = ok, 1 = bad
705                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
706                     log verbose "  * acceptable primary key."
707                     if [ -z "$sshKey" ] ; then
708                         log error "    ! primary key could not be translated (not RSA?)."
709                     else
710                         echo "0:${sshKey}"
711                     fi
712                 else
713                     log debug "  - unacceptable primary key."
714                     if [ -z "$sshKey" ] ; then
715                         log debug "    ! primary key could not be translated (not RSA?)."
716                     else
717                         echo "1:${sshKey}"
718                     fi
719                 fi
720                 ;;
721             'sub') # sub keys
722                 # unset acceptability of last key
723                 lastKey=sub
724                 lastKeyOK=
725                 fingerprint=
726                 
727                 # don't bother with sub keys if the primary key is not valid
728                 if [ "$keyOK" != true ] ; then
729                     continue
730                 fi
731
732                 # don't bother with sub keys if no user ID is acceptable:
733                 if [ "$uidOK" != true ] ; then
734                     continue
735                 fi
736                 
737                 # if sub key validity is not ok, skip
738                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
739                     log debug "  - unacceptable sub key validity ($validity)."
740                     continue
741                 fi
742                 # if sub key capability is not ok, skip
743                 if ! check_capability "$usage" $requiredCapability ; then
744                     log debug "  - unacceptable sub key capability ($usage)."
745                     continue
746                 fi
747
748                 # mark sub key as ok
749                 lastKeyOK=true
750                 ;;
751             'fpr') # key fingerprint
752                 fingerprint="$uidfpr"
753
754                 sshKey=$(gpg2ssh "$fingerprint")
755
756                 # if the last key was the pub key, skip
757                 if [ "$lastKey" = pub ] ; then
758                     continue
759                 fi
760
761                 # output a line for the sub key
762                 # 0 = ok, 1 = bad
763                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
764                     log verbose "  * acceptable sub key."
765                     if [ -z "$sshKey" ] ; then
766                         log error "    ! sub key could not be translated (not RSA?)."
767                     else
768                         echo "0:${sshKey}"
769                     fi
770                 else
771                     log debug "  - unacceptable sub key."
772                     if [ -z "$sshKey" ] ; then
773                         log debug "    ! sub key could not be translated (not RSA?)."
774                     else
775                         echo "1:${sshKey}"
776                     fi
777                 fi
778                 ;;
779         esac
780     done | sort -t: -k1 -n -r
781     # NOTE: this last sort is important so that the "good" keys (key
782     # flag '0') come last.  This is so that they take precedence when
783     # being processed in the key files over "bad" keys (key flag '1')
784 }
785
786 # output all valid keys for specified user ID literal
787 keys_for_userid() {
788     local userID
789     local noKey=
790     local nKeys
791     local nKeysOK
792     local ok
793     local sshKey
794     local tmpfile
795
796     userID="$1"
797
798     log verbose "processing: $userID"
799
800     nKeys=0
801     nKeysOK=0
802
803     IFS=$'\n'
804     for line in $(process_user_id "${userID}") ; do
805         # note that key was found
806         nKeys=$((nKeys+1))
807
808         ok=$(echo "$line" | cut -d: -f1)
809         sshKey=$(echo "$line" | cut -d: -f2)
810
811         if [ -z "$sshKey" ] ; then
812             continue
813         fi
814
815         # if key OK, output key to stdout
816         if [ "$ok" -eq '0' ] ; then
817             # note that key was found ok
818             nKeysOK=$((nKeysOK+1))
819
820             printf '%s\n' "$sshKey"
821         fi
822     done
823
824     # if at least one key was found...
825     if [ "$nKeys" -gt 0 ] ; then
826         # if ok keys were found, return 0
827         if [ "$nKeysOK" -gt 0 ] ; then
828             return 0
829         # else return 2
830         else
831             return 2
832         fi
833     # if no keys were found, return 1
834     else
835         return 1
836     fi
837 }
838
839 # process a single host in the known_host file
840 process_host_known_hosts() {
841     local host
842     local userID
843     local noKey=
844     local nKeys
845     local nKeysOK
846     local ok
847     local sshKey
848     local tmpfile
849
850     # set the key processing mode
851     export REQUIRED_KEY_CAPABILITY="$REQUIRED_HOST_KEY_CAPABILITY"
852
853     host="$1"
854     userID="ssh://${host}"
855
856     log verbose "processing: $host"
857
858     nKeys=0
859     nKeysOK=0
860
861     IFS=$'\n'
862     for line in $(process_user_id "${userID}") ; do
863         # note that key was found
864         nKeys=$((nKeys+1))
865
866         ok=$(echo "$line" | cut -d: -f1)
867         sshKey=$(echo "$line" | cut -d: -f2)
868
869         if [ -z "$sshKey" ] ; then
870             continue
871         fi
872
873         # remove any old host key line, and note if removed nothing is
874         # removed
875         remove_line "$KNOWN_HOSTS" "$sshKey" || noKey=true
876
877         # if key OK, add new host line
878         if [ "$ok" -eq '0' ] ; then
879             # note that key was found ok
880             nKeysOK=$((nKeysOK+1))
881
882             # hash if specified
883             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
884                 # FIXME: this is really hackish cause ssh-keygen won't
885                 # hash from stdin to stdout
886                 tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
887                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
888                 ssh-keygen -H -f "$tmpfile" 2>/dev/null
889                 cat "$tmpfile" >> "$KNOWN_HOSTS"
890                 rm -f "$tmpfile" "${tmpfile}.old"
891             else
892                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
893             fi
894
895             # log if this is a new key to the known_hosts file
896             if [ "$noKey" ] ; then
897                 log info "* new key for $host added to known_hosts file."
898             fi
899         fi
900     done
901
902     # if at least one key was found...
903     if [ "$nKeys" -gt 0 ] ; then
904         # if ok keys were found, return 0
905         if [ "$nKeysOK" -gt 0 ] ; then
906             return 0
907         # else return 2
908         else
909             return 2
910         fi
911     # if no keys were found, return 1
912     else
913         return 1
914     fi
915 }
916
917 # update the known_hosts file for a set of hosts listed on command
918 # line
919 update_known_hosts() {
920     local returnCode=0
921     local nHosts
922     local nHostsOK
923     local nHostsBAD
924     local fileCheck
925     local host
926     local newUmask
927
928     # the number of hosts specified on command line
929     nHosts="$#"
930
931     nHostsOK=0
932     nHostsBAD=0
933
934     # touch the known_hosts file so that the file permission check
935     # below won't fail upon not finding the file
936     if [ ! -f "$KNOWN_HOSTS" ]; then
937         # make sure to create any files or directories with the appropriate write bits turned off:
938         newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
939         [ -d $(dirname "$KNOWN_HOSTS") ] \
940             || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
941             || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
942         # make sure to create this file with the appropriate bits turned off:
943         (umask "$newUmask" && touch "$KNOWN_HOSTS") \
944             || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
945     fi
946
947     # check permissions on the known_hosts file path
948     check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
949         || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
950
951     # create a lockfile on known_hosts:
952     lock create "$KNOWN_HOSTS"
953     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
954     trap "lock remove $KNOWN_HOSTS" EXIT
955
956     # note pre update file checksum
957     fileCheck=$(file_hash "$KNOWN_HOSTS")
958
959     for host ; do
960         # process the host
961         process_host_known_hosts "$host" || returnCode="$?"
962         # note the result
963         case "$returnCode" in
964             0)
965                 nHostsOK=$((nHostsOK+1))
966                 ;;
967             2)
968                 nHostsBAD=$((nHostsBAD+1))
969                 ;;
970         esac
971
972         # touch the lockfile, for good measure.
973         lock touch "$KNOWN_HOSTS"
974     done
975
976     # remove the lockfile and the trap
977     lock remove "$KNOWN_HOSTS"
978     trap - EXIT
979
980     # note if the known_hosts file was updated
981     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
982         log debug "known_hosts file updated."
983     fi
984
985     # if an acceptable host was found, return 0
986     if [ "$nHostsOK" -gt 0 ] ; then
987         return 0
988     # else if no ok hosts were found...
989     else
990         # if no bad host were found then no hosts were found at all,
991         # and return 1
992         if [ "$nHostsBAD" -eq 0 ] ; then
993             return 1
994         # else if at least one bad host was found, return 2
995         else
996             return 2
997         fi
998     fi
999 }
1000
1001 # process hosts from a known_hosts file
1002 process_known_hosts() {
1003     local hosts
1004
1005     # exit if the known_hosts file does not exist
1006     if [ ! -e "$KNOWN_HOSTS" ] ; then
1007         failure "known_hosts file '$KNOWN_HOSTS' does not exist."
1008     fi
1009
1010     log debug "processing known_hosts file:"
1011     log debug " $KNOWN_HOSTS"
1012
1013     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
1014
1015     if [ -z "$hosts" ] ; then
1016         log debug "no hosts to process."
1017         return
1018     fi
1019
1020     # take all the hosts from the known_hosts file (first
1021     # field), grep out all the hashed hosts (lines starting
1022     # with '|')...
1023     update_known_hosts $hosts
1024 }
1025
1026 # process uids for the authorized_keys file
1027 process_uid_authorized_keys() {
1028     local userID
1029     local nKeys
1030     local nKeysOK
1031     local ok
1032     local sshKey
1033
1034     # set the key processing mode
1035     export REQUIRED_KEY_CAPABILITY="$REQUIRED_USER_KEY_CAPABILITY"
1036
1037     userID="$1"
1038
1039     log verbose "processing: $userID"
1040
1041     nKeys=0
1042     nKeysOK=0
1043
1044     IFS=$'\n'
1045     for line in $(process_user_id "$userID") ; do
1046         # note that key was found
1047         nKeys=$((nKeys+1))
1048
1049         ok=$(echo "$line" | cut -d: -f1)
1050         sshKey=$(echo "$line" | cut -d: -f2)
1051
1052         if [ -z "$sshKey" ] ; then
1053             continue
1054         fi
1055
1056         # remove the old host key line
1057         remove_line "$AUTHORIZED_KEYS" "$sshKey"
1058
1059         # if key OK, add new host line
1060         if [ "$ok" -eq '0' ] ; then
1061             # note that key was found ok
1062             nKeysOK=$((nKeysOK+1))
1063
1064             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
1065         fi
1066     done
1067
1068     # if at least one key was found...
1069     if [ "$nKeys" -gt 0 ] ; then
1070         # if ok keys were found, return 0
1071         if [ "$nKeysOK" -gt 0 ] ; then
1072             return 0
1073         # else return 2
1074         else
1075             return 2
1076         fi
1077     # if no keys were found, return 1
1078     else
1079         return 1
1080     fi
1081 }
1082
1083 # update the authorized_keys files from a list of user IDs on command
1084 # line
1085 update_authorized_keys() {
1086     local returnCode=0
1087     local userID
1088     local nIDs
1089     local nIDsOK
1090     local nIDsBAD
1091     local fileCheck
1092
1093     # the number of ids specified on command line
1094     nIDs="$#"
1095
1096     nIDsOK=0
1097     nIDsBAD=0
1098
1099     log debug "updating authorized_keys file:"
1100     log debug " $AUTHORIZED_KEYS"
1101
1102     # check permissions on the authorized_keys file path
1103     check_key_file_permissions $(whoami) "$AUTHORIZED_KEYS" || failure
1104
1105     # create a lockfile on authorized_keys
1106     lock create "$AUTHORIZED_KEYS"
1107     # FIXME: we're discarding any pre-existing EXIT trap; is this bad?
1108     trap "lock remove $AUTHORIZED_KEYS" EXIT
1109
1110     # note pre update file checksum
1111     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
1112
1113     # remove any monkeysphere lines from authorized_keys file
1114     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
1115
1116     for userID ; do
1117         # process the user ID, change return code if key not found for
1118         # user ID
1119         process_uid_authorized_keys "$userID" || returnCode="$?"
1120
1121         # note the result
1122         case "$returnCode" in
1123             0)
1124                 nIDsOK=$((nIDsOK+1))
1125                 ;;
1126             2)
1127                 nIDsBAD=$((nIDsBAD+1))
1128                 ;;
1129         esac
1130
1131         # touch the lockfile, for good measure.
1132         lock touch "$AUTHORIZED_KEYS"
1133     done
1134
1135     # remove the lockfile and the trap
1136     lock remove "$AUTHORIZED_KEYS"
1137
1138     # remove the trap
1139     trap - EXIT
1140
1141     # note if the authorized_keys file was updated
1142     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
1143         log debug "authorized_keys file updated."
1144     fi
1145
1146     # if an acceptable id was found, return 0
1147     if [ "$nIDsOK" -gt 0 ] ; then
1148         return 0
1149     # else if no ok ids were found...
1150     else
1151         # if no bad ids were found then no ids were found at all, and
1152         # return 1
1153         if [ "$nIDsBAD" -eq 0 ] ; then
1154             return 1
1155         # else if at least one bad id was found, return 2
1156         else
1157             return 2
1158         fi
1159     fi
1160 }
1161
1162 # process an authorized_user_ids file for authorized_keys
1163 process_authorized_user_ids() {
1164     local line
1165     local nline
1166     local userIDs
1167
1168     authorizedUserIDs="$1"
1169
1170     # exit if the authorized_user_ids file is empty
1171     if [ ! -e "$authorizedUserIDs" ] ; then
1172         failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
1173     fi
1174
1175     log debug "processing authorized_user_ids file:"
1176     log debug " $authorizedUserIDs"
1177
1178     # check permissions on the authorized_user_ids file path
1179     check_key_file_permissions $(whoami) "$authorizedUserIDs" || failure
1180
1181     if ! meat "$authorizedUserIDs" >/dev/null ; then
1182         log debug " no user IDs to process."
1183         return
1184     fi
1185
1186     nline=0
1187
1188     # extract user IDs from authorized_user_ids file
1189     IFS=$'\n'
1190     for line in $(meat "$authorizedUserIDs") ; do
1191         userIDs["$nline"]="$line"
1192         nline=$((nline+1))
1193     done
1194
1195     update_authorized_keys "${userIDs[@]}"
1196 }
1197
1198 # takes a gpg key or keys on stdin, and outputs a list of
1199 # fingerprints, one per line:
1200 list_primary_fingerprints() {
1201     local fake=$(msmktempdir)
1202     trap "rm -rf $fake" EXIT
1203     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
1204     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
1205         awk -F: '/^fpr:/{ print $10 }'
1206     trap - EXIT
1207     rm -rf "$fake"
1208 }
1209
1210 # takes an OpenPGP key or set of keys on stdin, a fingerprint or other
1211 # key identifier as $1, and outputs the gpg-formatted information for
1212 # the requested keys from the material on stdin
1213 get_cert_info() {
1214     local fake=$(msmktempdir)
1215     trap "rm -rf $fake" EXIT
1216     GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
1217     GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
1218     trap - EXIT
1219     rm -rf "$fake"
1220 }
1221
1222
1223 check_cruft_file() {
1224     local loc="$1"
1225     local version="$2"
1226     
1227     if [ -e "$loc" ] ; then
1228         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
1229     fi
1230 }
1231
1232 check_upgrade_dir() {
1233     local loc="$1"
1234     local version="$2"
1235
1236     if [ -d "$loc" ] ; then
1237         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
1238     fi
1239 }
1240
1241 ## look for cruft from old versions of the monkeysphere, and notice if
1242 ## upgrades have not been run:
1243 report_cruft() {
1244     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-host" 0.23
1245     check_upgrade_dir "${SYSCONFIGDIR}/gnupg-authentication" 0.23
1246
1247     check_cruft_file "${SYSCONFIGDIR}/gnupg-authentication.conf" 0.23
1248     check_cruft_file "${SYSCONFIGDIR}/gnupg-host.conf" 0.23
1249
1250     local found=
1251     for foo in "${SYSDATADIR}/backup-from-"*"-transition"  ; do
1252         if [ -d "$foo" ] ; then
1253             printf "! %s\n" "$foo" | log info
1254             found=true
1255         fi
1256     done
1257     if [ "$found" ] ; then
1258         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
1259     fi
1260 }