Skip to content

fix: enforce BLS spec KeyValidate and normalize error handling#123

Open
UdjinM6 wants to merge 6 commits into
dashpay:developfrom
UdjinM6:enf_bls_spec
Open

fix: enforce BLS spec KeyValidate and normalize error handling#123
UdjinM6 wants to merge 6 commits into
dashpay:developfrom
UdjinM6:enf_bls_spec

Conversation

@UdjinM6

@UdjinM6 UdjinM6 commented Mar 5, 2026

Copy link
Copy Markdown

Per draft-irtf-cfrg-bls-signature-05, KeyValidate (section 2.5) must reject the identity element and CoreVerify (section 2.7) must call KeyValidate before pairing.

  • Reject identity public keys in CoreMPL::Verify and CoreMPL::AggregateVerify per KeyValidate (section 2.5), which CoreVerify (section 2.7) and CoreAggregateVerify (section 2.9) require before pairing; the branch also rejects identity signatures there as additional hardening.
  • Add IsValid() and identity checks to CoreMPL::VerifySecure and LegacySchemeMPL::Verify/AggregateVerify for defense-in-depth, matching the hardened core verification path.
  • Override VerifySecure in AugSchemeMPL to verify T-weighted individual public keys against per-signer augmented messages, since Aug signs H(pk_i || msg) per signer and collapsing to one combined public key would lose that augmentation.
  • Make these byte-based verification paths return false on malformed input instead of throwing: CoreMPL::Verify, CoreMPL::AggregateVerify, BasicSchemeMPL::AggregateVerify(Bytes...), AugSchemeMPL::AggregateVerify(Bytes...), PopSchemeMPL::PopVerify(Bytes...), and PopSchemeMPL::FastAggregateVerify(Bytes...).
  • Add identity-element rejection to PopSchemeMPL::PopVerify(G1Element, G2Element) in line with PopVerify (section 3.3.3) calling KeyValidate(PK), and move validation before hashing for early exit.
  • Add per-public-key validity and identity checks to PopSchemeMPL::FastAggregateVerify; this enforces the non-identity public-key expectation implied by the PopVerify precondition behind section 3.3.4, even though FastAggregateVerify itself delegates to CoreVerify after aggregating.

Summary by CodeRabbit

  • New Features

    • Added a public secure aggregate verification overload for simplified single-call verification.
  • Bug Fixes

    • Stronger input validation: default/identity keys and invalid signatures are rejected.
    • Byte parsing now fails gracefully (returns false) instead of throwing.
    • Hardened aggregation/verification flows with defensive pre-checks.
  • Tests

    • Extensive tests for malformed inputs, identity-key rejection, and aggregation edge cases.

Per draft-irtf-cfrg-bls-signature-05, KeyValidate must reject the
identity element and CoreVerify must call KeyValidate before pairing.

- Reject identity public keys and signatures in CoreMPL::Verify,
  CoreMPL::AggregateVerify, and CoreMPL::VerifySecure per spec
  KeyValidate: "If xP is the identity element, return INVALID"
- Add IsValid() and identity checks to LegacySchemeMPL::Verify and
  LegacySchemeMPL::AggregateVerify, matching CoreMPL::Verify
- Override VerifySecure in AugSchemeMPL to call CoreMPL::AggregateVerify
  directly, preventing double-augmentation from virtual dispatch
- Make PopSchemeMPL::PopVerify(Bytes, Bytes) return false on invalid
  input instead of throwing, per spec PopVerify returns VALID/INVALID

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d5758aa-1492-4947-bf0d-924e99ded2f4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a public AugSchemeMPL::VerifySecure overload and extensive hardening of verification and aggregation paths: strict input validation, exception-safe deserialization, per-key guards, normalized/weighted key aggregation, and expanded tests to cover invalid/default key and signature handling.

Changes

Cohort / File(s) Summary
Public API
include/dashbls/schemes.hpp
Added public declaration: bool AugSchemeMPL::VerifySecure(const std::vector<G1Element>& vecPublicKeys, const G2Element& signature, const Bytes& message) override.
Core verification & aggregation logic
src/schemes.cpp
Widespread changes: added try/catch around deserialization, reject default/zero G1/G2 elements and invalid signatures, added public AugSchemeMPL::VerifySecure implementation, introduced safe byte→element parsing and early-false returns, normalized/sorted key weighting and delegation to aggregate verification, and adjusted multiple scheme verification entry points for defensive checks.
Tests
src/test.cpp
Large additions: tests asserting rejection of identity/default pubkeys and signatures, bytes-overload parse-failure handling (return false), expanded coverage for Verify/VerifySecure/PopVerify/FastAggregateVerify/AggregateVerify and malformed-input scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as Client
  participant Aug as AugSchemeMPL
  participant Normalizer as Hasher/Normalizer
  participant Core as CoreMPL

  Client->>Aug: VerifySecure(vecPubKeys, signature, message)
  Aug->>Normalizer: serialize, sort, compute weights/t-factors
  Normalizer-->>Aug: weighted keys & t-factors
  Aug->>Aug: build weightedPubKeys & augmented messages
  Aug->>Core: AggregateVerify(weightedPubKeys, signature, messages)
  Core->>Core: deserialize & validate pubkeys/signature (reject defaults)
  Core-->>Aug: verification result (true/false)
  Aug-->>Client: return result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled bytes and checked each key,
