#!/usr/bin/env bash

# eac-status checks if the system is set up for Easy Anti-Cheat Linux v2.
# Run it in the linux-cachyos-eac boot entry to also check the running kernel.

EAC_CA_NAME="${EAC_CA_NAME:-CachyOS EAC CA}"
WIKI_URL=https://wiki.cachyos.org/configuration/easy_anti_cheat/#troubleshooting

die() {
    echo "Error: $*"
    exit 1
}

if [ "$(id -u)" -ne 0 ]; then
    die "This script must be run with root privileges."
fi

for cmd in sbctl sbverify tpm2_eventlog; do
    if ! command -v "$cmd" >/dev/null; then
        die "$cmd is missing, install sbctl, sbsigntools and tpm2-tools."
    fi
done

failed=0
warned=0

ok() {
    echo "[ OK ] $*"
}

fail() {
    echo "[FAIL] $*"
    failed=$((failed + 1))
}

warn() {
    echo "[WARN] $*"
    warned=$((warned + 1))
}

# Common name of a DER certificate
cn() {
    openssl x509 -inform DER -noout -subject -nameopt multiline 2>/dev/null \
        | sed -n 's/^ *commonName *= //p'
}

tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT

check_secure_boot() {
    if sbctl status | grep -q 'Secure Boot:.*Enabled'; then
        ok "Secure Boot is enabled"
    else
        fail "Secure Boot is disabled"
    fi

    if sbctl status | grep -q 'Setup Mode:.*Disabled'; then
        ok "Setup Mode is disabled"
    else
        fail "Firmware is in Setup Mode"
    fi
}

check_tpm() {
    if [ "$(cat /sys/class/tpm/tpm0/tpm_version_major 2>/dev/null)" = 2 ]; then
        ok "TPM 2.0 found"
    else
        fail "No TPM 2.0 found"
    fi
}

# Find the EAC certificate in db
check_db() {
    local der

    sbctl --disable-landlock --quiet export-enrolled-keys --dir "$tmp/keys" >/dev/null
    for der in "$tmp"/keys/DB/*.der; do
        if [ "$(cn < "$der")" = "$EAC_CA_NAME" ]; then
            openssl x509 -inform DER -in "$der" -out "$tmp/eac.pem"
            break
        fi
    done

    if [ -f "$tmp/eac.pem" ]; then
        ok "$EAC_CA_NAME is enrolled"
    else
        fail "$EAC_CA_NAME is not enrolled, see the wiki on how to enroll it"
    fi
}

# The file must only be signed by CachyOS. sbctl may sign it again with your
# own key, for example through its mkinitcpio hook.
check_signature() {
    local sigs

    sigs=$(sbverify --list "$1" 2>/dev/null | grep -c '^signature')
    if [ -f "$tmp/eac.pem" ] && sbverify --cert "$tmp/eac.pem" "$1" >/dev/null 2>&1; then
        if [ "$sigs" -eq 1 ]; then
            ok "$1 is signed by CachyOS"
        else
            fail "$1 has $sigs signatures"
        fi
    else
        fail "$1 is not signed by CachyOS"
    fi
}

# The systemd-boot that starts the EAC kernel. In the EAC boot, systemd-boot
# reports the file it was started from. Otherwise it is the copy in
# EFI/cachyos-eac (Limine, rEFInd) or the one from bootctl install.
eac_boot_loader() {
    local esp="$1" file path
    local var=/sys/firmware/efi/efivars/LoaderImageIdentifier-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f

    if [[ "$(uname -r)" == *-cachyos-eac ]] && [ -r "$var" ]; then
        # Skip the 4 byte attributes, the path is UTF-16 with backslashes
        path=$(tail -c +5 "$var" | iconv -f UTF-16LE -t UTF-8 | tr -d '\0')
        echo "$esp${path//\\//}"
        return
    fi
    for file in "$esp/EFI/cachyos-eac/systemd-bootx64.efi" "$esp/EFI/systemd/systemd-bootx64.efi"; do
        if [ -f "$file" ]; then
            echo "$file"
            return
        fi
    done
}

check_boot_files() {
    local esp file loader found=0

    esp=$(bootctl --print-esp-path 2>/dev/null || echo /boot)

    # systemd-boot and rEFInd use the kernel in /boot, Limine its own copy
    for file in /boot/vmlinuz-linux-cachyos-eac \
                "$esp/$(cat /etc/machine-id)/linux-cachyos-eac/vmlinuz"; do
        [ -f "$file" ] || continue
        check_signature "$file"
        found=1
    done
    if [ "$found" -eq 0 ]; then
        fail "The EAC kernel is missing, install linux-cachyos-eac"
    fi

    loader=$(eac_boot_loader "$esp")
    if [ -z "$loader" ]; then
        fail "No systemd-boot found for the EAC boot entry"
    elif [ -f "$loader" ]; then
        check_signature "$loader"
    else
        fail "$loader is missing"
    fi
}

check_cmdline() {
    local opt last

    for opt in lockdown=integrity lsm=landlock,lockdown,yama,integrity,bpf iommu.passthrough=0 \
               intel_iommu=on module.sig_enforce=1 sysctl.kernel.kexec_load_disabled=1; do
        # The kernel uses the last one if an option is given more than once
        last=$(tr ' ' '\n' < /proc/cmdline | grep "^${opt%%=*}=" | tail -n1)
        if [ "$last" = "$opt" ]; then
            ok "$opt"
        else
            fail "$opt is missing from the kernel command line"
        fi
    done
}

check_kernel() {
    if grep -q '\[integrity\]' /sys/kernel/security/lockdown; then
        ok "Lockdown is in integrity mode"
    else
        fail "Lockdown is not in integrity mode"
    fi

    if grep -q 'bpf' /sys/kernel/security/lsm; then
        ok "BPF LSM is active"
    else
        fail "BPF LSM is not active"
    fi

    if [ "$(cat /sys/module/module/parameters/sig_enforce)" = Y ]; then
        ok "Module signatures are enforced"
    else
        fail "Module signatures are not enforced"
    fi

    if [ "$(cat /proc/sys/kernel/kexec_load_disabled)" = 1 ]; then
        ok "kexec is disabled"
    else
        fail "kexec is not disabled"
    fi
}

# The kernel lets AMD GPUs with PASID support bypass the IOMMU (identity domain)
# even with iommu.passthrough=0, so they only get a warning. Any other device
# that bypasses it fails the check.
check_iommu() {
    local group dev gpu devs
    local -a gpus=() others=()

    if ! ls /sys/kernel/iommu_groups/*/type >/dev/null 2>&1; then
        fail "IOMMU is disabled"
        return
    elif ! grep -qx -e DMA -e DMA-FQ /sys/kernel/iommu_groups/*/type; then
        fail "IOMMU is in passthrough mode"
        return
    fi

    for group in /sys/kernel/iommu_groups/*; do
        [ "$(cat "$group/type")" = identity ] || continue

        gpu='' devs=''
        for dev in "$group"/devices/*; do
            if [ "$(cat "$dev/vendor" 2>/dev/null)" = 0x1002 ] \
               && [[ "$(cat "$dev/class" 2>/dev/null)" == 0x03* ]]; then
                gpu=${dev##*/}
            fi
            devs+=" ${dev##*/}"
        done
        if [ -n "$gpu" ]; then
            gpus+=("$gpu")
        else
            others+=("${devs# }")
        fi
    done

    for devs in "${others[@]}"; do
        fail "IOMMU is bypassed by $devs"
    done
    if [ "${#others[@]}" -eq 0 ]; then
        ok "IOMMU is enabled"
    fi
    for gpu in "${gpus[@]}"; do
        warn "IOMMU is bypassed by the AMD GPU $gpu (kernel default for AMD GPUs)"
    done
}

