diff --git a/pdns/Makefile.am b/pdns/Makefile.am index ee4ea9126f89..0737761091f1 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -198,6 +198,7 @@ pdns_server_SOURCES = \ statbag.cc statbag.hh \ stubresolver.cc stubresolver.hh \ tcpreceiver.cc tcpreceiver.hh \ + tsigverifier.cc tsigverifier.hh \ tkey.cc \ ueberbackend.cc ueberbackend.hh \ unix_semaphore.cc \ @@ -584,6 +585,7 @@ ixplore_SOURCES = \ sillyrecords.cc \ sstuff.hh \ statbag.cc \ + tsigverifier.cc tsigverifier.hh \ unix_utility.cc zoneparser-tng.cc ixplore_LDADD = $(LIBCRYPTO_LIBS) @@ -735,6 +737,7 @@ tsig_tests_SOURCES = \ sstuff.hh \ statbag.cc \ tsig-tests.cc \ + tsigverifier.cc tsigverifier.hh \ unix_utility.cc tsig_tests_LDADD = $(LIBCRYPTO_LIBS) diff --git a/pdns/dnspacket.cc b/pdns/dnspacket.cc index 8202a7e270ba..64619d721039 100644 --- a/pdns/dnspacket.cc +++ b/pdns/dnspacket.cc @@ -469,7 +469,7 @@ bool DNSPacket::getTSIGDetails(TSIGRecordContent* trc, DNSName* keyname, string* bool gotit=false; for(MOADNSParser::answers_t::const_iterator i=mdp.d_answers.begin(); i!=mdp.d_answers.end(); ++i) { - if(i->first.d_type == QType::TSIG) { + if(i->first.d_type == QType::TSIG && i->first.d_class == QType::ANY) { // cast can fail, f.e. if d_content is an UnknownRecordContent. shared_ptr content = std::dynamic_pointer_cast(i->first.d_content); if (!content) { @@ -644,7 +644,10 @@ bool checkForCorrectTSIG(const DNSPacket* q, UeberBackend* B, DNSName* keyname, { string message; - q->getTSIGDetails(trc, keyname, &message); + if (!q->getTSIGDetails(trc, keyname, &message)) { + return false; + } + uint64_t delta = std::abs((int64_t)trc->d_time - (int64_t)time(0)); if(delta > trc->d_fudge) { L<qdomain<<"' denied: TSIG (key '"<<*keyname<<"') time delta "<< delta <<" > 'fudge' "<d_fudge<(key.c_str()), key.size(), reinterpret_cast(text.c_str()), text.size(), hash, &outlen); - if (out != NULL && outlen > 0) { - return string((char*) hash, outlen); + if (out == NULL || outlen == 0) { + throw PDNSException("HMAC computation failed"); } - return ""; + return string((char*) hash, outlen); +} + +bool constantTimeStringEquals(const std::string& a, const std::string& b) +{ + if (a.size() != b.size()) { + return false; + } + const size_t size = a.size(); +#if OPENSSL_VERSION_NUMBER >= 0x0090819fL + return CRYPTO_memcmp(a.c_str(), b.c_str(), size) == 0; +#else + const volatile unsigned char *_a = (const volatile unsigned char *) a.c_str(); + const volatile unsigned char *_b = (const volatile unsigned char *) b.c_str(); + unsigned char res = 0; + + for (size_t idx = 0; idx < size; idx++) { + res |= _a[idx] ^ _b[idx]; + } + + return res == 0; +#endif } string makeTSIGMessageFromTSIGPacket(const string& opacket, unsigned int tsigOffset, const DNSName& keyname, const TSIGRecordContent& trc, const string& previous, bool timersonly, unsigned int dnsHeaderOffset) diff --git a/pdns/dnssecinfra.hh b/pdns/dnssecinfra.hh index 503ca5c106af..41a6a0fd4073 100644 --- a/pdns/dnssecinfra.hh +++ b/pdns/dnssecinfra.hh @@ -157,6 +157,7 @@ class DNSPacket; void addRRSigs(DNSSECKeeper& dk, UeberBackend& db, const std::set& authMap, vector& rrs); string calculateHMAC(const std::string& key, const std::string& text, TSIGHashEnum hash); +bool constantTimeStringEquals(const std::string& a, const std::string& b); string makeTSIGMessageFromTSIGPacket(const string& opacket, unsigned int tsigoffset, const DNSName& keyname, const TSIGRecordContent& trc, const string& previous, bool timersonly, unsigned int dnsHeaderOffset=0); void addTSIG(DNSPacketWriter& pw, TSIGRecordContent* trc, const DNSName& tsigkeyname, const string& tsigsecret, const string& tsigprevious, bool timersonly); diff --git a/pdns/ixfr.cc b/pdns/ixfr.cc index b57d40ea34c0..a325eaec5fbd 100644 --- a/pdns/ixfr.cc +++ b/pdns/ixfr.cc @@ -24,7 +24,7 @@ #include "dns_random.hh" #include "dnsrecords.hh" #include "dnssecinfra.hh" - +#include "tsigverifier.hh" // Returns pairs of "remove & add" vectors. If you get an empty remove, it means you got an AXFR! vector, vector > > getIXFRDeltas(const ComboAddress& master, const DNSName& zone, const DNSRecord& oursr, @@ -40,10 +40,11 @@ vector, vector > > getIXFRDeltas(const ComboAd oursr.d_content->toPacket(pw); pw.commit(); + TSIGRecordContent trc; + TSIGTCPVerifier tsigVerifier(tt, master, trc); if(!tt.algo.empty()) { TSIGHashEnum the; getTSIGHashEnum(tt.algo, the); - TSIGRecordContent trc; try { trc.d_algoName = getTSIGAlgoName(the); } catch(PDNSException& pe) { @@ -77,6 +78,7 @@ vector, vector > > getIXFRDeltas(const ComboAd shared_ptr masterSOA; vector records; size_t receivedBytes = 0; + for(;;) { if(s.read((char*)&len, 2)!=2) break; @@ -96,6 +98,11 @@ vector, vector > > getIXFRDeltas(const ComboAd throw std::runtime_error("Got an error trying to IXFR zone '"+zone.toString()+"' from master '"+master.toStringWithPort()+"': "+RCode::to_s(mdp.d_header.rcode)); // cout<<"Got a response, rcode: "<* records) // if (answer.first.d_type == QType::SOA) d_soacount++; - if(!d_tt.name.empty()) { // TSIG verify message - // If we have multiple messages, we need to concatenate them together. We also need to make sure we know the location of - // the TSIG record so we can remove it in makeTSIGMessageFromTSIGPacket - d_signData.append(d_buf.get(), len); - if (mdp.getTSIGPos() == 0) - d_tsigPos += len; - else - d_tsigPos += mdp.getTSIGPos(); - - string theirMac; - bool checkTSIG = false; - - for(const MOADNSParser::answers_t::value_type& answer : mdp.d_answers) { - if (answer.first.d_type == QType::SOA) // A SOA is either the first or the last record. We need to check TSIG if that's the case. - checkTSIG = true; - - if(answer.first.d_type == QType::TSIG) { - shared_ptr trc = getRR(answer.first); - if(trc) { - theirMac = trc->d_mac; - d_trc.d_time = trc->d_time; - checkTSIG = true; - } - } - } - - if( ! checkTSIG && d_nonSignedMessages > 99) { // We're allowed to get 100 digest without a TSIG. - throw ResolverException("No TSIG message received in last 100 messages of AXFR transfer."); - } - - if (checkTSIG) { - if (theirMac.empty()) - throw ResolverException("No TSIG on AXFR response from "+d_remote.toStringWithPort()+" , should be signed with TSIG key '"+d_tt.name.toString()+"'"); - - string message; - if (!d_prevMac.empty()) { - message = makeTSIGMessageFromTSIGPacket(d_signData, d_tsigPos, d_tt.name, d_trc, d_prevMac, true, d_signData.size()-len); - } else { - message = makeTSIGMessageFromTSIGPacket(d_signData, d_tsigPos, d_tt.name, d_trc, d_trc.d_mac, false); - } - - TSIGHashEnum algo; - if (!getTSIGHashEnum(d_trc.d_algoName, algo)) { - throw ResolverException("Unsupported TSIG HMAC algorithm " + d_trc.d_algoName.toString()); - } - - if (algo == TSIG_GSS) { - GssContext gssctx(d_tt.name); - if (!gss_verify_signature(d_tt.name, message, theirMac)) { - throw ResolverException("Signature failed to validate on AXFR response from "+d_remote.toStringWithPort()+" signed with TSIG key '"+d_tt.name.toString()+"'"); - } - } else { - string ourMac=calculateHMAC(d_tt.secret, message, algo); - - // ourMac[0]++; // sabotage == for testing :-) - if(ourMac != theirMac) { - throw ResolverException("Signature failed to validate on AXFR response from "+d_remote.toStringWithPort()+" signed with TSIG key '"+d_tt.name.toString()+"'"); - } - } - - // Reset and store some values for the next chunks. - d_prevMac = theirMac; - d_nonSignedMessages = 0; - d_signData.clear(); - d_tsigPos = 0; - } - else - d_nonSignedMessages++; + try { + d_tsigVerifier.check(std::string(d_buf.get(), len), mdp); } - + catch(const std::runtime_error& re) { + throw ResolverException(re.what()); + } + return true; } diff --git a/pdns/resolver.hh b/pdns/resolver.hh index 352636593693..196831b4bec5 100644 --- a/pdns/resolver.hh +++ b/pdns/resolver.hh @@ -41,6 +41,7 @@ #include "namespaces.hh" #include "dnsrecords.hh" #include "dnssecinfra.hh" +#include "tsigverifier.hh" class ResolverException : public PDNSException { @@ -101,14 +102,10 @@ class AXFRRetriever : public boost::noncopyable int d_sock; int d_soacount; ComboAddress d_remote; - - TSIGTriplet d_tt; - string d_prevMac; // RFC2845 4.4 - string d_signData; + TSIGTCPVerifier d_tsigVerifier; + size_t d_receivedBytes; size_t d_maxReceivedBytes; - uint32_t d_tsigPos; - uint d_nonSignedMessages; // RFC2845 4.4 TSIGRecordContent d_trc; }; diff --git a/pdns/saxfr.cc b/pdns/saxfr.cc index ef42bd335771..040522c62709 100644 --- a/pdns/saxfr.cc +++ b/pdns/saxfr.cc @@ -35,7 +35,7 @@ bool validateTSIG(const string& message, const TSIGHashEnum& algo, const DNSName } return true; } - return calculateHMAC(secret, message, algo) == trc->d_mac; + return constantTimeStringEquals(calculateHMAC(secret, message, algo), trc->d_mac); } diff --git a/pdns/tcpreceiver.cc b/pdns/tcpreceiver.cc index 123e0dbe357f..0255c5b4e52d 100644 --- a/pdns/tcpreceiver.cc +++ b/pdns/tcpreceiver.cc @@ -683,9 +683,9 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou DNSName tsigkeyname; string tsigsecret; - q->getTSIGDetails(&trc, &tsigkeyname, 0); + bool haveTSIGDetails = q->getTSIGDetails(&trc, &tsigkeyname, 0); - if(!tsigkeyname.empty()) { + if(haveTSIGDetails && !tsigkeyname.empty()) { string tsig64; DNSName algorithm=trc.d_algoName; // FIXME400: check if (algorithm == DNSName("hmac-md5.sig-alg.reg.int")) @@ -714,7 +714,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou addRRSigs(dk, signatureDB, authSet, outpacket->getRRS()); } - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac); // first answer is 'normal' sendPacket(outpacket, outsock); @@ -969,7 +969,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou for(;;) { outpacket->getRRS() = csp.getChunk(); if(!outpacket->getRRS().empty()) { - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac, true); sendPacket(outpacket, outsock); trc.d_mac=outpacket->d_trc.d_mac; @@ -1020,7 +1020,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou for(;;) { outpacket->getRRS() = csp.getChunk(); if(!outpacket->getRRS().empty()) { - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac, true); sendPacket(outpacket, outsock); trc.d_mac=outpacket->d_trc.d_mac; @@ -1055,7 +1055,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou for(;;) { outpacket->getRRS() = csp.getChunk(); if(!outpacket->getRRS().empty()) { - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac, true); sendPacket(outpacket, outsock); trc.d_mac=outpacket->d_trc.d_mac; @@ -1076,7 +1076,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou for(;;) { outpacket->getRRS() = csp.getChunk(true); // flush the pipe if(!outpacket->getRRS().empty()) { - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac, true); // first answer is 'normal' sendPacket(outpacket, outsock); trc.d_mac=outpacket->d_trc.d_mac; @@ -1095,7 +1095,7 @@ int TCPNameserver::doAXFR(const DNSName &target, shared_ptr q, int ou outpacket=getFreshAXFRPacket(q); outpacket->addRecord(dzrsoa); editSOA(dk, sd.qname, outpacket.get()); - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac, true); sendPacket(outpacket, outsock); @@ -1197,9 +1197,9 @@ int TCPNameserver::doIXFR(shared_ptr q, int outsock) DNSName tsigkeyname; string tsigsecret; - q->getTSIGDetails(&trc, &tsigkeyname, 0); + bool haveTSIGDetails = q->getTSIGDetails(&trc, &tsigkeyname, 0); - if(!tsigkeyname.empty()) { + if(haveTSIGDetails && !tsigkeyname.empty()) { string tsig64; DNSName algorithm=trc.d_algoName; // FIXME400: was toLowerCanonic, compare output if (algorithm == DNSName("hmac-md5.sig-alg.reg.int")) @@ -1226,7 +1226,7 @@ int TCPNameserver::doIXFR(shared_ptr q, int outsock) addRRSigs(dk, signatureDB, authSet, outpacket->getRRS()); } - if(!tsigkeyname.empty()) + if(haveTSIGDetails && !tsigkeyname.empty()) outpacket->setTSIGDetails(trc, tsigkeyname, tsigsecret, trc.d_mac); // first answer is 'normal' sendPacket(outpacket, outsock); diff --git a/pdns/tsigverifier.cc b/pdns/tsigverifier.cc new file mode 100644 index 000000000000..3daeef47b554 --- /dev/null +++ b/pdns/tsigverifier.cc @@ -0,0 +1,89 @@ + +#include "tsigverifier.hh" +#include "dnssecinfra.hh" +#include "gss_context.hh" + +bool TSIGTCPVerifier::check(const string& data, const MOADNSParser& mdp) +{ + if(d_tt.name.empty()) { // TSIG verify message + return true; + } + + string theirMac; + bool checkTSIG = false; + // If we have multiple messages, we need to concatenate them together. We also need to make sure we know the location of + // the TSIG record so we can remove it in makeTSIGMessageFromTSIGPacket + d_signData.append(data); + if (mdp.getTSIGPos() == 0) { + d_tsigPos += data.size(); + } + else { + d_tsigPos += mdp.getTSIGPos(); + } + + for(const auto& answer : mdp.d_answers) { + if (answer.first.d_type == QType::SOA) { + // A SOA is either the first or the last record. We need to check TSIG if that's the case. + checkTSIG = true; + } + + if(answer.first.d_type == QType::TSIG) { + shared_ptr trc = getRR(answer.first); + if(trc) { + theirMac = trc->d_mac; + d_trc.d_time = trc->d_time; + d_trc.d_fudge = trc->d_fudge; + checkTSIG = true; + } + } + } + + if(!checkTSIG && d_nonSignedMessages > 99) { // We're allowed to get 100 digest without a TSIG. + throw std::runtime_error("No TSIG message received in last 100 messages of AXFR transfer."); + } + + if (checkTSIG) { + if (theirMac.empty()) { + throw std::runtime_error("No TSIG on AXFR response from "+d_remote.toStringWithPort()+" , should be signed with TSIG key '"+d_tt.name.toString()+"'"); + } + + uint64_t delta = std::abs((int64_t)d_trc.d_time - (int64_t)time(nullptr)); + if(delta > d_trc.d_fudge) { + throw std::runtime_error("Invalid TSIG time delta " + std::to_string(delta) + " > fudge " + std::to_string(d_trc.d_fudge)); + } + string message; + if (!d_prevMac.empty()) { + message = makeTSIGMessageFromTSIGPacket(d_signData, d_tsigPos, d_tt.name, d_trc, d_prevMac, true, d_signData.size()-data.size()); + } else { + message = makeTSIGMessageFromTSIGPacket(d_signData, d_tsigPos, d_tt.name, d_trc, d_trc.d_mac, false); + } + + TSIGHashEnum algo; + if (!getTSIGHashEnum(d_trc.d_algoName, algo)) { + throw std::runtime_error("Unsupported TSIG HMAC algorithm " + d_trc.d_algoName.toString()); + } + + if (algo == TSIG_GSS) { + GssContext gssctx(d_tt.name); + if (!gss_verify_signature(d_tt.name, message, theirMac)) { + throw std::runtime_error("Signature failed to validate on AXFR response from "+d_remote.toStringWithPort()+" signed with TSIG key '"+d_tt.name.toString()+"'"); + } + } else { + string ourMac=calculateHMAC(d_tt.secret, message, algo); + + if(!constantTimeStringEquals(ourMac, theirMac)) { + throw std::runtime_error("Signature failed to validate on AXFR response from "+d_remote.toStringWithPort()+" signed with TSIG key '"+d_tt.name.toString()+"'"); + } + } + + // Reset and store some values for the next chunks. + d_prevMac = theirMac; + d_nonSignedMessages = 0; + d_signData.clear(); + d_tsigPos = 0; + } + else + d_nonSignedMessages++; + + return true; +} diff --git a/pdns/tsigverifier.hh b/pdns/tsigverifier.hh new file mode 100644 index 000000000000..84b87ebd2cda --- /dev/null +++ b/pdns/tsigverifier.hh @@ -0,0 +1,22 @@ + +#pragma once + +#include "dnsrecords.hh" +#include "iputils.hh" + +class TSIGTCPVerifier +{ +public: + TSIGTCPVerifier(const TSIGTriplet& tt, const ComboAddress& remote, TSIGRecordContent& trc): d_tt(tt), d_remote(remote), d_trc(trc) + { + } + bool check(const string& data, const MOADNSParser& mdp); +private: + const TSIGTriplet& d_tt; + const ComboAddress& d_remote; + TSIGRecordContent& d_trc; + string d_prevMac; // RFC2845 4.4 + string d_signData; + size_t d_tsigPos{0}; + uint8_t d_nonSignedMessages{0}; // RFC2845 4.4 +};