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