#!/bin/bash

#
# fips-tool: Get PrivX and host FIPS status in RPM environment.
#
# Author: Cuong Nguyen <cuong.nguyen@ssh.com>
# Copyright (c) 2025 SSH Communications Security Inc.
# See the LICENSE file for the details on licensing.
#
#

function print_help() {
    cat <<EOF
Usage: fips-tool [Command] [FLAGS]
Commands:
    help            Print help text

    check-support   Print (true/false) whether FIPS feature is available
                    for this RPM environment, prior to configuring.

    locate-cnf      Show OpenSSL config file real path for a reference FIPS status
        <no flag>           Use host FIPS status as the reference
        --fipsmode [VAL]    Use VAL as the reference (enabled/disabled/error)

    status          Show FIPS status (enabled/disabled/error: string)
        <no flag>   Show both host and PrivX FIPS status
        --host      Show host FIPS status (enabled/disabled/error)
        --privx     Show PrivX FIPS status (enabled/disabled/error)
        --extender  Show PrivX Extender FIPS status (enabled/disabled/error)

    verify          Verify PrivX and host FIPS status against a reference value.
                    Returns 0 if both statuses equal the reference value.
        <no flag>   Use current host FIPS status (enabled/disabled) as reference value
        --enabled   Use reference value "enabled"
        --disabled  Use reference value "disabled"
        --extender  Verify PrivX Extender FIPS and host FIPS status

Status value "error" indicates broken or inaccessible configuration.
PrivX services and tools require _both_ statuses to not be "error".
EOF
}

#
# Utilities
#

# Print error message on stderr
function stderr() { >&2 echo "Error: $1";}

# Print message on stdout as final command output
function cmdout() { echo "$1"; }

#
# Command: status
# 

# Print enabled/disabled/error as PrivX FIPS status
# Function always return success.
function cmd_status() {
    # print_mode: host = 0x01, privx = 0x02, else print statuses for both
    local print_mode=0

    for flag in "$@";
    do
        case "$flag" in
            --host)     (( print_mode|=1 ));;
            --privx)    (( print_mode|=2 ));;
            --extender) (( print_mode|=4 ));;
            *)          stderr "unknown flag '$flag' (supported: host, privx, extender)"; exit 1;
        esac
    done

    if [ "$print_mode" -eq 0 ]; then print_mode=3; fi

    case "$print_mode" in
        1) print_status_host; exit 0;;
        2) print_status_privx; exit 0;;
        3)
            host_err=$(mktemp)
            privx_err=$(mktemp)
            host_out=$(print_status_host 2>"$host_err")
            privx_out=$(print_status_privx 2>"$privx_err")
            printf "%-24s%-12s%s\n" "Host FIPS status:" "$host_out" "$(<"$host_err")"
            printf "%-24s%-12s%s\n" "PrivX FIPS status:" "$privx_out" "$(<"$privx_err")"
            rm -f "$host_err" "$privx_err" || true
            exit 0
        ;;
        4) print_status_extender; exit 0;;
        5)
            host_err=$(mktemp)
            extender_err=$(mktemp)
            host_out=$(print_status_host 2>"$host_err")
            extender_out=$(print_status_extender 2>"$extender_err")
            printf "%-24s%-12s%s\n" "Host FIPS status:" "$host_out" "$(<"$host_err")"
            printf "%-24s%-12s%s\n" "PrivX Extender FIPS status:" "$extender_out" "$(<"$extender_err")"
            rm -f "$host_err" "$extender_err" || true
            exit 0
        ;;
        *) stderr "unsupported combination of flags host, privx, extender"; exit 1;;
    esac
}

# Print OS FIPS mode status (enabled/disabled/error)
# - enabled:    Host kernel FIPS flag exists and is 1
# - disabled:   Host kernel FIPS flag exists and is 0
# - error:      Host kernel FIPS flag is inaccessible or unregcognized
# Note: this is not a system-wide verification of host FIPS status
# The check only concerns kernel flag, which matters for PrivX.
function print_status_host() {
    # Case: kernel flag exists and is readable
    if [[ -r /proc/sys/crypto/fips_enabled ]]; then
        value=$(cat /proc/sys/crypto/fips_enabled)
        case "$value" in
            1) echo "enabled"; return 0;;
            0) echo "disabled"; return 0;;
            *) 
                echo "error"
                stderr "unexpected kernel fips_enabled flag: $value"
                return 1;;
        esac
    fi
    # Case: kernel flag not accessible. There is no fallback.
    cmdout "error"
    stderr "kernel FIPS enabled flag not accessible"
}

# Print PrivX FIPS mode status (enabled/disabled/error)
# - enabled:  only PrivX FIPS and base providers are loaded
# - disabled: PrivX FIPS provider not loaded
# - error:  files missing or configurations are broken
function print_status_privx() {
    if [[ ! -x /opt/privx/bin/auth ]]; then
        cmdout "error"
        stderr "PrivX is not installed"
        return 1
    fi

    if ! print_status_redemption; then return $?; fi
    print_status_openssl
}

function print_status_extender() {
    if [[ ! -x /opt/privx/bin/privx-extender ]]; then
        cmdout "error"
        stderr "PrivX Extender FIPS is not installed"
        return 1
    fi

    print_status_openssl
}

function print_status_openssl() {
    # PrivX OpenSSL must exist
    if ! command -v /opt/privx/openssl/bin/openssl &> /dev/null; then
        cmdout "error"
        stderr "PrivX OpenSSL binary missing"
        return 1
    fi
    # PrivX OpenSSL cnf file must exist
    if [[ ! -f /opt/privx/openssl/ssl/openssl.cnf ]]; then
        cmdout "error"
        stderr "PrivX OpenSSL config file missing"
        return 1
    fi
    # PrivX OpenSSL libraries must exist
    local libdir="/opt/privx/openssl/lib64"
    for file in "libcrypto.so.3" "libssl.so.3"; do
        if [[ ! -f "$libdir/$file" ]]; then
            cmdout "error"
            stderr "PrivX OpenSSL $libdir/$file missing"
            return 1
        fi
    done

    # Get all loaded providers using current PrivX OpenSSL cnf file
    providers=$(/opt/privx/openssl/bin/openssl list -providers)

    # PrivX FIPS module status
    local fips_loaded=false
    echo "$providers" | grep "KeyPair FIPS Provider" &> /dev/null && fips_loaded=true
    fips_count=$(echo "$providers" | grep -c "FIPS Provider")

    ### Case: PrivX FIPS module not loaded
    if [[ "$fips_loaded" == false ]]; then
        if [[ ! "$fips_count" -eq 0 ]]; then
            cmdout "error"
            stderr "PrivX FIPS provider not loaded but unknown FIPS provider(s) loaded"
            return 1
        fi

        # Check conf files in the "disabled" state
        verify_conf_disabled
        return $?;
    fi

    ### Case: PrivX FIPS module loaded

    # PrivX FIPS provider should be the only one loaded
    if [[ ! "$fips_count" -eq 1 ]]; then
        cmdout "error"
        stderr "Multiple ($fips_count) FIPS providers loaded"
        return 1
    fi

    # OpenSSL base module must also be loaded if PrivX FIPS provider is present
    local base_loaded=false
    echo "$providers" | grep "Base Provider" &> /dev/null && base_loaded=true
    if [[ $base_loaded == false ]]; then
        cmdout "error"
        stderr "PrivX FIPS provider loaded but base provider not loaded"
        return 1
    fi

    # OpenSSL legacy/default providers: must not be present with FIPS provider
    local legacy_loaded=false
    local default_loaded=false
    echo "$providers" | grep "Default Provider" &> /dev/null && default_loaded=true
    echo "$providers" | grep "Legacy Provider" &> /dev/null && legacy_loaded=true
    if [[ $legacy_loaded == true || $default_loaded == true ]]; then
        cmdout "error"
        stderr "PrivX FIPS provider loaded but legacy/default providers are also loaded"
        return 1
    fi

    # Check conf file content against "enabled" state
    verify_conf_enabled
    return $?
}

function print_status_redemption() {
    # Redemption ini
    ini=/opt/privx/etc/rdpmitm/rdpproxy.ini
    if [[ ! -f "$ini" ]]; then
        cmdout "error"
        stderr "rdpproxy.ini not found"
        return 1
    fi
    return 0
}

# Verify the configuration files for PrivX FIPS "disabled" state
# Print disabled/error depending on the verification result.
function verify_conf_disabled() {
    cnf=/opt/privx/openssl/ssl/openssl.cnf
    # Checking for EVP default properties assignment. fips=yes must not be set
    count=$(grep -c "^[[:space:]]*default_properties[[:space:]]*=.*fips=yes.*" "$cnf")
    if [[ ! "$count" -eq 0 ]]; then
        cmdout "error"
        stderr "PrivX FIPS provider not loaded but fips=yes set in (EVP) default_properties"
        return 1
    fi

    # Checking for EVP default properties assignment. fips=no must not be set
    # fips=no prevents certain non-crypto OpenSSL methods. Leave blank or use -fips
    count=$(grep -c "^[[:space:]]*default_properties[[:space:]]*=.*fips=no.*" "$cnf")
    if [[ ! "$count" -eq 0 ]]; then
        cmdout "error"
        stderr "Using fips=no in (EVP) default_properties will cause non-crypto methods to fail"
        return 1
    fi

    # End of verification
    cmdout "disabled"
    return 0
}

