34d9b5adfcace1cf8cf4858788a03d602ade4e4a
[monkeysphere.git] / src / common
1 # -*-shell-script-*-
2
3 # Shared sh functions for the monkeysphere
4 #
5 # Written by
6 # Jameson Rollins <jrollins@fifthhorseman.net>
7 #
8 # Copyright 2008, released under the GPL, version 3 or later
9
10 # all-caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
12
13 ########################################################################
14 ### COMMON VARIABLES
15
16 # managed directories
17 ETC="/etc/monkeysphere"
18 export ETC
19
20 ########################################################################
21 ### UTILITY FUNCTIONS
22
23 # failure function.  exits with code 255, unless specified otherwise.
24 failure() {
25     echo "$1" >&2
26     exit ${2:-'255'}
27 }
28
29 # write output to stderr
30 log() {
31     local level
32
33     level="$1"
34     shift
35
36     echo -n "ms: " >&2
37     echo "$@" >&2
38 }
39
40 # cut out all comments(#) and blank lines from standard input
41 meat() {
42     grep -v -e "^[[:space:]]*#" -e '^$' "$1"
43 }
44
45 # cut a specified line from standard input
46 cutline() {
47     head --line="$1" "$2" | tail -1
48 }
49
50 # check that characters are in a string (in an AND fashion).
51 # used for checking key capability
52 # check_capability capability a [b...]
53 check_capability() {
54     local usage
55     local capcheck
56
57     usage="$1"
58     shift 1
59
60     for capcheck ; do
61         if echo "$usage" | grep -q -v "$capcheck" ; then
62             return 1
63         fi
64     done
65     return 0
66 }
67
68 # hash of a file
69 file_hash() {
70     md5sum "$1" 2> /dev/null
71 }
72
73 # convert escaped characters in pipeline from gpg output back into
74 # original character
75 # FIXME: undo all escape character translation in with-colons gpg
76 # output
77 gpg_unescape() {
78     sed 's/\\x3a/:/g'
79 }
80
81 # convert nasty chars into gpg-friendly form in pipeline
82 # FIXME: escape everything, not just colons!
83 gpg_escape() {
84     sed 's/:/\\x3a/g'
85 }
86
87 # prompt for GPG-formatted expiration, and emit result on stdout
88 get_gpg_expiration() {
89     local keyExpire
90
91     keyExpire="$1"
92
93     if [ -z "$keyExpire" ]; then
94         cat >&2 <<EOF
95 Please specify how long the key should be valid.
96          0 = key does not expire
97       <n>  = key expires in n days
98       <n>w = key expires in n weeks
99       <n>m = key expires in n months
100       <n>y = key expires in n years
101 EOF
102         while [ -z "$keyExpire" ] ; do
103             read -p "Key is valid for? (0) " keyExpire
104             if ! test_gpg_expire ${keyExpire:=0} ; then
105                 echo "invalid value" >&2
106                 unset keyExpire
107             fi
108         done
109     elif ! test_gpg_expire "$keyExpire" ; then
110         failure "invalid key expiration value '$keyExpire'."
111     fi
112         
113     echo "$keyExpire"
114 }
115
116 passphrase_prompt() {
117     local prompt="$1"
118     local fifo="$2"
119     local PASS
120
121     if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
122         "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
123     else
124         read -s -p "$prompt" PASS
125         # Uses the builtin echo, so should not put the passphrase into
126         # the process table.  I think. --dkg
127         echo "$PASS" > "$fifo"
128     fi
129 }
130
131 test_gnu_dummy_s2k_extension() {
132
133 # this block contains a demonstration private key that has had the
134 # primary key stripped out using the GNU S2K extension known as
135 # "gnu-dummy" (see /usr/share/doc/gnupg/DETAILS.gz).  The subkey is
136 # present in cleartext, however.
137
138 # openpgp2ssh will be able to deal with this based on whether the
139 # local copy of GnuTLS contains read_s2k support that can handle it.
140
141 # read up on that here:
142
143 # http://lists.gnu.org/archive/html/gnutls-devel/2008-08/msg00005.html
144
145 echo "
146 -----BEGIN PGP PRIVATE KEY BLOCK-----
147 Version: GnuPG v1.4.9 (GNU/Linux)
148
149 lQCVBEO3YdABBACRqqEnucag4+vyZny2M67Pai5+5suIRRvY+Ly8Ms5MvgCi3EVV
150 xT05O/+0ShiRaf+QicCOFrhbU9PZzzU+seEvkeW2UCu4dQfILkmj+HBEIltGnHr3
151 G0yegHj5pnqrcezERURf2e17gGFWX91cXB9Cm721FPXczuKraphKwCA9PwARAQAB
152 /gNlAkdOVQG0OURlbW9uc3RyYXRpb24gS2V5IGZvciBTMksgR05VIGV4dGVuc2lv
153 biAxMDAxIC0tIGdudS1kdW1teYi8BBMBAgAmBQJDt2HQAhsDBQkB4TOABgsJCAcD
154 AgQVAggDBBYCAwECHgECF4AACgkQQZUwSa4UDezTOQP/TMQXUVrWzHYZGopoPZ2+
155 ZS3qddiznBHsgb7MGYg1KlTiVJSroDUBCHIUJvdQKZV9zrzrFl47D07x6hGyUPHV
156 aZXvuITW8t1o5MMHkCy3pmJ2KgfDvdUxrBvLfgPMICA4c6zA0mWquee43syEW9NY
157 g3q61iPlQwD1J1kX1wlimLCdAdgEQ7dh0AEEANAwa63zlQbuy1Meliy8otwiOa+a
158 mH6pxxUgUNggjyjO5qx+rl25mMjvGIRX4/L1QwIBXJBVi3SgvJW1COZxZqBYqj9U
159 8HVT07mWKFEDf0rZLeUE2jTm16cF9fcW4DQhW+sfYm+hi2sY3HeMuwlUBK9KHfW2
160 +bGeDzVZ4pqfUEudABEBAAEAA/0bemib+wxub9IyVFUp7nPobjQC83qxLSNzrGI/
161 RHzgu/5CQi4tfLOnwbcQsLELfker2hYnjsLrT9PURqK4F7udrWEoZ1I1LymOtLG/
162 4tNZ7Mnul3wRC2tCn7FKx8sGJwGh/3li8vZ6ALVJAyOia5TZ/buX0+QZzt6+hPKk
163 7MU1WQIA4bUBjtrsqDwro94DvPj3/jBnMZbXr6WZIItLNeVDUcM8oHL807Am97K1
164 ueO/f6v1sGAHG6lVPTmtekqPSTWBfwIA7CGFvEyvSALfB8NUa6jtk27NCiw0csql
165 kuhCmwXGMVOiryKEfegkIahf2bAd/gnWHPrpWp7bUE20v8YoW22I4wIAhnm5Wr5Q
166 Sy7EHDUxmJm5TzadFp9gq08qNzHBpXSYXXJ3JuWcL1/awUqp3tE1I6zZ0hZ38Ia6
167 SdBMN88idnhDPqPoiKUEGAECAA8FAkO3YdACGyAFCQHhM4AACgkQQZUwSa4UDezm
168 vQP/ZhK+2ly9oI2z7ZcNC/BJRch0/ybQ3haahII8pXXmOThpZohr/LUgoWgCZdXg
169 vP6yiszNk2tIs8KphCAw7Lw/qzDC2hEORjWO4f46qk73RAgSqG/GyzI4ltWiDhqn
170 vnQCFl3+QFSe4zinqykHnLwGPMXv428d/ZjkIc2ju8dRsn4=
171 =CR5w
172 -----END PGP PRIVATE KEY BLOCK-----
173 " | openpgp2ssh 4129E89D17C1D591 >/dev/null 2>/dev/null
174
175 }
176
177 # remove all lines with specified string from specified file
178 remove_line() {
179     local file
180     local string
181
182     file="$1"
183     string="$2"
184
185     if [ -z "$file" -o -z "$string" ] ; then
186         return 1
187     fi
188
189     if [ ! -e "$file" ] ; then
190         return 1
191     fi
192
193     # if the string is in the file...
194     if grep -q -F "$string" "$file" 2> /dev/null ; then
195         # remove the line with the string, and return 0
196         grep -v -F "$string" "$file" | sponge "$file"
197         return 0
198     # otherwise return 1
199     else
200         return 1
201     fi
202 }
203
204 # remove all lines with MonkeySphere strings in file
205 remove_monkeysphere_lines() {
206     local file
207
208     file="$1"
209
210     if [ -z "$file" ] ; then
211         return 1
212     fi
213
214     if [ ! -e "$file" ] ; then
215         return 1
216     fi
217
218     egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
219         "$file" | sponge "$file"
220 }
221
222 # translate ssh-style path variables %h and %u
223 translate_ssh_variables() {
224     local uname
225     local home
226
227     uname="$1"
228     path="$2"
229
230     # get the user's home directory
231     userHome=$(getent passwd "$uname" | cut -d: -f6)
232
233     # translate '%u' to user name
234     path=${path/\%u/"$uname"}
235     # translate '%h' to user home directory
236     path=${path/\%h/"$userHome"}
237
238     echo "$path"
239 }
240
241 # test that a string to conforms to GPG's expiration format
242 test_gpg_expire() {
243     echo "$1" | egrep -q "^[0-9]+[mwy]?$"
244 }
245
246 # check that a file is properly owned, and that all it's parent
247 # directories are not group/other writable
248 check_key_file_permissions() {
249     local user
250     local path
251     local access
252     local gAccess
253     local oAccess
254
255     # function to check that an octal corresponds to writability
256     is_write() {
257         [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
258     }
259
260     user="$1"
261     path="$2"
262
263     # return 0 is path does not exist
264     [ -e "$path" ] || return 0
265
266     owner=$(stat --format '%U' "$path")
267     access=$(stat --format '%a' "$path")
268     gAccess=$(echo "$access" | cut -c2)
269     oAccess=$(echo "$access" | cut -c3)
270
271     # check owner
272     if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
273         return 1
274     fi
275
276     # check group/other writability
277     if is_write "$gAccess" || is_write "$oAccess" ; then
278         return 2
279     fi
280
281     if [ "$path" = '/' ] ; then
282         return 0
283     else
284         check_key_file_permissions $(dirname "$path")
285     fi
286 }
287
288 ### CONVERSION UTILITIES
289
290 # output the ssh key for a given key ID
291 gpg2ssh() {
292     local keyID
293     
294     keyID="$1"
295
296     gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
297 }
298
299 # output known_hosts line from ssh key
300 ssh2known_hosts() {
301     local host
302     local key
303
304     host="$1"
305     key="$2"
306
307     echo -n "$host "
308     echo -n "$key" | tr -d '\n'
309     echo " MonkeySphere${DATE}"
310 }
311
312 # output authorized_keys line from ssh key
313 ssh2authorized_keys() {
314     local userID
315     local key
316     
317     userID="$1"
318     key="$2"
319
320     echo -n "$key" | tr -d '\n'
321     echo " MonkeySphere${DATE} ${userID}"
322 }
323
324 # convert key from gpg to ssh known_hosts format
325 gpg2known_hosts() {
326     local host
327     local keyID
328
329     host="$1"
330     keyID="$2"
331
332     # NOTE: it seems that ssh-keygen -R removes all comment fields from
333     # all lines in the known_hosts file.  why?
334     # NOTE: just in case, the COMMENT can be matched with the
335     # following regexp:
336     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
337     echo -n "$host "
338     gpg2ssh "$keyID" | tr -d '\n'
339     echo " MonkeySphere${DATE}"
340 }
341
342 # convert key from gpg to ssh authorized_keys format
343 gpg2authorized_keys() {
344     local userID
345     local keyID
346
347     userID="$1"
348     keyID="$2"
349
350     # NOTE: just in case, the COMMENT can be matched with the
351     # following regexp:
352     # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
353     gpg2ssh "$keyID" | tr -d '\n'
354     echo " MonkeySphere${DATE} ${userID}"
355 }
356
357 ### GPG UTILITIES
358
359 # retrieve all keys with given user id from keyserver
360 # FIXME: need to figure out how to retrieve all matching keys
361 # (not just first N (5 in this case))
362 gpg_fetch_userid() {
363     local userID
364     local returnCode
365
366     if [ "$CHECK_KEYSERVER" != 'true' ] ; then
367         return 0
368     fi
369
370     userID="$1"
371
372     log info " checking keyserver $KEYSERVER... "
373     echo 1,2,3,4,5 | \
374         gpg --quiet --batch --with-colons \
375         --command-fd 0 --keyserver "$KEYSERVER" \
376         --search ="$userID" > /dev/null 2>&1
377     returnCode="$?"
378
379     # if the user is the monkeysphere user, then update the
380     # monkeysphere user's trustdb
381     if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
382         gpg_authentication "--check-trustdb" > /dev/null 2>&1
383     fi
384
385     return "$returnCode"
386 }
387
388 ########################################################################
389 ### PROCESSING FUNCTIONS
390
391 # userid and key policy checking
392 # the following checks policy on the returned keys
393 # - checks that full key has appropriate valididy (u|f)
394 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
395 # - checks that requested user ID has appropriate validity
396 # (see /usr/share/doc/gnupg/DETAILS.gz)
397 # output is one line for every found key, in the following format:
398 #
399 # flag:sshKey
400 #
401 # "flag" is an acceptability flag, 0 = ok, 1 = bad
402 # "sshKey" is the translated gpg key
403 #
404 # all log output must go to stderr, as stdout is used to pass the
405 # flag:sshKey to the calling function.
406 #
407 # expects global variable: "MODE"
408 process_user_id() {
409     local userID
410     local requiredCapability
411     local requiredPubCapability
412     local gpgOut
413     local type
414     local validity
415     local keyid
416     local uidfpr
417     local usage
418     local keyOK
419     local uidOK
420     local lastKey
421     local lastKeyOK
422     local fingerprint
423
424     userID="$1"
425
426     # set the required key capability based on the mode
427     if [ "$MODE" = 'known_hosts' ] ; then
428         requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
429     elif [ "$MODE" = 'authorized_keys' ] ; then
430         requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
431     fi
432     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
433
434     # fetch the user ID if necessary/requested
435     gpg_fetch_userid "$userID"
436
437     # output gpg info for (exact) userid and store
438     gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
439         --with-fingerprint --with-fingerprint \
440         ="$userID" 2>/dev/null)
441
442     # if the gpg query return code is not 0, return 1
443     if [ "$?" -ne 0 ] ; then
444         log error " no primary keys found."
445         return 1
446     fi
447
448     # loop over all lines in the gpg output and process.
449     echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
450     while IFS=: read -r type validity keyid uidfpr usage ; do
451         # process based on record type
452         case $type in
453             'pub') # primary keys
454                 # new key, wipe the slate
455                 keyOK=
456                 uidOK=
457                 lastKey=pub
458                 lastKeyOK=
459                 fingerprint=
460
461                 log error " primary key found: $keyid"
462
463                 # if overall key is not valid, skip
464                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
465                     log error "  - unacceptable primary key validity ($validity)."
466                     continue
467                 fi
468                 # if overall key is disabled, skip
469                 if check_capability "$usage" 'D' ; then
470                     log error "  - key disabled."
471                     continue
472                 fi
473                 # if overall key capability is not ok, skip
474                 if ! check_capability "$usage" $requiredPubCapability ; then
475                     log error "  - unacceptable primary key capability ($usage)."
476                     continue
477                 fi
478
479                 # mark overall key as ok
480                 keyOK=true
481
482                 # mark primary key as ok if capability is ok
483                 if check_capability "$usage" $requiredCapability ; then
484                     lastKeyOK=true
485                 fi
486                 ;;
487             'uid') # user ids
488                 if [ "$lastKey" != pub ] ; then
489                     log error " - got a user ID after a sub key?!  user IDs should only follow primary keys!"
490                     continue
491                 fi
492                 # if an acceptable user ID was already found, skip
493                 if [ "$uidOK" = 'true' ] ; then
494                     continue
495                 fi
496                 # if the user ID does matches...
497                 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
498                     # and the user ID validity is ok
499                     if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
500                         # mark user ID acceptable
501                         uidOK=true
502                     fi
503                 else
504                     continue
505                 fi
506
507                 # output a line for the primary key
508                 # 0 = ok, 1 = bad
509                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
510                     log error "  * acceptable primary key."
511                     if [ -z "$sshKey" ] ; then
512                         log error "    ! primary key could not be translated (not RSA or DSA?)."
513                     else
514                         echo "0:${sshKey}"
515                     fi
516                 else
517                     log error "  - unacceptable primary key."
518                     if [ -z "$sshKey" ] ; then
519                         log error "   ! primary key could not be translated (not RSA or DSA?)."
520                     else
521                         echo "1:${sshKey}"
522                     fi
523                 fi
524                 ;;
525             'sub') # sub keys
526                 # unset acceptability of last key
527                 lastKey=sub
528                 lastKeyOK=
529                 fingerprint=
530                 
531                 # don't bother with sub keys if the primary key is not valid
532                 if [ "$keyOK" != true ] ; then
533                     continue
534                 fi
535
536                 # don't bother with sub keys if no user ID is acceptable:
537                 if [ "$uidOK" != true ] ; then
538                     continue
539                 fi
540                 
541                 # if sub key validity is not ok, skip
542                 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
543                     continue
544                 fi
545                 # if sub key capability is not ok, skip
546                 if ! check_capability "$usage" $requiredCapability ; then
547                     continue
548                 fi
549
550                 # mark sub key as ok
551                 lastKeyOK=true
552                 ;;
553             'fpr') # key fingerprint
554                 fingerprint="$uidfpr"
555
556                 sshKey=$(gpg2ssh "$fingerprint")
557
558                 # if the last key was the pub key, skip
559                 if [ "$lastKey" = pub ] ; then
560                     continue
561                 fi
562
563                 # output a line for the sub key
564                 # 0 = ok, 1 = bad
565                 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
566                     log error "  * acceptable sub key."
567                     if [ -z "$sshKey" ] ; then
568                         log error "    ! sub key could not be translated (not RSA or DSA?)."
569                     else
570                         echo "0:${sshKey}"
571                     fi
572                 else
573                     log error "  - unacceptable sub key."
574                     if [ -z "$sshKey" ] ; then
575                         log error "    ! sub key could not be translated (not RSA or DSA?)."
576                     else
577                         echo "1:${sshKey}"
578                     fi
579                 fi
580                 ;;
581         esac
582     done | sort -t: -k1 -n -r
583     # NOTE: this last sort is important so that the "good" keys (key
584     # flag '0') come last.  This is so that they take precedence when
585     # being processed in the key files over "bad" keys (key flag '1')
586 }
587
588 # process a single host in the known_host file
589 process_host_known_hosts() {
590     local host
591     local userID
592     local nKeys
593     local nKeysOK
594     local ok
595     local sshKey
596     local tmpfile
597
598     host="$1"
599     userID="ssh://${host}"
600
601     log info "processing: $host"
602
603     nKeys=0
604     nKeysOK=0
605
606     IFS=$'\n'
607     for line in $(process_user_id "${userID}") ; do
608         # note that key was found
609         nKeys=$((nKeys+1))
610
611         ok=$(echo "$line" | cut -d: -f1)
612         sshKey=$(echo "$line" | cut -d: -f2)
613
614         if [ -z "$sshKey" ] ; then
615             continue
616         fi
617
618         # remove the old host key line, and note if removed
619         remove_line "$KNOWN_HOSTS" "$sshKey"
620
621         # if key OK, add new host line
622         if [ "$ok" -eq '0' ] ; then
623             # note that key was found ok
624             nKeysOK=$((nKeysOK+1))
625
626             # hash if specified
627             if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
628                 # FIXME: this is really hackish cause ssh-keygen won't
629                 # hash from stdin to stdout
630                 tmpfile=$(mktemp)
631                 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
632                 ssh-keygen -H -f "$tmpfile" 2> /dev/null
633                 cat "$tmpfile" >> "$KNOWN_HOSTS"
634                 rm -f "$tmpfile" "${tmpfile}.old"
635             else
636                 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
637             fi
638         fi
639     done
640
641     # if at least one key was found...
642     if [ "$nKeys" -gt 0 ] ; then
643         # if ok keys were found, return 0
644         if [ "$nKeysOK" -gt 0 ] ; then
645             return 0
646         # else return 2
647         else
648             return 2
649         fi
650     # if no keys were found, return 1
651     else
652         return 1
653     fi
654 }
655
656 # update the known_hosts file for a set of hosts listed on command
657 # line
658 update_known_hosts() {
659     local nHosts
660     local nHostsOK
661     local nHostsBAD
662     local fileCheck
663     local host
664
665     # the number of hosts specified on command line
666     nHosts="$#"
667
668     nHostsOK=0
669     nHostsBAD=0
670
671     # set the trap to remove any lockfiles on exit
672     trap "lockfile-remove $KNOWN_HOSTS" EXIT
673
674     # create a lockfile on known_hosts
675     lockfile-create "$KNOWN_HOSTS"
676
677     # note pre update file checksum
678     fileCheck="$(file_hash "$KNOWN_HOSTS")"
679
680     for host ; do
681         # process the host
682         process_host_known_hosts "$host"
683         # note the result
684         case "$?" in
685             0)
686                 nHostsOK=$((nHostsOK+1))
687                 ;;
688             2)
689                 nHostsBAD=$((nHostsBAD+1))
690                 ;;
691         esac
692
693         # touch the lockfile, for good measure.
694         lockfile-touch --oneshot "$KNOWN_HOSTS"
695     done
696
697     # remove the lockfile
698     lockfile-remove "$KNOWN_HOSTS"
699
700     # note if the known_hosts file was updated
701     if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
702         log info "known_hosts file updated."
703     fi
704
705     # if an acceptable host was found, return 0
706     if [ "$nHostsOK" -gt 0 ] ; then
707         return 0
708     # else if no ok hosts were found...
709     else
710         # if no bad host were found then no hosts were found at all,
711         # and return 1
712         if [ "$nHostsBAD" -eq 0 ] ; then
713             return 1
714         # else if at least one bad host was found, return 2
715         else
716             return 2
717         fi
718     fi
719 }
720
721 # process hosts from a known_hosts file
722 process_known_hosts() {
723     local hosts
724
725     log info "processing known_hosts file..."
726
727     hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
728
729     if [ -z "$hosts" ] ; then
730         log error "no hosts to process."
731         return
732     fi
733
734     # take all the hosts from the known_hosts file (first
735     # field), grep out all the hashed hosts (lines starting
736     # with '|')...
737     update_known_hosts $hosts
738 }
739
740 # process uids for the authorized_keys file
741 process_uid_authorized_keys() {
742     local userID
743     local nKeys
744     local nKeysOK
745     local ok
746     local sshKey
747
748     userID="$1"
749
750     log info "processing: $userID"
751
752     nKeys=0
753     nKeysOK=0
754
755     IFS=$'\n'
756     for line in $(process_user_id "$userID") ; do
757         # note that key was found
758         nKeys=$((nKeys+1))
759
760         ok=$(echo "$line" | cut -d: -f1)
761         sshKey=$(echo "$line" | cut -d: -f2)
762
763         if [ -z "$sshKey" ] ; then
764             continue
765         fi
766
767         # remove the old host key line
768         remove_line "$AUTHORIZED_KEYS" "$sshKey"
769
770         # if key OK, add new host line
771         if [ "$ok" -eq '0' ] ; then
772             # note that key was found ok
773             nKeysOK=$((nKeysOK+1))
774
775             ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
776         fi
777     done
778
779     # if at least one key was found...
780     if [ "$nKeys" -gt 0 ] ; then
781         # if ok keys were found, return 0
782         if [ "$nKeysOK" -gt 0 ] ; then
783             return 0
784         # else return 2
785         else
786             return 2
787         fi
788     # if no keys were found, return 1
789     else
790         return 1
791     fi
792 }
793
794 # update the authorized_keys files from a list of user IDs on command
795 # line
796 update_authorized_keys() {
797     local userID
798     local nIDs
799     local nIDsOK
800     local nIDsBAD
801     local fileCheck
802
803     # the number of ids specified on command line
804     nIDs="$#"
805
806     nIDsOK=0
807     nIDsBAD=0
808
809     # set the trap to remove any lockfiles on exit
810     trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
811
812     # create a lockfile on authorized_keys
813     lockfile-create "$AUTHORIZED_KEYS"
814
815     # note pre update file checksum
816     fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
817
818     # remove any monkeysphere lines from authorized_keys file
819     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
820
821     for userID ; do
822         # process the user ID, change return code if key not found for
823         # user ID
824         process_uid_authorized_keys "$userID"
825
826         # note the result
827         case "$?" in
828             0)
829                 nIDsOK=$((nIDsOK+1))
830                 ;;
831             2)
832                 nIDsBAD=$((nIDsBAD+1))
833                 ;;
834         esac
835
836         # touch the lockfile, for good measure.
837         lockfile-touch --oneshot "$AUTHORIZED_KEYS"
838     done
839
840     # remove the lockfile
841     lockfile-remove "$AUTHORIZED_KEYS"
842
843     # note if the authorized_keys file was updated
844     if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
845         log info "authorized_keys file updated."
846     fi
847
848     # if an acceptable id was found, return 0
849     if [ "$nIDsOK" -gt 0 ] ; then
850         return 0
851     # else if no ok ids were found...
852     else
853         # if no bad ids were found then no ids were found at all, and
854         # return 1
855         if [ "$nIDsBAD" -eq 0 ] ; then
856             return 1
857         # else if at least one bad id was found, return 2
858         else
859             return 2
860         fi
861     fi
862 }
863
864 # process an authorized_user_ids file for authorized_keys
865 process_authorized_user_ids() {
866     local line
867     local nline
868     local userIDs
869
870     authorizedUserIDs="$1"
871
872     log info "processing authorized_user_ids file..."
873
874     if ! meat "$authorizedUserIDs" > /dev/null ; then
875         log error "no user IDs to process."
876         return
877     fi
878
879     nline=0
880
881     # extract user IDs from authorized_user_ids file
882     IFS=$'\n'
883     for line in $(meat "$authorizedUserIDs") ; do
884         userIDs["$nline"]="$line"
885         nline=$((nline+1))
886     done
887
888     update_authorized_keys "${userIDs[@]}"
889 }