Check the BLS pairing result before reading it - #121
Conversation
bls_pop_verify and bls_signature_verify call bls_pairing and then compare its output buffer against GT_ONE without checking the return code. The host rejects a point that is not on the curve and returns failure without writing that buffer, so both functions compared whatever the stack slot already held -- a key the host refused to deserialize was accepted or rejected by accident rather than by decision. Check the return code and fail closed, and value-initialize the buffers both functions pass in.
huangminghuang
left a comment
There was a problem hiding this comment.
I found two issues to address before approval:
-
[P2] Valid native verification now deterministically fails. The new checks at
libraries/sysiolib/core/sysio/crypto_bls_ext.hpp:483and:503expose a bad native trampoline:libraries/native/intrinsics.cpp:764forwardsg1_points, g1_points_lenas both pairing operands instead of forwardingg2_points, g2_points_lenfor the second operand. For these helpers (n == 2), the standard host receives a 192-byte G2 span where 384 bytes are required, returnsfailure, and both valid verification APIs returnfalse. Please fix the trampoline as part of this change. -
[P2] The new failure branches have no regression coverage. Existing BLS integration tests exercise only valid PoP/signature inputs, and PR CI does not run that integration suite. Please add malformed/off-curve cases for both
bls_pop_verifyandbls_signature_verify, plus a native valid-input regression that catches the trampoline issue. This is especially important for a fix whose purpose is deterministic failure handling.
The native bls_pairing trampoline forwarded g1_points for both operands, discarding g2_points and g2_points_len. Every caller that pairs two points -- bls_pop_verify and bls_signature_verify -- handed the host 192 bytes of G2 where 384 are required, so the host rejected the call and native verification of a valid key failed. Checking the pairing return code is what made that visible: before, the rejection was hidden behind the uninitialized comparison. The new unit test registers a stub pairing intrinsic and pins both contracts. It asserts the operand spans the trampoline forwards, and its failure stub deliberately writes a result that would compare equal, so it passes only if the return code is actually read.
|
Both confirmed and addressed in 102f208. 1. Trampoline. Verified — One refinement to the framing: valid native verification failed before this PR too. The host sees 192 bytes where 2. Coverage. Added
Each half of the change is independently caught by it:
Both defects are inherited from upstream — |
huangminghuang
left a comment
There was a problem hiding this comment.
One remaining test gap before approval:
[P2] Assert the G2 operand itself, not only its length. The pairing stub at tests/unit/crypto_ext_tests.cpp:118 discards both operand pointers and records only their lengths. A trampoline that forwarded (g1_points, g1_points_len, g1_points, g2_points_len, ...) would pass every new assertion while presenting the host with a 384-byte span starting at a 192-byte G1 object. Please give proof distinctive bytes and assert that the second operand contains them for both helpers, or otherwise assert the G1 and G2 pointers are distinct. That makes the regression test pin both parts of the original forwarding defect.
The stub recorded operand lengths only, so a trampoline forwarding the G1 pointer with the G2 length satisfied every assertion while handing the host a 384-byte span over a 192-byte object. Fill the key and the proof with distinct bytes and assert that the second G1 point carries the key and the first G2 point carries the proof.
|
Good catch — the length assertions alone did not pin it. Addressed in 8e22f1c.
Verified against the exact mutant you described, a trampoline forwarding
So the content assertion is load-bearing for exactly the case the lengths miss. |
huangminghuang
left a comment
There was a problem hiding this comment.
Re-reviewed the full current diff at 8e22f1c5. The native trampoline fix, fail-closed return handling, and regression coverage now address the prior findings. The content assertions pin the forwarded G1/G2 operands as well as their lengths for both verification helpers.
Two defects in the BLS support, which compound: the second hid the first.
The pairing result was read without checking it
bls_pop_verifyandbls_signature_verifycalledbls_pairingand compared its output buffer againstGT_ONE, without checking the return code:The host deserializes both operands with validity checking, and when a point is not on the curve it returns
return_code::failurewithout writingres. On that path both functions compared whatever the buffer already held. A key the host explicitly refused to deserialize was accepted or rejected by accident rather than by decision, and the buffer was read uninitialized.Both now check the return code and fail closed, and value-initialize the buffers they pass in.
bls_signature_verifyalso left its two point arrays uninitialized, unlikebls_pop_verifyimmediately above it; they are initialized the same way now. The success path is unchanged — when the pairing succeeds it returns 0 and writesresin full, so the comparison is exactly what it was.The native pairing trampoline dropped its G2 operand
libraries/native/intrinsics.cppforwardedg1_points, g1_points_lenfor both operands, acceptingg2_pointsandg2_points_lenand silently discarding them. Both helpers pair two points, so the host received 192 bytes of G2 where 384 are required and rejected the call — meaning native verification of a perfectly valid key failed.That failure was invisible precisely because of the first defect: it surfaced as an ordinary verification failure rather than a rejected call. Checking the return code is what exposed it.
Coverage
bls_verify_pairing_contract_testintests/unit/crypto_ext_tests.cpp, which ctest runs in PR CI. It registers a stub pairing intrinsic viaintrinsics::set_intrinsic<>— the patterncrypto_tests.cppalready uses — so it needs no bls12-381 implementation, and pins both contracts:Each half of the change is independently caught by it. With the fixed header and the unfixed trampoline it fails on the two
g2_lenassertions; with the unfixed header and the fixed trampoline it fails on the twoverify == falseassertions; with both fixed it passes.Provenance
Both defects are inherited from upstream —
AntelopeIO/cdtmain carries the identical trampoline line and the identical uncheckedbls_gt r;. Reported there as AntelopeIO/cdt#389.Why it matters here
bls_pop_verifygates finalizer key registration insysio.system. An off-curve key reaching the finalizers table makesset_finalizersthrow while deserializing it — and the producer schedule rebuild callsset_finalizersfrom insideonblock, where a throw rolls back the rebuild and repeats on every subsequent block. Wire-Network/wire-sysio#632 closes that door on the contract side with an explicit validity check at registration; this makes the CDT primitive answer the question deliberately rather than by luck.