# Verify configuration files for PrivX FIPS "enabled" state
# Print enabled/error depending on the verification result.
function verify_conf_enabled() {
    cnf=/opt/privx/openssl/ssl/openssl.cnf

    # All EVP default_properties assignment must have fips=yes
    # Assume that default_properties entries are for EVP.
    assignments=$(grep "^[[:space:]]*default_properties[[:space:]]*=" "$cnf")
    assignment_count=$(echo "$assignments" | wc -l)
    fips_count=$(echo "$assignments" | grep -c "fips=yes")

    if [[ ! "$assignment_count" -eq "$fips_count" ]]; then
        cmdout "error"
        stderr "PrivX FIPS provider loaded but fips=yes not set in default_properties assignment(s)"
        return 1
    fi

    # End of verification
    cmdout "enabled"
}

#
# Command: verify
#

# Compare PrivX and host FIPS status against a reference value.
# Return 0 if both equals the referecene value. Return 1 otherwise.
# A flag pins the reference value. If no flag is provided, get & use
# host FIPS status. Verification fails with exit code 1. Other
# failures use exit code 2.
function cmd_verify(){
    # modes:
    #   0x00 use host FIPS status
    #   0x01 enabled
    #   0x02 disabled
    local verify_mode=0
    # components:
    #   0x00 host and PrivX FIPS status
    #   0x01 host and PrivX Extender FIPS status
    local verify_component=0
    for flag in "$@";
    do
        case "$flag" in
            --enabled)  (( verify_mode|=1 ));;
            --disabled) (( verify_mode|=2 ));;
            --extender) (( verify_component|=1));;
            *)
                stderr "unknown flag $flag (supported: enabled, disabled, extender)"
                exit 2
            ;;
        esac
    done

    local status_ref=""
    case $verify_mode in
        0) ;; # using host status as reference
        1) status_ref="enabled";;
        2) status_ref="disabled";;
        3) stderr "Invalid combination of flags"; exit 2;;
        *) stderr "Invalid verify mode $verify_mode"; exit 2;;
    esac

    local status_host
    local status_component

    status_host=$(cmd_status --host)

    if [[ $verify_component -eq 0 ]]; then
        status_component=$(cmd_status --privx)
    else
        status_component=$(cmd_status --extender)
    fi

    if [[ -z "$status_ref" ]]; then
        status_ref="$status_host"
    fi

    # If using host status as ref, and value is error
    case "$status_ref" in
        enabled|disabled)   ;;
        error)  stderr "Unable to get host FIPS status"; exit 2;;
        *)      stderr "Invalid reference status: ($status_ref)"; exit 2;;
    esac

    if [[ "$status_component" == "$status_ref" && "$status_host" == "$status_ref" ]]; then
        exit 0
    else
        stderr "Conflict between PrivX and host OS FIPS configuration"
        stderr "Host FIPS: $status_host, PrivX FIPS: $status_component"
        exit 1
    fi
}


#
# Command check-support
#

# Print true/false: whether this RPM environment supports FIPS feature.
# It checks that PrivX FIPS crypto module is present (shipped with RPM).
# The check should only be done __before__ configuring PrivX for FIPS mode.
# Requirements:
#   - /opt/privx/openssl/lib64/ossl-modules/fips.so
#   - /opt/privx/openssl/lib64/ossl-modules/fips.so.mac
function cmd_check_support() {
    local module=/opt/privx/openssl/lib64/ossl-modules/fips.so
    local module_mac=/opt/privx/openssl/lib64/ossl-modules/fips.so.mac

    for file in $module $module_mac; do
        if [[ ! -f "$file" ]]; then
            cmdout "false"
            stderr "FIPS component not available: $file"
            return 0
        fi
    done
    cmdout "true"
}

#
# Command locate-cnf
#

# Print OpenSSL configuration file real path for a reference FIPS status.
# Use --fipsmode [enabled/disabled/error] to specify the reference status.
# Without the flag, the command uses print_status_host() output as ref status.
function cmd_locate_cnf() {
    local fipsmode
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --fipsmode  ) fipsmode="$2"; shift; shift;;
            --fipsmode=*) fipsmode="${1#*=}"; shift;;
            *           ) stderr "Unknown flag: $1 (valid: fipsmode)"; exit 1;;
        esac
    done

    if [[ -z "$fipsmode" ]]; then
        fipsmode=$(print_status_host 2>/dev/null)
    fi

    local cnf="/opt/privx/openssl/ssl/openssl-regular.cnf"
    case "$fipsmode" in
        enabled ) cnf="/opt/privx/openssl/ssl/openssl-fips.cnf";;
        disabled) ;; # use regular cnf
        error   ) ;; # use regular cnf
        *       ) stderr "Unknown fipsmode: $fipsmode (valid: enabled/disabled/error)"; exit 1;;
    esac

    if [[ -f "$cnf" ]]; then
        cmdout "$cnf"
    fi
}


#
# Main program
#

case "$1" in
    -h|--help|help) print_help;;
    status)         shift; cmd_status "$@";;
    verify)         shift; cmd_verify "$@";;
    locate-cnf)     shift; cmd_locate_cnf "$@";;
    check-support)  cmd_check_support;;
    "")             cmd_status;; 
    *)              stderr "unknown command $1"; >&2 print_help; exit 1;;
esac