]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Support the import of ECDSA keys from PEM files
authorFred Morcos <fred.morcos@open-xchange.com>
Sun, 27 Feb 2022 19:23:29 +0000 (20:23 +0100)
committerFred Morcos <fred.morcos@open-xchange.com>
Thu, 17 Mar 2022 10:33:39 +0000 (11:33 +0100)
- Add support to import ECDSA keys from PEM files.
- Adds a test for ECDSA key import from PEM.
- Instantiate key engine without trial-and-error algorithm search.

Part of #11325

Co-authored-by: Peter van Dijk <peter.van.dijk@powerdns.com>
pdns/dnssecinfra.cc
pdns/dnssecinfra.hh
pdns/opensslsigners.cc
pdns/pdnsutil.cc
pdns/pkcs11signers.hh
pdns/test-signers.cc

index 63fb16fe8dd07311ba63baf76e173099a489f82c..92f80da46977c8fdf7ebe8d7be9c9f0c90039f6e 100644 (file)
@@ -168,21 +168,11 @@ std::unique_ptr<DNSCryptoKeyEngine> DNSCryptoKeyEngine::makeFromISCString(DNSKEY
   return dpk;
 }
 
-std::unique_ptr<DNSCryptoKeyEngine> DNSCryptoKeyEngine::makeFromPEMString(DNSKEYRecordContent& drc, const std::string& raw)
+std::unique_ptr<DNSCryptoKeyEngine> DNSCryptoKeyEngine::makeFromPEMFile(DNSKEYRecordContent& drc, const std::string& filename, std::FILE& fp, const uint8_t algorithm)
 {
-  for (const makers_t::value_type& maker : getMakers()) {
-    std::unique_ptr<DNSCryptoKeyEngine> ret = nullptr;
-
-    try {
-      ret = maker.second(maker.first);
-      ret->fromPEMString(drc, raw);
-      return ret;
-    }
-    catch (...) {
-    }
-  }
-
-  return nullptr;
+  auto maker = DNSCryptoKeyEngine::make(algorithm);
+  maker->createFromPEMFile(drc, filename, fp);
+  return maker;
 }
 
 std::string DNSCryptoKeyEngine::convertToISC() const
index 97939917edd62e49d6a7f3bed6dc711932cab21c..9341c2fb67b84df1c2650de6f4f9c49f10482ec7 100644 (file)
@@ -41,6 +41,10 @@ class DNSCryptoKeyEngine
     typedef std::map<std::string, std::string> stormap_t;
     typedef std::vector<std::pair<std::string, std::string > > storvector_t;
     virtual void create(unsigned int bits)=0;
+    virtual void createFromPEMFile(DNSKEYRecordContent& drc, const std::string& filename, std::FILE& fp)
+    {
+      throw std::runtime_error("Can't create key from PEM file");
+    }
     virtual storvector_t convertToISCVector() const =0;
     std::string convertToISC() const ;
     virtual std::string sign(const std::string& msg) const =0;
@@ -60,18 +64,36 @@ class DNSCryptoKeyEngine
     }
 
     virtual void fromISCMap(DNSKEYRecordContent& drc, stormap_t& stormap) = 0;
-    virtual void fromPEMString(DNSKEYRecordContent& drc, const std::string& raw)
-    {
-      throw std::runtime_error("Can't import from PEM string");
-    }
     virtual void fromPublicKeyString(const std::string& content) = 0;
     virtual bool checkKey(vector<string>* errorMessages = nullptr) const
     {
       return true;
     }
     static std::unique_ptr<DNSCryptoKeyEngine> makeFromISCFile(DNSKEYRecordContent& drc, const char* fname);
+
+    /**
+     * \brief Creates a key engine from a PEM file.
+     *
+     * Receives an open file handle with PEM contents and creates a key
+     * engine corresponding to the algorithm requested.
+     *
+     * \param[in] drc Key record contents to be populated.
+     *
+     * \param[in] filename Only used for providing filename information
+     * in error messages.
+     *
+     * \param[in] fp An open file handle to a file containing PEM
+     * contents.
+     *
+     * \param[in] algorithm Which algorithm to use. See
+     * https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml
+     *
+     * \return A key engine corresponding to the requested algorithm and
+     * populated with the contents of the PEM file.
+     */
+    static std::unique_ptr<DNSCryptoKeyEngine> makeFromPEMFile(DNSKEYRecordContent& drc, const std::string& filename, std::FILE& fp, uint8_t algorithm);
+
     static std::unique_ptr<DNSCryptoKeyEngine> makeFromISCString(DNSKEYRecordContent& drc, const std::string& content);
