001/* 002 * Copyright 2015-2024 the original author or authors 003 * 004 * This software is licensed under the Apache License, Version 2.0, 005 * the GNU Lesser General Public License version 2 or later ("LGPL") 006 * and the WTFPL. 007 * You may choose either license to govern your use of this software only 008 * upon the condition that you accept all of the terms of either 009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. 010 */ 011package org.minidns.dnssec; 012 013import org.minidns.DnsCache; 014import org.minidns.dnsmessage.DnsMessage; 015import org.minidns.dnsmessage.Question; 016import org.minidns.dnsname.DnsName; 017import org.minidns.dnsqueryresult.DnsQueryResult; 018import org.minidns.dnssec.DnssecUnverifiedReason.NoActiveSignaturesReason; 019import org.minidns.dnssec.DnssecUnverifiedReason.NoSecureEntryPointReason; 020import org.minidns.dnssec.DnssecUnverifiedReason.NoSignaturesReason; 021import org.minidns.dnssec.DnssecUnverifiedReason.NoTrustAnchorReason; 022import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa; 023import org.minidns.iterative.ReliableDnsClient; 024import org.minidns.record.DLV; 025import org.minidns.record.DNSKEY; 026import org.minidns.record.DS; 027import org.minidns.record.Data; 028import org.minidns.record.DelegatingDnssecRR; 029import org.minidns.record.NSEC; 030import org.minidns.record.NSEC3; 031import org.minidns.record.RRSIG; 032import org.minidns.record.Record; 033import org.minidns.record.Record.CLASS; 034import org.minidns.record.Record.TYPE; 035 036import java.io.IOException; 037import java.math.BigInteger; 038import java.util.ArrayList; 039import java.util.Collection; 040import java.util.Date; 041import java.util.HashSet; 042import java.util.Iterator; 043import java.util.LinkedList; 044import java.util.List; 045import java.util.Map; 046import java.util.Set; 047import java.util.concurrent.ConcurrentHashMap; 048 049public class DnssecClient extends ReliableDnsClient { 050 051 /** 052 * The root zone's KSK. 053 * The ID of the current key is "Klajeyz", and the key tag value is "20326". 054 */ 055 private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493"); 056 057 private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org"); 058 059 /** 060 * Create a new DNSSEC aware DNS client using the global default cache. 061 */ 062 public DnssecClient() { 063 this(DEFAULT_CACHE); 064 } 065 066 /** 067 * Create a new DNSSEC aware DNS client with the given DNS cache. 068 * 069 * @param cache The backend DNS cache. 070 */ 071 public DnssecClient(DnsCache cache) { 072 super(cache); 073 addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray()); 074 } 075 076 /** 077 * Known secure entry points (SEPs). 078 */ 079 private final Map<DnsName, byte[]> knownSeps = new ConcurrentHashMap<>(); 080 081 private boolean stripSignatureRecords = true; 082 083 /** 084 * The active DNSSEC Look-aside Validation Registry. May be <code>null</code>. 085 */ 086 private DnsName dlv; 087 088 @Override 089 public DnsQueryResult query(Question q) throws IOException { 090 DnssecQueryResult dnssecQueryResult = queryDnssec(q); 091 if (!dnssecQueryResult.isAuthenticData()) { 092 // TODO: Refine exception. 093 throw new IOException(); 094 } 095 return dnssecQueryResult.dnsQueryResult; 096 } 097 098 public DnssecQueryResult queryDnssec(CharSequence name, TYPE type) throws IOException { 099 Question q = new Question(name, type, CLASS.IN); 100 return queryDnssec(q); 101 } 102 103 public DnssecQueryResult queryDnssec(Question q) throws IOException { 104 DnsQueryResult dnsQueryResult = super.query(q); 105 DnssecQueryResult dnssecQueryResult = performVerification(dnsQueryResult); 106 return dnssecQueryResult; 107 } 108 109 private DnssecQueryResult performVerification(DnsQueryResult dnsQueryResult) throws IOException { 110 if (dnsQueryResult == null) return null; 111 112 DnsMessage dnsMessage = dnsQueryResult.response; 113 DnsMessage.Builder messageBuilder = dnsMessage.asBuilder(); 114 115 Set<DnssecUnverifiedReason> unverifiedReasons = verify(dnsMessage); 116 117 messageBuilder.setAuthenticData(unverifiedReasons.isEmpty()); 118 119 List<Record<? extends Data>> answers = dnsMessage.answerSection; 120 List<Record<? extends Data>> nameserverRecords = dnsMessage.authoritySection; 121 List<Record<? extends Data>> additionalResourceRecords = dnsMessage.additionalSection; 122 Set<Record<RRSIG>> signatures = new HashSet<>(); 123 Record.filter(signatures, RRSIG.class, answers); 124 Record.filter(signatures, RRSIG.class, nameserverRecords); 125 Record.filter(signatures, RRSIG.class, additionalResourceRecords); 126 127 if (stripSignatureRecords) { 128 messageBuilder.setAnswers(stripSignatureRecords(answers)); 129 messageBuilder.setNameserverRecords(stripSignatureRecords(nameserverRecords)); 130 messageBuilder.setAdditionalResourceRecords(stripSignatureRecords(additionalResourceRecords)); 131 } 132 133 return new DnssecQueryResult(messageBuilder.build(), dnsQueryResult, signatures, unverifiedReasons); 134 } 135 136 private static List<Record<? extends Data>> stripSignatureRecords(List<Record<? extends Data>> records) { 137 if (records.isEmpty()) return records; 138 List<Record<? extends Data>> recordList = new ArrayList<>(records.size()); 139 for (Record<? extends Data> record : records) { 140 if (record.type != TYPE.RRSIG) { 141 recordList.add(record); 142 } 143 } 144 return recordList; 145 } 146 147 private Set<DnssecUnverifiedReason> verify(DnsMessage dnsMessage) throws IOException { 148 if (!dnsMessage.answerSection.isEmpty()) { 149 return verifyAnswer(dnsMessage); 150 } else { 151 return verifyNsec(dnsMessage); 152 } 153 } 154 155 private Set<DnssecUnverifiedReason> verifyAnswer(DnsMessage dnsMessage) throws IOException { 156 Question q = dnsMessage.questions.get(0); 157 List<Record<? extends Data>> answers = dnsMessage.answerSection; 158 List<Record<? extends Data>> toBeVerified = dnsMessage.copyAnswers(); 159 VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified); 160 Set<DnssecUnverifiedReason> result = verifiedSignatures.reasons; 161 if (!result.isEmpty()) { 162 return result; 163 } 164 165 // Keep SEPs separated, we only need one valid SEP. 166 boolean sepSignatureValid = false; 167 Set<DnssecUnverifiedReason> sepReasons = new HashSet<>(); 168 for (Iterator<Record<? extends Data>> iterator = toBeVerified.iterator(); iterator.hasNext(); ) { 169 Record<DNSKEY> record = iterator.next().ifPossibleAs(DNSKEY.class); 170 if (record == null) { 171 continue; 172 } 173 174 // Verify all DNSKEYs as if it was a SEP. If we find a single SEP we are safe. 175 Set<DnssecUnverifiedReason> reasons = verifySecureEntryPoint(record); 176 if (reasons.isEmpty()) { 177 sepSignatureValid = true; 178 } else { 179 sepReasons.addAll(reasons); 180 } 181 if (!verifiedSignatures.sepSignaturePresent) { 182 LOGGER.finer("SEP key is not self-signed."); 183 } 184 iterator.remove(); 185 } 186 187 if (verifiedSignatures.sepSignaturePresent && !sepSignatureValid) { 188 result.addAll(sepReasons); 189 } 190 if (verifiedSignatures.sepSignatureRequired && !verifiedSignatures.sepSignaturePresent) { 191 result.add(new NoSecureEntryPointReason(q.name)); 192 } 193 if (!toBeVerified.isEmpty()) { 194 if (toBeVerified.size() != answers.size()) { 195 throw new DnssecValidationFailedException(q, "Only some records are signed!"); 196 } else { 197 result.add(new NoSignaturesReason(q)); 198 } 199 } 200 return result; 201 } 202 203 private Set<DnssecUnverifiedReason> verifyNsec(DnsMessage dnsMessage) throws IOException { 204 Set<DnssecUnverifiedReason> result = new HashSet<>(); 205 Question q = dnsMessage.questions.get(0); 206 boolean validNsec = false; 207 boolean nsecPresent = false; 208 209 // Get the SOA RR that has to be in the authority section. Note that we will verify its signature later, after 210 // we have verified the NSEC3 RR. And although the data form the SOA RR is only required for NSEC3 we check for 211 // its existence here, since it would be invalid if there is none. 212 // TODO: Add a reference to the relevant RFC parts which specify that there has to be a SOA RR in X. 213 DnsName zone = null; 214 List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection; 215 for (Record<? extends Data> authorityRecord : authoritySection) { 216 if (authorityRecord.type == TYPE.SOA) { 217 zone = authorityRecord.name; 218 break; 219 } 220 } 221 if (zone == null) 222 throw new AuthorityDoesNotContainSoa(dnsMessage); 223 224 // TODO Examine if it is better to verify the RRs in the authority section *before* we verify NSEC(3). We 225 // currently do it the other way around. 226 227 // TODO: This whole logic needs to be changed. It currently checks one NSEC(3) record after another, when it 228 // should first determine if we are dealing with NSEC or NSEC3 and the verify the whole response. 229 for (Record<? extends Data> record : authoritySection) { 230 DnssecUnverifiedReason reason; 231 232 switch (record.type) { 233 case NSEC: 234 nsecPresent = true; 235 Record<NSEC> nsecRecord = record.as(NSEC.class); 236 reason = Verifier.verifyNsec(nsecRecord, q); 237 break; 238 case NSEC3: 239 nsecPresent = true; 240 Record<NSEC3> nsec3Record = record.as(NSEC3.class); 241 reason = Verifier.verifyNsec3(zone, nsec3Record, q); 242 break; 243 default: 244 continue; 245 } 246 247 if (reason != null) { 248 result.add(reason); 249 } else { 250 validNsec = true; 251 } 252 } 253 254 // TODO: Shouldn't we also throw if !nsecPresent? 255 if (nsecPresent && !validNsec) { 256 throw new DnssecValidationFailedException(q, "Invalid NSEC!"); 257 } 258 259 List<Record<? extends Data>> toBeVerified = dnsMessage.copyAuthority(); 260 VerifySignaturesResult verifiedSignatures = verifySignatures(q, authoritySection, toBeVerified); 261 if (validNsec && verifiedSignatures.reasons.isEmpty()) { 262 result.clear(); 263 } else { 264 result.addAll(verifiedSignatures.reasons); 265 } 266 267 if (!toBeVerified.isEmpty() && toBeVerified.size() != authoritySection.size()) { 268 // TODO Refine this exception and include the missing toBeVerified RRs and the whole DnsMessage into it. 269 throw new DnssecValidationFailedException(q, "Only some resource records from the authority section are signed!"); 270 } 271 272 return result; 273 } 274 275 private static class VerifySignaturesResult { 276 boolean sepSignatureRequired = false; 277 boolean sepSignaturePresent = false; 278 Set<DnssecUnverifiedReason> reasons = new HashSet<>(); 279 } 280 281 private VerifySignaturesResult verifySignatures(Question q, Collection<Record<? extends Data>> reference, List<Record<? extends Data>> toBeVerified) throws IOException { 282 final Date now = new Date(); 283 final List<RRSIG> outdatedRrSigs = new LinkedList<>(); 284 VerifySignaturesResult result = new VerifySignaturesResult(); 285 final List<Record<RRSIG>> rrsigs = new ArrayList<>(toBeVerified.size()); 286 287 for (Record<? extends Data> recordToBeVerified : toBeVerified) { 288 Record<RRSIG> record = recordToBeVerified.ifPossibleAs(RRSIG.class); 289 if (record == null) continue; 290 291 RRSIG rrsig = record.payloadData; 292 if (rrsig.signatureExpiration.compareTo(now) < 0 || rrsig.signatureInception.compareTo(now) > 0) { 293 // This RRSIG is out of date, but there might be one that is not. 294 outdatedRrSigs.add(rrsig); 295 continue; 296 } 297 rrsigs.add(record); 298 } 299 300 if (rrsigs.isEmpty()) { 301 if (!outdatedRrSigs.isEmpty()) { 302 result.reasons.add(new NoActiveSignaturesReason(q, outdatedRrSigs)); 303 } else { 304 // TODO: Check if QNAME results should have signatures and add a different reason if there are RRSIGs 305 // expected compared to when not. 306 result.reasons.add(new NoSignaturesReason(q)); 307 } 308 return result; 309 } 310 311 for (Record<RRSIG> sigRecord : rrsigs) { 312 RRSIG rrsig = sigRecord.payloadData; 313 314 List<Record<? extends Data>> records = new ArrayList<>(reference.size()); 315 for (Record<? extends Data> record : reference) { 316 if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) { 317 records.add(record); 318 } 319 } 320 321 Set<DnssecUnverifiedReason> reasons = verifySignedRecords(q, rrsig, records); 322 result.reasons.addAll(reasons); 323 324 if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) { 325 for (Iterator<Record<? extends Data>> iterator = records.iterator(); iterator.hasNext(); ) { 326 Record<DNSKEY> dnsKeyRecord = iterator.next().ifPossibleAs(DNSKEY.class); 327 // dnsKeyRecord should never be null here. 328 DNSKEY dnskey = dnsKeyRecord.payloadData; 329 // DNSKEYs are verified separately, so don't mark them verified now. 330 iterator.remove(); 331 if (dnskey.getKeyTag() == rrsig.keyTag) { 332 result.sepSignaturePresent = true; 333 } 334 } 335 // DNSKEY's should be signed by a SEP 336 result.sepSignatureRequired = true; 337 } 338 339 if (!isParentOrSelf(sigRecord.name.ace, rrsig.signerName.ace)) { 340 LOGGER.finer("Records at " + sigRecord.name + " are cross-signed with a key from " + rrsig.signerName); 341 } else { 342 toBeVerified.removeAll(records); 343 } 344 toBeVerified.remove(sigRecord); 345 } 346 return result; 347 } 348 349 private static boolean isParentOrSelf(String child, String parent) { 350 if (child.equals(parent)) return true; 351 if (parent.isEmpty()) return true; 352 String[] childSplit = child.split("\\."); 353 String[] parentSplit = parent.split("\\."); 354 if (parentSplit.length > childSplit.length) return false; 355 for (int i = 1; i <= parentSplit.length; i++) { 356 if (!parentSplit[parentSplit.length - i].equals(childSplit[childSplit.length - i])) { 357 return false; 358 } 359 } 360 return true; 361 } 362 363 private Set<DnssecUnverifiedReason> verifySignedRecords(Question q, RRSIG rrsig, List<Record<? extends Data>> records) throws IOException { 364 Set<DnssecUnverifiedReason> result = new HashSet<>(); 365 DNSKEY dnskey = null; 366 367 if (rrsig.typeCovered == TYPE.DNSKEY) { 368 // Key must be present 369 List<Record<DNSKEY>> dnskeyRrs = Record.filter(DNSKEY.class, records); 370 for (Record<DNSKEY> dnsKeyRecord : dnskeyRrs) { 371 if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { 372 dnskey = dnsKeyRecord.payloadData; 373 break; 374 } 375 } 376 } else if (q.type == TYPE.DS && rrsig.signerName.equals(q.name)) { 377 // We should not probe for the self signed DS negative response, as it will be an endless loop. 378 result.add(new NoTrustAnchorReason(q.name)); 379 return result; 380 } else { 381 DnssecQueryResult dnskeyRes = queryDnssec(rrsig.signerName, TYPE.DNSKEY); 382 result.addAll(dnskeyRes.getUnverifiedReasons()); 383 List<Record<DNSKEY>> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class); 384 for (Record<DNSKEY> dnsKeyRecord : dnskeyRrs) { 385 if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { 386 dnskey = dnsKeyRecord.payloadData; 387 break; 388 } 389 } 390 } 391 392 if (dnskey == null) { 393 throw new DnssecValidationFailedException(q, records.size() + " " + rrsig.typeCovered + " record(s) are signed using an unknown key."); 394 } 395 396 DnssecUnverifiedReason unverifiedReason = Verifier.verify(records, rrsig, dnskey); 397 if (unverifiedReason != null) { 398 result.add(unverifiedReason); 399 } 400 401 return result; 402 } 403 404 private Set<DnssecUnverifiedReason> verifySecureEntryPoint(final Record<DNSKEY> sepRecord) throws IOException { 405 final DNSKEY dnskey = sepRecord.payloadData; 406 407 Set<DnssecUnverifiedReason> unverifiedReasons = new HashSet<>(); 408 Set<DnssecUnverifiedReason> activeReasons = new HashSet<>(); 409 if (knownSeps.containsKey(sepRecord.name)) { 410 if (dnskey.keyEquals(knownSeps.get(sepRecord.name))) { 411 return unverifiedReasons; 412 } else { 413 unverifiedReasons.add(new DnssecUnverifiedReason.ConflictsWithSep(sepRecord)); 414 return unverifiedReasons; 415 } 416 } 417 418 // If we are looking for the SEP of the root zone at this point, then the client was not 419 // configured with one. Hence we can abort and state the reason why we aborted. 420 if (sepRecord.name.isRootLabel()) { 421 unverifiedReasons.add(new DnssecUnverifiedReason.NoRootSecureEntryPointReason()); 422 return unverifiedReasons; 423 } 424 425 DelegatingDnssecRR delegation = null; 426 DnssecQueryResult dsResp = queryDnssec(sepRecord.name, TYPE.DS); 427 unverifiedReasons.addAll(dsResp.getUnverifiedReasons()); 428 429 List<Record<DS>> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class); 430 for (Record<DS> dsRecord : dsRrs) { 431 DS ds = dsRecord.payloadData; 432 if (dnskey.getKeyTag() == ds.keyTag) { 433 delegation = ds; 434 activeReasons = dsResp.getUnverifiedReasons(); 435 break; 436 } 437 } 438 439 if (delegation == null) { 440 LOGGER.fine("There is no DS record for \'" + sepRecord.name + "\', server gives empty result"); 441 } 442 443 if (delegation == null && dlv != null && !dlv.isChildOf(sepRecord.name)) { 444 DnssecQueryResult dlvResp = queryDnssec(DnsName.from(sepRecord.name, dlv), TYPE.DLV); 445 unverifiedReasons.addAll(dlvResp.getUnverifiedReasons()); 446 447 List<Record<DLV>> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class); 448 for (Record<DLV> dlvRecord : dlvRrs) { 449 if (sepRecord.payloadData.getKeyTag() == dlvRecord.payloadData.keyTag) { 450 LOGGER.fine("Found DLV for " + sepRecord.name + ", awesome."); 451 delegation = dlvRecord.payloadData; 452 activeReasons = dlvResp.getUnverifiedReasons(); 453 break; 454 } 455 } 456 } 457 458 if (delegation != null) { 459 DnssecUnverifiedReason unverifiedReason = Verifier.verify(sepRecord, delegation); 460 if (unverifiedReason != null) { 461 unverifiedReasons.add(unverifiedReason); 462 } else { 463 unverifiedReasons = activeReasons; 464 } 465 } else if (unverifiedReasons.isEmpty()) { 466 unverifiedReasons.add(new NoTrustAnchorReason(sepRecord.name)); 467 } 468 return unverifiedReasons; 469 } 470 471 @Override 472 protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { 473 message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(); 474 message.setCheckingDisabled(true); 475 return super.newQuestion(message); 476 } 477 478 @Override 479 protected String isResponseAcceptable(DnsMessage response) { 480 boolean dnssecOk = response.isDnssecOk(); 481 if (!dnssecOk) { 482 // This is a deliberate violation of RFC 6840 ยง 5.6. I doubt that 483 // "resolvers MUST ignore the DO bit in responses" does any good. Also we basically ignore the DO bit after 484 // the fall back to iterative mode. 485 return "DNSSEC OK (DO) flag not set in response"; 486 } 487 boolean checkingDisabled = response.checkingDisabled; 488 if (!checkingDisabled) { 489 return "CHECKING DISABLED (CD) flag not set in response"; 490 } 491 return super.isResponseAcceptable(response); 492 } 493 494 /** 495 * Add a new secure entry point to the list of known secure entry points. 496 * 497 * A secure entry point acts as a trust anchor. By default, the only secure entry point is the key signing key 498 * provided by the root zone. 499 * 500 * @param name The domain name originating the key. Once the secure entry point for this domain is requested, 501 * the resolver will use this key without further verification instead of using the DNS system to 502 * verify the key. 503 * @param key The secure entry point corresponding to the domain name. This key can be retrieved by requesting 504 * the DNSKEY record for the domain and using the key with first flags bit set 505 * (also called key signing key) 506 */ 507 public final void addSecureEntryPoint(DnsName name, byte[] key) { 508 knownSeps.put(name, key); 509 } 510 511 /** 512 * Remove the secure entry point stored for a domain name. 513 * 514 * @param name The domain name of which the corresponding secure entry point shall be removed. For the root zone, 515 * use the empty string here. 516 */ 517 public void removeSecureEntryPoint(DnsName name) { 518 knownSeps.remove(name); 519 } 520 521 /** 522 * Clears the list of known secure entry points. 523 * 524 * This will also remove the secure entry point of the root zone and 525 * thus render this instance useless until a new secure entry point is added. 526 */ 527 public void clearSecureEntryPoints() { 528 knownSeps.clear(); 529 } 530 531 /** 532 * Whether signature records (RRSIG) are stripped from the resulting {@link DnsMessage}. 533 * 534 * Default is {@code true}. 535 * 536 * @return Whether signature records are stripped. 537 */ 538 public boolean isStripSignatureRecords() { 539 return stripSignatureRecords; 540 } 541 542 /** 543 * Enable or disable stripping of signature records (RRSIG) from the result {@link DnsMessage}. 544 * @param stripSignatureRecords Whether signature records shall be stripped. 545 */ 546 public void setStripSignatureRecords(boolean stripSignatureRecords) { 547 this.stripSignatureRecords = stripSignatureRecords; 548 } 549 550 /** 551 * Enables DNSSEC Lookaside Validation (DLV) using the default DLV service at dlv.isc.org. 552 */ 553 public void enableLookasideValidation() { 554 configureLookasideValidation(DEFAULT_DLV); 555 } 556 557 /** 558 * Disables DNSSEC Lookaside Validation (DLV). 559 * DLV is disabled by default, this is only required if {@link #enableLookasideValidation()} was used before. 560 */ 561 public void disableLookasideValidation() { 562 configureLookasideValidation(null); 563 } 564 565 /** 566 * Enables DNSSEC Lookaside Validation (DLV) using the given DLV service. 567 * 568 * @param dlv The domain name of the DLV service to be used or {@code null} to disable DLV. 569 */ 570 public void configureLookasideValidation(DnsName dlv) { 571 this.dlv = dlv; 572 } 573}