Files
martnet-ddns/dnssec-verify.sh

141 lines
4.6 KiB
Bash
Executable File

#!/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 '<parent>'".
#
# 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 <domain>" >&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