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.
This commit is contained in:
@@ -57,6 +57,10 @@
|
||||
#include "../include/secp256k1_frost.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_MODULE_CHILLDKG
|
||||
#include "../include/secp256k1_chilldkg.h"
|
||||
#endif
|
||||
|
||||
static void run_tests(secp256k1_context *ctx, unsigned char *key);
|
||||
|
||||
int main(void) {
|
||||
@@ -455,6 +459,134 @@ static void run_tests(secp256k1_context *ctx, unsigned char *key) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_MODULE_CHILLDKG
|
||||
{
|
||||
/* Full ChillDKG session with n = 2, t = 2 (pmsg1: 227 bytes, cmsg1:
|
||||
* 357 bytes, cert: 128 bytes, recovery data: 394 bytes). */
|
||||
unsigned char hostseckeys[2][32];
|
||||
unsigned char hostpubkeys[2 * 33];
|
||||
unsigned char dkg_random[2][32];
|
||||
unsigned char aux_rands[2][32];
|
||||
secp256k1_chilldkg_participant_state1 state1[2];
|
||||
secp256k1_chilldkg_participant_state2 state2[2];
|
||||
secp256k1_chilldkg_coordinator_state cstate;
|
||||
unsigned char pmsgs1[2][227];
|
||||
const unsigned char *pmsgs1_ptrs[2];
|
||||
unsigned char cmsg1[357];
|
||||
unsigned char pmsgs2[2][64];
|
||||
const unsigned char *pmsgs2_ptrs[2];
|
||||
unsigned char cmsg2[128];
|
||||
unsigned char secshare32[32];
|
||||
unsigned char thresh_pk33[33];
|
||||
unsigned char pubshares33[2 * 33];
|
||||
unsigned char recovery[394];
|
||||
unsigned char rec_secshare32[32];
|
||||
unsigned char rec_thresh_pk33[33];
|
||||
unsigned char rec_pubshares33[33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
||||
unsigned char rec_hostpubkeys33[33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
|
||||
unsigned char ack_sig[64];
|
||||
size_t rec_n;
|
||||
uint32_t rec_t;
|
||||
uint32_t fault_index;
|
||||
secp256k1_chilldkg_fault fault;
|
||||
|
||||
pmsgs1_ptrs[0] = pmsgs1[0];
|
||||
pmsgs1_ptrs[1] = pmsgs1[1];
|
||||
pmsgs2_ptrs[0] = pmsgs2[0];
|
||||
pmsgs2_ptrs[1] = pmsgs2[1];
|
||||
|
||||
/* All public inputs are derived from defined memory. key is reused as
|
||||
* the base of the (secret) host keys, session randomness and
|
||||
* auxiliary randomness. */
|
||||
SECP256K1_CHECKMEM_DEFINE(key, 32);
|
||||
for (i = 0; i < 2; i++) {
|
||||
memcpy(hostseckeys[i], key, 32);
|
||||
hostseckeys[i][0] = hostseckeys[i][0] + 4 + i;
|
||||
memcpy(dkg_random[i], key, 32);
|
||||
dkg_random[i][0] = dkg_random[i][0] + 6 + i;
|
||||
memcpy(aux_rands[i], key, 32);
|
||||
aux_rands[i][0] = aux_rands[i][0] + 8 + i;
|
||||
}
|
||||
|
||||
/* Test chilldkg_hostpubkey_gen. The host secret keys are secret; the
|
||||
* host public keys are public. */
|
||||
for (i = 0; i < 2; i++) {
|
||||
SECP256K1_CHECKMEM_UNDEFINE(hostseckeys[i], 32);
|
||||
ret = secp256k1_chilldkg_hostpubkey_gen(ctx, &hostpubkeys[33 * i], hostseckeys[i]);
|
||||
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
|
||||
CHECK(ret == 1);
|
||||
SECP256K1_CHECKMEM_DEFINE(&hostpubkeys[33 * i], 33);
|
||||
}
|
||||
|
||||
/* Test chilldkg_participant_step1. The host secret key and the
|
||||
* session randomness are secret; pmsg1 and the state1 object (which
|
||||
* contains no secrets) are public. */
|
||||
for (i = 0; i < 2; i++) {
|
||||
SECP256K1_CHECKMEM_UNDEFINE(hostseckeys[i], 32);
|
||||
SECP256K1_CHECKMEM_UNDEFINE(dkg_random[i], 32);
|
||||
ret = secp256k1_chilldkg_participant_step1(ctx, &state1[i], pmsgs1[i], hostseckeys[i], hostpubkeys, 2, 2, dkg_random[i]);
|
||||
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
|
||||
CHECK(ret == 1);
|
||||
SECP256K1_CHECKMEM_DEFINE(pmsgs1[i], sizeof(pmsgs1[i]));
|
||||
SECP256K1_CHECKMEM_DEFINE(&state1[i], sizeof(state1[i]));
|
||||
}
|
||||
|
||||
/* The coordinator's steps use only public inputs. */
|
||||
fault = secp256k1_chilldkg_coordinator_step1(ctx, &cstate, cmsg1, &fault_index, pmsgs1_ptrs, hostpubkeys, 2, 2);
|
||||
SECP256K1_CHECKMEM_DEFINE(&fault, sizeof(fault));
|
||||
CHECK(fault == SECP256K1_CHILLDKG_OK);
|
||||
|
||||
/* Test chilldkg_participant_step2. The host secret key and the aux
|
||||
* randomness are secret; the CertEq signature is public. The state2
|
||||
* object contains the secret share and stays secret. */
|
||||
for (i = 0; i < 2; i++) {
|
||||
SECP256K1_CHECKMEM_UNDEFINE(hostseckeys[i], 32);
|
||||
SECP256K1_CHECKMEM_UNDEFINE(aux_rands[i], 32);
|
||||
fault = secp256k1_chilldkg_participant_step2(ctx, &state2[i], pmsgs2[i], &fault_index, NULL, &state1[i], hostseckeys[i], cmsg1, aux_rands[i]);
|
||||
SECP256K1_CHECKMEM_DEFINE(&fault, sizeof(fault));
|
||||
CHECK(fault == SECP256K1_CHILLDKG_OK);
|
||||
SECP256K1_CHECKMEM_DEFINE(pmsgs2[i], sizeof(pmsgs2[i]));
|
||||
}
|
||||
|
||||
fault = secp256k1_chilldkg_coordinator_finalize(ctx, cmsg2, thresh_pk33, pubshares33, recovery, &fault_index, &cstate, pmsgs2_ptrs);
|
||||
SECP256K1_CHECKMEM_DEFINE(&fault, sizeof(fault));
|
||||
CHECK(fault == SECP256K1_CHILLDKG_OK);
|
||||
|
||||
/* Test chilldkg_participant_finalize. The state2 input and the
|
||||
* secshare output are secret; the threshold public key, the public
|
||||
* shares and the recovery data are public. */
|
||||
fault = secp256k1_chilldkg_participant_finalize(ctx, secshare32, thresh_pk33, pubshares33, recovery, &fault_index, &state2[0], cmsg2);
|
||||
SECP256K1_CHECKMEM_DEFINE(&fault, sizeof(fault));
|
||||
CHECK(fault == SECP256K1_CHILLDKG_OK);
|
||||
SECP256K1_CHECKMEM_DEFINE(thresh_pk33, sizeof(thresh_pk33));
|
||||
SECP256K1_CHECKMEM_DEFINE(pubshares33, sizeof(pubshares33));
|
||||
SECP256K1_CHECKMEM_DEFINE(recovery, sizeof(recovery));
|
||||
|
||||
/* Test chilldkg_participant_recover. The host secret key and the
|
||||
* recovered secret share are secret; the recovery data and the
|
||||
* remaining outputs are public. */
|
||||
SECP256K1_CHECKMEM_UNDEFINE(hostseckeys[0], 32);
|
||||
fault = secp256k1_chilldkg_participant_recover(ctx, rec_secshare32, rec_thresh_pk33, rec_pubshares33, rec_hostpubkeys33, &rec_n, &rec_t, &fault_index, hostseckeys[0], recovery, sizeof(recovery));
|
||||
SECP256K1_CHECKMEM_DEFINE(&fault, sizeof(fault));
|
||||
CHECK(fault == SECP256K1_CHILLDKG_OK);
|
||||
SECP256K1_CHECKMEM_DEFINE(rec_thresh_pk33, sizeof(rec_thresh_pk33));
|
||||
SECP256K1_CHECKMEM_DEFINE(rec_pubshares33, 2 * 33);
|
||||
SECP256K1_CHECKMEM_DEFINE(rec_hostpubkeys33, 2 * 33);
|
||||
SECP256K1_CHECKMEM_DEFINE(&rec_n, sizeof(rec_n));
|
||||
SECP256K1_CHECKMEM_DEFINE(&rec_t, sizeof(rec_t));
|
||||
CHECK(rec_n == 2 && rec_t == 2);
|
||||
|
||||
/* Test chilldkg_recovery_ack_sign. The host secret key and the aux
|
||||
* randomness are secret; the acknowledgment signature is public. */
|
||||
SECP256K1_CHECKMEM_UNDEFINE(hostseckeys[0], 32);
|
||||
SECP256K1_CHECKMEM_UNDEFINE(aux_rands[0], 32);
|
||||
ret = secp256k1_chilldkg_recovery_ack_sign(ctx, ack_sig, hostseckeys[0], hostpubkeys, 2, 2, recovery, sizeof(recovery), aux_rands[0]);
|
||||
SECP256K1_CHECKMEM_DEFINE(&ret, sizeof(ret));
|
||||
CHECK(ret == 1);
|
||||
SECP256K1_CHECKMEM_DEFINE(ack_sig, sizeof(ack_sig));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(__GNUC__)
|
||||
|
||||
@@ -212,6 +212,10 @@ static int secp256k1_chilldkg_encpedpop_participant_step1(const secp256k1_contex
|
||||
secp256k1_ge pubnonce_ge;
|
||||
secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &pubnoncej, &secnonce);
|
||||
secp256k1_ge_set_gej(&pubnonce_ge, &pubnoncej);
|
||||
/* The pubnonce is public (it is part of the participant message);
|
||||
* declassify it so that the variable-time serialization is allowed
|
||||
* to branch on it. */
|
||||
secp256k1_declassify(ctx, &pubnonce_ge, sizeof(pubnonce_ge));
|
||||
secp256k1_chilldkg_point_save(pubnonce33, &pubnonce_ge);
|
||||
secp256k1_gej_clear(&pubnoncej);
|
||||
|
||||
|
||||
@@ -262,6 +262,9 @@ int secp256k1_chilldkg_hostpubkey_gen(const secp256k1_context *ctx, unsigned cha
|
||||
/* pubkey_gen_plain in the reference (IndividualPubkey of BIP 327). */
|
||||
secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &pubkeyj, &seckey);
|
||||
secp256k1_ge_set_gej(&pubkey, &pubkeyj);
|
||||
/* The host public key is not secret; declassify it so that the
|
||||
* variable-time serialization is allowed to branch on it. */
|
||||
secp256k1_declassify(ctx, &pubkey, sizeof(pubkey));
|
||||
secp256k1_chilldkg_point_save(hostpubkey33, &pubkey);
|
||||
secp256k1_gej_clear(&pubkeyj);
|
||||
}
|
||||
@@ -348,12 +351,18 @@ int secp256k1_chilldkg_participant_step1(const secp256k1_context *ctx, secp256k1
|
||||
}
|
||||
/* HostSeckeyError if there is no match; RandomnessError if the
|
||||
* randomness is all zeroes (guards against a malfunctioning random
|
||||
* number generator). */
|
||||
if (found && !secp256k1_is_zero_array(random32, 32)) {
|
||||
/* In EncPedPop, both the seed and the deckey are the host secret
|
||||
* key, and the enckeys are the host public keys (see
|
||||
* chilldkg_ref/chilldkg.py participant_step1). */
|
||||
ret = secp256k1_chilldkg_encpedpop_participant_step1(ctx, &enc_state, pmsg1, hostseckey32, hostseckey32, hostpubkeys33, threshold, participant_id, random32, n_participants);
|
||||
* number generator). Whether the randomness is all zeroes does not
|
||||
* leak anything about its value otherwise (if it is, the session
|
||||
* fails), so the check can be declassified. */
|
||||
{
|
||||
int random_is_zero = secp256k1_is_zero_array(random32, 32);
|
||||
secp256k1_declassify(ctx, &random_is_zero, sizeof(random_is_zero));
|
||||
if (found && !random_is_zero) {
|
||||
/* In EncPedPop, both the seed and the deckey are the host
|
||||
* secret key, and the enckeys are the host public keys (see
|
||||
* chilldkg_ref/chilldkg.py participant_step1). */
|
||||
ret = secp256k1_chilldkg_encpedpop_participant_step1(ctx, &enc_state, pmsg1, hostseckey32, hostseckey32, hostpubkeys33, threshold, participant_id, random32, n_participants);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -411,9 +411,17 @@ static secp256k1_chilldkg_fault secp256k1_chilldkg_simplpedpop_participant_inves
|
||||
for (i = 0; i < n; i++) {
|
||||
secp256k1_scalar_add(&secshare_sum, &secshare_sum, &partial_secshares[i]);
|
||||
}
|
||||
if (!secp256k1_scalar_eq(&secshare_sum, &inv_data->secshare)) {
|
||||
secp256k1_scalar_clear(&secshare_sum);
|
||||
return SECP256K1_CHILLDKG_FAULTY_COORDINATOR;
|
||||
/* We can declassify the result of the comparison because branching on it
|
||||
* only leaks whether the sum of the partial secshares equals the
|
||||
* secshare, which is exactly what the resulting public fault code
|
||||
* reveals. */
|
||||
{
|
||||
int sums_match = secp256k1_scalar_eq(&secshare_sum, &inv_data->secshare);
|
||||
secp256k1_declassify(ctx, &sums_match, sizeof(sums_match));
|
||||
if (!sums_match) {
|
||||
secp256k1_scalar_clear(&secshare_sum);
|
||||
return SECP256K1_CHILLDKG_FAULTY_COORDINATOR;
|
||||
}
|
||||
}
|
||||
secp256k1_scalar_clear(&secshare_sum);
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ static int secp256k1_chilldkg_schnorrsig_sign(const secp256k1_context *ctx, unsi
|
||||
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. */
|
||||
secp256k1_declassify(ctx, &pk, sizeof(pk));
|
||||
secp256k1_fe_normalize_var(&pk.x);
|
||||
secp256k1_fe_normalize_var(&pk.y);
|
||||
if (secp256k1_fe_is_odd(&pk.y)) {
|
||||
@@ -242,6 +243,10 @@ static int secp256k1_chilldkg_schnorrsig_sign(const secp256k1_context *ctx, unsi
|
||||
secp256k1_memclear_explicit(seckey, sizeof(seckey));
|
||||
secp256k1_memclear_explicit(nonce32, sizeof(nonce32));
|
||||
|
||||
/* 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));
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ static void secp256k1_chilldkg_vss_commit(const secp256k1_context *ctx, secp256k
|
||||
* in the point at infinity, matching the reference. */
|
||||
secp256k1_ecmult_gen_gej(&ctx->ecmult_gen_ctx, &comj, &coeffs[j]);
|
||||
secp256k1_ge_set_gej(&coms[j], &comj);
|
||||
/* The commitment is public (it is part of the participant message);
|
||||
* declassify it so that the variable-time serialization is allowed
|
||||
* to branch on it. */
|
||||
secp256k1_declassify(ctx, &coms[j], sizeof(coms[j]));
|
||||
secp256k1_gej_clear(&comj);
|
||||
}
|
||||
}
|
||||
@@ -121,6 +125,11 @@ static int secp256k1_chilldkg_vss_verify_secshare(const secp256k1_context *ctx,
|
||||
|
||||
VERIFY_CHECK(ctx != NULL);
|
||||
secp256k1_ecmult_gen_ge(&ctx->ecmult_gen_ctx, &actual, secshare);
|
||||
/* We can declassify because in honest runs actual = secshare*G is the
|
||||
* (public) pubshare itself, and because the verification result is always
|
||||
* public: it determines a public fault code. The discrete log of actual
|
||||
* (the secret share) is not revealed. */
|
||||
secp256k1_declassify(ctx, &actual, sizeof(actual));
|
||||
if (secp256k1_ge_is_infinity(&actual) || secp256k1_ge_is_infinity(pubshare)) {
|
||||
return secp256k1_ge_is_infinity(&actual) && secp256k1_ge_is_infinity(pubshare);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user