Skip to content

Commit d82152a

Browse files
committed
finding 7: add subgroup check on g2 points.
1 parent e47cc10 commit d82152a

10 files changed

Lines changed: 137 additions & 13 deletions

File tree

barretenberg/cpp/src/barretenberg/bbapi/bbapi_ecc.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ Bn254G2Mul::Response Bn254G2Mul::execute(BBApiRequest& request) &&
9999
if (!point.on_curve()) {
100100
BBAPI_ERROR(request, "Input point must be on the curve");
101101
}
102+
// BN254 G2 has cofactor h2 ≈ 2^254. An on-curve point may lie in a cofactor subgroup of order
103+
// dividing h2 rather than the prime-order subgroup; we do not want to allow such points
104+
// as inputs to bbapi.
105+
if (!point.is_in_prime_subgroup()) {
106+
BBAPI_ERROR(request, "Input point must lie in the prime-order subgroup");
107+
}
102108
auto result = point * scalar;
103109
if (!result.on_curve()) {
104110
BBAPI_ERROR(request, "Output point must be on the curve");

barretenberg/cpp/src/barretenberg/bbapi/bbapi_srs.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,15 @@ SrsInitSrs::Response SrsInitSrs::execute(BB_UNUSED BBApiRequest& request) &&
5050
std::to_string(bytes_per_point));
5151
}
5252

53-
// Parse G2 point from buffer (128 bytes)
53+
// Parse G2 point from buffer (128 bytes). `serialize_from_buffer` validates that the bytes
54+
// decode to a curve point but does NOT enforce subgroup membership. BN254 G2 has a non-trivial
55+
// cofactor (h2 ≈ 2^254), so a curve point may lie in a small cofactor subgroup of order
56+
// dividing h2 rather than the prime-order subgroup of order r. Reject anything outside
57+
// the prime-order subgroup before it reaches the SRS factory.
5458
auto g2_point_elem = from_buffer<g2::affine_element>(g2_point.data());
59+
if (!g2_point_elem.is_in_prime_subgroup()) {
60+
throw_or_abort("SrsInitSrs: g2_point is not in the BN254 G2 prime-order subgroup");
61+
}
5562

5663
// Initialize BN254 SRS
5764
bb::srs::init_bn254_mem_crs_factory(g1_points, g2_point_elem);

barretenberg/cpp/src/barretenberg/ecc/curves/bn254/g2.test.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,39 @@ TEST(g2, GeneratorIsCorrect)
182182
fq("0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b") } };
183183
EXPECT_EQ(generator, expected);
184184
}
185+
186+
// The generator, infinity, and arbitrary scalar multiples of the generator must be accepted as
187+
// members of the BN254 G2 prime-order subgroup.
188+
TEST(g2, IsInPrimeSubgroupAcceptsSubgroupPoints)
189+
{
190+
const g2::affine_element gen(Bn254G2Params::one_x, Bn254G2Params::one_y);
191+
EXPECT_TRUE(gen.is_in_prime_subgroup());
192+
EXPECT_TRUE(g2::affine_element::infinity().is_in_prime_subgroup());
193+
194+
for (size_t i = 0; i < 4; ++i) {
195+
const g2::affine_element P(g2::element(gen) * fr::random_element());
196+
EXPECT_TRUE(P.is_in_prime_subgroup());
197+
}
198+
}
199+
200+
// BN254 G2 has cofactor h2 ≈ 2^254, so on-curve does NOT imply prime-order subgroup membership. The hardcoded point
201+
// below was constructed by sampling x = i + u (for the smallest positive integer i that yields a curve point) and
202+
// recovering y via Fq2 sqrt; because only a 1/h2 fraction of E'(Fq2) lies in G_r, this specimen lies in a cofactor
203+
// subgroup. Such a point must be rejected. Coordinates are in Montgomery form to match `Bn254G2Params::one_x` etc.
204+
TEST(g2, IsInPrimeSubgroupRejectsCofactorPoint)
205+
{
206+
const g2::affine_element off_subgroup{
207+
fq2{ fq{ 0xa6ba871b8b1e1b3a, 0x14f1d651eb8e167b, 0xccdd46def0f28c58, 0x1c14ef83340fbe5e },
208+
fq{ 0xd35d438dc58f0d9d, 0x0a78eb28f5c70b3d, 0x666ea36f7879462c, 0x0e0a77c19a07df2f } },
209+
fq2{ fq{ 0x0294a5225573dc93, 0x53874be07988f4f1, 0x2a05d8b41ccce7d3, 0x20045194f06acd0e },
210+
fq{ 0x3814c8e5e4179a98, 0x793241f4d911e617, 0x28cf8e4b0df4482e, 0x0d612bd6f79bd361 } }
211+
};
212+
ASSERT_TRUE(off_subgroup.on_curve());
213+
EXPECT_FALSE(off_subgroup.is_in_prime_subgroup());
214+
215+
// Sanity check that scalar multiplication via the Fr-typed `*` operator does NOT detect
216+
// subgroup membership — multiplying by `Fr(0)` (the additive identity, which equals `r mod r`)
217+
// gives infinity for every input, including off-subgroup points. This is precisely why
218+
// is_in_prime_subgroup() routes through a uint256_t scalar instead.
219+
EXPECT_TRUE((off_subgroup * fr::zero()).is_point_at_infinity());
220+
}

