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