From 03ec948359c4981a65fc1f6fd1435ff85c0172c8 Mon Sep 17 00:00:00 2001 From: Jorj Bauer Date: Tue, 9 Sep 2025 20:36:40 +0000 Subject: [PATCH] doesn't really belong in this repo but I don't want to make a new repo for just this dnssec key validation tool --- dnssec-verify.sh | 140 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 dnssec-verify.sh diff --git a/dnssec-verify.sh b/dnssec-verify.sh new file mode 100755 index 0000000..6e06212 --- /dev/null +++ b/dnssec-verify.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# dnssec-verify.sh — emit exact DS lines to ADD/REMOVE in the *right place*. +# If parent is a TLD (like com/org), suggest "registrar". +# If parent is another zone (delegated child), suggest "parent zone ''". +# +# Compares parent DS vs child-derived DS (SHA-256) robustly and prints only real diffs. + +set -Eeuo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 " >&2; exit 2 +fi +domain="${1%.}" + +need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing $1" >&2; exit 3; }; } +for t in dig dnssec-dsfromkey awk sed tr sort uniq grep; do need "$t"; done + +parent_of() { + local d="$1" + [[ "$d" == *.* ]] || { echo ""; return; } + echo "${d#*.}" +} + +pick_ns_all() { + # Return up to 6 NS for a zone (one per line) + dig +short NS "$1" | awk 'NF' | head -n 6 +} + +normalize_parent_ds() { + # Normalize DS from full +answer or +short: "keytag alg 2 DIGEST" + awk -v dom="$domain." ' + BEGIN { IGNORECASE=1 } + { + line = $0 + gsub(/\(/,"",line); gsub(/\)/,"",line) + n = split(line, f, /[ \t]+/) + dsidx = 0 + for (i=1; i<=n; i++) if (toupper(f[i])=="DS") { dsidx=i; break } + if (dsidx>0) { + kt=f[dsidx+1]; alg=f[dsidx+2]; dt=f[dsidx+3] + if (kt ~ /^[0-9]+$/ && alg ~ /^[0-9]+$/ && dt ~ /^[0-9]+$/) { + digest="" + for (j=dsidx+4; j<=n; j++) if (f[j] ~ /^[0-9A-Fa-f]+$/) digest=digest toupper(f[j]) + if (dt=="2" && digest!="") printf "%s %s %s\n", kt, alg, digest + } + } else { + if (n>=4 && f[1] ~ /^[0-9]+$/ && f[2] ~ /^[0-9]+$/ && f[3] ~ /^[0-9]+$/) { + if (f[3]=="2") { + digest="" + for (k=4; k<=n; k++) if (f[k] ~ /^[0-9A-Fa-f]+$/) digest=digest toupper(f[k]) + if (digest!="") printf "%s %s %s\n", f[1], f[2], digest + } + } + } + }' +} + +normalize_child_ds() { + # Normalize dnssec-dsfromkey output: "keytag alg 2 DIGEST" + awk -v dom="$domain." ' + BEGIN { IGNORECASE=1 } + toupper($3)=="DS" && $4 ~ /^[0-9]+$/ && $5 ~ /^[0-9]+$/ && $6 ~ /^[0-9]+$/ { + if ($6=="2") { + digest="" + for (i=7; i<=NF; i++) if ($i ~ /^[0-9A-Fa-f]+$/) digest=digest toupper($i) + if (digest!="") printf "%s %s %s\n", $4, $5, digest + } + }' +} + +parent_zone="$(parent_of "$domain")" +[[ -z "$parent_zone" || "$parent_zone" == "$domain" ]] && { echo "Cannot determine parent for $domain" >&2; exit 4; } + +# Where to publish DS? +# - If parent has no additional dot (e.g., "org", "com"): registrar. +# - Else: publish in the parent authoritative zone you control. +if [[ "$parent_zone" == *.* ]]; then + where="parent zone '$parent_zone'" +else + where="registrar (parent '$parent_zone')" +fi + +# Parent NS set (authoritative for the parent zone) +mapfile -t parent_ns_list < <(pick_ns_all "$parent_zone") +((${#parent_ns_list[@]})) || { echo "No NS for parent zone $parent_zone" >&2; exit 5; } + +# Child NS (authoritative for the child) +child_ns="$(dig +short NS "$domain" | awk 'NR==1{print}')" +[[ -z "${child_ns:-}" ]] && child_ns="${parent_ns_list[0]}" + +# ----- Fetch and normalize parent DS (union across parent NS) ----- +parent_ds_tmp="$(mktemp)"; trap 'rm -f "$parent_ds_tmp"' EXIT; : >"$parent_ds_tmp" +for ns in "${parent_ns_list[@]}"; do + dig @"$ns" +norec +dnssec +noall +answer "$domain" DS 2>/dev/null \ + | normalize_parent_ds >> "$parent_ds_tmp" || true +done +mapfile -t parent_ds < <(sort -u "$parent_ds_tmp") + +# ----- Fetch child DNSKEY and derive DS (SHA-256) ----- +dnskey_ans="$(dig @"$child_ns" +tcp +dnssec +noall +answer "$domain" DNSKEY 2>/dev/null || true)" +if [[ -z "$dnskey_ans" ]]; then + echo "Could not fetch DNSKEY from $child_ns for $domain" >&2 + exit 6 +fi +mapfile -t child_ds < <( + printf '%s\n' "$dnskey_ans" \ + | dnssec-dsfromkey -f - -a SHA-256 "$domain" 2>/dev/null \ + | normalize_child_ds \ + | sort -u +) + +# Build sets +declare -A P C +for r in "${parent_ds[@]:-}"; do [[ -n "${r// }" ]] && P["$r"]=1; done +for r in "${child_ds[@]:-}"; do [[ -n "${r// }" ]] && C["$r"]=1; done + +adds=() removes=() + +# ADD = child-derived not present at parent +for r in "${child_ds[@]:-}"; do [[ -z "${P[$r]+x}" ]] && adds+=("$r"); done +# REMOVE = present at parent but not derivable from current child keys +for r in "${parent_ds[@]:-}"; do [[ -z "${C[$r]+x}" ]] && removes+=("$r"); done + +emit_lines() { + local hdr="$1"; shift + local -a arr=( "$@" ) + ((${#arr[@]})) || return 0 + echo "## $hdr in $where:" + for r in "${arr[@]}"; do + set -- $r + echo "$domain. IN DS $1 $2 2 $3" + done +} + +if ((${#adds[@]}==0 && ${#removes[@]}==0)); then + echo "## Parent DS already matches child KSKs (no changes) — in $where." +else + emit_lines "ADD these DS" "${adds[@]}" + emit_lines "REMOVE these DS" "${removes[@]}" +fi