Skip to content

Dnssec detect signature stripping - #161

Merged
Flowdalic merged 7 commits into
MiniDNS:masterfrom
Flowdalic:dnssec-detect-signature-stripping
Oct 20, 2026
Merged

Flowdalic merged 7 commits into
MiniDNS:masterfrom
Flowdalic:dnssec-detect-signature-stripping

Conversation

@Flowdalic

Copy link
Copy Markdown
Collaborator

No description provided.

The flag was set if, and only if, a DNSKEY was added to
selfSigningKeys. Hence it is equivalent to the set not being empty.
Use the set instead, so that there is no second piece of state that
needs to be kept in sync.

Furthermore, only DNSKEYs in selfSigningKeys are now verified as SEP,
also if the set is empty. Previously, if the set was empty, all
DNSKEYs were verified as SEP, but the result was ignored. Not doing so
avoids the DS lookups for those DNSKEYs, and exceptions thrown while
verifying a SEP whose result does not matter.

The log message "SEP key is not self-signed.", which was logged for
every DNSKEY if the set was empty, is replaced by a message that is
logged for every DNSKEY that is not considered as SEP, and that names
the DNSKEY and the reason.
A DNSKEY RRset is authenticated only through a secure entry point
(SEP), that is, a key of the RRset that validly signs the RRset and
that is a trust anchor or is referred to by a DS RR in the parent zone
(RFC 4035 § 5.2). It is never authenticated by a signature from
another zone.

However, if a response's DNSKEY RRset was only signed by a key of
another zone (a "cross-signature"), or was otherwise not validly
self-signed, then verifyAnswer() removed the DNSKEY records from the
set of records to be verified without finding a SEP, and reported no
unverified reason. As a result, an attacker able to inject DNS
responses could take a zone's genuine DNSKEY RRset, add a key of their
own, sign the RRset with that key while claiming an unrelated signer
name, and have the RRset, and every record signed by the added key,
considered authentic. This works even if the DNSKEY RRset contains the
key that the parent's DS RR refers to, as that key is not required to
sign the RRset.

verifyAnswer() now determines whether the answer carries a DNSKEY
RRset owned by the queried name. If so, a key of the RRset that
validly signs it has to verify as a SEP, otherwise the answer is not
authentic. This closes the chain of trust to a DNSKEY RRset that the
previous, separate checks for "is signed" and "contains a SEP" left
open.
When a DNSKEY RRset of the queried name is not authentic because it is
only signed by a key of another zone (a "cross-signature"), and not by
one of its own keys, verifyAnswer() reported the generic
NoSecureEntryPointReason. That reason suggests a missing DS RR or
trust anchor, whereas the actual problem is that the DNSKEY RRset is
not self-signed and hence can not be anchored at all, which is a
strong indication of an attack or a misconfiguration.

verifySignatures() already detects such cross-signatures. It now
records the cross-signing zones of the queried name's DNSKEY RRset, so
that verifyAnswer() can report the new CrossSignedDnskeyRrsetReason,
which names those zones. NoSecureEntryPointReason remains as a
fallback.
DnssecClient had a private isParentOrSelf(String, String) helper that
split the ACE representations of two names on "." and compared the
labels. This is exactly what DnsName.isChildOf() does, which considers
a name a child of, or equal to, another name, and which DnssecClient
already uses elsewhere. Use it and drop the custom helper.
When a response carries no signatures, DnssecClient reported the
generic NoSignaturesReason, regardless of whether the queried name is
within a signed zone (where signatures were expected and their absence
indicates stripping, a downgrade attack, or a misconfigured name
server) or within an unsigned, insecure zone (where their absence is
normal). This addresses the long-standing TODO in verifySignatures().

DnssecClient gained a setDetectSignatureStripping() setting, disabled
by default. When enabled, a response without signatures is probed by
querying the DNSKEY of the queried name: the name is within a signed
zone if, and only if, that response is authentic, either as an
authentic DNSKEY RRset at a signed zone's apex, or as an authentic
denial of existence below a signed apex. In that case the new, more
specific SignaturesExpectedButMissingReason is reported instead of
NoSignaturesReason.

The setting is off by default because the probe performs an additional
DNSKEY query. It only refines the reported reason; a response without
signatures is not authentic data regardless. The detection is
best-effort: it is accurate for zone apexes, and an on-path attacker
who also strips the probe response can evade it. A complete defense
requires authenticated denial of existence of the delegating DS RR,
which MiniDNS does not yet implement.
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 39398608611

Coverage increased (+0.2%) to 63.887%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 40 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

40 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
org/minidns/dnssec/DnssecClient.java 30 88.82%
org/minidns/dnssec/DnssecUnverifiedReason.java 10 72.6%

Coverage Stats

Coverage Status
Relevant Lines: 4942
Covered Lines: 3312
Line Coverage: 67.02%
Relevant Branches: 1906
Covered Branches: 1063
Branch Coverage: 55.77%
Branches in Coverage %: Yes
Coverage Strength: 2.98 hits per line

💛 - Coveralls

@Flowdalic
Flowdalic merged commit f0930fd into MiniDNS:master Oct 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants