ssa.batch_verify_ answers True for a signature ssa.verify_ answers
False for, and that Sig.assert_valid refuses outright:
genuine signatures:
verify_ #1 : True
verify_ #2 : True
batch_verify_: True
same second signature with s += n, i.e. s not in 0..n-1:
assert_valid : REFUSED -> scalar s not in 0..n-1: '01 3DE30B4C ...'
verify_ : False
batch_verify_: True
Batch verification is meant to answer what verifying each signature
separately answers. Here it does not, and the two disagree about a
signature BIP340 calls invalid.
Why
assert_batch_as_valid_ reads sig.r, sig.s and sig.ec straight
into the multi-scalar equation without ever calling assert_valid() on
them. s + n is congruent to s modulo the group order, so the
equation is satisfied and the batch passes; single verification refuses
it because assert_valid enforces the canonical range that BIP340
requires.
Only the batch_size == 1 shortcut is safe, and by accident: it
delegates to assert_as_valid_, which validates. So the divergence needs
two or more signatures.
The three functions above it inherit the hole, none adding a check of its
own: assert_batch_as_valid reduces the messages and delegates,
batch_verify_ wraps the assert in a try/except, and batch_verify does
both.
How far it reaches — the honest bound
Not from the wire. I checked both ways a signature can be
non-canonical:
s + n does not fit in 32 bytes, so no 64-byte signature can carry it.
r >= p does fit, and is expressible on the wire — but Sig.parse
refuses it, and the batch equation refuses it too (it has to lift r
to a point, which fails), so batch_verify_ correctly answers False.
So it takes a caller building Sig objects itself and passing
check_validity=False — which is a supported thing to do, and is exactly
the case the rule decided in #684 exists for. Nothing remotely
triggerable, and no parsed input reaches it.
That bound is why this is filed as a correctness defect rather than a
vulnerability. It still means the library gives two different answers to
one question, and that the cheaper of the two is the one that says yes.
Fix
Whatever #684 settles on for the general shape: the public
assert_batch_as_valid_ validates every Sig it is handed, with the
work moving to a private twin that does not, so the batch_size == 1
path stops being the only validated one. No test pins the current
behaviour — the batch tests all use signatures built through validating
paths — so the fix is additive, and wants a test with a
check_validity=False Sig asserting that batch and single agree.
ssa.batch_verify_answersTruefor a signaturessa.verify_answersFalsefor, and thatSig.assert_validrefuses outright:Batch verification is meant to answer what verifying each signature
separately answers. Here it does not, and the two disagree about a
signature BIP340 calls invalid.
Why
assert_batch_as_valid_readssig.r,sig.sandsig.ecstraightinto the multi-scalar equation without ever calling
assert_valid()onthem.
s + nis congruent tosmodulo the group order, so theequation is satisfied and the batch passes; single verification refuses
it because
assert_validenforces the canonical range that BIP340requires.
Only the
batch_size == 1shortcut is safe, and by accident: itdelegates to
assert_as_valid_, which validates. So the divergence needstwo or more signatures.
The three functions above it inherit the hole, none adding a check of its
own:
assert_batch_as_validreduces the messages and delegates,batch_verify_wraps the assert in a try/except, andbatch_verifydoesboth.
How far it reaches — the honest bound
Not from the wire. I checked both ways a signature can be
non-canonical:
s + ndoes not fit in 32 bytes, so no 64-byte signature can carry it.r >= pdoes fit, and is expressible on the wire — butSig.parserefuses it, and the batch equation refuses it too (it has to lift
rto a point, which fails), so
batch_verify_correctly answersFalse.So it takes a caller building
Sigobjects itself and passingcheck_validity=False— which is a supported thing to do, and is exactlythe case the rule decided in #684 exists for. Nothing remotely
triggerable, and no parsed input reaches it.
That bound is why this is filed as a correctness defect rather than a
vulnerability. It still means the library gives two different answers to
one question, and that the cheaper of the two is the one that says yes.
Fix
Whatever #684 settles on for the general shape: the public
assert_batch_as_valid_validates everySigit is handed, with thework moving to a private twin that does not, so the
batch_size == 1path stops being the only validated one. No test pins the current
behaviour — the batch tests all use signatures built through validating
paths — so the fix is additive, and wants a test with a
check_validity=FalseSigasserting that batch and single agree.