Skip to content

Commit

Permalink
Refactor pcurves affine conversions and inversion usage
Browse files Browse the repository at this point in the history
We did not consistently take advantage of any optimized addition chain
for inversions, particularly in table setup and hash to curve code.

Also refactor hash to curve such that we avoid a pointless affine
conversion in the NU case; previously we returned a projective point
even in that case, then later converted it to affine.
  • Loading branch information
randombit committed Jul 31, 2024
1 parent 0d4969a commit 709ffed
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 149 deletions.
18 changes: 13 additions & 5 deletions src/lib/math/pcurves/pcurves.h
Original file line number Diff line number Diff line change
Expand Up @@ -415,14 +415,22 @@ class BOTAN_TEST_API PrimeOrderCurve {
virtual Scalar random_scalar(RandomNumberGenerator& rng) const = 0;

/**
* RFC 9380 hash to curve
* RFC 9380 hash to curve (NU variant)
*
* This is currently only supported for a few specific curves
*/
virtual ProjectivePoint hash_to_curve(std::string_view hash,
std::span<const uint8_t> input,
std::span<const uint8_t> domain_sep,
bool random_oracle) const = 0;
virtual AffinePoint hash_to_curve_nu(std::string_view hash,
std::span<const uint8_t> input,
std::span<const uint8_t> domain_sep) const = 0;

/**
* RFC 9380 hash to curve (RO variant)
*
* This is currently only supported for a few specific curves
*/
virtual ProjectivePoint hash_to_curve_ro(std::string_view hash,
std::span<const uint8_t> input,
std::span<const uint8_t> domain_sep) const = 0;
};

} // namespace Botan::PCurve
Expand Down
257 changes: 149 additions & 108 deletions src/lib/math/pcurves/pcurves_impl/pcurves_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

namespace Botan {

namespace {

template <typename Params>
class MontgomeryRep final {
public:
Expand Down Expand Up @@ -842,77 +844,6 @@ class ProjectiveCurvePoint {

constexpr Self negate() const { return Self(x(), y().negate(), z()); }

constexpr AffinePoint to_affine() const {
// Not strictly required right? - default should work as long
// as (0,0) is identity and invert returns 0 on 0
if(this->is_identity().as_bool()) {
return AffinePoint::identity();
}

const auto z_inv = m_z.invert();
const auto z2_inv = z_inv.square();
const auto z3_inv = z_inv * z2_inv;

const auto x = m_x * z2_inv;
const auto y = m_y * z3_inv;
return AffinePoint(x, y);
}

static std::vector<AffinePoint> to_affine_batch(std::span<const Self> projective) {
const size_t N = projective.size();
std::vector<AffinePoint> affine(N, AffinePoint::identity());

bool any_identity = false;
for(size_t i = 0; i != N; ++i) {
if(projective[i].is_identity().as_bool()) {
any_identity = true;
// If any of the elements are the identity we fall back to
// performing the conversion without a batch
break;
}
}

if(N <= 2 || any_identity) {
for(size_t i = 0; i != N; ++i) {
affine[i] = projective[i].to_affine();
}
} else {
std::vector<FieldElement> c(N);

/*
Batch projective->affine using Montgomery's trick
See Algorithm 2.26 in "Guide to Elliptic Curve Cryptography"
(Hankerson, Menezes, Vanstone)
*/

c[0] = projective[0].z();
for(size_t i = 1; i != N; ++i) {
c[i] = c[i - 1] * projective[i].z();
}

auto s_inv = c[N - 1].invert();

for(size_t i = N - 1; i > 0; --i) {
const auto& p = projective[i];

const auto z_inv = s_inv * c[i - 1];
const auto z2_inv = z_inv.square();
const auto z3_inv = z_inv * z2_inv;

s_inv = s_inv * p.z();

affine[i] = AffinePoint(p.x() * z2_inv, p.y() * z3_inv);
}

const auto z2_inv = s_inv.square();
const auto z3_inv = s_inv * z2_inv;
affine[0] = AffinePoint(projective[0].x() * z2_inv, projective[0].y() * z3_inv);
}

return affine;
}

void randomize_rep(RandomNumberGenerator& rng) {
if(!rng.is_seeded()) {
return;
Expand Down Expand Up @@ -1011,24 +942,11 @@ class EllipticCurve {
(Params::Z != 0 && A.is_nonzero().as_bool() && B.is_nonzero().as_bool() && FieldElement::P_MOD_4 == 3);

static constexpr bool OrderIsLessThanField = bigint_cmp(NW.data(), NW.size(), PW.data(), PW.size()) == -1;
};

// (-B / A), will be zero if A == 0 or B == 0 or Z == 0
static const FieldElement& SSWU_C1()
requires ValidForSswuHash
{
// We derive it from C2 to avoid a second inversion
static const auto C1 = (SSWU_C2() * SSWU_Z).negate();
return C1;
}

// (B / (Z * A)), will be zero if A == 0 or B == 0 or Z == 0
static const FieldElement& SSWU_C2()
requires ValidForSswuHash
{
// This could use a variable time inversion
static const auto C2 = (B * (SSWU_Z * A).invert());
return C2;
}
template <typename C>
concept curve_supports_fe_invert2 = requires(const typename C::FieldElement& fe) {
{ C::fe_invert2(fe) } -> std::same_as<typename C::FieldElement>;
};

/**
Expand Down Expand Up @@ -1134,6 +1052,111 @@ class UnblindedScalarBits final {
std::array<uint8_t, C::Scalar::BYTES> m_bytes;
};

template <typename C>
inline auto invert_field_element(const typename C::FieldElement& fe) {
if constexpr(curve_supports_fe_invert2<C>) {
return C::fe_invert2(fe) * fe;
} else {
return fe.invert();
}
}

/// Convert a projective point into affine
template <typename C>
auto to_affine(const typename C::ProjectivePoint& pt) {
// Not strictly required right? - default should work as long
// as (0,0) is identity and invert returns 0 on 0
if(pt.is_identity().as_bool()) {
return C::AffinePoint::identity();
}

if constexpr(curve_supports_fe_invert2<C>) {
const auto z2_inv = C::fe_invert2(pt.z());
const auto z3_inv = z2_inv.square() * pt.z();
return typename C::AffinePoint(pt.x() * z2_inv, pt.y() * z3_inv);
} else {
const auto z_inv = invert_field_element<C>(pt.z());
const auto z2_inv = z_inv.square();
const auto z3_inv = z_inv * z2_inv;
return typename C::AffinePoint(pt.x() * z2_inv, pt.y() * z3_inv);
}
}

/// Convert a projective point into affine and return x coordinate only
template <typename C>
auto to_affine_x(const typename C::ProjectivePoint& pt) {
if constexpr(curve_supports_fe_invert2<C>) {
return pt.x() * C::fe_invert2(pt.z());
} else {
return to_affine<C>(pt).x();
}
}

/**
* Batch projective->affine conversion
*/
template <typename C>
auto to_affine_batch(std::span<const typename C::ProjectivePoint> projective) {
typedef typename C::AffinePoint AffinePoint;
typedef typename C::FieldElement FieldElement;

const size_t N = projective.size();
std::vector<AffinePoint> affine(N, AffinePoint::identity());

bool any_identity = false;
for(size_t i = 0; i != N; ++i) {
if(projective[i].is_identity().as_bool()) {
any_identity = true;
// If any of the elements are the identity we fall back to
// performing the conversion without a batch
break;
}
}

if(N <= 2 || any_identity) {
// If there are identity elements, using the batch inversion gets
// tricky. It can be done, but this should be a rare situation so
// just punt to the serial conversion if it occurs
for(size_t i = 0; i != N; ++i) {
affine[i] = to_affine<C>(projective[i]);
}
} else {
std::vector<FieldElement> c(N);

/*
Batch projective->affine using Montgomery's trick
See Algorithm 2.26 in "Guide to Elliptic Curve Cryptography"
(Hankerson, Menezes, Vanstone)
*/

c[0] = projective[0].z();
for(size_t i = 1; i != N; ++i) {
c[i] = c[i - 1] * projective[i].z();
}

auto s_inv = invert_field_element<C>(c[N - 1]);

for(size_t i = N - 1; i > 0; --i) {
const auto& p = projective[i];

const auto z_inv = s_inv * c[i - 1];
const auto z2_inv = z_inv.square();
const auto z3_inv = z_inv * z2_inv;

s_inv = s_inv * p.z();

affine[i] = AffinePoint(p.x() * z2_inv, p.y() * z3_inv);
}

const auto z2_inv = s_inv.square();
const auto z3_inv = s_inv * z2_inv;
affine[0] = AffinePoint(projective[0].x() * z2_inv, projective[0].y() * z3_inv);
}

return affine;
}

/**
* Base point precomputation table
*
Expand Down Expand Up @@ -1212,7 +1235,7 @@ class PrecomputedBaseMulTable final {
accum = table[i + (WindowElements / 2)].dbl();
}

m_table = ProjectivePoint::to_affine_batch(table);
m_table = to_affine_batch<C>(table);
}

ProjectivePoint mul(const Scalar& s, RandomNumberGenerator& rng) const {
Expand Down Expand Up @@ -1291,7 +1314,7 @@ class WindowedMulTable final {
}
}

m_table = ProjectivePoint::to_affine_batch(table);
m_table = to_affine_batch<C>(table);
}

ProjectivePoint mul(const Scalar& s, RandomNumberGenerator& rng) const {
Expand Down Expand Up @@ -1433,7 +1456,7 @@ class WindowedMul2Table final {
table.emplace_back(next_tbl_e());
}

m_table = ProjectivePoint::to_affine_batch(table);
m_table = to_affine_batch<C>(table);
}

/**
Expand Down Expand Up @@ -1478,14 +1501,34 @@ class WindowedMul2Table final {
std::vector<AffinePoint> m_table;
};

// SSWU constant C1 - (B / (Z * A))
template <typename C>
const auto& SSWU_C2()
requires C::ValidForSswuHash
{
// This could use a variable time inversion
static const typename C::FieldElement C2 = C::B * invert_field_element<C>(C::SSWU_Z * C::A);
return C2;
}

// SSWU constant C1 - (-B / A)
template <typename C>
const auto& SSWU_C1()
requires C::ValidForSswuHash
{
// We derive it from C2 to avoid a second inversion
static const typename C::FieldElement C1 = (SSWU_C2<C>() * C::SSWU_Z).negate();
return C1;
}

template <typename C>
inline auto map_to_curve_sswu(const typename C::FieldElement& u) -> typename C::AffinePoint {
CT::poison(u);
const auto z_u2 = C::SSWU_Z * u.square(); // z * u^2
const auto z2_u4 = z_u2.square();
const auto tv1 = (z2_u4 + z_u2).invert();
auto x1 = C::SSWU_C1() * (C::FieldElement::one() + tv1);
C::FieldElement::conditional_assign(x1, tv1.is_zero(), C::SSWU_C2());
const auto tv1 = invert_field_element<C>(z2_u4 + z_u2);
auto x1 = SSWU_C1<C>() * (C::FieldElement::one() + tv1);
C::FieldElement::conditional_assign(x1, tv1.is_zero(), SSWU_C2<C>());
const auto gx1 = C::AffinePoint::x3_ax_b(x1);

const auto x2 = z_u2 * x1;
Expand All @@ -1509,38 +1552,36 @@ inline auto map_to_curve_sswu(const typename C::FieldElement& u) -> typename C::
return pt;
}

template <typename C>
inline auto hash_to_curve_sswu(std::string_view hash,
bool random_oracle,
std::span<const uint8_t> pw,
std::span<const uint8_t> dst) -> typename C::ProjectivePoint {
template <typename C, bool RO>
inline auto hash_to_curve_sswu(std::string_view hash, std::span<const uint8_t> pw, std::span<const uint8_t> dst) {
static_assert(C::ValidForSswuHash);
#if defined(BOTAN_HAS_XMD)

const size_t SecurityLevel = (C::OrderBits + 1) / 2;
const size_t L = (C::PrimeFieldBits + SecurityLevel + 7) / 8;

const size_t Cnt = (random_oracle ? 2 : 1);
constexpr size_t SecurityLevel = (C::OrderBits + 1) / 2;
constexpr size_t L = (C::PrimeFieldBits + SecurityLevel + 7) / 8;
constexpr size_t Cnt = RO ? 2 : 1;

std::vector<uint8_t> xmd(L * Cnt);
std::array<uint8_t, L * Cnt> xmd;
expand_message_xmd(hash, xmd, pw, dst);

if(Cnt == 1) {
const auto u = C::FieldElement::from_wide_bytes(std::span<const uint8_t, L>(xmd));
return C::ProjectivePoint::from_affine(map_to_curve_sswu<C>(u));
} else {
if constexpr(RO) {
const auto u0 = C::FieldElement::from_wide_bytes(std::span<const uint8_t, L>(xmd.data(), L));
const auto u1 = C::FieldElement::from_wide_bytes(std::span<const uint8_t, L>(xmd.data() + L, L));

auto accum = C::ProjectivePoint::from_affine(map_to_curve_sswu<C>(u0));
accum += map_to_curve_sswu<C>(u1);
return accum;
} else {
const auto u = C::FieldElement::from_wide_bytes(std::span<const uint8_t, L>(xmd.data(), L));
return map_to_curve_sswu<C>(u);
}
#else
throw Not_Implemented("Hash to curve not available due to missing XMD");
#endif
}

} // namespace

} // namespace Botan

#endif
Loading

0 comments on commit 709ffed

Please sign in to comment.