Skip to content

ssa batch verification accepts a signature single verification refuses #688

Description

@fametrano

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions