From e21df721e8b3908123d9ba7d41db8c685ee4fdb7 Mon Sep 17 00:00:00 2001 From: Otto Date: Tue, 14 Dec 2021 14:16:08 +0100 Subject: [PATCH] Refactor zonemd verify code out of pdnsutil --- pdns/Makefile.am | 1 + pdns/pdnsutil.cc | 241 +++-------------------------------------------- pdns/sha.hh | 66 ++++++++++++- pdns/zonemd.cc | 184 ++++++++++++++++++++++++++++++++++++ pdns/zonemd.hh | 33 +++++++ 5 files changed, 297 insertions(+), 228 deletions(-) create mode 100644 pdns/zonemd.cc create mode 100644 pdns/zonemd.hh diff --git a/pdns/Makefile.am b/pdns/Makefile.am index 004c8becfe..3c762b2deb 100644 --- a/pdns/Makefile.am +++ b/pdns/Makefile.am @@ -378,6 +378,7 @@ pdnsutil_SOURCES = \ tsigutils.hh tsigutils.cc \ ueberbackend.cc \ unix_utility.cc \ + zonemd.hh zonemd.cc \ zoneparser-tng.cc pdnsutil_LDFLAGS = \ diff --git a/pdns/pdnsutil.cc b/pdns/pdnsutil.cc index ccbe0f4c3a..b929b375fe 100644 --- a/pdns/pdnsutil.cc +++ b/pdns/pdnsutil.cc @@ -27,6 +27,7 @@ #include "dns_random.hh" #include "ipcipher.hh" #include "misc.hh" +#include "zonemd.hh" #include #include #include //termios, TCSANOW, ECHO, ICANON @@ -39,8 +40,6 @@ #include "bind-dnssec.schema.sqlite3.sql.h" #endif -#include - StatBag S; AuthPacketCache PC; AuthQueryCache QC; @@ -1360,246 +1359,34 @@ static int xcryptIP(const std::string& cmd, const std::string& ip, const std::st return EXIT_SUCCESS; } + static int zonemdVerifyFile(const DNSName& zone, const string& fname) { ZoneParserTNG zpt(fname, zone); zpt.setMaxGenerateSteps(::arg().asNum("max-generate-steps")); - typedef std::pair rrSetKey_t; - typedef std::vector> rrVector_t; - - struct CanonrrSetKeyCompare: public std::binary_function - { - bool operator()(const rrSetKey_t&a, const rrSetKey_t& b) const - { - // FIXME surely we can be smarter here - if(a.first.canonCompare(b.first)) - return true; - if(b.first.canonCompare(a.first)) - return false; - - return a.second < b.second; - } - }; - - typedef std::map RRsetMap_t; - - RRsetMap_t RRsets; - std::map RRsetTTLs; - - DNSResourceRecord rr; - std::map, std::shared_ptr> zonemdRecords; - std::shared_ptr soarc; - - class SHADigest - { - public: - SHADigest(int bits) - { - mdctx = EVP_MD_CTX_new(); - if (mdctx == nullptr) { - throw std::runtime_error("VSHADigest: P_MD_CTX_new failed"); - } - switch (bits) { - case 256: - md = EVP_sha256(); - break; - case 384: - md = EVP_sha384(); - break; - case 512: - md = EVP_sha512(); - break; - default: - throw std::runtime_error("SHADigest: unsupported size"); - } - if (EVP_DigestInit_ex(mdctx, md, NULL) == 0) { - throw std::runtime_error("SHADigest: init error"); - } - } - ~SHADigest() - { - // No free of md needed afaik - if (mdctx != nullptr) { - EVP_MD_CTX_free(mdctx); - } - } - void process(const string& msg, size_t sz) - { - if (EVP_DigestUpdate(mdctx, msg.data(), msg.size()) == 0) { - throw std::runtime_error("SHADigest: update error"); - } - } - std::string digest() - { - string md_value; - md_value.resize(EVP_MD_size(md)); - unsigned int md_len; - if (EVP_DigestFinal_ex(mdctx, reinterpret_cast(md_value.data()), &md_len) == 0) { - throw std::runtime_error("SHADigest: finalize error"); - } - if (md_len != md_value.size()) { - throw std::runtime_error("SHADigest: inconsisten size"); - } - return md_value; - } - private: - EVP_MD_CTX *mdctx{nullptr}; - const EVP_MD *md; - }; - - while (zpt.get(rr)) { - if(!rr.qname.isPartOf(zone) && rr.qname!=zone) { - continue; - cerr<<"File contains record named '"< drc; - try { - drc = DNSRecordContent::mastermake(rr.qtype, QClass::IN, rr.content); - } - catch (const PDNSException &pe) { - cerr<<"Bad record content in record for "<(drc); - } - if (rr.qtype == QType::ZONEMD && rr.qname == zone) { - auto zonemd = std::dynamic_pointer_cast(drc); - auto inserted = zonemdRecords.insert(pair(pair(zonemd->d_scheme, zonemd->d_hashalgo), zonemd)).second; - if (!inserted) { - cerr << "Duplicate ZONEMD record!" << endl; - } - } - rrSetKey_t key = std::pair(rr.qname, rr.qtype); - RRsets[key].push_back(drc); - RRsetTTLs[key] = rr.ttl; - } - - unique_ptr sha384digest{nullptr}, sha512digest{nullptr}; + bool validationDone, validationOK; - for (const auto& [k, zonemd] : zonemdRecords) { - cerr << "Checking against " << zonemd->getZoneRepresentation() << endl; - if (zonemd->d_serial != soarc->d_st.serial) { - cerr << "SOA serial does not match " << endl; - continue; - } - if (zonemd->d_scheme != 1) { - cerr << "Unsupported scheme " << std::to_string(zonemd->d_scheme) << endl; - continue; - } - if (zonemd->d_hashalgo == 1) { - sha384digest = make_unique(384); - } - else if (zonemd->d_hashalgo == 2) { - sha512digest = make_unique(512); - } - else { - cerr << "Unsupported hashalgo " << std::to_string(zonemd->d_hashalgo) << endl; - continue; - } + try { + pdns::zonemdVerify(zone, zpt, validationDone, validationOK); } - - for (auto& rrset: RRsets) { - const auto& qname = rrset.first.first; - const auto& qtype = rrset.first.second; - if (qtype == QType::ZONEMD && qname == zone) { - continue; // the apex ZONEMD is not digested - } - - sortedRecords_t sorted; - for (auto& _rr: rrset.second) { - if (qtype == QType::RRSIG) { - const auto rrsig = std::dynamic_pointer_cast(_rr); - if (rrsig->d_type == QType::ZONEMD && qname == zone) { - continue; - } - } - sorted.insert(_rr); - } - - if (qtype != QType::RRSIG) { - RRSIGRecordContent rrc; - rrc.d_originalttl = RRsetTTLs[rrset.first]; - rrc.d_type = qtype; - auto msg = getMessageForRRSET(qname, rrc, sorted, false, false); - if (sha384digest) { - sha384digest->process(msg, msg.size()); - } - if (sha512digest) { - sha512digest->process(msg, msg.size()); - } - } else { - for (const auto& rrsig : sorted) { - auto rrsigc = std::dynamic_pointer_cast(rrsig); - RRSIGRecordContent rrc; - rrc.d_originalttl = RRsetTTLs[pair(rrset.first.first, rrsigc->d_type)]; - rrc.d_type = qtype; - auto msg = getMessageForRRSET(qname, rrc, { rrsigc }, false, false); - if (sha384digest) { - sha384digest->process(msg, msg.size()); - } - if (sha512digest) { - sha512digest->process(msg, msg.size()); - } - } - } + catch (const PDNSException& ex) { + cerr << "zonemd-verify-file: " << ex.reason << endl; + return EXIT_FAILURE; } - - bool validationDone = false; - bool validationOK = false; - - for (const auto& [k, zonemd] : zonemdRecords) { - if (zonemd->d_serial != soarc->d_st.serial) { - cerr << "SOA serial does not match " << endl; - continue; - } - if (zonemd->d_scheme != 1) { - cerr << "Unsupported scheme " << std::to_string(zonemd->d_scheme) << endl; - continue; - } - if (zonemd->d_hashalgo == 1) { - validationDone = true; - if (zonemd->d_digest == sha384digest->digest()) { - validationOK = true; - break; // Per RFC: a single succeeding validation is enough - } else { - continue; - } - } - else if (zonemd->d_hashalgo == 2) { - validationDone = true; - if (zonemd->d_digest == sha512digest->digest()) { - validationOK = true; - break; // Per RFC: a single succeeding validation is enough - } else { - continue; - } - } - else { - cerr << "Unsupported hashalgo " << std::to_string(zonemd->d_hashalgo) << endl; - continue; - } + catch (const std::exception& ex) { + cerr << "zonemd-verify-file: " << ex.what() << endl; } if (validationDone) { if (validationOK) { - cerr << "OK!" << endl; + cout << "zonemd-verify-file: Validation of ZONEMD record succeeded" << endl; return EXIT_SUCCESS; } else { - cerr << "Validation of ZONEMD record(s) failed" << endl; + cerr << "zonemd-verify-file: Validation of ZONEMD record(s) failed" << endl; } } - if (!validationDone) { - cerr << "No suitable ZONEMD record found to verify against" << endl; + else { + cerr << "zonemd-verify-file: No suitable ZONEMD record found to verify against" << endl; } return EXIT_FAILURE; } diff --git a/pdns/sha.hh b/pdns/sha.hh index 06bace9ab7..7284dda113 100644 --- a/pdns/sha.hh +++ b/pdns/sha.hh @@ -20,9 +20,10 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #pragma once + #include -#include #include +#include inline std::string pdns_sha1sum(const std::string& input) { @@ -51,3 +52,66 @@ inline std::string pdns_sha512sum(const std::string& input) SHA512(reinterpret_cast(input.c_str()), input.length(), result); return std::string(result, result + sizeof result); } + +namespace pdns +{ + class SHADigest + { + public: + SHADigest(int bits) + { + mdctx = EVP_MD_CTX_new(); + if (mdctx == nullptr) { + throw std::runtime_error("VSHADigest: P_MD_CTX_new failed"); + } + switch (bits) { + case 256: + md = EVP_sha256(); + break; + case 384: + md = EVP_sha384(); + break; + case 512: + md = EVP_sha512(); + break; + default: + throw std::runtime_error("SHADigest: unsupported size"); + } + if (EVP_DigestInit_ex(mdctx, md, NULL) == 0) { + throw std::runtime_error("SHADigest: init error"); + } + } + + ~SHADigest() + { + // No free of md needed afaik + if (mdctx != nullptr) { + EVP_MD_CTX_free(mdctx); + } + } + + void process(const std::string& msg, size_t sz) + { + if (EVP_DigestUpdate(mdctx, msg.data(), msg.size()) == 0) { + throw std::runtime_error("SHADigest: update error"); + } + } + + std::string digest() + { + std::string md_value; + md_value.resize(EVP_MD_size(md)); + unsigned int md_len; + if (EVP_DigestFinal_ex(mdctx, reinterpret_cast(md_value.data()), &md_len) == 0) { + throw std::runtime_error("SHADigest: finalize error"); + } + if (md_len != md_value.size()) { + throw std::runtime_error("SHADigest: inconsisten size"); + } + return md_value; + } + private: + EVP_MD_CTX *mdctx{nullptr}; + const EVP_MD *md; + }; +} diff --git a/pdns/zonemd.cc b/pdns/zonemd.cc new file mode 100644 index 0000000000..c52f101b8b --- /dev/null +++ b/pdns/zonemd.cc @@ -0,0 +1,184 @@ +#include "zonemd.hh" + +#include "dnsrecords.hh" +#include "dnssecinfra.hh" +#include "sha.hh" +#include "zoneparser-tng.hh" + +typedef std::pair rrSetKey_t; +typedef std::vector> rrVector_t; + +struct CanonrrSetKeyCompare: public std::binary_function +{ + bool operator()(const rrSetKey_t&a, const rrSetKey_t& b) const + { + // FIXME surely we can be smarter here + if(a.first.canonCompare(b.first)) { + return true; + } + if (b.first.canonCompare(a.first)) { + return false; + } + return a.second < b.second; + } +}; + +typedef std::map RRsetMap_t; + +RRsetMap_t RRsets; +std::map RRsetTTLs; + +void pdns::zonemdVerify(const DNSName& zone, ZoneParserTNG &zpt, bool& validationDone, bool& validationOK) +{ + validationDone = false; + validationOK = false; + + DNSResourceRecord dnsrr; + std::map, std::shared_ptr> zonemdRecords; + std::shared_ptr soarc; + + // Get all records and remember RRSets and TTLs + while (zpt.get(dnsrr)) { + if (!dnsrr.qname.isPartOf(zone) && dnsrr.qname != zone) { + continue; + } + if (dnsrr.qtype == QType::SOA && soarc) { + // XXX skip extra SOA? + continue; + } + std::shared_ptr drc; + try { + drc = DNSRecordContent::mastermake(dnsrr.qtype, QClass::IN, dnsrr.content); + } + catch (const PDNSException& pe) { + std::string err = "Bad record content in record for '" + dnsrr.qname.toStringNoDot() + "'|" + dnsrr.qtype.toString() + ": "+ pe.reason; + throw PDNSException(err); + } + catch (const std::exception& e) { + std::string err = "Bad record content in record for '" + dnsrr.qname.toStringNoDot() + "|" + dnsrr.qtype.toString() + "': " + e.what(); + throw PDNSException(err); + } + if (dnsrr.qtype == QType::SOA && dnsrr.qname == zone) { + soarc = std::dynamic_pointer_cast(drc); + } + if (dnsrr.qtype == QType::ZONEMD && dnsrr.qname == zone) { + auto zonemd = std::dynamic_pointer_cast(drc); + auto inserted = zonemdRecords.insert(pair(pair(zonemd->d_scheme, zonemd->d_hashalgo), zonemd)).second; + if (!inserted) { + throw PDNSException("Duplicate ZONEMD record"); + } + } + rrSetKey_t key = std::pair(dnsrr.qname, dnsrr.qtype); + RRsets[key].push_back(drc); + RRsetTTLs[key] = dnsrr.ttl; + } + + // Determine which digests to compute based on accepted zonemd records present + unique_ptr sha384digest{nullptr}, sha512digest{nullptr}; + + for (const auto& [k, zonemd] : zonemdRecords) { + if (zonemd->d_serial != soarc->d_st.serial) { + // The SOA Serial field MUST exactly match the ZONEMD Serial + // field. If the fields do not match, digest verification MUST + // NOT be considered successful with this ZONEMD RR. + continue; + } + if (zonemd->d_scheme != 1) { + // The Scheme field MUST be checked. If the verifier does not + // support the given scheme, verification MUST NOT be considered + // successful with this ZONEMD RR. + continue; + } + // The Hash Algorithm field MUST be checked. If the verifier does + // not support the given hash algorithm, verification MUST NOT be + // considered successful with this ZONEMD RR. + if (zonemd->d_hashalgo == 1) { + sha384digest = make_unique(384); + } + else if (zonemd->d_hashalgo == 2) { + sha512digest = make_unique(512); + } + } + + // A little helper + auto hash = [&sha384digest, &sha512digest](const std::string& msg) { + if (sha384digest) { + sha384digest->process(msg, msg.size()); + } + if (sha512digest) { + sha512digest->process(msg, msg.size()); + } + }; + + // Compute requested digests + for (auto& rrset: RRsets) { + const auto& qname = rrset.first.first; + const auto& qtype = rrset.first.second; + if (qtype == QType::ZONEMD && qname == zone) { + continue; // the apex ZONEMD is not digested + } + + sortedRecords_t sorted; + for (auto& rr: rrset.second) { + if (qtype == QType::RRSIG) { + const auto rrsig = std::dynamic_pointer_cast(rr); + if (rrsig->d_type == QType::ZONEMD && qname == zone) { + continue; + } + } + sorted.insert(rr); + } + + if (qtype != QType::RRSIG) { + RRSIGRecordContent rrc; + rrc.d_originalttl = RRsetTTLs[rrset.first]; + rrc.d_type = qtype; + auto msg = getMessageForRRSET(qname, rrc, sorted, false, false); + hash(msg); + } else { + // RRSIG is special, since original TTL depends on qtype covered by RRSIG + // which can be different per record + for (const auto& rrsig : sorted) { + auto rrsigc = std::dynamic_pointer_cast(rrsig); + RRSIGRecordContent rrc; + rrc.d_originalttl = RRsetTTLs[pair(rrset.first.first, rrsigc->d_type)]; + rrc.d_type = qtype; + auto msg = getMessageForRRSET(qname, rrc, { rrsigc }, false, false); + hash(msg); + } + } + } + + // Final verify + for (const auto& [k, zonemd] : zonemdRecords) { + if (zonemd->d_serial != soarc->d_st.serial) { + // The SOA Serial field MUST exactly match the ZONEMD Serial + // field. If the fields do not match, digest verification MUST + // NOT be considered successful with this ZONEMD RR. + continue; + } + if (zonemd->d_scheme != 1) { + // The Scheme field MUST be checked. If the verifier does not + // support the given scheme, verification MUST NOT be considered + // successful with this ZONEMD RR. + continue; + } + // The Hash Algorithm field MUST be checked. If the verifier does + // not support the given hash algorithm, verification MUST NOT be + // considered successful with this ZONEMD RR. + if (zonemd->d_hashalgo == 1) { + validationDone = true; + if (constantTimeStringEquals(zonemd->d_digest, sha384digest->digest())) { + validationOK = true; + break; // Per RFC: a single succeeding validation is enough + } + } + else if (zonemd->d_hashalgo == 2) { + validationDone = true; + if (constantTimeStringEquals(zonemd->d_digest, sha512digest->digest())) { + validationOK = true; + break; // Per RFC: a single succeeding validation is enough + } + } + } +} diff --git a/pdns/zonemd.hh b/pdns/zonemd.hh new file mode 100644 index 0000000000..f4ef53bc74 --- /dev/null +++ b/pdns/zonemd.hh @@ -0,0 +1,33 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include "config.h" + +class DNSName; +class ZoneParserTNG; + +namespace pdns +{ + void zonemdVerify(const DNSName& zone, ZoneParserTNG &zpt, bool& validationDone, bool& validationOK); + +} -- 2.47.2