Files
secp256k1-zkp/src/modules/chilldkg/util.h
Kgothatso Ngako 2a0e14d076 chilldkg: Phase 3 - public participant API and CertEq
Add the public participant-facing ChillDKG API to
include/secp256k1_chilldkg.h and the CertEq sub-protocol, completing
the participant side of the protocol (bip-frost-dkg v0.3.0-dev,
reference pinned at a91896883f85b159415ecf298d5e844879af112d).

New module files:
- certeq.h / certeq_impl.h: CertEq sub-protocol. Participants sign
  pad33("BIP DKG/certeq message") || u32be(i) || eq_input with plain
  BIP0340-tagged Schnorr signatures under their host key
  (certeq_participant_step); verification is per-index against the
  x-only hostpubkeys[i][1:33] exactly as the reference
  (certeq_verify). The coordinator side reuses certeq_verify in
  Phase 4.

Public API (all no-malloc, caller-allocated buffers, outputs zeroed on
failure, secret paths cleared):
- secp256k1_chilldkg_hostpubkey_gen: plain compressed host pubkey
  generation; rejects zero / >= group order seckeys.
- secp256k1_chilldkg_params_hash: validates session params (participant
  and threshold ranges, strictly compressed non-infinity pubkeys, no
  duplicates) and computes TH("BIP DKG/params_hash", u32be(t) ||
  hostpubkeys).
- Message-length helpers so callers can size buffers:
  participant_msg1_len (33t+32n+97), coordinator_msg1_len
  (162n+33(t-1)), participant_msg2_len (64), coordinator_msg2_len
  (64n), recovery_data_len (4+33t+162n).
- secp256k1_chilldkg_participant_step1: full EncPedPop step1 with
  seed=deckey=hostseckey; rejects zero randomness and hostseckeys not
  matching the claimed hostpubkey (input errors, not protocol faults).
- secp256k1_chilldkg_participant_step2: parses and verifies cmsg1 via
  the Phase 2 encpedpop/simplpedpop participant path, computes the
  tweaked secshare/pubshares/threshold pubkey, appends enc_secshares
  to eq_input (matching the reference for recovery consistency), and
  emits the 64-byte CertEq signature.
- secp256k1_chilldkg_participant_finalize: re-verifies all n CertEq
  signatures in the certificate, then outputs the 32-byte secshare,
  33-byte threshold pubkey, n pubshares and the self-delimiting
  recovery data (eq_input || cert).

Blame reporting without exceptions: public enum
secp256k1_chilldkg_fault (OK / FAULTY_COORDINATOR /
FAULTY_PARTICIPANT / FAULTY_PARTICIPANT_OR_COORDINATOR /
UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR / INVALID_INPUT) plus an out
fault_index, mapping the reference's exception taxonomy:
- hostseckey invalid/mismatch -> INVALID_INPUT (HostSeckeyError),
- cmsg1 scalar overflow/parse -> FAULTY_COORDINATOR (MsgParseError),
- pubnonce/commitment/PoP faults -> FAULTY_PARTICIPANT_OR_COORDINATOR(i),
- share-vs-pubshare mismatch -> UNKNOWN with fault_index = UINT32_MAX,
- certificate signature failure -> FAULTY_COORDINATOR (documented
  deviation: fault_index carries the failing signature index as
  diagnostic info; the reference discards it).
Enum-returning functions use a local CHILLDKG_ARG_CHECK that fires the
illegal-argument callback and returns INVALID_INPUT (ARG_CHECK would
return 0 = OK).

Opaque state objects with magic-validated save/load (frost idiom):
participant_state1 (4306 bytes, no secrets) and participant_state2
(21073 bytes, contains the secshare; documented keep-secret/no-copy).
Fixed-size at SECP256K1_CHILLDKG_MAX_PARTICIPANTS = 128.

Also fixes a noverify-build bug: state1_load ran point_load inside
VERIFY_CHECK, which compiles out in noverify builds and left the
commitment uninitialized; now called unconditionally.

tests_impl.h: participant_api_test with full-session reference vectors
(n=3, t=2; coordinator aggregation simulated through the internal
Phase 2 coordinator step and verified byte-identical to the
reference's coordinator_step1): msglen helpers, hostpubkey_gen and
params_hash vectors incl. duplicate/invalid/infinity rejection,
byte-exact pmsg1/cmsg1/CertEq sigs/secshare/thresh_pk/pubshares/
recovery, blame cases (tampered enc_secshare -> UNKNOWN, invalid
pubnonce -> FAULTY_PARTICIPANT_OR_COORDINATOR(1), overflowing
enc_secshare -> FAULTY_COORDINATOR, corrupted cert sig ->
FAULTY_COORDINATOR with fault_index and zeroed outputs), NULL-arg
misuse and bad-magic state rejection.

Verified: make check 3/3 (incl. noverify); CMake ctest 363/363;
make distdir includes all new files.
2026-08-31 05:22:53 +02:00

106 lines
6.8 KiB
C

/***********************************************************************
* Distributed under the MIT software license, see the accompanying *
* file COPYING or https://www.opensource.org/licenses/mit-license.php.*
***********************************************************************/
#ifndef SECP256K1_MODULE_CHILLDKG_UTIL_H
#define SECP256K1_MODULE_CHILLDKG_UTIL_H
#include "../../../include/secp256k1.h"
#include "../../../include/secp256k1_chilldkg.h"
#include "../../group.h"
#include "../../hash.h"
#include "../../scalar.h"
/* The public header defines SECP256K1_CHILLDKG_MAX_PARTICIPANTS and the fault
* enum secp256k1_chilldkg_fault (SECP256K1_CHILLDKG_OK, ...), which the
* internal SimplPedPop/EncPedPop functions use directly as their return
* codes. */
/* This file contains the internal primitives of the ChillDKG module that
* mirror chilldkg_ref/util.py, secp256k1lab/bip340.py and secp256k1lab/ecdh.py
* of the bip-frost-dkg reference implementation. Byte-exactness with the
* reference is the goal; the tagged hash strings below are copied verbatim
* from the Python sources. */
/* Tagged hash initializers (BIP 340 tagged hashes). All ChillDKG tags share
* the "BIP DKG/" prefix (BIP_TAG in chilldkg_ref/util.py); "TapTweak" is the
* BIP 341 tag. */
static void secp256k1_chilldkg_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_seed(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_simplpedpop_aux(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_secnonce(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_vss_coeffs(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
static void secp256k1_chilldkg_sha256_tagged_taptweak(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha);
/* Serialize a group element to 33 bytes: the point at infinity maps to 33
* zero bytes, otherwise the standard SEC compressed encoding (0x02/0x03
* prefix) is used. Mirrors GE.to_bytes_compressed_with_infinity. */
static void secp256k1_chilldkg_point_save(unsigned char *out33, const secp256k1_ge *p);
/* Parse a 33-byte group element encoding: 33 zero bytes map to the point at
* infinity, otherwise standard SEC compressed parsing applies. Returns 1 on
* success and 0 if the encoding is invalid (wrong prefix, x >= p, or x not on
* the curve). On failure, *p is set to the point at infinity. Mirrors
* GE.from_bytes_compressed_with_infinity. */
static int secp256k1_chilldkg_point_load(secp256k1_ge *p, const unsigned char *in33);
/* Serialize the x coordinate of a non-infinity group element (32 bytes).
* Mirrors GE.to_bytes_xonly, which asserts non-infinity. */
static void secp256k1_chilldkg_xonly_save(unsigned char *out32, const secp256k1_ge *p);
/* Parse a 32-byte x-only group element encoding (even y). Returns 1 on
* success, 0 if x >= p or x is not on the curve. Mirrors
* GE.from_bytes_xonly. */
static int secp256k1_chilldkg_xonly_load(secp256k1_ge *p, const unsigned char *in32);
/* Write str (at most 33 bytes) zero-padded to 33 bytes. Used to build the
* domain separation prefixes of the CertEq and recovery acknowledgment
* messages (see certeq_message/recovery_ack_message in chilldkg_ref). */
static void secp256k1_chilldkg_pad33(unsigned char *out33, const char *str);
/* BIP 340 Schnorr signing/verification with a parameterized tag prefix and
* arbitrary-length messages, mirroring secp256k1lab/bip340.py's schnorr_sign
* and schnorr_verify. The tags used for aux, nonce and challenge hashes are
* tag_prefix || "/aux", tag_prefix || "/nonce" and tag_prefix ||
* "/challenge". ChillDKG uses tag_prefix "BIP DKG/pop message" for proofs of
* possession (POP_MSG_TAG in simplpedpop.py) and the standard "BIP0340"
* prefix for CertEq and recovery acknowledgments.
*
* sign returns 1 on success and 0 if seckey32 is not in range 1..n-1 or the
* derived nonce is zero (negligible probability). On failure, sig64 is set to
* zero. verify returns 1 if the signature is valid and 0 otherwise. */
static int secp256k1_chilldkg_schnorrsig_sign(const secp256k1_context *ctx, unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *seckey32, const unsigned char *aux_rand32, const char *tag_prefix);
static int secp256k1_chilldkg_schnorrsig_verify(const secp256k1_context *ctx, const unsigned char *sig64, const unsigned char *msg, size_t msglen, const unsigned char *pubkey32, const char *tag_prefix);
/* Compute the EncPedPop ECDH pad (encpedpop.py `ecdh`):
* shared = SHA256(compressed(seckey * their_point)) [libsecp256k1 ECDH]
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
* shared || my_pubkey33 || their_pubkey33 || context)) if sending
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encpedpop ecdh",
* shared || their_pubkey33 || my_pubkey33 || context)) otherwise
* In the protocol, my/their_pubkey33 are the sender's pubnonce and the
* receiver's host pubkey (in this order in the hash input, regardless of
* sending), and context is u32be(receiver_index) || enc_context.
* their_point must be the parsed their_pubkey33 and must not be infinity
* (enforced by VERIFY_CHECK). seckey must be nonzero. */
static void secp256k1_chilldkg_encpedpop_ecdh(const secp256k1_context *ctx, secp256k1_scalar *out, const secp256k1_scalar *seckey, const secp256k1_ge *their_point, const unsigned char *my_pubkey33, const unsigned char *their_pubkey33, const unsigned char *context, size_t context_len, int sending);
/* Compute the EncPedPop symmetric pad for encrypting to ourselves
* (encpedpop.py `self_pad`):
* out = Scalar.from_bytes_wrapping(TH("BIP DKG/encaps_multi self_pad",
* symkey32 || nonce33 || context))
* where symkey32 is the host seckey, nonce33 is the own pubnonce and context
* is u32be(own_index) || enc_context. */
static void secp256k1_chilldkg_encpedpop_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *out, const unsigned char *symkey32, const unsigned char *nonce33, const unsigned char *context, size_t context_len);
/* Compute the session parameters hash (chilldkg.py `params_hash`):
* out32 = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys[0] || ... || hostpubkeys[n-1])
* where hostpubkeys33 is an array of n 33-byte compressed host public keys. */
static void secp256k1_chilldkg_params_hash_internal(const secp256k1_hash_ctx *hash_ctx, unsigned char *out32, const unsigned char *hostpubkeys33, size_t n, uint32_t t);
#endif