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 passphrase_prompt() {
113 if [ "$DISPLAY" ] && which "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
114 "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
116 read -s -p "$prompt" PASS
117 # Uses the builtin echo, so should not put the passphrase into
118 # the process table. I think. --dkg
119 echo "$PASS" > "$fifo"
123 # remove all lines with specified string from specified file
131 if [ -z "$file" -o -z "$string" ] ; then
135 if [ ! -e "$file" ] ; then
139 # if the string is in the file...
140 if grep -q -F "$string" "$file" 2> /dev/null ; then
141 # remove the line with the string, and return 0
142 grep -v -F "$string" "$file" | sponge "$file"
150 # remove all lines with MonkeySphere strings in file
151 remove_monkeysphere_lines() {
156 if [ -z "$file" ] ; then
160 if [ ! -e "$file" ] ; then
164 egrep -v '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$' \
165 "$file" | sponge "$file"
168 # translate ssh-style path variables %h and %u
169 translate_ssh_variables() {
176 # get the user's home directory
177 userHome=$(getent passwd "$uname" | cut -d: -f6)
179 # translate '%u' to user name
180 path=${path/\%u/"$uname"}
181 # translate '%h' to user home directory
182 path=${path/\%h/"$userHome"}
187 # test that a string to conforms to GPG's expiration format
189 echo "$1" | egrep -q "^[0-9]+[mwy]?$"
192 # check that a file is properly owned, and that all it's parent
193 # directories are not group/other writable
194 check_key_file_permissions() {
201 # function to check that an octal corresponds to writability
203 [ "$1" -eq 2 -o "$1" -eq 3 -o "$1" -eq 6 -o "$1" -eq 7 ]
209 # return 0 is path does not exist
210 [ -e "$path" ] || return 0
212 owner=$(stat --format '%U' "$path")
213 access=$(stat --format '%a' "$path")
214 gAccess=$(echo "$access" | cut -c2)
215 oAccess=$(echo "$access" | cut -c3)
218 if [ "$owner" != "$user" -a "$owner" != 'root' ] ; then
222 # check group/other writability
223 if is_write "$gAccess" || is_write "$oAccess" ; then
227 if [ "$path" = '/' ] ; then
230 check_key_file_permissions $(dirname "$path")
234 ### CONVERSION UTILITIES
236 # output the ssh key for a given key ID
242 gpg --export "$keyID" | openpgp2ssh "$keyID" 2> /dev/null
245 # output known_hosts line from ssh key
254 echo -n "$key" | tr -d '\n'
255 echo " MonkeySphere${DATE}"
258 # output authorized_keys line from ssh key
259 ssh2authorized_keys() {
266 echo -n "$key" | tr -d '\n'
267 echo " MonkeySphere${DATE} ${userID}"
270 # convert key from gpg to ssh known_hosts format
278 # NOTE: it seems that ssh-keygen -R removes all comment fields from
279 # all lines in the known_hosts file. why?
280 # NOTE: just in case, the COMMENT can be matched with the
282 # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
284 gpg2ssh "$keyID" | tr -d '\n'
285 echo " MonkeySphere${DATE}"
288 # convert key from gpg to ssh authorized_keys format
289 gpg2authorized_keys() {
296 # NOTE: just in case, the COMMENT can be matched with the
298 # '^MonkeySphere[[:digit:]]{4}(-[[:digit:]]{2}){2}T[[:digit:]]{2}(:[[:digit:]]{2}){2}$'
299 gpg2ssh "$keyID" | tr -d '\n'
300 echo " MonkeySphere${DATE} ${userID}"
305 # retrieve all keys with given user id from keyserver
306 # FIXME: need to figure out how to retrieve all matching keys
307 # (not just first N (5 in this case))
312 if [ "$CHECK_KEYSERVER" != 'true' ] ; then
318 log -n " checking keyserver $KEYSERVER... "
320 gpg --quiet --batch --with-colons \
321 --command-fd 0 --keyserver "$KEYSERVER" \
322 --search ="$userID" > /dev/null 2>&1
326 # if the user is the monkeysphere user, then update the
327 # monkeysphere user's trustdb
328 if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
329 gpg_authentication "--check-trustdb" > /dev/null 2>&1
335 ########################################################################
336 ### PROCESSING FUNCTIONS
338 # userid and key policy checking
339 # the following checks policy on the returned keys
340 # - checks that full key has appropriate valididy (u|f)
341 # - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
342 # - checks that requested user ID has appropriate validity
343 # (see /usr/share/doc/gnupg/DETAILS.gz)
344 # output is one line for every found key, in the following format:
348 # "flag" is an acceptability flag, 0 = ok, 1 = bad
349 # "fingerprint" is the fingerprint of the key
351 # expects global variable: "MODE"
354 local requiredCapability
355 local requiredPubCapability
370 # set the required key capability based on the mode
371 if [ "$MODE" = 'known_hosts' ] ; then
372 requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
373 elif [ "$MODE" = 'authorized_keys' ] ; then
374 requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"
376 requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
378 # fetch the user ID if necessary/requested
379 gpg_fetch_userid "$userID"
381 # output gpg info for (exact) userid and store
382 gpgOut=$(gpg --list-key --fixed-list-mode --with-colon \
383 --with-fingerprint --with-fingerprint \
384 ="$userID" 2>/dev/null)
386 # if the gpg query return code is not 0, return 1
387 if [ "$?" -ne 0 ] ; then
388 log " no primary keys found."
392 # loop over all lines in the gpg output and process.
393 echo "$gpgOut" | cut -d: -f1,2,5,10,12 | \
394 while IFS=: read -r type validity keyid uidfpr usage ; do
395 # process based on record type
397 'pub') # primary keys
398 # new key, wipe the slate
405 log " primary key found: $keyid"
407 # if overall key is not valid, skip
408 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
409 log " - unacceptable primary key validity ($validity)."
412 # if overall key is disabled, skip
413 if check_capability "$usage" 'D' ; then
414 log " - key disabled."
417 # if overall key capability is not ok, skip
418 if ! check_capability "$usage" $requiredPubCapability ; then
419 log " - unacceptable primary key capability ($usage)."
423 # mark overall key as ok
426 # mark primary key as ok if capability is ok
427 if check_capability "$usage" $requiredCapability ; then
432 if [ "$lastKey" != pub ] ; then
433 log " - got a user ID after a sub key?! user IDs should only follow primary keys!"
436 # if an acceptable user ID was already found, skip
437 if [ "$uidOK" = 'true' ] ; then
440 # if the user ID does matches...
441 if [ "$(echo "$uidfpr" | gpg_unescape)" = "$userID" ] ; then
442 # and the user ID validity is ok
443 if [ "$validity" = 'u' -o "$validity" = 'f' ] ; then
444 # mark user ID acceptable
451 # output a line for the primary key
453 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
454 log " * acceptable primary key."
455 if [ -z "$sshKey" ] ; then
456 log " ! primary key could not be translated (not RSA or DSA?)."
461 log " - unacceptable primary key."
462 if [ -z "$sshKey" ] ; then
463 log " ! primary key could not be translated (not RSA or DSA?)."
470 # unset acceptability of last key
475 # don't bother with sub keys if the primary key is not valid
476 if [ "$keyOK" != true ] ; then
480 # don't bother with sub keys if no user ID is acceptable:
481 if [ "$uidOK" != true ] ; then
485 # if sub key validity is not ok, skip
486 if [ "$validity" != 'u' -a "$validity" != 'f' ] ; then
489 # if sub key capability is not ok, skip
490 if ! check_capability "$usage" $requiredCapability ; then
497 'fpr') # key fingerprint
498 fingerprint="$uidfpr"
500 sshKey=$(gpg2ssh "$fingerprint")
502 # if the last key was the pub key, skip
503 if [ "$lastKey" = pub ] ; then
507 # output a line for the sub key
509 if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
510 log " * acceptable sub key."
511 if [ -z "$sshKey" ] ; then
512 log " ! sub key could not be translated (not RSA or DSA?)."
517 log " - unacceptable sub key."
518 if [ -z "$sshKey" ] ; then
519 log " ! sub key could not be translated (not RSA or DSA?)."
526 done | sort -t: -k1 -n -r
527 # NOTE: this last sort is important so that the "good" keys (key
528 # flag '0') come last. This is so that they take precedence when
529 # being processed in the key files over "bad" keys (key flag '1')
532 # process a single host in the known_host file
533 process_host_known_hosts() {
543 userID="ssh://${host}"
545 log "processing: $host"
551 for line in $(process_user_id "${userID}") ; do
552 # note that key was found
555 ok=$(echo "$line" | cut -d: -f1)
556 sshKey=$(echo "$line" | cut -d: -f2)
558 if [ -z "$sshKey" ] ; then
562 # remove the old host key line, and note if removed
563 remove_line "$KNOWN_HOSTS" "$sshKey"
565 # if key OK, add new host line
566 if [ "$ok" -eq '0' ] ; then
567 # note that key was found ok
568 nKeysOK=$((nKeysOK+1))
571 if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
572 # FIXME: this is really hackish cause ssh-keygen won't
573 # hash from stdin to stdout
575 ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
576 ssh-keygen -H -f "$tmpfile" 2> /dev/null
577 cat "$tmpfile" >> "$KNOWN_HOSTS"
578 rm -f "$tmpfile" "${tmpfile}.old"
580 ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
585 # if at least one key was found...
586 if [ "$nKeys" -gt 0 ] ; then
587 # if ok keys were found, return 0
588 if [ "$nKeysOK" -gt 0 ] ; then
594 # if no keys were found, return 1
600 # update the known_hosts file for a set of hosts listed on command
602 update_known_hosts() {
609 # the number of hosts specified on command line
615 # set the trap to remove any lockfiles on exit
616 trap "lockfile-remove $KNOWN_HOSTS" EXIT
618 # create a lockfile on known_hosts
619 lockfile-create "$KNOWN_HOSTS"
621 # note pre update file checksum
622 fileCheck="$(file_hash "$KNOWN_HOSTS")"
626 process_host_known_hosts "$host"
630 nHostsOK=$((nHostsOK+1))
633 nHostsBAD=$((nHostsBAD+1))
637 # touch the lockfile, for good measure.
638 lockfile-touch --oneshot "$KNOWN_HOSTS"
641 # remove the lockfile
642 lockfile-remove "$KNOWN_HOSTS"
644 # note if the known_hosts file was updated
645 if [ "$(file_hash "$KNOWN_HOSTS")" != "$fileCheck" ] ; then
646 log "known_hosts file updated."
649 # if an acceptable host was found, return 0
650 if [ "$nHostsOK" -gt 0 ] ; then
652 # else if no ok hosts were found...
654 # if no bad host were found then no hosts were found at all,
656 if [ "$nHostsBAD" -eq 0 ] ; then
658 # else if at least one bad host was found, return 2
665 # process hosts from a known_hosts file
666 process_known_hosts() {
669 log "processing known_hosts file..."
671 hosts=$(meat "$KNOWN_HOSTS" | cut -d ' ' -f 1 | grep -v '^|.*$' | tr , ' ' | tr '\n' ' ')
673 if [ -z "$hosts" ] ; then
674 log "no hosts to process."
678 # take all the hosts from the known_hosts file (first
679 # field), grep out all the hashed hosts (lines starting
681 update_known_hosts $hosts
684 # process uids for the authorized_keys file
685 process_uid_authorized_keys() {
694 log "processing: $userID"
700 for line in $(process_user_id "$userID") ; do
701 # note that key was found
704 ok=$(echo "$line" | cut -d: -f1)
705 sshKey=$(echo "$line" | cut -d: -f2)
707 if [ -z "$sshKey" ] ; then
711 # remove the old host key line
712 remove_line "$AUTHORIZED_KEYS" "$sshKey"
714 # if key OK, add new host line
715 if [ "$ok" -eq '0' ] ; then
716 # note that key was found ok
717 nKeysOK=$((nKeysOK+1))
719 ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
723 # if at least one key was found...
724 if [ "$nKeys" -gt 0 ] ; then
725 # if ok keys were found, return 0
726 if [ "$nKeysOK" -gt 0 ] ; then
732 # if no keys were found, return 1
738 # update the authorized_keys files from a list of user IDs on command
740 update_authorized_keys() {
747 # the number of ids specified on command line
753 # set the trap to remove any lockfiles on exit
754 trap "lockfile-remove $AUTHORIZED_KEYS" EXIT
756 # create a lockfile on authorized_keys
757 lockfile-create "$AUTHORIZED_KEYS"
759 # note pre update file checksum
760 fileCheck="$(file_hash "$AUTHORIZED_KEYS")"
762 # remove any monkeysphere lines from authorized_keys file
763 remove_monkeysphere_lines "$AUTHORIZED_KEYS"
766 # process the user ID, change return code if key not found for
768 process_uid_authorized_keys "$userID"
776 nIDsBAD=$((nIDsBAD+1))
780 # touch the lockfile, for good measure.
781 lockfile-touch --oneshot "$AUTHORIZED_KEYS"
784 # remove the lockfile
785 lockfile-remove "$AUTHORIZED_KEYS"
787 # note if the authorized_keys file was updated
788 if [ "$(file_hash "$AUTHORIZED_KEYS")" != "$fileCheck" ] ; then
789 log "authorized_keys file updated."
792 # if an acceptable id was found, return 0
793 if [ "$nIDsOK" -gt 0 ] ; then
795 # else if no ok ids were found...
797 # if no bad ids were found then no ids were found at all, and
799 if [ "$nIDsBAD" -eq 0 ] ; then
801 # else if at least one bad id was found, return 2
808 # process an authorized_user_ids file for authorized_keys
809 process_authorized_user_ids() {
814 authorizedUserIDs="$1"
816 log "processing authorized_user_ids file..."
818 if ! meat "$authorizedUserIDs" > /dev/null ; then
819 log "no user IDs to process."
825 # extract user IDs from authorized_user_ids file
827 for line in $(meat "$authorizedUserIDs") ; do
828 userIDs["$nline"]="$line"
832 update_authorized_keys "${userIDs[@]}"