]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Refactor zonemd verify code out of pdnsutil
authorOtto <otto.moerbeek@open-xchange.com>
Tue, 14 Dec 2021 13:16:08 +0000 (14:16 +0100)
committerOtto <otto.moerbeek@open-xchange.com>
Tue, 14 Dec 2021 14:29:04 +0000 (15:29 +0100)
pdns/Makefile.am
pdns/pdnsutil.cc
pdns/sha.hh
pdns/zonemd.cc [new file with mode: 0644]
pdns/zonemd.hh [new file with mode: 0644]

index 004c8becfedcaaa09dfa202ffd7a63fb22e45d3a..3c762b2deb7b8f544baedbc33225de3de94ce1ea 100644 (file)
@@ -378,6 +378,7 @@ pdnsutil_SOURCES = \
        tsigutils.hh tsigutils.cc \
        ueberbackend.cc \
        unix_utility.cc \
+       zonemd.hh zonemd.cc \
        zoneparser-tng.cc
 
 pdnsutil_LDFLAGS = \
index ccbe0f4c3aa2da1328ffbebc2e352da2c52aef78..b929b375fe316e349e646df08796278655596424 100644 (file)
@@ -27,6 +27,7 @@
 #include "dns_random.hh"
 #include "ipcipher.hh"
 #include "misc.hh"
+#include "zonemd.hh"
 #include <fstream>
 #include <utility>
 #include <termios.h>            //termios, TCSANOW, ECHO, ICANON
@@ -39,8 +40,6 @@
 #include "bind-dnssec.schema.sqlite3.sql.h"
 #endif
 
-#include <openssl/evp.h>
-
 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<DNSName, QType> rrSetKey_t;
-  typedef std::vector<std::shared_ptr<DNSRecordContent>> rrVector_t;
-
-  struct CanonrrSetKeyCompare: public std::binary_function<rrSetKey_t, rrSetKey_t, bool>
-  {
-    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<rrSetKey_t, rrVector_t, CanonrrSetKeyCompare> RRsetMap_t;
-
-  RRsetMap_t RRsets;
-  std::map<rrSetKey_t, uint32_t> RRsetTTLs;
-
-  DNSResourceRecord rr;
-  std::map<std::pair<uint8_t, uint8_t>, std::shared_ptr<ZONEMDRecordContent>> zonemdRecords;
-  std::shared_ptr<SOARecordContent> 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<unsigned char*>(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 '"<<rr.qname<<"' which is not part of zone '"<<zone<<"'"<<endl;
-      return EXIT_FAILURE;
-    }
-    if (rr.qtype == QType::SOA) {
-      if (soarc)
-        continue;
-    }
-    std::shared_ptr<DNSRecordContent> drc;
-    try {
-      drc = DNSRecordContent::mastermake(rr.qtype, QClass::IN, rr.content);
-    }
-    catch (const PDNSException &pe) {
-      cerr<<"Bad record content in record for "<<rr.qname<<"|"<<rr.qtype<<": "<<pe.reason<<endl;
-      return EXIT_FAILURE;
-    }
-    catch (const std::exception &e) {
-      cerr<<"Bad record content in record for "<<rr.qname<<"|"<<rr.qtype<<": "<<e.what()<<endl;
-      return EXIT_FAILURE;
-    }
-    if (rr.qtype == QType::SOA && rr.qname == zone) {
-      soarc = std::dynamic_pointer_cast<SOARecordContent>(drc);
-    }
-    if (rr.qtype == QType::ZONEMD && rr.qname == zone) {
-      auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<SHADigest> 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<SHADigest>(384);
-    }
-    else if (zonemd->d_hashalgo == 2) {
-      sha512digest = make_unique<SHADigest>(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<RRSIGRecordContent>(_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<RRSIGRecordContent>(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;
 }
index 06bace9ab7b10068f421c339b8f75604169c2b11..7284dda113254ae8a5db74d9f2876bb9ec539702 100644 (file)
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
 #pragma once
+
 #include <string>
-#include <stdint.h>
 #include <openssl/sha.h>
+#include <openssl/evp.h>
 
 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<const unsigned char*>(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<unsigned char*>(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 (file)
index 0000000..c52f101
--- /dev/null
@@ -0,0 +1,184 @@
+#include "zonemd.hh"
+
+#include "dnsrecords.hh"
+#include "dnssecinfra.hh"
+#include "sha.hh"
+#include "zoneparser-tng.hh"
+
+typedef std::pair<DNSName, QType> rrSetKey_t;
+typedef std::vector<std::shared_ptr<DNSRecordContent>> rrVector_t;
+
+struct CanonrrSetKeyCompare: public std::binary_function<rrSetKey_t, rrSetKey_t, bool>
+{
+  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<rrSetKey_t, rrVector_t, CanonrrSetKeyCompare> RRsetMap_t;
+
+RRsetMap_t RRsets;
+std::map<rrSetKey_t, uint32_t> RRsetTTLs;
+
+void pdns::zonemdVerify(const DNSName& zone, ZoneParserTNG &zpt, bool& validationDone, bool& validationOK)
+{
+  validationDone = false;
+  validationOK = false;
+
+  DNSResourceRecord dnsrr;
+  std::map<std::pair<uint8_t, uint8_t>, std::shared_ptr<ZONEMDRecordContent>> zonemdRecords;
+  std::shared_ptr<SOARecordContent> 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<DNSRecordContent> 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<SOARecordContent>(drc);
+    }
+    if (dnsrr.qtype == QType::ZONEMD && dnsrr.qname == zone) {
+      auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<pdns::SHADigest> 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<pdns::SHADigest>(384);
+    }
+    else if (zonemd->d_hashalgo == 2) {
+      sha512digest = make_unique<pdns::SHADigest>(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<RRSIGRecordContent>(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<RRSIGRecordContent>(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 (file)
index 0000000..f4ef53b
--- /dev/null
@@ -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);
+
+}