Skip to content

Commit bc06b53

Browse files
committed
EDE validation
1 parent d571da5 commit bc06b53

28 files changed

+299
-133
lines changed

src/main/java/org/xbill/DNS/dnssec/DnsSecVerifier.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,17 @@ public JustifiedSecStatus verify(SRRset rrset, RRset keyRrset, Instant date) {
164164
* @param date The date against which to verify the rrset.
165165
* @return SecurityStatus.SECURE if the rrset verified, BOGUS otherwise.
166166
*/
167-
public SecurityStatus verify(RRset rrset, DNSKEYRecord dnskey, Instant date) {
167+
public JustifiedSecStatus verify(RRset rrset, DNSKEYRecord dnskey, Instant date) {
168168
List<RRSIGRecord> sigs = rrset.sigs();
169169
if (sigs.isEmpty()) {
170170
log.info("RRset failed to verify due to lack of signatures");
171-
return SecurityStatus.BOGUS;
171+
return new JustifiedSecStatus(
172+
SecurityStatus.BOGUS,
173+
ExtendedErrorCodeOption.RRSIGS_MISSING,
174+
R.get("dnskey.no_sigs", rrset.getName()));
172175
}
173176

177+
DNSSECException lastException = null;
174178
for (RRSIGRecord sigrec : sigs) {
175179
// Skip RRSIGs that do not match our given key's footprint.
176180
if (sigrec.getFootprint() != dnskey.getFootprint()) {
@@ -179,13 +183,22 @@ public SecurityStatus verify(RRset rrset, DNSKEYRecord dnskey, Instant date) {
179183

180184
try {
181185
DNSSEC.verify(rrset, sigrec, dnskey, date);
182-
return SecurityStatus.SECURE;
186+
return new JustifiedSecStatus(SecurityStatus.SECURE, -1, null);
183187
} catch (DNSSECException e) {
184188
log.error("Failed to validate RRset", e);
189+
lastException = e;
185190
}
186191
}
187192

188193
log.info("RRset failed to verify: all signatures were BOGUS");
189-
return SecurityStatus.BOGUS;
194+
int edeReason = ExtendedErrorCodeOption.DNSSEC_BOGUS;
195+
String reason = "dnskey.invalid";
196+
if (lastException instanceof SignatureExpiredException) {
197+
edeReason = ExtendedErrorCodeOption.SIGNATURE_EXPIRED;
198+
} else if (lastException instanceof SignatureNotYetValidException) {
199+
edeReason = ExtendedErrorCodeOption.SIGNATURE_NOT_YET_VALID;
200+
}
201+
202+
return new JustifiedSecStatus(SecurityStatus.BOGUS, edeReason, reason);
190203
}
191204
}

src/main/java/org/xbill/DNS/dnssec/KeyEntry.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@
1919
@Slf4j
2020
@EqualsAndHashCode(
2121
callSuper = true,
22-
of = {"badReason", "isEmpty"})
22+
of = {"edeReason", "badReason", "isEmpty"})
2323
final class KeyEntry extends SRRset {
24-
private int edeReason;
24+
private int edeReason = -1;
2525
private String badReason;
2626
private boolean isEmpty;
2727

src/main/java/org/xbill/DNS/dnssec/NSEC3ValUtils.java

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
// Copyright (c) 2013-2021 Ingo Bauersachs
44
package org.xbill.DNS.dnssec;
55

6+
import static org.xbill.DNS.ExtendedErrorCodeOption.DNSSEC_BOGUS;
7+
import static org.xbill.DNS.ExtendedErrorCodeOption.NSEC_MISSING;
8+
69
import java.security.NoSuchAlgorithmException;
710
import java.security.interfaces.DSAPublicKey;
811
import java.security.interfaces.ECPublicKey;
@@ -17,6 +20,7 @@
1720
import org.xbill.DNS.DNSKEYRecord;
1821
import org.xbill.DNS.DNSSEC.Algorithm;
1922
import org.xbill.DNS.DNSSEC.DNSSECException;
23+
import org.xbill.DNS.ExtendedErrorCodeOption;
2024
import org.xbill.DNS.NSEC3Record;
2125
import org.xbill.DNS.NSEC3Record.Flags;
2226
import org.xbill.DNS.Name;
@@ -492,38 +496,43 @@ public SecurityStatus proveNameError(List<SRRset> nsec3s, Name qname, Name zonen
492496
* @return {@link SecurityStatus#SECURE} if the NSEC3s prove the proposition, {@link
493497
* SecurityStatus#INSECURE} if qname is under opt-out, {@link SecurityStatus#BOGUS} otherwise.
494498
*/
495-
public SecurityStatus proveNodata(List<SRRset> nsec3s, Name qname, int qtype, Name zonename) {
499+
public JustifiedSecStatus proveNodata(List<SRRset> nsec3s, Name qname, int qtype, Name zonename) {
496500
if (nsec3s == null || nsec3s.isEmpty()) {
497-
return SecurityStatus.BOGUS;
501+
return new JustifiedSecStatus(
502+
SecurityStatus.BOGUS, ExtendedErrorCodeOption.NSEC_MISSING, R.get("failed.nsec3.none"));
498503
}
499504

500505
NSEC3Record nsec3 = this.findMatchingNSEC3(qname, zonename, nsec3s);
501506
// Cases 1 & 2.
502507
if (nsec3 != null) {
503508
if (nsec3.hasType(qtype)) {
504509
log.debug("proveNodata: Matching NSEC3 proved that type existed!");
505-
return SecurityStatus.BOGUS;
510+
return new JustifiedSecStatus(
511+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.type_exists"));
506512
}
507513

508514
if (nsec3.hasType(Type.CNAME)) {
509515
log.debug("proveNodata: Matching NSEC3 proved that a CNAME existed!");
510-
return SecurityStatus.BOGUS;
516+
return new JustifiedSecStatus(
517+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.cname_exists"));
511518
}
512519

513520
if (qtype == Type.DS && nsec3.hasType(Type.SOA) && !Name.root.equals(qname)) {
514521
log.debug("proveNodata: apex NSEC3 abused for no DS proof, bogus");
515-
return SecurityStatus.BOGUS;
522+
return new JustifiedSecStatus(
523+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.apex_abuse"));
516524
} else if (qtype != Type.DS && nsec3.hasType(Type.NS) && !nsec3.hasType(Type.SOA)) {
517525
if (!nsec3.hasType(Type.DS)) {
518526
log.debug("proveNodata: matching NSEC3 is insecure delegation");
519-
return SecurityStatus.INSECURE;
527+
return new JustifiedSecStatus(SecurityStatus.INSECURE, -1, null);
520528
}
521529

522530
log.debug("proveNodata: matching NSEC3 is a delegation, bogus");
523-
return SecurityStatus.BOGUS;
531+
return new JustifiedSecStatus(
532+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.delegation"));
524533
}
525534

526-
return SecurityStatus.SECURE;
535+
return new JustifiedSecStatus(SecurityStatus.SECURE, -1, null);
527536
}
528537

529538
// For cases 3 - 5, we need the proven closest encloser, and it can't
@@ -534,11 +543,12 @@ public SecurityStatus proveNodata(List<SRRset> nsec3s, Name qname, int qtype, Na
534543
// At this point, not finding a match or a proven closest encloser is a
535544
// problem.
536545
if (ce.status == SecurityStatus.BOGUS) {
537-
log.debug("proveNodata: did not match qname, nor found a proven closest encloser.");
538-
return SecurityStatus.BOGUS;
546+
log.debug("proveNodata: did not match qname, nor found a proven closest encloser");
547+
return new JustifiedSecStatus(
548+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.qname_ce"));
539549
} else if (ce.status == SecurityStatus.INSECURE && qtype != Type.DS) {
540-
log.debug("proveNodata: closest nsec3 is insecure delegation.");
541-
return SecurityStatus.INSECURE;
550+
log.debug("proveNodata: closest nsec3 is insecure delegation");
551+
return new JustifiedSecStatus(SecurityStatus.INSECURE, -1, null);
542552
}
543553

544554
// Case 3: REMOVED
@@ -549,26 +559,30 @@ public SecurityStatus proveNodata(List<SRRset> nsec3s, Name qname, int qtype, Na
549559
if (nsec3 != null) {
550560
if (nsec3.hasType(qtype)) {
551561
log.debug("proveNodata: matching wildcard had qtype!");
552-
return SecurityStatus.BOGUS;
562+
return new JustifiedSecStatus(
563+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.type_exists_wc"));
553564
} else if (nsec3.hasType(Type.CNAME)) {
554565
log.debug("nsec3 nodata proof: matching wildcard had a CNAME, bogus");
555-
return SecurityStatus.BOGUS;
566+
return new JustifiedSecStatus(
567+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.cname_exists_wc"));
556568
}
557569

558570
if (qtype == Type.DS && qname.labels() != 1 && nsec3.hasType(Type.SOA)) {
559571
log.debug("nsec3 nodata proof: matching wildcard for no DS proof has a SOA, bogus");
560-
return SecurityStatus.BOGUS;
572+
return new JustifiedSecStatus(
573+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.wc_soa"));
561574
} else if (qtype != Type.DS && nsec3.hasType(Type.NS) && !nsec3.hasType(Type.SOA)) {
562575
log.debug("nsec3 nodata proof: matching wilcard is a delegation, bogus");
563-
return SecurityStatus.BOGUS;
576+
return new JustifiedSecStatus(
577+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.delegation_wc"));
564578
}
565579

566580
if (ce.ncNsec3 != null && (ce.ncNsec3.getFlags() & Flags.OPT_OUT) == Flags.OPT_OUT) {
567581
log.debug("nsec3 nodata proof: matching wildcard is in optout range, insecure");
568-
return SecurityStatus.INSECURE;
582+
return new JustifiedSecStatus(SecurityStatus.INSECURE, -1, null);
569583
}
570584

571-
return SecurityStatus.SECURE;
585+
return new JustifiedSecStatus(SecurityStatus.SECURE, -1, null);
572586
}
573587

574588
// Case 5.
@@ -577,24 +591,27 @@ public SecurityStatus proveNodata(List<SRRset> nsec3s, Name qname, int qtype, Na
577591
// insecure delegation under an optout here */
578592
if (ce.ncNsec3 == null) {
579593
log.debug("nsec3 nodata proof: no next closer nsec3");
580-
return SecurityStatus.BOGUS;
594+
return new JustifiedSecStatus(
595+
SecurityStatus.BOGUS, NSEC_MISSING, R.get("failed.nsec3.no_next"));
581596
}
582597

583598
// We need to make sure that the covering NSEC3 is opt-out.
584599
if ((ce.ncNsec3.getFlags() & Flags.OPT_OUT) == 0) {
585600
if (qtype != Type.DS) {
586601
log.debug(
587-
"proveNodata: covering NSEC3 was not opt-out in an opt-out DS NOERROR/NODATA case.");
602+
"proveNodata: covering NSEC3 was not opt-out in an opt-out DS NOERROR/NODATA case");
603+
return new JustifiedSecStatus(
604+
SecurityStatus.BOGUS, DNSSEC_BOGUS, R.get("failed.nsec3.not_optout"));
588605
} else {
589606
log.debug(
590-
"proveNodata: could not find matching NSEC3, nor matching wildcard, and qtype is not DS -- no more options.");
607+
"proveNodata: could not find matching NSEC3, nor matching wildcard, and qtype is not DS -- no more options");
608+
return new JustifiedSecStatus(
609+
SecurityStatus.BOGUS, NSEC_MISSING, R.get("failed.nsec3.not_found"));
591610
}
592-
593-
return SecurityStatus.BOGUS;
594611
}
595612

596613
// RFC5155 section 9.2: if nc has optout then no AD flag set
597-
return SecurityStatus.INSECURE;
614+
return new JustifiedSecStatus(SecurityStatus.INSECURE, -1, null);
598615
}
599616

600617
/**

src/main/java/org/xbill/DNS/dnssec/ValUtils.java

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.xbill.DNS.DNSSEC.Algorithm;
1515
import org.xbill.DNS.DSRecord;
1616
import org.xbill.DNS.ExtendedErrorCodeOption;
17+
import org.xbill.DNS.Flags;
1718
import org.xbill.DNS.Message;
1819
import org.xbill.DNS.NSECRecord;
1920
import org.xbill.DNS.Name;
@@ -132,7 +133,9 @@ public static ResponseClassification classifyResponse(Message request, SMessage
132133
}
133134

134135
// check for referral: nonRD query and it looks like a nodata
135-
if (m.getCount(Section.ANSWER) == 0 && m.getRcode() != Rcode.NOERROR) {
136+
if (!request.getHeader().getFlag(Flags.RD)
137+
&& m.getCount(Section.ANSWER) == 0
138+
&& m.getRcode() != Rcode.NOERROR) {
136139
// SOA record in auth indicates it is NODATA instead.
137140
// All validation requiring NODATA messages have SOA in
138141
// authority section.
@@ -173,7 +176,7 @@ public static ResponseClassification classifyResponse(Message request, SMessage
173176
}
174177

175178
// Next is NODATA
176-
if (m.getCount(Section.ANSWER) == 0) {
179+
if (m.getRcode() == Rcode.NOERROR && m.getCount(Section.ANSWER) == 0) {
177180
return ResponseClassification.NODATA;
178181
}
179182

@@ -209,7 +212,7 @@ public static ResponseClassification classifyResponse(Message request, SMessage
209212
}
210213
}
211214

212-
log.warn("Failed to classify response message:\n" + m);
215+
log.warn("Failed to classify response message:\n{}", m);
213216
return ResponseClassification.UNKNOWN;
214217
}
215218

@@ -249,6 +252,7 @@ public KeyEntry verifyNewDNSKEYs(
249252
}
250253

251254
int favoriteDigestID = this.favoriteDSDigestID(dsRrset);
255+
KeyEntry ke = null;
252256
for (Record dsr : dsRrset.rrs()) {
253257
DSRecord ds = (DSRecord) dsr;
254258
if (this.digestHardenDowngrade && ds.getDigestID() != favoriteDigestID) {
@@ -264,8 +268,8 @@ public KeyEntry verifyNewDNSKEYs(
264268
continue;
265269
}
266270

267-
KeyEntry ke = getKeyEntry(dnskeyRrset, date, ds, dnskey);
268-
if (ke != null) {
271+
ke = getKeyEntry(dnskeyRrset, date, ds, dnskey);
272+
if (ke.isGood()) {
269273
return ke;
270274
}
271275

@@ -274,9 +278,11 @@ public KeyEntry verifyNewDNSKEYs(
274278
}
275279

276280
// If any were understandable, then it is bad.
277-
KeyEntry badKey = KeyEntry.newBadKeyEntry(dsRrset.getName(), dsRrset.getDClass(), badKeyTTL);
278-
badKey.setBadReason(ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("dnskey.no_ds_match"));
279-
return badKey;
281+
if (ke == null) {
282+
ke = KeyEntry.newBadKeyEntry(dsRrset.getName(), dsRrset.getDClass(), badKeyTTL);
283+
ke.setBadReason(ExtendedErrorCodeOption.DNSKEY_MISSING, R.get("dnskey.no_ds_match"));
284+
}
285+
return ke;
280286
}
281287

282288
private KeyEntry getKeyEntry(SRRset dnskeyRrset, Instant date, DSRecord ds, DNSKEYRecord dnskey) {
@@ -287,25 +293,38 @@ private KeyEntry getKeyEntry(SRRset dnskeyRrset, Instant date, DSRecord ds, DNSK
287293
byte[] dsHash = ds.getDigest();
288294

289295
// see if there is a length mismatch (unlikely)
296+
KeyEntry ke;
290297
if (keyHash.length != dsHash.length) {
291-
return null;
298+
ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL());
299+
ke.setBadReason(ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("dnskey.invalid"));
300+
return ke;
292301
}
293302

294303
for (int k = 0; k < keyHash.length; k++) {
295304
if (keyHash[k] != dsHash[k]) {
296-
return null;
305+
ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL());
306+
ke.setBadReason(ExtendedErrorCodeOption.DNSSEC_BOGUS, R.get("dnskey.invalid"));
307+
return ke;
297308
}
298309
}
299310

300311
// Otherwise, we have a match! Make sure that the DNSKEY
301312
// verifies *with this key*.
302-
SecurityStatus res = this.verifier.verify(dnskeyRrset, dnskey, date);
303-
if (res == SecurityStatus.SECURE) {
304-
log.trace("DS matched DNSKEY.");
305-
dnskeyRrset.setSecurityStatus(SecurityStatus.SECURE);
306-
return KeyEntry.newKeyEntry(dnskeyRrset);
313+
JustifiedSecStatus res = this.verifier.verify(dnskeyRrset, dnskey, date);
314+
switch (res.status) {
315+
case SECURE:
316+
dnskeyRrset.setSecurityStatus(SecurityStatus.SECURE);
317+
ke = KeyEntry.newKeyEntry(dnskeyRrset);
318+
break;
319+
case BOGUS:
320+
ke = KeyEntry.newBadKeyEntry(ds.getName(), ds.getDClass(), ds.getTTL());
321+
ke.setBadReason(res.edeReason, res.reason);
322+
break;
323+
default:
324+
throw new IllegalStateException("Unexpected security status");
307325
}
308-
return null;
326+
327+
return ke;
309328
}
310329

311330
/**
@@ -598,6 +617,7 @@ public static NsecProvesNodataResponse nsecProvesNodata(
598617
if (strictSubdomain(qname, ce)) {
599618
if (nsec.hasType(Type.CNAME)) {
600619
// should have gotten the wildcard CNAME
620+
log.debug("NSEC proofed wildcard CNAME");
601621
result.result = false;
602622
return result;
603623
}
@@ -606,11 +626,13 @@ public static NsecProvesNodataResponse nsecProvesNodata(
606626
// wrong parentside (wildcard) NSEC used, and it really
607627
// should not exist anyway:
608628
// http://tools.ietf.org/html/rfc4592#section-4.2
629+
log.debug("Wrong parent (wildcard) NSEC used");
609630
result.result = false;
610631
return result;
611632
}
612633

613634
if (nsec.hasType(qtype)) {
635+
log.debug("NSEC proofed that {} exists", Type.string(qtype));
614636
result.result = false;
615637
return result;
616638
}
@@ -629,12 +651,14 @@ public static NsecProvesNodataResponse nsecProvesNodata(
629651

630652
// If the qtype exists, then we should have gotten it.
631653
if (nsec.hasType(qtype)) {
654+
log.debug("NSEC proofed that {} exists", Type.string(qtype));
632655
result.result = false;
633656
return result;
634657
}
635658

636659
// if the name is a CNAME node, then we should have gotten the CNAME
637660
if (nsec.hasType(Type.CNAME)) {
661+
log.debug("NSEC proofed CNAME");
638662
result.result = false;
639663
return result;
640664
}
@@ -645,10 +669,12 @@ public static NsecProvesNodataResponse nsecProvesNodata(
645669
// The reverse of this check is used when qtype is DS, since that
646670
// must use the NSEC from above the zone cut.
647671
if (qtype != Type.DS && nsec.hasType(Type.NS) && !nsec.hasType(Type.SOA)) {
672+
log.debug("NSEC proofed missing referral");
648673
result.result = false;
649674
return result;
650675
}
651676
if (qtype == Type.DS && nsec.hasType(Type.SOA) && !Name.root.equals(qname)) {
677+
log.debug("NSEC from wrong zone");
652678
result.result = false;
653679
return result;
654680
}
@@ -680,7 +706,10 @@ public JustifiedSecStatus nsecProvesNodataDsReply(
680706
// The NSEC must verify, first of all.
681707
JustifiedSecStatus res = this.verifySRRset(nsecRrset, keyRrset, date);
682708
if (res.status != SecurityStatus.SECURE) {
683-
return new JustifiedSecStatus(SecurityStatus.BOGUS, res.edeReason, R.get("failed.ds.nsec"));
709+
return new JustifiedSecStatus(
710+
SecurityStatus.BOGUS,
711+
ExtendedErrorCodeOption.DNSSEC_BOGUS,
712+
R.get("failed.ds.nsec", res.reason));
684713
}
685714

686715
NSECRecord nsec = (NSECRecord) nsecRrset.first();

0 commit comments

Comments
 (0)