mirror of
https://github.com/sparrowwallet/drongo.git
synced 2025-11-05 11:56:38 +00:00
improve dnssec validation for cnames, wildcards and overrides
This commit is contained in:
parent
58cc096f8e
commit
056d5f83a6
7 changed files with 317 additions and 89 deletions
|
|
@ -0,0 +1,59 @@
|
||||||
|
package com.sparrowwallet.drongo.dns;
|
||||||
|
|
||||||
|
import org.xbill.DNS.*;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletionStage;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
public class AuthenticatingResolver implements Resolver {
|
||||||
|
private final Resolver delegate;
|
||||||
|
private boolean authenticated;
|
||||||
|
|
||||||
|
public AuthenticatingResolver(Resolver delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPort(int port) {
|
||||||
|
delegate.setPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTCP(boolean flag) {
|
||||||
|
delegate.setTCP(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIgnoreTruncation(boolean flag) {
|
||||||
|
delegate.setIgnoreTruncation(flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEDNS(int version, int payloadSize, int flags, List<EDNSOption> options) {
|
||||||
|
delegate.setEDNS(version, payloadSize, flags, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTSIGKey(TSIG key) {
|
||||||
|
delegate.setTSIGKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
delegate.setTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||||
|
return delegate.sendAsync(query, executor).thenApply(response -> {
|
||||||
|
this.authenticated = response.getHeader().getFlag(Flags.AD);
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return authenticated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.drongo.dns;
|
package com.sparrowwallet.drongo.dns;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
@ -7,12 +8,17 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.xbill.DNS.*;
|
import org.xbill.DNS.*;
|
||||||
import org.xbill.DNS.Record;
|
import org.xbill.DNS.Record;
|
||||||
import org.xbill.DNS.dnssec.ValidatingResolver;
|
import org.xbill.DNS.dnssec.ValidatingResolver;
|
||||||
|
import org.xbill.DNS.lookup.LookupResult;
|
||||||
|
import org.xbill.DNS.lookup.LookupSession;
|
||||||
|
import org.xbill.DNS.lookup.NoSuchDomainException;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Clock;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.uri.BitcoinURI.BITCOIN_SCHEME;
|
import static com.sparrowwallet.drongo.uri.BitcoinURI.BITCOIN_SCHEME;
|
||||||
|
|
||||||
|
|
@ -27,8 +33,13 @@ public class DnsPaymentResolver {
|
||||||
|
|
||||||
private final String hrn;
|
private final String hrn;
|
||||||
private final String domain;
|
private final String domain;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
public DnsPaymentResolver(String hrn) {
|
public DnsPaymentResolver(String hrn) {
|
||||||
|
this(hrn, Clock.systemUTC());
|
||||||
|
}
|
||||||
|
|
||||||
|
public DnsPaymentResolver(String hrn, Clock clock) {
|
||||||
if(!StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) {
|
if(!StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) {
|
||||||
throw new IllegalArgumentException("Invalid HRN containing non-ASCII characters: " + hrn);
|
throw new IllegalArgumentException("Invalid HRN containing non-ASCII characters: " + hrn);
|
||||||
}
|
}
|
||||||
|
|
@ -38,9 +49,10 @@ public class DnsPaymentResolver {
|
||||||
throw new IllegalArgumentException("Invalid HRN: " + hrn);
|
throw new IllegalArgumentException("Invalid HRN: " + hrn);
|
||||||
}
|
}
|
||||||
this.domain = parts[0] + ".user._bitcoin-payment." + parts[1];
|
this.domain = parts[0] + ".user._bitcoin-payment." + parts[1];
|
||||||
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<DnsPayment> resolve() throws IOException, DnsPaymentValidationException, BitcoinURIParseException {
|
public Optional<DnsPayment> resolve() throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||||
return resolve(DEFAULT_RESOLVER_IP_ADDRESS);
|
return resolve(DEFAULT_RESOLVER_IP_ADDRESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,32 +65,40 @@ public class DnsPaymentResolver {
|
||||||
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
||||||
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
||||||
*/
|
*/
|
||||||
public Optional<DnsPayment> resolve(String resolverIpAddress) throws IOException, DnsPaymentValidationException, BitcoinURIParseException {
|
public Optional<DnsPayment> resolve(String resolverIpAddress) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||||
log.debug("Resolving payment record for: " + domain);
|
log.debug("Resolving payment record for: " + domain);
|
||||||
|
|
||||||
PersistingResolver persistingResolver = new PersistingResolver(resolverIpAddress);
|
PersistingResolver persistingResolver = new PersistingResolver(resolverIpAddress);
|
||||||
ValidatingResolver resolver = new ValidatingResolver(persistingResolver);
|
ValidatingResolver validatingResolver = new ValidatingResolver(persistingResolver, clock);
|
||||||
resolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
validatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
||||||
resolver.setEDNS(0, 0, ExtendedFlags.DO);
|
validatingResolver.setEDNS(0, 0, ExtendedFlags.DO);
|
||||||
|
AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(validatingResolver);
|
||||||
|
|
||||||
Lookup lookup = new Lookup(domain, Type.TXT);
|
try {
|
||||||
lookup.setResolver(resolver);
|
LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build();
|
||||||
|
LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get();
|
||||||
Message query = getQuery();
|
if(result.getRecords().isEmpty()) {
|
||||||
Message response = resolver.send(query);
|
|
||||||
if(response.getSection(Section.ANSWER).isEmpty()) {
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponse(response, new ArrayList<>(persistingResolver.getChain()));
|
String strBitcoinUri = getBitcoinURI(result.getRecords());
|
||||||
String strBitcoinUri = getBitcoinURI(response.getSection(Section.ANSWER));
|
|
||||||
if(strBitcoinUri.isEmpty()) {
|
if(strBitcoinUri.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
||||||
validateResponse(response, new ArrayList<>(persistingResolver.getChain()));
|
validateResponse(authenticatingResolver, new ArrayList<>(persistingResolver.getChain()));
|
||||||
|
|
||||||
return Optional.of(new DnsPayment(hrn, bitcoinURI, persistingResolver.chainToWire()));
|
byte[] proofChain = persistingResolver.chainToWire();
|
||||||
|
log.debug("Resolved " + hrn + " with proof " + Utils.bytesToHex(proofChain));
|
||||||
|
|
||||||
|
return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain));
|
||||||
|
} catch(ExecutionException e) {
|
||||||
|
if(e.getCause() instanceof NoSuchDomainException) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -90,12 +110,13 @@ public class DnsPaymentResolver {
|
||||||
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
* @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure
|
||||||
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
* @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI
|
||||||
*/
|
*/
|
||||||
public Optional<DnsPayment> resolve(byte[] proofChain) throws IOException, DnsPaymentValidationException, BitcoinURIParseException {
|
public Optional<DnsPayment> resolve(byte[] proofChain) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||||
OfflineResolver offlineResolver = new OfflineResolver(proofChain);
|
OfflineResolver offlineResolver = new OfflineResolver(proofChain);
|
||||||
ValidatingResolver offlineValidatingResolver = new ValidatingResolver(offlineResolver);
|
ValidatingResolver offlineValidatingResolver = new ValidatingResolver(offlineResolver, clock);
|
||||||
offlineValidatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
offlineValidatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
||||||
|
AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(offlineValidatingResolver);
|
||||||
|
|
||||||
Instant now = Instant.now();
|
Instant now = clock.instant();
|
||||||
Instant oneHourAgo = now.minusSeconds(3600);
|
Instant oneHourAgo = now.minusSeconds(3600);
|
||||||
for(Record record : offlineResolver.getCachedSigs()) {
|
for(Record record : offlineResolver.getCachedSigs()) {
|
||||||
if(record instanceof RRSIGRecord rrsig) {
|
if(record instanceof RRSIGRecord rrsig) {
|
||||||
|
|
@ -107,44 +128,36 @@ public class DnsPaymentResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Message query = getQuery();
|
try {
|
||||||
Message offlineResponse = offlineValidatingResolver.send(query);
|
LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build();
|
||||||
if(offlineResponse.getSection(Section.ANSWER).isEmpty()) {
|
LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get();
|
||||||
|
if(result.getRecords().isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
checkResponse(offlineResponse, offlineResolver.getRecords());
|
String strBitcoinUri = getBitcoinURI(result.getRecords());
|
||||||
String strBitcoinUri = getBitcoinURI(offlineResponse.getSection(Section.ANSWER));
|
|
||||||
if(strBitcoinUri.isEmpty()) {
|
if(strBitcoinUri.isEmpty()) {
|
||||||
throw new BitcoinURIParseException("The DNS record for " + hrn + " did not contain a Bitcoin URI");
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri);
|
||||||
validateResponse(offlineResponse, offlineResolver.getRecords());
|
validateResponse(authenticatingResolver, offlineResolver.getRecords());
|
||||||
|
|
||||||
return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain));
|
return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain));
|
||||||
|
} catch(ExecutionException e) {
|
||||||
|
if(e.getCause() instanceof NoSuchDomainException) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Message getQuery() throws TextParseException {
|
|
||||||
Name queryName = Name.fromString(domain + ".");
|
|
||||||
Record question = Record.newRecord(queryName, Type.TXT, DClass.IN);
|
|
||||||
return Message.newQuery(question);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkResponse(Message response, List<Record> records) throws DnsPaymentValidationException {
|
|
||||||
if(response.getRcode() != Rcode.NOERROR) {
|
|
||||||
StringBuilder reason = new StringBuilder();
|
|
||||||
for(RRset set : response.getSectionRRsets(Section.ADDITIONAL)) {
|
|
||||||
if(set.getName().equals(Name.root) && set.getType() == Type.TXT && set.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) {
|
|
||||||
reason.append(((TXTRecord) set.first()).getStrings().getFirst());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new DnsPaymentValidationException("DNS query for " + domain + " failed, " + (reason.isEmpty() ? "rcode was " + response.getRcode() : reason.toString()));
|
private Name getName() throws TextParseException {
|
||||||
}
|
return Name.fromString(domain + ".");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateResponse(Message response, List<Record> records) throws DnsPaymentValidationException {
|
private void validateResponse(AuthenticatingResolver resolver, List<Record> records) throws DnsPaymentValidationException {
|
||||||
boolean isValidated = response.getHeader().getFlag(Flags.AD);
|
boolean isValidated = resolver.isAuthenticated();
|
||||||
if(!isValidated) {
|
if(!isValidated) {
|
||||||
throw new DnsPaymentValidationException("DNSSEC validation failed, could not authenticate the payment instruction");
|
throw new DnsPaymentValidationException("DNSSEC validation failed, could not authenticate the payment instruction");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,32 +58,80 @@ public class OfflineResolver implements Resolver {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||||
Record question = query.getQuestion();
|
Message response = makeNoErrorResponse(query);
|
||||||
List<Record> records = new ArrayList<>();
|
addRecords(query.getQuestion(), response);
|
||||||
|
|
||||||
for(Record it : cachedRrs) {
|
if(response.getSection(Section.ANSWER).isEmpty() && response.getSection(Section.AUTHORITY).isEmpty()) {
|
||||||
if(it.getName().equals(question.getName()) && it.getType() == question.getType() && it.getDClass() == question.getDClass()) {
|
response = makeNoDomainResponse(query);
|
||||||
records.add(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for(RRSIGRecord it : cachedSigs) {
|
|
||||||
if(it.getName().equals(question.getName()) && it.getTypeCovered() == question.getType() && it.getDClass() == question.getDClass()) {
|
|
||||||
records.add(it);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Message response;
|
|
||||||
if(records.isEmpty()) {
|
|
||||||
response = makeEmptyResponse(query);
|
|
||||||
} else {
|
|
||||||
response = makeResponseForRecords(records, query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(response);
|
return CompletableFuture.completedFuture(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Message makeEmptyResponse(Message query) {
|
private void addRecords(Record question, Message response) {
|
||||||
|
Name name = question.getName();
|
||||||
|
|
||||||
|
RRset cnameSet = getRRSet(name, Type.CNAME, question.getDClass());
|
||||||
|
addRRSetToMessage(response, cnameSet);
|
||||||
|
|
||||||
|
if(!cnameSet.rrs().isEmpty() && cnameSet.rrs().getFirst() instanceof CNAMERecord cnameRecord) {
|
||||||
|
name = cnameRecord.getTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
RRset answerSet = getRRSet(name, question.getType(), question.getDClass());
|
||||||
|
addRRSetToMessage(response, answerSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRRSetToMessage(Message response, RRset rrset) {
|
||||||
|
rrset.rrs().stream().forEach(it -> response.addRecord(it, Section.ANSWER));
|
||||||
|
rrset.sigs().stream().forEach(it -> response.addRecord(it, Section.ANSWER));
|
||||||
|
|
||||||
|
if(!rrset.sigs().isEmpty()) {
|
||||||
|
Name wildcard = RecordUtils.rrsetWildcard(rrset);
|
||||||
|
if(wildcard != null) {
|
||||||
|
RRset nsecRRset = getNSecRRSetForWildcard(wildcard);
|
||||||
|
nsecRRset.rrs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY));
|
||||||
|
nsecRRset.sigs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RRset getRRSet(Name name, int type, int dclass) {
|
||||||
|
RRset rrset = new RRset();
|
||||||
|
for(Record it : cachedRrs) {
|
||||||
|
if(it.getName().equals(name) && it.getType() == type && it.getDClass() == dclass) {
|
||||||
|
rrset.addRR(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(RRSIGRecord it : cachedSigs) {
|
||||||
|
if(it.getName().equals(name) && it.getTypeCovered() == type && it.getDClass() == dclass) {
|
||||||
|
rrset.addRR(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rrset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RRset getNSecRRSetForWildcard(Name wildcard) {
|
||||||
|
RRset rrset = new RRset();
|
||||||
|
|
||||||
|
for(Record it : cachedRrs) {
|
||||||
|
if((it.getType() == Type.NSEC || it.getType() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) {
|
||||||
|
rrset.addRR(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(RRSIGRecord it : cachedSigs) {
|
||||||
|
if((it.getTypeCovered() == Type.NSEC || it.getTypeCovered() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) {
|
||||||
|
rrset.addRR(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rrset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message makeNoDomainResponse(Message query) {
|
||||||
Header messageHeader = new Header();
|
Header messageHeader = new Header();
|
||||||
messageHeader.setID(query.getHeader().getID());
|
messageHeader.setID(query.getHeader().getID());
|
||||||
messageHeader.setRcode(Rcode.NXDOMAIN);
|
messageHeader.setRcode(Rcode.NXDOMAIN);
|
||||||
|
|
@ -95,12 +143,14 @@ public class OfflineResolver implements Resolver {
|
||||||
Message answerMessage = new Message();
|
Message answerMessage = new Message();
|
||||||
answerMessage.setHeader(messageHeader);
|
answerMessage.setHeader(messageHeader);
|
||||||
|
|
||||||
|
for(Record record : query.getSection(Section.QUESTION)) {
|
||||||
|
answerMessage.addRecord(record, Section.QUESTION);
|
||||||
|
}
|
||||||
|
|
||||||
return answerMessage;
|
return answerMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Message makeResponseForRecords(List<Record> records, Message query) {
|
private Message makeNoErrorResponse(Message query) {
|
||||||
Message answerMessage = new Message();
|
|
||||||
|
|
||||||
Header messageHeader = new Header();
|
Header messageHeader = new Header();
|
||||||
messageHeader.setID(query.getHeader().getID());
|
messageHeader.setID(query.getHeader().getID());
|
||||||
messageHeader.setRcode(Rcode.NOERROR);
|
messageHeader.setRcode(Rcode.NOERROR);
|
||||||
|
|
@ -108,16 +158,14 @@ public class OfflineResolver implements Resolver {
|
||||||
messageHeader.setFlag(Flags.CD);
|
messageHeader.setFlag(Flags.CD);
|
||||||
messageHeader.setFlag(Flags.RD);
|
messageHeader.setFlag(Flags.RD);
|
||||||
messageHeader.setFlag(Flags.RA);
|
messageHeader.setFlag(Flags.RA);
|
||||||
|
|
||||||
|
Message answerMessage = new Message();
|
||||||
answerMessage.setHeader(messageHeader);
|
answerMessage.setHeader(messageHeader);
|
||||||
|
|
||||||
for(Record record : query.getSection(Section.QUESTION)) {
|
for(Record record : query.getSection(Section.QUESTION)) {
|
||||||
answerMessage.addRecord(record, Section.QUESTION);
|
answerMessage.addRecord(record, Section.QUESTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Record record : records) {
|
|
||||||
answerMessage.addRecord(record, Section.ANSWER);
|
|
||||||
}
|
|
||||||
|
|
||||||
return answerMessage;
|
return answerMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package com.sparrowwallet.drongo.dns;
|
package com.sparrowwallet.drongo.dns;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.xbill.DNS.*;
|
import org.xbill.DNS.*;
|
||||||
import org.xbill.DNS.Record;
|
import org.xbill.DNS.Record;
|
||||||
|
|
||||||
|
|
@ -10,6 +12,8 @@ import java.util.concurrent.CompletionStage;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
public class PersistingResolver extends SimpleResolver {
|
public class PersistingResolver extends SimpleResolver {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PersistingResolver.class);
|
||||||
|
|
||||||
private final Set<Record> chain = new LinkedHashSet<>();
|
private final Set<Record> chain = new LinkedHashSet<>();
|
||||||
|
|
||||||
public PersistingResolver(String hostname) throws UnknownHostException {
|
public PersistingResolver(String hostname) throws UnknownHostException {
|
||||||
|
|
@ -20,6 +24,10 @@ public class PersistingResolver extends SimpleResolver {
|
||||||
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
public CompletionStage<Message> sendAsync(Message query, Executor executor) {
|
||||||
CompletionStage<Message> result = super.sendAsync(query, executor);
|
CompletionStage<Message> result = super.sendAsync(query, executor);
|
||||||
return result.thenApply(response -> {
|
return result.thenApply(response -> {
|
||||||
|
if(log.isDebugEnabled()) {
|
||||||
|
log.debug(responseToString(query, response));
|
||||||
|
}
|
||||||
|
|
||||||
addAnswerSectionToChain(response.getSection(Section.ANSWER));
|
addAnswerSectionToChain(response.getSection(Section.ANSWER));
|
||||||
addAuthoritySectionToChain(response.getSection(Section.AUTHORITY));
|
addAuthoritySectionToChain(response.getSection(Section.AUTHORITY));
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -35,7 +43,7 @@ public class PersistingResolver extends SimpleResolver {
|
||||||
private void addAuthoritySectionToChain(List<Record> section) {
|
private void addAuthoritySectionToChain(List<Record> section) {
|
||||||
if(section != null) {
|
if(section != null) {
|
||||||
for(Record r : section) {
|
for(Record r : section) {
|
||||||
if((r.getType() == Type.RRSIG && r.getRRsetType() == Type.NSEC && r.getRRsetType() == Type.NSEC3)|| r.getType() == Type.NSEC || r.getType() == Type.NSEC3) {
|
if((r.getType() == Type.RRSIG && (r.getRRsetType() == Type.NSEC || r.getRRsetType() == Type.NSEC3)) || r.getType() == Type.NSEC || r.getType() == Type.NSEC3) {
|
||||||
chain.add(r);
|
chain.add(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,4 +65,15 @@ public class PersistingResolver extends SimpleResolver {
|
||||||
|
|
||||||
return baos.toByteArray();
|
return baos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String responseToString(Message query, Message response) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Query for ").append(query.getQuestion().getName()).append(" returned:\n");
|
||||||
|
sb.append("Answer section:\n");
|
||||||
|
response.getSection(Section.ANSWER).stream().forEach(rr -> sb.append(rr).append("\n"));
|
||||||
|
sb.append("Authority section:\n");
|
||||||
|
response.getSection(Section.AUTHORITY).stream().forEach(rr -> sb.append(rr).append("\n"));
|
||||||
|
sb.append("\n");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,4 +63,61 @@ public class RecordUtils {
|
||||||
|
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine by looking at a signed RRset whether the RRset name was the result of a wildcard
|
||||||
|
* expansion. If so, return the name of the generating wildcard.
|
||||||
|
*
|
||||||
|
* @param rrset The rrset to chedck.
|
||||||
|
* @return the wildcard name, if the rrset was synthesized from a wildcard. null if not.
|
||||||
|
*/
|
||||||
|
public static Name rrsetWildcard(RRset rrset) {
|
||||||
|
List<RRSIGRecord> sigs = rrset.sigs();
|
||||||
|
RRSIGRecord firstSig = sigs.getFirst();
|
||||||
|
|
||||||
|
// check rest of signatures have identical label count
|
||||||
|
for(int i = 1; i < sigs.size(); i++) {
|
||||||
|
if(sigs.get(i).getLabels() != firstSig.getLabels()) {
|
||||||
|
throw new IllegalArgumentException("Label count mismatch on RRSIGs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the RRSIG label count is shorter than the number of actual labels,
|
||||||
|
// then this rrset was synthesized from a wildcard.
|
||||||
|
// Note that the RRSIG label count doesn't count the root label.
|
||||||
|
Name wn = rrset.getName();
|
||||||
|
|
||||||
|
// skip a leading wildcard label in the dname (RFC4035 2.2)
|
||||||
|
if(rrset.getName().isWild()) {
|
||||||
|
wn = new Name(wn, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int labelDiff = (wn.labels() - 1) - firstSig.getLabels();
|
||||||
|
if(labelDiff > 0) {
|
||||||
|
return wn.wild(labelDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the longest domain name in common with the given name.
|
||||||
|
*
|
||||||
|
* @param domain1 The first domain to process.
|
||||||
|
* @param domain2 The second domain to process.
|
||||||
|
* @return The longest label in common of domain1 and domain2. The least common name is the root.
|
||||||
|
*/
|
||||||
|
public static Name longestCommonName(Name domain1, Name domain2) {
|
||||||
|
int l = Math.min(domain1.labels(), domain2.labels());
|
||||||
|
domain1 = new Name(domain1, domain1.labels() - l);
|
||||||
|
domain2 = new Name(domain2, domain2.labels() - l);
|
||||||
|
for(int i = 0; i < l - 1; i++) {
|
||||||
|
Name ns1 = new Name(domain1, i);
|
||||||
|
if(ns1.equals(new Name(domain2, i))) {
|
||||||
|
return ns1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Name.root;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||||
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
|
||||||
|
|
@ -294,7 +295,7 @@ public class PSBTOutput {
|
||||||
return dnssecProof;
|
return dnssecProof;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<DnsPayment> getDnsPayment() throws DnsPaymentValidationException, IOException, BitcoinURIParseException {
|
public Optional<DnsPayment> getDnsPayment() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException {
|
||||||
if(dnssecProof == null || dnssecProof.isEmpty()) {
|
if(dnssecProof == null || dnssecProof.isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue