#!/bin/bash
# Author: Christophe Casalegno / Brain 0verride
# Contact: christophe.casalegno@scalarx.com
# sxtlscheck / ScalarX TLS Checker
# Version 0.7
#
# Copyright (c) 2020 Christophe Casalegno
#
# This program is free software: you can redistribute it and/or modify
#
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>
#
# The license is available on this server here:
# https://www.christophe-casalegno.com/licences/gpl-3.0.txt

NC='\033[0m'
RED='\033[38;5;160m'  
GREEN='\033[38;1;32m'  
YELLOW='\033[38;5;226m'
BLUE='\033[38;5;39m'  

PORT_DEFAULT=443 ; TIMEOUT_DEFAULT=8
FULL_CIPHERS=0 ; NO_COLOR=0
QUIET=0 ; DATE_ONLY=0

OPENSSL_BIN="${OPENSSL_BIN:-openssl}"

function usage()
{
  echo "Usage: $0 [options] <host|url>"
  echo ""
  echo "Options:"
  echo "  -p, --port <port>        Port (default: $PORT_DEFAULT)"
  echo "  -s, --sni <name>         SNI name (default: host)"
  echo "  -t, --timeout <seconds>  Connection timeout (default: $TIMEOUT_DEFAULT)"
  echo "  --full-ciphers           Enumerate all TLS1.2 ciphers (slow)"
  echo "  -d, --date               Only check hostname + certificate validity"
  echo "  --no-color               Disable ANSI colors"
  echo "  -q, --quiet              Minimal output"
  echo "  -h, --help               Show this help"
  echo ""
  echo "Notes: IPv6 must be provided as [addr] or [addr]:port"
}

function color_init()
{
  if [[ $NO_COLOR -eq 1 || ! -t 1 ]]
  then
    RED=''; NC=''; GREEN=''; YELLOW=''; BLUE=''
  fi
}

function log_ok()
{
  echo -e "${GREEN} [ OK ]${NC} $*"
}
function log_warn()
{
  echo -e "${YELLOW} [WARN]${NC} $*"
}
function log_err()
{
  echo -e "${RED} [ERROR]${NC} $*"
}
function log_info()
{
  echo -e "${BLUE} [INFO]${NC} $*"
}

function is_ipv4()
{
  local v="$1"
  local o
  if [[ ! "$v" =~ ^[0-9]+(\.[0-9]+){3}$ ]]
  then
    return 1
  fi
  IFS='.' read -r o1 o2 o3 o4 <<< "$v"
  for o in "$o1" "$o2" "$o3" "$o4"
  do
    if [[ -z "$o" || ! "$o" =~ ^[0-9]+$ ]]
    then
      return 1
    fi
    if (( o < 0 || o > 255 ))
    then
      return 1
    fi
  done
  return 0
}

function count_hextets()
{
  local part="$1"
  local count=0
  local i h
  if [[ -z "$part" ]]
  then
    echo 0
    return 0
  fi
  IFS=':' read -ra arr <<< "$part"
  for i in "${!arr[@]}"
  do
    h="${arr[$i]}"
    if [[ -z "$h" ]]
    then
      return 1
    fi
    if [[ "$h" == *"."* ]]
    then
      if (( i != ${#arr[@]} - 1 ))
      then
        return 1
      fi
      if is_ipv4 "$h"
      then
        count=$((count + 2))
      else
        return 1
      fi
    else
      if [[ ! "$h" =~ ^[0-9a-fA-F]{1,4}$ ]]
      then
        return 1
      fi
      count=$((count + 1))
    fi
  done
  echo "$count"
  return 0
}

function is_ipv6()
{
  local v="$1"
  local head tail count_head count_tail
  if [[ "$v" != *:* ]]
  then
    return 1
  fi
  if [[ "$v" =~ [^0-9a-fA-F:\.] ]]
  then
    return 1
  fi

  local tmp="$v"
  local dbl=0
  while [[ "$tmp" == *"::"* ]]
  do
    dbl=$((dbl + 1))
    tmp="${tmp#*::}"
  done
  if (( dbl > 1 ))
  then
    return 1
  fi

  if [[ "$v" == *"::"* ]]
  then
    head="${v%%::*}"
    tail="${v#*::}"
    count_head=$(count_hextets "$head") || return 1
    count_tail=$(count_hextets "$tail") || return 1
    if (( count_head + count_tail >= 8 ))
    then
      return 1
    fi
    return 0
  fi

  count_head=$(count_hextets "$v") || return 1
  if (( count_head != 8 ))
  then
    return 1
  fi
  return 0
}

function is_ip()
{
  local v="$1"
  if is_ipv4 "$v"
  then
    return 0
  fi
  if is_ipv6 "$v"
  then
    return 0
  fi
  return 1
}

function parse_target()
{
  local input="$1"
  local hp host port
  local colon_count

  if [[ "$input" =~ ^https?:// ]]
  then
    input="${input#*://}"
    input="${input%%/*}"
  fi

  if [[ "$input" =~ ^\[.*\] ]]
  then
    host="${input#\[}"
    host="${host%%\]*}"
    if [[ "$input" =~ \]:(.+)$ ]]
    then
      port="${input##*:}"
    fi
  else
    if [[ "$input" == *":"* ]]
    then
      tmp="${input//[^:]/}"
      colon_count=${#tmp}
      if [[ "$colon_count" -gt 1 ]]
      then
        echo "__IPV6_BRACKET_ERROR__"
        return
      fi
      host="${input%%:*}"
      port="${input##*:}"
    else
      host="$input"
    fi
  fi

  echo "$host" "${port:-}" 
}

function sclient_run()
{
  local extra=("$@")
  if command -v timeout >/dev/null 2>&1
  then
    timeout "$TIMEOUT" "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} "${extra[@]}" </dev/null 2>&1
  else
    "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} "${extra[@]}" </dev/null 2>&1
  fi
}

function sclient_run_nosni()
{
  local extra=("$@")
  if command -v timeout >/dev/null 2>&1
  then
    timeout "$TIMEOUT" "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" "${extra[@]}" </dev/null 2>&1
  else
    "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" "${extra[@]}" </dev/null 2>&1
  fi
}

function http_head()
{
  local hosthdr="$1"
  local req
  req="HEAD / HTTP/1.1\r\nHost: ${hosthdr}\r\nUser-Agent: sxtlscheck\r\nConnection: close\r\n\r\n"
  if command -v timeout >/dev/null 2>&1
  then
    printf "%b" "$req" | timeout "$TIMEOUT" "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} -quiet 2>/dev/null
  else
    printf "%b" "$req" | "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} -quiet 2>/dev/null
  fi
}

function http_head_sessout()
{
  local sessfile="$1"
  local hosthdr="$2"
  local req
  req="HEAD / HTTP/1.1\r\nHost: ${hosthdr}\r\nUser-Agent: sxtlscheck\r\nConnection: close\r\n\r\n"
  if command -v timeout >/dev/null 2>&1
  then
    printf "%b" "$req" | timeout "$TIMEOUT" "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} -quiet -sess_out "$sessfile" 2>/dev/null
  else
    printf "%b" "$req" | "$OPENSSL_BIN" s_client -connect "${HOST}:${PORT}" ${SNI_OPT[@]} -quiet -sess_out "$sessfile" 2>/dev/null
  fi
}

function handshake_ok()
{
  local out="$1"
  if echo "$out" | grep -qi "Cipher is (NONE)"
  then
    return 1
  fi
  if echo "$out" | grep -qiE "handshake failure|no peer certificate|connect:errno|connection refused|connection timed out|alert\\s*\\(.*\\)"
  then
    return 1
  fi
  if echo "$out" | grep -qiE "Protocol *:|Protocol:|SSL-Session:|Verify return code|Verification: OK|Server Temp Key|CONNECTED"
  then
    return 0
  fi
  return 1
}

function get_protocol_line()
{
  echo "$1" | awk -F':' '/^Protocol/ {gsub(/^[[:space:]]+/, "", $2); print $2; exit}'
}

function get_cipher_line()
{
  echo "$1" | awk -F':' '/^Cipher/ {gsub(/^[[:space:]]+/, "", $2); print $2; exit}'
}

function match_host()
{
  local host_lc="$1"
  local pattern="$2"
  local pat_lc
  pat_lc="$(echo "$pattern" | tr '[:upper:]' '[:lower:]')"
  if [[ -z "$pat_lc" ]]
  then
    return 1
  fi
  if [[ "$host_lc" == $pat_lc ]]
  then
    return 0
  fi
  if [[ "$pat_lc" == \*.* ]]
  then
    local rest="${pat_lc#\*}"
    if [[ "$rest" == *"*"* ]]
    then
      return 1
    fi
    local suffix="${pat_lc#*.}"
    if [[ -z "$suffix" ]]
    then
      return 1
    fi
    if [[ "$host_lc" == *".${suffix}" ]]
    then
      local host_suffix="${host_lc#*.}"
      if [[ "$host_suffix" == "$suffix" ]]
      then
        return 0
      fi
    fi
  fi
  return 1
}

function extract_san_dns()
{
  echo "$1" | tr '\n' ' ' | \
    sed -n 's/.*Subject Alternative Name:[[:space:]]*//Ip' | \
    tr ',' '\n' | \
    sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \
    grep -i '^DNS:' | cut -d: -f2-
}

function extract_san_ip()
{
  echo "$1" | tr '\n' ' ' | \
    sed -n 's/.*Subject Alternative Name:[[:space:]]*//Ip' | \
    tr ',' '\n' | \
    sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | \
    grep -i '^IP Address:' | cut -d: -f2-
}

function extract_cn()
{
  echo "$1" | sed -n 's/.*CN[[:space:]]*=[[:space:]]*\\([^,/]*\\).*/\\1/p'
}

function check_host_match()
{
  local host_lc="$1"
  local san_dns="$2"
  local san_ip="$3"
  local cn="$4"

  if is_ip "$host_lc"
  then
    for ip in $san_ip
    do
      ip_lc=$(echo "$ip" | tr '[:upper:]' '[:lower:]')
      if [[ "$host_lc" == "$ip_lc" ]]
      then
        return 0
      fi
    done
  else
    for name in $san_dns
    do
      if match_host "$host_lc" "$name"
      then
        return 0
      fi
    done
    if [[ -n "$cn" ]]
    then
      if match_host "$host_lc" "$cn"
      then
        return 0
      fi
    fi
  fi
  return 1
}

function check_date_only()
{
  SHOWCERTS_OUT=$(sclient_run -showcerts 2>/dev/null)
  CERT_PEM=$(echo "$SHOWCERTS_OUT" | awk 'BEGIN{f=0} /-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{if(f){exit}}')
  if [[ -z "$CERT_PEM" ]]
  then
    log_warn "No certificate could be retrieved"
    return
  fi

  CERT_SUBJECT=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject | sed 's/^subject=//')
  CERT_SUBJECT_RFC=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject -nameopt RFC2253 | sed 's/^subject=//')
  CERT_TEXT=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -text)
  CERT_START=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -startdate | cut -d= -f2)
  CERT_END=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -enddate | cut -d= -f2)
  CERT_SAN=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -ext subjectAltName 2>/dev/null)
  CERT_CN=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject -nameopt RFC2253 | sed 's/^subject=//' | tr ',' '\n' | grep -m1 '^CN=' | cut -d= -f2-)
  if [[ -z "$CERT_CN" ]]
  then
    CERT_CN=$(extract_cn "$CERT_SUBJECT_RFC")
  fi
  if [[ -z "$CERT_CN" ]]
  then
    CERT_CN=$(extract_cn "$CERT_SUBJECT")
  fi

  SAN_DNS=$(extract_san_dns "$CERT_SAN")
  SAN_IP=$(extract_san_ip "$CERT_SAN")

  host_lc=$(echo "$HOST" | tr '[:upper:]' '[:lower:]')
  if check_host_match "$host_lc" "$SAN_DNS" "$SAN_IP" "$CERT_CN"
  then
    log_ok "Hostname matches certificate"
  else
    log_warn "Hostname does NOT match certificate"
  fi

  if [[ -n "$CERT_END" ]]
  then
    end_epoch=$(date -d "$CERT_END" +%s 2>/dev/null)
    now_epoch=$(date +%s)
    if [[ -n "$end_epoch" ]]
    then
      days_left=$(( (end_epoch - now_epoch) / 86400 ))
      if [[ $days_left -lt 0 ]]
      then
        log_err "Certificate expired ($days_left days)"
      elif [[ $days_left -lt 30 ]]
      then
        log_warn "Certificate expires in $days_left days"
      else
        log_ok "Certificate valid for $days_left days"
      fi
    else
      log_warn "Certificate validity: unknown"
    fi
  else
    log_warn "Certificate validity: unknown"
  fi
}

function cipher_is_weak()
{
  local c="$1"
  if echo "$c" | grep -qiE 'NULL|aNULL|eNULL|EXP|EXPORT|RC4|DES|3DES|MD5|SHA1|DSS'
  then
    return 0
  fi
  return 1
}

function print_banner()
{
  if [[ $QUIET -eq 1 || $DATE_ONLY -eq 1 ]]
  then
    return
  fi
  echo -e "${BLUE}sxtlscheck${NC} / ScalarX TLS Checker"
}

function cleanup_tmp()
{
  if [[ -n "$TMPDIR" && -d "$TMPDIR" ]]
  then
    rm -rf "$TMPDIR"
  fi
}

if ! command -v "$OPENSSL_BIN" >/dev/null 2>&1
then
  echo "ERROR: openssl not found (set OPENSSL_BIN or install openssl)"
  exit 1
fi

PORT="$PORT_DEFAULT"
TIMEOUT="$TIMEOUT_DEFAULT"
SNI=""
TARGET=""

while [[ $# -gt 0 ]]
do
  case "$1" in
    -p|--port)
      PORT="$2"; shift 2; ;;
    -s|--sni)
      SNI="$2"; shift 2; ;;
    -t|--timeout)
      TIMEOUT="$2"; shift 2; ;;
    -d|--date)
      DATE_ONLY=1; shift; ;;
    --full-ciphers)
      FULL_CIPHERS=1; shift; ;;
    --no-color)
      NO_COLOR=1; shift; ;;
    -q|--quiet)
      QUIET=1; shift; ;;
    -h|--help)
      usage; exit 0; ;;
    *)
      if [[ -z "$TARGET" ]]
      then
        TARGET="$1"; shift
      else
        echo "ERROR: unexpected argument: $1"; exit 1
      fi
      ;;
  esac
done

if [[ -z "$TARGET" ]]
then
  usage
  exit 1
fi

read HOST PORT_FROM_TARGET < <(parse_target "$TARGET")
if [[ "$HOST" = "__IPV6_BRACKET_ERROR__" ]]
then
  echo "ERROR: IPv6 must be provided as [addr] or [addr]:port"
  exit 1
fi
if [[ -z "$HOST" ]]
then
  echo "ERROR: invalid target"
  exit 1
fi
if [[ -n "$PORT_FROM_TARGET" ]]
then
  PORT="$PORT_FROM_TARGET"
fi

if [[ -z "$SNI" ]]
then
  SNI="$HOST"
fi

SNI_OPT=()
if [[ -n "$SNI" ]]
then
  SNI_OPT=(-servername "$SNI")
fi

color_init
print_banner

TMPDIR="/tmp/sxtlscheck-$$"
mkdir -p "$TMPDIR"
trap 'cleanup_tmp' EXIT INT TERM

if [[ "$DATE_ONLY" -eq 0 ]]
then
  log_info "Target: $HOST:$PORT"
  log_info "SNI: $SNI"
  log_info "OpenSSL: $($OPENSSL_BIN version)"
fi

# Basic handshake
BASE_OUT=$(sclient_run -brief)
if handshake_ok "$BASE_OUT"
then
  if [[ "$DATE_ONLY" -eq 0 ]]
  then
    proto=$(get_protocol_line "$BASE_OUT")
    cipher=$(get_cipher_line "$BASE_OUT")
    log_ok "Handshake OK (Protocol: ${proto:-unknown}, Cipher: ${cipher:-unknown})"
  fi
else
  log_err "Handshake failed"
  echo "$BASE_OUT" | tail -n 5
  exit 2
fi

if [[ "$DATE_ONLY" -eq 1 ]]
then
  check_date_only
  exit 0
fi

SCLIENT_HELP=$($OPENSSL_BIN s_client -help 2>&1)
function proto_check()
{
  local flag="$1"
  if echo "$SCLIENT_HELP" | grep -q -- "$flag"
  then
    local out
    out=$(sclient_run "$flag" -brief)
    if handshake_ok "$out"
    then
      echo "yes"
    else
      echo "no"
    fi
  else
    echo "local-no"
  fi
}

function check_protocols()
{
  # Protocol support
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "Protocols"
  fi
  TLS10_STATE=$(proto_check "-tls1")
  TLS11_STATE=$(proto_check "-tls1_1")
  TLS12_STATE=$(proto_check "-tls1_2")
  TLS13_STATE=$(proto_check "-tls1_3")

  if [[ "$TLS10_STATE" = "yes" ]]
  then
    log_warn "TLS 1.0 supported (legacy enabled)"
  elif [[ "$TLS10_STATE" = "no" ]]
  then
    log_ok "TLS 1.0 not supported (good)"
  else
    log_info "TLS 1.0 not supported by local openssl"
  fi

  if [[ "$TLS11_STATE" = "yes" ]]
  then
    log_warn "TLS 1.1 supported (legacy enabled)"
  elif [[ "$TLS11_STATE" = "no" ]]
  then
    log_ok "TLS 1.1 not supported (good)"
  else
    log_info "TLS 1.1 not supported by local openssl"
  fi

  if [[ "$TLS12_STATE" = "yes" ]]
  then
    log_ok "TLS 1.2 supported"
  elif [[ "$TLS12_STATE" = "no" ]]
  then
    if [[ "$TLS13_STATE" = "yes" ]]
    then
      log_info "TLS 1.2 not supported (TLS 1.3 only)"
    else
      log_warn "TLS 1.2 not supported"
    fi
  else
    log_info "TLS 1.2 not supported by local openssl"
  fi

  if [[ "$TLS13_STATE" = "yes" ]]
  then
    log_ok "TLS 1.3 supported"
  elif [[ "$TLS13_STATE" = "no" ]]
  then
    log_warn "TLS 1.3 not supported"
  else
    log_warn "TLS 1.3 not supported by local openssl"
  fi
}

check_protocols

function check_legacy_downgrade()
{
  # Legacy / Downgrade tests
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "Legacy / Downgrade"
  fi

  SSL3_STATE=$(proto_check "-ssl3")
  if [[ "$SSL3_STATE" = "yes" ]]
  then
    log_warn "SSLv3 supported (legacy enabled)"
  elif [[ "$SSL3_STATE" = "no" ]]
  then
    log_ok "SSLv3 not supported (good)"
  else
    log_info "SSLv3 not supported by local openssl"
  fi

  SSL2_STATE=$(proto_check "-ssl2")
  if [[ "$SSL2_STATE" = "yes" ]]
  then
    log_warn "SSLv2 supported (legacy enabled)"
  elif [[ "$SSL2_STATE" = "no" ]]
  then
    log_ok "SSLv2 not supported (good)"
  else
    log_info "SSLv2 not supported by local openssl"
  fi

  # TLS_FALLBACK_SCSV
  if echo "$SCLIENT_HELP" | grep -q -- "-fallback_scsv"
  then
    if [[ "$TLS13_STATE" = "yes" && "$TLS12_STATE" = "yes" ]]
    then
      F_OUT=$(sclient_run -tls1_2 -fallback_scsv -brief)
      if echo "$F_OUT" | grep -qi "inappropriate fallback"
      then
        log_ok "Fallback SCSV: protected"
      elif handshake_ok "$F_OUT"
      then
        log_warn "Fallback SCSV: not enforced"
      else
        log_info "Fallback SCSV: test inconclusive"
      fi
    else
      log_info "Fallback SCSV: skipped"
    fi
  else
    log_info "Fallback SCSV: not supported by local openssl"
  fi

  # BEAST indicator (TLS 1.0 + CBC)
  if [[ "$TLS10_STATE" = "yes" ]]
  then
    B_OUT=$(sclient_run -tls1 -cipher "AES128-SHA" -brief)
    if handshake_ok "$B_OUT"
    then
      log_warn "BEAST: TLS 1.0 + CBC supported"
    else
      log_ok "BEAST: TLS 1.0 CBC not supported"
    fi
  else
    log_info "BEAST: TLS 1.0 not supported"
  fi
}

check_legacy_downgrade

function check_certificate()
{
  # Certificate details
  SHOWCERTS_OUT=$(sclient_run -showcerts 2>/dev/null)
  ALL_CERTS_PEM=$(echo "$SHOWCERTS_OUT" | awk 'BEGIN{f=0} /-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}')
  CERT_PEM=$(echo "$SHOWCERTS_OUT" | awk 'BEGIN{f=0} /-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{if(f){exit}}')
  MUST_STAPLE=0
  if [[ -n "$CERT_PEM" ]]
  then
    CERT_SUBJECT=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject | sed 's/^subject=//')
    CERT_SUBJECT_RFC=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject -nameopt RFC2253 | sed 's/^subject=//')
    CERT_TEXT=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -text)
    CERT_ISSUER=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -issuer | sed 's/^issuer=//')
    CERT_START=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -startdate | cut -d= -f2)
    CERT_END=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -enddate | cut -d= -f2)
    CERT_SIGALG=$(echo "$CERT_TEXT" | grep -m1 "Signature Algorithm" | sed 's/^[[:space:]]*Signature Algorithm:[[:space:]]*//')
    CERT_PUBKEY=$(echo "$CERT_TEXT" | grep -m1 "Public-Key" | sed 's/^[[:space:]]*//')
    CERT_PUBALG=$(echo "$CERT_TEXT" | grep -m1 "Public Key Algorithm" | sed 's/^[[:space:]]*Public Key Algorithm:[[:space:]]*//')
    CERT_PUBBITS=$(echo "$CERT_PUBKEY" | grep -oE '[0-9]+' | head -n1)
    CERT_SAN=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -ext subjectAltName 2>/dev/null)
    CERT_CN=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -subject -nameopt RFC2253 | sed 's/^subject=//' | tr ',' '\n' | grep -m1 '^CN=' | cut -d= -f2-)
    if [[ -z "$CERT_CN" ]]
    then
      CERT_CN=$(extract_cn "$CERT_SUBJECT_RFC")
    fi
    if [[ -z "$CERT_CN" ]]
    then
      CERT_CN=$(extract_cn "$CERT_SUBJECT")
    fi
    if [[ -z "$CERT_CN" ]]
    then
      CERT_SUBJECT_LINE=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -text | grep -m1 "Subject:" | sed 's/^[[:space:]]*Subject:[[:space:]]*//')
      CERT_CN=$(extract_cn "$CERT_SUBJECT_LINE")
    fi

    if [[ $QUIET -eq 0 ]]
    then
      echo ""
      echo "Certificate"
    fi
    log_info "Subject: $CERT_SUBJECT"
    log_info "Issuer:  $CERT_ISSUER"
    log_info "CN:      ${CERT_CN:-n/a}"
    log_info "Valid:   $CERT_START  ->  $CERT_END"
    if echo "$CERT_SIGALG" | grep -qiE "md5|sha1"
    then
      log_warn "Weak signature algorithm: $CERT_SIGALG"
    else
      log_ok "Signature algorithm: $CERT_SIGALG"
    fi

    if echo "$CERT_PUBALG" | grep -qi "rsa"
    then
      if [[ -n "$CERT_PUBBITS" && "$CERT_PUBBITS" -lt 2048 ]]
      then
        log_warn "RSA key too small (${CERT_PUBBITS} bit)"
      else
        log_ok "RSA key: ${CERT_PUBBITS:-unknown} bit"
      fi
    elif echo "$CERT_PUBALG" | grep -qi "ec"
    then
      if [[ -n "$CERT_PUBBITS" && "$CERT_PUBBITS" -lt 256 ]]
      then
        log_warn "EC key too small (${CERT_PUBBITS} bit)"
      else
        log_ok "EC key: ${CERT_PUBBITS:-unknown} bit"
      fi
    else
      log_info "$CERT_PUBKEY"
    fi

    # Extensions checks
    if echo "$CERT_TEXT" | grep -qi "Basic Constraints"
    then
      if echo "$CERT_TEXT" | grep -qi "CA:FALSE"
      then
        log_ok "Basic Constraints: CA:FALSE"
      else
        log_warn "Basic Constraints: CA:TRUE or missing CA:FALSE"
      fi
    else
      log_warn "Basic Constraints missing"
    fi

    if echo "$CERT_TEXT" | grep -qi "X509v3 Key Usage"
    then
      if echo "$CERT_TEXT" | grep -qiE "Digital Signature|Key Encipherment|Key Agreement"
      then
        log_ok "Key Usage: ok"
      else
        log_warn "Key Usage: missing expected flags"
      fi
    else
      log_warn "Key Usage missing"
    fi

    if echo "$CERT_TEXT" | grep -qi "X509v3 Extended Key Usage"
    then
      if echo "$CERT_TEXT" | grep -qiE "TLS Web Server Authentication|serverAuth"
      then
        log_ok "Extended Key Usage: serverAuth"
      else
        log_warn "Extended Key Usage: serverAuth missing"
      fi
    else
      log_warn "Extended Key Usage missing"
    fi

    MUST_STAPLE=0
    if echo "$CERT_TEXT" | grep -qi "TLS Feature"
    then
      if echo "$CERT_TEXT" | awk '/TLS Feature/{flag=1} flag{print} /X509v3/ && NR>1 {flag=0}' | grep -qi "status_request"
      then
        MUST_STAPLE=1
        log_info "TLS Feature: status_request (must-staple)"
      fi
    fi

    if echo "$CERT_TEXT" | grep -qi "Signed Certificate Timestamp"
    then
      log_info "SCT: present"
    else
      log_info "SCT: not present"
    fi

    # Days left
    if [[ -n "$CERT_END" ]]
    then
      end_epoch=$(date -d "$CERT_END" +%s 2>/dev/null)
      start_epoch=$(date -d "$CERT_START" +%s 2>/dev/null)
      now_epoch=$(date +%s)
      if [[ -n "$end_epoch" ]]
      then
        days_left=$(( (end_epoch - now_epoch) / 86400 ))
        if [[ $days_left -lt 0 ]]
        then
          log_err "Certificate expired ($days_left days)"
        elif [[ $days_left -lt 30 ]]
        then
          log_warn "Certificate expires in $days_left days"
        else
          log_ok "Certificate valid for $days_left days"
        fi
      fi
      if [[ -n "$start_epoch" && -n "$end_epoch" ]]
      then
        validity_days=$(( (end_epoch - start_epoch) / 86400 ))
        if [[ "$validity_days" -gt 398 ]]
        then
          log_warn "Validity too long ($validity_days days)"
        fi
      fi
    fi

    SAN_DNS=$(extract_san_dns "$CERT_SAN")
    SAN_IP=$(extract_san_ip "$CERT_SAN")
    if [[ -n "$SAN_DNS" || -n "$SAN_IP" ]]
    then
      log_info "SAN DNS: $(echo $SAN_DNS)"
      if [[ -n "$SAN_IP" ]]
      then
        log_info "SAN IP:  $(echo $SAN_IP)"
      fi
    else
      log_warn "No subjectAltName present (legacy CN only)"
    fi

    host_lc=$(echo "$HOST" | tr '[:upper:]' '[:lower:]')
    if check_host_match "$host_lc" "$SAN_DNS" "$SAN_IP" "$CERT_CN"
    then
      log_ok "Hostname matches certificate"
    else
      log_warn "Hostname does NOT match certificate"
    fi

    CERT_COUNT=$(echo "$ALL_CERTS_PEM" | grep -c "BEGIN CERTIFICATE")
    if [[ -n "$CERT_COUNT" ]]
    then
      if [[ "$CERT_COUNT" -ge 2 ]]
      then
        log_ok "Chain length: $CERT_COUNT"
      else
        log_info "Chain length: $CERT_COUNT (leaf only)"
      fi
    fi

    if [[ -n "$ALL_CERTS_PEM" ]]
    then
      CHAIN_DIR="$TMPDIR/chain"
      mkdir -p "$CHAIN_DIR"
      echo "$ALL_CERTS_PEM" | awk -v dir="$CHAIN_DIR" 'BEGIN{n=0} /-----BEGIN CERTIFICATE-----/{n++; file=sprintf("%s/cert%02d.pem", dir, n)} {print > file}'
      chain_warn=0
      chain_count=0
      for f in "$CHAIN_DIR"/cert*.pem
      do
        [[ -f "$f" ]] || continue
        chain_count=$((chain_count + 1))
      done
      for f in "$CHAIN_DIR"/cert*.pem
      do
        [[ -f "$f" ]] || continue
        idx=$(basename "$f" | sed 's/^cert//' | sed 's/\.pem$//')
        if [[ "$idx" = "01" ]]
        then
          continue
        fi
        C_TEXT=$($OPENSSL_BIN x509 -noout -text -in "$f" 2>/dev/null)
        C_SIG=$(echo "$C_TEXT" | grep -m1 "Signature Algorithm" | sed 's/^[[:space:]]*Signature Algorithm:[[:space:]]*//')
        if echo "$C_SIG" | grep -qiE "md5|sha1"
        then
          log_warn "Chain cert $idx weak signature: $C_SIG"
          chain_warn=1
        fi
        C_END=$($OPENSSL_BIN x509 -noout -enddate -in "$f" 2>/dev/null | cut -d= -f2)
        C_END_EPOCH=$(date -d "$C_END" +%s 2>/dev/null)
        if [[ -n "$C_END_EPOCH" && "$C_END_EPOCH" -lt "$(date +%s)" ]]
        then
          log_warn "Chain cert $idx expired ($C_END)"
          chain_warn=1
        fi
        C_PUBKEY=$(echo "$C_TEXT" | grep -m1 "Public-Key" | sed 's/^[[:space:]]*//')
        C_PUBALG=$(echo "$C_TEXT" | grep -m1 "Public Key Algorithm" | sed 's/^[[:space:]]*Public Key Algorithm:[[:space:]]*//')
        C_PUBBITS=$(echo "$C_PUBKEY" | grep -oE '[0-9]+' | head -n1)
        if echo "$C_PUBALG" | grep -qi "rsa"
        then
          if [[ -n "$C_PUBBITS" && "$C_PUBBITS" -lt 2048 ]]
          then
            log_warn "Chain cert $idx RSA key too small (${C_PUBBITS} bit)"
            chain_warn=1
          fi
        elif echo "$C_PUBALG" | grep -qi "ec"
        then
          if [[ -n "$C_PUBBITS" && "$C_PUBBITS" -lt 256 ]]
          then
            log_warn "Chain cert $idx EC key too small (${C_PUBBITS} bit)"
            chain_warn=1
          fi
        fi
      done
      if [[ "$chain_count" -ge 2 ]]
      then
        if [[ "$chain_warn" -eq 0 ]]
        then
          log_ok "Chain intermediates: ok"
        else
          log_warn "Chain intermediates: issues found"
        fi
      fi
    fi
  else
    log_warn "No certificate could be retrieved"
  fi
}

check_certificate

function check_server_preference()
{
  # Server cipher preference (TLS 1.2)
  if [[ "$TLS12_STATE" = "yes" ]]
  then
    if [[ -z "$CERT_PUBALG" ]]
    then
      log_info "Server preference test skipped (no certificate)"
    else
      if echo "$CERT_PUBALG" | grep -qi "ec"
      then
        STRONG_CIPHER="ECDHE-ECDSA-AES256-GCM-SHA384"
      else
        STRONG_CIPHER="ECDHE-RSA-AES256-GCM-SHA384"
      fi
      WEAK_CIPHER="AES128-SHA"
      S1=$(sclient_run -tls1_2 -cipher "${STRONG_CIPHER}:${WEAK_CIPHER}" -brief)
      S2=$(sclient_run -tls1_2 -cipher "${WEAK_CIPHER}:${STRONG_CIPHER}" -brief)
      if handshake_ok "$S1" && handshake_ok "$S2"
      then
        C1=$(get_cipher_line "$S1")
        C2=$(get_cipher_line "$S2")
        if [[ -n "$C1" && -n "$C2" ]]
        then
          if [[ "$C1" = "$C2" ]]
          then
            if [[ "$C1" = "$WEAK_CIPHER" ]]
            then
              log_warn "Server cipher preference selects weak cipher"
            else
              log_ok "Server cipher preference active"
            fi
          else
            log_info "Server honors client cipher order"
          fi
        else
          log_info "Server preference test inconclusive"
        fi
      else
        log_info "Server preference test not possible"
      fi
    fi
  else
    log_info "Server preference test skipped (no TLS 1.2)"
  fi
}

check_server_preference

