Repository: randombit/botan
Pull request #2726 (2021-04-24 13:20:27)

Implement draft-irtf-cfrg-hash-to-curve
Specifically SSWU with xmd based expansion

Currently only P-256, P-384 and P-521 are supported but in principle this could be extended to most curves except those with A*B == 0


Patch:
diff --git a/src/cli/speed.cpp b/src/cli/speed.cpp
index cc9cfb09a..5244b66e6 100644
--- a/src/cli/speed.cpp
+++ b/src/cli/speed.cpp
@@ -748,6 +748,12 @@ class Speed final : public Command
                {
                bench_os2ecp(ecc_groups, msec);
                }
+#endif
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+            else if(algo == "ec_h2c")
+               {
+               bench_ec_h2c(msec);
+               }
 #endif
             else if(algo == "RNG")
                {
@@ -1223,6 +1229,41 @@ class Speed final : public Command
 
 #endif
 
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+      void bench_ec_h2c(const std::chrono::milliseconds runtime)
+         {
+         for(std::string group_name : { "secp256r1", "secp384r1", "secp521r1" })
+            {
+            auto h2c_ro_timer = make_timer(group_name + "-RO", "", "hash to curve");
+            auto h2c_nu_timer = make_timer(group_name + "-NU", "", "hash to curve");
+
+            const Botan::EC_Group group(group_name);
+
+            while(h2c_ro_timer->under(runtime))
+               {
+               std::vector<uint8_t> input(32);
+
+               rng().randomize(input.data(), input.size());
+
+               const Botan::PointGFp p1 = h2c_ro_timer->run([&]() {
+                  return group.hash_to_curve("SHA-256", input.data(), input.size(), nullptr, 0, true);
+                  });
+
+               BOTAN_ASSERT_NOMSG(p1.on_the_curve());
+
+               const Botan::PointGFp p2 = h2c_nu_timer->run([&]() {
+                  return group.hash_to_curve("SHA-256", input.data(), input.size(), nullptr, 0, false);
+                  });
+
+               BOTAN_ASSERT_NOMSG(p2.on_the_curve());
+               }
+
+            record_result(h2c_ro_timer);
+            record_result(h2c_nu_timer);
+            }
+         }
+#endif
+
 #if defined(BOTAN_HAS_FPE_FE1)
 
       void bench_fpe_fe1(const std::chrono::milliseconds runtime)
diff --git a/src/lib/math/numbertheory/monty.cpp b/src/lib/math/numbertheory/monty.cpp
index a6e22c9b7..e89f497de 100644
--- a/src/lib/math/numbertheory/monty.cpp
+++ b/src/lib/math/numbertheory/monty.cpp
@@ -433,6 +433,11 @@ Montgomery_Int Montgomery_Int::square(secure_vector<word>& ws) const
    return Montgomery_Int(m_params, m_params->sqr(m_v, ws), false);
    }
 
+Montgomery_Int Montgomery_Int::cube(secure_vector<word>& ws) const
+   {
+   return Montgomery_Int(m_params, m_params->sqr(m_v, ws), false);
+   }
+
 Montgomery_Int Montgomery_Int::multiplicative_inverse() const
    {
    secure_vector<word> ws;
diff --git a/src/lib/math/numbertheory/monty.h b/src/lib/math/numbertheory/monty.h
index b4c90397e..a3ceb23e0 100644
--- a/src/lib/math/numbertheory/monty.h
+++ b/src/lib/math/numbertheory/monty.h
@@ -106,6 +106,8 @@ class BOTAN_UNSTABLE_API Montgomery_Int final
 
       Montgomery_Int square(secure_vector<word>& ws) const;
 
+      Montgomery_Int cube(secure_vector<word>& ws) const;
+
       Montgomery_Int& square_this(secure_vector<word>& ws);
 
       Montgomery_Int& square_this_n_times(secure_vector<word>& ws, size_t n);
diff --git a/src/lib/math/numbertheory/reducer.h b/src/lib/math/numbertheory/reducer.h
index b1c2c87a9..54e97b458 100644
--- a/src/lib/math/numbertheory/reducer.h
+++ b/src/lib/math/numbertheory/reducer.h
@@ -31,6 +31,13 @@ class BOTAN_PUBLIC_API(2,0) Modular_Reducer
       BigInt multiply(const BigInt& x, const BigInt& y) const
          { return reduce(x * y); }
 
+      /**
+      * Multiply mod p
+      * @return (x * y * z) % p
+      */
+      BigInt multiply(const BigInt& x, const BigInt& y, const BigInt& z) const
+         { return multiply(x, multiply(y, z)); }
+
       /**
       * Square mod p
       * @param x the value to square
diff --git a/src/lib/pubkey/ec_group/ec_group.cpp b/src/lib/pubkey/ec_group/ec_group.cpp
index b253f56ab..fc150113d 100644
--- a/src/lib/pubkey/ec_group/ec_group.cpp
+++ b/src/lib/pubkey/ec_group/ec_group.cpp
@@ -19,6 +19,10 @@
 #include <botan/rng.h>
 #include <vector>
 
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+  #include <botan/internal/ec_h2c.h>
+#endif
+
 namespace Botan {
 
 class EC_Group_Data final
@@ -613,6 +617,15 @@ PointGFp EC_Group::OS2ECP(const uint8_t bits[], size_t len) const
 
 PointGFp EC_Group::point(const BigInt& x, const BigInt& y) const
    {
+   #if 0
+   BigInt l = (x*x*x + x*get_a() + get_b()) % get_p();
+   BigInt r = (y*y) % get_p();
+
+   if(l != r)
+      {
+      printf("invalid point in EC_Group::point\n");
+      }
+#endif
    // TODO: randomize the representation?
    return PointGFp(data().curve(), x, y);
    }
@@ -660,6 +673,46 @@ PointGFp EC_Group::zero_point() const
    return PointGFp(data().curve());
    }
 
+PointGFp EC_Group::hash_to_curve(const std::string& hash_fn,
+                                 const uint8_t input[],
+                                 size_t input_len,
+                                 const std::string& domain,
+                                 bool random_oracle) const
+   {
+   return this->hash_to_curve(hash_fn,
+                              input,
+                              input_len,
+                              reinterpret_cast<const uint8_t*>(domain.c_str()),
+                              domain.size(),
+                              random_oracle);
+   }
+
+PointGFp EC_Group::hash_to_curve(const std::string& hash_fn,
+                                 const uint8_t input[],
+                                 size_t input_len,
+                                 const uint8_t domain_sep[],
+                                 size_t domain_sep_len,
+                                 bool random_oracle) const
+   {
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+
+   // Only have SSWU currently
+   if(get_a().is_zero() || get_b().is_zero() || get_p() % 4 == 1)
+      {
+      throw Not_Implemented("EC_Group::hash_to_curve not available for this curve type");
+      }
+
+   return hash_to_curve_sswu(*this, hash_fn,
+                             input, input_len,
+                             domain_sep, domain_sep_len,
+                             random_oracle);
+
+#else
+   BOTAN_UNUSED(hash_fn, random_oracle, input, input_len, domain_sep, domain_sep_len);
+   throw Not_Implemented("EC_Group::hash_to_curve functionality not available in this configuration");
+#endif
+   }
+
 std::vector<uint8_t>
 EC_Group::DER_encode(EC_Group_Encoding form) const
    {
diff --git a/src/lib/pubkey/ec_group/ec_group.h b/src/lib/pubkey/ec_group/ec_group.h
index 03a4463a0..6ae180ee9 100644
--- a/src/lib/pubkey/ec_group/ec_group.h
+++ b/src/lib/pubkey/ec_group/ec_group.h
@@ -223,6 +223,14 @@ class BOTAN_PUBLIC_API(2,0) EC_Group final
       */
       BigInt multiply_mod_order(const BigInt& x, const BigInt& y, const BigInt& z) const;
 
+      /*
+      * Return x^3 modulo the order
+      */
+      inline BigInt cube_mod_order(const BigInt& x) const
+         {
+         return multiply_mod_order(x, square_mod_order(x));
+         }
+
       /**
       * Check if y is a plausible point on the curve
       *
@@ -290,6 +298,44 @@ class BOTAN_PUBLIC_API(2,0) EC_Group final
       */
       BigInt random_scalar(RandomNumberGenerator& rng) const;
 
+      /**
+      * Hash onto the curve.
+      * For some curve types no mapping is currently available, in this
+      * case this function will throw an exception.
+      *
+      * @param hash_fn the hash function to use (typically "SHA-256" or "SHA-512")
+      * @param input the input to hash
+      * @param input_len length of input in bytes
+      * @param domain_sep a domain seperator
+      * @param domain_sep_len length of domain_sep in bytes
+      * @param random_oracle if the mapped point must be uniform (use
+               "true" here unless you know what you are doing)
+      */
+      PointGFp hash_to_curve(const std::string& hash_fn,
+                             const uint8_t input[],
+                             size_t input_len,
+                             const uint8_t domain_sep[],
+                             size_t domain_sep_len,
+                             bool random_oracle = true) const;
+
+      /**
+      * Hash onto the curve.
+      * For some curve types no mapping is currently available, in this
+      * case this function will throw an exception.
+      *
+      * @param hash_fn the hash function to use (typically "SHA-256" or "SHA-512")
+      * @param input the input to hash
+      * @param input_len length of input in bytes
+      * @param domain_sep a domain seperator
+      * @param random_oracle if the mapped point must be uniform (use
+               "true" here unless you know what you are doing)
+      */
+      PointGFp hash_to_curve(const std::string& hash_fn,
+                             const uint8_t input[],
+                             size_t input_len,
+                             const std::string& domain_sep,
+                             bool random_oracle = true) const;
+
       /**
       * Return the zero (or infinite) point on this curve
       */
diff --git a/src/lib/pubkey/ec_h2c/ec_h2c.cpp b/src/lib/pubkey/ec_h2c/ec_h2c.cpp
new file mode 100644
index 000000000..4a1d69fdd
--- /dev/null
+++ b/src/lib/pubkey/ec_h2c/ec_h2c.cpp
@@ -0,0 +1,208 @@
+/*
+* (C) 2019,2020,2021 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/internal/ec_h2c.h>
+#include <botan/ec_group.h>
+#include <botan/numthry.h>
+#include <botan/reducer.h>
+#include <botan/hash.h>
+
+namespace Botan {
+
+void expand_message_xmd(const std::string& hash_fn,
+                        uint8_t output[],
+                        size_t output_len,
+                        const uint8_t input[],
+                        size_t input_len,
+                        const uint8_t domain_sep[],
+                        size_t domain_sep_len)
+   {
+   if(domain_sep_len > 0xFF)
+      throw Invalid_Argument("expand_message_xmd domain seperator too long");
+
+   auto hash = HashFunction::create_or_throw(hash_fn);
+   const size_t block_size = hash->hash_block_size();
+   if(block_size == 0)
+      throw Invalid_Argument("expand_message_xmd cannot be used with " + hash_fn);
+
+   const size_t hash_output_size = hash->output_length();
+   if(output_len > 255*hash_output_size || output_len > 0xFFFF)
+      throw Invalid_Argument("expand_message_xmd requested output length too long");
+
+   // Compute b_0 = H(msg_prime) = H(Z_pad || msg || l_i_b_str || 0x00 || DST_prime)
+
+   hash->update(std::vector<uint8_t>(block_size));
+   hash->update(input, input_len);
+   hash->update_be(static_cast<uint16_t>(output_len));
+   hash->update(0x00);
+   hash->update(domain_sep, domain_sep_len);
+   hash->update(static_cast<uint8_t>(domain_sep_len));
+
+   const secure_vector<uint8_t> b_0 = hash->final();
+
+   // Compute b_1 = H(b_0 || 0x01 || DST_prime)
+
+   hash->update(b_0);
+   hash->update(0x01);
+   hash->update(domain_sep, domain_sep_len);
+   hash->update(static_cast<uint8_t>(domain_sep_len));
+
+   secure_vector<uint8_t> b_i = hash->final();
+
+   uint8_t cnt = 2;
+   while(output_len > 0)
+      {
+      const size_t produced = std::min(output_len, hash_output_size);
+
+      copy_mem(output, b_i.data(), produced);
+      output += produced;
+      output_len -= produced;
+
+      // Now compute the next b_i
+
+      b_i ^= b_0;
+      hash->update(b_i);
+      hash->update(cnt);
+      hash->update(domain_sep, domain_sep_len);
+      hash->update(static_cast<uint8_t>(domain_sep_len));
+      hash->final(b_i.data());
+      cnt += 1;
+      }
+   }
+
+namespace {
+
+std::vector<BigInt>
+hash_to_field(const EC_Group& group,
+              const Modular_Reducer& mod_p,
+              const std::string& hash_fn,
+              uint8_t count,
+              const uint8_t input[], size_t input_len,
+              const uint8_t domain_sep[], size_t domain_sep_len)
+   {
+   const size_t k = (group.get_order_bits() + 1) / 2;
+   const size_t L = (group.get_p_bits() + k + 7) / 8;
+
+   std::vector<BigInt> results;
+   results.reserve(count);
+
+   secure_vector<uint8_t> output(L * count);
+   expand_message_xmd(hash_fn,
+                      output.data(), output.size(),
+                      input, input_len,
+                      domain_sep, domain_sep_len);
+
+   for(size_t i = 0; i != count; ++i)
+      {
+      BigInt v(&output[i*L], L);
+      results.push_back(mod_p.reduce(v));
+      }
+
+   return results;
+   }
+
+BigInt sswu_z(const EC_Group& group)
+   {
+   const BigInt& p = group.get_p();
+   const OID oid = group.get_curve_oid();
+
+   if(oid == OID{1,2,840,10045,3,1,7}) // secp256r1
+      return p - 10;
+   if(oid == OID{1,3,132,0,34}) // secp384r1
+      return p - 12;
+   if(oid == OID{1,3,132,0,35}) // secp521r1
+      return p - 4;
+
+   return 0;
+   }
+
+BigInt ct_choose(bool first, const BigInt& x, const BigInt& y)
+   {
+   BigInt z = y;
+   z.ct_cond_assign(first, x);
+   return z;
+   }
+
+PointGFp map_to_curve_sswu(const EC_Group& group, const Modular_Reducer& mod_p, const BigInt& u)
+   {
+   const BigInt& p = group.get_p();
+   const BigInt& A = group.get_a();
+   const BigInt& B = group.get_b();
+   const BigInt Z = sswu_z(group);
+
+   if(Z.is_zero() || A.is_zero() || B.is_zero() || p % 4 != 3)
+      throw Invalid_Argument("map_to_curve_sswu does not support this curve");
+
+   // These values could be precomputed:
+   const BigInt c1 = mod_p.multiply(p - B, inverse_mod(A, p));
+   const BigInt c2 = mod_p.multiply(p - 1, inverse_mod(Z, p));
+
+   /*
+   * See Appendix F.2 of draft-irtf-cfrg-hash-to-curve
+   */
+
+   const BigInt tv1 = mod_p.multiply(Z, mod_p.square(u));
+   const BigInt tv2 = mod_p.square(tv1);
+
+   BigInt x1 = inverse_mod(tv1 + tv2, p);
+   const bool e1 = x1.is_zero();
+   x1 += 1;
+   x1.ct_cond_assign(e1, c2);
+   x1 = mod_p.multiply(x1, c1);
+
+   // gx1 = x1^3 + A*x1 + B;
+   BigInt gx1 = mod_p.square(x1);
+   gx1 += A;
+   gx1 = mod_p.multiply(gx1, x1);
+   gx1 += B;
+   gx1 = mod_p.reduce(gx1);
+
+   const BigInt x2 = mod_p.multiply(tv1, x1);
+
+   // gx2 = (Z * u^2)^3 * gx1
+   const BigInt gx2 = mod_p.multiply(gx1, mod_p.multiply(tv1, tv2));
+
+   // assumes p % 4 == 3
+   const bool gx1_is_square = (power_mod(gx1, (p-1)/2, p) <= 1);
+
+   const BigInt x = ct_choose(gx1_is_square, x1, x2);
+   const BigInt y2 = ct_choose(gx1_is_square, gx1, gx2);
+
+   // assumes p % 4 == 3
+   const BigInt y = power_mod(y2, (p + 1)/4, p);
+   const BigInt neg_y = p - y;
+
+   const bool uy_sign = u.get_bit(0) != y.get_bit(0);
+   return group.point(x, ct_choose(uy_sign, neg_y, y));
+   }
+
+}
+
+PointGFp hash_to_curve_sswu(const EC_Group& group,
+                            const std::string& hash_fn,
+                            const uint8_t input[],
+                            size_t input_len,
+                            const uint8_t domain_sep[],
+                            size_t domain_sep_len,
+                            bool random_oracle)
+   {
+   const Modular_Reducer mod_p(group.get_p());
+
+   const uint8_t count = (random_oracle ? 2 : 1);
+
+   const auto u = hash_to_field(group, mod_p, hash_fn, count,
+                                input, input_len,
+                                domain_sep, domain_sep_len);
+
+   PointGFp pt = map_to_curve_sswu(group, mod_p, u[0]);
+
+   for(size_t i = 1; i != u.size(); ++i)
+      pt += map_to_curve_sswu(group, mod_p, u[i]);
+
+   return pt;
+   }
+
+}
diff --git a/src/lib/pubkey/ec_h2c/ec_h2c.h b/src/lib/pubkey/ec_h2c/ec_h2c.h
new file mode 100644
index 000000000..f932e76a5
--- /dev/null
+++ b/src/lib/pubkey/ec_h2c/ec_h2c.h
@@ -0,0 +1,47 @@
+/*
+* (C) 2019,2020,2021 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_ECC_HASH_TO_CURVE_H_
+#define BOTAN_ECC_HASH_TO_CURVE_H_
+
+#include <botan/types.h>
+#include <botan/point_gfp.h>
+#include <string>
+
+namespace Botan {
+
+class EC_Group;
+
+/**
+* expand_message_xmd
+*/
+void BOTAN_TEST_API expand_message_xmd(const std::string& hash_fn,
+                                       uint8_t output[],
+                                       size_t output_len,
+                                       const uint8_t input[],
+                                       size_t input_len,
+                                       const uint8_t domain_sep[],
+                                       size_t domain_sep_len);
+
+
+/**
+* Hash an input onto an elliptic curve point using the
+* methods from draft-irtf-cfrg-hash-to-curve
+*
+* This method requires that the ECC group have (a*b) != 0
+* which excludes certain groups including secp256k1
+*/
+PointGFp hash_to_curve_sswu(const EC_Group& group,
+                            const std::string& hash_fn,
+                            const uint8_t input[],
+                            size_t input_len,
+                            const uint8_t domain_sep[],
+                            size_t domain_sep_len,
+                            bool random_oracle);
+
+}
+
+#endif
diff --git a/src/lib/pubkey/ec_h2c/info.txt b/src/lib/pubkey/ec_h2c/info.txt
new file mode 100644
index 000000000..af342a2de
--- /dev/null
+++ b/src/lib/pubkey/ec_h2c/info.txt
@@ -0,0 +1,12 @@
+<defines>
+EC_HASH_TO_CURVE -> 20210420
+</defines>
+
+<requires>
+ec_group
+hash
+</requires>
+
+<header:internal>
+ec_h2c.h
+</header:internal>


Test patch:
diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py
index 7d69f8211..2fe2ade16 100755
--- a/src/scripts/test_cli.py
+++ b/src/scripts/test_cli.py
@@ -1147,12 +1147,12 @@ def cli_speed_pk_tests(_tmp_dir):
 
     pk_algos = ["ECDSA", "ECDH", "SM2", "ECKCDSA", "ECGDSA", "GOST-34.10",
                 "DH", "DSA", "ElGamal", "Ed25519", "Curve25519", "NEWHOPE", "McEliece",
-                "RSA", "RSA_keygen", "XMSS"]
+                "RSA", "RSA_keygen", "XMSS", "ec_h2c"]
 
     output = test_cli("speed", ["--msec=%d" % (msec)] + pk_algos, None).split('\n')
 
     # ECDSA-secp256r1 106 keygen/sec; 9.35 ms/op 37489733 cycles/op (1 op in 9 ms)
-    format_re = re.compile(r'^.* [0-9]+ ([A-Za-z ]+)/sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9\.]+ ms\)')
+    format_re = re.compile(r'^.* [0-9]+ ([A-Za-z0-9 ]+)/sec; [0-9]+\.[0-9]+ ms/op .*\([0-9]+ (op|ops) in [0-9\.]+ ms\)')
     for line in output:
         if format_re.match(line) is None:
             logging.error("Unexpected line %s", line)
