#!/bin/bash

set -e
shopt -s nullglob

sshdconf=/etc/ssh/sshd_config
rc=0

declare -A algotypes
algotypes[ed25519]="good"
algotypes[ed25519-sk]="good"
algotypes[rsa]="good"
algotypes[ecdsa]="deprecated"
algotypes[ecdsa-sk]="deprecated"
algotypes[dsa]="weak"

# Used in authorized_keys and known_hosts.
declare -A keytypes
keytypes[ssh-ed25519]="good"
keytypes[sk-ssh-ed25519@openssh.com]="good"
keytypes[ssh-rsa]="good"
keytypes[ecdsa-sha2-nistp256]="deprecated"
keytypes[ecdsa-sha2-nistp384]="deprecated"
keytypes[ecdsa-sha2-nistp521]="deprecated"
keytypes[sk-ecdsa-sha2-nistp256@openssh.com]="deprecated"
keytypes[ssh-dss]="weak"

declare -A keybits_good keybits_weak
keybits_good[ed25519]=0
keybits_weak[ed25519]=256
keybits_good[rsa]=4096
keybits_weak[rsa]=3072
keybits_good[ecdsa]=0
keybits_weak[ecdsa]=256
keybits_good[dsa]=0
keybits_weak[dsa]=4096

declare -a reqkeys
reqkeys=(ed25519 rsa)

declare -A mdtype
mdtype[sha512]="good"
mdtype[sha256]="good"
mdtype[sha1]="weak"
mdtype[md5]="weak"

warn()
{
  local msg="$*"

  printf "warning: %s\n" "${msg}" >&2

  if (( rc < 1 )); then
    rc=1
  fi
}

error()
{
  local msg="$*"

  printf "error: %s\n" "${msg}" >&2

  if (( rc < 2 )); then
    rc=2
  fi
}

check_authorized_keys()
{
  local file="$1"

  if ! [[ -e "${file}" ]]; then
    return
  fi

  # Parse input process substitution from the end of the block.
  while read -r -a fields; do
    # Skip comments and blank lines.
    if [[ "${fields[0]}" =~ ^# ]] ||
       [[ "${fields[*]}" =~ ^[[:space:]]*$ ]]; then
      continue
    fi

    local keytype
    case "${#fields[@]}" in
      4)
        # options keytype fingerprint comment
        keytype="${fields[1]}"
        ;;
      3)
        # keytype fingerprint comment
        keytype="${fields[0]}"
        if [[ -z "${keytypes[${keytype}]}" ]]; then
          # options keytype fingerprint
          keytype="${fields[1]}"
        fi
        ;;
      2)
        # keytype fingerprint
        keytype="${fields[0]}"
        ;;
      *)
        warn "unknown format in authorized_keys ${file}: ${fields[*]}"
        continue
        ;;
    esac

    case "${keytypes[${keytype}]}" in
      good)
        # Good.
        ;;
      deprecated)
        warn "deprecated key type in authorized_keys ${file}): ${fields[*]}"
        ;;
      weak)
        error "weak key type in authorized_keys ${file}: ${fields[*]}"
        ;;
      *)
        warn "unknown key type in authorized_keys ${file}: ${fields[*]}"
        ;;
    esac
  done <"${file}"
}

check_known_hosts()
{
  local file="$1"

  if ! [[ -e "${file}" ]]; then
    return
  fi

  # Parse input process substitution from the end of the block.
  while read -r hostnames keytype key comment; do
    case "${keytypes[${keytype}]}" in
      good)
        # Good.
        ;;
      deprecated)
        warn "deprecated key type in known_hosts ${file}: ${hostnames} ${keytype} ${key} ${comment}"
        ;;
      weak)
        error "weak key type in known_hosts ${file}: ${hostnames} ${keytype} ${key} ${comment}"
        ;;
      *)
        warn "unknown key type in known_hosts ${file}: ${hostnames} ${keytype} ${key} ${comment}"
        ;;
    esac
  done < <(grep -Ev '^(#|[[:space:]]*$)' < "${file}")
}

check_key()
{
  local key="$1"

  # Parse input process substitution from the end of the block.
  while read -r bits fingerprint user algo; do
    algo=${algo@L}
    algo=${algo##(}
    algo=${algo%%)}

    case "${algotypes[${algo}]}" in
      good)
        # Good.
        ;;
      deprecated)
        warn "deprecated key type in key ${key}: ${bits} ${fingerprint} ${user} ${algo}"
        ;;
      weak)
        error "weak key type in key ${key}: ${bits} ${fingerprint} ${user} ${algo}"
        ;;
      *)
        warn "unknown key type in key ${key}: ${bits} ${fingerprint} ${user} ${algo}"
        ;;
    esac

    if (( bits < keybits_good[algo] )); then
      warn "key ${key} bits are not strong (${bits} < ${keybits_good[${algo}]})"
    elif (( bits < keybits_weak[algo] )); then
      if (( keybits_good[algo] == 0 )); then
        error "key ${key} bits are weak (${bits} < ${keybits_weak[${algo}]})"
      else
        error "key ${key} bits are weak (${bits} < ${keybits_weak[${algo}]} < ${keybits_good[${algo}]})"
      fi
    fi

    local digest=${fingerprint%:*}
    digest=${digest@L}
    case "${mdtype[${digest}]}" in
      good)
        # Good.
        ;;
      *)
        error "key ${key} uses weak digest function (${digest})"
        ;;
    esac
  done < <(ssh-keygen -l -f "${key}")
}

### Host checks ###

# Get the list of host key files from less to more explicit: known, disk, conf.
declare -A hostkeys

for keytype in "${!algotypes[@]}"; do
  hostkey="/etc/ssh/ssh_host_${keytype}_key"
  hostkeys[${hostkey}]="known"
done

for hostkey in /etc/ssh/ssh_host_*_key; do
  hostkeys[${hostkey}]="disk"
done

if grep -q ^HostKey "${sshdconf}"; then
  mapfile -t hostkeys_conf < <(sed -ne 's/.*HostKey[[:space:]]*//p' "${sshdconf}")
  for hostkey in "${hostkeys_conf[@]}"; do
    hostkeys[${hostkey}]="conf"
  done
fi

for hostkey in "${!hostkeys[@]}"; do
  # Check missing host keys
  for reqkey in "${reqkeys[@]}"; do
    case "${hostkey}" in
      *_"${reqkey}"_*)
        if [[ ! -e ${hostkey} ]]; then
          warn "missing required host key ${hostkey}"
        fi
        ;;
    esac
  done

  # Check weak host keys
  if [[ -e "${hostkey}" ]]; then
    check_key "${hostkey}"
  fi
done

# Check system known hosts for weak keys
check_known_hosts "/etc/ssh/ssh_known_hosts"

### User checks ###

declare -a homes
for home in $(getent passwd | cut -d: -f 6 | sort -u); do
  if [[ -d "${home}/.ssh/" ]]; then
    homes+=("${home}")
  fi
done

for home in "${homes[@]}"; do
  declare -a pubkeys
  declare -a seckeys
  for key in "${home}"/.ssh/id_*; do
    if [[ "${key}" =~ \.pub$ ]]; then
      pubkeys+=("${key}")
    else
      seckeys+=("${key}")
    fi
  done

  if [[ ${#pubkeys[@]} -eq 0 ]] || [[ ${#seckeys[@]} -eq 0 ]]; then
    continue
  fi

  for pubkey in "${pubkeys[@]}"; do
    seckey="${pubkey%%.pub}"

    if [[ ! -e "${seckey}" ]]; then
      error "missing user secret key for ${pubkey}"
    fi
  done

  # Check weak user keys
  for seckey in "${seckeys[@]}"; do
    pubkey="${seckey}.pub"

    if [[ ! -e "${pubkey}" ]]; then
      error "missing user public key for ${seckey}"
    fi

    if [[ -e "${pubkey}" ]]; then
      check_key "${pubkey}"
    fi
  done

  # Check missing user keys
  for reqkey in "${reqkeys[@]}"; do
    seckey="${home}/.ssh/id_${reqkey}"
    pubkey="${seckey}.pub"

    if [[ ! -e ${seckey} ]]; then
      warn "missing required user secret key ${seckey}"
    fi
    if [[ ! -e ${pubkey} ]]; then
      warn "missing required user public key ${pubkey}"
    fi
  done

  # Check weak authorized keys
  check_authorized_keys "${home}/.ssh/authorized_keys"

  # Check user known_hosts for weak keys
  check_known_hosts "${home}/.ssh/known_hosts"
done

if (( rc > 0 )); then
  echo
  echo "Hint: Check the ngcp-check-ssh-keys(8) man page for actions to take."
fi

exit ${rc}