function check_session_info()
{
  # Preferred cipher details and forward secrecy hint
  FULL_OUT=$(sclient_run)
  # Verify return code
  VERIFY_LINE=$(echo "$FULL_OUT" | grep -i "Verify return code" | tail -n1)
  if [[ -z "$VERIFY_LINE" ]]
  then
    VERIFY_LINE=$(echo "$BASE_OUT" | grep -i "Verify return code" | tail -n1)
  fi
  if [[ -n "$VERIFY_LINE" ]]
  then
    VERIFY_CODE=$(echo "$VERIFY_LINE" | grep -oE 'Verify return code:[[:space:]]*[0-9]+' | grep -oE '[0-9]+')
    VERIFY_MSG=$(echo "$VERIFY_LINE" | sed -n 's/.*Verify return code:[[:space:]]*[0-9]\\+[[:space:]]*(\\(.*\\)).*/\\1/p')
    if [[ -z "$VERIFY_CODE" ]]
    then
      log_info "$VERIFY_LINE"
    elif [[ "$VERIFY_CODE" = "0" ]]
    then
      log_ok "Certificate verify: ok"
    else
      log_warn "Certificate verify: ${VERIFY_MSG:-unknown} (code $VERIFY_CODE)"
    fi
  else
    VERIFY_OK_LINE=$(echo "$FULL_OUT" | grep -i "^Verification:" | tail -n1)
    if [[ -z "$VERIFY_OK_LINE" ]]
    then
      VERIFY_OK_LINE=$(echo "$BASE_OUT" | grep -i "^Verification:" | tail -n1)
    fi
    if [[ -n "$VERIFY_OK_LINE" ]]
    then
      if echo "$VERIFY_OK_LINE" | grep -qi "OK"
      then
        log_ok "$VERIFY_OK_LINE"
      else
        log_warn "$VERIFY_OK_LINE"
      fi
    fi
  fi
  PREF_PROTO=$(get_protocol_line "$FULL_OUT")
  PREF_CIPHER=$(get_cipher_line "$FULL_OUT")
  FS_LINE=$(echo "$FULL_OUT" | grep -i "Server Temp Key" | head -n1)
  RENEG_LINE=$(echo "$FULL_OUT" | grep -i "Secure Renegotiation" | head -n1)

  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "Session"
  fi
  if [[ -n "$PREF_PROTO" || -n "$PREF_CIPHER" ]]
  then
    log_info "Preferred: ${PREF_PROTO:-?} / ${PREF_CIPHER:-?}"
  fi
  if [[ -n "$FS_LINE" ]]
  then
    FS_ALGO=$(echo "$FS_LINE" | sed -n 's/.*Server Temp Key:[[:space:]]*\\([^,]*\\).*/\\1/p')
    FS_BITS=$(echo "$FS_LINE" | grep -oE '[0-9]+' | head -n1)
    if echo "$FS_ALGO" | grep -qi "DH"
    then
      if [[ -n "$FS_BITS" && "$FS_BITS" -lt 2048 ]]
      then
        log_warn "Weak DH key (${FS_BITS} bit)"
      else
        log_ok "$FS_LINE"
      fi
    elif echo "$FS_ALGO" | grep -qiE "secp192r1|prime192v1|secp224r1|prime224v1"
    then
      log_warn "Weak ECDHE curve: $FS_ALGO"
    else
      log_ok "$FS_LINE"
    fi
  else
    log_warn "No forward secrecy indication (Server Temp Key not reported)"
  fi
  if [[ -n "$RENEG_LINE" ]]
  then
    if echo "$RENEG_LINE" | grep -qi "IS NOT supported"
    then
      if [[ "$TLS13_STATE" = "yes" && "$TLS12_STATE" != "yes" ]]
      then
        log_info "$RENEG_LINE"
      else
        log_warn "$RENEG_LINE"
      fi
    else
      log_ok "$RENEG_LINE"
    fi
  fi

  COMP_LINE=$(echo "$FULL_OUT" | grep -i "^Compression" | head -n1)
  if [[ -n "$COMP_LINE" ]]
  then
    COMP_VAL=$(echo "$COMP_LINE" | awk -F':' '{gsub(/^[[:space:]]+/, "", $2); print $2}')
    if [[ "$COMP_VAL" = "NONE" ]]
    then
      log_ok "Compression: NONE"
    else
      log_warn "Compression enabled: $COMP_VAL"
    fi
  else
    log_info "Compression: not reported"
  fi
}

check_session_info

function check_resumption()
{
  # Session resumption
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "Resumption"
  fi
  SESS_FILE="$TMPDIR/session.pem"

  if [[ "$TLS13_STATE" = "yes" ]]
  then
    http_head_sessout "$SESS_FILE" "$SNI" >/dev/null 2>&1
  elif [[ "$TLS12_STATE" = "yes" ]]
  then
    http_head_sessout "$SESS_FILE" "$SNI" >/dev/null 2>&1
  else
    sclient_run -sess_out "$SESS_FILE" -brief >/dev/null 2>&1
  fi
  if [[ -s "$SESS_FILE" ]]
  then
    RES_OUT=$(sclient_run -sess_in "$SESS_FILE")
    if handshake_ok "$RES_OUT"
    then
      if echo "$RES_OUT" | grep -qi "Reused"
      then
        log_ok "Session resumption: supported"
      else
        log_info "Session resumption: not reused"
      fi
    else
      log_warn "Session resumption: handshake failed"
    fi
  else
    if echo "$SCLIENT_HELP" | grep -q -- "-reconnect"
    then
      R_OUT=$(sclient_run -reconnect 2>/dev/null)
      if echo "$R_OUT" | grep -qi "Reused"
      then
        log_ok "Session resumption: supported (reconnect)"
      else
        log_info "Session resumption: not reused (reconnect)"
      fi
    else
      log_info "Session resumption: no session ticket"
    fi
  fi
}

check_resumption

function check_zero_rtt()
{
  # TLS 1.3 0-RTT
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "0-RTT"
  fi
  if echo "$SCLIENT_HELP" | grep -q -- "-early_data"
  then
    if [[ "$TLS13_STATE" = "yes" && -s "$SESS_FILE" ]]
    then
      REQ_FILE="$TMPDIR/early.txt"
      printf "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n" "$SNI" > "$REQ_FILE"
      EARLY_OUT=$(sclient_run -sess_in "$SESS_FILE" -early_data "$REQ_FILE")
      if echo "$EARLY_OUT" | grep -qi "Early data was accepted"
      then
        log_ok "TLS 1.3 0-RTT: accepted"
      elif echo "$EARLY_OUT" | grep -qi "Early data was rejected"
      then
        log_warn "TLS 1.3 0-RTT: rejected"
      elif echo "$EARLY_OUT" | grep -qi "Early data was not sent"
      then
        log_info "TLS 1.3 0-RTT: not sent"
      else
        log_info "TLS 1.3 0-RTT: not reported"
      fi
    else
      log_info "TLS 1.3 0-RTT: skipped (no TLS 1.3 session)"
    fi
  else
    log_info "TLS 1.3 0-RTT: not supported by local openssl"
  fi
}

check_zero_rtt

function check_sni()
{
  # No-SNI certificate
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "SNI"
  fi
  if is_ip "$HOST"
  then
    log_info "No-SNI test skipped for IP target"
  else
    NOSNI_OUT=$(sclient_run_nosni -showcerts 2>/dev/null)
    NOSNI_CERT_PEM=$(echo "$NOSNI_OUT" | awk 'BEGIN{f=0} /-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{if(f){exit}}')
    if [[ -n "$NOSNI_CERT_PEM" ]]
    then
      NOSNI_SUBJECT=$(echo "$NOSNI_CERT_PEM" | $OPENSSL_BIN x509 -noout -subject | sed 's/^subject=//')
      NOSNI_CN=$(extract_cn "$NOSNI_SUBJECT")
      NOSNI_FP=$(echo "$NOSNI_CERT_PEM" | $OPENSSL_BIN x509 -noout -fingerprint -sha256 | cut -d= -f2)
      if [[ -n "$CERT_PEM" ]]
      then
        SNI_FP=$(echo "$CERT_PEM" | $OPENSSL_BIN x509 -noout -fingerprint -sha256 | cut -d= -f2)
      else
        SNI_FP=""
      fi
      if [[ -n "$SNI_FP" && "$NOSNI_FP" = "$SNI_FP" ]]
      then
        log_ok "No-SNI certificate matches SNI"
      else
        log_info "No-SNI certificate differs (CN: ${NOSNI_CN:-n/a})"
      fi
    else
      log_warn "No-SNI certificate could not be retrieved"
    fi
  fi
}

check_sni

function check_alpn()
{
  # ALPN
  ALPN_OUT=$(sclient_run -alpn "h2,http/1.1" -brief)
  ALPN_LINE=$(echo "$ALPN_OUT" | grep -i "ALPN protocol" | head -n1)
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "ALPN"
  fi
  if [[ -n "$ALPN_LINE" ]]
  then
    if echo "$ALPN_LINE" | grep -qi "h2"
    then
      log_ok "HTTP/2 active (ALPN h2)"
    else
      ALPN_PROTO=$(echo "$ALPN_LINE" | sed -n 's/.*ALPN protocol:[[:space:]]*//p')
      log_info "HTTP/2 not active (ALPN ${ALPN_PROTO:-unknown})"
    fi
  else
    log_info "HTTP/2 not active (no ALPN)"
  fi
}

check_alpn

function check_ocsp()
{
  # OCSP stapling
  OCSP_OUT=$(sclient_run -status)
  OCSP_LINE=$(echo "$OCSP_OUT" | grep -i "OCSP Response Status" | head -n1)
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "OCSP"
  fi
  if [[ -n "$OCSP_LINE" ]]
  then
    log_ok "$OCSP_LINE"
  else
    log_info "No OCSP stapling"
  fi

  if [[ "$MUST_STAPLE" = "1" ]]
  then
    if [[ -n "$OCSP_LINE" ]]
    then
      log_ok "Must-staple satisfied"
    else
      log_warn "Must-staple present but no OCSP stapling"
    fi
  fi
}

check_ocsp

function check_http()
{
  # HTTP / HSTS
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "HTTP"
  fi
  HTTP_OUT=$(http_head "$SNI" | tr -d '\r')
  HTTP_LINE=$(echo "$HTTP_OUT" | grep -i "^HTTP/" | head -n1)
  if [[ -n "$HTTP_LINE" ]]
  then
    log_info "$HTTP_LINE"
    HSTS_LINE=$(echo "$HTTP_OUT" | grep -i "^strict-transport-security:" | head -n1)
    if [[ -n "$HSTS_LINE" ]]
    then
      log_ok "HSTS: ${HSTS_LINE#*: }"
    else
      log_info "HSTS header not present"
    fi
  else
    log_warn "No HTTP response received"
  fi
}

check_http

function check_weak_ciphers()
{
  # Weak cipher checks (quick)
  if [[ $QUIET -eq 0 ]]
  then
    echo ""
    echo "Weak Cipher Checks (TLS1.2)"
  fi
  if [[ "$TLS12_STATE" = "yes" ]]
  then
    WEAK_CIPHERS=("NULL" "aNULL" "EXPORT" "RC4" "3DES" "DES" "MD5" "SHA1")
    for w in "${WEAK_CIPHERS[@]}"
    do
      out=$(sclient_run -tls1_2 -cipher "$w" -brief)
      if handshake_ok "$out"
      then
        cipher=$(get_cipher_line "$out")
        log_warn "Weak cipher accepted for $w (selected: ${cipher:-unknown})"
      else
        log_ok "No $w"
      fi
    done
  else
    log_info "Skipped (no TLS 1.2)"
  fi
}

check_weak_ciphers

function check_full_ciphers()
{
  # Full cipher enumeration (slow)
  if [[ $FULL_CIPHERS -eq 1 ]]
  then
    if [[ $QUIET -eq 0 ]]
    then
      echo ""
      echo "Cipher Enumeration (TLS1.2)"
    fi
    if [[ "$TLS12_STATE" = "yes" ]]
    then
      CIPHER_LIST=$($OPENSSL_BIN ciphers 'ALL:!eNULL' 2>/dev/null | tr ':' ' ')
      for c in $CIPHER_LIST
      do
        out=$(sclient_run -tls1_2 -cipher "$c" -brief)
        if handshake_ok "$out"
        then
          if cipher_is_weak "$c"
          then
            log_warn "Supported: $c"
          else
            log_ok "Supported: $c"
          fi
        fi
      done
    else
      log_info "Skipped (no TLS 1.2)"
    fi
  fi
}

check_full_ciphers

exit 0
