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.List; 044import java.util.Map; 045import java.util.Set; 046import java.util.concurrent.ConcurrentHashMap; 047 048public class DnssecClient extends ReliableDnsClient { 049 050 /** 051 * The root zone's KSK. 052 * The ID of the current key is "Klajeyz", and the key tag value is "20326". 053 */ 054 private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493"); 055 056 private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org"); 057 058 /** 059 * Create a new DNSSEC aware DNS client using the global default cache. 060 */ 061 public DnssecClient() { 062 this(DEFAULT_CACHE); 063 } 064 065 /** 066 * Create a new DNSSEC aware DNS client with the given DNS cache. 067 * 068 * @param cache The backend DNS cache. 069 */ 070 public DnssecClient(DnsCache cache) { 071 super(cache); 072 addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray()); 073 } 074 075 /** 076 * Known secure entry points (SEPs). 077 */ 078 private final Map<DnsName, byte[]> knownSeps = new ConcurrentHashMap<>(); 079 080 private boolean stripSignatureRecords = true; 081 082 /** 083 * The active DNSSEC Look-aside Validation Registry. May be <code>null</code>. 084 */ 085 private DnsName dlv; 086 087 @Override 088 public DnsQueryResult query(Question q) throws IOException { 089 DnssecQueryResult dnssecQueryResult = queryDnssec(q); 090 if (!dnssecQueryResult.isAuthenticData()) { 091 // TODO: Refine exception. 092 throw new IOException(); 093 } 094 return dnssecQueryResult.dnsQueryResult; 095 } 096 097 public DnssecQueryResult queryDnssec(CharSequence name, TYPE type) throws IOException { 098 Question q = new Question(name, type, CLASS.IN); 099 return queryDnssec(q); 100 } 101 102 public DnssecQueryResult queryDnssec(Question q) throws IOException { 103 DnsQueryResult dnsQueryResult = super.query(q); 104 DnssecQueryResult dnssecQueryResult = performVerification(dnsQueryResult); 105 return dnssecQueryResult; 106 } 107 108 private DnssecQueryResult performVerification(DnsQueryResult dnsQueryResult) throws IOException { 109 if (dnsQueryResult == null) return null; 110 111 DnsMessage dnsMessage = dnsQueryResult.response; 112 DnsMessage.Builder messageBuilder = dnsMessage.asBuilder(); 113 114 Set<DnssecUnverifiedReason> unverifiedReasons = verify(dnsMessage); 115 116 messageBuilder.setAuthenticData(unverifiedReasons.isEmpty()); 117 118 List<Record<? extends Data>> answers = dnsMessage.answerSection; 119 List<Record<? extends Data>> nameserverRecords = dnsMessage.authoritySection; 120 List<Record<? extends Data>> additionalResourceRecords = dnsMessage.additionalSection; 121 Set<Record<RRSIG>> signatures = new HashSet<>(); 122 Record.filter(signatures, RRSIG.class, answers); 123 Record.filter(signatures, RRSIG.class, nameserverRecords); 124 Record.filter(signatures, RRSIG.class, additionalResourceRecords); 125 126 if (stripSignatureRecords) { 127 messageBuilder.setAnswers(stripSignatureRecords(answers)); 128 messageBuilder.setNameserverRecords(stripSignatureRecords(nameserverRecords)); 129 messageBuilder.setAdditionalResourceRecords(stripSignatureRecords(additionalResourceRecords)); 130 } 131 132 return new DnssecQueryResult(messageBuilder.build(), dnsQueryResult, signatures, unverifiedReasons); 133 } 134 135 private static List<Record<? extends Data>> stripSignatureRecords(List<Record<? extends Data>> records) { 136 if (records.isEmpty()) return records; 137 List<Record<? extends Data>> recordList = new ArrayList<>(records.size()); 138 for (Record<? extends Data> record : records) { 139 if (record.type != TYPE.RRSIG) { 140 recordList.add(record); 141 } 142 } 143 return recordList; 144 } 145 146 private Set<DnssecUnverifiedReason> verify(DnsMessage dnsMessage) throws IOException { 147 if (!dnsMessage.answerSection.isEmpty()) { 148 return verifyAnswer(dnsMessage); 149 } else { 150 return verifyNsec(dnsMessage); 151 } 152 } 153 154 private Set<DnssecUnverifiedReason> verifyAnswer(DnsMessage dnsMessage) throws IOException { 155 Question q = dnsMessage.questions.get(0); 156 List<Record<? extends Data>> answers = dnsMessage.answerSection; 157 List<Record<? extends Data>> toBeVerified = dnsMessage.copyAnswers(); 158 VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified); 159 Set<DnssecUnverifiedReason> result = verifiedSignatures.reasons; 160 if (!result.isEmpty()) { 161 return result; 162 } 163 164 // Keep SEPs separated, we only need one valid SEP. 165 boolean sepSignatureValid = false; 166 Set<DnssecUnverifiedReason> sepReasons = new HashSet<>(); 167 for (Iterator<Record<? extends Data>> iterator = toBeVerified.iterator(); iterator.hasNext(); ) { 168 Record<DNSKEY> record = iterator.next().ifPossibleAs(DNSKEY.class); 169 if (record == null) { 170 continue; 171 } 172 173 // Verify all DNSKEYs as if it was a SEP. If we find a single SEP we are safe. 174 Set<DnssecUnverifiedReason> reasons = verifySecureEntryPoint(record); 175 if (reasons.isEmpty()) { 176 sepSignatureValid = true; 177 } else { 178 sepReasons.addAll(reasons); 179 } 180 if (!verifiedSignatures.sepSignaturePresent) { 181 LOGGER.finer("SEP key is not self-signed."); 182 } 183 iterator.remove(); 184 } 185 186 if (verifiedSignatures.sepSignaturePresent && !sepSignatureValid) { 187 result.addAll(sepReasons); 188 } 189 if (verifiedSignatures.sepSignatureRequired && !verifiedSignatures.sepSignaturePresent) { 190 result.add(new NoSecureEntryPointReason(q.name)); 191 } 192 if (!toBeVerified.isEmpty()) { 193 if (toBeVerified.size() != answers.size()) { 194 throw new DnssecValidationFailedException(q, "Only some records are signed!"); 195 } else { 196 result.add(new NoSignaturesReason(q)); 197 } 198 } 199 return result; 200 } 201 202 private Set<DnssecUnverifiedReason> verifyNsec(DnsMessage dnsMessage) throws IOException { 203 Set<DnssecUnverifiedReason> result = new HashSet<>(); 204 Question q = dnsMessage.questions.get(0); 205 boolean validNsec = false; 206 boolean nsecPresent = false; 207 208 // Get the SOA RR that has to be in the authority section. Note that we will verify its signature later, after 209 // we have verified the NSEC3 RR. And although the data form the SOA RR is only required for NSEC3 we check for 210 // its existence here, since it would be invalid if there is none. 211 // TODO: Add a reference to the relevant RFC parts which specify that there has to be a SOA RR in X. 212 DnsName zone = null; 213 List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection; 214 for (Record<? extends Data> authorityRecord : authoritySection) { 215 if (authorityRecord.type == TYPE.SOA) { 216 zone = authorityRecord.name; 217 break; 218 } 219 } 220 if (zone == null) 221 throw new AuthorityDoesNotContainSoa(dnsMessage); 222 223 // TODO Examine if it is better to verify the RRs in the authority section *before* we verify NSEC(3). We 224 // currently do it the other way around. 225 226 // TODO: This whole logic needs to be changed. It currently checks one NSEC(3) record after another, when it 227 // should first determine if we are dealing with NSEC or NSEC3 and the verify the whole response. 228 for (Record<? extends Data> record : authoritySection) { 229 DnssecUnverifiedReason reason; 230 231 switch (record.type) { 232 case NSEC: 233 nsecPresent = true; 234 Record<NSEC> nsecRecord = record.as(NSEC.class); 235 reason = Verifier.verifyNsec(nsecRecord, q); 236 break; 237 case NSEC3: 238 nsecPresent = true; 239 Record<NSEC3> nsec3Record = record.as(NSEC3.class); 240 reason = Verifier.verifyNsec3(zone, nsec3Record, q); 241 break; 242 default: 243 continue; 244 } 245 246 if (reason != null) { 247 result.add(reason); 248 } else { 249 validNsec = true; 250 } 251 } 252 253 // TODO: Shouldn't we also throw if !nsecPresent? 254 if (nsecPresent && !validNsec) { 255 throw new DnssecValidationFailedException(q, "Invalid NSEC!"); 256 } 257 258 List<Record<? extends Data>> toBeVerified = dnsMessage.copyAuthority(); 259 VerifySignaturesResult verifiedSignatures = verifySignatures(q, authoritySection, toBeVerified); 260 if (validNsec && verifiedSignatures.reasons.isEmpty()) { 261 result.clear(); 262 } else { 263 result.addAll(verifiedSignatures.reasons); 264 } 265 266 if (!toBeVerified.isEmpty() && toBeVerified.size() != authoritySection.size()) { 267 // TODO Refine this exception and include the missing toBeVerified RRs and the whole DnsMessage into it. 268 throw new DnssecValidationFailedException(q, "Only some resource records from the authority section are signed!"); 269 } 270 271 return result; 272 } 273 274 private static final class VerifySignaturesResult { 275 boolean sepSignatureRequired = false; 276 boolean sepSignaturePresent = false; 277 Set<DnssecUnverifiedReason> reasons = new HashSet<>(); 278 } 279 280 @SuppressWarnings("JavaUtilDate") 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 ArrayList<>(); 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}