I hopped through sorts and weights with glee,
I caught the defaults, tossed them from the burrow,
Aggregates clean, no hiccup to harrow,
Tests nibble bugs — the branch now free.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: enforce BLS spec KeyValidate and normalize error handling' directly reflects the main changes: enforcing BLS specification KeyValidate behavior and standardizing error handling across verification methods.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

@PastaPastaPasta

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/schemes.cpp (1)

235-242: Consider adding IsValid() checks for consistency with other verification methods.

The identity checks are correct, but CoreMPL::Verify and CoreMPL::AggregateVerify also check IsValid() before the identity check. While the input G1Element objects should already be valid (since FromBytes throws on invalid points), adding IsValid() checks here would provide defense-in-depth and maintain consistency across all verification entry points.

🔧 Suggested change for consistency
 bool CoreMPL::VerifySecure(const std::vector<G1Element>& vecPublicKeys,
                            const G2Element& signature,
                            const Bytes& message,
                            const bool fLegacy) {
     for (const auto& pk : vecPublicKeys) {
-        if (pk == G1Element()) {
+        if (!pk.IsValid() || pk == G1Element()) {
             return false;
         }
     }
-    if (signature == G2Element()) {
+    if (!signature.IsValid() || signature == G2Element()) {
         return false;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemes.cpp` around lines 235 - 242, Add explicit IsValid() checks for
the public keys and signature before the identity comparisons: iterate
vecPublicKeys and for each pk call pk.IsValid() (return false if any invalid)
prior to comparing against G1Element(), and call signature.IsValid() (return
false if invalid) before comparing to G2Element(); this mirrors
CoreMPL::Verify/CoreMPL::AggregateVerify behavior and provides defense-in-depth
while keeping the existing identity checks (symbols: vecPublicKeys, pk,
signature).
src/test.cpp (1)

1634-1663: Consider adding test coverage for AugSchemeMPL::VerifySecure.

The tests validate BasicSchemeMPL::VerifySecure but the PR specifically adds an override in AugSchemeMPL::VerifySecure to prevent double-augmentation. A test verifying that AugSchemeMPL::VerifySecure works correctly (and doesn't double-augment) would strengthen coverage for this key change.

💡 Suggested test addition
SECTION("AugSchemeMPL VerifySecure round-trips correctly")
{
    auto sk1 = AugSchemeMPL().KeyGen(getRandomSeed());
    auto sk2 = AugSchemeMPL().KeyGen(getRandomSeed());
    auto pk1 = sk1.GetG1Element();
    auto pk2 = sk2.GetG1Element();
    auto msg = getRandomSeed();

    auto sig1 = AugSchemeMPL().Sign(sk1, msg);
    auto sig2 = AugSchemeMPL().Sign(sk2, msg);

    vector<G1Element> pks = {pk1, pk2};
    vector<G2Element> sigs = {sig1, sig2};
    auto aggSig = AugSchemeMPL().AggregateSecure(pks, sigs, Bytes(msg));

    // This should work without double-augmentation
    REQUIRE(AugSchemeMPL().VerifySecure(pks, aggSig, Bytes(msg)));
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test.cpp` around lines 1634 - 1663, Add a new test SECTION mirroring the
BasicScheme case but using AugSchemeMPL to ensure AugSchemeMPL::VerifySecure
does not double-augment: generate two keys with
AugSchemeMPL().KeyGen(getRandomSeed()), get their G1Element() pubkeys, sign the
same message with AugSchemeMPL().Sign, build vectors of G1Element and G2Element,
create an aggregate signature with AugSchemeMPL().AggregateSecure(pks, sigs,
Bytes(msg)), then assert REQUIRE(AugSchemeMPL().VerifySecure(pks, aggSig,
Bytes(msg))); place this with the existing VerifySecure tests so it covers the
override in AugSchemeMPL::VerifySecure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/schemes.cpp`:
- Around line 235-242: Add explicit IsValid() checks for the public keys and
signature before the identity comparisons: iterate vecPublicKeys and for each pk
call pk.IsValid() (return false if any invalid) prior to comparing against
G1Element(), and call signature.IsValid() (return false if invalid) before
comparing to G2Element(); this mirrors CoreMPL::Verify/CoreMPL::AggregateVerify
behavior and provides defense-in-depth while keeping the existing identity
checks (symbols: vecPublicKeys, pk, signature).

In `@src/test.cpp`:
- Around line 1634-1663: Add a new test SECTION mirroring the BasicScheme case
but using AugSchemeMPL to ensure AugSchemeMPL::VerifySecure does not
double-augment: generate two keys with AugSchemeMPL().KeyGen(getRandomSeed()),
get their G1Element() pubkeys, sign the same message with AugSchemeMPL().Sign,
build vectors of G1Element and G2Element, create an aggregate signature with
AugSchemeMPL().AggregateSecure(pks, sigs, Bytes(msg)), then assert
REQUIRE(AugSchemeMPL().VerifySecure(pks, aggSig, Bytes(msg))); place this with
the existing VerifySecure tests so it covers the override in
AugSchemeMPL::VerifySecure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 809086fc-aba6-4df3-bdcc-05f138cc6842

📥 Commits

Reviewing files that changed from the base of the PR and between a89fd49 and 3481d0e.

📒 Files selected for processing (3)
  • include/dashbls/schemes.hpp
  • src/schemes.cpp
  • src/test.cpp

UdjinM6 and others added 2 commits March 10, 2026 17:27
CoreMPL::VerifySecure and AugSchemeMPL::VerifySecure only checked for
identity elements but not IsValid(), unlike CoreMPL::Verify and
CoreMPL::AggregateVerify which check both. Add the missing IsValid()
checks for consistency and early rejection of invalid elements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…essages

AugSchemeMPL::Sign hashes H(pk_i || msg) per signer, so VerifySecure
cannot combine pubkeys into a single key (the augmentation prefix
differs per signer). Pass T-weighted individual pubkeys with their
per-signer augmented messages to CoreMPL::AggregateVerify instead.

Add AugSchemeMPL VerifySecure round-trip test to exercise this path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@UdjinM6

UdjinM6 commented Mar 10, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/test.cpp (1)

1685-1690: Split the invalid PopVerify bytes case by failure mode.

Using a bad pubkey and a bad proof in the same assertion only proves that some parse failure returns false. Separate badPk + validProof and validPk + badProof cases would lock in both error-handling branches individually.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test.cpp` around lines 1685 - 1690, Split the combined-failure test into
two focused cases: one that supplies a malformed pubkey with a valid proof, and
one that supplies a valid pubkey with a malformed proof. For the first SECTION
(e.g. "Invalid pubkey returns false instead of throwing") construct badPk of
length G1Element::SIZE filled with 0xff and obtain a known-good proof bytes
(from a fixture or by generating a proof with PopSchemeMPL::PopProve/valid
keypair) then assert REQUIRE_NOTHROW(PopSchemeMPL().PopVerify(Bytes(badPk),
Bytes(validProof))) and REQUIRE(...PopVerify(...) == false). For the second
SECTION (e.g. "Invalid proof returns false instead of throwing") construct
badProof of length G2Element::SIZE filled with 0xff and use a known-good pubkey
bytes (from a fixture/valid keypair) then assert
REQUIRE_NOTHROW(PopSchemeMPL().PopVerify(Bytes(validPk), Bytes(badProof))) and
REQUIRE(...PopVerify(...) == false). Ensure you reference
PopSchemeMPL::PopVerify, G1Element::SIZE, G2Element::SIZE and Bytes when
locating the test to edit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test.cpp`:
- Around line 1634-1681: Add failing-case tests to cover
AugSchemeMPL::VerifySecure and identity-signature rejection: extend the
TEST_CASE to include a SECTION that mirrors "VerifySecure rejects identity
pubkey" but calls AugSchemeMPL().VerifySecure(pks, sig, Bytes(msg)) and asserts
false, and add another SECTION that constructs a valid public key list and
passes an identity G2Element() as the signature (for both
BasicSchemeMPL().VerifySecure and AugSchemeMPL().VerifySecure) asserting the
call returns false; reference AggregateSecure/VerifySecure and the
AugSchemeMPL/BasicSchemeMPL classes to locate where to add these checks.
- Around line 1595-1632: Update the LegacySchemeMPL tests to also cover the new
!IsValid() paths by creating malformed (non-identity) G1Element and G2Element
values via FromBytesUnchecked(..., true) and using those in the existing Verify
and AggregateVerify sections (in addition to the default-constructed identity
cases); specifically, call LegacySchemeMPL::Verify with a malformed G1Element
and with a malformed G2Element and assert false, and call
LegacySchemeMPL::AggregateVerify with malformed entries in the pks or sig
parameter and assert false, reusing the existing legacy invalid fixtures present
later in this file to construct the invalid elements.

---

Nitpick comments:
In `@src/test.cpp`:
- Around line 1685-1690: Split the combined-failure test into two focused cases:
one that supplies a malformed pubkey with a valid proof, and one that supplies a
valid pubkey with a malformed proof. For the first SECTION (e.g. "Invalid pubkey
returns false instead of throwing") construct badPk of length G1Element::SIZE
filled with 0xff and obtain a known-good proof bytes (from a fixture or by
generating a proof with PopSchemeMPL::PopProve/valid keypair) then assert
REQUIRE_NOTHROW(PopSchemeMPL().PopVerify(Bytes(badPk), Bytes(validProof))) and
REQUIRE(...PopVerify(...) == false). For the second SECTION (e.g. "Invalid proof
returns false instead of throwing") construct badProof of length G2Element::SIZE
filled with 0xff and use a known-good pubkey bytes (from a fixture/valid
keypair) then assert REQUIRE_NOTHROW(PopSchemeMPL().PopVerify(Bytes(validPk),
Bytes(badProof))) and REQUIRE(...PopVerify(...) == false). Ensure you reference
PopSchemeMPL::PopVerify, G1Element::SIZE, G2Element::SIZE and Bytes when
locating the test to edit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d53e702a-fd79-45b6-a423-ba0808207efc

📥 Commits

Reviewing files that changed from the base of the PR and between 3481d0e and 2bc4320.

📒 Files selected for processing (2)
  • src/schemes.cpp
  • src/test.cpp
✅ Files skipped from review due to trivial changes (1)
  • src/schemes.cpp

Comment thread src/test.cpp
Comment thread src/test.cpp
- Add LegacySchemeMPL tests for malformed non-identity elements using
  FromBytesUnchecked to exercise IsValid() guards in Verify and
  AggregateVerify
- Add AugSchemeMPL VerifySecure rejection tests for identity pubkeys
  and identity signatures
- Add BasicSchemeMPL VerifySecure rejection test for identity signatures
- Split PopVerify combined parse-failure test into separate malformed
  pubkey and malformed proof cases for independent branch coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@UdjinM6

UdjinM6 commented Mar 10, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

1 similar comment
@UdjinM6

UdjinM6 commented Mar 11, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@PastaPastaPasta PastaPastaPasta left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK: 509bf2f

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/schemes.cpp (1)

388-426: ⚠️ Potential issue | 🟠 Major

Route the vector<uint8_t> overload through the guarded bytes path.

Line 393 still deserializes signature with G2Element::FromByteVector(signature) before delegation, so BasicSchemeMPL::AggregateVerify(vector<vector<uint8_t>>...) can still throw on malformed input while the new Bytes overload returns false. That leaves the public API inconsistent for the same operation.

♻️ Suggested fix
 bool BasicSchemeMPL::AggregateVerify(const vector<vector<uint8_t>> &pubkeys,
                                      const vector<vector<uint8_t>> &messages,
                                      const vector<uint8_t> &signature)
 {
-    const size_t nPubKeys = pubkeys.size();
-    auto arg_check = VerifyAggregateSignatureArguments(nPubKeys, messages.size(), G2Element::FromByteVector(signature));
-    if (arg_check != CONTINUE) {
-        return arg_check;
-    }
-
-    const std::set<vector<uint8_t>> setMessages(messages.begin(), messages.end());
-    if (setMessages.size() != nPubKeys) {
-        return false;
-    }
-    return CoreMPL::AggregateVerify(pubkeys, messages, signature);
+    const std::vector<Bytes> vecPubKeyBytes(pubkeys.begin(), pubkeys.end());
+    const std::vector<Bytes> vecMessagesBytes(messages.begin(), messages.end());
+    return BasicSchemeMPL::AggregateVerify(vecPubKeyBytes, vecMessagesBytes, Bytes(signature));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemes.cpp` around lines 388 - 426, The vector<uint8_t> overload
BasicSchemeMPL::AggregateVerify should not directly call
G2Element::FromByteVector(signature); instead convert the signature vector into
a Bytes (or a Bytes-compatible wrapper) and delegate to the Bytes overload
BasicSchemeMPL::AggregateVerify(const vector<Bytes>&, const vector<Bytes>&,
const Bytes&) to reuse its guarded deserialization (G2Element::FromBytes) and
argument checking; update the function to build a Bytes from the incoming
vector<uint8_t> (and similarly ensure messages/pubkeys are converted to the
expected types or forwarded unchanged) and return the result of
CoreMPL::AggregateVerify only via the Bytes path so malformed signatures return
false consistently.
♻️ Duplicate comments (1)
src/test.cpp (1)

1595-1676: ⚠️ Potential issue | 🟡 Minor

Add malformed aggregate-signature coverage for LegacySchemeMPL::AggregateVerify.

The new sections pin identity signatures and malformed pubkeys, but the non-identity !signature.IsValid() branch in LegacySchemeMPL::AggregateVerify is still untested. A malformed G2Element in the aggregate path would close the remaining gap.

➕ Suggested test addition
+    SECTION("AggregateVerify rejects malformed non-identity signature")
+    {
+        g2_t point_native;
+        g2_set_infty(point_native);
+        fp2_rand(point_native->x);
+        fp2_rand(point_native->y);
+        fp2_rand(point_native->z);
+        G2Element badSig = G2Element::FromNative(point_native);
+        REQUIRE(badSig.IsValid() == false);
+
+        auto sk = BasicSchemeMPL().KeyGen(getRandomSeed());
+        auto pk = sk.GetG1Element();
+        auto msg = getRandomSeed();
+        vector<G1Element> pks = {pk};
+        vector<Bytes> msgs = {Bytes(msg)};
+        REQUIRE(LegacySchemeMPL().AggregateVerify(pks, msgs, badSig) == false);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test.cpp` around lines 1595 - 1676, Add a new SECTION that constructs a
malformed non-identity G2Element (e.g. using native point creation like in the
"Verify rejects malformed non-identity signature" test: set g2_t to non-infinity
and fp2_rand on coordinates, then wrap with G2Element::FromNative and assert
badSig.IsValid() == false), then create a valid pk via
BasicSchemeMPL().KeyGen()->GetG1Element(), put that pk and a message into
vectors and call LegacySchemeMPL().AggregateVerify(pks, msgs, badSig) and
REQUIRE the result is false; this exercises the non-identity
!signature.IsValid() branch in LegacySchemeMPL::AggregateVerify.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/schemes.cpp`:
- Around line 388-426: The vector<uint8_t> overload
BasicSchemeMPL::AggregateVerify should not directly call
G2Element::FromByteVector(signature); instead convert the signature vector into
a Bytes (or a Bytes-compatible wrapper) and delegate to the Bytes overload
BasicSchemeMPL::AggregateVerify(const vector<Bytes>&, const vector<Bytes>&,
const Bytes&) to reuse its guarded deserialization (G2Element::FromBytes) and
argument checking; update the function to build a Bytes from the incoming
vector<uint8_t> (and similarly ensure messages/pubkeys are converted to the
expected types or forwarded unchanged) and return the result of
CoreMPL::AggregateVerify only via the Bytes path so malformed signatures return
false consistently.

---

Duplicate comments:
In `@src/test.cpp`:
- Around line 1595-1676: Add a new SECTION that constructs a malformed
non-identity G2Element (e.g. using native point creation like in the "Verify
rejects malformed non-identity signature" test: set g2_t to non-infinity and
fp2_rand on coordinates, then wrap with G2Element::FromNative and assert
badSig.IsValid() == false), then create a valid pk via
BasicSchemeMPL().KeyGen()->GetG1Element(), put that pk and a message into
vectors and call LegacySchemeMPL().AggregateVerify(pks, msgs, badSig) and
REQUIRE the result is false; this exercises the non-identity
!signature.IsValid() branch in LegacySchemeMPL::AggregateVerify.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31c7e839-ddb0-48ef-9c17-a9655f43d4be

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc4320 and 509bf2f.

📒 Files selected for processing (2)
  • src/schemes.cpp
  • src/test.cpp

@UdjinM6

UdjinM6 commented Mar 11, 2026

Copy link
Copy Markdown
Author

Addressed #123 (review)

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

@UdjinM6 Thank you for addressing the previous review feedback! Triggering a new review now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/schemes.cpp (1)

577-578: ⚠️ Potential issue | 🟡 Minor

Signed/unsigned mismatch in loop counter.

nPubKeys is size_t but the loop counter i is declared as int. This causes a signed/unsigned comparison warning and could theoretically cause issues if the collection size exceeds INT_MAX.

🐛 Proposed fix
-    for (int i = 0; i < nPubKeys; ++i) {
+    for (size_t i = 0; i < nPubKeys; ++i) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemes.cpp` around lines 577 - 578, The loop uses a signed int counter i
against an unsigned size_t nPubKeys causing a signed/unsigned mismatch; change
the loop counter to size_t (or auto) to match nPubKeys and avoid warnings and
potential overflow—update the for loop that iterates to nPubKeys (the one
initializing augMessages) so i is declared as size_t (or use std::size_t/auto)
and ensure any uses of i within the loop are compatible.
🧹 Nitpick comments (1)
src/schemes.cpp (1)

402-418: Minor: Signature is parsed twice.

The signature is parsed at line 404, but at line 418 the raw signature Bytes is passed to CoreMPL::AggregateVerify, which will parse it again. This could be avoided by calling the element-based overload with signatureElement.

♻️ Proposed optimization
-    return CoreMPL::AggregateVerify(pubkeys, messages, signature);
+    vector<G1Element> pubkeyElements;
+    pubkeyElements.reserve(nPubKeys);
+    try {
+        for (const auto& pk : pubkeys) {
+            pubkeyElements.push_back(G1Element::FromBytes(pk));
+        }
+    } catch (...) {
+        return false;
+    }
+    return CoreMPL::AggregateVerify(pubkeyElements, messages, signatureElement);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schemes.cpp` around lines 402 - 418, The code parses the signature into a
G2Element (signatureElement) but then calls CoreMPL::AggregateVerify with the
raw signature bytes, causing a second parse; change the final call to use the
element-based overload by passing signatureElement (the parsed G2Element) to
CoreMPL::AggregateVerify (instead of the raw signature) so the already-parsed
element is reused; locate the usage around G2Element::FromBytes,
VerifyAggregateSignatureArguments, and the current CoreMPL::AggregateVerify call
to update the parameter accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/schemes.cpp`:
- Around line 577-578: The loop uses a signed int counter i against an unsigned
size_t nPubKeys causing a signed/unsigned mismatch; change the loop counter to
size_t (or auto) to match nPubKeys and avoid warnings and potential
overflow—update the for loop that iterates to nPubKeys (the one initializing
augMessages) so i is declared as size_t (or use std::size_t/auto) and ensure any
uses of i within the loop are compatible.

---

Nitpick comments:
In `@src/schemes.cpp`:
- Around line 402-418: The code parses the signature into a G2Element
(signatureElement) but then calls CoreMPL::AggregateVerify with the raw
signature bytes, causing a second parse; change the final call to use the
element-based overload by passing signatureElement (the parsed G2Element) to
CoreMPL::AggregateVerify (instead of the raw signature) so the already-parsed
element is reused; locate the usage around G2Element::FromBytes,
VerifyAggregateSignatureArguments, and the current CoreMPL::AggregateVerify call
to update the parameter accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d4c78b6-ccf6-4c96-9ba4-8b3676b45087

📥 Commits

Reviewing files that changed from the base of the PR and between 509bf2f and e802d21.

📒 Files selected for processing (2)
  • src/schemes.cpp
  • src/test.cpp

@UdjinM6

UdjinM6 commented Mar 11, 2026

Copy link
Copy Markdown
Author

402-418: Minor: Signature is parsed twice.

The signature is parsed at line 404, but at line 418 the raw signature Bytes is passed to CoreMPL::AggregateVerify, which will parse it again. This could be avoided by calling the element-based overload with signatureElement.

Should be fixed now.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

@UdjinM6 Thanks for confirming the fix! Triggering a new review now to verify the changes.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

PR correctly implements BLS spec KeyValidate with consistent identity-element rejection across all verification paths. The AugSchemeMPL::VerifySecure per-signer augmented message fix is cryptographically sound. Three minor defense-in-depth gaps remain, none exploitable.

Reviewed commit: 180a8cf

🟡 2 suggestion(s) | 💬 1 nitpick(s)

2 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/schemes.cpp`:
- [SUGGESTION] lines 695-709: FastAggregateVerify omits signature identity/validity check at entry point
  Every other native-element verification path in this PR checks `!signature.IsValid() || signature == G2Element()` before doing work. `FastAggregateVerify(vector<G1Element>, Bytes, G2Element)` skips this and proceeds directly to `CoreMPL::Aggregate(pubkeys)` — an O(n) group addition — before delegating to `CoreMPL::Verify`, which does check the signature. Adding the check early avoids unnecessary aggregation on identity/invalid signatures and keeps the defense-in-depth pattern consistent across all entry points.
- [SUGGESTION] lines 262-266: Uncaught FromBytes in CoreMPL::VerifySecure and AugSchemeMPL::VerifySecure inner loops
  Lines 264 and 630 call `G1Element::FromBytes()` on round-tripped serialized data from elements that already passed `IsValid()` and identity checks, so these should never throw in practice. However, every other `FromBytes` call in the PR's verification paths is wrapped in try-catch. Wrapping these two sites would complete the pattern and guard against hypothetical serialization round-trip bugs.

Comment thread src/schemes.cpp

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, correct incremental fix that adds early identity-signature and validity rejection to PopSchemeMPL::FastAggregateVerify, consistent with the pattern in CoreMPL::Verify (line 152) and CoreMPL::VerifySecure (line 246). No blocking issues. Two minor observations worth noting.

Reviewed commit: a78ad28

🟡 1 suggestion(s) | 💬 1 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/test.cpp`:
- [SUGGESTION] lines 1902-1909: No coverage for the !signature.IsValid() branch
  The test exercises `signature == G2Element()` but not `!signature.IsValid()`. The new guard at schemes.cpp:702 has two conditions — only the identity half is tested. A section constructing a malformed G2Element (e.g. via FromBytes with invalid data, caught and stored before IsValid is false) would exercise the other branch.

Comment thread src/test.cpp
Comment thread src/test.cpp
Add IsValid() and identity checks for the signature before aggregating
pubkeys in PopSchemeMPL::FastAggregateVerify, avoiding O(n) group
additions when the signature is already known to be invalid.

The spec (draft-irtf-cfrg-bls-signature-05) does not explicitly require
FastAggregateVerify to check the signature for identity — it delegates
to CoreVerify (section 2.7) which does. But section 2.5 (KeyValidate)
establishes the principle that identity elements are invalid. This is a
defense-in-depth optimization, not a spec deviation — it fails faster on
the same inputs that would fail anyway downstream in CoreVerify.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Trivial incremental: renames test case to broader title and adds a new SECTION exercising the !signature.IsValid() branch of FastAggregateVerify, directly addressing both findings from the prior review. The malformed-G2Element construction pattern follows existing test conventions (lines 1579-1584, 1650-1654). No issues found.

Reviewed commit: b2cfcdf

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental push addresses both prior findings (test case rename + malformed-signature branch coverage). LGTM.

@thepastaclaw

thepastaclaw commented Apr 7, 2026

Copy link
Copy Markdown

✅ Review complete (commit b2cfcdf)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

PR substantively hardens verification across schemes by rejecting identity G1/G2 elements per the BLS KeyValidate/CoreVerify spec and normalizing malformed-byte handling to return false. Two legitimate concerns remain: the broad catch (...) in bytes overloads now hides operational errors like missing BLS::Init(), and the new identity-sig rejection in VerifySecure breaks the empty round-trip with AggregateSecure. Also worth a chain-state check before legacy identity-rejection lands in a Dash Core release.

Reviewed commit: b2cfcdf

🟡 3 suggestion(s)

1 additional finding

🟡 suggestion: Legacy identity rejection is a consensus-relevant behavioral change — confirm via chain-state scan

src/schemes.cpp (lines 750-767)

Adding pubkey == G1Element() / signature == G2Element() rejection to LegacySchemeMPL::Verify and LegacySchemeMPL::AggregateVerify (line 781) is correct per the BLS spec, but LegacySchemeMPL exists specifically to keep historical Dash chain verification stable for pre-V19 data. Any historical artifact (masternode list entry, quorum commitment, IS/CL signature) that contains an identity public key or identity signature would previously verify and would now be rejected — causing a hard fork for nodes upgrading past this commit. In practice no rational signer produces identity elements and there is no known on-chain instance, but please confirm with a chain-state scan of observed BLS pubkeys/signatures before this lands in a Dash Core release. The Basic/Aug/Pop changes carry no equivalent risk because they post-date V19.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/schemes.cpp`:
- [SUGGESTION] lines 121-131: `catch (...)` in bytes overloads also swallows operational errors like missing BLS::Init()
  The new `try { ... } catch (...) { return false; }` blocks across `CoreMPL::Verify`, `CoreMPL::AggregateVerify`, `BasicSchemeMPL::AggregateVerify`, `AugSchemeMPL::AggregateVerify`, `PopSchemeMPL::PopVerify`, and `PopSchemeMPL::FastAggregateVerify` (bytes overloads) catch *every* exception, not just malformed-input ones. `G1Element::FromBytes`/`G2Element::FromBytes` call `BLS::CheckRelicErrors()`, which throws `std::runtime_error("Library not initialized properly. Call BLS::Init()")` (`src/bls.cpp:81-85`). After this PR those operational faults are indistinguishable from invalid signatures: callers see `false` and have no way to detect that the library was never initialized. Narrow the catch to the specific exception types thrown by element parsing (e.g. `std::invalid_argument`) and let `std::runtime_error`/other operational exceptions propagate so initialization bugs remain diagnosable.
- [SUGGESTION] lines 237-248: VerifySecure no longer accepts the empty aggregate that AggregateSecure still produces
  `CoreMPL::AggregateSecure({}, {}, msg)` returns `G2Element()` (identity) because `CoreMPL::Aggregate` over an empty vector yields identity. Previously `VerifySecure({}, identity, msg)` round-tripped to true through `AggregateVerify({identity_pk}, {msg}, identity_sig)` — `VerifyAggregateSignatureArguments` itself documents the empty-aggregate convention at `src/schemes.cpp:69-71` (zero pubkeys + zero messages + identity signature is GOOD). The new identity-signature rejection at lines 246-247 (and the same code path at `src/schemes.cpp:611-612` in `AugSchemeMPL::VerifySecure`) makes the same identity aggregate unverifiable, breaking the `AggregateSecure`/`VerifySecure` round-trip the public API previously supported. If empty secure aggregates are intentionally unsupported now, also reject `vecPublicKeys.empty()` in `AggregateSecure` so the contract is symmetric; otherwise the early identity-signature reject should allow the empty-pubkey case through.
- [SUGGESTION] lines 750-767: Legacy identity rejection is a consensus-relevant behavioral change — confirm via chain-state scan
  Adding `pubkey == G1Element()` / `signature == G2Element()` rejection to `LegacySchemeMPL::Verify` and `LegacySchemeMPL::AggregateVerify` (line 781) is correct per the BLS spec, but `LegacySchemeMPL` exists specifically to keep historical Dash chain verification stable for pre-V19 data. Any historical artifact (masternode list entry, quorum commitment, IS/CL signature) that contains an identity public key or identity signature would previously verify and would now be rejected — causing a hard fork for nodes upgrading past this commit. In practice no rational signer produces identity elements and there is no known on-chain instance, but please confirm with a chain-state scan of observed BLS pubkeys/signatures before this lands in a Dash Core release. The Basic/Aug/Pop changes carry no equivalent risk because they post-date V19.

Comment thread src/schemes.cpp
Comment on lines 121 to +131
bool CoreMPL::Verify(const Bytes& pubkey, const Bytes& message, const Bytes& signature)
{
return CoreMPL::Verify(G1Element::FromBytes(pubkey), message, G2Element::FromBytes(signature));
G1Element pubkeyElement;
G2Element signatureElement;
try {
pubkeyElement = G1Element::FromBytes(pubkey);
signatureElement = G2Element::FromBytes(signature);
} catch (...) {
return false;
}
return CoreMPL::Verify(pubkeyElement, message, signatureElement);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: catch (...) in bytes overloads also swallows operational errors like missing BLS::Init()

The new try { ... } catch (...) { return false; } blocks across CoreMPL::Verify, CoreMPL::AggregateVerify, BasicSchemeMPL::AggregateVerify, AugSchemeMPL::AggregateVerify, PopSchemeMPL::PopVerify, and PopSchemeMPL::FastAggregateVerify (bytes overloads) catch every exception, not just malformed-input ones. G1Element::FromBytes/G2Element::FromBytes call BLS::CheckRelicErrors(), which throws std::runtime_error("Library not initialized properly. Call BLS::Init()") (src/bls.cpp:81-85). After this PR those operational faults are indistinguishable from invalid signatures: callers see false and have no way to detect that the library was never initialized. Narrow the catch to the specific exception types thrown by element parsing (e.g. std::invalid_argument) and let std::runtime_error/other operational exceptions propagate so initialization bugs remain diagnosable.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/schemes.cpp`:
- [SUGGESTION] lines 121-131: `catch (...)` in bytes overloads also swallows operational errors like missing BLS::Init()
  The new `try { ... } catch (...) { return false; }` blocks across `CoreMPL::Verify`, `CoreMPL::AggregateVerify`, `BasicSchemeMPL::AggregateVerify`, `AugSchemeMPL::AggregateVerify`, `PopSchemeMPL::PopVerify`, and `PopSchemeMPL::FastAggregateVerify` (bytes overloads) catch *every* exception, not just malformed-input ones. `G1Element::FromBytes`/`G2Element::FromBytes` call `BLS::CheckRelicErrors()`, which throws `std::runtime_error("Library not initialized properly. Call BLS::Init()")` (`src/bls.cpp:81-85`). After this PR those operational faults are indistinguishable from invalid signatures: callers see `false` and have no way to detect that the library was never initialized. Narrow the catch to the specific exception types thrown by element parsing (e.g. `std::invalid_argument`) and let `std::runtime_error`/other operational exceptions propagate so initialization bugs remain diagnosable.

Comment thread src/schemes.cpp
Comment on lines 238 to +248
const G2Element& signature,
const Bytes& message,
const bool fLegacy) {
for (const auto& pk : vecPublicKeys) {
if (!pk.IsValid() || pk == G1Element()) {
return false;
}
}
if (!signature.IsValid() || signature == G2Element()) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: VerifySecure no longer accepts the empty aggregate that AggregateSecure still produces

CoreMPL::AggregateSecure({}, {}, msg) returns G2Element() (identity) because CoreMPL::Aggregate over an empty vector yields identity. Previously VerifySecure({}, identity, msg) round-tripped to true through AggregateVerify({identity_pk}, {msg}, identity_sig)VerifyAggregateSignatureArguments itself documents the empty-aggregate convention at src/schemes.cpp:69-71 (zero pubkeys + zero messages + identity signature is GOOD). The new identity-signature rejection at lines 246-247 (and the same code path at src/schemes.cpp:611-612 in AugSchemeMPL::VerifySecure) makes the same identity aggregate unverifiable, breaking the AggregateSecure/VerifySecure round-trip the public API previously supported. If empty secure aggregates are intentionally unsupported now, also reject vecPublicKeys.empty() in AggregateSecure so the contract is symmetric; otherwise the early identity-signature reject should allow the empty-pubkey case through.

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/schemes.cpp`:
- [SUGGESTION] lines 237-248: VerifySecure no longer accepts the empty aggregate that AggregateSecure still produces
  `CoreMPL::AggregateSecure({}, {}, msg)` returns `G2Element()` (identity) because `CoreMPL::Aggregate` over an empty vector yields identity. Previously `VerifySecure({}, identity, msg)` round-tripped to true through `AggregateVerify({identity_pk}, {msg}, identity_sig)` — `VerifyAggregateSignatureArguments` itself documents the empty-aggregate convention at `src/schemes.cpp:69-71` (zero pubkeys + zero messages + identity signature is GOOD). The new identity-signature rejection at lines 246-247 (and the same code path at `src/schemes.cpp:611-612` in `AugSchemeMPL::VerifySecure`) makes the same identity aggregate unverifiable, breaking the `AggregateSecure`/`VerifySecure` round-trip the public API previously supported. If empty secure aggregates are intentionally unsupported now, also reject `vecPublicKeys.empty()` in `AggregateSecure` so the contract is symmetric; otherwise the early identity-signature reject should allow the empty-pubkey case through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants