#!/bin/sh
# Audit the system crypto policy against this sshd before it is applied.
#
# crypto-policies puts the policy in CRYPTO_POLICY as a list of -o options and
# the service unit passes it on sshd's command line (el8 mechanism; the variable
# is set by /etc/crypto-policies/back-ends/opensshserver.config, which the unit
# reads as an EnvironmentFile, and may be emptied in /etc/sysconfig/sshd to opt
# out).  Two kinds of entry in it are fatal for an upstream build, because sshd
# aborts on an unknown -o option or algorithm name:
#
#   * options only RHEL's patched openssh has, notably GSSAPIKexAlgorithms from
#     its GSSAPI-key-exchange patch.  Upstream has GSSAPI authentication but no
#     GSSAPI key exchange at all, so the option has nothing to configure here
#     and dropping it loses nothing.
#   * algorithm names from a policy generated for a newer openssh, e.g.
#     mlkem1024nistp384-sha384 in the mlkem-capable policies: upstream 10.5
#     implements ML-KEM-768 only (kexmlkem768x25519.c, kexmlkem768ecdh.c), so
#     that name -- and only that name -- is dropped from the list.
#
# Everything else is passed through unchanged and in the policy's own order, so
# update-crypto-policies keeps working and nothing has to be pinned.
#
# Each entry is tried against the daemon itself, on a private throwaway
# configuration, so the answer is about this binary and the policy entry alone:
# the host's sshd_config and its host keys cannot make a good entry look bad (an
# unreadable host key makes a plain "sshd -T" fail outright, with no output to
# judge by).  Nothing is ever dropped because of the host's own state.
#
# Everything dropped is logged to stderr, which lands in the journal.

SSHD=/usr/sbin/sshd

policy=${CRYPTO_POLICY:-}
if [ -z "$policy" ]; then
    # No policy from crypto-policies (or the admin opted out in
    # /etc/sysconfig/sshd): pass nothing, add nothing.
    exit 0
fi

log() {
    echo "crypto-policy: $*" >&2
}

# A configuration of our own, holding nothing but a throwaway host key, so that
# probing does not depend on /etc/ssh at all.
probe_dir=$(mktemp -d) || exit 0
trap 'rm -rf "$probe_dir"' EXIT
ssh-keygen -q -t ed25519 -N '' -f "$probe_dir/hostkey" >/dev/null 2>&1 || exit 0
printf 'HostKey %s\n' "$probe_dir/hostkey" >"$probe_dir/sshd_config"

probe="$SSHD -T -f $probe_dir/sshd_config"

# Option sshd does not implement at all?
unknown_option() {
    $probe -o "$1=x" 2>&1 | grep -q "Bad configuration option"
}

# Does sshd take this option with this value?  Judged from the configuration it
# prints, never from stderr.
takes_option() {
    $probe -o "$1=$2" 2>/dev/null | grep -iq "^$1 "
}

# Same question for one algorithm name inside a list-valued option.
takes_algorithm() {
    $probe -o "$1=$2" 2>/dev/null | grep -i "^$1 " | grep -qF -- "$2"
}

is_list_option() {
    case "$1" in
    KexAlgorithms | Ciphers | MACs | HostKeyAlgorithms | \
        HostbasedAcceptedAlgorithms | HostbasedAcceptedKeyTypes | \
        PubkeyAcceptedAlgorithms | PubkeyAcceptedKeyTypes | \
        CASignatureAlgorithms)
        return 0
        ;;
    *) return 1 ;;
    esac
}

# The el8 crypto policies still use the pre-8.5 names for two options, which
# this build no longer knows.  Renaming them keeps the restriction the policy
# asks for instead of dropping it; sshd resolves the new name below.
rename_option() {
    case "$1" in
    PubkeyAcceptedKeyTypes) echo PubkeyAcceptedAlgorithms ;;
    HostbasedAcceptedKeyTypes) echo HostbasedAcceptedAlgorithms ;;
    *) echo "$1" ;;
    esac
}

args=""
for word in $policy; do
    case "$word" in
    -o*=*) ;;
    *) continue ;;
    esac
    option=${word#-o}
    name=${option%%=*}
    value=${option#*=}
    name=$(rename_option "$name")

    if unknown_option "$name"; then
        log "dropping option $name: this sshd does not implement it"
        continue
    fi

    if is_list_option "$name"; then
        kept=""
        dropped=""
        # Split on commas, then put IFS back immediately: the probes below expand
        # $probe, and a comma IFS would keep them from being word split (which
        # would make every algorithm look unsupported).
        saved_ifs=$IFS
        IFS=,
        set -- $value
        IFS=$saved_ifs
        for alg in "$@"; do
            if takes_algorithm "$name" "$alg"; then
                kept="$kept,$alg"
            else
                dropped="$dropped $alg"
            fi
        done
        value=${kept#,}
        if [ -n "$dropped" ]; then
            log "dropping$dropped from $name: not implemented by this sshd"
        fi
        if [ -z "$value" ]; then
            log "dropping option $name: no value this sshd supports"
            continue
        fi
    elif ! takes_option "$name" "$value"; then
        log "dropping option $name=$value: this sshd does not accept it"
        continue
    fi

    args="$args -o $name=$value"
done

printf '%s\n' $args
