fix need for only single argument to gpg_sphere
[monkeysphere.git] / src / share / common
index c88857e44adabca90d57bb02358bcce92d16f67d..34ac52da7f5dcfcdd2e893be3234ac400002b18c 100644 (file)
@@ -103,12 +103,12 @@ su_monkeysphere_user() {
     case $(id -un) in
        # if monkeysphere user, run the command under bash
        "$MONKEYSPHERE_USER")
-           bash -c "$@"
+           bash -c "$*"
            ;;
 
          # if root, su command as monkeysphere user
        'root')
-           su "$MONKEYSPHERE_USER" -c "$@"
+           su "$MONKEYSPHERE_USER" -c "$*"
            ;;
 
        # otherwise, fail
@@ -281,7 +281,7 @@ get_gpg_expiration() {
 
     keyExpire="$1"
 
-    if [ -z "$keyExpire" -a "$PROMPT" = 'true' ]; then
+    if [ -z "$keyExpire" -a "$PROMPT" != 'false' ]; then
        cat >&2 <<EOF
 Please specify how long the key should be valid.
          0 = key does not expire
@@ -310,7 +310,7 @@ passphrase_prompt() {
     local fifo="$2"
     local PASS
 
-    if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null; then
+    if [ "$DISPLAY" ] && type "${SSH_ASKPASS:-ssh-askpass}" >/dev/null 2>/dev/null; then
        printf 'Launching "%s"\n' "${SSH_ASKPASS:-ssh-askpass}" | log info
        printf '(with prompt "%s")\n' "$prompt" | log debug
        "${SSH_ASKPASS:-ssh-askpass}" "$prompt" > "$fifo"
@@ -411,58 +411,16 @@ test_gpg_expire() {
 check_key_file_permissions() {
     local uname
     local path
-    local stat
-    local access
-    local gAccess
-    local oAccess
-
-    # function to check that the given permission corresponds to writability
-    is_write() {
-       [ "$1" = "w" ]
-    }
 
     uname="$1"
     path="$2"
 
-    log debug "checking path permission '$path'..."
-
-    # rewrite path if it points to a symlink
-    if [ -h "$path" ] ; then
-       path=$(readlink -f "$path")
-       log debug "checking path symlink '$path'..."
-    fi
-
-    # return 255 if cannot stat file
-    if ! stat=$(ls -ld "$path" 2>/dev/null) ; then
-        log error "could not stat path '$path'."
-       return 255
-    fi
-
-    owner=$(echo "$stat" | awk '{ print $3 }')
-    gAccess=$(echo "$stat" | cut -c6)
-    oAccess=$(echo "$stat" | cut -c9)
-
-    # return 1 if path has invalid owner
-    if [ "$owner" != "$uname" -a "$owner" != 'root' ] ; then
-       log error "improper ownership on path '$path':"
-       log error " $owner != ($uname|root)"
-       return 1
-    fi
-
-    # return 2 if path has group or other writability
-    if is_write "$gAccess" || is_write "$oAccess" ; then
-       log error "improper group or other writability on path '$path':"
-       log error " group: $gAccess, other: $oAccess"
-       return 2
-    fi
-
-    # return zero if all clear, or go to next path
-    if [ "$path" = '/' ] ; then
-       log debug "path ok."
+    if [ "$STRICT_MODES" = 'false' ] ; then
+       log debug "skipping path permission check for '$path' because STRICT_MODES is false..."
        return 0
-    else
-       check_key_file_permissions "$uname" $(dirname "$path")
     fi
+    log debug "checking path permission '$path'..."
+    "${SYSSHAREDIR}/checkperms" "$uname" "$path"
 }
 
 # return a list of all users on the system
@@ -478,12 +436,40 @@ list_users() {
     fi
 }
 
+# take one argument, a service name.  in response, print a series of
+# lines, each with a unique numeric port number that might be
+# associated with that service name.  (e.g. in: "https", out: "443")
+# if nothing is found, print nothing, and return 0.
+# 
+# return 1 if there was an error in the search somehow
+get_port_for_service() {
+
+    [[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]] || \
+        failure $(printf "This is not a valid service name: '%s'" "$1")
+    if type getent &>/dev/null ; then
+        # for linux and FreeBSD systems (getent returns 2 if not found, 0 on success, 1 or 3 on various failures)
+        (getent services "$service" || if [ "$?" -eq 2 ] ; then true ; else false; fi) | awk '{ print $2 }' | cut -f1 -d/ | sort -u
+    elif [ -r /etc/services ] ; then
+        # fall back to /etc/services for systems that don't have getent (MacOS?)
+        # FIXME: doesn't handle aliases like "null" (or "http"?), which don't show up at the beginning of the line.
+        awk $(printf '/^%s[[:space:]]/{ print $2 }' "$1") /etc/services | cut -f1 -d/ | sort -u
+    else
+        return 1
+    fi
+}
+
 # return the path to the home directory of a user
 get_homedir() {
     local uname=${1:-`whoami`}
     eval "echo ~${uname}"
 }
 
+# return the primary group of a user
+get_primary_group() {
+    local uname=${1:-`whoami`}
+    groups "$uname" | sed 's/^..* : //' | awk '{ print $1 }'
+}
+
 ### CONVERSION UTILITIES
 
 # output the ssh key for a given key ID
@@ -492,7 +478,7 @@ gpg2ssh() {
     
     keyID="$1"
 
-    gpg --export "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
+    gpg --export --no-armor "$keyID" | openpgp2ssh "$keyID" 2>/dev/null
 }
 
 # output known_hosts line from ssh key
@@ -519,13 +505,15 @@ ssh2known_hosts() {
 
 # output authorized_keys line from ssh key
 ssh2authorized_keys() {
-    local userID
-    local key
-    
-    userID="$1"
-    key="$2"
+    local koptions="$1"
+    local userID="$2"
+    local key="$3"
 
-    printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
+    if [[ -z "$koptions" ]]; then
+        printf "%s MonkeySphere%s %s\n" "$key" "$DATE" "$userID"
+    else
+        printf "%s %s MonkeySphere%s %s\n" "$koptions" "$key" "$DATE" "$userID"
+    fi
 }
 
 # convert key from gpg to ssh known_hosts format
@@ -566,6 +554,15 @@ gpg2authorized_keys() {
 
 ### GPG UTILITIES
 
+# script to determine if gpg version is equal to or greater than specified version
+is_gpg_version_greater_equal() {
+    local gpgVersion=$(gpg --version | head -1 | awk '{ print $3 }')
+    local latest=$(printf '%s\n%s\n' "$1" "$gpgVersion" \
+       | tr '.' ' ' | sort -g -k1 -k2 -k3 \
+       | tail -1 | tr ' ' '.')
+    [[ "$gpgVersion" == "$latest" ]]
+}
+
 # retrieve all keys with given user id from keyserver
 # FIXME: need to figure out how to retrieve all matching keys
 # (not just first N (5 in this case))
@@ -586,6 +583,10 @@ gpg_fetch_userid() {
        --search ="$userID" &>/dev/null
     returnCode="$?"
 
+    if [ "$returnCode" != 0 ] ; then
+        log error "Failure ($returnCode) searching keyserver $KEYSERVER for user id '$userID'"
+    fi
+
     return "$returnCode"
 }
 
@@ -595,7 +596,7 @@ gpg_fetch_userid() {
 # userid and key policy checking
 # the following checks policy on the returned keys
 # - checks that full key has appropriate valididy (u|f)
-# - checks key has specified capability (REQUIRED_*_KEY_CAPABILITY)
+# - checks key has specified capability (REQUIRED_KEY_CAPABILITY)
 # - checks that requested user ID has appropriate validity
 # (see /usr/share/doc/gnupg/DETAILS.gz)
 # output is one line for every found key, in the following format:
@@ -603,15 +604,13 @@ gpg_fetch_userid() {
 # flag:sshKey
 #
 # "flag" is an acceptability flag, 0 = ok, 1 = bad
-# "sshKey" is the translated gpg key
+# "sshKey" is the relevant OpenPGP key, in the form accepted by OpenSSH
 #
 # all log output must go to stderr, as stdout is used to pass the
 # flag:sshKey to the calling function.
-#
-# expects global variable: "MODE"
 process_user_id() {
     local returnCode=0
-    local userID
+    local userID="$1"
     local requiredCapability
     local requiredPubCapability
     local gpgOut
@@ -626,14 +625,8 @@ process_user_id() {
     local lastKeyOK
     local fingerprint
 
-    userID="$1"
-
     # set the required key capability based on the mode
-    if [ "$MODE" = 'known_hosts' ] ; then
-       requiredCapability="$REQUIRED_HOST_KEY_CAPABILITY"
-    elif [ "$MODE" = 'authorized_keys' ] ; then
-       requiredCapability="$REQUIRED_USER_KEY_CAPABILITY"      
-    fi
+    requiredCapability=${REQUIRED_KEY_CAPABILITY:="a"}
     requiredPubCapability=$(echo "$requiredCapability" | tr "[:lower:]" "[:upper:]")
 
     # fetch the user ID if necessary/requested
@@ -716,7 +709,7 @@ process_user_id() {
                if [ "$keyOK" -a "$uidOK" -a "$lastKeyOK" ] ; then
                    log verbose "  * acceptable primary key."
                    if [ -z "$sshKey" ] ; then
-                       log error "    ! primary key could not be translated (not RSA?)."
+                       log verbose "    ! primary key could not be translated (not RSA?)."
                    else
                        echo "0:${sshKey}"
                    fi
@@ -794,6 +787,59 @@ process_user_id() {
     # being processed in the key files over "bad" keys (key flag '1')
 }
 
+# output all valid keys for specified user ID literal
+keys_for_userid() {
+    local userID
+    local noKey=
+    local nKeys
+    local nKeysOK
+    local ok
+    local sshKey
+    local tmpfile
+
+    userID="$1"
+
+    log verbose "processing: $userID"
+
+    nKeys=0
+    nKeysOK=0
+
+    IFS=$'\n'
+    for line in $(process_user_id "${userID}") ; do
+       # note that key was found
+       nKeys=$((nKeys+1))
+
+       ok=$(echo "$line" | cut -d: -f1)
+       sshKey=$(echo "$line" | cut -d: -f2)
+
+        if [ -z "$sshKey" ] ; then
+            continue
+        fi
+
+       # if key OK, output key to stdout
+       if [ "$ok" -eq '0' ] ; then
+           # note that key was found ok
+           nKeysOK=$((nKeysOK+1))
+
+           printf '%s\n' "$sshKey"
+       fi
+    done
+
+    # if at least one key was found...
+    if [ "$nKeys" -gt 0 ] ; then
+       # if ok keys were found, return 0
+       if [ "$nKeysOK" -gt 0 ] ; then
+           return 0
+       # else return 2
+       else
+           return 2
+       fi
+    # if no keys were found, return 1
+    else
+       return 1
+    fi
+}
+
 # process a single host in the known_host file
 process_host_known_hosts() {
     local host
@@ -806,7 +852,7 @@ process_host_known_hosts() {
     local tmpfile
 
     # set the key processing mode
-    export MODE='known_hosts'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_HOST_KEY_CAPABILITY"
 
     host="$1"
     userID="ssh://${host}"
@@ -839,13 +885,25 @@ process_host_known_hosts() {
 
            # hash if specified
            if [ "$HASH_KNOWN_HOSTS" = 'true' ] ; then
+                if (type ssh-keygen >/dev/null) ; then
                # FIXME: this is really hackish cause ssh-keygen won't
                # hash from stdin to stdout
-               tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
-               ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
-               ssh-keygen -H -f "$tmpfile" 2>/dev/null
-               cat "$tmpfile" >> "$KNOWN_HOSTS"
-               rm -f "$tmpfile" "${tmpfile}.old"
+                   tmpfile=$(mktemp ${TMPDIR:-/tmp}/tmp.XXXXXXXXXX)
+                   ssh2known_hosts "$host" "$sshKey" > "$tmpfile"
+                   ssh-keygen -H -f "$tmpfile" 2>/dev/null
+                   cat "$tmpfile" >> "$KNOWN_HOSTS"
+                   rm -f "$tmpfile" "${tmpfile}.old"
+                else
+       # FIXME: we could do this without needing ssh-keygen.  hashed
+       # known_hosts looks like: |1|X|Y where 1 means SHA1 (nothing
+       # else is defined in openssh sources), X is the salt (same
+       # length as the digest output), base64-encoded, and Y is the
+       # digested hostname (also base64-encoded).
+
+       # see hostfile.{c,h} in openssh sources.
+
+                    failure "Cannot hash known_hosts as requested"
+                fi
            else
                ssh2known_hosts "$host" "$sshKey" >> "$KNOWN_HOSTS"
            fi
@@ -881,6 +939,7 @@ update_known_hosts() {
     local nHostsBAD
     local fileCheck
     local host
+    local newUmask
 
     # the number of hosts specified on command line
     nHosts="$#"
@@ -890,10 +949,20 @@ update_known_hosts() {
 
     # touch the known_hosts file so that the file permission check
     # below won't fail upon not finding the file
-    (umask 0022 && touch "$KNOWN_HOSTS")
+    if [ ! -f "$KNOWN_HOSTS" ]; then
+       # make sure to create any files or directories with the appropriate write bits turned off:
+       newUmask=$(printf "%04o" $(( 0$(umask) | 0022 )) )
+       [ -d $(dirname "$KNOWN_HOSTS") ] \
+           || (umask "$newUmask" && mkdir -p -m 0700 $(dirname "$KNOWN_HOSTS") ) \
+           || failure "Could not create path to known_hosts file '$KNOWN_HOSTS'"
+       # make sure to create this file with the appropriate bits turned off:
+       (umask "$newUmask" && touch "$KNOWN_HOSTS") \
+           || failure "Unable to create known_hosts file '$KNOWN_HOSTS'"
+    fi
 
     # check permissions on the known_hosts file path
-    check_key_file_permissions $(whoami) "$KNOWN_HOSTS" || failure
+    check_key_file_permissions $(whoami) "$KNOWN_HOSTS" \
+       || failure "Bad permissions governing known_hosts file '$KNOWN_HOSTS'"
 
     # create a lockfile on known_hosts:
     lock create "$KNOWN_HOSTS"
@@ -901,7 +970,7 @@ update_known_hosts() {
     trap "lock remove $KNOWN_HOSTS" EXIT
 
     # note pre update file checksum
-    fileCheck="$(file_hash "$KNOWN_HOSTS")"
+    fileCheck=$(file_hash "$KNOWN_HOSTS")
 
     for host ; do
        # process the host
@@ -973,15 +1042,17 @@ process_known_hosts() {
 # process uids for the authorized_keys file
 process_uid_authorized_keys() {
     local userID
+    local koptions
     local nKeys
     local nKeysOK
     local ok
     local sshKey
 
     # set the key processing mode
-    export MODE='authorized_keys'
+    export REQUIRED_KEY_CAPABILITY="$REQUIRED_USER_KEY_CAPABILITY"
 
-    userID="$1"
+    koptions="$1"
+    userID="$2"
 
     log verbose "processing: $userID"
 
@@ -1008,7 +1079,7 @@ process_uid_authorized_keys() {
            # note that key was found ok
            nKeysOK=$((nKeysOK+1))
 
-           ssh2authorized_keys "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
+           ssh2authorized_keys "$koptions" "$userID" "$sshKey" >> "$AUTHORIZED_KEYS"
        fi
     done
 
@@ -1036,9 +1107,14 @@ update_authorized_keys() {
     local nIDsOK
     local nIDsBAD
     local fileCheck
+    local x koptions
+    declare -i argtype
+
+    if (( $# % 2 )); then log error "Bad number of arguments; this should never happen."; return 1; fi
 
     # the number of ids specified on command line
-    nIDs="$#"
+    (( nIDs=$#/2 ))
+    (( argtype=0 ))
 
     nIDsOK=0
     nIDsBAD=0
@@ -1060,23 +1136,30 @@ update_authorized_keys() {
     # remove any monkeysphere lines from authorized_keys file
     remove_monkeysphere_lines "$AUTHORIZED_KEYS"
 
-    for userID ; do
-       # process the user ID, change return code if key not found for
-       # user ID
-       process_uid_authorized_keys "$userID" || returnCode="$?"
-
-       # note the result
-       case "$returnCode" in
-           0)
-               nIDsOK=$((nIDsOK+1))
-               ;;
-           2)
-               nIDsBAD=$((nIDsBAD+1))
-               ;;
-       esac
-
-       # touch the lockfile, for good measure.
-       lock touch "$AUTHORIZED_KEYS"
+    for x; do
+        (( argtype++ ))
+        if (( $argtype % 2 )); then
+            koptions="$x"
+        else
+            userID="$x"
+
+           # process the user ID, change return code if key not found
+           # for user ID
+           process_uid_authorized_keys "$koptions" "$userID" || returnCode="$?"
+
+           # note the result
+           case "$returnCode" in
+               0)
+                   nIDsOK=$((nIDsOK+1))
+                   ;;
+               2)
+                   nIDsBAD=$((nIDsBAD+1))
+                   ;;
+           esac
+
+           # touch the lockfile, for good measure.
+           lock touch "$AUTHORIZED_KEYS"
+       fi
     done
 
     # remove the lockfile and the trap
@@ -1109,11 +1192,15 @@ update_authorized_keys() {
 # process an authorized_user_ids file for authorized_keys
 process_authorized_user_ids() {
     local line
-    local nline
-    local userIDs
+    declare -i nline
+    declare -a userIDs
+    declare -a koptions
+    declare -a export_array
 
     authorizedUserIDs="$1"
 
+    (( nline=0 ))
+
     # exit if the authorized_user_ids file is empty
     if [ ! -e "$authorizedUserIDs" ] ; then
        failure "authorized_user_ids file '$authorizedUserIDs' does not exist."
@@ -1135,20 +1222,50 @@ process_authorized_user_ids() {
     # extract user IDs from authorized_user_ids file
     IFS=$'\n'
     for line in $(meat "$authorizedUserIDs") ; do
-       userIDs["$nline"]="$line"
-       nline=$((nline+1))
+       case "$line" in
+           (" "*|$'\t'*)
+               if [[ -z ${koptions[${nline}]} ]]; then
+                   koptions[${nline}]=$(echo $line | sed 's/^[         ]*//;s/[        ]$//;')
+               else
+                   koptions[${nline}]="${koptions[${nline}]},$(echo $line | sed 's/^[  ]*//;s/[        ]$//;')"
+               fi
+               ;;
+            (*)
+               ((nline++))
+               userIDs[${nline}]="$line"
+               unset koptions[${nline}] || true
+               ;;
+       esac
+    done
+
+    for i in $(seq 1 $nline); do
+        export_array+=("${koptions[$i]}" "${userIDs[$i]}")
     done
 
-    update_authorized_keys "${userIDs[@]}"
+    update_authorized_keys "${export_array[@]}"
 }
 
 # takes a gpg key or keys on stdin, and outputs a list of
 # fingerprints, one per line:
 list_primary_fingerprints() {
     local fake=$(msmktempdir)
-    GNUPGHOME="$fake" gpg --no-tty --quiet --import
+    trap "rm -rf $fake" EXIT
+    GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
     GNUPGHOME="$fake" gpg --with-colons --fingerprint --list-keys | \
        awk -F: '/^fpr:/{ print $10 }'
+    trap - EXIT
+    rm -rf "$fake"
+}
+
+# takes an OpenPGP key or set of keys on stdin, a fingerprint or other
+# key identifier as $1, and outputs the gpg-formatted information for
+# the requested keys from the material on stdin
+get_cert_info() {
+    local fake=$(msmktempdir)
+    trap "rm -rf $fake" EXIT
+    GNUPGHOME="$fake" gpg --no-tty --quiet --import --ignore-time-conflict 2>/dev/null
+    GNUPGHOME="$fake" gpg --with-colons --fingerprint --fixed-list-mode --list-keys "$1"
+    trap - EXIT
     rm -rf "$fake"
 }