# Certificates the firmware used to verify the boot files, recorded in PCR 7
check_pcr7() {
    local data name

    tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements 2>/dev/null \
        | awk '/EV_EFI_VARIABLE_AUTHORITY/ {f=1} f && /VariableData:/ {print $2; f=0}' \
        | tr -d '"' \
        | while read -r data; do
            # Skip the 16 byte owner GUID in front of the certificate
            echo "${data:32}" | tr a-f A-F | basenc --base16 -d 2>/dev/null | cn
        done > "$tmp/authorities"

    if grep -qx "$EAC_CA_NAME" "$tmp/authorities"; then
        ok "Boot was verified by $EAC_CA_NAME"
    else
        fail "Boot was not verified by $EAC_CA_NAME"
    fi

    # Not in a pipe, warn has to count in this shell
    while read -r name; do
        warn "Boot was also verified by $name"
    done < <(grep -v -e "^$EAC_CA_NAME$" -e '^Microsoft' "$tmp/authorities")
}

# Print the result and exit with 1 if a check failed. The checks only cover this
# system, whether a game starts is still up to Easy Anti-Cheat.
summary() {
    echo
    if [ "$failed" -eq 1 ]; then
        echo "1 check failed, see $WIKI_URL"
    elif [ "$failed" -gt 1 ]; then
        echo "$failed checks failed, see $WIKI_URL"
    elif [ "$1" = boot-files ]; then
        echo "The boot files are ready. Boot the EAC entry to check the running kernel."
    elif [ "$warned" -gt 0 ]; then
        echo "No check failed, but Easy Anti-Cheat may not accept the warnings above."
    else
        echo "All checks passed. This boot meets the requirements of Easy Anti-Cheat Linux v2."
    fi
    exit $((failed > 0))
}

check_secure_boot
check_tpm
check_db
check_boot_files

if [[ "$(uname -r)" != *-cachyos-eac ]]; then
    summary boot-files
fi

check_cmdline
check_kernel
check_iommu
check_pcr7

summary