-    static std::unique_ptr<DNSCryptoKeyEngine> makeFromPEMString(DNSKEYRecordContent& drc, const std::string& raw);
     static std::unique_ptr<DNSCryptoKeyEngine> makeFromPublicKeyString(unsigned int algorithm, const std::string& raw);
     static std::unique_ptr<DNSCryptoKeyEngine> make(unsigned int algorithm);
     static bool isAlgorithmSupported(unsigned int algo);
index 2b7a863d3c146bffde6bdd6e98469ca2ac9012b9..e6c4f0a4214bf2ddc4fb3834c608534e98ab119b 100644 (file)
@@ -35,6 +35,7 @@
 #include <openssl/rsa.h>
 #include <openssl/opensslv.h>
 #include <openssl/err.h>
+#include <openssl/pem.h>
 #include "opensslsigners.hh"
 #include "dnssecinfra.hh"
 #include "dnsseckeeper.hh"
@@ -592,6 +593,26 @@ public:
   int getBits() const override { return d_len << 3; }
 
   void create(unsigned int bits) override;
+
+  /**
+   * \brief Creates an ECDSA key engine from a PEM file.
+   *
+   * Receives an open file handle with PEM contents and creates an ECDSA
+   * key engine.
+   *
+   * \param[in] drc Key record contents to be populated.
+   *
+   * \param[in] filename Only used for providing filename information
+   * in error messages.
+   *
+   * \param[in] fp An open file handle to a file containing ECDSA PEM
+   * contents.
+   *
+   * \return An ECDSA key engine populated with the contents of the PEM
+   * file.
+   */
+  void createFromPEMFile(DNSKEYRecordContent& drc, const std::string& filename, std::FILE& fp) override;
+
   storvector_t convertToISCVector() const override;
   std::string hash(const std::string& hash) const override;
   std::string sign(const std::string& hash) const override;
@@ -627,6 +648,39 @@ void OpenSSLECDSADNSCryptoKeyEngine::create(unsigned int bits)
   }
 }
 
+void OpenSSLECDSADNSCryptoKeyEngine::createFromPEMFile(DNSKEYRecordContent& drc, const string& filename, std::FILE& fp)
+{
+  drc.d_algorithm = d_algorithm;
+  d_eckey = std::unique_ptr<EC_KEY, decltype(&EC_KEY_free)>(PEM_read_ECPrivateKey(&fp, nullptr, nullptr, nullptr), &EC_KEY_free);
+  if (d_eckey == nullptr) {
+    throw runtime_error(getName() + ": Failed to read private key from PEM file `" + filename + "`");
+  }
+
+  int ret = EC_KEY_set_group(d_eckey.get(), d_ecgroup.get());
+  if (ret != 1) {
+    throw runtime_error(getName() + " setting key group failed");
+  }
+
+  const BIGNUM* privateKeyBN = EC_KEY_get0_private_key(d_eckey.get());
+
+  auto pub_key = std::unique_ptr<EC_POINT, void (*)(EC_POINT*)>(EC_POINT_new(d_ecgroup.get()), EC_POINT_free);
+  if (!pub_key) {
+    throw runtime_error(getName() + " allocation of public key point failed");
+  }
+
+  ret = EC_POINT_mul(d_ecgroup.get(), pub_key.get(), privateKeyBN, nullptr, nullptr, nullptr);
+  if (ret != 1) {
+    throw runtime_error(getName() + " computing public key from private failed");
+  }
+
+  ret = EC_KEY_set_public_key(d_eckey.get(), pub_key.get());
+  if (ret != 1) {
+    ERR_print_errors_fp(stderr);
+    throw runtime_error(getName() + " setting public key failed");
+  }
+
+  EC_KEY_set_asn1_flag(d_eckey.get(), OPENSSL_EC_NAMED_CURVE);
+}
 
 DNSCryptoKeyEngine::storvector_t OpenSSLECDSADNSCryptoKeyEngine::convertToISCVector() const
 {
index 325b259ddf9bafc22230064261a475c85a785f22..285d9bbbf1e1686bc1c86413f9b6f1c21bc003fc 100644 (file)
@@ -30,6 +30,7 @@
 #include "zonemd.hh"
 #include <fstream>
 #include <utility>
+#include <cerrno>
 #include <termios.h>            //termios, TCSANOW, ECHO, ICANON
 #include "opensslsigners.hh"
 #ifdef HAVE_LIBSODIUM
@@ -3284,34 +3285,28 @@ try
       return 1;
     }
 
