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}