Files

186 lines
4.9 KiB
Bash

#!/bin/bash
set -e
# Global defaults
TARGET=""
PORT=25
RESOLVER="system"
DNS_FULL=""
TLSA_RECORDS=()
DNSSEC_STATUS="insecure"
CERT_DIR=""
CERT_COUNT=0
parse_args() {
if [[ -z "$1" ]]; then
echo "Usage: check.sh <hostname>[:port] [-r resolver]" >&2
return 1
fi
local INPUT="$1"
shift
if [[ "$INPUT" == *":"* ]]; then
TARGET="${INPUT%%:*}"
PORT="${INPUT##*:}"
else
TARGET="$INPUT"
PORT=25
fi
RESOLVER="system"
while [[ $# -gt 0 ]]; do
case "$1" in
-r)
RESOLVER="${2:?Error: -r requires a resolver address}"
shift 2
;;
*)
echo "ERROR: Unknown option $1" >&2
echo "Usage: check.sh <hostname>[:port] [-r resolver]" >&2
return 1
;;
esac
done
}
fetch_dns() {
local dns_query_target="_${PORT}._tcp.${TARGET}"
if [[ "$RESOLVER" == "system" ]]; then
DNS_FULL=$(dig +adflag +dnssec TLSA "$dns_query_target" 2>&1) || true
else
DNS_FULL=$(dig "@${RESOLVER}" +adflag +dnssec TLSA "$dns_query_target" 2>&1) || true
fi
if [[ -z "$DNS_FULL" ]] || echo "$DNS_FULL" | grep -q "no servers could be reached" || echo "$DNS_FULL" | grep -q "communications error"; then
echo "ERROR: DNS query failed for ${TARGET} using resolver ${RESOLVER}" >&2
[[ -n "$DNS_FULL" ]] && echo "$DNS_FULL" >&2
return 1
fi
mapfile -t TLSA_RECORDS < <(echo "$DNS_FULL" | grep -v '^;' | grep -E "\s+IN\s+TLSA\s+" | tr -d '()"' || true)
if [[ ${#TLSA_RECORDS[@]} -eq 0 ]]; then
echo "ERROR: No TLSA record found for $dns_query_target using resolver ${RESOLVER}" >&2
return 1
fi
if [[ "$DNS_FULL" =~ "flags:".*"ad" ]]; then
DNSSEC_STATUS="secure"
else
DNSSEC_STATUS="insecure"
fi
}
fetch_certs() {
local TEMP_CHAIN
TEMP_CHAIN=$(mktemp)
echo "QUIT" | openssl s_client -starttls smtp -connect "${TARGET}:${PORT}" \
-servername "${TARGET}" -showcerts >"$TEMP_CHAIN" 2>/dev/null || true
CERT_DIR=$(mktemp -d)
awk -v dir="$CERT_DIR" '
/BEGIN CERTIFICATE/ {
c++
file = dir "/cert_" c ".pem"
}
/BEGIN CERTIFICATE/,/END CERTIFICATE/ {
print $0 > file
}
' "$TEMP_CHAIN"
rm -f "$TEMP_CHAIN"
CERT_COUNT=$(ls -1 "$CERT_DIR"/cert_*.pem 2>/dev/null | wc -l || echo 0)
if [[ $CERT_COUNT -eq 0 ]]; then
echo "ERROR: Could not retrieve certificates from server." >&2
rm -rf "$CERT_DIR"
return 1
fi
}
verify_dane() {
local MATCH_FOUND=false
echo "--------------------------------------------------------"
echo "DANE VERIFICATION AUDIT"
echo "--------------------------------------------------------"
echo "Target Host: $TARGET"
echo "Resolver Used: $RESOLVER"
echo "DNSSEC Status: $DNSSEC_STATUS"
echo "Total Records: ${#TLSA_RECORDS[@]}"
echo "Total Certs: $CERT_COUNT"
echo "--------------------------------------------------------"
local RECORD_LINE
for RECORD_LINE in "${TLSA_RECORDS[@]}"; do
local USAGE SELECTOR MATCHING DNS_HASH EXTRACT_CMD HASH_CMD LIVE_HASH
USAGE=$(echo "$RECORD_LINE" | awk '{print $5}')
SELECTOR=$(echo "$RECORD_LINE" | awk '{print $6}')
MATCHING=$(echo "$RECORD_LINE" | awk '{print $7}')
DNS_HASH=$(echo "$RECORD_LINE" | awk '{for(i=8;i<=NF;i++) printf $i; print ""}' | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]' || true)
# Choose extraction command
if [[ "$SELECTOR" == "1" ]]; then
EXTRACT_CMD="openssl x509 -pubkey -noout | openssl pkey -pubin -outform DER"
else
EXTRACT_CMD="openssl x509 -outform DER"
fi
# Choose hashing command
case "$MATCHING" in
1) HASH_CMD="openssl dgst -sha256 -hex" ;;
2) HASH_CMD="openssl dgst -sha512 -hex" ;;
0) HASH_CMD="xxd -p -c 0" ;;
*)
echo "WARNING: Unsupported matching type $MATCHING" >&2
continue
;;
esac
# Validate based on Usage
if [[ "$USAGE" == "3" ]]; then
if [[ ! -f "$CERT_DIR/cert_1.pem" ]]; then
continue
fi
LIVE_HASH=$(eval "$EXTRACT_CMD" < "$CERT_DIR/cert_1.pem" | eval "$HASH_CMD" | sed 's/.* //; s/.*=//' | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]' || true)
if [[ "$DNS_HASH" == "$LIVE_HASH" ]]; then
echo "MATCH: Found matching Usage 3 TLSA record (Leaf Certificate)"
MATCH_FOUND=true
break
fi
elif [[ "$USAGE" == "2" ]]; then
local i
for ((i=2; i<=CERT_COUNT; i++)); do
if [[ ! -f "$CERT_DIR/cert_${i}.pem" ]]; then
continue
fi
LIVE_HASH=$(eval "$EXTRACT_CMD" < "$CERT_DIR/cert_${i}.pem" | eval "$HASH_CMD" | sed 's/.* //; s/.*=//' | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]' || true)
if [[ "$DNS_HASH" == "$LIVE_HASH" ]]; then
echo "MATCH: Found matching Usage 2 TLSA record (CA Certificate at depth $((i-1)))"
MATCH_FOUND=true
break 2
fi
done
fi
done
if [[ -d "$CERT_DIR" ]]; then
rm -rf "$CERT_DIR"
fi
if [[ "$MATCH_FOUND" == "true" && "$DNSSEC_STATUS" == "secure" ]]; then
echo "VERIFICATION: SUCCESS"
return 0
else
echo "VERIFICATION: FAILURE (Match: $MATCH_FOUND, DNSSEC: $DNSSEC_STATUS)"
return 1
fi
}
# Guard entry point to allow testing/sourcing
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
parse_args "$@"
fetch_dns
fetch_certs
verify_dane
fi