test: fuzz paths that random inputs almost never reach #1957

pull fametrano wants to merge 13 commits into bitcoin-core:master from fametrano:fuzz-nonce-retry-infinity changing 24 files +1270 −1
  1. fametrano commented at 2:54 PM on October 10, 2026: contributor

    Based on #1956; only the last three commits are new. Random inputs almost never reach three paths: the retry loop in ECDSA signing, because RFC6979 practically never returns an invalid nonce; the rejection in ECDSA and Schnorr verification when the computed point is infinity; and silent payments output creation for a spend key of minus the output tweak. The first commit signs in the ecdsa and recovery targets with a nonce function that first returns 0 or a value above n, or fails at a chosen attempt, and checks that signing fails exactly when the nonce function does; schnorrsig gets such nonces too, checked only as far as its header documents. The second commit builds the key or signature that makes verification compute infinity, and checks that it is rejected. Both techniques come from @erickcestari's diffsecp. The third commit, as @Yudis-bit suggested on #1956, sometimes sets the spend key to minus the tweak, checks that output creation fails as documented and that the target's fallback then works, and moves the sent output among the decoys.

    With -seed=1 -runs=20000, the retry loop in ecdsa and recovery, both infinity rejections and the silent payments fallback each run thousands of times; on #1956 all five are 0. Breaking each path on purpose, by stopping at an invalid nonce, accepting at infinity or creating an output for minus the tweak, makes the targets fail.

    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.

  2. test: add libFuzzer harness with ecdsa, recovery and ecdh targets
    This is the harness of PR #744, rebased on the current tree. It is
    built as one binary, `fuzz`, which picks the target from the FUZZ
    environment variable, as Bitcoin Core does. Like the tests, it includes
    secp256k1.c, so that targets for internal functions can be added later.
    Module targets live in src/modules/<module>/fuzz_impl.h.
    
    The code is updated to the current API (secp256k1_context_static,
    SECP256K1_CONTEXT_NONE, secp256k1_ec_seckey_*), uses
    secp256k1_memcmp_var, and is C89 clean under the project's warning
    flags. The checks are those of #744, with these changes:
    
    - consume_pubkey copies the x coordinate after the prefix byte instead
      of over it.
    - The Pubkey(seckey1+seckey2) check accepts seckey1 + seckey2 == 0 mod
      n, where seckey_tweak_add, pubkey_combine and pubkey_tweak_add all
      return 0. It checks that they agree and fail exactly in that case.
    - secret_msg and predefined_pubkey are one byte longer, for the NUL: a
      32-byte literal in a 32-byte array fails -Werror with
      -Wunterminated-string-initialization (clang 23, GCC 15).
    - The scratch space block is dropped: secp256k1_scratch_space_create
      left the API in 0.6.0.
    - A NULL data pointer with size 0 is replaced by a non-NULL one, since
      the targets do arithmetic on it.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    e873b25516
  3. build: add fuzz target to autotools and CMake
    The fuzz target is built with --enable-fuzz or -DSECP256K1_BUILD_FUZZ=ON
    and is off by default. Like tests, it is compiled with VERIFY, so the
    library's internal VERIFY_CHECKs run. It is not a test and does not run
    in make check or ctest.
    
    The fuzzing engine is linked from FUZZ_LIBS (SECP256K1_FUZZ_LIBS in
    CMake), default -fsanitize=fuzzer, into the fuzz target only.
    Instrumentation comes from CFLAGS, e.g.
    -fsanitize=fuzzer-no-link,address,undefined. This is the split Bitcoin
    Core and OSS-Fuzz use, so that LIB_FUZZING_ENGINE can be passed as
    FUZZ_LIBS.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    abc1649335
  4. doc: describe fuzzing
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    cdcbd85002
  5. test: add extrakeys and schnorrsig fuzz targets
    extrakeys checks that keypair and x-only keys agree with
    ec_pubkey_create, that x-only keys round-trip through serialization,
    and that keypair_xonly_tweak_add, xonly_pubkey_tweak_add and
    xonly_pubkey_tweak_add_check agree. Where the two tweak functions
    fail, it checks the cause: a tweak >= n, or one that cancels the even-y
    secret key.
    
    schnorrsig checks that signing succeeds and verifies, that sign_custom
    with a 32-byte message equals sign32, that a NULL aux_rand32 equals an
    all-zero one, that a signature with one bit flipped does not verify,
    and that a garbage signature does not verify for a key whose secret
    nobody knows.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    2067a2f710
  6. test: add ellswift fuzz target
    It checks that decode(encode(pubkey)) is pubkey, that every 64-byte
    string decodes to a key that re-parses, that ellswift_create decodes to
    ec_pubkey_create's key, and that both parties of ellswift_xdh get the
    same secret. Against 64 arbitrary bytes from the peer, as in BIP324, it
    checks that the shared x is the x of our secret key times the decoded
    peer key.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    38b1a164d3
  7. test: add musig fuzz target
    Each input runs one two-signer session. It checks that nonce_gen
    succeeds for a non-zero session_secrand32, that sorted key aggregation
    does not depend on the key order, that both partial signatures verify,
    that the aggregate signature verifies with schnorrsig_verify, and that
    a tampered partial signature does not verify.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    c1d5660951
  8. test: add silentpayments fuzz target
    One sender input pays one recipient. The recipient scans the sent
    output among decoys: its own spend key and up to two x-only keys from
    the fuzz input. The target checks that the sent output is found, and
    that every found output is spendable with the spend key plus its tweak.
    
    create_outputs fails, with one valid input key, only for a spend key
    equal to minus the output tweak. There the target retries with the
    negated spend key and checks that cause.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    c7b5420e2f
  9. test: check at fuzz startup that valid inputs are accepted
    The targets skip inputs that the library rejects as invalid. A library
    that rejected every valid secret key, public key or signature would
    then pass every run without reaching a check. At startup, the fuzz
    binary now aborts unless secret key 1, the generator, a compact ECDSA
    signature and, where their modules are enabled, the generator's
    x-only key and a partial signature are accepted.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    2bfecf6feb
  10. ci: add libFuzzer job
    The job builds with clang, ASan and UBSan, and runs every fuzz target
    on an empty corpus for 10000 runs or 20 seconds, whichever comes first.
    
    It passes -len_control=0, so inputs have full length from the first
    run. Otherwise libFuzzer lengthens them slowly, and in 10000 runs they
    stay under 100 bytes, too short for the musig, ellswift and
    silentpayments targets to reach their last checks.
    
    CFLAGS carry -fsanitize=fuzzer-no-link, not -fsanitize=fuzzer: on Linux
    clang links the latter as a whole archive, libFuzzer's main included,
    into every program it links, which clashes with the main of configure's
    checks, the tests and the examples.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    fec3ea735a
  11. fametrano renamed this:
    test: fuzz nonce retries and verification at infinity
    test: fuzz paths that random inputs almost never reach
    on Oct 10, 2026
  12. test: tighten fuzz checks after review
    fuzz_check_bool checks that a library function returns 0 or 1, as the
    API documents.
    
    Public keys parsed from raw input, compressed and uncompressed, must
    serialize back to the input, with a hybrid 0x06/0x07 prefix as 0x04.
    This catches a parser that accepts a non-canonical encoding.
    
    Raw 32-byte input also checks that ec_pubkey_create and
    ec_seckey_verify accept the same secret keys.
    
    The startup check now runs the consume helpers on secret key 1 and the
    generator, instead of the library's parsers, which the unit tests
    cover. It aborts on a harness bug like the one in #744's
    consume_pubkey, which copied the x coordinate over the prefix byte.
    
    Suggested by erickcestari in #1956.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    26b9224d54
  13. test: fuzz signing with nonce functions that force retries
    RFC6979 practically never returns a nonce that signing must reject, so
    the retry loop in ECDSA signing is not reached by the fuzz targets. The
    ecdsa and recovery targets now sign with a nonce function whose first
    attempts return 0 or a value >= n, and which can fail at a chosen
    attempt; later attempts use RFC6979. They check what the headers
    document: attempt counts the previous calls, algo16 is NULL, and
    signing fails exactly when the nonce function does. A signature that
    is created verifies, or recovers the signing key.
    
    The schnorrsig nonce function has no attempt argument, and the header
    does not say what signing does with a nonce of 0 or >= n. The
    schnorrsig target signs with a nonce function returning such a nonce, or
    failing, and checks only what is documented: a failing nonce function
    makes signing fail, a created signature verifies, and the function
    receives the x-only key of its secret key.
    
    The technique follows Erick Cestari's diffsecp
    (https://github.com/erickcestari/diffsecp, src/targets/ecdsa.c and
    src/targets/schnorrsig.c at c9776c4, MIT).
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    68ab9e5476
  14. test: fuzz verification where the computed point is infinity
    Random keys and signatures practically never make verification compute
    the point at infinity, so its rejection is not reached by the fuzz
    targets. The targets now build the key or signature that leads there,
    using internal functions only to construct the inputs:
    
    - ecdsa: for a signature (r, s) and message hash z, the key
      Q = -(z/r)*G makes u1*G + u2*Q infinity. No signer holding -z/r can
      produce (r, s), since s would be 0, so verification must fail.
    - schnorrsig: for a key with secret d and a fuzzed r, s = e*d makes
      R = s*G - e*P infinity, which BIP-340 verification rejects.
    
    Both constructions follow Erick Cestari's diffsecp
    (https://github.com/erickcestari/diffsecp, src/targets/ecdsa.c and
    src/targets/schnorrsig.c at c9776c4, MIT).
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    f700c35461
  15. test: fuzz silent payments with a spend key of minus the tweak
    create_outputs fails for a recipient spend key of -t, where t is the
    output tweak, because the output would be infinity. A fuzzed spend key
    practically never is -t, so the target's fallback for that case was
    never reached. The target now sometimes sets the spend key to -t, built
    with internal functions only, and checks that create_outputs returns 0,
    as its header documents for an invalid output public key. The fallback
    then runs.
    
    It also moves the sent output to a position chosen by the fuzz input,
    among the decoys.
    
    Both follow Yudis-bit's suggestion in
    https://github.com/bitcoin-core/secp256k1/pull/1956#issuecomment-6099025547.
    
    Assisted-by: my usual tools (LLM, Internet, computer). Mistakes are mine.
    Signed-off-by: Ferdinando Ametrano <ferdinando@ametrano.net>
    5de46b118a
  16. fametrano force-pushed on Oct 10, 2026
  17. Yudis-bit commented at 11:18 AM on October 13, 2026: contributor

    Tested ACK 5de46b1 on Windows. Thanks for picking up the -t fallback and decoy shuffle from #1956, looks good.

  18. theStack added the label assurance on Oct 18, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin-core/secp256k1. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-23 07:15 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me