Files
secp256k1-zkp/src/modules/chilldkg/util_impl.h

412 lines
17 KiB
C
Raw Normal View History

chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
/***********************************************************************
* 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_IMPL_H
#define SECP256K1_MODULE_CHILLDKG_UTIL_IMPL_H
#include <string.h>
#include "../../../include/secp256k1.h"
#include "util.h"
#include "../../ecmult.h"
#include "../../ecmult_const.h"
#include "../../util.h"
static void secp256k1_chilldkg_sha256_tagged_params_hash(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/params_hash", sizeof("BIP DKG/params_hash") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_encpedpop_seed(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop seed", sizeof("BIP DKG/encpedpop seed") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_simplpedpop_aux(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/simplpedpop aux", sizeof("BIP DKG/simplpedpop aux") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_encpedpop_secnonce(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop secnonce", sizeof("BIP DKG/encpedpop secnonce") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encpedpop ecdh", sizeof("BIP DKG/encpedpop ecdh") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_self_pad(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/encaps_multi self_pad", sizeof("BIP DKG/encaps_multi self_pad") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_vss_coeffs(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"BIP DKG/vss coeffs", sizeof("BIP DKG/vss coeffs") - 1);
}
static void secp256k1_chilldkg_sha256_tagged_taptweak(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha) {
secp256k1_sha256_initialize_tagged(hash_ctx, sha, (const unsigned char *)"TapTweak", sizeof("TapTweak") - 1);
}
static void secp256k1_chilldkg_point_save(unsigned char *out33, const secp256k1_ge *p) {
if (secp256k1_ge_is_infinity(p)) {
memset(out33, 0, 33);
return;
}
{
secp256k1_ge tmp = *p;
/* Serialization operates on public data (commitments, pubnonces, host
* public keys), so variable-time normalization is fine. */
secp256k1_fe_normalize_var(&tmp.x);
secp256k1_fe_normalize_var(&tmp.y);
out33[0] = secp256k1_fe_is_odd(&tmp.y) ? 0x03 : 0x02;
secp256k1_fe_get_b32(&out33[1], &tmp.x);
}
}
static int secp256k1_chilldkg_point_load(secp256k1_ge *p, const unsigned char *in33) {
static const unsigned char zeros33[33] = { 0 };
secp256k1_fe x;
/* Parsed data comes from protocol messages, i.e., it is public. */
if (secp256k1_memcmp_var(in33, zeros33, 33) == 0) {
secp256k1_ge_set_infinity(p);
return 1;
}
if (in33[0] != 0x02 && in33[0] != 0x03) {
secp256k1_ge_set_infinity(p);
return 0;
}
if (!secp256k1_fe_set_b32_limit(&x, &in33[1])) {
secp256k1_ge_set_infinity(p);
return 0;
}
if (!secp256k1_ge_set_xo_var(p, &x, in33[0] == 0x03)) {
secp256k1_ge_set_infinity(p);
return 0;
}
return 1;
}
static void secp256k1_chilldkg_xonly_save(unsigned char *out32, const secp256k1_ge *p) {
secp256k1_ge tmp = *p;
VERIFY_CHECK(!secp256k1_ge_is_infinity(p));
secp256k1_fe_normalize_var(&tmp.x);
secp256k1_fe_get_b32(out32, &tmp.x);
}
static int secp256k1_chilldkg_xonly_load(secp256k1_ge *p, const unsigned char *in32) {
secp256k1_fe x;
if (!secp256k1_fe_set_b32_limit(&x, in32)) {
return 0;
}
return secp256k1_ge_set_xo_var(p, &x, 0);
}
static void secp256k1_chilldkg_pad33(unsigned char *out33, const char *str) {
size_t len = strlen(str);
chilldkg: enforce the tag length bounds in noverify builds secp256k1_chilldkg_pad33 and secp256k1_chilldkg_schnorrsig_sha256_tagged each guarded a memcpy into a fixed-size stack buffer with VERIFY_CHECK, which is compiled out in noverify (release) builds. In pad33 the consequence is worse than the overflowing copy: the following memset(out33 + len, 0, 33 - len) underflows its length to a huge value when len exceeds 33. Neither is reachable today. Every call site passes a string literal of this module: "BIP DKG/certeq message" (22) and "BIP DKG/recovery acknowledgment" (31) for pad33, and at most "BIP DKG/pop message" || "/challenge" (29 of 64) for the tagged-hash helper. This is the same shape as the persisted-state guards promoted in ceccb50a, without the attacker-controlled input path -- so the change is defence in depth, to keep a future longer tag from smashing the stack in a release build rather than failing a debug assertion. Enforce both bounds outside VERIFY_CHECK and keep VERIFY_CHECK(0) inside the branch as the debug-build diagnostic, matching the existing idiom in this module (see the point_load fallbacks in the state loaders). The tagged-hash helper clamps rather than returning early: an early return would leave the caller's secp256k1_sha256 uninitialized and every call site writes into it immediately, which is a worse failure than the one being fixed. A clamped tag changes every hash the module computes, so the chilldkg vectors would fail loudly rather than silently. No behaviour change on any reachable input: the full test suite, including the chilldkg vectors, is unaffected in both verify and noverify builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 23:37:35 +02:00
/* Every call site passes a string literal of the module, so this cannot
* trigger. The clamp must not sit inside VERIFY_CHECK, which is compiled
* out in noverify builds: an over-long tag would overflow out33 and make
* the memset length below underflow to a huge value. */
if (len > 33) {
VERIFY_CHECK(0);
len = 33;
}
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
memcpy(out33, str, len);
memset(out33 + len, 0, 33 - len);
}
/* Initializes sha with the BIP 340 tagged hash tag tag_prefix || subtag,
* e.g., "BIP DKG/pop message" || "/nonce". Tag strings are public constants. */
static void secp256k1_chilldkg_schnorrsig_sha256_tagged(const secp256k1_hash_ctx *hash_ctx, secp256k1_sha256 *sha, const char *tag_prefix, const char *subtag) {
unsigned char tag[64];
size_t prefix_len = strlen(tag_prefix);
size_t subtag_len = strlen(subtag);
chilldkg: enforce the tag length bounds in noverify builds secp256k1_chilldkg_pad33 and secp256k1_chilldkg_schnorrsig_sha256_tagged each guarded a memcpy into a fixed-size stack buffer with VERIFY_CHECK, which is compiled out in noverify (release) builds. In pad33 the consequence is worse than the overflowing copy: the following memset(out33 + len, 0, 33 - len) underflows its length to a huge value when len exceeds 33. Neither is reachable today. Every call site passes a string literal of this module: "BIP DKG/certeq message" (22) and "BIP DKG/recovery acknowledgment" (31) for pad33, and at most "BIP DKG/pop message" || "/challenge" (29 of 64) for the tagged-hash helper. This is the same shape as the persisted-state guards promoted in ceccb50a, without the attacker-controlled input path -- so the change is defence in depth, to keep a future longer tag from smashing the stack in a release build rather than failing a debug assertion. Enforce both bounds outside VERIFY_CHECK and keep VERIFY_CHECK(0) inside the branch as the debug-build diagnostic, matching the existing idiom in this module (see the point_load fallbacks in the state loaders). The tagged-hash helper clamps rather than returning early: an early return would leave the caller's secp256k1_sha256 uninitialized and every call site writes into it immediately, which is a worse failure than the one being fixed. A clamped tag changes every hash the module computes, so the chilldkg vectors would fail loudly rather than silently. No behaviour change on any reachable input: the full test suite, including the chilldkg vectors, is unaffected in both verify and noverify builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 23:37:35 +02:00
/* The longest tag the module builds is "BIP DKG/pop message" ||
* "/challenge", 29 bytes. As in secp256k1_chilldkg_pad33, the bound is
* enforced outside VERIFY_CHECK so that a future over-long tag cannot
* overflow tag[] in a noverify build. Clamping rather than returning
* early keeps sha initialized for the caller; a truncated tag changes
* every hash the module computes, so the test vectors fail loudly. */
if (prefix_len > sizeof(tag)) {
VERIFY_CHECK(0);
prefix_len = sizeof(tag);
}
if (subtag_len > sizeof(tag) - prefix_len) {
VERIFY_CHECK(0);
subtag_len = sizeof(tag) - prefix_len;
}
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
memcpy(tag, tag_prefix, prefix_len);
memcpy(tag + prefix_len, subtag, subtag_len);
secp256k1_sha256_initialize_tagged(hash_ctx, sha, tag, prefix_len + subtag_len);
}
/* BIP 340 nonce derivation with parameterized tag prefix, mirroring
* schnorr_sign in secp256k1lab/bip340.py:
* t = seckey32 XOR TH(tag_prefix || "/aux", aux_rand32)
* nonce32 = TH(tag_prefix || "/nonce", t || xonly_pk32 || msg) */
static void secp256k1_chilldkg_schnorrsig_nonce(const secp256k1_hash_ctx *hash_ctx, unsigned char *nonce32, const unsigned char *msg, size_t msglen, const unsigned char *seckey32, const unsigned char *xonly_pk32, const unsigned char *aux_rand32, const char *tag_prefix) {
secp256k1_sha256 sha;
unsigned char masked_key[32];
unsigned char rand[32];
int i;
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/aux");
secp256k1_sha256_write(hash_ctx, &sha, aux_rand32, 32);
secp256k1_sha256_finalize(hash_ctx, &sha, rand);
for (i = 0; i < 32; i++) {
masked_key[i] = seckey32[i] ^ rand[i];
}
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/nonce");
secp256k1_sha256_write(hash_ctx, &sha, masked_key, 32);
secp256k1_sha256_write(hash_ctx, &sha, xonly_pk32, 32);
secp256k1_sha256_write(hash_ctx, &sha, msg, msglen);
secp256k1_sha256_finalize(hash_ctx, &sha, nonce32);
secp256k1_sha256_clear(&sha);
secp256k1_memclear_explicit(masked_key, sizeof(masked_key));
secp256k1_memclear_explicit(rand, sizeof(rand));
}
/* BIP 340 challenge hash with parameterized tag prefix:
* e = TH(tag_prefix || "/challenge", r32 || pubkey32 || msg) mod n
* The reduction modulo the group order matches the reference, which reduces
* the hash output with int_from_bytes(...) % GE.ORDER. */
static void secp256k1_chilldkg_schnorrsig_challenge(const secp256k1_hash_ctx *hash_ctx, secp256k1_scalar *e, const unsigned char *r32, const unsigned char *msg, size_t msglen, const unsigned char *pubkey32, const char *tag_prefix) {
unsigned char buf[32];
secp256k1_sha256 sha;
secp256k1_chilldkg_schnorrsig_sha256_tagged(hash_ctx, &sha, tag_prefix, "/challenge");
secp256k1_sha256_write(hash_ctx, &sha, r32, 32);
secp256k1_sha256_write(hash_ctx, &sha, pubkey32, 32);
secp256k1_sha256_write(hash_ctx, &sha, msg, msglen);
secp256k1_sha256_finalize(hash_ctx, &sha, buf);
secp256k1_scalar_set_b32(e, buf, NULL);
}
/* Mirrors schnorr_sign in secp256k1lab/bip340.py. The structure follows
* secp256k1_schnorrsig_sign_internal, which cannot be reused directly because
* it hardcodes the "BIP0340" tags. */
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) {
const secp256k1_hash_ctx *hash_ctx;
secp256k1_scalar sk;
secp256k1_scalar e;
secp256k1_scalar k;
secp256k1_ge pk;
secp256k1_ge r;
unsigned char nonce32[32] = { 0 };
unsigned char pk32[32];
unsigned char seckey[32];
int overflow;
int ret = 1;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(secp256k1_ecmult_gen_context_is_built(&ctx->ecmult_gen_ctx));
ARG_CHECK(sig64 != NULL);
ARG_CHECK(msg != NULL || msglen == 0);
ARG_CHECK(seckey32 != NULL);
ARG_CHECK(aux_rand32 != NULL);
ARG_CHECK(tag_prefix != NULL);
hash_ctx = secp256k1_get_hash_context(ctx);
secp256k1_scalar_set_b32(&sk, seckey32, &overflow);
overflow |= secp256k1_scalar_is_zero(&sk);
/* Branching on the validity of the secret key is fine: whether the
* caller's secret key is in range 1..n-1 is not secret. */
secp256k1_declassify(ctx, &overflow, sizeof(overflow));
if (overflow) {
memset(sig64, 0, 64);
secp256k1_scalar_clear(&sk);
return 0;
}
secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &pk, &sk);
/* The public key is not secret, so variable-time normalization and
* branching on its y parity are fine. */
chilldkg: CI wiring, ctime_tests coverage, declassify fixes CI: - ci/ci.sh: new CHILLDKG environment variable, passed to configure as --enable-module-chilldkg (mirroring FROST). - .github/workflows/ci.yml: default CHILLDKG: 'no' and CHILLDKG: 'yes' in every job that enables FROST, except the x86_64 matrix entry that deliberately builds without the ecdh module (chilldkg requires schnorrsig + ecdh; the configure-time dependency error would fire there). YAML validity and per-job dependency presence checked programmatically. ctime_tests: - src/ctime_tests.c: run a full ChillDKG session (n = 2, t = 2) through the public API under the memory checker: hostpubkey_gen, params_hash, participant_step1, coordinator_step1, participant_step2, coordinator_finalize, participant_finalize, participant_recover and recovery_ack_sign. Host secret keys, session randomness, aux randomness and the resulting secret shares are undefined (secret); all protocol messages, the certificate, threshold public key, public shares, recovery data, ack signature and the secret-free state1 objects are defined (public). state2 stays secret (contains the secret share). Constant-time fixes found by running the new block under MemorySanitizer (valgrind unavailable locally; MSan build via clang + CMake). All are missing declassifications of secret-derived but public (or public-outcome) values, following the frost module's secp256k1_declassify pattern with justification comments; no real constant-time bugs were found: - hostpubkey_gen: declassify the computed host public key before serialization (public output). - participant_step1: declassify the zero-randomness check result (only reveals "the RNG returned 32 zero bytes", which aborts the session). - encpedpop participant_step1: declassify the pubnonce point before serialization (public, part of pmsg1). - chilldkg_schnorrsig_sign: declassify the signer public key before normalization/parity branch, and declassify the return value (a failure only reveals a zero derived nonce, negligible probability). - vss_commit: declassify the VSS commitments before serialization (public, part of pmsg1). - vss_verify_secshare: declassify secshare*G before the infinity/eq checks (equals the public pubshare in honest runs; the discrete log is not revealed). - simplpedpop_participant_investigate (proactive audit; not reached by ctime_tests): declassify the secshare-sum comparison result (the public fault code reveals it anyway). Verified: MSan ctime_tests exits 0; autotools make check 10/10 (the local tree is configured without --enable-ctime-tests because neither valgrind nor an MSan-instrumented gcc build is available; CI runs ctime_tests under valgrind as before); CMake ctest 428/428; ./tests --target=chilldkg and ./chilldkg_example pass.
2026-08-31 10:25:14 +02:00
secp256k1_declassify(ctx, &pk, sizeof(pk));
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
secp256k1_fe_normalize_var(&pk.x);
secp256k1_fe_normalize_var(&pk.y);
if (secp256k1_fe_is_odd(&pk.y)) {
secp256k1_scalar_negate(&sk, &sk);
}
secp256k1_scalar_get_b32(seckey, &sk);
secp256k1_fe_get_b32(pk32, &pk.x);
secp256k1_chilldkg_schnorrsig_nonce(hash_ctx, nonce32, msg, msglen, seckey, pk32, aux_rand32, tag_prefix);
/* The reference reduces the nonce hash modulo the group order. */
secp256k1_scalar_set_b32(&k, nonce32, NULL);
ret &= !secp256k1_scalar_is_zero(&k);
secp256k1_scalar_cmov(&k, &secp256k1_scalar_one, !ret);
secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &r, &k);
/* We declassify r to allow using it as a branch point. This is fine
* because r is not a secret. */
secp256k1_declassify(ctx, &r, sizeof(r));
secp256k1_fe_normalize_var(&r.y);
if (secp256k1_fe_is_odd(&r.y)) {
secp256k1_scalar_negate(&k, &k);
}
secp256k1_fe_normalize_var(&r.x);
secp256k1_fe_get_b32(&sig64[0], &r.x);
secp256k1_chilldkg_schnorrsig_challenge(hash_ctx, &e, &sig64[0], msg, msglen, pk32, tag_prefix);
secp256k1_scalar_mul(&e, &e, &sk);
secp256k1_scalar_add(&e, &e, &k);
secp256k1_scalar_get_b32(&sig64[32], &e);
secp256k1_memczero(sig64, 64, !ret);
secp256k1_scalar_clear(&k);
secp256k1_scalar_clear(&sk);
secp256k1_scalar_clear(&e);
secp256k1_ge_clear(&pk);
secp256k1_ge_clear(&r);
secp256k1_memclear_explicit(seckey, sizeof(seckey));
secp256k1_memclear_explicit(nonce32, sizeof(nonce32));
chilldkg: CI wiring, ctime_tests coverage, declassify fixes CI: - ci/ci.sh: new CHILLDKG environment variable, passed to configure as --enable-module-chilldkg (mirroring FROST). - .github/workflows/ci.yml: default CHILLDKG: 'no' and CHILLDKG: 'yes' in every job that enables FROST, except the x86_64 matrix entry that deliberately builds without the ecdh module (chilldkg requires schnorrsig + ecdh; the configure-time dependency error would fire there). YAML validity and per-job dependency presence checked programmatically. ctime_tests: - src/ctime_tests.c: run a full ChillDKG session (n = 2, t = 2) through the public API under the memory checker: hostpubkey_gen, params_hash, participant_step1, coordinator_step1, participant_step2, coordinator_finalize, participant_finalize, participant_recover and recovery_ack_sign. Host secret keys, session randomness, aux randomness and the resulting secret shares are undefined (secret); all protocol messages, the certificate, threshold public key, public shares, recovery data, ack signature and the secret-free state1 objects are defined (public). state2 stays secret (contains the secret share). Constant-time fixes found by running the new block under MemorySanitizer (valgrind unavailable locally; MSan build via clang + CMake). All are missing declassifications of secret-derived but public (or public-outcome) values, following the frost module's secp256k1_declassify pattern with justification comments; no real constant-time bugs were found: - hostpubkey_gen: declassify the computed host public key before serialization (public output). - participant_step1: declassify the zero-randomness check result (only reveals "the RNG returned 32 zero bytes", which aborts the session). - encpedpop participant_step1: declassify the pubnonce point before serialization (public, part of pmsg1). - chilldkg_schnorrsig_sign: declassify the signer public key before normalization/parity branch, and declassify the return value (a failure only reveals a zero derived nonce, negligible probability). - vss_commit: declassify the VSS commitments before serialization (public, part of pmsg1). - vss_verify_secshare: declassify secshare*G before the infinity/eq checks (equals the public pubshare in honest runs; the discrete log is not revealed). - simplpedpop_participant_investigate (proactive audit; not reached by ctime_tests): declassify the secshare-sum comparison result (the public fault code reveals it anyway). Verified: MSan ctime_tests exits 0; autotools make check 10/10 (the local tree is configured without --enable-ctime-tests because neither valgrind nor an MSan-instrumented gcc build is available; CI runs ctime_tests under valgrind as before); CMake ctest 428/428; ./tests --target=chilldkg and ./chilldkg_example pass.
2026-08-31 10:25:14 +02:00
/* Branching on the return value only leaks whether the derived nonce is
* zero, which happens with negligible probability (the case of an invalid
* secret key is declassified above). */
secp256k1_declassify(ctx, &ret, sizeof(ret));
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
return ret;
}
/* Mirrors schnorr_verify in secp256k1lab/bip340.py. The structure follows
* secp256k1_schnorrsig_verify, which cannot be reused directly because it
* hardcodes the "BIP0340/challenge" tag. */
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) {
secp256k1_scalar s;
secp256k1_scalar e;
secp256k1_gej rj;
secp256k1_ge pk;
secp256k1_gej pkj;
secp256k1_fe rx;
secp256k1_ge r;
unsigned char buf[32];
int overflow;
VERIFY_CHECK(ctx != NULL);
ARG_CHECK(sig64 != NULL);
ARG_CHECK(msg != NULL || msglen == 0);
ARG_CHECK(pubkey32 != NULL);
ARG_CHECK(tag_prefix != NULL);
if (!secp256k1_fe_set_b32_limit(&rx, &sig64[0])) {
return 0;
}
secp256k1_scalar_set_b32(&s, &sig64[32], &overflow);
if (overflow) {
return 0;
}
if (!secp256k1_chilldkg_xonly_load(&pk, pubkey32)) {
return 0;
}
/* Compute e. */
secp256k1_fe_get_b32(buf, &pk.x);
secp256k1_chilldkg_schnorrsig_challenge(secp256k1_get_hash_context(ctx), &e, &sig64[0], msg, msglen, buf, tag_prefix);
/* Compute rj = s*G + (-e)*pkj */
secp256k1_scalar_negate(&e, &e);
secp256k1_gej_set_ge(&pkj, &pk);
secp256k1_ecmult(&rj, &pkj, &e, &s);
secp256k1_ge_set_gej_var(&r, &rj);
if (secp256k1_ge_is_infinity(&r)) {
return 0;
}
secp256k1_fe_normalize_var(&r.y);
return !secp256k1_fe_is_odd(&r.y) &&
secp256k1_fe_equal(&rx, &r.x);
}
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) {
const secp256k1_hash_ctx *hash_ctx = secp256k1_get_hash_context(ctx);
secp256k1_gej resj;
secp256k1_ge res;
unsigned char x[32];
unsigned char y[32];
unsigned char shared[32];
unsigned char hash32[32];
secp256k1_sha256 sha;
VERIFY_CHECK(ctx != NULL);
VERIFY_CHECK(!secp256k1_scalar_is_zero(seckey));
VERIFY_CHECK(!secp256k1_ge_is_infinity(their_point));
/* libsecp256k1-style ECDH: SHA256 of the compressed shared point. */
secp256k1_ecmult_const(&resj, their_point, seckey);
secp256k1_ge_set_gej(&res, &resj);
/* The result cannot be the point at infinity: their_point is not
* infinity, seckey is nonzero, and the group has prime order. */
VERIFY_CHECK(!secp256k1_ge_is_infinity(&res));
secp256k1_fe_normalize(&res.x);
secp256k1_fe_normalize(&res.y);
secp256k1_fe_get_b32(x, &res.x);
secp256k1_fe_get_b32(y, &res.y);
/* This hash function always succeeds; call it unconditionally (it must
* not sit inside VERIFY_CHECK, which is compiled out in noverify
* builds). */
if (!ecdh_hash_function_sha256_impl(hash_ctx, shared, x, y, NULL)) {
VERIFY_CHECK(0);
memset(shared, 0, sizeof(shared));
}
secp256k1_chilldkg_sha256_tagged_encpedpop_ecdh(hash_ctx, &sha);
secp256k1_sha256_write(hash_ctx, &sha, shared, 32);
/* The sender's pubnonce always comes first in the hash input. */
if (sending) {
secp256k1_sha256_write(hash_ctx, &sha, my_pubkey33, 33);
secp256k1_sha256_write(hash_ctx, &sha, their_pubkey33, 33);
} else {
secp256k1_sha256_write(hash_ctx, &sha, their_pubkey33, 33);
secp256k1_sha256_write(hash_ctx, &sha, my_pubkey33, 33);
}
secp256k1_sha256_write(hash_ctx, &sha, context, context_len);
secp256k1_sha256_finalize(hash_ctx, &sha, hash32);
secp256k1_sha256_clear(&sha);
/* Pads are reduced modulo the group order (from_bytes_wrapping in the
* reference). */
secp256k1_scalar_set_b32(out, hash32, NULL);
secp256k1_memclear_explicit(x, sizeof(x));
secp256k1_memclear_explicit(y, sizeof(y));
secp256k1_memclear_explicit(shared, sizeof(shared));
secp256k1_memclear_explicit(hash32, sizeof(hash32));
secp256k1_ge_clear(&res);
secp256k1_gej_clear(&resj);
}
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) {
unsigned char hash32[32];
secp256k1_sha256 sha;
secp256k1_chilldkg_sha256_tagged_self_pad(hash_ctx, &sha);
secp256k1_sha256_write(hash_ctx, &sha, symkey32, 32);
secp256k1_sha256_write(hash_ctx, &sha, nonce33, 33);
secp256k1_sha256_write(hash_ctx, &sha, context, context_len);
secp256k1_sha256_finalize(hash_ctx, &sha, hash32);
secp256k1_sha256_clear(&sha);
/* from_bytes_wrapping in the reference. */
secp256k1_scalar_set_b32(out, hash32, NULL);
secp256k1_memclear_explicit(hash32, sizeof(hash32));
}
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
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) {
chilldkg: Phase 1 - internal primitives (util, vss) Add the byte-exact internal primitives for the ChillDKG module, mirroring the Python reference implementation of the bip-frost-dkg draft (v0.3.0-dev), pinned to upstream commit a91896883f85b159415ecf298d5e844879af112d. util.h / util_impl.h (mirrors chilldkg_ref/util.py): - Point (de)serialization with explicit point-at-infinity support: 33 zero bytes <-> infinity, otherwise SEC compressed. Checked parse rejects invalid encodings and out-of-range x coordinates (point_save/point_load, xonly_save/xonly_load). - Internal parameterized-tag BIP-340 Schnorr sign/verify (chilldkg_schnorrsig_sign/_verify): tag prefix selects the <prefix>/aux, /nonce, /challenge subtags ("BIP DKG/pop message" for proofs of possession, "BIP0340" for CertEq signatures and recovery acks), arbitrary-length messages, pad33 zero-padding helper. The public schnorrsig API hardcodes BIP0340/32-byte messages, so the algorithm is replicated from secp256k1_schnorrsig_sign_internal with a custom tag; cross-checked against secp256k1_schnorrsig_sign32. - Tagged hashes via secp256k1_sha256_initialize_tagged: "BIP DKG/params_hash", "BIP DKG/encpedpop seed", "BIP DKG/simplpedpop aux", "BIP DKG/encpedpop secnonce", "BIP DKG/encpedpop ecdh", "BIP DKG/encaps_multi self_pad", "BIP DKG/vss coeffs", and BIP-341 "TapTweak" (32-byte x-only input). - params_hash = TH("BIP DKG/params_hash", u32be(t) || hostpubkeys) (note: plan had the operand order reversed; the reference hashes t first). - ECDH pads: reuses the ecdh module's SHA256-of-compressed-shared- point hash, then TH("BIP DKG/encpedpop ecdh", ecdh || sender_pubnonce || receiver_hostpubkey || context) with a sending flag fixing the sender|receiver order; self_pad for the own index. Pads are parsed wrapping (mod-n reduction); wire scalars, VSS coefficients and the TapTweak are parsed checked. vss.h / vss_impl.h (mirrors chilldkg_ref/vss.py): - vss_gen_coeffs: per-coefficient TH("BIP DKG/vss coeffs", seed || u32be(j)), checked parse with bitwise error accumulation. - vss_poly_eval (Horner) and vss_secshare_for with the x = id+1 convention (safe at UINT32_MAX). - vss_commit (constant-time ecmult_gen, zero coefficient -> infinity), vss_pubshare (powers-of-x over commitments, skips infinity), vss_commitment_add, vss_verify_secshare. - vss_invalid_taproot_commit: TapTweak applied to the x-only constant term so the Taproot script path is unspendable; returns tweak and pubtweak. tests_impl.h: 7 vector tests (tagged hashes, params_hash, point serialization incl. infinity roundtrip and parity prefixes, checked vs wrapping scalar parse at the group order boundary, custom-tag schnorrsig incl. wrong-tag/key/msg rejection, ECDH pad sender/receiver symmetry, VSS coeff derivation/Horner/commitment/pubshare/tweak) with expected values generated once from the Python reference (committed into the test file, reference commit recorded). Verified: make check 3/3 suites pass; CMake ctest all pass; ./tests --target=chilldkg runs all 7 new tests green in both verify and noverify builds.
2026-08-31 04:05:15 +02:00
unsigned char buf[4];
secp256k1_sha256 sha;
secp256k1_chilldkg_sha256_tagged_params_hash(hash_ctx, &sha);
secp256k1_write_be32(buf, t);
secp256k1_sha256_write(hash_ctx, &sha, buf, sizeof(buf));
secp256k1_sha256_write(hash_ctx, &sha, hostpubkeys33, 33 * n);
secp256k1_sha256_finalize(hash_ctx, &sha, out32);
secp256k1_sha256_clear(&sha);
}
#endif