diff --git a/src/tests/data/pubkey/ec_h2c.vec b/src/tests/data/pubkey/ec_h2c.vec
new file mode 100644
index 000000000..3e705d060
--- /dev/null
+++ b/src/tests/data/pubkey/ec_h2c.vec
@@ -0,0 +1,151 @@
+[SSWU-RO]
+Group = secp256r1
+Hash = SHA-256
+Domain = QUUX-V01-CS02-with-P256_XMD:SHA-256_SSWU_RO_
+
+Input =
+PointX = 0x2c15230b26dbc6fc9a37051158c95b79656e17a1a920b11394ca91c44247d3e4
+PointY = 0x8a7a74985cc5c776cdfe4b1f19884970453912e9d31528c060be9ab5c43e8415
+
+Input = abc
+PointX = 0x0bb8b87485551aa43ed54f009230450b492fead5f1cc91658775dac4a3388a0f
+PointY = 0x5c41b3d0731a27a7b14bc0bf0ccded2d8751f83493404c84a88e71ffd424212e
+
+Input = abcdef0123456789
+PointX = 0x65038ac8f2b1def042a5df0b33b1f4eca6bff7cb0f9c6c1526811864e544ed80
+PointY = 0xcad44d40a656e7aff4002a8de287abc8ae0482b5ae825822bb870d6df9b56ca3
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0x4be61ee205094282ba8a2042bcb48d88dfbb609301c49aa8b078533dc65a0b5d
+PointY = 0x98f8df449a072c4721d241a3b1236d3caccba603f916ca680f4539d2bfb3c29e
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0x457ae2981f70ca85d8e24c308b14db22f3e3862c5ea0f652ca38b5e49cd64bc5
+PointY = 0xecb9f0eadc9aeed232dabc53235368c1394c78de05dd96893eefa62b0f4757dc
+
+[SSWU-NU]
+Group = secp256r1
+Hash = SHA-256
+Domain = QUUX-V01-CS02-with-P256_XMD:SHA-256_SSWU_NU_
+
+Input =
+PointX = 0xf871caad25ea3b59c16cf87c1894902f7e7b2c822c3d3f73596c5ace8ddd14d1
+PointY = 0x87b9ae23335bee057b99bac1e68588b18b5691af476234b8971bc4f011ddc99b
+
+Input = abc
+PointX = 0xfc3f5d734e8dce41ddac49f47dd2b8a57257522a865c124ed02b92b5237befa4
+PointY = 0xfe4d197ecf5a62645b9690599e1d80e82c500b22ac705a0b421fac7b47157866
+
+Input = abcdef0123456789
+PointX = 0xf164c6674a02207e414c257ce759d35eddc7f55be6d7f415e2cc177e5d8faa84
+PointY = 0x3aa274881d30db70485368c0467e97da0e73c18c1d00f34775d012b6fcee7f97
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0x324532006312be4f162614076460315f7a54a6f85544da773dc659aca0311853
+PointY = 0x8d8197374bcd52de2acfefc8a54fe2c8d8bebd2a39f16be9b710e4b1af6ef883
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0x5c4bad52f81f39c8e8de1260e9a06d72b8b00a0829a8ea004a610b0691bea5d9
+PointY = 0xc801e7c0782af1f74f24fc385a8555da0582032a3ce038de637ccdcb16f7ef7b
+
+[SSWU-RO]
+Group = secp384r1
+Hash = SHA-512
+Domain = QUUX-V01-CS02-with-P384_XMD:SHA-512_SSWU_RO_
+
+Input =
+PointX = 0xc3144d47428d071d4169420c91006a0bd48d7259d492af86e7f82d98e3497519d8550045557b7d55cc2a0f339df088b9
+PointY = 0xaa5f165f0146101363d1b34fe65bcf638532e3b2eb1744cdbd60e9384c6c1838bbaea988963cc9f0f0902798e9f8058a
+
+
+Input = abc
+PointX = 0x7bce42d575e64bc7828478f1bba94000c3ddb02ac03052061a7b7ff81479823350e2a8e1da74e17be3016ab163094bcf
+PointY = 0x6634b2f0acb32b84b75ecfad96c676b3863cb3cec4f76c9bccef1894a650830e60cd1c0f20c9d05e9ee58d8a611db87d
+
+
+Input = abcdef0123456789
+PointX = 0xaf1a87bee29167676e41d8eb0518a9e44e570207519c11fa126c33f32d62bbf6d312fd5812b182d59389f26ea496e58d
+PointY = 0x76ab30527be12a53a3bd63457072840ea516aa945fbe2dc48a42cfbd031c3f93896e4a66093b2f56cc9da4694ec95f27
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0x5c24f67b2175279f4e94a0af9cf09213f0e7e2e3ccb6d4feae9403281c1962507ba0588ef895c9b7c6cff28ce1d15a1f
+PointY = 0x259ce2c65f35f5fb3a611e5bbf56d2979ce9de429afd6271fbda57c3d412c78292d0cb6f27e0ee96f91fba9f0af54327
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0xed18cea59aabd90a3c84b48eeb09b42409f42340aec2ec1b70687215fa4befc64fd4de4620d12e70b9890ad9a70e6ee8
+PointY = 0xdbc9b0e5e718539c785b7b787829a1c01b92591aed954e08b853dc96fb303ba4bc8aad06712b8b3b4fae2047d6269d68
+
+[SSWU-NU]
+Group = secp384r1
+Hash = SHA-512
+Domain = QUUX-V01-CS02-with-P384_XMD:SHA-512_SSWU_NU_
+
+Input =
+PointX = 0x5b87392cdbf306d62141cf266a1fcc8b6a8129469b1e4a56a16db6371b70279d3155514580300f77a23dbeaa70eeda32
+PointY = 0x9094b16177a04f5c5afac87efc78b6e65a2583a5adc91c04cd508fd602d528530eb54932dff8b7e156d470996606cd9c
+
+Input = abc
+PointX = 0x496ed56a37cb85a82826a4234948dd3ceee17da6412c87242165b7f798b702f2292237bddac386cfcfa8f22e7b85ca2d
+PointY = 0x9524181274d1313c12872ea835c7ddc9444124d22aae6e474d55b1fe68e480250374e689e6c2745323da7222732d2cce
+
+Input = abcdef0123456789
+PointX = 0xa1289920ba2c52de5f384b1316788438ac5564a20c2e0f7ff0ff2fa34cb4488bd4683c0cc45ee6234b4a515ddda31f99
+PointY = 0xb5e24b855275729db25cecc83ec5fc1dcf8f055ad981a0901448d84c6278cd10a28f65316db5ae1f5738ed06ae9c2f55
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0xa0ed20ca2c7c69a6b7a3e5b9cf77d8f9bd979aa83a7f75e5f90d6becd107925e5dcbbd96978ab74a7b5e96b48135d04c
+PointY = 0x4c9ed613516ae3e818139b6c9aea2ac42063c06ff0303a38c101ab822c15d5fe6413e64adbac82a0da770cf110d7ace9
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0xbc36cda196f8084052fc41a5c4ef5c9e1c724cc0bd83ef8eaef07bb2cbc3db99ff5cdb31ba3018a6afe59b0db040c980
+PointY = 0x5106450163d90d99d3191bc92f8a3d116f15b18b23eff8e9996481c6878bd16c8e202f44abc3d09325c2016b5dacc8f0
+
+[SSWU-RO]
+Group = secp521r1
+Hash = SHA-512
+Domain = QUUX-V01-CS02-with-P521_XMD:SHA-512_SSWU_RO_
+
+Input =
+PointX = 0x00fd767cebb2452030358d0e9cf907f525f50920c8f607889a6a35680727f64f4d66b161fafeb2654bea0d35086bec0a10b30b14adef3556ed9f7f1bc23cecc9c088
+PointY = 0x0169ba78d8d851e930680322596e39c78f4fe31b97e57629ef6460ddd68f8763fd7bd767a4e94a80d3d21a3c2ee98347e024fc73ee1c27166dc3fe5eeef782be411d
+
+Input = abc
+PointX = 0x002f89a1677b28054b50d15e1f81ed6669b5a2158211118ebdef8a6efc77f8ccaa528f698214e4340155abc1fa08f8f613ef14a043717503d57e267d57155cf784a4
+PointY = 0x010e0be5dc8e753da8ce51091908b72396d3deed14ae166f66d8ebf0a4e7059ead169ea4bead0232e9b700dd380b316e9361cfdba55a08c73545563a80966ecbb86d
+
+Input = abcdef0123456789
+PointX = 0x006e200e276a4a81760099677814d7f8794a4a5f3658442de63c18d2244dcc957c645e94cb0754f95fcf103b2aeaf94411847c24187b89fb7462ad3679066337cbc4
+PointY = 0x001dd8dfa9775b60b1614f6f169089d8140d4b3e4012949b52f98db2deff3e1d97bf73a1fa4d437d1dcdf39b6360cc518d8ebcc0f899018206fded7617b654f6b168
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0x01b264a630bd6555be537b000b99a06761a9325c53322b65bdc41bf196711f9708d58d34b3b90faf12640c27b91c70a507998e55940648caa8e71098bf2bc8d24664
+PointY = 0x01ea9f445bee198b3ee4c812dcf7b0f91e0881f0251aab272a12201fd89b1a95733fd2a699c162b639e9acdcc54fdc2f6536129b6beb0432be01aa8da02df5e59aaa
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0x00c12bc3e28db07b6b4d2a2b1167ab9e26fc2fa85c7b0498a17b0347edf52392856d7e28b8fa7a2dd004611159505835b687ecf1a764857e27e9745848c436ef3925
+PointY = 0x01cd287df9a50c22a9231beb452346720bb163344a41c5f5a24e8335b6ccc595fd436aea89737b1281aecb411eb835f0b939073fdd1dd4d5a2492e91ef4a3c55bcbd
+
+[SSWU-NU]
+Group = secp521r1
+Hash = SHA-512
+Domain = QUUX-V01-CS02-with-P521_XMD:SHA-512_SSWU_NU_
+
+Input =
+PointX = 0x01ec604b4e1e3e4c7449b7a41e366e876655538acf51fd40d08b97be066f7d020634e906b1b6942f9174b417027c953d75fb6ec64b8cee2a3672d4f1987d13974705
+PointY = 0x00944fc439b4aad2463e5c9cfa0b0707af3c9a42e37c5a57bb4ecd12fef9fb21508568aedcdd8d2490472df4bbafd79081c81e99f4da3286eddf19be47e9c4cf0e91
+
+Input = abc
+PointX = 0x00c720ab56aa5a7a4c07a7732a0a4e1b909e32d063ae1b58db5f0eb5e09f08a9884bff55a2bef4668f715788e692c18c1915cd034a6b998311fcf46924ce66a2be9a
+PointY = 0x003570e87f91a4f3c7a56be2cb2a078ffc153862a53d5e03e5dad5bccc6c529b8bab0b7dbb157499e1949e4edab21cf5d10b782bc1e945e13d7421ad8121dbc72b1d
+
+Input = abcdef0123456789
+PointX = 0x00bcaf32a968ff7971b3bbd9ce8edfbee1309e2019d7ff373c38387a782b005dce6ceffccfeda5c6511c8f7f312f343f3a891029c5858f45ee0bf370aba25fc990cc
+PointY = 0x00923517e767532d82cb8a0b59705eec2b7779ce05f9181c7d5d5e25694ef8ebd4696343f0bc27006834d2517215ecf79482a84111f50c1bae25044fe1dd77744bbd
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+PointX = 0x001ac69014869b6c4ad7aa8c443c255439d36b0e48a0f57b03d6fe9c40a66b4e2eaed2a93390679a5cc44b3a91862b34b673f0e92c83187da02bf3db967d867ce748
+PointY = 0x00d5603d530e4d62b30fccfa1d90c2206654d74291c1db1c25b86a051ee3fffc294e5d56f2e776853406bd09206c63d40f37ad8829524cf89ad70b5d6e0b4a3b7341
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+PointX = 0x01801de044c517a80443d2bd4f503a9e6866750d2f94a22970f62d721f96e4310e4a828206d9cdeaa8f2d476705cc3bbc490a6165c687668f15ec178a17e3d27349b
+PointY = 0x0068889ea2e1442245fe42bfda9e58266828c0263119f35a61631a3358330f3bb84443fcb54fcd53a1d097fccbe310489b74ee143fc2938959a83a1f7dd4a6fd395b
diff --git a/src/tests/data/pubkey/ec_h2c_xmd.vec b/src/tests/data/pubkey/ec_h2c_xmd.vec
new file mode 100644
index 000000000..e0a257cdd
--- /dev/null
+++ b/src/tests/data/pubkey/ec_h2c_xmd.vec
@@ -0,0 +1,65 @@
+[SHA-256]
+Domain = QUUX-V01-CS02-with-expander
+
+Input =
+Output = f659819a6473c1835b25ea59e3d38914c98b374f0970b7e4c92181df928fca88
+
+Input = abc
+Output = 1c38f7c211ef233367b2420d04798fa4698080a8901021a795a1151775fe4da7
+
+Input = abcdef0123456789
+Output = 8f7e7b66791f0da0dbb5ec7c22ec637f79758c0a48170bfb7c4611bd304ece89
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+Output = 72d5aa5ec810370d1f0013c0df2f1d65699494ee2a39f72e1716b1b964e1c642
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+Output = 3b8e704fc48336aca4c2a12195b720882f2162a4b7b13a9c350db46f429b771b
+
+Input =
+Output = 8bcffd1a3cae24cf9cd7ab85628fd111bb17e3739d3b53f89580d217aa79526f1708354a76a402d3569d6a9d19ef3de4d0b991e4f54b9f20dcde9b95a66824cbdf6c1a963a1913d43fd7ac443a02fc5d9d8d77e2071b86ab114a9f34150954a7531da568a1ea8c760861c0cde2005afc2c114042ee7b5848f5303f0611cf297f
+
+Input = abc
+Output = fe994ec51bdaa821598047b3121c149b364b178606d5e72bfbb713933acc29c186f316baecf7ea22212f2496ef3f785a27e84a40d8b299cec56032763eceeff4c61bd1fe65ed81decafff4a31d0198619c0aa0c6c51fca15520789925e813dcfd318b542f8799441271f4db9ee3b8092a7a2e8d5b75b73e28fb1ab6b4573c192
+
+Input = abcdef0123456789
+Output = c9ec7941811b1e19ce98e21db28d22259354d4d0643e301175e2f474e030d32694e9dd5520dde93f3600d8edad94e5c364903088a7228cc9eff685d7eaac50d5a5a8229d083b51de4ccc3733917f4b9535a819b445814890b7029b5de805bf62b33a4dc7e24acdf2c924e9fe50d55a6b832c8c84c7f82474b34e48c6d43867be
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+Output = 48e256ddba722053ba462b2b93351fc966026e6d6db493189798181c5f3feea377b5a6f1d8368d7453faef715f9aecb078cd402cbd548c0e179c4ed1e4c7e5b048e0a39d31817b5b24f50db58bb3720fe96ba53db947842120a068816ac05c159bb5266c63658b4f000cbf87b1209a225def8ef1dca917bcda79a1e42acd8069
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+Output = 396962db47f749ec3b5042ce2452b619607f27fd3939ece2746a7614fb83a1d097f554df3927b084e55de92c7871430d6b95c2a13896d8a33bc48587b1f66d21b128a1a8240d5b0c26dfe795a1a842a0807bb148b77c2ef82ed4b6c9f7fcb732e7f94466c8b51e52bf378fba044a31f5cb44583a892f5969dcd73b3fa128816e
+
+[SHA-512]
+Domain = QUUX-V01-CS02-with-expander
+
+Input =
+Output = 2eaa1f7b5715f4736e6a5dbe288257abf1faa028680c1d938cd62ac699ead642
+
+Input = abc
+Output = 0eeda81f69376c80c0f8986496f22f21124cb3c562cf1dc608d2c13005553b0f
+
+Input = abcdef0123456789
+Output = 2e375fc05e05e80dbf3083796fde2911789d9e8847e1fcebf4ca4b36e239b338
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+Output = c37f9095fe7fe4f01c03c3540c1229e6ac8583b07510085920f62ec66acc0197
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+Output = af57a7f56e9ed2aa88c6eab45c8c6e7638ae02da7c92cc04f6648c874ebd560e
+
+Input =
+Output = 0687ce02eba5eb3faf1c3c539d1f04babd3c0f420edae244eeb2253b6c6d6865145c31458e824b4e87ca61c3442dc7c8c9872b0b7250aa33e0668ccebbd2b386de658ca11a1dcceb51368721ae6dcd2d4bc86eaebc4e0d11fa02ad053289c9b28a03da6c942b2e12c14e88dbde3b0ba619d6214f47212b628f3e1b537b66efcf
+
+Input = abc
+Output = 779ae4fd8a92f365e4df96b9fde97b40486bb005c1a2096c86f55f3d92875d89045fbdbc4a0e9f2d3e1e6bcd870b2d7131d868225b6fe72881a81cc5166b5285393f71d2e68bb0ac603479959370d06bdbe5f0d8bfd9af9494d1e4029bd68ab35a561341dd3f866b3ef0c95c1fdfaab384ce24a23427803dda1db0c7d8d5344a
+
+Input = abcdef0123456789
+Output = f0953d28846a50e9f88b7ae35b643fc43733c9618751b569a73960c655c068db7b9f044ad5a40d49d91c62302eaa26163c12abfa982e2b5d753049e000adf7630ae117aeb1fb9b61fc724431ac68b369e12a9481b4294384c3c890d576a79264787bc8076e7cdabe50c044130e480501046920ff090c1a091c88391502f0fbac
+
+Input = q128_qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
+Output = 64d3e59f0bc3c5e653011c914b419ba8310390a9585311fddb26791d26663bd71971c347e1b5e88ba9274d2445ed9dcf48eea9528d807b7952924159b7c27caa4f25a2ea94df9508e70a7012dfce0e8021b37e59ea21b80aa9af7f1a1f2efa4fbe523c4266ce7d342acaacd438e452c501c131156b4945515e9008d2b155c258
+
+Input = a512_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+Output = 01524feea5b22f6509f6b1e805c97df94faf4d821b01aadeebc89e9daaed0733b4544e50852fd3e019d58eaad6d267a134c8bc2c08bc46c10bfeff3ee03110bcd8a0d695d75a34092bd8b677bdd369a13325549abab54f4ac907b712bdd3567f38c4554c51902b735b81f43a7ef6f938c7690d107c052c7e7b795ac635b3200a
diff --git a/src/tests/test_ecc_h2c.cpp b/src/tests/test_ecc_h2c.cpp
new file mode 100644
index 000000000..9e667e915
--- /dev/null
+++ b/src/tests/test_ecc_h2c.cpp
@@ -0,0 +1,93 @@
+/*
+* (C) 2019,2020 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "tests.h"
+
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+   #include <botan/ec_group.h>
+   #include <botan/internal/ec_h2c.h>
+#endif
+
+namespace Botan_Tests {
+
+namespace {
+
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+
+class ECC_H2C_XMD_Tests final : public Text_Based_Test
+   {
+   public:
+      ECC_H2C_XMD_Tests() : Text_Based_Test("pubkey/ec_h2c_xmd.vec", "Domain,Input,Output") {}
+
+      bool clear_between_callbacks() const override { return false; }
+
+      Test::Result run_one_test(const std::string& hash, const VarMap& vars) override
+         {
+         Test::Result result("ECC hash to curve XMD " + hash);
+
+         const std::string domain = vars.get_req_str("Domain");
+         const std::string input = vars.get_req_str("Input");
+         const std::vector<uint8_t> expected = vars.get_req_bin("Output");
+
+         std::vector<uint8_t> output(expected.size());
+         Botan::expand_message_xmd(hash, output.data(), output.size(),
+                                   reinterpret_cast<const uint8_t*>(input.data()),
+                                   input.size(),
+                                   reinterpret_cast<const uint8_t*>(domain.data()),
+                                   domain.size());
+
+         result.test_eq("XMD output", output, expected);
+         return result;
+         }
+
+   };
+
+BOTAN_REGISTER_TEST("ec_h2c", "ec_h2c_xmd", ECC_H2C_XMD_Tests);
+
+class ECC_H2C_Tests final : public Text_Based_Test
+   {
+   public:
+      ECC_H2C_Tests() : Text_Based_Test("pubkey/ec_h2c.vec", "Group,Hash,Domain,Input,PointX,PointY") {}
+
+      bool clear_between_callbacks() const override { return false; }
+
+      Test::Result run_one_test(const std::string& method, const VarMap& vars) override
+         {
+         const std::string group_id = vars.get_req_str("Group");
+
+         Test::Result result("ECC hash to curve " + method + " " + group_id);
+
+         const std::string hash = vars.get_req_str("Hash");
+         const std::string domain = vars.get_req_str("Domain");
+         const std::string input = vars.get_req_str("Input");
+         const BigInt exp_point_x = vars.get_req_bn("PointX");
+         const BigInt exp_point_y = vars.get_req_bn("PointY");
+         const bool random_oracle = method.find("-RO") != std::string::npos;
+
+         Botan::EC_Group group(group_id);
+
+         const auto point = group.hash_to_curve(hash,
+                                                reinterpret_cast<const uint8_t*>(input.data()),
+                                                input.size(),
+                                                domain,
+                                                random_oracle);
+
+         result.confirm("Generated point is on the curve", point.on_the_curve());
+
+         result.test_eq("Affine X", point.get_affine_x(), exp_point_x);
+         result.test_eq("Affine Y", point.get_affine_y(), exp_point_y);
+
+         return result;
+         }
+   };
+
+BOTAN_REGISTER_TEST("ec_h2c", "ec_h2c_kat", ECC_H2C_Tests);
+
+#endif
+
+}
+
+}
diff --git a/src/tests/test_ecc_pointmul.cpp b/src/tests/test_ecc_pointmul.cpp
index 28231a169..ede4a80fa 100644
--- a/src/tests/test_ecc_pointmul.cpp
+++ b/src/tests/test_ecc_pointmul.cpp
@@ -6,6 +6,10 @@
 
 #include "tests.h"
 
+#if defined(BOTAN_HAS_ECDSA)
+   #include <botan/ecdsa.h>
+#endif
+
 #if defined(BOTAN_HAS_ECC_GROUP)
    #include <botan/ec_group.h>
 #endif
diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp
index f9305354f..991387264 100644
--- a/src/tests/tests.cpp
+++ b/src/tests/tests.cpp
@@ -1093,7 +1093,7 @@ std::vector<Test::Result> Text_Based_Test::run()
                oss << "Test # " << test_cnt << " ";
                if(!header.empty())
                   oss << header << " ";
-               oss << " failed [Key=" << vars.get_req_str(m_output_key) << "]";
+               oss << " failed [" << m_output_key << "=" << vars.get_req_str(m_output_key) << "]";
 
                result.test_note(oss.str());
                }
@@ -1105,8 +1105,8 @@ std::vector<Test::Result> Text_Based_Test::run()
             oss << "Test # " << test_cnt << " ";
             if(!header.empty())
                oss << header << " ";
-            oss << " failed with exception '" << e.what() << "'";
-            oss << " [Key=" << vars.get_req_str(m_output_key) << "]";
+            oss << "[" << m_output_key << "=" << vars.get_req_str(m_output_key) << "]"
+                << " failed with exception '" << e.what() << "'";
 
             results.push_back(Test::Result::Failure(header_or_name, oss.str()));
             }