barretenberg/cpp/src/barretenberg/ecc/curves/bn254/pairing.hpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,13 @@ constexpr fq12 final_exponentiation_tricky_part(const fq12& elt);
195195
// ======================
196196
// Pairing
197197
//
198-
// NOTE: All points supplied for pairing calculations are checked to be on the curve. This is equivalent to a subgroup
199-
// membership check for points in G1 = BN254. We don't implement subgroup membership checks for G2 because the only
200-
// place in the codebase where we use pairings is in PairingPoints::check(), which takes two points P1, P2 in G1
201-
// and checks e(P1, [1]) * e(P2, [x]) = 1. The points [1] and [x] are taken from the SRS, so we know they belong to G2.
198+
// NOTE: All points supplied for pairing calculations are checked to be on the curve. For G1 = BN254 this is equivalent
199+
// to a subgroup membership check (cofactor 1). For G2 = BN254 twist, on-curve does NOT imply membership in the
200+
// prime-order subgroup because the cofactor is non-trivial. The two G2 points consumed inside pairings come from
201+
// PairingPoints::check(), which feeds them the SRS values [1]_2 and [x]_2 — the SRS ingress (bbapi::SrsInitSrs)
202+
// invokes `affine_element::is_in_prime_subgroup()` on the user-supplied [x]_2 before installation, so by the time
203+
// they reach this file they are already known to be in the prime-order subgroup. Any future code path that brings a
204+
// new G2 point in from outside the SRS must run the subgroup check itself.
202205
//
203206
// ======================
204207

barretenberg/cpp/src/barretenberg/ecc/groups/affine_element.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ template <typename Fq_, typename Fr_, typename Params_> class alignas(64) affine
9292

9393
[[nodiscard]] constexpr bool on_curve() const noexcept;
9494

95+
/**
96+
* @brief Check that the point lies in the prime-order subgroup of size `Fr::modulus`.
97+
*
98+
* @details For curves whose cofactor is 1 (e.g. BN254 G1, Grumpkin) every on-curve point trivially
99+
* satisfies this, so callers that already validated `on_curve()` can skip the call. For curves with
100+
* non-trivial cofactor (notably BN254 G2, with cofactor h2 ≈ 2^254), `on_curve()` alone is
101+
* insufficient: an attacker can supply a curve point that lies in a small subgroup of order
102+
* dividing h2 and pass `on_curve()`. This routine performs the full subgroup check via `[r]·P == ∞`.
103+
*
104+
* @note Not constant-time. Intended for one-shot validation of public, externally-supplied points
105+
* (e.g. at the bbapi boundary or when loading SRS bytes).
106+
*
107+
* @return true iff `*this` is the point at infinity or has order dividing `Fr::modulus`.
108+
*/
109+
[[nodiscard]] bool is_in_prime_subgroup() const noexcept;
110+
95111
static constexpr std::optional<affine_element> derive_from_x_coordinate(const Fq& x, bool sign_bit) noexcept;
96112

