#!/bin/bash # # Validate an HTTPS endpoint against Apple's ATS and FCP_v2.1 diagnostics. # Run this on macOS 26.4 or later for the FCP_v2.1 result Apple references. set -u usage() { printf 'Usage: %s \n' "$0" printf 'Examples:\n' printf ' %s fw.example.org\n' "$0" printf ' %s https://fw.example.org\n' "$0" printf ' %s fw.example.org:8443\n' "$0" } die() { printf 'Error: %s\n' "$1" >&2 exit 1 } trim() { awk '{$1=$1; print}' } if [ "$#" -ne 1 ]; then usage exit 1 fi if [ ! -x /usr/bin/nscurl ]; then die "This script must be run on macOS with /usr/bin/nscurl available." fi if ! command -v openssl >/dev/null 2>&1; then die "openssl was not found in PATH." fi INPUT_TARGET="$1" TARGET="$INPUT_TARGET" case "$TARGET" in http://*) TARGET="${TARGET#http://}" ;; https://*) TARGET="${TARGET#https://}" ;; *://*) die "Only HTTP or HTTPS URLs are supported. Provide a DNS name or HTTPS URL." ;; esac TARGET="${TARGET%%/*}" TARGET="${TARGET%%\?*}" TARGET="${TARGET%%#*}" TARGET="${TARGET##*@}" TARGET="${TARGET%.}" if [ -z "$TARGET" ]; then die "Could not determine a host from: $INPUT_TARGET" fi case "$TARGET" in *'['*|*']'*) die "IPv6 literals are not supported by this helper. Use the server DNS name." ;; esac HOST="$TARGET" PORT="443" case "$TARGET" in *:*) HOST="${TARGET%%:*}" PORT="${TARGET##*:}" ;; esac if [ -z "$HOST" ]; then die "Host is empty after parsing: $INPUT_TARGET" fi case "$PORT" in ''|*[!0-9]*) die "Port must be numeric: $PORT" ;; esac URL="https://$HOST" if [ "$PORT" != "443" ]; then URL="$URL:$PORT" fi URL="$URL/" CONNECT_TARGET="$HOST:$PORT" extract_nscurl_result() { label="$1" printf '%s\n' "$NSCURL_OUT" | awk -v wanted="$label" ' BEGIN { wanted = tolower(wanted) in_block = 0 } { line = tolower($0) if (index(line, wanted) > 0) { in_block = 1 next } if (in_block && $0 ~ /Result[[:space:]]*:/) { print $NF exit } if (in_block && $0 ~ /^={10,}/) { in_block = 0 } } ' } print_result_line() { label="$1" result="$2" case "$result" in PASS) printf ' %-18s PASS\n' "$label:" ;; FAIL) printf ' %-18s FAIL\n' "$label:" ;; *) printf ' %-18s UNKNOWN\n' "$label:" ;; esac } extract_ssl_field() { field="$1" printf '%s\n' "$OPENSSL_OUT" | awk -F: -v field="$field" ' { key = $1 gsub(/^[ \t]+|[ \t]+$/, "", key) } key == field { value = $2 gsub(/^[ \t]+|[ \t]+$/, "", value) print value exit } ' } printf '==========================================================\n' printf 'Validating Apple ATS and FCP_v2.1 readiness for %s\n' "$CONNECT_TARGET" printf '==========================================================\n' printf '\n[1/3] Running Apple nscurl ATS diagnostics...\n' NSCURL_OUT="$(/usr/bin/nscurl --ats-diagnostics "$URL" 2>&1)" NSCURL_STATUS=$? if [ "$NSCURL_STATUS" -ne 0 ]; then printf ' nscurl exited with status %s. Raw diagnostic output follows.\n' "$NSCURL_STATUS" printf '%s\n' "$NSCURL_OUT" exit 1 fi DEFAULT_RESULT="$(extract_nscurl_result "Default ATS Secure Connection")" FCP_RESULT="$(extract_nscurl_result "FCP_v2.1")" print_result_line "Standard ATS" "$DEFAULT_RESULT" print_result_line "FCP_v2.1" "$FCP_RESULT" printf '\n[2/3] Checking negotiated TLS protocol and cipher with OpenSSL...\n' OPENSSL_OUT="$(openssl s_client -connect "$CONNECT_TARGET" -servername "$HOST" /dev/null)" TLS_PROTOCOL="$(extract_ssl_field "Protocol" | trim)" TLS_CIPHER="$(extract_ssl_field "Cipher" | trim)" VERIFY_RESULT="$(printf '%s\n' "$OPENSSL_OUT" | awk '/Verify return code:/ {sub(/^[ \t]*Verify return code:[ \t]*/, ""); print; exit}')" TEMP_KEY="$(printf '%s\n' "$OPENSSL_OUT" | awk -F': ' '/Server Temp Key:/ {print $2; exit}')" if [ -n "$TLS_PROTOCOL" ]; then printf ' Protocol: %s\n' "$TLS_PROTOCOL" else printf ' Protocol: UNKNOWN\n' fi if [ -n "$TLS_CIPHER" ]; then printf ' Cipher: %s\n' "$TLS_CIPHER" else printf ' Cipher: UNKNOWN\n' fi if [ -n "$TEMP_KEY" ]; then printf ' Temp key: %s\n' "$TEMP_KEY" fi case "$TLS_PROTOCOL:$TLS_CIPHER" in TLSv1.3:*) printf ' PFS: yes, TLS 1.3 ciphers provide forward secrecy\n' ;; *:ECDHE*) printf ' PFS: yes, ECDHE was negotiated\n' ;; *) printf ' PFS: review needed; OpenSSL did not show TLS 1.3 or ECDHE\n' ;; esac if [ -n "$VERIFY_RESULT" ]; then printf ' Verify: %s\n' "$VERIFY_RESULT" fi printf '\n[3/3] Checking certificate signature details...\n' CERT_TEXT="$(printf '%s\n' "$OPENSSL_OUT" | openssl x509 -text -noout 2>/dev/null)" SIG_ALG="$(printf '%s\n' "$CERT_TEXT" | awk -F': ' '/Signature Algorithm/ {print $2; exit}' | trim)" PUBLIC_KEY="$(printf '%s\n' "$CERT_TEXT" | awk -F'[()]' '/Public-Key:/ {print $2; exit}' | trim)" if [ -n "$SIG_ALG" ]; then printf ' Signature algorithm: %s\n' "$SIG_ALG" case "$(printf '%s' "$SIG_ALG" | tr '[:upper:]' '[:lower:]')" in *sha1*|*md5*) printf ' Signature status: review needed; weak hash algorithm detected\n' ;; *) printf ' Signature status: no SHA-1 or MD5 signature detected\n' ;; esac else printf ' Signature algorithm: UNKNOWN\n' fi if [ -n "$PUBLIC_KEY" ]; then printf ' Public key size: %s\n' "$PUBLIC_KEY" fi printf '\n==========================================================\n' EXIT_CODE=0 if [ "$DEFAULT_RESULT" = "FAIL" ] || [ "$FCP_RESULT" = "FAIL" ]; then printf 'RESULT: Remediation required before this endpoint can be treated as ready for Apple stricter TLS requirements.\n' EXIT_CODE=2 elif [ "$DEFAULT_RESULT" = "PASS" ] && [ "$FCP_RESULT" = "PASS" ]; then printf 'RESULT: nscurl reports this endpoint passes Standard ATS and FCP_v2.1.\n' else printf 'RESULT: Unable to parse a complete nscurl ATS/FCP result. Run the raw nscurl command and review the output manually.\n' EXIT_CODE=3 fi printf 'Raw Apple test command: /usr/bin/nscurl --ats-diagnostics %s\n' "$URL" exit "$EXIT_CODE"