Skip to content

DNS

DNS

DNSSEC

Can Only Check if a DNS Record has been changed.

Algorithms

Number Mnemonics DNSSEC Signing DNSSEC Validation
1 RSAMD5 MUST NOT MUST NOT
3 DSA MUST NOT MUST NOT
5 RSASHA1 NOT RECOMMENDED MUST
6 DSA-NSEC3-SHA1 MUST NOT MUST NOT
7 RSASHA1-NSEC3-SHA1 NOT RECOMMENDED MUST
8 RSASHA256 MUST MUST
10 RSASHA512 NOT RECOMMENDED MUST
12 ECC-GOST MUST NOT MAY
13 ECDSAP256SHA256 MUST MUST
14 ECDSAP384SHA384 MAY RECOMMENDED
15 ED25519 RECOMMENDED RECOMMENDED
16 ED448 MAY RECOMMENDED

Keys

Get Top Level DNS Key:

>>> dig @ganz.ns.cloudflare.com. generalzero.org DNSKEY +dnssec

; <<>> DiG 9.20.4 <<>> @ganz.ns.cloudflare.com. generalzero.org DNSKEY +dnssec
; (6 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 57095
;; flags: qr aa rd; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 1232
;; QUESTION SECTION:
;generalzero.org.               IN      DNSKEY

;; ANSWER SECTION:
generalzero.org.        3600    IN      DNSKEY  257 3 13 mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAeF+ KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==
generalzero.org.        3600    IN      DNSKEY  256 3 13 oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IRd8 KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==
generalzero.org.        3600    IN      RRSIG   DNSKEY 13 2 3600 20250305013940 20250103013940 2371 generalzero.org. reY4OL9yVqdZQYpbG6+n+Kb7kD5wpYPy4nxznuIErhp9uqZ8IpM+8YbG OY8dkk89dZlPBnQjC8+uAqHmxK6pHA==

;; Query time: 3 msec
;; SERVER: 2606:4700:58::a29f:2c28#53(ganz.ns.cloudflare.com.) (UDP)
;; WHEN: Tue Jan 07 22:21:00 EST 2025
;; MSG SIZE  rcvd: 315

The DNSKEY Response contains 2 DNSKEY Keys. These are differentiated by the flags.
Key Signing Key(Flag 257): The Key that is used to sign the Zone Signing Key with the signature information in the RRSIG info.
Zone Signing Key(Flag 256): The Key that is used to sign subdomains. RRSIG data

DNSSec Record:

>>> dig www.generalzero.org +dnssec

; <<>> DiG 9.20.4 <<>> www.generalzero.org +dnssec
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 59755
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 65494
;; QUESTION SECTION:
;www.generalzero.org.           IN      A

;; ANSWER SECTION:
www.generalzero.org.    253     IN      A       172.67.179.53
www.generalzero.org.    253     IN      A       104.21.91.200
www.generalzero.org.    253     IN      RRSIG   A 13 3 300 20250107222441 20250105202441 34505 generalzero.org. bCLHO171apwxiIvCI0zZYgHWBX6CmtpdvsKDykystJM+2IEXmQPsocv7 SUYyErUAKLf7VwKaSufHy+fdgMkO1Q==

;; Query time: 0 msec
;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP)
;; WHEN: Mon Jan 06 16:25:27 EST 2025
;; MSG SIZE  rcvd: 191

Implementation

DNSSec Manual Verify:

import dns.dnssec
import dns.resolver
import dns.query
import dns.message
import base64, sys
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.exceptions import InvalidSignature
from dns.dnssecalgs import (  # pylint: disable=C0412
	get_algorithm_cls_from_dnskey,
)

def parse_dnskey_to_public_key(dnskey):
	"""Convert a DNSKEY record to a public key object, supporting various algorithms."""
	#print(f"DNSKey: {dnskey.flags} {dnskey.protocol} {dnskey.algorithm} {dnskey.key}")
	#flags, protocol, algorithm, key = [dnskey.flags, dnskey.protocol, dnskey.algorithm, dnskey.key]
	return get_algorithm_cls_from_dnskey(dnskey)


def validate_domain_key(domain, ns_address="1.1.1.1", timeout=20):
	# Get DNSKey from Name Servers
	request = dns.message.make_query(domain, dns.rdatatype.DNSKEY, want_dnssec=True)
	try:
		response = dns.query.udp(request, ns_address, timeout=timeout)
		#Check If valid Response
		if response.rcode() != 0:
			print("ERROR: no DNSKEY record found or SERVEFAIL")
			return
		
		#Check If Key exists
		answer = response.answer
		if len(answer) != 2:
			print("ERROR: could not find RRSET record (DNSKEY and RR DNSKEY) in zone")
			return

		# check if is the DNSKEY record signed, RRSET validation
		name = dns.name.from_text(domain)

		#Get DNSKeys
		public_key = None
		signing_key = None
		for dnskey in answer[0]:
			if dnskey.flags == 256:
				#Zone Signing Key
				public_key = parse_dnskey_to_public_key(dnskey).public_cls.from_dnskey(dnskey)
			elif dnskey.flags == 257:
				#Key Signing Key
				signing_key = parse_dnskey_to_public_key(dnskey).public_cls.from_dnskey(dnskey)

		#_validate_rrsig
		for rrsig in answer[1]:
			print(f"Signature: {rrsig.signature.hex()}")
			#dns.dnssec._validate_rrsig(answer[0], rrsig, {name: answer[0]})

			data = dns.dnssec._make_rrsig_signature_data(answer[0], rrsig, domain)
			print(f"SignData: {data.hex()}")

			print(f"Public Key: {signing_key.key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)}")
			try:
				signing_key.verify(rrsig.signature, data)
				print(f"Verify: True")
				break
			except InvalidSignature as e:
				print("Verify: False")
	except Exception as e:
		print(e)

	return public_key

def validate_dnssec_resp(domain, record_type, public_key, ns_address="1.1.1.1", timeout=20):
	"""Validate DNSSEC for the given domain and record type."""
	# Fetch the DNSKEY record from the nameserver
	try:
		request = dns.message.make_query(domain, record_type, want_dnssec=True)
		response = dns.query.udp(request, ns_address, timeout=timeout)

		if response.rcode() != 0:
			print(f"ERROR: {record_type} request failed for {domain}")
			return

		if len(response.answer) < 2:
			print(f"ERROR: Incomplete DNSSEC records for {domain}")
			return

		dnskeys = response.answer[0]
		rrsigs = response.answer[1]
		
		# Validate DNSKEY RRSIG
		for rrsig in rrsigs:
			data = dns.dnssec._make_rrsig_signature_data(dnskeys, rrsig, domain)
			print(f"Signature: {rrsig.signature.hex()}")
			print(f"SignData: {data.hex()}")
			print(f"Public Key: {public_key.key.public_bytes(encoding=serialization.Encoding.PEM,format=serialization.PublicFormat.SubjectPublicKeyInfo)}")

			try:
				public_key.verify(rrsig.signature, data)
				print(f"DNSKEY signature verified for {domain}")
			except InvalidSignature:
				print(f"DNSKEY signature verification failed for {domain}")
				return

	except Exception as e:
		print(f"Error validating DNSSEC for {domain}: {e}")

# Update __main__ to include subdomain validation
if __name__ == "__main__":
	domain = sys.argv[1]
	record_type = sys.argv[2]

	# Strip Subdomain
	top_level_domain = '.'.join(domain.split('.')[-2:]) + "."
	ns_server = None
	
	#Insure Remote Resolver
	resolver = dns.resolver.Resolver(configure=False)
	resolver.timeout = 20
	resolver.nameservers = ['1.1.1.1', '9.9.9.9', '8.8.8.8']

	#Get NS Server
	try:
		response = resolver.resolve(top_level_domain, rdtype=dns.rdatatype.NS)
		ns_server = response.rrset[0]
		response = resolver.resolve(str(ns_server), rdtype=dns.rdatatype.A)
		ns_address = response.rrset[0].to_text()
		print(f"Subdomain NS Server: {ns_server}, Address: {ns_address}")
	except Exception as e:
		print(f"Error resolving nameserver for {domain}: {e}")

	# Validate Top Domain Key
	print(f"Validate {top_level_domain} Keys and signature\n")
	public_key = validate_domain_key(top_level_domain)

	# Validate Main Domain Query
	print("\n")
	print(f"Validate {domain} Signature")
	validate_dnssec_resp(domain, record_type, public_key)

# >>> python dnssec.py mail.generalzero.org A
# Subdomain NS Server: ganz.ns.cloudflare.com., Address: 172.64.35.40
# Validate generalzero.org. Keys and signature

# Signature: ade63838bf7256a759418a5b1bafa7f8a6fb903e70a583f2e27c739ee204ae1a7dbaa67c22933ef186c6398f1d924f3d75994f0674230bcfae02a1e6c4aea91c
# SignData: 00300d0200000e1067c7ab5c67773fdc09430b67656e6572616c7a65726f036f7267000b67656e6572616c7a65726f036f7267000030000100000e1000440100030da09311112cf9138818cd2feae970ebbd4d6a30f6088c25b325a39abbc5cd1197aa098283e5aaf421177c2aa5d714992a9957d1bcc18f98cd71f1f1806b65e1480b67656e6572616c7a65726f036f7267000030000100000e1000440101030d99db2cc14cabdc33d6d77da63a2f15f71112584f234e8d1dc428e39e8a4a97e1aa271a555dc90701e17e2a4c4b6f120b7c32d44f4ac02bd894cf2d4be7778a19
# Public Key: b'-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmdsswUyr3DPW132mOi8V9xESWE8j\nTo0dxCjjnopKl+GqJxpVXckHAeF+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==\n-----END PUBLIC KEY-----\n'
# Verify: True


# Validate mail.generalzero.org Signature
# Signature: be9810e6620f9c863d16b7dc026c44362bdcd893011086ade06d8240f30b85f1fe984e12d931651f3592e65c39f9d2a34d06a37bfefb760904ffc0117f4912ed
# SignData: 00010d030000012c677f4d57677c8e3786c90b67656e6572616c7a65726f036f726700046d61696c0b67656e6572616c7a65726f036f726700000100010000012c000464231f4e
# Public Key: b'-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoJMRESz5E4gYzS/q6XDrvU1qMPYI\njCWzJaOau8XNEZeqCYKD5ar0IRd8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==\n-----END PUBLIC KEY-----\n'
# DNSKEY signature verified for mail.generalzero.org