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}