UltrafastSecp256k1/cpu/fuzz/fuzz_schnorr.cpp
shrec ccf8f4a97d
audit(gaps#4,5,6,7): ethereum diff KAT, musig2/frost fuzz, cflite +2 targets, opencl zk+bip324
Gap #4 RESOLVED: audit/test_exploit_ethereum_differential.cpp — 10 tests / 15 sub-checks
  against go-ethereum, web3.py, ethers.js reference vectors (address derivation KAT,
  ecrecover, EIP-191, EIP-155, eth_personal_sign, keccak256 KAT, tamper detection).

Gap #7 RESOLVED: audit/test_fuzz_musig2_frost.cpp — 15 tests / 16 sub-checks
  (MuSig2 key_agg / nonce_agg / partial_verify / partial_sig_agg, FROST keygen_finalize /
  sign / verify_partial / aggregate, schnorr + ecdsa adaptor, boundary n_signers=0 → error).

ClusterFuzzLite expanded to 5 targets:
  + cpu/fuzz/fuzz_ecdsa.cpp  (ECDSA sign→verify invariants, parse_compact_strict)
  + cpu/fuzz/fuzz_schnorr.cpp (BIP-340 sign→verify, adversarial from_bytes verify)

Gap #5/#6 PARTIALLY RESOLVED: OpenCL now wires zk_knowledge_verify_batch,
  zk_dleq_verify_batch, bip324_aead_encrypt_batch, bip324_aead_decrypt_batch.
  bulletproof_verify_batch: PARITY-EXCEPTION (no WNAF multi-scalar on OpenCL).
  Metal: stubs documented with PARITY-EXCEPTION / TODO(parity) markers.
2026-03-24 20:53:23 +00:00

77 lines
3.0 KiB
C++

// ============================================================================
// Fuzz target: BIP-340 Schnorr sign / verify cycle
//
// Input layout (96 bytes):
// bytes 0-31 : private key (raw big-endian; reduced mod n internally)
// bytes 32-63 : message (32 bytes)
// bytes 64-95 : aux_rand (32 bytes of "randomness" from the fuzzer)
//
// Invariants checked:
// 1. sign → verify produces true
// 2. sign → verify with wrong message produces false
// 3. schnorr_xonly_pubkey_parse from the same x-coordinate round-trips
// 4. adversarial schnorr_verify with random sig+key bytes does not crash
// ============================================================================
#include <cstdint>
#include <cstring>
#include <array>
#include "secp256k1/schnorr.hpp"
#include "secp256k1/scalar.hpp"
#include "secp256k1/point.hpp"
using secp256k1::fast::Scalar;
using secp256k1::SchnorrKeypair;
using secp256k1::SchnorrSignature;
using secp256k1::SchnorrXonlyPubkey;
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
if (size < 96) return 0;
std::array<uint8_t, 32> key_buf{};
std::array<uint8_t, 32> msg_buf{};
std::array<uint8_t, 32> aux_buf{};
std::memcpy(key_buf.data(), data, 32);
std::memcpy(msg_buf.data(), data + 32, 32);
std::memcpy(aux_buf.data(), data + 64, 32);
auto priv = Scalar::from_bytes(key_buf);
// Zero key: no crash required, no invariant to enforce
if (priv.is_zero()) return 0;
// -- Create keypair -------------------------------------------------------
SchnorrKeypair kp = secp256k1::schnorr_keypair_create(priv);
// -- Invariant 1: sign → verify = true -----------------------------------
auto sig = secp256k1::schnorr_sign(kp, msg_buf, aux_buf);
bool ok = secp256k1::schnorr_verify(kp.px, msg_buf, sig);
if (!ok) __builtin_trap();
// -- Invariant 2: wrong message → verify = false -------------------------
std::array<uint8_t, 32> wrong_msg = msg_buf;
wrong_msg[0] ^= 0xFF;
bool ok_wrong = secp256k1::schnorr_verify(kp.px, wrong_msg, sig);
if (ok_wrong) __builtin_trap();
// -- Invariant 3: xonly_pubkey_parse round-trips -------------------------
SchnorrXonlyPubkey xpk{};
if (secp256k1::schnorr_xonly_pubkey_parse(xpk, kp.px)) {
// The parsed pubkey's x_bytes must equal kp.px
if (xpk.x_bytes != kp.px) __builtin_trap();
// Verify using cached pubkey must agree
bool ok2 = secp256k1::schnorr_verify(xpk, msg_buf, sig);
if (!ok2) __builtin_trap();
}
// -- Invariant 4: adversarial verify with fuzzer sig bytes ---------------
// Feed raw bytes as a "signature" to verify against our real pubkey.
// We do NOT assert the result -- it might be true by coincidence.
// This only checks for crashes or undefined behaviour.
if (size >= 96 + 64) {
auto adv_sig = SchnorrSignature::from_bytes(data + 96);
(void)secp256k1::schnorr_verify(kp.px, msg_buf, adv_sig);
}
return 0;
}