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}