chilldkg: Phase 5 - recovery, recovery acks, investigation

Complete the ChillDKG protocol surface with the recovery and blame-
attribution procedures (bip-frost-dkg v0.3.0-dev, reference commit
a91896883f85b159415ecf298d5e844879af112d).

Recovery:
- secp256k1_chilldkg_participant_recover / _coordinator_recover: parse
  the self-delimiting recovery layout u32be(t) || sum_coms(33t) ||
  hostpubkeys(33n) || pubnonces(33n) || enc_secshares(32n, checked) ||
  cert(64n), deriving n = (len-4-33t)/162 exactly as the reference's
  deserialize_recovery_data; re-verify the certificate, recompute the
  TapTweak and the receiver's ECDH/self pads from (hostseckey,
  pubnonces, enc_context), recompute the tweaked secshare, and
  sanity-check secshare*G == pubshares[own]. Also return hostpubkeys,
  n and t so callers can re-derive session params. RecoveryDataError /
  HostSeckeyError / params failures map to INVALID_INPUT (no index,
  as in the reference); an invalid pubnonce during decrypt passes
  through as FAULTY_PARTICIPANT_OR_COORDINATOR(i), matching the
  reference leaking that exception from recover().

Recovery acks:
- secp256k1_chilldkg_recovery_ack_sign / _acks_verify: BIP-340
  (standard BIP0340 tags) over pad33("BIP DKG/recovery acknowledgment")
  || u32be(i) || recovery_data. Verification failure maps to
  FAULTY_PARTICIPANT(i) (InvalidRecoveryAckError subclasses
  FaultyParticipantError in the reference).

Investigation:
- secp256k1_chilldkg_coordinator_investigate: builds one 65n-byte
  per-participant message (per-dealer encrypted partial secshares
  (32n) + partial pubshares (33n)) per call; the reference returns all
  n at once -- equivalent, the caller iterates.
- secp256k1_chilldkg_participant_investigate: the reference's
  three-step blame attribution -- sum-of-pubshares check ->
  FAULTY_COORDINATOR; sum-of-secshares check -> FAULTY_COORDINATOR
  (covers the reference's SecshareSumError translation); per-dealer
  decrypted share vs commitment -> FAULTY_PARTICIPANT_OR_COORDINATOR(i)
  (or FAULTY_COORDINATOR for the own index); all-consistent ->
  INVALID_INPUT (the reference's uncaught RuntimeError).
- Investigation data is transported via a new opaque, secret-bearing
  secp256k1_chilldkg_participant_inv_data object (4205 bytes,
  magic-validated save/load, secret-cleared) filled by
  participant_step2 on the UNKNOWN_FAULT paths. This amends the
  Phase 3 participant_step2 signature with a nullable inv_data
  out-param -- required because recomputing inside
  participant_investigate would duplicate step2's decrypt/verify
  logic.
- New length helper secp256k1_chilldkg_investigation_msg_len (65n).

tests_impl.h: chilldkg_recovery_test (recover roundtrips byte-exact
vs the session outputs and reference vectors, tampered/truncated/
over-long recovery data, unknown/invalid hostseckey, ack sign
byte-exact + verify with wrong-index and tampered-ack blame,
params/recovery mismatch rejects, misuse) and
chilldkg_investigate_test (two end-to-end public-API scenarios
generated from the reference: dealer corrupting a participant's
encrypted share, and coordinator tampering with an enc_secshare;
cmsg1/cinv/inv_data byte-exact, blame codes and indices matching the
reference's exception type and index; malformed cinv ->
FAULTY_COORDINATOR; malformed pmsg1 -> FAULTY_PARTICIPANT(j); misuse).
The all-consistent investigate path is not constructible without
discrete logs and matches the reference's unreachable RuntimeError.

Verified: make check 3/3 (incl. noverify); CMake ctest 369/369;
make distdir clean.
This commit is contained in:
Kgothatso Ngako
2026-08-31 06:09:03 +02:00
parent 5409aae813
commit d48a1579cf
7 changed files with 1334 additions and 45 deletions

View File

@@ -112,6 +112,21 @@ typedef struct secp256k1_chilldkg_participant_state2 {
unsigned char data[12 + 4 + 131 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS + 32 + 33 + 33 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
} secp256k1_chilldkg_participant_state2;
/** Opaque data structure that holds the investigation data output by
* secp256k1_chilldkg_participant_step2 when it returns
* SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR, to be passed
* to secp256k1_chilldkg_participant_investigate together with the
* coordinator's investigation message.
*
* This structure contains secret data (the untweaked secret share and the
* decryption pads); it MUST be kept secret.
*
* Guaranteed to be 4205 bytes in size.
*/
typedef struct secp256k1_chilldkg_participant_inv_data {
unsigned char data[12 + 32 + 33 + 32 + 32 * SECP256K1_CHILLDKG_MAX_PARTICIPANTS];
} secp256k1_chilldkg_participant_inv_data;
/** Opaque data structure that holds the coordinator's session state after
* secp256k1_chilldkg_coordinator_step1, to be passed to
* secp256k1_chilldkg_coordinator_finalize (it must not be reused).
@@ -261,6 +276,12 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_participant_st
* fault_index: pointer to a uint32 that receives the identifier of
* the (suspected) faulty participant where applicable,
* and UINT32_MAX otherwise
* inv_data: pointer to a participant_inv_data object that
* receives the investigation data if (and only if) the
* return value is
* SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR,
* or NULL if the caller does not intend to run
* secp256k1_chilldkg_participant_investigate
* In: state1: pointer to the state1 object output by
* secp256k1_chilldkg_participant_step1
* hostseckey32: pointer to the 32-byte host secret key (must be the
@@ -276,11 +297,12 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_ch
secp256k1_chilldkg_participant_state2 *state2,
unsigned char *sig64,
uint32_t *fault_index,
secp256k1_chilldkg_participant_inv_data *inv_data,
const secp256k1_chilldkg_participant_state1 *state1,
const unsigned char *hostseckey32,
const unsigned char *cmsg1,
const unsigned char *aux_rand32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8);
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9);
/** Perform a participant's final step of a ChillDKG session.
*
@@ -400,6 +422,252 @@ SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_ch
const unsigned char *const *pmsgs2
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8);
/** Length of a coordinator investigation message (cinv): 65*n bytes (n
* encrypted partial secret shares of 32 bytes each, followed by n partial
* public shares of 33 bytes each). Returns 0 if n is out of range. */
SECP256K1_API size_t secp256k1_chilldkg_investigation_msg_len(
size_t n_participants
);
/** Recover the DKG output of a participant from recovery data.
*
* This function serves two purposes: recovering from a failure of
* secp256k1_chilldkg_participant_finalize after obtaining the recovery data
* from another participant or the coordinator, and reproducing the DKG
* outputs on a new device (e.g., after data loss).
*
* The recovery data (see secp256k1_chilldkg_recovery_data_len) is
* self-delimiting: the number of participants n and the threshold t are
* derived from its length and contents.
*
* Returns: SECP256K1_CHILLDKG_OK on success,
* SECP256K1_CHILLDKG_INVALID_INPUT if the recovery data is invalid
* (this includes an invalid certificate) or if the host secret key
* is invalid or does not match any host public key in the recovery
* data (RecoveryDataError and HostSeckeyError in the reference
* implementation), or
* SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR (with
* fault_index set) if a pubnonce in the recovery data is invalid.
* On failure, secshare32 and thresh_pk33 are set to zero and
* *n_participants_out and *threshold_out are set to 0; pubshares33
* and hostpubkeys33_out are set to zero whenever n could be
* determined.
* Args: ctx: pointer to a context object
* Out: secshare32: pointer to a 32-byte array to store the
* (tweaked) secret share
* thresh_pk33: pointer to a 33-byte array to store the
* threshold public key (compressed serialization)
* pubshares33: pointer to an array capable of holding
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
* to store the public shares (33 bytes each; the
* first n entries are filled)
* hostpubkeys33_out: pointer to an array capable of holding
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
* to store the host public keys of the recovered
* session (the first n entries are filled)
* n_participants_out: pointer to a size_t to store the number of
* participants n of the recovered session
* threshold_out: pointer to a uint32 to store the threshold t
* of the recovered session
* fault_index: pointer to a uint32 that receives the identifier
* of the suspected participant where applicable,
* and UINT32_MAX otherwise
* In: hostseckey32: pointer to the 32-byte host secret key
* recovery: pointer to the recovery data
* recovery_len: length of the recovery data
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_recover(
const secp256k1_context *ctx,
unsigned char *secshare32,
unsigned char *thresh_pk33,
unsigned char *pubshares33,
unsigned char *hostpubkeys33_out,
size_t *n_participants_out,
uint32_t *threshold_out,
uint32_t *fault_index,
const unsigned char *hostseckey32,
const unsigned char *recovery,
size_t recovery_len
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(8) SECP256K1_ARG_NONNULL(9) SECP256K1_ARG_NONNULL(10);
/** Recover the DKG output of the coordinator from recovery data.
*
* Like secp256k1_chilldkg_participant_recover, but for the coordinator, who
* has no secret share.
*
* Returns: SECP256K1_CHILLDKG_OK on success,
* SECP256K1_CHILLDKG_INVALID_INPUT if the recovery data is invalid.
* On failure, thresh_pk33 is set to zero and *n_participants_out
* and *threshold_out are set to 0; pubshares33 and
* hostpubkeys33_out are set to zero whenever n could be determined.
* Args: ctx: pointer to a context object
* Out: thresh_pk33: pointer to a 33-byte array to store the
* threshold public key (compressed serialization)
* pubshares33: pointer to an array capable of holding
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
* to store the public shares (the first n
* entries are filled)
* hostpubkeys33_out: pointer to an array capable of holding
* 33*SECP256K1_CHILLDKG_MAX_PARTICIPANTS bytes
* to store the host public keys of the recovered
* session (the first n entries are filled)
* n_participants_out: pointer to a size_t to store n
* threshold_out: pointer to a uint32 to store t
* In: recovery: pointer to the recovery data
* recovery_len: length of the recovery data
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_recover(
const secp256k1_context *ctx,
unsigned char *thresh_pk33,
unsigned char *pubshares33,
unsigned char *hostpubkeys33_out,
size_t *n_participants_out,
uint32_t *threshold_out,
const unsigned char *recovery,
size_t recovery_len
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(7);
/** Sign recovery data to create a recovery acknowledgment.
*
* The acknowledgment signature (64 bytes) is a BIP 340 signature over
* pad33("BIP DKG/recovery acknowledgment") || u32be(participant_id) ||
* recovery_data under the participant's host key. Acks can be collected in
* an optional acknowledgment round to confirm that all participants have
* received the recovery data.
*
* Returns: 1 on success, 0 on invalid input (invalid host secret key, host
* secret key not matching any host public key, invalid session
* parameters, or recovery data that is invalid or does not match
* the session parameters). On failure, sig64 is set to zero.
* Args: ctx: pointer to a context object
* Out: sig64: pointer to a 64-byte array to store the
* acknowledgment signature
* In: hostseckey32: pointer to the 32-byte host secret key
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
* each)
* n_participants: total number of participants n
* threshold: threshold t
* recovery: pointer to the recovery data
* recovery_len: length of the recovery data
* aux_rand32: pointer to 32 bytes of auxiliary randomness (see
* BIP 340)
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT int secp256k1_chilldkg_recovery_ack_sign(
const secp256k1_context *ctx,
unsigned char *sig64,
const unsigned char *hostseckey32,
const unsigned char *hostpubkeys33,
size_t n_participants,
uint32_t threshold,
const unsigned char *recovery,
size_t recovery_len,
const unsigned char *aux_rand32
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(7) SECP256K1_ARG_NONNULL(9);
/** Verify recovery acknowledgment signatures from all participants.
*
* Note that a failure of this function does NOT mean the DKG failed
* (reaching this point implies the DKG itself was successful). It only means
* it cannot be confirmed that all participants have a copy of the recovery
* data.
*
* Returns: SECP256K1_CHILLDKG_OK if all n acknowledgment signatures are
* valid, SECP256K1_CHILLDKG_INVALID_INPUT on invalid session
* parameters or recovery data, or SECP256K1_CHILLDKG_FAULTY_PARTICIPANT
* (with fault_index set to the signer) if an acknowledgment
* signature is invalid (InvalidRecoveryAckError in the reference
* implementation).
* Args: ctx: pointer to a context object
* Out: fault_index: pointer to a uint32 that receives the identifier of
* the participant whose signature is invalid, where
* applicable, and UINT32_MAX otherwise
* In: hostpubkeys33: pointer to an array of n host public keys (33 bytes
* each)
* n_participants: total number of participants n
* threshold: threshold t
* recovery: pointer to the recovery data
* recovery_len: length of the recovery data
* ack_sigs64: array of n pointers to the acknowledgment
* signatures (64 bytes each), in the same order as
* hostpubkeys33
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_recovery_acks_verify(
const secp256k1_context *ctx,
uint32_t *fault_index,
const unsigned char *hostpubkeys33,
size_t n_participants,
uint32_t threshold,
const unsigned char *recovery,
size_t recovery_len,
const unsigned char *const *ack_sigs64
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(6) SECP256K1_ARG_NONNULL(8);
/** Generate the investigation message for a single participant.
*
* The investigation message (65*n bytes, see
* secp256k1_chilldkg_investigation_msg_len) allows the given participant to
* investigate who is to blame for a failed ChillDKG session (see
* secp256k1_chilldkg_participant_investigate). The message contains no
* confidential information and can be safely broadcast. (Unlike the
* reference implementation, which computes the investigation messages for
* all participants at once, this function computes one message per call.)
*
* Returns: SECP256K1_CHILLDKG_OK on success,
* SECP256K1_CHILLDKG_INVALID_INPUT on invalid session parameters or
* participant_id >= n, or SECP256K1_CHILLDKG_FAULTY_PARTICIPANT
* (with fault_index set to the sender) if a participant message is
* malformed. On failure, cinv is set to zero.
* Args: ctx: pointer to a context object
* Out: cinv: pointer to a 65*n byte array to store the
* investigation message for the given participant
* fault_index: pointer to a uint32 (see above)
* In: pmsgs1: array of n pointers to the participants' first
* messages (33*t + 32*n + 97 bytes each)
* hostpubkeys33: pointer to an array of n host public keys (33 bytes
* each)
* n_participants: total number of participants n
* threshold: threshold t
* participant_id: the participant the investigation message is for
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_coordinator_investigate(
const secp256k1_context *ctx,
unsigned char *cinv,
uint32_t *fault_index,
const unsigned char *const *pmsgs1,
const unsigned char *hostpubkeys33,
size_t n_participants,
uint32_t threshold,
uint32_t participant_id
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4) SECP256K1_ARG_NONNULL(5);
/** Investigate who is to blame for a failed ChillDKG session.
*
* This function can be called when secp256k1_chilldkg_participant_step2
* returned SECP256K1_CHILLDKG_UNKNOWN_FAULTY_PARTICIPANT_OR_COORDINATOR. It
* narrows down the suspected faulty parties by analyzing the investigation
* message provided by the coordinator.
*
* Returns: SECP256K1_CHILLDKG_FAULTY_COORDINATOR if the coordinator is
* faulty, SECP256K1_CHILLDKG_FAULTY_PARTICIPANT_OR_COORDINATOR
* (with fault_index set to the suspected participant) if another
* participant or the coordinator is faulty, or
* SECP256K1_CHILLDKG_INVALID_INPUT if all inputs are consistent
* (i.e., the function was called even though no fault occurred).
* Args: ctx: pointer to a context object
* Out: fault_index: pointer to a uint32 (see above)
* In: inv_data: pointer to the participant_inv_data object output by
* secp256k1_chilldkg_participant_step2
* cinv: pointer to the coordinator's investigation message
* for this participant (65*n bytes, see
* secp256k1_chilldkg_investigation_msg_len)
*/
SECP256K1_API SECP256K1_WARN_UNUSED_RESULT secp256k1_chilldkg_fault secp256k1_chilldkg_participant_investigate(
const secp256k1_context *ctx,
uint32_t *fault_index,
const secp256k1_chilldkg_participant_inv_data *inv_data,
const unsigned char *cinv
) SECP256K1_ARG_NONNULL(1) SECP256K1_ARG_NONNULL(2) SECP256K1_ARG_NONNULL(3) SECP256K1_ARG_NONNULL(4);
#ifdef __cplusplus
}
#endif