-    string zone = cmds.at(1);
-    string fname = cmds.at(2);
+    const string zone = cmds.at(1);
+    const string filename = cmds.at(2);
+    const auto algorithm = pdns::checked_stoi<unsigned int>(cmds.at(3));
 
-    ifstream ifs(fname.c_str());
-    string line;
-    string interim;
-    while (getline(ifs, line)) {
-      if (line[0] == '-') {
-        continue;
-      }
-      boost::trim(line);
-      interim += line;
+    errno = 0;
+    std::unique_ptr<std::FILE, decltype(&std::fclose)> fp{std::fopen(filename.c_str(), "r"), &std::fclose};
+    if (fp == nullptr) {
+      auto errMsg = pdns::getMessageFromErrno(errno);
+      throw runtime_error("Failed to open PEM file `" + filename + "`: " + errMsg);
     }
 
-    string raw;
-    B64Decode(interim, raw);
-
-    DNSSECPrivateKey dpk;
     DNSKEYRecordContent drc;
-    shared_ptr<DNSCryptoKeyEngine> key(DNSCryptoKeyEngine::makeFromPEMString(drc, raw));
+    shared_ptr<DNSCryptoKeyEngine> key{DNSCryptoKeyEngine::makeFromPEMFile(drc, filename, *fp, algorithm)};
     if (!key) {
       cerr << "Could not convert key from PEM to internal format" << endl;
       return 1;
     }
+
+    DNSSECPrivateKey dpk;
     dpk.setKey(key);
 
     pdns::checked_stoi_into(dpk.d_algorithm, cmds.at(3));
-
     if (dpk.d_algorithm == DNSSECKeeper::RSASHA1NSEC3SHA1) {
       dpk.d_algorithm = DNSSECKeeper::RSASHA1;
     }
index 992fb228e35906389bc976f54c7d8c7fdbe874a5..831493deb29e975f808bf2a78af1954a6d618417 100644 (file)
@@ -59,7 +59,6 @@ class PKCS11DNSCryptoKeyEngine : public DNSCryptoKeyEngine
 
     void fromISCMap(DNSKEYRecordContent& drc, stormap_t& stormap) override;
 
-    void fromPEMString(DNSKEYRecordContent& drc, const std::string& raw) override { throw "Unimplemented"; };
     void fromPublicKeyString(const std::string& content) override { throw "Unimplemented"; };
 
     static std::unique_ptr<DNSCryptoKeyEngine> maker(unsigned int algorithm);
index 4e2da007381f6395517d06e66da161425b909bf8..21654f7ec7b95c026cda3c964b06dc208fb1fb6b 100644 (file)
 #include "dnssecinfra.hh"
 #include "misc.hh"
 
+#include <cstdio>
+
 BOOST_AUTO_TEST_SUITE(test_signers)
 
 static const std::string message = "Very good, young padawan.";
 
-static const struct signerParams
+struct SignerParams
 {
   std::string iscMap;
   std::string dsSHA1;
@@ -32,9 +34,14 @@ static const struct signerParams
   uint16_t rfcFlags;
   uint8_t algorithm;
   bool isDeterministic;
-} signers[] = {
+  std::optional<std::string> pem;
+};
+
+static const std::array<struct SignerParams, 3> signers
+{
   /* RSA from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sample_keys.h */
-  { "Algorithm: 8\n"
+  SignerParams{
+    "Algorithm: 8\n"
     "Modulus: qtunSiHnYq4XRLBehKAw1Glxb+48oIpAC7w3Jhpj570bb2uHt6orWGqnuyRtK8oqUi2ABoV0PFm8+IPgDMEdCQ==\n"
     "PublicExponent: AQAB\n"
     "PrivateExponent: MiItniUAngXzMeaGdWgDq/AcpvlCtOCcFlVt4TJRKkfp8DNRSxIxG53NNlOFkp1W00iLHqYC2GrH1qkKgT9l+Q==\n"
@@ -43,11 +50,19 @@ static const struct signerParams
     "Exponent1: WuUwhjfN1+4djlrMxHmisixWNfpwI1Eg7Ss/UXsnrMk=\n"
     "Exponent2: vfMqas1cNsXRqP3Fym6D2Pl2BRuTQBv5E1B/ZrmQPTk=\n"
     "Coefficient: Q10z43cA3hkwOkKsj5T0W5jrX97LBwZoY5lIjDCa4+M=\n",
+
     "1506 8 1 172a500b374158d1a64ba3073cdbbc319b2fdf2c",
     "1506 8 2 253b099ff47b02c6ffa52695a30a94c6681c56befe0e71a5077d6f79514972f9",
     "1506 8 4 22ea940600dc2d9a98b1126c26ac0dc5c91b31eb50fe784b36ad675e9eecfe6573c1f85c53b6bc94580f3ac443d13c4c",
+
+    // clang-format off
     /* from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sign.c */
-    { 0x93, 0x93, 0x5f, 0xd8, 0xa1, 0x2b, 0x4c, 0x0b, 0xf3, 0x67, 0x42, 0x13, 0x52, 0x00, 0x35, 0xdc, 0x09, 0xe0, 0xdf, 0xe0, 0x3e, 0xc2, 0xcf, 0x64, 0xab, 0x9f, 0x9f, 0x51, 0x5f, 0x5c, 0x27, 0xbe, 0x13, 0xd6, 0x17, 0x07, 0xa6, 0xe4, 0x3b, 0x63, 0x44, 0x85, 0x06, 0x13, 0xaa, 0x01, 0x3c, 0x58, 0x52, 0xa3, 0x98, 0x20, 0x65, 0x03, 0xd0, 0x40, 0xc8, 0xa0, 0xe9, 0xd2, 0xc0, 0x03, 0x5a, 0xab },
+    { 0x93, 0x93, 0x5f, 0xd8, 0xa1, 0x2b, 0x4c, 0x0b, 0xf3, 0x67, 0x42, 0x13, 0x52, 0x00, 0x35, 0xdc,
+      0x09, 0xe0, 0xdf, 0xe0, 0x3e, 0xc2, 0xcf, 0x64, 0xab, 0x9f, 0x9f, 0x51, 0x5f, 0x5c, 0x27, 0xbe,
+      0x13, 0xd6, 0x17, 0x07, 0xa6, 0xe4, 0x3b, 0x63, 0x44, 0x85, 0x06, 0x13, 0xaa, 0x01, 0x3c, 0x58,
+      0x52, 0xa3, 0x98, 0x20, 0x65, 0x03, 0xd0, 0x40, 0xc8, 0xa0, 0xe9, 0xd2, 0xc0, 0x03, 0x5a, 0xab },
+    // clang-format on
+
     "256 3 8 AwEAAarbp0oh52KuF0SwXoSgMNRpcW/uPKCKQAu8NyYaY+e9G29rh7eqK1hqp7skbSvKKlItgAaFdDxZvPiD4AzBHQk=",
     "rsa.",
     "",
@@ -56,54 +71,89 @@ static const struct signerParams
     256,
     0,
     DNSSECKeeper::RSASHA256,
-    true
-  },
+    true,
+
+    std::nullopt},
+
 #ifdef HAVE_LIBCRYPTO_ECDSA
-  /* ECDSA-P256-SHA256 from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sample_keys.h */
-  { "Algorithm: 13\n"
-    "PrivateKey: iyLIPdk3DOIxVmmSYlmTstbtUPiVlEyDX46psyCwNVQ=\n",
-    "5345 13 1 954103ac7c43810ce9f414e80f30ab1cbe49b236",
-    "5345 13 2 bac2107036e735b50f85006ce409a19a3438cab272e70769ebda032239a3d0ca",
-    "5345 13 4 a0ac6790483872be72a258314200a88ab75cdd70f66a18a09f0f414c074df0989fdb1df0e67d82d4312cda67b93a76c1",
-    /* from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sign.c */
-    { 0xa2, 0x95, 0x76, 0xb5, 0xf5, 0x7e, 0xbd, 0xdd, 0xf5, 0x62, 0xa2, 0xc3, 0xa4, 0x8d, 0xd4, 0x53, 0x5c, 0xba, 0x29, 0x71,  0x8c, 0xcc, 0x28, 0x7b, 0x58, 0xf3, 0x1e, 0x4e, 0x58, 0xe2, 0x36, 0x7e, 0xa0, 0x1a, 0xb6, 0xe6, 0x29, 0x71, 0x1b, 0xd3, 0x8c, 0x88, 0xc3, 0xee, 0x12, 0x0e, 0x69, 0x70, 0x55, 0x99, 0xec, 0xd5, 0xf6, 0x4f, 0x4b, 0xe2, 0x41, 0xd9, 0x10, 0x7e, 0x67, 0xe5, 0xad, 0x2f, },
-    "256 3 13 8uD7C4THTM/w7uhryRSToeE/jKT78/p853RX0L5EwrZrSLBubLPiBw7gbvUP6SsIga5ZQ4CSAxNmYA/gZsuXzA==",
-    "ecdsa.",
-    "",
-    "",
-    256,
-    256,
-    0,
-    DNSSECKeeper::ECDSA256,
-    false
-  },
+    /* ECDSA-P256-SHA256 from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sample_keys.h */
+    SignerParams{
+      "Algorithm: 13\n"
+      "PrivateKey: iyLIPdk3DOIxVmmSYlmTstbtUPiVlEyDX46psyCwNVQ=\n",
+
+      "5345 13 1 954103ac7c43810ce9f414e80f30ab1cbe49b236",
+      "5345 13 2 bac2107036e735b50f85006ce409a19a3438cab272e70769ebda032239a3d0ca",
+      "5345 13 4 a0ac6790483872be72a258314200a88ab75cdd70f66a18a09f0f414c074df0989fdb1df0e67d82d4312cda67b93a76c1",
+
+      // clang-format off
+      /* from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sign.c */
+      { 0xa2, 0x95, 0x76, 0xb5, 0xf5, 0x7e, 0xbd, 0xdd, 0xf5, 0x62, 0xa2, 0xc3, 0xa4, 0x8d, 0xd4, 0x53,
+        0x5c, 0xba, 0x29, 0x71, 0x8c, 0xcc, 0x28, 0x7b, 0x58, 0xf3, 0x1e, 0x4e, 0x58, 0xe2, 0x36, 0x7e,
+        0xa0, 0x1a, 0xb6, 0xe6, 0x29, 0x71, 0x1b, 0xd3, 0x8c, 0x88, 0xc3, 0xee, 0x12, 0x0e, 0x69, 0x70,
+        0x55, 0x99, 0xec, 0xd5, 0xf6, 0x4f, 0x4b, 0xe2, 0x41, 0xd9, 0x10, 0x7e, 0x67, 0xe5, 0xad, 0x2f },
+      // clang-format on
+
+      "256 3 13 8uD7C4THTM/w7uhryRSToeE/jKT78/p853RX0L5EwrZrSLBubLPiBw7gbvUP6SsIga5ZQ4CSAxNmYA/gZsuXzA==",
+      "ecdsa.",
+      "",
+      "",
+      256,
+      256,
+      0,
+      DNSSECKeeper::ECDSA256,
+      false,
+
+      std::make_optional(std::string{
+        "-----BEGIN EC PRIVATE KEY-----\n"
+        "MHcCAQEEIIsiyD3ZNwziMVZpkmJZk7LW7VD4lZRMg1+OqbMgsDVUoAoGCCqGSM49\n"
+        "AwEHoUQDQgAE8uD7C4THTM/w7uhryRSToeE/jKT78/p853RX0L5EwrZrSLBubLPi\n"
+        "Bw7gbvUP6SsIga5ZQ4CSAxNmYA/gZsuXzA==\n"
+        "-----END EC PRIVATE KEY-----\n"})},
 #endif /* HAVE_LIBCRYPTO_ECDSA */
+
 #if defined(HAVE_LIBSODIUM) || defined(HAVE_LIBDECAF) || defined(HAVE_LIBCRYPTO_ED25519)
-  /* ed25519 from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sample_keys.h,
-     also from rfc8080 section 6.1 */
-  { "Algorithm: 15\n"
-    "PrivateKey: ODIyNjAzODQ2MjgwODAxMjI2NDUxOTAyMDQxNDIyNjI=\n",
-    "3612 15 1 501249721e1f09a79d30d5c6c4dca1dc1da4ed5d",
-    "3612 15 2 1b1c8766b2a96566ff196f77c0c4194af86aaa109c5346ff60231a27d2b07ac0",
-    "3612 15 4 d11831153af4985efbd0ae792c967eb4aff3c35488db95f7e2f85dcec74ae8f59f9a72641798c91c67c675db1d710c18",
-    /* from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sign.c */
-    { 0x0a, 0x9e, 0x51, 0x5f, 0x16, 0x89, 0x49, 0x27, 0x0e, 0x98, 0x34, 0xd3, 0x48, 0xef, 0x5a, 0x6e, 0x85, 0x2f, 0x7c, 0xd6, 0xd7, 0xc8, 0xd0, 0xf4, 0x2c, 0x68, 0x8c, 0x1f, 0xf7, 0xdf, 0xeb, 0x7c, 0x25, 0xd6, 0x1a, 0x76, 0x3e, 0xaf, 0x28, 0x1f, 0x1d, 0x08, 0x10, 0x20, 0x1c, 0x01, 0x77, 0x1b, 0x5a, 0x48, 0xd6, 0xe5, 0x1c, 0xf9, 0xe3, 0xe0, 0x70, 0x34, 0x5e, 0x02, 0x49, 0xfb, 0x9e, 0x05 },
-    "256 3 15 l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=",
-    "ed25519.",
-    // vector extracted from https://gitlab.labs.nic.cz/labs/ietf/blob/master/dnskey.py (rev 476d6ded) by printing signature_data
-    "00 0f 0f 02 00 00 0e 10 55 d4 fc 60 55 b9 4c e0 0e 1d 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 0f 00 01 00 00 0e 10 00 14 00 0a 04 6d 61 69 6c 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 ",
-    // vector verified from dnskey.py as above, and confirmed with https://www.rfc-editor.org/errata_search.php?rfc=8080&eid=4935
-    "oL9krJun7xfBOIWcGHi7mag5/hdZrKWw15jPGrHpjQeRAvTdszaPD+QLs3fx8A4M3e23mRZ9VrbpMngwcrqNAg==",
-    256,
-    256,
-    257,
-    DNSSECKeeper::ED25519,
-    true
-  },
+    /* ed25519 from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sample_keys.h,
+       also from rfc8080 section 6.1 */
+    SignerParams{
+      "Algorithm: 15\n"
+      "PrivateKey: ODIyNjAzODQ2MjgwODAxMjI2NDUxOTAyMDQxNDIyNjI=\n",
+
+      "3612 15 1 501249721e1f09a79d30d5c6c4dca1dc1da4ed5d",
+      "3612 15 2 1b1c8766b2a96566ff196f77c0c4194af86aaa109c5346ff60231a27d2b07ac0",
+      "3612 15 4 d11831153af4985efbd0ae792c967eb4aff3c35488db95f7e2f85dcec74ae8f59f9a72641798c91c67c675db1d710c18",
+
+      // clang-format off
+      /* from https://github.com/CZ-NIC/knot/blob/master/src/dnssec/tests/sign.c */
+      { 0x0a, 0x9e, 0x51, 0x5f, 0x16, 0x89, 0x49, 0x27, 0x0e, 0x98, 0x34, 0xd3, 0x48, 0xef, 0x5a, 0x6e,
+        0x85, 0x2f, 0x7c, 0xd6, 0xd7, 0xc8, 0xd0, 0xf4, 0x2c, 0x68, 0x8c, 0x1f, 0xf7, 0xdf, 0xeb, 0x7c,
+        0x25, 0xd6, 0x1a, 0x76, 0x3e, 0xaf, 0x28, 0x1f, 0x1d, 0x08, 0x10, 0x20, 0x1c, 0x01, 0x77, 0x1b,
+        0x5a, 0x48, 0xd6, 0xe5, 0x1c, 0xf9, 0xe3, 0xe0, 0x70, 0x34, 0x5e, 0x02, 0x49, 0xfb, 0x9e, 0x05 },
+      // clang-format on
+
+      "256 3 15 l02Woi0iS8Aa25FQkUd9RMzZHJpBoRQwAQEX1SxZJA4=",
+      "ed25519.",
+
+      // vector extracted from https://gitlab.labs.nic.cz/labs/ietf/blob/master/dnskey.py
+      // (rev 476d6ded) by printing signature_data
+      "00 0f 0f 02 00 00 0e 10 55 d4 fc 60 55 b9 4c e0 0e 1d 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 "
+      "07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 0f 00 01 00 00 0e 10 00 14 00 0a 04 6d 61 69 6c 07 "
+      "65 78 61 6d 70 6c 65 03 63 6f 6d 00 ",
+
+      // vector verified from dnskey.py as above, and confirmed with
+      // https://www.rfc-editor.org/errata_search.php?rfc=8080&eid=4935
+      "oL9krJun7xfBOIWcGHi7mag5/hdZrKWw15jPGrHpjQeRAvTdszaPD+QLs3fx8A4M3e23mRZ9VrbpMngwcrqNAg==",
+
+      256,
+      256,
+      257,
+      DNSSECKeeper::ED25519,
+      true,
+
+      std::nullopt},
 #endif /* defined(HAVE_LIBSODIUM) || defined(HAVE_LIBDECAF) || defined(HAVE_LIBCRYPTO_ED25519) */
 };
 
-static void checkRR(const signerParams& signer)
+static void checkRR(const SignerParams& signer)
 {
   DNSKEYRecordContent drc;
   auto dcke = std::shared_ptr<DNSCryptoKeyEngine>(DNSCryptoKeyEngine::makeFromISCString(drc, signer.iscMap));
@@ -159,56 +209,73 @@ static void checkRR(const signerParams& signer)
   }
 }
 
-BOOST_AUTO_TEST_CASE(test_generic_signers)
+static auto test_generic_signer(std::shared_ptr<DNSCryptoKeyEngine> dcke, DNSKEYRecordContent& drc, const SignerParams& signer)
 {
-  for (const auto& signer : signers) {
-    DNSKEYRecordContent drc;
-    auto dcke = std::shared_ptr<DNSCryptoKeyEngine>(DNSCryptoKeyEngine::makeFromISCString(drc, signer.iscMap));
+  BOOST_CHECK_EQUAL(dcke->getAlgorithm(), signer.algorithm);
+  BOOST_CHECK_EQUAL(dcke->getBits(), signer.bits);
+  BOOST_CHECK_EQUAL(dcke->checkKey(nullptr), true);
+
+  BOOST_CHECK_EQUAL(drc.d_algorithm, signer.algorithm);
 
-    BOOST_CHECK_EQUAL(dcke->getAlgorithm(), signer.algorithm);
-    BOOST_CHECK_EQUAL(dcke->getBits(), signer.bits);
-    BOOST_CHECK_EQUAL(dcke->checkKey(nullptr), true);
+  DNSSECPrivateKey dpk;
+  dpk.setKey(dcke);
+  dpk.d_flags = signer.flags;
+  drc = dpk.getDNSKEY();
 
-    BOOST_CHECK_EQUAL(drc.d_algorithm, signer.algorithm);
+  BOOST_CHECK_EQUAL(drc.d_algorithm, signer.algorithm);
+  BOOST_CHECK_EQUAL(drc.d_protocol, 3);
+  BOOST_CHECK_EQUAL(drc.getZoneRepresentation(), signer.zoneRepresentation);
 
-    DNSSECPrivateKey dpk;
-    dpk.setKey(dcke);
-    dpk.d_flags = signer.flags;
-    drc = dpk.getDNSKEY();
-
-    BOOST_CHECK_EQUAL(drc.d_algorithm, signer.algorithm);
-    BOOST_CHECK_EQUAL(drc.d_protocol, 3);
-    BOOST_CHECK_EQUAL(drc.getZoneRepresentation(), signer.zoneRepresentation);
-
-    DNSName name(signer.name);
-    auto ds1 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA1);
-    if (!signer.dsSHA1.empty()) {
-      BOOST_CHECK_EQUAL(ds1.getZoneRepresentation(), signer.dsSHA1);
-    }
+  DNSName name(signer.name);
+  auto ds1 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA1);
+  if (!signer.dsSHA1.empty()) {
+    BOOST_CHECK_EQUAL(ds1.getZoneRepresentation(), signer.dsSHA1);
+  }
 
-    auto ds2 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA256);
-    if (!signer.dsSHA256.empty()) {
-      BOOST_CHECK_EQUAL(ds2.getZoneRepresentation(), signer.dsSHA256);
-    }
+  auto ds2 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA256);
+  if (!signer.dsSHA256.empty()) {
+    BOOST_CHECK_EQUAL(ds2.getZoneRepresentation(), signer.dsSHA256);
+  }
 
-    auto ds4 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA384);
-    if (!signer.dsSHA384.empty()) {
-      BOOST_CHECK_EQUAL(ds4.getZoneRepresentation(), signer.dsSHA384);
-    }
+  auto ds4 = makeDSFromDNSKey(name, drc, DNSSECKeeper::DIGEST_SHA384);
+  if (!signer.dsSHA384.empty()) {
+    BOOST_CHECK_EQUAL(ds4.getZoneRepresentation(), signer.dsSHA384);
+  }
 
-    auto signature = dcke->sign(message);
-    BOOST_CHECK(dcke->verify(message, signature));
+  auto signature = dcke->sign(message);
+  BOOST_CHECK(dcke->verify(message, signature));
 
-    if (signer.isDeterministic) {
-      BOOST_CHECK_EQUAL(signature, std::string(signer.signature.begin(), signer.signature.end()));
-    } else {
-      /* since the signing process is not deterministic, we can't directly compare our signature
-         with the one we have. Still the one we have should also validate correctly. */
-      BOOST_CHECK(dcke->verify(message, std::string(signer.signature.begin(), signer.signature.end())));
-    }
+  if (signer.isDeterministic) {
+    BOOST_CHECK_EQUAL(signature, std::string(signer.signature.begin(), signer.signature.end()));
+  }
+  else {
+    /* since the signing process is not deterministic, we can't directly compare our signature
+       with the one we have. Still the one we have should also validate correctly. */
+    BOOST_CHECK(dcke->verify(message, std::string(signer.signature.begin(), signer.signature.end())));
+  }
+
+  if (!signer.rfcMsgDump.empty() && !signer.rfcB64Signature.empty()) {
+    checkRR(signer);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_generic_signers)
+{
+  for (const auto& signer : signers) {
+    DNSKEYRecordContent drc;
+    auto dcke = std::shared_ptr<DNSCryptoKeyEngine>(DNSCryptoKeyEngine::makeFromISCString(drc, signer.iscMap));
+    test_generic_signer(dcke, drc, signer);
+
+    if (signer.pem.has_value()) {
+      unique_ptr<std::FILE, decltype(&std::fclose)> fp{fmemopen((void*)signer.pem->c_str(), signer.pem->length(), "r"), &std::fclose};
+      BOOST_REQUIRE(fp.get() != nullptr);
+
+      DNSKEYRecordContent pemDRC;
+      shared_ptr<DNSCryptoKeyEngine> pemKey{DNSCryptoKeyEngine::makeFromPEMFile(pemDRC, "<buffer>", *fp, signer.algorithm)};
+
+      BOOST_CHECK_EQUAL(pemKey->convertToISC(), dcke->convertToISC());
 
-    if (!signer.rfcMsgDump.empty() && !signer.rfcB64Signature.empty()) {
-      checkRR(signer);
+      test_generic_signer(pemKey, pemDRC, signer);
     }
   }
 }