97113
/**

barretenberg/cpp/src/barretenberg/ecc/groups/affine_element.test.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,19 @@ template <typename G1> class TestAffineElement : public testing::Test {
349349
auto reconstructed = FrCodec::deserialize_from_fields<affine_element>(limbs);
350350
EXPECT_EQ(reconstructed, point);
351351
}
352+
353+
// The point at infinity, the generator, and any scalar multiple of the generator must all be
354+
// recognized as members of the prime-order subgroup.
355+
static void test_is_in_prime_subgroup_accepts_subgroup_points()
356+
{
357+
EXPECT_TRUE(affine_element::infinity().is_in_prime_subgroup());
358+
EXPECT_TRUE(affine_element::one().is_in_prime_subgroup());
359+
360+
for (size_t i = 0; i < 8; ++i) {
361+
affine_element P = affine_element(element::random_element());
362+
EXPECT_TRUE(P.is_in_prime_subgroup());
363+
}
364+
}
352365
};
353366

354367
// using TestTypes = testing::Types<bb::g1>;
@@ -539,6 +552,12 @@ TYPED_TEST(TestAffineElement, DeserializeOffCurveThrows)
539552
TestFixture::test_deserialize_off_curve_throws();
540553
}
541554

555+
// Verify is_in_prime_subgroup accepts known prime-order subgroup points
556+
TYPED_TEST(TestAffineElement, IsInPrimeSubgroupAcceptsSubgroupPoints)
557+
{
558+
TestFixture::test_is_in_prime_subgroup_accepts_subgroup_points();
559+
}
560+
542561
// Verify that from_compressed returns the (0,0) sentinel for x values with no valid y.
543562
TYPED_TEST(TestAffineElement, PointCompressionInvalidX)
544563
{

barretenberg/cpp/src/barretenberg/ecc/groups/affine_element_impl.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,29 @@ template <class Fq, class Fr, class T> constexpr bool affine_element<Fq, Fr, T>:
135135
return (xxx == yy);
136136
}
137137

138+
template <class Fq, class Fr, class T> bool affine_element<Fq, Fr, T>::is_in_prime_subgroup() const noexcept
139+
{
140+
if (is_point_at_infinity()) {
141+
return true;
142+
}
143+
using Element = element<Fq, Fr, T>;
144+
145+
// To compute r * P, we convert modulus r to u256 and perform a left-to-right double-and-add.
146+
constexpr uint256_t r = Fr::modulus;
147+
const uint64_t r_msb = r.get_msb();
148+
149+
// Left-to-right double-and-add over the bits of r below the MSB. The MSB itself is consumed by
150+
// initializing `acc` with `*this`. Loop terminates via unsigned underflow (i wraps past 0).
151+
Element acc(*this);
152+
for (uint64_t i = r_msb - 1; i < r_msb; --i) {
153+
acc.self_dbl();
154+
if (r.get_bit(i)) {
155+
acc += *this;
156+
}
157+
}
158+
return acc.is_point_at_infinity();
159+
}
160+
138161
template <class Fq, class Fr, class T>
139162
constexpr bool affine_element<Fq, Fr, T>::operator==(const affine_element& other) const noexcept
140163
{

barretenberg/cpp/src/barretenberg/ecc/groups/element.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ namespace bb::group_elements {
2222
* @brief element class. Implements ecc group arithmetic using Jacobian coordinates
2323
* See https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l
2424
*
25-
* Note: Currently subgroup checks are NOT IMPLEMENTED
26-
* Our current implementation uses G1 points that have a cofactor of 1.
27-
* All G2 points are precomputed (generator [1]_2 and trusted setup point [x]_2).
28-
* Explicitly assume precomputed points are valid members of the prime-order subgroup for G2.
25+
* Note: BN254 / Grumpkin G1 have cofactor 1, so on-curve membership coincides with prime-order
26+
* subgroup membership. BN254 G2 has a non-trivial cofactor; an explicit subgroup check is provided
27+
* by `affine_element::is_in_prime_subgroup()` and must be applied to externally-supplied G2 bytes
28+
* (see bbapi). The arithmetic in this file does not rederive subgroup membership and assumes the
29+
* caller already ensured operands are valid prime-order subgroup elements.
30+
*
2931
* @tparam Fq prime field the curve is defined over
3032
* @tparam Fr prime field whose characteristic equals the size of the prime-order elliptic curve subgroup
3133
* @tparam Params curve parameters

barretenberg/cpp/src/barretenberg/ecc/groups/group.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ namespace bb {
2424
* @brief group class. Represents an elliptic curve group element.
2525
* Group is parametrised by Fq and Fr
2626
*
27-
* Note: Currently subgroup checks are NOT IMPLEMENTED
28-
* Our current implementation uses G1 points that have a cofactor of 1.
29-
* All G2 points are precomputed (generator [1]_2 and trusted setup point [x]_2).
30-
* Explicitly assume precomputed points are valid members of the prime-order subgroup for G2.
27+
* Note: BN254 / Grumpkin G1 have cofactor 1, so `affine_element::on_curve()` is itself a subgroup
28+
* check. BN254 G2 has a non-trivial cofactor, so callers that accept externally-supplied G2 bytes
29+
* must additionally invoke `affine_element::is_in_prime_subgroup()` to reject cofactor-subgroup
30+
* points before they reach pairing-based verifiers; routine internal G2 arithmetic stays inside
31+
* the prime-order subgroup because every starting point is the precomputed generator [1]_2 or the
32+
* SRS point [x]_2.
3133
*
3234
* @tparam Fq
3335
* @tparam subgroup_field

barretenberg/cpp/src/barretenberg/srs/factories/crs_factory.test.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ TEST(CrsFactory, DISABLED_Bn254Fallback)
123123
fs::remove_all(temp_crs_path);
124124
}
125125

126+
// The hardcoded `[x]_2` baked into the BB native binary must be a member of the BN254 G2
127+
// prime-order subgroup. Pinning this in a test catches accidental corruption in any future change
128+
// to `bn254_crs_data.hpp` (or to the deserialization path).
129+
TEST(CrsFactory, Bn254HardcodedG2IsInPrimeSubgroup)
130+
{
131+
auto g2_point = bb::srs::get_bn254_g2_crs_element();
132+
ASSERT_TRUE(g2_point.on_curve());
133+
EXPECT_TRUE(g2_point.is_in_prime_subgroup());
134+
}
135+
126136
TEST(CrsFactory, Bn254CompressedChunkHashFirstChunk)
127137
{
128138
// Download the first chunk of compressed CRS from CDN and verify its hash.

0 commit comments

Comments
 (0)