Repository: randombit/botan
Pull request #4042 (2024-05-05 19:28:22)

Add EC_Scalar and EC_AffinePoint types
This is the other side of #3979 where we push the interface that's exposed to the rest of the library to a non-BigInt oriented model.

#4027

coveralls: 
[![Coverage Status](https://coveralls.io/builds/67309694/badge)](https://coveralls.io/builds/67309694)

coverage: 91.998% (-0.03%) from 92.024%
when pulling **bae2ce8560c7c7436f55cc461db33d6181307276 on jack/new-ec-types**
into **adda2dd9f7799d2c64a5541c3733e47213b57bd1 on master**.

reneme: I will go through this in the coming days. Now with the underlying PR merged. 👍
randombit: BTW I should mention in terms of reviewing, the overall structure behind the new types isn't that important since it's going to change quite a bit to handle the possibility of different backend implementations. That part is literally just the first thing that came to mind that works. Whereas `EC_Scalar` and `EC_AffinePoint`s interfaces, and how they are used to implement the various schemes, are IMO "final" and so should deserve more scrutiny.
randombit: OK with eb4f1bda91a13a8f72f3b3a70945143cbd616746 in place, previous comment can be disregarded. I think this is more or less the "final" [*] approach for implementing the new scalar and point types. You can see in #4143 what the bridge looks like for pcurves.

[*] Not ideal in any way, I'd like there to be fewer allocations, `dynamic_cast`, etc in here - but it works and doesn't seem to have that much overhead in practice. I expect we'll iterate on this in the future. A lot of what I'd like to do isn't possible until we've eliminated the real weirdo curves and all the deprecated interfaces, `EC_Point`, etc. [**]

[**] Not *technically* true - we could get away with a *third* internal EC implementation - classic BigInt approach, pcurves, plus a third generic curve but bounded size impl. But it just doesn't seem worth it from a complexity perspective. I expect that in Botan 4, with the restrictions on curves in place, we'll transition away from BigInt entirely within EC.
randombit: @reneme Was not sure if you were planning further review of this
reneme: > Was not sure if you were planning further review of this

I believe I wasn't fully done but no need to block the merge.
randombit: OK I'm going to go ahead and merge. Feel free to do a retro-review anytime, we have plenty of time between now and 3.6

Patch:
diff --git a/src/lib/pubkey/ec_group/ec_apoint.cpp b/src/lib/pubkey/ec_group/ec_apoint.cpp
new file mode 100644
index 000000000..1baa53bfc
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_apoint.cpp
@@ -0,0 +1,113 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ec_apoint.h>
+
+#include <botan/ec_group.h>
+#include <botan/ec_scalar.h>
+#include <botan/internal/ec_inner_data.h>
+
+namespace Botan {
+
+EC_AffinePoint::EC_AffinePoint(std::unique_ptr<EC_AffinePoint_Data> point) : m_point(std::move(point)) {
+   BOTAN_ASSERT_NONNULL(m_point);
+}
+
+EC_AffinePoint::EC_AffinePoint(const EC_AffinePoint& other) : m_point(other.inner().clone()) {}
+
+EC_AffinePoint::EC_AffinePoint(EC_AffinePoint&& other) noexcept : m_point(std::move(other.m_point)) {}
+
+EC_AffinePoint& EC_AffinePoint::operator=(const EC_AffinePoint& other) {
+   if(this != &other) {
+      m_point = other.inner().clone();
+   }
+   return (*this);
+}
+
+EC_AffinePoint& EC_AffinePoint::operator=(EC_AffinePoint&& other) noexcept {
+   m_point.swap(other.m_point);
+   return (*this);
+}
+
+EC_AffinePoint::EC_AffinePoint(const EC_Group& group, std::span<const uint8_t> bytes) {
+   m_point = group._data()->point_deserialize(bytes);
+   if(!m_point) {
+      throw Decoding_Error("Failed to deserialize elliptic curve point");
+   }
+}
+
+EC_AffinePoint::EC_AffinePoint(const EC_Group& group, const EC_Point& pt) :
+      EC_AffinePoint(group, pt.encode(EC_Point_Format::Uncompressed)) {}
+
+size_t EC_AffinePoint::field_element_bytes() const {
+   return inner().field_element_bytes();
+}
+
+EC_AffinePoint EC_AffinePoint::hash_to_curve_ro(const EC_Group& group,
+                                                std::string_view hash_fn,
+                                                std::span<const uint8_t> input,
+                                                std::span<const uint8_t> domain_sep) {
+   auto pt = group._data()->point_hash_to_curve_ro(hash_fn, input, domain_sep);
+   return EC_AffinePoint(std::move(pt));
+}
+
+EC_AffinePoint EC_AffinePoint::hash_to_curve_nu(const EC_Group& group,
+                                                std::string_view hash_fn,
+                                                std::span<const uint8_t> input,
+                                                std::span<const uint8_t> domain_sep) {
+   auto pt = group._data()->point_hash_to_curve_nu(hash_fn, input, domain_sep);
+   return EC_AffinePoint(std::move(pt));
+}
+
+EC_AffinePoint::~EC_AffinePoint() = default;
+
+std::optional<EC_AffinePoint> EC_AffinePoint::deserialize(const EC_Group& group, std::span<const uint8_t> bytes) {
+   auto pt = group._data()->point_deserialize(bytes);
+   return EC_AffinePoint(std::move(pt));
+}
+
+EC_AffinePoint EC_AffinePoint::g_mul(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws) {
+   auto pt = scalar._inner().group()->point_g_mul(scalar.inner(), rng, ws);
+   return EC_AffinePoint(std::move(pt));
+}
+
+EC_AffinePoint EC_AffinePoint::mul(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const {
+   return EC_AffinePoint(inner().mul(scalar._inner(), rng, ws));
+}
+
+void EC_AffinePoint::serialize_x_to(std::span<uint8_t> bytes) const {
+   m_point->serialize_x_to(bytes);
+}
+
+void EC_AffinePoint::serialize_y_to(std::span<uint8_t> bytes) const {
+   m_point->serialize_y_to(bytes);
+}
+
+void EC_AffinePoint::serialize_xy_to(std::span<uint8_t> bytes) const {
+   m_point->serialize_xy_to(bytes);
+}
+
+void EC_AffinePoint::serialize_compressed_to(std::span<uint8_t> bytes) const {
+   m_point->serialize_compressed_to(bytes);
+}
+
+void EC_AffinePoint::serialize_uncompressed_to(std::span<uint8_t> bytes) const {
+   m_point->serialize_uncompressed_to(bytes);
+}
+
+EC_Point EC_AffinePoint::to_legacy_point() const {
+   return m_point->to_legacy_point();
+}
+
+EC_AffinePoint EC_AffinePoint::_from_inner(std::unique_ptr<EC_AffinePoint_Data> inner) {
+   return EC_AffinePoint(std::move(inner));
+}
+
+const std::shared_ptr<const EC_Group_Data>& EC_AffinePoint::_group() const {
+   return inner().group();
+}
+
+}  // namespace Botan
diff --git a/src/lib/pubkey/ec_group/ec_apoint.h b/src/lib/pubkey/ec_group/ec_apoint.h
new file mode 100644
index 000000000..592e06e5d
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_apoint.h
@@ -0,0 +1,168 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_EC_APOINT_H_
+#define BOTAN_EC_APOINT_H_
+
+#include <botan/concepts.h>
+#include <botan/secmem.h>
+#include <botan/types.h>
+#include <optional>
+#include <span>
+#include <string_view>
+#include <vector>
+
+namespace Botan {
+
+class BigInt;
+class RandomNumberGenerator;
+class EC_Group;
+class EC_Scalar;
+class EC_Point;
+
+class EC_Group_Data;
+class EC_AffinePoint_Data;
+
+class BOTAN_UNSTABLE_API EC_AffinePoint final {
+   public:
+      /// Point deserialization. Returns nullopt if wrong length or not a valid point
+      static std::optional<EC_AffinePoint> deserialize(const EC_Group& group, std::span<const uint8_t> bytes);
+
+      /// Multiply by the group generator returning a complete point
+      ///
+      /// Workspace argument is transitional
+      static EC_AffinePoint g_mul(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws);
+
+      /// Hash to curve (RFC 9380), random oracle variant
+      ///
+      /// Only supported for specific groups
+      static EC_AffinePoint hash_to_curve_ro(const EC_Group& group,
+                                             std::string_view hash_fn,
+                                             std::span<const uint8_t> input,
+                                             std::span<const uint8_t> domain_sep);
+
+      /// Hash to curve (RFC 9380), non uniform variant
+      ///
+      /// Only supported for specific groups
+      static EC_AffinePoint hash_to_curve_nu(const EC_Group& group,
+                                             std::string_view hash_fn,
+                                             std::span<const uint8_t> input,
+                                             std::span<const uint8_t> domain_sep);
+
+      /// Multiply a point by a scalar returning a complete point
+      ///
+      /// Workspace argument is transitional
+      EC_AffinePoint mul(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const;
+
+      /// Return the number of bytes of a field element
+      ///
+      /// A point consists of two field elements, plus possibly a header
+      size_t field_element_bytes() const;
+
+      /// Write the fixed length encoding of affine x coordinate
+      ///
+      /// The output span must be exactly field_element_bytes long
+      void serialize_x_to(std::span<uint8_t> bytes) const;
+
+      /// Write the fixed length encoding of affine y coordinate
+      ///
+      /// The output span must be exactly field_element_bytes long
+      void serialize_y_to(std::span<uint8_t> bytes) const;
+
+      /// Write the fixed length encoding of affine x and y coordinates
+      ///
+      /// The output span must be exactly 2*field_element_bytes long
+      void serialize_xy_to(std::span<uint8_t> bytes) const;
+
+      /// Write the fixed length SEC1 compressed encoding
+      ///
+      /// The output span must be exactly 1 + field_element_bytes long
+      void serialize_compressed_to(std::span<uint8_t> bytes) const;
+
+      /// Return the fixed length encoding of SEC1 uncompressed encoding
+      ///
+      /// The output span must be exactly 1 + 2*field_element_bytes long
+      void serialize_uncompressed_to(std::span<uint8_t> bytes) const;
+
+      /// Return the bytes of the affine x coordinate in a container
+      template <concepts::resizable_byte_buffer T = secure_vector<uint8_t>>
+      T x_bytes() const {
+         T bytes(this->field_element_bytes());
+         this->serialize_x_to(bytes);
+         return bytes;
+      }
+
+      /// Return the bytes of the affine y coordinate in a container
+      template <concepts::resizable_byte_buffer T = secure_vector<uint8_t>>
+      T y_bytes() const {
+         T bytes(this->field_element_bytes());
+         this->serialize_y_to(bytes);
+         return bytes;
+      }
+
+      /// Return the bytes of the affine x and y coordinates in a container
+      template <concepts::resizable_byte_buffer T = secure_vector<uint8_t>>
+      T xy_bytes() const {
+         T bytes(2 * this->field_element_bytes());
+         this->serialize_xy_to(bytes);
+         return bytes;
+      }
+
+      /// Return the bytes of the affine x and y coordinates in a container
+      template <concepts::resizable_byte_buffer T = std::vector<uint8_t>>
+      T serialize_uncompressed() const {
+         T bytes(1 + 2 * this->field_element_bytes());
+         this->serialize_uncompressed_to(bytes);
+         return bytes;
+      }
+
+      /// Return the bytes of the affine x and y coordinates in a container
+      template <concepts::resizable_byte_buffer T = std::vector<uint8_t>>
+      T serialize_compressed() const {
+         T bytes(1 + this->field_element_bytes());
+         this->serialize_compressed_to(bytes);
+         return bytes;
+      }
+
+      EC_AffinePoint(const EC_AffinePoint& other);
+      EC_AffinePoint(EC_AffinePoint&& other) noexcept;
+
+      EC_AffinePoint& operator=(const EC_AffinePoint& other);
+      EC_AffinePoint& operator=(EC_AffinePoint&& other) noexcept;
+
+      EC_AffinePoint(const EC_Group& group, std::span<const uint8_t> bytes);
+
+      /**
+      * Deprecated conversion
+      */
+      EC_AffinePoint(const EC_Group& group, const EC_Point& pt);
+
+      /**
+      * Deprecated conversion
+      */
+      EC_Point to_legacy_point() const;
+
+      ~EC_AffinePoint();
+
+      const EC_AffinePoint_Data& _inner() const { return inner(); }
+
+      static EC_AffinePoint _from_inner(std::unique_ptr<EC_AffinePoint_Data> inner);
+
+      const std::shared_ptr<const EC_Group_Data>& _group() const;
+
+   private:
+      friend class EC_Mul2Table;
+
+      EC_AffinePoint(std::unique_ptr<EC_AffinePoint_Data> point);
+
+      const EC_AffinePoint_Data& inner() const { return *m_point; }
+
+      std::unique_ptr<EC_AffinePoint_Data> m_point;
+};
+
+}  // namespace Botan
+
+#endif
diff --git a/src/lib/pubkey/ec_group/ec_group.cpp b/src/lib/pubkey/ec_group/ec_group.cpp
index e8c9afc96..2d66d3137 100644
--- a/src/lib/pubkey/ec_group/ec_group.cpp
+++ b/src/lib/pubkey/ec_group/ec_group.cpp
@@ -2,7 +2,7 @@
 * ECC Domain Parameters
 *
 * (C) 2007 Falko Strenzke, FlexSecure GmbH
-* (C) 2008,2018 Jack Lloyd
+* (C) 2008,2018,2024 Jack Lloyd
 * (C) 2018 Tobias Niemann
 *
 * Botan is released under the Simplified BSD License (see license.txt)
@@ -16,132 +16,14 @@
 #include <botan/pem.h>
 #include <botan/reducer.h>
 #include <botan/rng.h>
+#include <botan/internal/ec_inner_data.h>
 #include <botan/internal/fmt.h>
 #include <botan/internal/point_mul.h>
 #include <botan/internal/primality.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 {
-   public:
-      EC_Group_Data(const BigInt& p,
-                    const BigInt& a,
-                    const BigInt& b,
-                    const BigInt& g_x,
-                    const BigInt& g_y,
-                    const BigInt& order,
-                    const BigInt& cofactor,
-                    const OID& oid,
-                    EC_Group_Source source) :
-            m_curve(p, a, b),
-            m_base_point(m_curve, g_x, g_y),
-            m_g_x(g_x),
-            m_g_y(g_y),
-            m_order(order),
-            m_cofactor(cofactor),
-            m_mod_order(order),
-            m_base_mult(m_base_point, m_mod_order),
-            m_oid(oid),
-            m_p_bits(p.bits()),
-            m_order_bits(order.bits()),
-            m_a_is_minus_3(a == p - 3),
-            m_a_is_zero(a.is_zero()),
-            m_source(source) {}
-
-      bool params_match(const BigInt& p,
-                        const BigInt& a,
-                        const BigInt& b,
-                        const BigInt& g_x,
-                        const BigInt& g_y,
-                        const BigInt& order,
-                        const BigInt& cofactor) const {
-         return (this->p() == p && this->a() == a && this->b() == b && this->order() == order &&
-                 this->cofactor() == cofactor && this->g_x() == g_x && this->g_y() == g_y);
-      }
-
-      bool params_match(const EC_Group_Data& other) const {
-         return params_match(
-            other.p(), other.a(), other.b(), other.g_x(), other.g_y(), other.order(), other.cofactor());
-      }
-
-      void set_oid(const OID& oid) {
-         BOTAN_STATE_CHECK(m_oid.empty());
-         m_oid = oid;
-      }
-
-      const OID& oid() const { return m_oid; }
-
-      const BigInt& p() const { return m_curve.get_p(); }
-
-      const BigInt& a() const { return m_curve.get_a(); }
-
-      const BigInt& b() const { return m_curve.get_b(); }
-
-      const BigInt& order() const { return m_order; }
-
-      const BigInt& cofactor() const { return m_cofactor; }
-
-      const BigInt& g_x() const { return m_g_x; }
-
-      const BigInt& g_y() const { return m_g_y; }
-
-      size_t p_bits() const { return m_p_bits; }
-
-      size_t p_bytes() const { return (m_p_bits + 7) / 8; }
-
-      size_t order_bits() const { return m_order_bits; }
-
-      size_t order_bytes() const { return (m_order_bits + 7) / 8; }
-
-      const CurveGFp& curve() const { return m_curve; }
-
-      const EC_Point& base_point() const { return m_base_point; }
-
-      bool a_is_minus_3() const { return m_a_is_minus_3; }
-
-      bool a_is_zero() const { return m_a_is_zero; }
-
-      BigInt mod_order(const BigInt& x) const { return m_mod_order.reduce(x); }
-
-      BigInt square_mod_order(const BigInt& x) const { return m_mod_order.square(x); }
-
-      BigInt multiply_mod_order(const BigInt& x, const BigInt& y) const { return m_mod_order.multiply(x, y); }
-
-      BigInt multiply_mod_order(const BigInt& x, const BigInt& y, const BigInt& z) const {
-         return m_mod_order.multiply(m_mod_order.multiply(x, y), z);
-      }
-
-      BigInt inverse_mod_order(const BigInt& x) const { return inverse_mod(x, m_order); }
-
-      EC_Point blinded_base_point_multiply(const BigInt& k, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const {
-         return m_base_mult.mul(k, rng, m_order, ws);
-      }
-
-      EC_Group_Source source() const { return m_source; }
-
-   private:
-      CurveGFp m_curve;
-      EC_Point m_base_point;
-
-      BigInt m_g_x;
-      BigInt m_g_y;
-      BigInt m_order;
-      BigInt m_cofactor;
-      Modular_Reducer m_mod_order;
-      EC_Point_Base_Point_Precompute m_base_mult;
-      OID m_oid;
-      size_t m_p_bits;
-      size_t m_order_bits;
-      bool m_a_is_minus_3;
-      bool m_a_is_zero;
-      EC_Group_Source m_source;
-};
-
 class EC_Group_Data_Map final {
    public:
       EC_Group_Data_Map() = default;
@@ -564,7 +446,7 @@ const BigInt& EC_Group::get_b() const {
    return data().b();
 }
 
-const EC_Point& EC_Group::get_base_point() const {
+const EC_Point& EC_Group::generator() const {
    return data().base_point();
 }
 
@@ -584,6 +466,10 @@ const BigInt& EC_Group::get_cofactor() const {
    return data().cofactor();
 }
 
+bool EC_Group::has_cofactor() const {
+   return data().has_cofactor();
+}
+
 BigInt EC_Group::mod_order(const BigInt& k) const {
    return data().mod_order(k);
 }
@@ -691,19 +577,13 @@ EC_Point EC_Group::hash_to_curve(std::string_view hash_fn,
                                  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");
+   if(random_oracle) {
+      return EC_AffinePoint::hash_to_curve_ro(*this, hash_fn, {input, input_len}, {domain_sep, domain_sep_len})
+         .to_legacy_point();
+   } else {
+      return EC_AffinePoint::hash_to_curve_nu(*this, hash_fn, {input, input_len}, {domain_sep, domain_sep_len})
+         .to_legacy_point();
    }
-
-   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 {
@@ -777,7 +657,7 @@ bool EC_Group::verify_public_element(const EC_Point& point) const {
       return false;
    }
 
-   if(get_cofactor() > 1) {
+   if(has_cofactor()) {
       if((point * get_cofactor()).is_zero()) {
          return false;
       }
@@ -856,4 +736,35 @@ bool EC_Group::verify_group(RandomNumberGenerator& rng, bool strong) const {
    return true;
 }
 
+EC_Group::Mul2Table::Mul2Table(const EC_Group& group, const EC_Point& h) :
+      EC_Group::Mul2Table(EC_AffinePoint(group, h)) {}
+
+EC_Group::Mul2Table::Mul2Table(const EC_AffinePoint& h) : m_tbl(h._group()->make_mul2_table(h._inner())) {}
+
+EC_Group::Mul2Table::~Mul2Table() = default;
+
+std::optional<EC_AffinePoint> EC_Group::Mul2Table::mul2_vartime(const EC_Scalar& x, const EC_Scalar& y) const {
+   auto pt = m_tbl->mul2_vartime(x._inner(), y._inner());
+   if(pt) {
+      return EC_AffinePoint::_from_inner(std::move(pt));
+   } else {
+      return {};
+   }
+}
+
+std::optional<EC_Scalar> EC_Group::Mul2Table::mul2_vartime_x_mod_order(const EC_Scalar& x, const EC_Scalar& y) const {
+   auto s = m_tbl->mul2_vartime_x_mod_order(x._inner(), y._inner());
+   if(s) {
+      return EC_Scalar::_from_inner(std::move(s));
+   } else {
+      return {};
+   }
+}
+
+std::optional<EC_Scalar> EC_Group::Mul2Table::mul2_vartime_x_mod_order(const EC_Scalar& c,
+                                                                       const EC_Scalar& x,
+                                                                       const EC_Scalar& y) const {
+   return this->mul2_vartime_x_mod_order(c * x, c * y);
+}
+
 }  // namespace Botan
diff --git a/src/lib/pubkey/ec_group/ec_group.h b/src/lib/pubkey/ec_group/ec_group.h
index 502cce4bf..9bdd063e3 100644
--- a/src/lib/pubkey/ec_group/ec_group.h
+++ b/src/lib/pubkey/ec_group/ec_group.h
@@ -2,7 +2,7 @@
 * ECC Domain Parameters
 *
 * (C) 2007 Falko Strenzke, FlexSecure GmbH
-*     2008-2010 Jack Lloyd
+*     2008-2010,2024 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
@@ -11,7 +11,9 @@
 #define BOTAN_ECC_DOMAIN_PARAMETERS_H_
 
 #include <botan/asn1_obj.h>
+#include <botan/ec_apoint.h>
 #include <botan/ec_point.h>
+#include <botan/ec_scalar.h>
 #include <memory>
 #include <set>
 #include <span>
@@ -36,6 +38,7 @@ enum class EC_Group_Source {
    ExternalSource,
 };
 
+class EC_Mul2Table_Data;
 class EC_Group_Data;
 class EC_Group_Data_Map;
 
@@ -212,6 +215,62 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       */
       bool verify_public_element(const EC_Point& y) const;
 
+      /// Table for computing g*x + h*y
+      class Mul2Table final {
+         public:
+            /**
+            * Internal transition function
+            *
+            * @warning this will be removed in 3.6.0, NOT COVERED BY SEMVER
+            */
+            Mul2Table(const EC_Group& group, const EC_Point& h);
+
+            /**
+            * Create a table for computing g*x + h*y
+            */
+            Mul2Table(const EC_AffinePoint& h);
+
+            /**
+            * Return the elliptic curve point g*x + h*y
+            *
+            * Where g is the group generator and h is the value passed to the constructor
+            *
+            * Returns nullopt if g*x + h*y was the point at infinity
+            *
+            * @warning this function is variable time with respect to x and y
+            */
+            std::optional<EC_AffinePoint> mul2_vartime(const EC_Scalar& x, const EC_Scalar& y) const;
+
+            /**
+            * Return the x coordinate of g*x + h*y, reduced modulo the order
+            *
+            * Where g is the group generator and h is the value passed to the constructor
+            *
+            * Returns nullopt if g*x + h*y was the point at infinity
+            *
+            * @warning this function is variable time with respect to x and y
+            */
+            std::optional<EC_Scalar> mul2_vartime_x_mod_order(const EC_Scalar& x, const EC_Scalar& y) const;
+
+            /**
+            * Return the x coordinate of g*x*c + h*y*c, reduced modulo the order
+            *
+            * Where g is the group generator and h is the value passed to the constructor
+            *
+            * Returns nullopt if g*x*c + h*y*c was the point at infinity
+            *
+            * @warning this function is variable time with respect to c, x and y
+            */
+            std::optional<EC_Scalar> mul2_vartime_x_mod_order(const EC_Scalar& c,
+                                                              const EC_Scalar& x,
+                                                              const EC_Scalar& y) const;
+
+            ~Mul2Table();
+
+         private:
+            std::unique_ptr<EC_Mul2Table_Data> m_tbl;
+      };
+
       /**
       * Return the OID of these domain parameters
       * @result the OID
@@ -237,7 +296,13 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       * Return group base point
       * @result base point
       */
-      const EC_Point& get_base_point() const;
+      const EC_Point& get_base_point() const { return generator(); }
+
+      /**
+      * Return the canonical group generator
+      * @result standard generator of the curve
+      */
+      const EC_Point& generator() const;
 
       /**
       * Return the x coordinate of the base point
@@ -261,10 +326,16 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       */
       const BigInt& get_cofactor() const;
 
+      /**
+      * Return true if the cofactor is > 1
+      */
+      bool has_cofactor() const;
+
       /**
       * Multi exponentiate. Not constant time.
       * @return base_point*x + pt*y
       */
+      BOTAN_DEPRECATED("Use EC_Group::Mul2Table")
       EC_Point point_multiply(const BigInt& x, const EC_Point& pt, const BigInt& y) const;
 
       /**
@@ -274,6 +345,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       * @param ws a temp workspace
       * @return base_point*k
       */
+      BOTAN_DEPRECATED("Use EC_AffinePoint and EC_Scalar")
       EC_Point blinded_base_point_multiply(const BigInt& k, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const;
 
       /**
@@ -285,6 +357,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       * @param ws a temp workspace
       * @return x coordinate of base_point*k
       */
+      BOTAN_DEPRECATED("Use EC_AffinePoint and EC_Scalar")
       BigInt blinded_base_point_multiply_x(const BigInt& k, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const;
 
       /**
@@ -295,6 +368,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       * @param ws a temp workspace
       * @return point*k
       */
+      BOTAN_DEPRECATED("Use EC_AffinePoint and EC_Scalar")
       EC_Point blinded_var_point_multiply(const EC_Point& point,
                                           const BigInt& k,
                                           RandomNumberGenerator& rng,
@@ -303,7 +377,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       /**
       * Return a random scalar ie an integer in [1,order)
       */
-      BigInt random_scalar(RandomNumberGenerator& rng) const;
+      BOTAN_DEPRECATED("Use EC_Scalar::random") BigInt random_scalar(RandomNumberGenerator& rng) const;
 
       /**
       * Hash onto the curve.
@@ -414,7 +488,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       /*
       * Reduce (x*y*z) modulo the order
       */
-      BOTAN_DEPRECATED("Deprecated no replacement")
+      BOTAN_DEPRECATED("Use EC_Scalar")
       BigInt multiply_mod_order(const BigInt& x, const BigInt& y, const BigInt& z) const;
 
       /*
@@ -449,6 +523,11 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
       */
       static OID EC_group_identity_from_order(const BigInt& order);
 
+      /*
+      * For internal use only
+      */
+      const std::shared_ptr<EC_Group_Data>& _data() const { return m_data; }
+
    private:
       static EC_Group_Data_Map& ec_group_data();
 
@@ -465,8 +544,9 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final {
                                                                const char* order,
                                                                const OID& oid);
 
-      // Member data
       const EC_Group_Data& data() const;
+
+      // Member data
       std::shared_ptr<EC_Group_Data> m_data;
       bool m_explicit_encoding = false;
 };
diff --git a/src/lib/pubkey/ec_group/ec_inner_bn.cpp b/src/lib/pubkey/ec_group/ec_inner_bn.cpp
new file mode 100644
index 000000000..710fec320
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_inner_bn.cpp
@@ -0,0 +1,180 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/internal/ec_inner_bn.h>
+
+namespace Botan {
+
+const EC_Scalar_Data_BN& EC_Scalar_Data_BN::checked_ref(const EC_Scalar_Data& data) {
+   const auto* p = dynamic_cast<const EC_Scalar_Data_BN*>(&data);
+   if(!p) {
+      throw Invalid_State("Failed conversion to EC_Scalar_Data_BN");
+   }
+   return *p;
+}
+
+const std::shared_ptr<const EC_Group_Data>& EC_Scalar_Data_BN::group() const {
+   return m_group;
+}
+
+size_t EC_Scalar_Data_BN::bytes() const {
+   return this->group()->order_bytes();
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::clone() const {
+   return std::make_unique<EC_Scalar_Data_BN>(this->group(), this->value());
+}
+
+bool EC_Scalar_Data_BN::is_zero() const {
+   return this->value().is_zero();
+}
+
+bool EC_Scalar_Data_BN::is_eq(const EC_Scalar_Data& other) const {
+   return (value() == checked_ref(other).value());
+}
+
+void EC_Scalar_Data_BN::assign(const EC_Scalar_Data& other) {
+   m_v = checked_ref(other).value();
+}
+
+void EC_Scalar_Data_BN::square_self() {
+   m_group->square_mod_order(m_v);
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::negate() const {
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->mod_order(-m_v));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::invert() const {
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->inverse_mod_order(m_v));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::add(const EC_Scalar_Data& other) const {
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->mod_order(m_v + checked_ref(other).value()));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::sub(const EC_Scalar_Data& other) const {
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->mod_order(m_v - checked_ref(other).value()));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Scalar_Data_BN::mul(const EC_Scalar_Data& other) const {
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->multiply_mod_order(m_v, checked_ref(other).value()));
+}
+
+void EC_Scalar_Data_BN::serialize_to(std::span<uint8_t> bytes) const {
+   BOTAN_ARG_CHECK(bytes.size() == m_group->order_bytes(), "Invalid output length");
+   m_v.serialize_to(bytes);
+}
+
+EC_AffinePoint_Data_BN::EC_AffinePoint_Data_BN(std::shared_ptr<const EC_Group_Data> group, EC_Point pt) :
+      m_group(std::move(group)), m_pt(std::move(pt)) {
+   m_pt.force_affine();
+   m_xy = m_pt.xy_bytes();
+}
+
+EC_AffinePoint_Data_BN::EC_AffinePoint_Data_BN(std::shared_ptr<const EC_Group_Data> group,
+                                               std::span<const uint8_t> pt) :
+      m_group(std::move(group)) {
+   BOTAN_ASSERT_NONNULL(m_group);
+   m_pt = Botan::OS2ECP(pt.data(), pt.size(), m_group->curve());
+   m_xy = m_pt.xy_bytes();
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_AffinePoint_Data_BN::clone() const {
+   return std::make_unique<EC_AffinePoint_Data_BN>(m_group, m_pt);
+}
+
+const std::shared_ptr<const EC_Group_Data>& EC_AffinePoint_Data_BN::group() const {
+   return m_group;
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_AffinePoint_Data_BN::mul(const EC_Scalar_Data& scalar,
+                                                                 RandomNumberGenerator& rng,
+                                                                 std::vector<BigInt>& ws) const {
+   BOTAN_ARG_CHECK(scalar.group() == m_group, "Curve mismatch");
+   const auto& bn = EC_Scalar_Data_BN::checked_ref(scalar);
+
+   EC_Point_Var_Point_Precompute mul(m_pt, rng, ws);
+
+   const auto order = m_group->order() * m_group->cofactor();
+   auto pt = mul.mul(bn.value(), rng, order, ws);
+   return std::make_unique<EC_AffinePoint_Data_BN>(m_group, std::move(pt));
+}
+
+size_t EC_AffinePoint_Data_BN::field_element_bytes() const {
+   return m_xy.size() / 2;
+}
+
+void EC_AffinePoint_Data_BN::serialize_x_to(std::span<uint8_t> bytes) const {
+   const size_t fe_bytes = this->field_element_bytes();
+   BOTAN_ARG_CHECK(bytes.size() == fe_bytes, "Invalid output size");
+   copy_mem(bytes, std::span{m_xy}.first(fe_bytes));
+}
+
+void EC_AffinePoint_Data_BN::serialize_y_to(std::span<uint8_t> bytes) const {
+   const size_t fe_bytes = this->field_element_bytes();
+   BOTAN_ARG_CHECK(bytes.size() == fe_bytes, "Invalid output size");
+   copy_mem(bytes, std::span{m_xy}.last(fe_bytes));
+}
+
+void EC_AffinePoint_Data_BN::serialize_xy_to(std::span<uint8_t> bytes) const {
+   const size_t fe_bytes = this->field_element_bytes();
+   BOTAN_ARG_CHECK(bytes.size() == 2 * fe_bytes, "Invalid output size");
+   copy_mem(bytes, m_xy);
+}
+
+void EC_AffinePoint_Data_BN::serialize_compressed_to(std::span<uint8_t> bytes) const {
+   const size_t fe_bytes = this->field_element_bytes();
+   BOTAN_ARG_CHECK(bytes.size() == 1 + fe_bytes, "Invalid output size");
+   const bool y_is_odd = (m_xy[m_xy.size() - 1] & 0x01) == 0x01;
+
+   BufferStuffer stuffer(bytes);
+   stuffer.append(y_is_odd ? 0x03 : 0x02);
+   serialize_x_to(stuffer.next(fe_bytes));
+}
+
+void EC_AffinePoint_Data_BN::serialize_uncompressed_to(std::span<uint8_t> bytes) const {
+   const size_t fe_bytes = this->field_element_bytes();
+   BOTAN_ARG_CHECK(bytes.size() == 1 + 2 * fe_bytes, "Invalid output size");
+   BufferStuffer stuffer(bytes);
+   stuffer.append(0x04);
+   stuffer.append(m_xy);
+}
+
+EC_Mul2Table_Data_BN::EC_Mul2Table_Data_BN(const EC_AffinePoint_Data& g, const EC_AffinePoint_Data& h) :
+      m_group(g.group()), m_tbl(g.to_legacy_point(), h.to_legacy_point()) {
+   BOTAN_ARG_CHECK(h.group() == m_group, "Curve mismatch");
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_Mul2Table_Data_BN::mul2_vartime(const EC_Scalar_Data& x,
+                                                                        const EC_Scalar_Data& y) const {
+   BOTAN_ARG_CHECK(x.group() == m_group && y.group() == m_group, "Curve mismatch");
+
+   const auto& bn_x = EC_Scalar_Data_BN::checked_ref(x);
+   const auto& bn_y = EC_Scalar_Data_BN::checked_ref(y);
+   auto pt = m_tbl.multi_exp(bn_x.value(), bn_y.value());
+
+   if(pt.is_zero()) {
+      return nullptr;
+   }
+   return std::make_unique<EC_AffinePoint_Data_BN>(m_group, std::move(pt));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Mul2Table_Data_BN::mul2_vartime_x_mod_order(const EC_Scalar_Data& x,
+                                                                               const EC_Scalar_Data& y) const {
+   BOTAN_ARG_CHECK(x.group() == m_group && y.group() == m_group, "Curve mismatch");
+
+   const auto& bn_x = EC_Scalar_Data_BN::checked_ref(x);
+   const auto& bn_y = EC_Scalar_Data_BN::checked_ref(y);
+   auto pt = m_tbl.multi_exp(bn_x.value(), bn_y.value());
+
+   if(pt.is_zero()) {
+      return nullptr;
+   }
+   return std::make_unique<EC_Scalar_Data_BN>(m_group, m_group->mod_order(pt.get_affine_x()));
+}
+
+}  // namespace Botan
diff --git a/src/lib/pubkey/ec_group/ec_inner_bn.h b/src/lib/pubkey/ec_group/ec_inner_bn.h
new file mode 100644
index 000000000..a130ccf26
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_inner_bn.h
@@ -0,0 +1,105 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_EC_INNER_DATA_BN_H_
+#define BOTAN_EC_INNER_DATA_BN_H_
+
+#include <botan/internal/ec_inner_data.h>
+
+namespace Botan {
+
+class EC_Scalar_Data_BN final : public EC_Scalar_Data {
+   public:
+      EC_Scalar_Data_BN(std::shared_ptr<const EC_Group_Data> group, BigInt v) :
+            m_group(std::move(group)), m_v(std::move(v)) {}
+
+      static const EC_Scalar_Data_BN& checked_ref(const EC_Scalar_Data& data);
+
+      const std::shared_ptr<const EC_Group_Data>& group() const override;
+
+      std::unique_ptr<EC_Scalar_Data> clone() const override;
+
+      size_t bytes() const override;
+
+      bool is_zero() const override;
+
+      bool is_eq(const EC_Scalar_Data& y) const override;
+
+      void assign(const EC_Scalar_Data& y) override;
+
+      void square_self() override;
+
+      std::unique_ptr<EC_Scalar_Data> negate() const override;
+
+      std::unique_ptr<EC_Scalar_Data> invert() const override;
+
+      std::unique_ptr<EC_Scalar_Data> add(const EC_Scalar_Data& other) const override;
+
+      std::unique_ptr<EC_Scalar_Data> sub(const EC_Scalar_Data& other) const override;
+
+      std::unique_ptr<EC_Scalar_Data> mul(const EC_Scalar_Data& other) const override;
+
+      void serialize_to(std::span<uint8_t> bytes) const override;
+
+      const BigInt& value() const { return m_v; }
+
+   private:
+      std::shared_ptr<const EC_Group_Data> m_group;
+      BigInt m_v;
+};
+
+class EC_AffinePoint_Data_BN final : public EC_AffinePoint_Data {
+   public:
+      EC_AffinePoint_Data_BN(std::shared_ptr<const EC_Group_Data> group, EC_Point pt);
+
+      EC_AffinePoint_Data_BN(std::shared_ptr<const EC_Group_Data> group, std::span<const uint8_t> pt);
+
+      const std::shared_ptr<const EC_Group_Data>& group() const override;
+
+      std::unique_ptr<EC_AffinePoint_Data> clone() const override;
+
+      size_t field_element_bytes() const override;
+
+      void serialize_x_to(std::span<uint8_t> bytes) const override;
+
+      void serialize_y_to(std::span<uint8_t> bytes) const override;
+
+      void serialize_xy_to(std::span<uint8_t> bytes) const override;
+
+      void serialize_compressed_to(std::span<uint8_t> bytes) const override;
+
+      void serialize_uncompressed_to(std::span<uint8_t> bytes) const override;
+
+      std::unique_ptr<EC_AffinePoint_Data> mul(const EC_Scalar_Data& scalar,
+                                               RandomNumberGenerator& rng,
+                                               std::vector<BigInt>& ws) const override;
+
+      EC_Point to_legacy_point() const override { return m_pt; }
+
+   private:
+      std::shared_ptr<const EC_Group_Data> m_group;
+      EC_Point m_pt;
+      secure_vector<uint8_t> m_xy;
+};
+
+class EC_Mul2Table_Data_BN final : public EC_Mul2Table_Data {
+   public:
+      EC_Mul2Table_Data_BN(const EC_AffinePoint_Data& g, const EC_AffinePoint_Data& h);
+
+      std::unique_ptr<EC_AffinePoint_Data> mul2_vartime(const EC_Scalar_Data& x,
+                                                        const EC_Scalar_Data& y) const override;
+
+      std::unique_ptr<EC_Scalar_Data> mul2_vartime_x_mod_order(const EC_Scalar_Data& x,
+                                                               const EC_Scalar_Data& y) const override;
+
+   private:
+      std::shared_ptr<const EC_Group_Data> m_group;
+      EC_Point_Multi_Point_Precompute m_tbl;
+};
+
+}  // namespace Botan
+
+#endif
diff --git a/src/lib/pubkey/ec_group/ec_inner_data.cpp b/src/lib/pubkey/ec_group/ec_inner_data.cpp
new file mode 100644
index 000000000..1ec53cf95
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_inner_data.cpp
@@ -0,0 +1,159 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/internal/ec_inner_data.h>
+
+#include <botan/internal/ec_inner_bn.h>
+
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+   #include <botan/internal/ec_h2c.h>
+#endif
+
+namespace Botan {
+
+EC_Group_Data::EC_Group_Data(const BigInt& p,
+                             const BigInt& a,
+                             const BigInt& b,
+                             const BigInt& g_x,
+                             const BigInt& g_y,
+                             const BigInt& order,
+                             const BigInt& cofactor,
+                             const OID& oid,
+                             EC_Group_Source source) :
+      m_curve(p, a, b),
+      m_base_point(m_curve, g_x, g_y),
+      m_g_x(g_x),
+      m_g_y(g_y),
+      m_order(order),
+      m_cofactor(cofactor),
+      m_mod_order(order),
+      m_base_mult(m_base_point, m_mod_order),
+      m_oid(oid),
+      m_p_bits(p.bits()),
+      m_order_bits(order.bits()),
+      m_order_bytes((m_order_bits + 7) / 8),
+      m_a_is_minus_3(a == p - 3),
+      m_a_is_zero(a.is_zero()),
+      m_has_cofactor(m_cofactor != 1),
+      m_source(source) {}
+
+bool EC_Group_Data::params_match(const BigInt& p,
+                                 const BigInt& a,
+                                 const BigInt& b,
+                                 const BigInt& g_x,
+                                 const BigInt& g_y,
+                                 const BigInt& order,
+                                 const BigInt& cofactor) const {
+   return (this->p() == p && this->a() == a && this->b() == b && this->order() == order &&
+           this->cofactor() == cofactor && this->g_x() == g_x && this->g_y() == g_y);
+}
+
+bool EC_Group_Data::params_match(const EC_Group_Data& other) const {
+   return params_match(other.p(), other.a(), other.b(), other.g_x(), other.g_y(), other.order(), other.cofactor());
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_from_bytes_with_trunc(std::span<const uint8_t> bytes) const {
+   auto bn = BigInt::from_bytes_with_max_bits(bytes.data(), bytes.size(), m_order_bits);
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), mod_order(bn));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_from_bytes_mod_order(std::span<const uint8_t> bytes) const {
+   BOTAN_ARG_CHECK(bytes.size() <= 2 * order_bytes(), "Input too large");
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), mod_order(BigInt(bytes)));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_random(RandomNumberGenerator& rng) const {
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), BigInt::random_integer(rng, BigInt::one(), m_order));
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_zero() const {
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), BigInt::zero());
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_one() const {
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), BigInt::one());
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_from_bigint(const BigInt& bn) const {
+   // Assumed to have been already checked as in range
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), bn);
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::gk_x_mod_order(const EC_Scalar_Data& scalar,
+                                                              RandomNumberGenerator& rng,
+                                                              std::vector<BigInt>& ws) const {
+   const auto& bn = EC_Scalar_Data_BN::checked_ref(scalar);
+   const auto pt = m_base_mult.mul(bn.value(), rng, m_order, ws);
+
+   if(pt.is_zero()) {
+      return scalar_zero();
+   } else {
+      return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), mod_order(pt.get_affine_x()));
+   }
+}
+
+std::unique_ptr<EC_Scalar_Data> EC_Group_Data::scalar_deserialize(std::span<const uint8_t> bytes) {
+   if(bytes.size() != m_order_bytes) {
+      return nullptr;
+   }
+
+   BigInt r(bytes.data(), bytes.size());
+
+   if(r.is_zero() || r >= m_order) {
+      return nullptr;
+   }
+
+   return std::make_unique<EC_Scalar_Data_BN>(shared_from_this(), std::move(r));
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_Group_Data::point_deserialize(std::span<const uint8_t> bytes) const {
+   try {
+      auto pt = Botan::OS2ECP(bytes.data(), bytes.size(), curve());
+      return std::make_unique<EC_AffinePoint_Data_BN>(shared_from_this(), std::move(pt));
+   } catch(...) {
+      return nullptr;
+   }
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_Group_Data::point_hash_to_curve_ro(std::string_view hash_fn,
+                                                                           std::span<const uint8_t> input,
+                                                                           std::span<const uint8_t> domain_sep) const {
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+   auto pt = hash_to_curve_sswu(*this, hash_fn, input, domain_sep, true);
+   return std::make_unique<EC_AffinePoint_Data_BN>(shared_from_this(), std::move(pt));
+#else
+   BOTAN_UNUSED(hash_fn, input, domain_sep);
+   throw Not_Implemented("Hashing to curve not available in this build");
+#endif
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_Group_Data::point_hash_to_curve_nu(std::string_view hash_fn,
+                                                                           std::span<const uint8_t> input,
+                                                                           std::span<const uint8_t> domain_sep) const {
+#if defined(BOTAN_HAS_EC_HASH_TO_CURVE)
+   auto pt = hash_to_curve_sswu(*this, hash_fn, input, domain_sep, false);
+   return std::make_unique<EC_AffinePoint_Data_BN>(shared_from_this(), std::move(pt));
+#else
+   BOTAN_UNUSED(hash_fn, input, domain_sep);
+   throw Not_Implemented("Hashing to curve not available in this build");
+#endif
+}
+
+std::unique_ptr<EC_AffinePoint_Data> EC_Group_Data::point_g_mul(const EC_Scalar_Data& scalar,
+                                                                RandomNumberGenerator& rng,
+                                                                std::vector<BigInt>& ws) const {
+   const auto& group = scalar.group();
+   const auto& bn = EC_Scalar_Data_BN::checked_ref(scalar);
+   auto pt = group->blinded_base_point_multiply(bn.value(), rng, ws);
+   return std::make_unique<EC_AffinePoint_Data_BN>(shared_from_this(), std::move(pt));
+}
+
+std::unique_ptr<EC_Mul2Table_Data> EC_Group_Data::make_mul2_table(const EC_AffinePoint_Data& h) const {
+   EC_AffinePoint_Data_BN g(shared_from_this(), this->base_point());
+   return std::make_unique<EC_Mul2Table_Data_BN>(g, h);
+}
+
+}  // namespace Botan
diff --git a/src/lib/pubkey/ec_group/ec_inner_data.h b/src/lib/pubkey/ec_group/ec_inner_data.h
new file mode 100644
index 000000000..c6462ebba
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_inner_data.h
@@ -0,0 +1,236 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_EC_INNER_DATA_H_
+#define BOTAN_EC_INNER_DATA_H_
+
+#include <botan/ec_group.h>
+
+#include <botan/asn1_obj.h>
+#include <botan/bigint.h>
+#include <botan/reducer.h>
+#include <botan/internal/point_mul.h>
+#include <botan/internal/stl_util.h>
+#include <memory>
+#include <span>
+
+namespace Botan {
+
+class EC_Group_Data;
+
+class EC_Scalar_Data {
+   public:
+      virtual ~EC_Scalar_Data() = default;
+
+      virtual const std::shared_ptr<const EC_Group_Data>& group() const = 0;
+
+      virtual size_t bytes() const = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> clone() const = 0;
+
+      virtual bool is_zero() const = 0;
+
+      virtual bool is_eq(const EC_Scalar_Data& y) const = 0;
+
+      virtual void assign(const EC_Scalar_Data& y) = 0;
+
+      virtual void square_self() = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> negate() const = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> invert() const = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> add(const EC_Scalar_Data& other) const = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> sub(const EC_Scalar_Data& other) const = 0;
+
+      virtual std::unique_ptr<EC_Scalar_Data> mul(const EC_Scalar_Data& other) const = 0;
+
+      virtual void serialize_to(std::span<uint8_t> bytes) const = 0;
+};
+
+class EC_AffinePoint_Data {
+   public:
+      virtual ~EC_AffinePoint_Data() = default;
+
+      virtual const std::shared_ptr<const EC_Group_Data>& group() const = 0;
+
+      virtual std::unique_ptr<EC_AffinePoint_Data> clone() const = 0;
+
+      // Return size of a field element
+      virtual size_t field_element_bytes() const = 0;
+
+      // Writes 1 field element worth of data to bytes
+      virtual void serialize_x_to(std::span<uint8_t> bytes) const = 0;
+
+      // Writes 1 field element worth of data to bytes
+      virtual void serialize_y_to(std::span<uint8_t> bytes) const = 0;
+
+      // Writes 2 field elements worth of data to bytes
+      virtual void serialize_xy_to(std::span<uint8_t> bytes) const = 0;
+
+      // Writes 1 byte + 1 field element worth of data to bytes
+      virtual void serialize_compressed_to(std::span<uint8_t> bytes) const = 0;
+
+      // Writes 1 byte + 2 field elements worth of data to bytes
+      virtual void serialize_uncompressed_to(std::span<uint8_t> bytes) const = 0;
+
+      virtual std::unique_ptr<EC_AffinePoint_Data> mul(const EC_Scalar_Data& scalar,
+                                                       RandomNumberGenerator& rng,
+                                                       std::vector<BigInt>& ws) const = 0;
+
+      virtual EC_Point to_legacy_point() const = 0;
+};
+
+class EC_Mul2Table_Data {
+   public:
+      virtual ~EC_Mul2Table_Data() = default;
+
+      // Returns nullptr if g*x + h*y was point at infinity
+      virtual std::unique_ptr<EC_AffinePoint_Data> mul2_vartime(const EC_Scalar_Data& x,
+                                                                const EC_Scalar_Data& y) const = 0;
+
+      // Returns nullptr if g*x + h*y was point at infinity
+      virtual std::unique_ptr<EC_Scalar_Data> mul2_vartime_x_mod_order(const EC_Scalar_Data& x,
+                                                                       const EC_Scalar_Data& y) const = 0;
+};
+
+class EC_Group_Data final : public std::enable_shared_from_this<EC_Group_Data> {
+   public:
+      EC_Group_Data(const BigInt& p,
+                    const BigInt& a,
+                    const BigInt& b,
+                    const BigInt& g_x,
+                    const BigInt& g_y,
+                    const BigInt& order,
+                    const BigInt& cofactor,
+                    const OID& oid,
+                    EC_Group_Source source);
+
+      bool params_match(const BigInt& p,
+                        const BigInt& a,
+                        const BigInt& b,
+                        const BigInt& g_x,
+                        const BigInt& g_y,
+                        const BigInt& order,
+                        const BigInt& cofactor) const;
+
+      bool params_match(const EC_Group_Data& other) const;
+
+      void set_oid(const OID& oid) {
+         BOTAN_STATE_CHECK(m_oid.empty());
+         m_oid = oid;
+      }
+
+      const OID& oid() const { return m_oid; }
+
+      const BigInt& p() const { return m_curve.get_p(); }
+
+      const BigInt& a() const { return m_curve.get_a(); }
+
+      const BigInt& b() const { return m_curve.get_b(); }
+
+      const BigInt& order() const { return m_order; }
+
+      const BigInt& cofactor() const { return m_cofactor; }
+
+      bool has_cofactor() const { return m_has_cofactor; }
+
+      const BigInt& g_x() const { return m_g_x; }
+
+      const BigInt& g_y() const { return m_g_y; }
+
+      size_t p_bits() const { return m_p_bits; }
+
+      size_t p_bytes() const { return (m_p_bits + 7) / 8; }
+
+      size_t order_bits() const { return m_order_bits; }
+
+      size_t order_bytes() const { return m_order_bytes; }
+
+      const CurveGFp& curve() const { return m_curve; }
+
+      const EC_Point& base_point() const { return m_base_point; }
+
+      bool a_is_minus_3() const { return m_a_is_minus_3; }
+
+      bool a_is_zero() const { return m_a_is_zero; }
+
+      BigInt mod_order(const BigInt& x) const { return m_mod_order.reduce(x); }
+
+      BigInt square_mod_order(const BigInt& x) const { return m_mod_order.square(x); }
+
+      BigInt multiply_mod_order(const BigInt& x, const BigInt& y) const { return m_mod_order.multiply(x, y); }
+
+      BigInt multiply_mod_order(const BigInt& x, const BigInt& y, const BigInt& z) const {
+         return m_mod_order.multiply(m_mod_order.multiply(x, y), z);
+      }
+
+      BigInt inverse_mod_order(const BigInt& x) const { return inverse_mod(x, m_order); }
+
+      EC_Point blinded_base_point_multiply(const BigInt& k, RandomNumberGenerator& rng, std::vector<BigInt>& ws) const {
+         return m_base_mult.mul(k, rng, m_order, ws);
+      }
+
+      EC_Group_Source source() const { return m_source; }
+
+      std::unique_ptr<EC_Scalar_Data> scalar_from_bytes_with_trunc(std::span<const uint8_t> bytes) const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_from_bytes_mod_order(std::span<const uint8_t> bytes) const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_from_bigint(const BigInt& bn) const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_random(RandomNumberGenerator& rng) const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_zero() const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_one() const;
+
+      std::unique_ptr<EC_Scalar_Data> gk_x_mod_order(const EC_Scalar_Data& scalar,
+                                                     RandomNumberGenerator& rng,
+                                                     std::vector<BigInt>& ws) const;
+
+      std::unique_ptr<EC_Scalar_Data> scalar_deserialize(std::span<const uint8_t> bytes);
+
+      std::unique_ptr<EC_AffinePoint_Data> point_deserialize(std::span<const uint8_t> bytes) const;
+
+      std::unique_ptr<EC_AffinePoint_Data> point_hash_to_curve_ro(std::string_view hash_fn,
+                                                                  std::span<const uint8_t> input,
+                                                                  std::span<const uint8_t> domain_sep) const;
+
+      std::unique_ptr<EC_AffinePoint_Data> point_hash_to_curve_nu(std::string_view hash_fn,
+                                                                  std::span<const uint8_t> input,
+                                                                  std::span<const uint8_t> domain_sep) const;
+
+      std::unique_ptr<EC_AffinePoint_Data> point_g_mul(const EC_Scalar_Data& scalar,
+                                                       RandomNumberGenerator& rng,
+                                                       std::vector<BigInt>& ws) const;
+
+      std::unique_ptr<EC_Mul2Table_Data> make_mul2_table(const EC_AffinePoint_Data& pt) const;
+
+   private:
+      CurveGFp m_curve;
+      EC_Point m_base_point;
+
+      BigInt m_g_x;
+      BigInt m_g_y;
+      BigInt m_order;
+      BigInt m_cofactor;
+      Modular_Reducer m_mod_order;
+      EC_Point_Base_Point_Precompute m_base_mult;
+      OID m_oid;
+      size_t m_p_bits;
+      size_t m_order_bits;
+      size_t m_order_bytes;
+      bool m_a_is_minus_3;
+      bool m_a_is_zero;
+      bool m_has_cofactor;
+      EC_Group_Source m_source;
+};
+
+}  // namespace Botan
+
+#endif
diff --git a/src/lib/pubkey/ec_group/ec_scalar.cpp b/src/lib/pubkey/ec_group/ec_scalar.cpp
new file mode 100644
index 000000000..7021b76b6
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_scalar.cpp
@@ -0,0 +1,145 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/ec_scalar.h>
+
+#include <botan/ec_group.h>
+#include <botan/internal/ec_inner_data.h>
+
+namespace Botan {
+
+EC_Scalar EC_Scalar::_from_inner(std::unique_ptr<EC_Scalar_Data> inner) {
+   return EC_Scalar(std::move(inner));
+}
+
+EC_Scalar::EC_Scalar(std::unique_ptr<EC_Scalar_Data> scalar) : m_scalar(std::move(scalar)) {
+   BOTAN_ASSERT_NONNULL(m_scalar);
+}
+
+EC_Scalar::EC_Scalar(const EC_Scalar& other) : m_scalar(other.inner().clone()) {}
+
+EC_Scalar::EC_Scalar(EC_Scalar&& other) noexcept : m_scalar(std::move(other.m_scalar)) {}
+
+EC_Scalar& EC_Scalar::operator=(const EC_Scalar& other) {
+   if(this != &other) {
+      this->assign(other);
+   }
+   return (*this);
+}
+
+EC_Scalar& EC_Scalar::operator=(EC_Scalar&& other) noexcept {
+   BOTAN_ARG_CHECK(_inner().group() == other._inner().group(), "Curve mismatch");
+   std::swap(m_scalar, other.m_scalar);
+   return (*this);
+}
+
+EC_Scalar::~EC_Scalar() = default;
+
+size_t EC_Scalar::bytes() const {
+   return m_scalar->bytes();
+}
+
+EC_Scalar EC_Scalar::from_bytes_with_trunc(const EC_Group& group, std::span<const uint8_t> bytes) {
+   return EC_Scalar(group._data()->scalar_from_bytes_with_trunc(bytes));
+}
+
+EC_Scalar EC_Scalar::from_bytes_mod_order(const EC_Group& group, std::span<const uint8_t> bytes) {
+   return EC_Scalar(group._data()->scalar_from_bytes_mod_order(bytes));
+}
+
+EC_Scalar EC_Scalar::random(const EC_Group& group, RandomNumberGenerator& rng) {
+   return EC_Scalar(group._data()->scalar_random(rng));
+}
+
+EC_Scalar EC_Scalar::one(const EC_Group& group) {
+   return EC_Scalar(group._data()->scalar_one());
+}
+
+EC_Scalar EC_Scalar::from_bigint(const EC_Group& group, const BigInt& bn) {
+   BOTAN_ARG_CHECK(bn.is_positive() && bn <= group._data()->order(), "EC_Scalar::from_bigint out of range");
+   return EC_Scalar(group._data()->scalar_from_bigint(bn));
+}
+
+EC_Scalar EC_Scalar::gk_x_mod_order(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws) {
+   const auto& group = scalar._inner().group();
+   return EC_Scalar(group->gk_x_mod_order(scalar.inner(), rng, ws));
+}
+
+void EC_Scalar::serialize_to(std::span<uint8_t> bytes) const {
+   inner().serialize_to(bytes);
+}
+
+void EC_Scalar::serialize_pair_to(std::span<uint8_t> bytes, const EC_Scalar& r, const EC_Scalar& s) {
+   BOTAN_ARG_CHECK(r._inner().group() == s._inner().group(), "Curve mismatch");
+   const size_t scalar_bytes = r.bytes();
+   BOTAN_ARG_CHECK(bytes.size() == 2 * scalar_bytes, "Invalid output length");
+   r.serialize_to(bytes.first(scalar_bytes));
+   s.serialize_to(bytes.last(scalar_bytes));
+}
+
+std::optional<std::pair<EC_Scalar, EC_Scalar>> EC_Scalar::deserialize_pair(const EC_Group& group,
+                                                                           std::span<const uint8_t> bytes) {
+   if(bytes.size() % 2 != 0) {
+      return {};
+   }
+
+   const size_t half = bytes.size() / 2;
+
+   auto r = EC_Scalar::deserialize(group, bytes.first(half));
+   auto s = EC_Scalar::deserialize(group, bytes.last(half));
+
+   if(r && s) {
+      return std::make_pair(r.value(), s.value());
+   } else {
+      return {};
+   }
+}
+
+std::optional<EC_Scalar> EC_Scalar::deserialize(const EC_Group& group, std::span<const uint8_t> bytes) {
+   if(auto v = group._data()->scalar_deserialize(bytes)) {
+      return EC_Scalar(std::move(v));
+   } else {
+      return {};
+   }
+}
+
+bool EC_Scalar::is_zero() const {
+   return inner().is_zero();
+}
+
+EC_Scalar EC_Scalar::invert() const {
+   return EC_Scalar(inner().invert());
+}
+
+EC_Scalar EC_Scalar::negate() const {
+   return EC_Scalar(inner().negate());
+}
+
+void EC_Scalar::square_self() {
+   m_scalar->square_self();
+}
+
+EC_Scalar EC_Scalar::add(const EC_Scalar& x) const {
+   return EC_Scalar(inner().add(x.inner()));
+}
+
+EC_Scalar EC_Scalar::sub(const EC_Scalar& x) const {
+   return EC_Scalar(inner().sub(x.inner()));
+}
+
+EC_Scalar EC_Scalar::mul(const EC_Scalar& x) const {
+   return EC_Scalar(inner().mul(x.inner()));
+}
+
+void EC_Scalar::assign(const EC_Scalar& x) {
+   m_scalar->assign(x.inner());
+}
+
+bool EC_Scalar::is_eq(const EC_Scalar& x) const {
+   return inner().is_eq(x.inner());
+}
+
+}  // namespace Botan
diff --git a/src/lib/pubkey/ec_group/ec_scalar.h b/src/lib/pubkey/ec_group/ec_scalar.h
new file mode 100644
index 000000000..fc8e21b42
--- /dev/null
+++ b/src/lib/pubkey/ec_group/ec_scalar.h
@@ -0,0 +1,215 @@
+/*
+* (C) 2024 Jack Lloyd
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_EC_SCALAR_H_
+#define BOTAN_EC_SCALAR_H_
+
+#include <botan/concepts.h>
+#include <botan/types.h>
+
+#include <optional>
+#include <span>
+#include <vector>
+
+namespace Botan {
+
+class BigInt;
+class RandomNumberGenerator;
+class EC_Group;
+class EC_Group_Data;
+class EC_Scalar_Data;
+
+/**
+* Represents an integer modulo the prime group order of an elliptic curve
+*/
+class BOTAN_UNSTABLE_API EC_Scalar final {
+   public:
+      /**
+      * Deserialize a scalar
+      *
+      * The span must be exactly bytes() long; this function does not accept
+      * either short inputs (eg [1] to encode the integer 1) or inputs with
+      * excess leading zero bytes.
+      *
+      * Returns nullopt if the length is incorrect or if the integer is not
+      * within the range [0,p) where p is the group order.
+      */
+      static std::optional<EC_Scalar> deserialize(const EC_Group& group, std::span<const uint8_t> bytes);
+
+      /**
+      * Convert a bytestring to an EC_Scalar
+      *
+      * This uses the truncation rules from ECDSA
+      */
+      static EC_Scalar from_bytes_with_trunc(const EC_Group& group, std::span<const uint8_t> bytes);
+
+      /**
+      * Convert a bytestring to an EC_Scalar
+      *
+      * This reduces the bytes modulo the group order. The input can be at most
+      * 2*bytes() long
+      */
+      static EC_Scalar from_bytes_mod_order(const EC_Group& group, std::span<const uint8_t> bytes);
+
+      /**
+      * Deserialize a pair of scalars
+      *
+      * Returns nullopt if the length is not 2*bytes(), or if either scalar is
+      * out of range or zero
+      */
+      static std::optional<std::pair<EC_Scalar, EC_Scalar>> deserialize_pair(const EC_Group& group,
+                                                                             std::span<const uint8_t> bytes);
+
+      /**
+      * Return a new random scalar value
+      */
+      static EC_Scalar random(const EC_Group& group, RandomNumberGenerator& rng);
+
+      /**
+      * Return the scalar value 1
+      */
+      static EC_Scalar one(const EC_Group& group);
+
+      /**
+      * Convert from the argument BigInt to a EC_Scalar
+      *
+      * Throws an exception if the provided bn is negative or too large
+      */
+      static EC_Scalar from_bigint(const EC_Group& group, const BigInt& bn);
+
+      /**
+      * Compute the elliptic curve scalar multiplication (g*k) where g is the
+      * standard base point on the curve. Then extract the x coordinate of
+      * the resulting point, and reduce it modulo the group order.
+      *
+      * Workspace argument is transitional
+      */
+      static EC_Scalar gk_x_mod_order(const EC_Scalar& scalar, RandomNumberGenerator& rng, std::vector<BigInt>& ws);
+
+      /**
+      * Return the byte size of this scalar
+      */
+      size_t bytes() const;
+
+      /**
+      * Write the fixed length serialization to bytes
+      *
+      * The provided span must be exactly bytes() long
+      */
+      void serialize_to(std::span<uint8_t> bytes) const;
+
+      /**
+      * Return the bytes of the encoded scalar in a container
+      */
+      template <concepts::resizable_byte_buffer T = std::vector<uint8_t>>
+      T serialize() const {
+         T s(this->bytes());
+         this->serialize_to(s);
+         return s;
+      }
+
+      /**
+      * Write the fixed length serialization to bytes
+      *
+      * The provided span must be exactly bytes() long
+      */
+      static void serialize_pair_to(std::span<uint8_t> bytes, const EC_Scalar& r, const EC_Scalar& s);
+
+      /**
+      * Return the bytes of the encoded scalar in a container
+      */
+      template <concepts::resizable_byte_buffer T = std::vector<uint8_t>>
+      static T serialize_pair(const EC_Scalar& r, const EC_Scalar& s) {
+         T bytes(r.bytes() + s.bytes());
+         serialize_pair_to(bytes, r, s);
+         return bytes;
+      }
+
+      /**
+      * Return true if this EC_Scalar is zero
+      */
+      bool is_zero() const;
+
+      /**
+      * Return true if this EC_Scalar is not zero
+      */
+      bool is_nonzero() const { return !is_zero(); }
+
+      /**
+      * Return the modular inverse of this EC_Scalar
+      *
+      * If the EC_Scalar is zero, then invert() returns zero also
+      * Since the EC_Scalar
+      */
+      EC_Scalar invert() const;
+
+      /**
+      */
+      EC_Scalar negate() const;
+
+      /**
+      * Scalar addition (modulo p)
+      */
+      EC_Scalar add(const EC_Scalar& x) const;
+
+      /**
+      * Scalar subtraction (modulo p)
+      */
+      EC_Scalar sub(const EC_Scalar& x) const;
+
+      /**
+      * Scalar multiplication (modulo p)
+      */
+      EC_Scalar mul(const EC_Scalar& x) const;
+
+      /**
+      * Assign a scalar
+      */
+      void assign(const EC_Scalar& x);
+
+      /**
+      * Set *this to its own square modulo p
+      */
+      void square_self();
+
+      /**
+      * Test for equality
+      */
+      bool is_eq(const EC_Scalar& x) const;
+
+      friend EC_Scalar operator+(const EC_Scalar& x, const EC_Scalar& y) { return x.add(y); }
+
+      friend EC_Scalar operator-(const EC_Scalar& x, const EC_Scalar& y) { return x.sub(y); }
+
+      friend EC_Scalar operator*(const EC_Scalar& x, const EC_Scalar& y) { return x.mul(y); }
+
+      friend bool operator==(const EC_Scalar& x, const EC_Scalar& y) { return x.is_eq(y); }
+
+      EC_Scalar(const EC_Scalar& other);
+      EC_Scalar(EC_Scalar&& other) noexcept;
+
+      EC_Scalar& operator=(const EC_Scalar& other);
+      EC_Scalar& operator=(EC_Scalar&& other) noexcept;
+
+      ~EC_Scalar();
+
+      const EC_Scalar_Data& _inner() const { return inner(); }
+
+      static EC_Scalar _from_inner(std::unique_ptr<EC_Scalar_Data> inner);
+
+   private:
+      friend class EC_AffinePoint;
+
+      EC_Scalar(std::unique_ptr<EC_Scalar_Data> scalar);
+
+      const EC_Scalar_Data& inner() const { return *m_scalar; }
+
+      std::unique_ptr<EC_Scalar_Data> m_scalar;
+};
+
+}  // namespace Botan
+
+#endif
diff --git a/src/lib/pubkey/ec_group/info.txt b/src/lib/pubkey/ec_group/info.txt
index 6fd382599..d918f52f0 100644
--- a/src/lib/pubkey/ec_group/info.txt
+++ b/src/lib/pubkey/ec_group/info.txt
@@ -16,10 +16,14 @@ pem
 
 <header:internal>
 point_mul.h
+ec_inner_bn.h
+ec_inner_data.h
 </header:internal>
 
 <header:public>
 curve_gfp.h
 ec_group.h
 ec_point.h
+ec_apoint.h
+ec_scalar.h
 </header:public>
diff --git a/src/lib/pubkey/ec_group/point_mul.cpp b/src/lib/pubkey/ec_group/point_mul.cpp
index 6e996a339..b600a2fe5 100644
--- a/src/lib/pubkey/ec_group/point_mul.cpp
+++ b/src/lib/pubkey/ec_group/point_mul.cpp
@@ -158,14 +158,17 @@ EC_Point EC_Point_Base_Point_Precompute::mul(const BigInt& k,
    return R;
 }
 
-EC_Point_Var_Point_Precompute::EC_Point_Var_Point_Precompute(const EC_Point& point,
+EC_Point_Var_Point_Precompute::EC_Point_Var_Point_Precompute(const EC_Point& ipoint,
                                                              RandomNumberGenerator& rng,
                                                              std::vector<BigInt>& ws) :
-      m_curve(point.get_curve()), m_p_words(m_curve.get_p_words()), m_window_bits(4) {
+      m_curve(ipoint.get_curve()), m_p_words(m_curve.get_p_words()), m_window_bits(4) {
    if(ws.size() < EC_Point::WORKSPACE_SIZE) {
       ws.resize(EC_Point::WORKSPACE_SIZE);
    }
 
+   auto point = ipoint;
+   point.randomize_repr(rng);
+
    std::vector<EC_Point> U(static_cast<size_t>(1) << m_window_bits);
    U[0] = point.zero();
    U[1] = point;
@@ -259,7 +262,7 @@ EC_Point EC_Point_Var_Point_Precompute::mul(const BigInt& k,
       windows--;
    }
 
-   BOTAN_DEBUG_ASSERT(R.on_the_curve());
+   BOTAN_ASSERT_NOMSG(R.on_the_curve());
 
    return R;
 }
diff --git a/src/lib/pubkey/ec_h2c/ec_h2c.cpp b/src/lib/pubkey/ec_h2c/ec_h2c.cpp
index 0a7018eca..e3a68a903 100644
--- a/src/lib/pubkey/ec_h2c/ec_h2c.cpp
+++ b/src/lib/pubkey/ec_h2c/ec_h2c.cpp
@@ -7,6 +7,7 @@
 #include <botan/internal/ec_h2c.h>
 
 #include <botan/ec_group.h>
+#include <botan/internal/ec_inner_data.h>
 #include <botan/internal/pcurves.h>
 
 namespace Botan {
@@ -16,10 +17,18 @@ EC_Point hash_to_curve_sswu(const EC_Group& group,
                             std::span<const uint8_t> input,
                             std::span<const uint8_t> domain_sep,
                             bool random_oracle) {
-   if(auto group_id = PCurve::PrimeOrderCurveId::from_oid(group.get_curve_oid())) {
+   return group.OS2ECP(hash_to_curve_sswu(*group._data(), hash_fn, input, domain_sep, random_oracle));
+}
+
+std::vector<uint8_t> hash_to_curve_sswu(const EC_Group_Data& group,
+                                        std::string_view hash_fn,
+                                        std::span<const uint8_t> input,
+                                        std::span<const uint8_t> domain_sep,
+                                        bool random_oracle) {
+   if(auto group_id = PCurve::PrimeOrderCurveId::from_oid(group.oid())) {
       if(auto curve = PCurve::PrimeOrderCurve::from_id(*group_id)) {
          const auto pt = curve->hash_to_curve(hash_fn, input, domain_sep, random_oracle);
-         return group.OS2ECP(pt.to_affine().serialize());
+         return pt.to_affine().serialize();
       }
    }
 
diff --git a/src/lib/pubkey/ec_h2c/ec_h2c.h b/src/lib/pubkey/ec_h2c/ec_h2c.h
index bcdf1fc9d..0d6a09c70 100644
--- a/src/lib/pubkey/ec_h2c/ec_h2c.h
+++ b/src/lib/pubkey/ec_h2c/ec_h2c.h
@@ -15,6 +15,7 @@
 namespace Botan {
 
 class EC_Group;
+class EC_Group_Data;
 
 /**
 * Hash an input onto an elliptic curve point using the
@@ -29,6 +30,19 @@ EC_Point hash_to_curve_sswu(const EC_Group& group,
                             std::span<const uint8_t> domain_sep,
                             bool random_oracle);
 
+/**
+* Hash an input onto an elliptic curve point using the
+* methods from RFC 9380
+*
+* This method requires that the ECC group have (a*b) != 0
+* which excludes certain groups including secp256k1
+*/
+std::vector<uint8_t> hash_to_curve_sswu(const EC_Group_Data& group_data,
+                                        std::string_view hash_fn,
+                                        std::span<const uint8_t> input,
+                                        std::span<const uint8_t> domain_sep,
+                                        bool random_oracle);
+
 }  // namespace Botan
 
 #endif
diff --git a/src/lib/pubkey/ecdh/ecdh.cpp b/src/lib/pubkey/ecdh/ecdh.cpp
index cd66c9bd6..dcdada0bf 100644
--- a/src/lib/pubkey/ecdh/ecdh.cpp
+++ b/src/lib/pubkey/ecdh/ecdh.cpp
@@ -26,28 +26,44 @@ namespace {
 class ECDH_KA_Operation final : public PK_Ops::Key_Agreement_with_KDF {
    public:
       ECDH_KA_Operation(const ECDH_PrivateKey& key, std::string_view kdf, RandomNumberGenerator& rng) :
-            PK_Ops::Key_Agreement_with_KDF(kdf), m_group(key.domain()), m_rng(rng) {
-         m_l_times_priv = m_group.inverse_mod_order(m_group.get_cofactor()) * key.private_value();
-      }
+            PK_Ops::Key_Agreement_with_KDF(kdf),
+            m_group(key.domain()),
+            m_l_times_priv(mul_cofactor_inv(m_group, key.private_value())),
+            m_rng(rng) {}
 
       size_t agreed_value_size() const override { return m_group.get_p_bytes(); }
 
       secure_vector<uint8_t> raw_agree(const uint8_t w[], size_t w_len) override {
-         EC_Point input_point = m_group.get_cofactor() * m_group.OS2ECP(w, w_len);
-         input_point.randomize_repr(m_rng);
-
-         const EC_Point S = m_group.blinded_var_point_multiply(input_point, m_l_times_priv, m_rng, m_ws);
-
-         if(S.on_the_curve() == false) {
-            throw Internal_Error("ECDH agreed value was not on the curve");
+         if(m_group.has_cofactor()) {
+            EC_AffinePoint input_point(m_group, m_group.get_cofactor() * m_group.OS2ECP(w, w_len));
+            return input_point.mul(m_l_times_priv, m_rng, m_ws).x_bytes();
+         } else {
+            if(auto input_point = EC_AffinePoint::deserialize(m_group, {w, w_len})) {
+               return input_point->mul(m_l_times_priv, m_rng, m_ws).x_bytes();
+            } else {
+               throw Decoding_Error("ECDH - Invalid elliptic curve point");
+            }
          }
-
-         return S.x_bytes();
       }
 
    private:
+      static EC_Scalar mul_cofactor_inv(const EC_Group& group, const BigInt& bn) {
+         // We implement BSI TR-03111 ECKAEG which only matters in the (rare/deprecated)
+         // case of a curve with cofactor.
+
+         auto private_key = EC_Scalar::from_bigint(group, bn);
+
+         if(group.has_cofactor()) {
+            // We could precompute this but cofactors are rare
+            auto l = group.inverse_mod_order(group.get_cofactor());
+            return private_key * EC_Scalar::from_bigint(group, l);
+         } else {
+            return private_key;
+         }
+      }
+
       const EC_Group m_group;
-      BigInt m_l_times_priv;
+      const EC_Scalar m_l_times_priv;
       RandomNumberGenerator& m_rng;
       std::vector<BigInt> m_ws;
 };
diff --git a/src/lib/pubkey/ecdsa/ecdsa.cpp b/src/lib/pubkey/ecdsa/ecdsa.cpp
index d02e9262e..d990552e1 100644
--- a/src/lib/pubkey/ecdsa/ecdsa.cpp
+++ b/src/lib/pubkey/ecdsa/ecdsa.cpp
@@ -2,7 +2,7 @@
 * ECDSA implemenation
 * (C) 2007 Manuel Hartl, FlexSecure GmbH
 *     2007 Falko Strenzke, FlexSecure GmbH
-*     2008-2010,2015,2016,2018 Jack Lloyd
+*     2008-2010,2015,2016,2018,2024 Jack Lloyd
 *     2016 René Korthaus
 *
 * Botan is released under the Simplified BSD License (see license.txt)
@@ -10,10 +10,8 @@
 
 #include <botan/ecdsa.h>
 
-#include <botan/reducer.h>
 #include <botan/internal/keypair.h>
 #include <botan/internal/pk_ops_impl.h>
-#include <botan/internal/point_mul.h>
 
 #if defined(BOTAN_HAS_RFC6979_GENERATOR)
    #include <botan/internal/rfc6979.h>
@@ -25,7 +23,7 @@ namespace {
 
 EC_Point recover_ecdsa_public_key(
    const EC_Group& group, const std::vector<uint8_t>& msg, const BigInt& r, const BigInt& s, uint8_t v) {
-   if(group.get_cofactor() != 1) {
+   if(group.has_cofactor()) {
       throw Invalid_Argument("ECDSA public key recovery only supported for prime order groups");
    }
 
@@ -44,9 +42,6 @@ EC_Point recover_ecdsa_public_key(
    const size_t p_bytes = group.get_p_bytes();
 
    try {
-      const BigInt e = BigInt::from_bytes_with_max_bits(msg.data(), msg.size(), group.get_order_bits());
-      const BigInt r_inv = group.inverse_mod_order(r);
-
       BigInt x = r + add_order * group_order;
 
       std::vector<uint8_t> X(p_bytes + 1);
@@ -54,16 +49,18 @@ EC_Point recover_ecdsa_public_key(
       X[0] = 0x02 | y_odd;
       x.serialize_to(std::span{X}.subspan(1));
 
-      const EC_Point R = group.OS2ECP(X);
+      if(auto R = EC_AffinePoint::deserialize(group, X)) {
+         // Compute r_inv * (-eG + s*R)
+         const auto ne = EC_Scalar::from_bytes_with_trunc(group, msg).negate();
+         const auto ss = EC_Scalar::from_bigint(group, s);
 
-      if((R * group_order).is_zero() == false) {
-         throw Decoding_Error("Unable to recover ECDSA public key");
-      }
+         const auto r_inv = EC_Scalar::from_bigint(group, r).invert();
 
-      // Compute r_inv * (s*R - eG)
-      EC_Point_Multi_Point_Precompute RG_mul(R, group.get_base_point());
-      const BigInt ne = group.mod_order(group_order - e);
-      return r_inv * RG_mul.multi_exp(s, ne);
+         EC_Group::Mul2Table GR_mul(R.value());
+         if(auto egsr = GR_mul.mul2_vartime(ne * r_inv, ss * r_inv)) {
+            return egsr->to_legacy_point();
+         }
+      }
    } catch(...) {
       // continue on and throw
    }
@@ -121,13 +118,15 @@ namespace {
 class ECDSA_Signature_Operation final : public PK_Ops::Signature_with_Hash {
    public:
       ECDSA_Signature_Operation(const ECDSA_PrivateKey& ecdsa, std::string_view padding, RandomNumberGenerator& rng) :
-            PK_Ops::Signature_with_Hash(padding), m_group(ecdsa.domain()), m_x(ecdsa.private_value()) {
+            PK_Ops::Signature_with_Hash(padding),
+            m_group(ecdsa.domain()),
+            m_x(EC_Scalar::from_bigint(m_group, ecdsa.private_value())),
+            m_b(EC_Scalar::random(m_group, rng)),
+            m_b_inv(m_b.invert()) {
 #if defined(BOTAN_HAS_RFC6979_GENERATOR)
-         m_rfc6979 = std::make_unique<RFC6979_Nonce_Generator>(this->rfc6979_hash_function(), m_group.get_order(), m_x);
+         m_rfc6979 = std::make_unique<RFC6979_Nonce_Generator>(
+            this->rfc6979_hash_function(), m_group.get_order(), ecdsa.private_value());
 #endif
-
-         m_b = m_group.random_scalar(rng);
-         m_b_inv = m_group.inverse_mod_order(m_b);
       }
 
       size_t signature_length() const override { return 2 * m_group.get_order_bytes(); }
@@ -138,7 +137,7 @@ class ECDSA_Signature_Operation final : public PK_Ops::Signature_with_Hash {
 
    private:
       const EC_Group m_group;
-      const BigInt m_x;
+      const EC_Scalar m_x;
 
 #if defined(BOTAN_HAS_RFC6979_GENERATOR)
       std::unique_ptr<RFC6979_Nonce_Generator> m_rfc6979;
@@ -146,7 +145,8 @@ class ECDSA_Signature_Operation final : public PK_Ops::Signature_with_Hash {
 
       std::vector<BigInt> m_ws;
 
-      BigInt m_b, m_b_inv;
+      EC_Scalar m_b;
+      EC_Scalar m_b_inv;
 };
 
 AlgorithmIdentifier ECDSA_Signature_Operation::algorithm_identifier() const {
@@ -158,35 +158,34 @@ AlgorithmIdentifier ECDSA_Signature_Operation::algorithm_identifier() const {
 secure_vector<uint8_t> ECDSA_Signature_Operation::raw_sign(const uint8_t msg[],
                                                            size_t msg_len,
                                                            RandomNumberGenerator& rng) {
-   BigInt m = m_group.mod_order(BigInt::from_bytes_with_max_bits(msg, msg_len, m_group.get_order_bits()));
+   const auto m = EC_Scalar::from_bytes_with_trunc(m_group, std::span{msg, msg_len});
 
 #if defined(BOTAN_HAS_RFC6979_GENERATOR)
-   const BigInt k = m_rfc6979->nonce_for(m);
+   const auto k = m_rfc6979->nonce_for(m_group, m);
 #else
-   const BigInt k = m_group.random_scalar(rng);
+   const auto k = EC_Scalar::random(m_group, rng);
 #endif
 
-   const BigInt r = m_group.mod_order(m_group.blinded_base_point_multiply_x(k, rng, m_ws));
+   const auto r = EC_Scalar::gk_x_mod_order(k, rng, m_ws);
 
-   const BigInt k_inv = m_group.inverse_mod_order(k);
+   const auto k_inv = k.invert();
 
    /*
    * Blind the input message and compute x*r+m as (x*r*b + m*b)/b
    */
-   m_b = m_group.square_mod_order(m_b);
-   m_b_inv = m_group.square_mod_order(m_b_inv);
+   m_b.square_self();
+   m_b_inv.square_self();
 
-   m = m_group.multiply_mod_order(m_b, m_group.mod_order(m));
-   const BigInt xr_m = m_group.mod_order(m_group.multiply_mod_order(m_x, m_b, r) + m);
+   const auto xr_m = ((m_x * m_b) * r) + (m * m_b);
 
-   const BigInt s = m_group.multiply_mod_order(k_inv, xr_m, m_b_inv);
+   const auto s = (k_inv * xr_m) * m_b_inv;
 
    // With overwhelming probability, a bug rather than actual zero r/s
    if(r.is_zero() || s.is_zero()) {
       throw Internal_Error("During ECDSA signature generated zero r/s");
    }
 
-   return BigInt::encode_fixed_length_int_pair(r, s, m_group.get_order_bytes());
+   return EC_Scalar::serialize_pair<secure_vector<uint8_t>>(r, s);
 }
 
 /**
@@ -195,53 +194,36 @@ secure_vector<uint8_t> ECDSA_Signature_Operation::raw_sign(const uint8_t msg[],
 class ECDSA_Verification_Operation final : public PK_Ops::Verification_with_Hash {
    public:
       ECDSA_Verification_Operation(const ECDSA_PublicKey& ecdsa, std::string_view padding) :
-            PK_Ops::Verification_with_Hash(padding),
-            m_group(ecdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), ecdsa.public_point()) {}
+            PK_Ops::Verification_with_Hash(padding), m_group(ecdsa.domain()), m_gy_mul(m_group, ecdsa.public_point()) {}
 
       ECDSA_Verification_Operation(const ECDSA_PublicKey& ecdsa, const AlgorithmIdentifier& alg_id) :
             PK_Ops::Verification_with_Hash(alg_id, "ECDSA", true),
             m_group(ecdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), ecdsa.public_point()) {}
+            m_gy_mul(m_group, ecdsa.public_point()) {}
 
       bool verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len) override;
 
    private:
       const EC_Group m_group;
-      const EC_Point_Multi_Point_Precompute m_gy_mul;
+      const EC_Group::Mul2Table m_gy_mul;
 };
 
 bool ECDSA_Verification_Operation::verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len) {
-   if(sig_len != m_group.get_order_bytes() * 2) {
-      return false;
-   }
+   if(auto rs = EC_Scalar::deserialize_pair(m_group, std::span{sig, sig_len})) {
+      const auto& [r, s] = rs.value();
 
-   const BigInt e = BigInt::from_bytes_with_max_bits(msg, msg_len, m_group.get_order_bits());
+      if(r.is_nonzero() && s.is_nonzero()) {
+         const auto m = EC_Scalar::from_bytes_with_trunc(m_group, std::span{msg, msg_len});
 
-   const BigInt r(sig, sig_len / 2);
-   const BigInt s(sig + sig_len / 2, sig_len / 2);
+         const auto w = s.invert();
 
-   // Cannot be negative here since we just decoded from binary
-   if(r.is_zero() || s.is_zero()) {
-      return false;
-   }
-
-   if(r >= m_group.get_order() || s >= m_group.get_order()) {
-      return false;
-   }
-
-   const BigInt w = m_group.inverse_mod_order(s);
-
-   const BigInt u1 = m_group.multiply_mod_order(m_group.mod_order(e), w);
-   const BigInt u2 = m_group.multiply_mod_order(r, w);
-   const EC_Point R = m_gy_mul.multi_exp(u1, u2);
-
-   if(R.is_zero()) {
-      return false;
+         if(const auto v = m_gy_mul.mul2_vartime_x_mod_order(w, m, r)) {
+            return (v == r);
+         }
+      }
    }
 
-   const BigInt v = m_group.mod_order(R.get_affine_x());
-   return (v == r);
+   return false;
 }
 
 }  // namespace
diff --git a/src/lib/pubkey/ecgdsa/ecgdsa.cpp b/src/lib/pubkey/ecgdsa/ecgdsa.cpp
index cb97c405c..502e4d30b 100644
--- a/src/lib/pubkey/ecgdsa/ecgdsa.cpp
+++ b/src/lib/pubkey/ecgdsa/ecgdsa.cpp
@@ -1,17 +1,15 @@
 /*
 * ECGDSA (BSI-TR-03111, version 2.0)
 * (C) 2016 René Korthaus
-* (C) 2018 Jack Lloyd
+* (C) 2018,2024 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
 
 #include <botan/ecgdsa.h>
 
-#include <botan/reducer.h>
 #include <botan/internal/keypair.h>
 #include <botan/internal/pk_ops_impl.h>
-#include <botan/internal/point_mul.h>
 
 namespace Botan {
 
@@ -39,7 +37,9 @@ namespace {
 class ECGDSA_Signature_Operation final : public PK_Ops::Signature_with_Hash {
    public:
       ECGDSA_Signature_Operation(const ECGDSA_PrivateKey& ecgdsa, std::string_view emsa) :
-            PK_Ops::Signature_with_Hash(emsa), m_group(ecgdsa.domain()), m_x(ecgdsa.private_value()) {}
+            PK_Ops::Signature_with_Hash(emsa),
+            m_group(ecgdsa.domain()),
+            m_x(EC_Scalar::from_bigint(m_group, ecgdsa.private_value())) {}
 
       secure_vector<uint8_t> raw_sign(const uint8_t msg[], size_t msg_len, RandomNumberGenerator& rng) override;
 
@@ -49,7 +49,7 @@ class ECGDSA_Signature_Operation final : public PK_Ops::Signature_with_Hash {
 
    private:
       const EC_Group m_group;
-      const BigInt m_x;
+      const EC_Scalar m_x;
       std::vector<BigInt> m_ws;
 };
 
@@ -62,22 +62,20 @@ AlgorithmIdentifier ECGDSA_Signature_Operation::algorithm_identifier() const {
 secure_vector<uint8_t> ECGDSA_Signature_Operation::raw_sign(const uint8_t msg[],
                                                             size_t msg_len,
                                                             RandomNumberGenerator& rng) {
-   const BigInt m = BigInt::from_bytes_with_max_bits(msg, msg_len, m_group.get_order_bits());
-
-   const BigInt k = m_group.random_scalar(rng);
+   const auto m = EC_Scalar::from_bytes_with_trunc(m_group, std::span{msg, msg_len});
 
-   const BigInt r = m_group.mod_order(m_group.blinded_base_point_multiply_x(k, rng, m_ws));
+   const auto k = EC_Scalar::random(m_group, rng);
 
-   const BigInt kr = m_group.multiply_mod_order(k, r);
+   const auto r = EC_Scalar::gk_x_mod_order(k, rng, m_ws);
 
-   const BigInt s = m_group.multiply_mod_order(m_x, kr - m);
+   const auto s = m_x * ((k * r) - m);
 
    // With overwhelming probability, a bug rather than actual zero r/s
    if(r.is_zero() || s.is_zero()) {
       throw Internal_Error("During ECGDSA signature generated zero r/s");
    }
 
-   return BigInt::encode_fixed_length_int_pair(r, s, m_group.get_order_bytes());
+   return EC_Scalar::serialize_pair<secure_vector<uint8_t>>(r, s);
 }
 
 /**
@@ -88,46 +86,36 @@ class ECGDSA_Verification_Operation final : public PK_Ops::Verification_with_Has
       ECGDSA_Verification_Operation(const ECGDSA_PublicKey& ecgdsa, std::string_view padding) :
             PK_Ops::Verification_with_Hash(padding),
             m_group(ecgdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), ecgdsa.public_point()) {}
+            m_gy_mul(m_group, ecgdsa.public_point()) {}
 
       ECGDSA_Verification_Operation(const ECGDSA_PublicKey& ecgdsa, const AlgorithmIdentifier& alg_id) :
             PK_Ops::Verification_with_Hash(alg_id, "ECGDSA"),
             m_group(ecgdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), ecgdsa.public_point()) {}
+            m_gy_mul(m_group, ecgdsa.public_point()) {}
 
       bool verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len) override;
 
    private:
       const EC_Group m_group;
-      const EC_Point_Multi_Point_Precompute m_gy_mul;
+      const EC_Group::Mul2Table m_gy_mul;
 };
 
 bool ECGDSA_Verification_Operation::verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len) {
-   if(sig_len != m_group.get_order_bytes() * 2) {
-      return false;
-   }
-
-   const BigInt e = BigInt::from_bytes_with_max_bits(msg, msg_len, m_group.get_order_bits());
+   if(auto rs = EC_Scalar::deserialize_pair(m_group, std::span{sig, sig_len})) {
+      const auto& [r, s] = rs.value();
 
-   const BigInt r(sig, sig_len / 2);
-   const BigInt s(sig + sig_len / 2, sig_len / 2);
+      if(r.is_nonzero() && s.is_nonzero()) {
+         const auto m = EC_Scalar::from_bytes_with_trunc(m_group, std::span{msg, msg_len});
 
-   if(r <= 0 || r >= m_group.get_order() || s <= 0 || s >= m_group.get_order()) {
-      return false;
-   }
-
-   const BigInt w = m_group.inverse_mod_order(r);
+         const auto w = r.invert();
 
-   const BigInt u1 = m_group.multiply_mod_order(e, w);
-   const BigInt u2 = m_group.multiply_mod_order(s, w);
-   const EC_Point R = m_gy_mul.multi_exp(u1, u2);
-
-   if(R.is_zero()) {
-      return false;
+         if(const auto v = m_gy_mul.mul2_vartime_x_mod_order(w, m, s)) {
+            return (v == r);
+         }
+      }
    }
 
-   const BigInt v = m_group.mod_order(R.get_affine_x());
-   return (v == r);
+   return false;
 }
 
 }  // namespace
diff --git a/src/lib/pubkey/ecies/ecies.cpp b/src/lib/pubkey/ecies/ecies.cpp
index 78785ef18..681060ed2 100644
--- a/src/lib/pubkey/ecies/ecies.cpp
+++ b/src/lib/pubkey/ecies/ecies.cpp
@@ -66,17 +66,12 @@ class ECIES_ECDH_KA_Operation final : public PK_Ops::Key_Agreement_with_KDF {
 
       secure_vector<uint8_t> raw_agree(const uint8_t w[], size_t w_len) override {
          const EC_Group& group = m_key.domain();
-
-         EC_Point input_point = group.OS2ECP(w, w_len);
-         input_point.randomize_repr(m_rng);
-
-         const EC_Point S = group.blinded_var_point_multiply(input_point, m_key.private_value(), m_rng, m_ws);
-
-         if(S.on_the_curve() == false) {
-            throw Internal_Error("ECDH agreed value was not on the curve");
+         const auto x = EC_Scalar::from_bigint(group, m_key.private_value());
+         if(auto input_point = EC_AffinePoint::deserialize(group, {w, w_len})) {
+            return input_point->mul(x, m_rng, m_ws).x_bytes();
+         } else {
+            throw Decoding_Error("ECIES - Invalid elliptic curve point");
          }
-
-         return S.x_bytes();
       }
 
    private:
diff --git a/src/lib/pubkey/eckcdsa/eckcdsa.cpp b/src/lib/pubkey/eckcdsa/eckcdsa.cpp
index fef3f8009..728d0d4be 100644
--- a/src/lib/pubkey/eckcdsa/eckcdsa.cpp
+++ b/src/lib/pubkey/eckcdsa/eckcdsa.cpp
@@ -1,7 +1,7 @@
 /*
 * ECKCDSA (ISO/IEC 14888-3:2006/Cor.2:2009)
 * (C) 2016 René Korthaus, Sirrix AG
-* (C) 2018 Jack Lloyd
+* (C) 2018,2024 Jack Lloyd
 * (C) 2023 Philippe Lieser - Rohde & Schwarz Cybersecurity
 *
 * Botan is released under the Simplified BSD License (see license.txt)
@@ -10,13 +10,11 @@
 #include <botan/eckcdsa.h>
 
 #include <botan/hash.h>
-#include <botan/reducer.h>
 #include <botan/rng.h>
 #include <botan/internal/fmt.h>
 #include <botan/internal/keypair.h>
 #include <botan/internal/parsing.h>
 #include <botan/internal/pk_ops_impl.h>
-#include <botan/internal/point_mul.h>
 #include <botan/internal/scan_name.h>
 #include <botan/internal/stl_util.h>
 
@@ -119,7 +117,7 @@ class ECKCDSA_Signature_Operation final : public PK_Ops::Signature {
    public:
       ECKCDSA_Signature_Operation(const ECKCDSA_PrivateKey& eckcdsa, std::string_view padding) :
             m_group(eckcdsa.domain()),
-            m_x(eckcdsa.private_value()),
+            m_x(EC_Scalar::from_bigint(m_group, eckcdsa.private_value())),
             m_hash(eckcdsa_signature_hash(padding)),
             m_prefix_used(false) {
          m_prefix = eckcdsa_prefix(eckcdsa.public_point(), m_hash->hash_block_size());
@@ -150,7 +148,7 @@ class ECKCDSA_Signature_Operation final : public PK_Ops::Signature {
       secure_vector<uint8_t> raw_sign(const uint8_t msg[], size_t msg_len, RandomNumberGenerator& rng);
 
       const EC_Group m_group;
-      const BigInt m_x;
+      const EC_Scalar m_x;
       std::unique_ptr<HashFunction> m_hash;
       std::vector<uint8_t> m_prefix;
       std::vector<BigInt> m_ws;
@@ -166,26 +164,24 @@ AlgorithmIdentifier ECKCDSA_Signature_Operation::algorithm_identifier() const {
 secure_vector<uint8_t> ECKCDSA_Signature_Operation::raw_sign(const uint8_t msg[],
                                                              size_t msg_len,
                                                              RandomNumberGenerator& rng) {
-   const BigInt k = m_group.random_scalar(rng);
-   const BigInt k_times_P_x = m_group.blinded_base_point_multiply_x(k, rng, m_ws);
+   const auto k = EC_Scalar::random(m_group, rng);
 
-   auto hash = m_hash->new_object();
-   hash->update(k_times_P_x.serialize(m_group.get_order_bytes()));
-   secure_vector<uint8_t> c = hash->final();
+   m_hash->update(EC_AffinePoint::g_mul(k, rng, m_ws).x_bytes());
+   secure_vector<uint8_t> c = m_hash->final();
    truncate_hash_if_needed(c, m_group.get_order_bytes());
 
    const auto r = c;
 
    BOTAN_ASSERT_NOMSG(msg_len == c.size());
    xor_buf(c, msg, c.size());
-   const BigInt w = m_group.mod_order(BigInt::from_bytes(c));
+   const auto w = EC_Scalar::from_bytes_mod_order(m_group, c);
 
-   const BigInt s = m_group.multiply_mod_order(m_x, k - w);
+   const auto s = m_x * (k - w);
    if(s.is_zero()) {
       throw Internal_Error("During ECKCDSA signature generation created zero s");
    }
 
-   return concat(r, s.serialize(m_group.get_order_bytes()));
+   return concat(r, s.serialize());
 }
 
 /**
@@ -195,7 +191,7 @@ class ECKCDSA_Verification_Operation final : public PK_Ops::Verification {
    public:
       ECKCDSA_Verification_Operation(const ECKCDSA_PublicKey& eckcdsa, std::string_view padding) :
             m_group(eckcdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), eckcdsa.public_point()),
+            m_gy_mul(m_group, eckcdsa.public_point()),
             m_hash(eckcdsa_signature_hash(padding)),
             m_prefix_used(false) {
          m_prefix = eckcdsa_prefix(eckcdsa.public_point(), m_hash->hash_block_size());
@@ -203,7 +199,7 @@ class ECKCDSA_Verification_Operation final : public PK_Ops::Verification {
 
       ECKCDSA_Verification_Operation(const ECKCDSA_PublicKey& eckcdsa, const AlgorithmIdentifier& alg_id) :
             m_group(eckcdsa.domain()),
-            m_gy_mul(m_group.get_base_point(), eckcdsa.public_point()),
+            m_gy_mul(m_group, eckcdsa.public_point()),
             m_hash(eckcdsa_signature_hash(alg_id)),
             m_prefix_used(false) {
          m_prefix = eckcdsa_prefix(eckcdsa.public_point(), m_hash->hash_block_size());
@@ -219,7 +215,7 @@ class ECKCDSA_Verification_Operation final : public PK_Ops::Verification {
       bool verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len);
 
       const EC_Group m_group;
-      const EC_Point_Multi_Point_Precompute m_gy_mul;
+      const EC_Group::Mul2Table m_gy_mul;
       std::vector<uint8_t> m_prefix;
       std::unique_ptr<HashFunction> m_hash;
       bool m_prefix_used;
@@ -252,29 +248,21 @@ bool ECKCDSA_Verification_Operation::verify(const uint8_t msg[], size_t msg_len,
 
    secure_vector<uint8_t> r(sig, sig + size_r);
 
-   // check that 0 < s < q
-   const BigInt s(sig + size_r, order_bytes);
+   if(auto s = EC_Scalar::deserialize(m_group, std::span{sig, sig_len}.last(order_bytes))) {
+      secure_vector<uint8_t> r_xor_e(r);
+      xor_buf(r_xor_e, msg, r.size());
 
-   if(s <= 0 || s >= m_group.get_order()) {
-      return false;
-   }
-
-   secure_vector<uint8_t> r_xor_e(r);
-   xor_buf(r_xor_e, msg, r.size());
+      const auto w = EC_Scalar::from_bytes_mod_order(m_group, r_xor_e);
 
-   const BigInt w = m_group.mod_order(BigInt::from_bytes(r_xor_e));
+      if(auto q = m_gy_mul.mul2_vartime(w, s.value())) {
+         secure_vector<uint8_t> v = m_hash->process(q->x_bytes());
+         truncate_hash_if_needed(v, m_group.get_order_bytes());
 
-   const EC_Point q = m_gy_mul.multi_exp(w, s);
-   if(q.is_zero()) {
-      return false;
+         return (v == r);
+      }
    }
 
-   auto c_hash = m_hash->new_object();
-   c_hash->update(q.x_bytes());
-   secure_vector<uint8_t> v = c_hash->final();
-   truncate_hash_if_needed(v, m_group.get_order_bytes());
-
-   return (v == r);
+   return false;
 }
 
 }  // namespace
diff --git a/src/lib/pubkey/gost_3410/gost_3410.cpp b/src/lib/pubkey/gost_3410/gost_3410.cpp
index 99439fd89..f8740fc4c 100644
--- a/src/lib/pubkey/gost_3410/gost_3410.cpp
+++ b/src/lib/pubkey/gost_3410/gost_3410.cpp
@@ -2,7 +2,7 @@
 * GOST 34.10-2012
 * (C) 2007 Falko Strenzke, FlexSecure GmbH
 *          Manuel Hartl, FlexSecure GmbH
-* (C) 2008-2010,2015,2018 Jack Lloyd
+* (C) 2008-2010,2015,2018,2024 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
@@ -11,10 +11,8 @@
 
 #include <botan/ber_dec.h>
 #include <botan/der_enc.h>
-#include <botan/reducer.h>
 #include <botan/internal/fmt.h>
 #include <botan/internal/pk_ops_impl.h>
-#include <botan/internal/point_mul.h>
 
 namespace Botan {
 
@@ -72,7 +70,7 @@ GOST_3410_PublicKey::GOST_3410_PublicKey(const AlgorithmIdentifier& alg_id, std:
    BER_Decoder(key_bits).decode(bits, ASN1_Type::OctetString);
 
    if(bits.size() != 2 * (p_bits / 8)) {
-      throw Decoding_Error("GOST-34.10-2020 invalid encoding of public key");
+      throw Decoding_Error("GOST-34.10-2012 invalid encoding of public key");
    }
 
    const size_t part_size = bits.size() / 2;
@@ -105,14 +103,15 @@ std::unique_ptr<Public_Key> GOST_3410_PrivateKey::public_key() const {
 
 namespace {
 
-BigInt decode_le(const uint8_t msg[], size_t msg_len) {
-   secure_vector<uint8_t> msg_le(msg, msg + msg_len);
+EC_Scalar gost_msg_to_scalar(const EC_Group& group, std::span<const uint8_t> msg) {
+   std::vector<uint8_t> rev_bytes(msg.rbegin(), msg.rend());
 
-   for(size_t i = 0; i != msg_le.size() / 2; ++i) {
-      std::swap(msg_le[i], msg_le[msg_le.size() - 1 - i]);
+   auto ie = EC_Scalar::from_bytes_mod_order(group, rev_bytes);
+   if(ie.is_zero()) {
+      return EC_Scalar::one(group);
+   } else {
+      return ie;
    }
-
-   return BigInt(msg_le.data(), msg_le.size());
 }
 
 /**
@@ -121,7 +120,9 @@ BigInt decode_le(const uint8_t msg[], size_t msg_len) {
 class GOST_3410_Signature_Operation final : public PK_Ops::Signature_with_Hash {
    public:
       GOST_3410_Signature_Operation(const GOST_3410_PrivateKey& gost_3410, std::string_view emsa) :
-            PK_Ops::Signature_with_Hash(emsa), m_group(gost_3410.domain()), m_x(gost_3410.private_value()) {}
+            PK_Ops::Signature_with_Hash(emsa),
+            m_group(gost_3410.domain()),
+            m_x(EC_Scalar::from_bigint(m_group, gost_3410.private_value())) {}
 
       size_t signature_length() const override { return 2 * m_group.get_order_bytes(); }
 
@@ -131,7 +132,7 @@ class GOST_3410_Signature_Operation final : public PK_Ops::Signature_with_Hash {
 
    private:
       const EC_Group m_group;
-      const BigInt m_x;
+      const EC_Scalar m_x;
       std::vector<BigInt> m_ws;
 };
 
@@ -161,24 +162,17 @@ AlgorithmIdentifier GOST_3410_Signature_Operation::algorithm_identifier() const
 secure_vector<uint8_t> GOST_3410_Signature_Operation::raw_sign(const uint8_t msg[],
                                                                size_t msg_len,
                                                                RandomNumberGenerator& rng) {
-   const BigInt k = m_group.random_scalar(rng);
-
-   BigInt e = decode_le(msg, msg_len);
-
-   e = m_group.mod_order(e);
-   if(e.is_zero()) {
-      e = BigInt::one();
-   }
-
-   const BigInt r = m_group.mod_order(m_group.blinded_base_point_multiply_x(k, rng, m_ws));
+   const auto e = gost_msg_to_scalar(m_group, std::span{msg, msg_len});
 
-   const BigInt s = m_group.mod_order(m_group.multiply_mod_order(r, m_x) + m_group.multiply_mod_order(k, e));
+   const auto k = EC_Scalar::random(m_group, rng);
+   const auto r = EC_Scalar::gk_x_mod_order(k, rng, m_ws);
+   const auto s = (r * m_x) + (k * e);
 
-   if(r == 0 || s == 0) {
+   if(r.is_zero() || s.is_zero()) {
       throw Internal_Error("GOST 34.10 signature generation failed, r/s equal to zero");
    }
 
-   return BigInt::encode_fixed_length_int_pair(s, r, m_group.get_order_bytes());
+   return EC_Scalar::serialize_pair<secure_vector<uint8_t>>(s, r);
 }
 
 std::string gost_hash_from_algid(const AlgorithmIdentifier& alg_id) {
@@ -209,57 +203,39 @@ std::string gost_hash_from_algid(const AlgorithmIdentifier& alg_id) {
 class GOST_3410_Verification_Operation final : public PK_Ops::Verification_with_Hash {
    public:
       GOST_3410_Verification_Operation(const GOST_3410_PublicKey& gost, std::string_view padding) :
-            PK_Ops::Verification_with_Hash(padding),
-            m_group(gost.domain()),
-            m_gy_mul(m_group.get_base_point(), gost.public_point()) {}
+            PK_Ops::Verification_with_Hash(padding), m_group(gost.domain()), m_gy_mul(m_group, gost.public_point()) {}
 
       GOST_3410_Verification_Operation(const GOST_3410_PublicKey& gost, const AlgorithmIdentifier& alg_id) :
             PK_Ops::Verification_with_Hash(gost_hash_from_algid(alg_id)),
             m_group(gost.domain()),
-            m_gy_mul(m_group.get_base_point(), gost.public_point()) {}
+            m_gy_mul(m_group, gost.public_point()) {}
 
       bool verify(const uint8_t msg[], size_t msg_len, const uint8_t sig[], size_t sig_len) override;
 
    private:
       const EC_Group m_group;
-      const EC_Point_Multi_Point_Precompute m_gy_mul;
+      const EC_Group::Mul2Table m_gy_mul;
 };
 
 bool GOST_3410_Verification_Operation::verify(const uint8_t msg[],
                                               size_t msg_len,
                                               const uint8_t sig[],
                                               size_t sig_len) {
-   if(sig_len != m_group.get_order_bytes() * 2) {
-      return false;
-   }
-
-   const BigInt s(sig, sig_len / 2);
-   const BigInt r(sig + sig_len / 2, sig_len / 2);
-
-   const BigInt& order = m_group.get_order();
-
-   if(r <= 0 || r >= order || s <= 0 || s >= order) {
-      return false;
-   }
-
-   BigInt e = decode_le(msg, msg_len);
-   e = m_group.mod_order(e);
-   if(e.is_zero()) {
-      e = BigInt::one();
-   }
-
-   const BigInt v = m_group.inverse_mod_order(e);
+   if(auto sr = EC_Scalar::deserialize_pair(m_group, std::span{sig, sig_len})) {
+      const auto& [s, r] = sr.value();
 
-   const BigInt z1 = m_group.multiply_mod_order(s, v);
-   const BigInt z2 = m_group.multiply_mod_order(-r, v);
+      if(r.is_nonzero() && s.is_nonzero()) {
+         const auto e = gost_msg_to_scalar(m_group, std::span{msg, msg_len});
 
-   const EC_Point R = m_gy_mul.multi_exp(z1, z2);
+         const auto v = e.invert();
 
-   if(R.is_zero()) {
-      return false;
+         if(const auto w = m_gy_mul.mul2_vartime_x_mod_order(v, s, r.negate())) {
+            return (w == r);
+         }
+      }
    }
 
-   return (R.get_affine_x() == r);
+   return false;
 }
 
 }  // namespace
diff --git a/src/lib/pubkey/rfc6979/rfc6979.cpp b/src/lib/pubkey/rfc6979/rfc6979.cpp
index d504e05cc..cccf34487 100644
--- a/src/lib/pubkey/rfc6979/rfc6979.cpp
+++ b/src/lib/pubkey/rfc6979/rfc6979.cpp
@@ -26,24 +26,54 @@ RFC6979_Nonce_Generator::RFC6979_Nonce_Generator(std::string_view hash, const Bi
 
 RFC6979_Nonce_Generator::~RFC6979_Nonce_Generator() = default;
 
-const BigInt& RFC6979_Nonce_Generator::nonce_for(const BigInt& m) {
-   m.serialize_to(std::span{m_rng_in}.subspan(m_rlen));
-   m_hmac_drbg->clear();
-   m_hmac_drbg->initialize_with(m_rng_in.data(), m_rng_in.size());
+BigInt RFC6979_Nonce_Generator::nonce_for(const BigInt& m) {
+   m.serialize_to(std::span{m_rng_in}.last(m_rlen));
+
+   m_hmac_drbg->initialize_with(m_rng_in);
+
+   const size_t shift = 8 * m_rlen - m_qlen;
+   BOTAN_ASSERT_NOMSG(shift < 8);
+
+   BigInt k;
 
    do {
-      m_hmac_drbg->randomize(m_rng_out.data(), m_rng_out.size());
-      m_k._assign_from_bytes(m_rng_out);
-      m_k >>= (8 * m_rlen - m_qlen);
-   } while(m_k == 0 || m_k >= m_order);
+      m_hmac_drbg->randomize(m_rng_out);
+      k._assign_from_bytes(m_rng_out);
 
-   return m_k;
-}
+      if(shift > 0) {
+         k >>= shift;
+      }
+   } while(k == 0 || k >= m_order);
 
-BigInt generate_rfc6979_nonce(const BigInt& x, const BigInt& q, const BigInt& h, std::string_view hash) {
-   RFC6979_Nonce_Generator gen(hash, q, x);
-   BigInt k = gen.nonce_for(h);
    return k;
 }
 
+#if defined(BOTAN_HAS_ECC_GROUP)
+EC_Scalar RFC6979_Nonce_Generator::nonce_for(const EC_Group& group, const EC_Scalar& m) {
+   m.serialize_to(std::span{m_rng_in}.last(m_rlen));
+
+   m_hmac_drbg->initialize_with(m_rng_in);
+
+   const size_t shift = 8 * m_rlen - m_qlen;
+   BOTAN_ASSERT_NOMSG(shift < 8);
+
+   for(;;) {
+      m_hmac_drbg->randomize(m_rng_out);
+
+      if(shift > 0) {
+         uint8_t carry = 0;
+         for(uint8_t& b : m_rng_out) {
+            const uint8_t w = b;
+            b = (w >> shift) | carry;
+            carry = w << (8 - shift);
+         }
+      }
+
+      if(auto k = EC_Scalar::deserialize(group, m_rng_out)) {
+         return *k;
+      }
+   }
+}
+#endif
+
 }  // namespace Botan
diff --git a/src/lib/pubkey/rfc6979/rfc6979.h b/src/lib/pubkey/rfc6979/rfc6979.h
index 6bb20987a..8dabc7f26 100644
--- a/src/lib/pubkey/rfc6979/rfc6979.h
+++ b/src/lib/pubkey/rfc6979/rfc6979.h
@@ -12,6 +12,10 @@
 #include <memory>
 #include <string>
 
+#if defined(BOTAN_HAS_ECC_GROUP)
+   #include <botan/ec_scalar.h>
+#endif
+
 namespace Botan {
 
 class HMAC_DRBG;
@@ -25,11 +29,14 @@ class BOTAN_TEST_API RFC6979_Nonce_Generator final {
 
       ~RFC6979_Nonce_Generator();
 
-      const BigInt& nonce_for(const BigInt& m);
+#if defined(BOTAN_HAS_ECC_GROUP)
+      EC_Scalar nonce_for(const EC_Group& group, const EC_Scalar& m);
+#endif
+
+      BigInt nonce_for(const BigInt& m);
 
    private:
       const BigInt& m_order;
-      BigInt m_k;
       size_t m_qlen, m_rlen;
       std::unique_ptr<HMAC_DRBG> m_hmac_drbg;
       secure_vector<uint8_t> m_rng_in, m_rng_out;
@@ -41,8 +48,10 @@ class BOTAN_TEST_API RFC6979_Nonce_Generator final {
 * @param h the message hash already reduced mod q
 * @param hash the hash function used to generate h
 */
-BOTAN_TEST_API
-BigInt generate_rfc6979_nonce(const BigInt& x, const BigInt& q, const BigInt& h, std::string_view hash);
+inline BigInt generate_rfc6979_nonce(const BigInt& x, const BigInt& q, const BigInt& h, std::string_view hash) {
+   RFC6979_Nonce_Generator gen(hash, q, x);
+   return gen.nonce_for(h);
+}
 
 }  // namespace Botan
 
diff --git a/src/lib/pubkey/sm2/sm2.cpp b/src/lib/pubkey/sm2/sm2.cpp
index 7e32fc709..d3f04ca80 100644
--- a/src/lib/pubkey/sm2/sm2.cpp
+++ b/src/lib/pubkey/sm2/sm2.cpp
@@ -1,7 +1,7 @@
 /*
 * SM2 Signatures
 * (C) 2017,2018 Ribose Inc
-* (C) 2018 Jack Lloyd
+* (C) 2018,2024 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
@@ -14,7 +14,6 @@
 #include <botan/internal/loadstor.h>
 #include <botan/internal/parsing.h>
 #include <botan/internal/pk_ops_impl.h>
-#include <botan/internal/point_mul.h>
 
 namespace Botan {
 
@@ -88,7 +87,9 @@ namespace {
 class SM2_Signature_Operation final : public PK_Ops::Signature {
    public:
       SM2_Signature_Operation(const SM2_PrivateKey& sm2, std::string_view ident, std::string_view hash) :
-            m_group(sm2.domain()), m_x(sm2.private_value()), m_da_inv(sm2.get_da_inv()) {
+            m_group(sm2.domain()),
+            m_x(EC_Scalar::from_bigint(m_group, sm2.private_value())),
+            m_da_inv(EC_Scalar::from_bigint(m_group, sm2.get_da_inv())) {
          if(hash == "Raw") {
             // m_hash is null, m_za is empty
          } else {
@@ -115,8 +116,8 @@ class SM2_Signature_Operation final : public PK_Ops::Signature {
 
    private:
       const EC_Group m_group;
-      const BigInt m_x;
-      const BigInt m_da_inv;
+      const EC_Scalar m_x;
+      const EC_Scalar m_da_inv;
 
       std::vector<uint8_t> m_za;
       secure_vector<uint8_t> m_digest;
@@ -125,22 +126,25 @@ class SM2_Signature_Operation final : public PK_Ops::Signature {
 };
 
 secure_vector<uint8_t> SM2_Signature_Operation::sign(RandomNumberGenerator& rng) {
-   BigInt e;
-   if(m_hash) {
-      e = BigInt::from_bytes(m_hash->final());
-      // prepend ZA for next signature if any
-      m_hash->update(m_za);
-   } else {
-      e = BigInt::from_bytes(m_digest);
-      m_digest.clear();
-   }
+   const auto e = [&]() {
+      if(m_hash) {
+         auto ie = EC_Scalar::from_bytes_mod_order(m_group, m_hash->final());
+         // prepend ZA for next signature if any
+         m_hash->update(m_za);
+         return ie;
+      } else {
+         auto ie = EC_Scalar::from_bytes_mod_order(m_group, m_digest);
+         m_digest.clear();
+         return ie;
+      }
+   }();
 
-   const BigInt k = m_group.random_scalar(rng);
+   const auto k = EC_Scalar::random(m_group, rng);
 
-   const BigInt r = m_group.mod_order(m_group.blinded_base_point_multiply_x(k, rng, m_ws) + e);
-   const BigInt s = m_group.multiply_mod_order(m_da_inv, m_group.mod_order(k - r * m_x));
+   const auto r = EC_Scalar::gk_x_mod_order(k, rng, m_ws) + e;
+   const auto s = (k - r * m_x) * m_da_inv;
 
-   return BigInt::encode_fixed_length_int_pair(r, s, m_group.get_order_bytes());
+   return EC_Scalar::serialize_pair<secure_vector<uint8_t>>(r, s);
 }
 
 /**
@@ -149,7 +153,7 @@ secure_vector<uint8_t> SM2_Signature_Operation::sign(RandomNumberGenerator& rng)
 class SM2_Verification_Operation final : public PK_Ops::Verification {
    public:
       SM2_Verification_Operation(const SM2_PublicKey& sm2, std::string_view ident, std::string_view hash) :
-            m_group(sm2.domain()), m_gy_mul(m_group.get_base_point(), sm2.public_point()) {
+            m_group(sm2.domain()), m_gy_mul(m_group, sm2.public_point()) {
          if(hash == "Raw") {
             // m_hash is null, m_za is empty
          } else {
@@ -174,48 +178,39 @@ class SM2_Verification_Operation final : public PK_Ops::Verification {
 
    private:
       const EC_Group m_group;
-      const EC_Point_Multi_Point_Precompute m_gy_mul;
+      const EC_Group::Mul2Table m_gy_mul;
       secure_vector<uint8_t> m_digest;
       std::vector<uint8_t> m_za;
       std::unique_ptr<HashFunction> m_hash;
 };
 
 bool SM2_Verification_Operation::is_valid_signature(const uint8_t sig[], size_t sig_len) {
-   BigInt e;
-   if(m_hash) {
-      e = BigInt::from_bytes(m_hash->final());
-      // prepend ZA for next signature if any
-      m_hash->update(m_za);
-   } else {
-      e = BigInt::from_bytes(m_digest);
-      m_digest.clear();
-   }
-
-   if(sig_len != m_group.get_order_bytes() * 2) {
-      return false;
-   }
-
-   const BigInt r(sig, sig_len / 2);
-   const BigInt s(sig + sig_len / 2, sig_len / 2);
-
-   if(r <= 0 || r >= m_group.get_order() || s <= 0 || s >= m_group.get_order()) {
-      return false;
-   }
-
-   const BigInt t = m_group.mod_order(r + s);
-
-   if(t == 0) {
-      return false;
-   }
+   const auto e = [&]() {
+      if(m_hash) {
+         auto ie = EC_Scalar::from_bytes_mod_order(m_group, m_hash->final());
+         // prepend ZA for next signature if any
+         m_hash->update(m_za);
+         return ie;
+      } else {
+         auto ie = EC_Scalar::from_bytes_mod_order(m_group, m_digest);
+         m_digest.clear();
+         return ie;
+      }
+   }();
 
-   const EC_Point R = m_gy_mul.multi_exp(s, t);
+   if(auto rs = EC_Scalar::deserialize_pair(m_group, std::span{sig, sig_len})) {
+      const auto& [r, s] = rs.value();
 
-   // ???
-   if(R.is_zero()) {
-      return false;
+      if(r.is_nonzero() && s.is_nonzero()) {
+         const auto t = r + s;
+         if(t.is_nonzero()) {
+            if(const auto v = m_gy_mul.mul2_vartime_x_mod_order(s, t)) {
+               return (v.value() + e) == r;
+            }
+         }
+      }
    }
-
-   return (m_group.mod_order(R.get_affine_x() + e) == r);
+   return false;
 }
 
 void parse_sm2_param_string(std::string_view params, std::string& userid, std::string& hash) {
diff --git a/src/lib/pubkey/sm2/sm2_enc.cpp b/src/lib/pubkey/sm2/sm2_enc.cpp
index 96acf8d2e..c54601333 100644
--- a/src/lib/pubkey/sm2/sm2_enc.cpp
+++ b/src/lib/pubkey/sm2/sm2_enc.cpp
@@ -14,7 +14,6 @@
 #include <botan/pk_ops.h>
 #include <botan/internal/ct_utils.h>
 #include <botan/internal/fmt.h>
-#include <botan/internal/point_mul.h>
 
 namespace Botan {
 
@@ -22,14 +21,10 @@ namespace {
 
 class SM2_Encryption_Operation final : public PK_Ops::Encryption {
    public:
-      SM2_Encryption_Operation(const SM2_Encryption_PublicKey& key,
-                               RandomNumberGenerator& rng,
-                               std::string_view kdf_hash) :
-            m_group(key.domain()), m_ws(EC_Point::WORKSPACE_SIZE), m_mul_public_point(key.public_point(), rng, m_ws) {
+      SM2_Encryption_Operation(const SM2_Encryption_PublicKey& key, std::string_view kdf_hash) :
+            m_group(key.domain()), m_peer(m_group, key.public_point()), m_ws(EC_Point::WORKSPACE_SIZE) {
          m_hash = HashFunction::create_or_throw(kdf_hash);
-
-         const std::string kdf_name = fmt("KDF2({})", kdf_hash);
-         m_kdf = KDF::create_or_throw(kdf_name);
+         m_kdf = KDF::create_or_throw(fmt("KDF2({})", kdf_hash));
       }
 
       size_t max_input_bits() const override {
@@ -45,13 +40,11 @@ class SM2_Encryption_Operation final : public PK_Ops::Encryption {
       }
 
       secure_vector<uint8_t> encrypt(const uint8_t msg[], size_t msg_len, RandomNumberGenerator& rng) override {
-         const BigInt k = m_group.random_scalar(rng);
+         const auto k = EC_Scalar::random(m_group, rng);
 
-         const EC_Point C1 = m_group.blinded_base_point_multiply(k, rng, m_ws);
-         const BigInt x1 = C1.get_affine_x();
-         const BigInt y1 = C1.get_affine_y();
+         const EC_AffinePoint C1 = EC_AffinePoint::g_mul(k, rng, m_ws);
 
-         const EC_Point kPB = m_mul_public_point.mul(k, rng, m_group.get_order(), m_ws);
+         const EC_AffinePoint kPB = m_peer.mul(k, rng, m_ws);
 
          const auto x2_bytes = kPB.x_bytes();
          const auto y2_bytes = kPB.y_bytes();
@@ -72,8 +65,8 @@ class SM2_Encryption_Operation final : public PK_Ops::Encryption {
 
          return DER_Encoder()
             .start_sequence()
-            .encode(x1)
-            .encode(y1)
+            .encode(BigInt(C1.x_bytes()))
+            .encode(BigInt(C1.y_bytes()))
             .encode(C3, ASN1_Type::OctetString)
             .encode(masked_msg, ASN1_Type::OctetString)
             .end_cons()
@@ -82,10 +75,10 @@ class SM2_Encryption_Operation final : public PK_Ops::Encryption {
 
    private:
       const EC_Group m_group;
+      const EC_AffinePoint m_peer;
       std::unique_ptr<HashFunction> m_hash;
       std::unique_ptr<KDF> m_kdf;
       std::vector<BigInt> m_ws;
-      EC_Point_Var_Point_Precompute m_mul_public_point;
 };
 
 class SM2_Decryption_Operation final : public PK_Ops::Decryption {
@@ -93,7 +86,7 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
       SM2_Decryption_Operation(const SM2_Encryption_PrivateKey& key,
                                RandomNumberGenerator& rng,
                                std::string_view kdf_hash) :
-            m_key(key), m_rng(rng) {
+            m_group(key.domain()), m_x(EC_Scalar::from_bigint(m_group, key.private_value())), m_rng(rng) {
          m_hash = HashFunction::create_or_throw(kdf_hash);
 
          const std::string kdf_name = fmt("KDF2({})", kdf_hash);
@@ -105,7 +98,7 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
          * This ignores the DER encoding and so overestimates the
          * plaintext length by 12 bytes or so
          */
-         const size_t elem_size = m_key.domain().get_order_bytes();
+         const size_t elem_size = m_group.get_order_bytes();
 
          if(ptext_len < 2 * elem_size + m_hash->output_length()) {
             return 0;
@@ -115,9 +108,8 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
       }
 
       secure_vector<uint8_t> decrypt(uint8_t& valid_mask, const uint8_t ciphertext[], size_t ciphertext_len) override {
-         const EC_Group& group = m_key.domain();
-         const BigInt& cofactor = group.get_cofactor();
-         const size_t p_bytes = group.get_p_bytes();
+         const BigInt& cofactor = m_group.get_cofactor();
+         const size_t p_bytes = m_group.get_p_bytes();
 
          valid_mask = 0x00;
 
@@ -155,8 +147,7 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
             return secure_vector<uint8_t>();
          }
 
-         EC_Point C1 = group.point(x1, y1);
-         C1.randomize_repr(m_rng);
+         EC_Point C1 = m_group.point(x1, y1);
 
          // Here C1 is publically invalid, so no problem with early return:
          if(!C1.on_the_curve()) {
@@ -167,24 +158,18 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
             return secure_vector<uint8_t>();
          }
 
-         const EC_Point dbC1 = group.blinded_var_point_multiply(C1, m_key.private_value(), m_rng, m_ws);
-
+         const auto dbC1 = EC_AffinePoint(m_group, C1).mul(m_x, m_rng, m_ws);
          const auto x2_bytes = dbC1.x_bytes();
          const auto y2_bytes = dbC1.y_bytes();
 
-         secure_vector<uint8_t> kdf_input;
-         kdf_input += x2_bytes;
-         kdf_input += y2_bytes;
-
-         const secure_vector<uint8_t> kdf_output =
-            m_kdf->derive_key(masked_msg.size(), kdf_input.data(), kdf_input.size());
+         const auto kdf_output = m_kdf->derive_key(masked_msg.size(), dbC1.xy_bytes());
 
          xor_buf(masked_msg.data(), kdf_output.data(), kdf_output.size());
 
          m_hash->update(x2_bytes);
          m_hash->update(masked_msg);
          m_hash->update(y2_bytes);
-         secure_vector<uint8_t> u = m_hash->final();
+         const auto u = m_hash->final();
 
          if(!CT::is_equal(u.data(), C3.data(), m_hash->output_length()).as_bool()) {
             return secure_vector<uint8_t>();
@@ -195,7 +180,8 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
       }
 
    private:
-      const SM2_Encryption_PrivateKey& m_key;
+      const EC_Group m_group;
+      const EC_Scalar m_x;
       RandomNumberGenerator& m_rng;
       std::vector<BigInt> m_ws;
       std::unique_ptr<HashFunction> m_hash;
@@ -207,11 +193,13 @@ class SM2_Decryption_Operation final : public PK_Ops::Decryption {
 std::unique_ptr<PK_Ops::Encryption> SM2_PublicKey::create_encryption_op(RandomNumberGenerator& rng,
                                                                         std::string_view params,
                                                                         std::string_view provider) const {
+   BOTAN_UNUSED(rng);
+
    if(provider == "base" || provider.empty()) {
       if(params.empty()) {
-         return std::make_unique<SM2_Encryption_Operation>(*this, rng, "SM3");
+         return std::make_unique<SM2_Encryption_Operation>(*this, "SM3");
       } else {
-         return std::make_unique<SM2_Encryption_Operation>(*this, rng, params);
+         return std::make_unique<SM2_Encryption_Operation>(*this, params);
       }
    }
 


Test patch:
diff --git a/src/tests/data/pubkey/ecdsa_verify.vec b/src/tests/data/pubkey/ecdsa_verify.vec
index 8bf5b5729..dd538ff9f 100644
--- a/src/tests/data/pubkey/ecdsa_verify.vec
+++ b/src/tests/data/pubkey/ecdsa_verify.vec
@@ -69,14 +69,6 @@ Py = 81333916963110019576228330948951168219884247801435258672405011123948094
 Msg = 0679246D6C4216DE0DAA08E5523FB2674DB2B6599C3B72FF946B488A15290B62
 Signature = 30cf3ae9da8c18ef37664e358e43b07f93ded599653e64acd171e197a1c72f9ad521e5e2e091e9fe4c27f1110265ec5cbb701a6faf3569304774de5f
 
-# From CryptoFuzz
-Group = secp256k1
-Valid = 0
-Px = 70000000000000000
-Py = 0
-Msg = d1a0b2537473db2edf298bab7cb968882ab9d558c2a6298b4556ee9d36298cae
-Signature = 79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798D1A0B2537473DB2EDF298BAB7CB968882AB9D558C2A6298B4556EE9D36298CAE
-
 # OSS-Fuzz 32470
 Group = secp256k1
 Valid = 1
diff --git a/src/tests/test_ecc_pointmul.cpp b/src/tests/test_ecc_pointmul.cpp
index 8953b8555..6a096f7fe 100644
--- a/src/tests/test_ecc_pointmul.cpp
+++ b/src/tests/test_ecc_pointmul.cpp
@@ -47,6 +47,14 @@ class ECC_Basepoint_Mul_Tests final : public Text_Based_Test {
          const Botan::EC_Point p3 = group.blinded_var_point_multiply(base_point, k, this->rng(), ws);
          result.test_eq("blinded_var_point_multiply", p3, pt);
 
+         const auto scalar = Botan::EC_Scalar::from_bigint(group, k);
+         const auto apg = Botan::EC_AffinePoint::g_mul(scalar, this->rng(), ws);
+         result.test_eq("AffinePoint::g_mul", apg.serialize_uncompressed(), P_bytes);
+
+         const auto ag = Botan::EC_AffinePoint(group, base_point);
+         const auto ap = ag.mul(scalar, this->rng(), ws);
+         result.test_eq("AffinePoint::mul", ap.to_legacy_point(), pt);
+
          return result;
       }
 };