3 # Shared sh functions for the monkeysphere
6 # Jameson Rollins <jrollins@fifthhorseman.net>
8 # Copyright 2008, released under the GPL, version 3 or later
10 # all-caps variables are meant to be user supplied (ie. from config
11 # file) and are considered global
13 ########################################################################
17 ETC="/etc/monkeysphere"
20 ########################################################################
23 # failure function. exits with code 255, unless specified otherwise.
29 # write output to stderr
39 # cut out all comments(#) and blank lines from standard input
41 grep -v -e "^[[:space:]]*#" -e '^$' "$1"
44 # cut a specified line from standard input
46 head --line="$1" "$2" | tail -1
49 # check that characters are in a string (in an AND fashion).
50 # used for checking key capability
51 # check_capability capability a [b...]
60 if echo "$usage" | grep -q -v "$capcheck" ; then
69 md5sum "$1" 2> /dev/null
72 # convert escaped characters in pipeline from gpg output back into
74 # FIXME: undo all escape character translation in with-colons gpg
80 # convert nasty chars into gpg-friendly form in pipeline
81 # FIXME: escape everything, not just colons!
86 # prompt for GPG-formatted expiration, and emit result on stdout
87 get_gpg_expiration() {
91 Please specify how long the key should be valid.
92 0 = key does not expire
93 <n> = key expires in n days
94 <n>w = key expires in n weeks
95 <n>m = key expires in n months
96 <n>y = key expires in n years
98 while [ -z "$keyExpire" ] ; do
99 read -p "Key is valid for? (0) " keyExpire
100 if ! test_gpg_expire ${keyExpire:=0} ; then
101 echo "invalid value" >&2
108 # remove all lines with specified string from specified file
116 if [ -z "$file" -o -z "$string" ] ; then
120 if [ ! -e "$file" ] ; then
124 # if the string is in the file...
125 if grep -q -F "$string" "$file" 2> /dev/null ; then
126 # remove the line with the string, and return 0
127 grep -v -F "$string" "$file" | sponge "$file"
135 # remove all lines with MonkeySphere strings in file
136 remove_monkeysphere_lines() {
141 if [ -z "$file" ] ; then
145 if [ ! -e "$file" ] ; then
149 egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
150 "$file" | sponge "$file"
153 # translate ssh-style path variables %h and %u
154 translate_ssh_variables() {
161 # get the user's home directory
162 userHome=$(getent passwd "$uname" | cut -d: -f6)
164 # translate '%u' to user name
165 path=${path/\%u/"$uname"}
166 # translate '%h' to user home directory
167 path=${path/\%h/"$userHome"}
172 # test that a string to conforms to GPG's expiration format
174 echo "$1" | egrep -q "^[0-9]+[mwy]?$"
177 # check that a file is properly owned, and that all it's parent
178 # directories are not group/other writable
179 check_key_file_permissions() {
186 # function to check that an octal corresponds to writability
188 [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
194 # return 0 is path does not exist
195 [ -e "$path" ] || return 0
197 owner=$(stat --format '%U' "$path")
198 access=$(stat --format '%a' "$path")
199 gAccess=$(echo "$access" | cut -c2)
200 oAccess=$(echo "$access" | cut -c3)
203 if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
207 # check group/other writability
208 if is_write "$gAccess" || is_write "$oAccess" ; then
212 if [ "$path" = '/' ] ; then
215 check_key_file_permissions $(dirname "$path")
219 ### CONVERSION UTILITIES
221 # output the ssh key for a given key ID
227 gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
230 # output known_hosts line from ssh key
239 echo -n "$key" | tr -d '\n'
240 echo " MonkeySphere${DATE}"
243 # output authorized_keys line from ssh key
244 ssh2authorized_keys() {
251 echo -n "$key" | tr -d '\n'
252 echo " MonkeySphere${DATE} ${userID}"
255 # convert key from gpg to ssh known_hosts format
263 # NOTE: it seems that ssh-keygen -R removes all comment fields from
264 # all lines in the known_hosts file. why?
265 # NOTE: just in case, the COMMENT can be matched with the
267 # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
269 gpg2ssh "$keyID" | tr -d '\n'
270 echo " MonkeySphere${DATE}"
273 # convert key from gpg to ssh authorized_keys format
274 gpg2authorized_keys() {
281 # NOTE: just in case, the COMMENT can be matched with the
283 # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
284 gpg2ssh "$keyID" | tr -d '\n'
285 echo " MonkeySphere${DATE} ${userID}"
290 # retrieve all keys with given user id from keyserver
291 # FIXME: need to figure out how to retrieve all matching keys
292 # (not just first N (5 in this case))
297 if [ "$CHECK_KEYSERVER" != 'true' ] ; then
303 log -n " checking keyserver $KEYSERVER... "
305 gpg --quiet --batch --with-colons \
306 --command-fd 0 --keyserver "$KEYSERVER" \
307 --search ="$userID" > /dev/null 2>&1
311 # if the user is the monkeysphere user, then update the
312 # monkeysphere user's trustdb
313 if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
314 gpg_authentication "--check-trustdb" > /dev/null 2>&1
320 ########################################################################
321 ### PROCESSING FUNCTIONS
323 # userid and key policy checking
324 # the following checks policy on the returned keys
325 # - checks that full key has appropriate valididy (u|f)
326 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
327 # - checks that requested user ID has appropriate validity
328 # (see /usr/share/doc/gnupg/DETAILS.gz)
329 # output is one line for every found key, in the following format:
333 # "flag" is an acceptability flag, 0 = ok, 1 = bad
334 # "fingerprint" is the fingerprint of the key
336 # expects global variable: "MODE"
339 local requiredCapability
340 local requiredPubCapability
355 # set the required key capability based on the mode
356 if [ "$MODE" = 'known_hosts' ] ; then
357 requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
358 elif [ "$MODE" = 'authorized_keys' ] ; then
359 requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
361 requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
363 # fetch the user ID if necessary/requested
364 gpg_fetch_userid "$userID"
366 # output gpg info for (exact) userid and store
367 gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
368 --with-fingerprint --with-fingerprint \
369 ="$userID" 2>/dev/null)
371 # if the gpg query return code is not 0, return 1
372 if [ "$?" -ne 0 ] ; then
373 log " no primary keys found."
377 # loop over all lines in the gpg output and process.
378 echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
379 while IFS=: read -r type validity keyid uidfpr usage ; do
380 # process based on record type
382 'pub') # primary keys
383 # new key, wipe the slate
390 log " primary key found: $keyid"
392 # if overall key is not valid, skip
393 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
394 log " - unacceptable primary key validity ($validity)."
397 # if overall key is disabled, skip
398 if check_capability "$usage" 'D' ; then
399 log " - key disabled."
402 # if overall key capability is not ok, skip
403 if ! check_capability "$usage" $requiredPubCapability ; then
404 log " - unacceptable primary key capability ($usage)."
408 # mark overall key as ok
411 # mark primary key as ok if capability is ok
412 if check_capability "$usage" $requiredCapability ; then
417 if [ "$lastKey" != pub ] ; then
418 log " - got a user ID after a sub key?! user IDs should only follow primary keys!"
421 # if an acceptable user ID was already found, skip
422 if [ "$uidOK" = 'true' ] ; then
425 # if the user ID does matches...
426 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
427 # and the user ID validity is ok
428 if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
429 # mark user ID acceptable
436 # output a line for the primary key
438 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
439 log " * acceptable primary key."
440 if [ -z "$sshKey" ] ; then
441 log " ! primary key could not be translated (not RSA or DSA?)."
446 log " - unacceptable primary key."
447 if [ -z "$sshKey" ] ; then
448 log " ! primary key could not be translated (not RSA or DSA?)."
455 # unset acceptability of last key
460 # don't bother with sub keys if the primary key is not valid
461 if [ "$keyOK" != true ] ; then
465 # don't bother with sub keys if no user ID is acceptable:
466 if [ "$uidOK" != true ] ; then
470 # if sub key validity is not ok, skip
471 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
474 # if sub key capability is not ok, skip
475 if ! check_capability "$usage" $requiredCapability ; then
482 'fpr') # key fingerprint
483 fingerprint="$uidfpr"
485 sshKey=$(gpg2ssh "$fingerprint")
487 # if the last key was the pub key, skip
488 if [ "$lastKey" = pub ] ; then
492 # output a line for the sub key
494 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
495 log " * acceptable sub key."
496 if [ -z "$sshKey" ] ; then
497 log " ! sub key could not be translated (not RSA or DSA?)."
502 log " - unacceptable sub key."
503 if [ -z "$sshKey" ] ; then
504 log " ! sub key could not be translated (not RSA or DSA?)."
511 done | sort -t: -k1 -n -r
512 # NOTE: this last sort is important so that the "good" keys (key
513 # flag '0') come last. This is so that they take precedence when
514 # being processed in the key files over "bad" keys (key flag '1')
517 # process a single host in the known_host file
518 process_host_known_hosts() {
528 userID="ssh://${host}"
530 log "processing: $host"
536 for line in $(process_user_id "${userID}") ; do
537 # note that key was found
540 ok=$(echo "$line" | cut -d: -f1)
541 sshKey=$(echo "$line" | cut -d: -f2)
543 if [ -z "$sshKey" ] ; then
547 # remove the old host key line, and note if removed
548 remove_line "$KNOWN_HOSTS" "$sshKey"
550 # if key OK, add new host line
551 if [ "$ok" -eq '0' ] ; then
552 # note that key was found ok
553 nKeysOK=$((nKeysOK+1))
556 if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
557 # FIXME: this is really hackish cause ssh-keygen won't
558 # hash from stdin to stdout
560 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
561 ssh-keygen -H -f "$tmpfile" 2> /dev/null
562 cat "$tmpfile" >> "$KNOWN_HOSTS"
563 rm -f "$tmpfile" "${tmpfile}.old"
565 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
570 # if at least one key was found...
571 if [ "$nKeys" -gt 0 ] ; then
572 # if ok keys were found, return 0
573 if [ "$nKeysOK" -gt 0 ] ; then
579 # if no keys were found, return 1
585 # update the known_hosts file for a set of hosts listed on command
587 update_known_hosts() {
594 # the number of hosts specified on command line
600 # set the trap to remove any lockfiles on exit
601 trap "lockfile-remove $KNOWN_HOSTS" EXIT
603 # create a lockfile on known_hosts
604 lockfile-create "$KNOWN_HOSTS"
606 # note pre update file checksum
607 fileCheck="$(file_hash "$KNOWN_HOSTS")"
611 process_host_known_hosts "$host"
615 nHostsOK=$((nHostsOK+1))
618 nHostsBAD=$((nHostsBAD+1))
622 # touch the lockfile, for good measure.
623 lockfile-touch --oneshot "$KNOWN_HOSTS"
626 # remove the lockfile
627 lockfile-remove "$KNOWN_HOSTS"
629 # note if the known_hosts file was updated
630 if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
631 log "known_hosts file updated."
634 # if an acceptable host was found, return 0
635 if [ "$nHostsOK" -gt 0 ] ; then
637 # else if no ok hosts were found...
639 # if no bad host were found then no hosts were found at all,
641 if [ "$nHostsBAD" -eq 0 ] ; then
643 # else if at least one bad host was found, return 2
650 # process hosts from a known_hosts file
651 process_known_hosts() {
654 log "processing known_hosts file..."
656 hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
658 if [ -z "$hosts" ] ; then
659 log "no hosts to process."
663 # take all the hosts from the known_hosts file (first
664 # field), grep out all the hashed hosts (lines starting
666 update_known_hosts $hosts
669 # process uids for the authorized_keys file
670 process_uid_authorized_keys() {
679 log "processing: $userID"
685 for line in $(process_user_id "$userID") ; do
686 # note that key was found
689 ok=$(echo "$line" | cut -d: -f1)
690 sshKey=$(echo "$line" | cut -d: -f2)
692 if [ -z "$sshKey" ] ; then
696 # remove the old host key line
697 remove_line "$AUTHORIZED_KEYS" "$sshKey"
699 # if key OK, add new host line
700 if [ "$ok" -eq '0' ] ; then
701 # note that key was found ok
702 nKeysOK=$((nKeysOK+1))
704 ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
708 # if at least one key was found...
709 if [ "$nKeys" -gt 0 ] ; then
710 # if ok keys were found, return 0
711 if [ "$nKeysOK" -gt 0 ] ; then
717 # if no keys were found, return 1
723 # update the authorized_keys files from a list of user IDs on command
725 update_authorized_keys() {
732 # the number of ids specified on command line
738 # set the trap to remove any lockfiles on exit
739 trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
741 # create a lockfile on authorized_keys
742 lockfile-create "$AUTHORIZED_KEYS"
744 # note pre update file checksum
745 fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
747 # remove any monkeysphere lines from authorized_keys file
748 remove_monkeysphere_lines "$AUTHORIZED_KEYS"
751 # process the user ID, change return code if key not found for
753 process_uid_authorized_keys "$userID"
761 nIDsBAD=$((nIDsBAD+1))
765 # touch the lockfile, for good measure.
766 lockfile-touch --oneshot "$AUTHORIZED_KEYS"
769 # remove the lockfile
770 lockfile-remove "$AUTHORIZED_KEYS"
772 # note if the authorized_keys file was updated
773 if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
774 log "authorized_keys file updated."
777 # if an acceptable id was found, return 0
778 if [ "$nIDsOK" -gt 0 ] ; then
780 # else if no ok ids were found...
782 # if no bad ids were found then no ids were found at all, and
784 if [ "$nIDsBAD" -eq 0 ] ; then
786 # else if at least one bad id was found, return 2
793 # process an authorized_user_ids file for authorized_keys
794 process_authorized_user_ids() {
799 authorizedUserIDs="$1"
801 log "processing authorized_user_ids file..."
803 if ! meat "$authorizedUserIDs" > /dev/null ; then
804 log "no user IDs to process."
810 # extract user IDs from authorized_user_ids file
812 for line in $(meat "$authorizedUserIDs") ; do
813 userIDs["$nline"]="$line"
817 update_authorized_keys "${userIDs[@]}"