diff --git a/README.md b/README.md index 892c912..ae3129c 100644 --- a/README.md +++ b/README.md @@ -779,6 +779,7 @@ Perform a participant's second step of a ChillDKG session. *Raises*: - `SecKeyError` - If the length of `hostseckey` is not 32 bytes. + FIXME - `FaultyParticipantError` - If `cmsg1` is invalid. This can happen if another participant has sent an invalid message to the coordinator, or if the coordinator has sent an invalid `cmsg1`. @@ -790,6 +791,7 @@ Perform a participant's second step of a ChillDKG session. coordinator is malicious or network connections are unreliable, and as a consequence, the caller should not conclude that the party hinted at is malicious. +- `UnknownFaultyPartyError` - TODO #### participant\_finalize @@ -834,6 +836,14 @@ of the success of the DKG session by presenting recovery data to us. - `SessionNotFinalizedError` - If finalizing the DKG session was not successful from this participant's perspective (see above). +#### participant\_blame + +```python +def participant_blame(hostseckey: bytes, state1: ParticipantState1, cmsg1: CoordinatorMsg1, blame_rec: BlameRecord) -> Tuple[DKGOutput, RecoveryData] +``` + +Perform a participant's blame step of a ChillDKG session. TODO + #### coordinator\_step1 ```python @@ -896,6 +906,14 @@ Perform the coordinator's final step of a ChillDKG session. received messages from other participants via a communication channel beside the coordinator (or be malicious). +#### coordinator\_blame + +```python +def coordinator_blame(pmsgs: List[ParticipantMsg1]) -> List[BlameRecord] +``` + +Perform the coordinator's blame step of a ChillDKG session. TODO + #### recover ```python diff --git a/python/chilldkg_ref/chilldkg.py b/python/chilldkg_ref/chilldkg.py index 769121f..141c6a2 100644 --- a/python/chilldkg_ref/chilldkg.py +++ b/python/chilldkg_ref/chilldkg.py @@ -26,6 +26,7 @@ ThresholdError, FaultyParticipantError, FaultyCoordinatorError, + UnknownFaultyPartyError, ) __all__ = [ @@ -43,6 +44,7 @@ "ThresholdError", "FaultyParticipantError", "FaultyCoordinatorError", + "UnknownFaultyPartyError", "InvalidRecoveryDataError", "DuplicateHostpubkeyError", "SessionNotFinalizedError", @@ -51,6 +53,7 @@ "DKGOutput", "ParticipantMsg1", "ParticipantMsg2", + "BlameRecord", "ParticipantState1", "ParticipantState2", "CoordinatorMsg1", @@ -256,7 +259,7 @@ def params_id(params: SessionParams) -> bytes: OverflowError: If `t >= 2^32` (so `t` cannot be serialized in 4 bytes). """ params_validate(params) - (hostpubkeys, t) = params + hostpubkeys, t = params t_bytes = t.to_bytes(4, byteorder="big") # OverflowError if t >= 2**32 params_id = tagged_hash_bip_dkg( @@ -308,6 +311,10 @@ class CoordinatorMsg2(NamedTuple): cert: bytes +class BlameRecord(NamedTuple): + enc_blame_rec: encpedpop.BlameRecord + + def deserialize_recovery_data( b: bytes, ) -> Tuple[int, VSSCommitment, List[bytes], List[bytes], List[Scalar], bytes]: @@ -448,6 +455,7 @@ def participant_step2( Raises: SecKeyError: If the length of `hostseckey` is not 32 bytes. + FIXME FaultyParticipantError: If `cmsg1` is invalid. This can happen if another participant has sent an invalid message to the coordinator, or if the coordinator has sent an invalid `cmsg1`. @@ -459,8 +467,9 @@ def participant_step2( coordinator is malicious or network connections are unreliable, and as a consequence, the caller should not conclude that the party hinted at is malicious. + UnknownFaultyPartyError: TODO """ - (params, idx, enc_state) = state1 + params, idx, enc_state = state1 enc_cmsg, enc_secshares = cmsg1 enc_dkg_output, eq_input = encpedpop.participant_step2( @@ -514,11 +523,28 @@ def participant_finalize( SessionNotFinalizedError: If finalizing the DKG session was not successful from this participant's perspective (see above). """ - (params, eq_input, dkg_output) = state2 + params, eq_input, dkg_output = state2 certeq_verify(params.hostpubkeys, eq_input, cmsg2.cert) # SessionNotFinalizedError return dkg_output, RecoveryData(eq_input + cmsg2.cert) +def participant_blame( + hostseckey: bytes, + state1: ParticipantState1, + cmsg1: CoordinatorMsg1, + blame_rec: BlameRecord, +) -> Tuple[DKGOutput, RecoveryData]: + """Perform a participant's blame step of a ChillDKG session. TODO""" + _, idx, enc_state = state1 + return encpedpop.participant_blame( + state=enc_state, + deckey=hostseckey, + cmsg=cmsg1.enc_cmsg, + enc_secshare=cmsg1.enc_secshares[idx], + blame_rec=blame_rec.enc_blame_rec, + ) + + ### ### Coordinator ### @@ -553,10 +579,12 @@ def coordinator_step1( OverflowError: If `t >= 2^32` (so `t` cannot be serialized in 4 bytes). """ params_validate(params) - (hostpubkeys, t) = params + hostpubkeys, t = params enc_cmsg, enc_dkg_output, eq_input, enc_secshares = encpedpop.coordinator_step( - pmsgs=[pmsg1.enc_pmsg for pmsg1 in pmsgs1], t=t, enckeys=hostpubkeys + pmsgs=[pmsg1.enc_pmsg for pmsg1 in pmsgs1], + t=t, + enckeys=hostpubkeys, ) eq_input += b"".join([bytes_from_int(int(share)) for share in enc_secshares]) dkg_output = DKGOutput._make(enc_dkg_output) # Convert to chilldkg.DKGOutput type @@ -589,12 +617,18 @@ def coordinator_finalize( received messages from other participants via a communication channel beside the coordinator (or be malicious). """ - (params, eq_input, dkg_output) = state + params, eq_input, dkg_output = state cert = certeq_coordinator_step([pmsg2.sig for pmsg2 in pmsgs2]) certeq_verify(params.hostpubkeys, eq_input, cert) # SessionNotFinalizedError return CoordinatorMsg2(cert), dkg_output, RecoveryData(eq_input + cert) +def coordinator_blame(pmsgs: List[ParticipantMsg1]) -> List[BlameRecord]: + """Perform the coordinator's blame step of a ChillDKG session. TODO""" + enc_blame_recs = encpedpop.coordinator_blame([pmsg.enc_pmsg for pmsg in pmsgs]) + return [BlameRecord(enc_blame_rec) for enc_blame_rec in enc_blame_recs] + + ### ### Recovery ### diff --git a/python/chilldkg_ref/encpedpop.py b/python/chilldkg_ref/encpedpop.py index 48c8c9c..dbb57f2 100644 --- a/python/chilldkg_ref/encpedpop.py +++ b/python/chilldkg_ref/encpedpop.py @@ -1,6 +1,6 @@ -from typing import Tuple, List, NamedTuple +from typing import Tuple, List, NamedTuple, NoReturn -from secp256k1proto.secp256k1 import Scalar +from secp256k1proto.secp256k1 import Scalar, GE from secp256k1proto.ecdh import ecdh_libsecp256k1 from secp256k1proto.keys import pubkey_gen_plain from secp256k1proto.util import int_from_bytes @@ -145,6 +145,11 @@ class CoordinatorMsg(NamedTuple): pubnonces: List[bytes] +class BlameRecord(NamedTuple): + enc_partial_secshares: List[Scalar] + partial_pubshares: List[GE] + + ### ### Participant ### @@ -221,6 +226,7 @@ def participant_step2( secshare = decrypt_sum( deckey, enckeys[idx], pubnonces, enc_context, idx, enc_secshare ) + dkg_output, eq_input = simplpedpop.participant_step2( simpl_state, simpl_cmsg, secshare ) @@ -228,6 +234,44 @@ def participant_step2( return dkg_output, eq_input +def participant_blame( + state: ParticipantState, + deckey: bytes, + cmsg: CoordinatorMsg, + enc_secshare: Scalar, + blame_rec: BlameRecord, +) -> NoReturn: + simpl_state, _, enckeys, idx = state + _, pubnonces = cmsg + enc_partial_secshares, partial_pubshares = blame_rec + + # Compute the encryption pads once and use them to decrypt both the + # enc_secshare and all enc_partial_secshares + enc_context = serialize_enc_context(simpl_state.t, enckeys) + pads = decaps_multi(deckey, enckeys[idx], pubnonces, enc_context, idx) + secshare = enc_secshare - Scalar.sum(*pads) + partial_secshares = [ + enc_partial_secshare - pad + for enc_partial_secshare, pad in zip(enc_partial_secshares, pads, strict=True) + ] + + simpl_blame_rec = simplpedpop.BlameRecord(partial_pubshares) + try: + simplpedpop.participant_blame( + simpl_state, secshare, partial_secshares, simpl_blame_rec + ) + except simplpedpop.SecshareSumError as e: + # The secshare is not equal to the sum of the partial secshares in the + # blame records. Since the encryption is additively homomorphic, this + # can only happen if the sum of the *encrypted* secshare is not equal + # to the sum of the encrypted partial sechares, which is the + # coordinator's fault. + assert Scalar.sum(*enc_partial_secshares) != enc_secshare + raise FaultyCoordinatorError( + "Sum of encrypted partial secshares not equal to encrypted secshare" + ) from e + + ### ### Coordinator ### @@ -241,9 +285,9 @@ def coordinator_step( n = len(enckeys) if n != len(pmsgs): raise ValueError - simpl_cmsg, dkg_output, eq_input = simplpedpop.coordinator_step( - [pmsg.simpl_pmsg for pmsg in pmsgs], t, n - ) + + simpl_pmsgs = [pmsg.simpl_pmsg for pmsg in pmsgs] + simpl_cmsg, dkg_output, eq_input = simplpedpop.coordinator_step(simpl_pmsgs, t, n) pubnonces = [pmsg.pubnonce for pmsg in pmsgs] for i in range(n): if len(pmsgs[i].enc_shares) != n: @@ -254,6 +298,7 @@ def coordinator_step( Scalar.sum(*([pmsg.enc_shares[i] for pmsg in pmsgs])) for i in range(n) ] eq_input += b"".join(enckeys) + b"".join(pubnonces) + # In ChillDKG, the coordinator needs to broadcast the entire enc_secshares # array to all participants. But in pure EncPedPop, the coordinator needs to # send to each participant i only their entry enc_secshares[i]. @@ -269,3 +314,18 @@ def coordinator_step( eq_input, enc_secshares, ) + + +def coordinator_blame(pmsgs: List[ParticipantMsg]) -> List[BlameRecord]: + n = len(pmsgs) + simpl_pmsgs = [pmsg.simpl_pmsg for pmsg in pmsgs] + + all_enc_partial_secshares = [ + [pmsg.enc_shares[i] for pmsg in pmsgs] for i in range(n) + ] + simpl_blame_recs = simplpedpop.coordinator_blame(simpl_pmsgs) + blame_recs = [ + BlameRecord(all_enc_partial_secshares[i], simpl_blame_recs[i].partial_pubshares) + for i in range(n) + ] + return blame_recs diff --git a/python/chilldkg_ref/simplpedpop.py b/python/chilldkg_ref/simplpedpop.py index ddd7b2f..6da27e8 100644 --- a/python/chilldkg_ref/simplpedpop.py +++ b/python/chilldkg_ref/simplpedpop.py @@ -1,5 +1,5 @@ from secrets import token_bytes as random_bytes -from typing import List, NamedTuple, NewType, Tuple, Optional +from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn from secp256k1proto.bip340 import schnorr_sign, schnorr_verify from secp256k1proto.secp256k1 import GE, Scalar @@ -14,6 +14,15 @@ from .vss import VSS, VSSCommitment +### +### Exceptions +### + + +class SecshareSumError(ValueError): + pass + + ### ### Proofs of possession (pops) ### @@ -63,6 +72,10 @@ def to_bytes(self) -> bytes: ) + b"".join(self.pops) +class BlameRecord(NamedTuple): + partial_pubshares: List[GE] + + ### ### Other common definitions ### @@ -145,7 +158,7 @@ def participant_step1( def participant_step2_prepare_secshare( partial_secshares: List[Scalar], ) -> Scalar: - secshare = Scalar.sum(*partial_secshares) + secshare: Scalar = Scalar.sum(*partial_secshares) return secshare @@ -182,10 +195,14 @@ def participant_step2( ) sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms, n) threshold_pubkey = sum_coms.commitment_to_secret() - pubshares = [sum_coms.pubshare(i) for i in range(n)] - if not VSSCommitment.verify_secshare(secshare, pubshares[idx]): - raise UnknownFaultyPartyError("Received invalid secshare") + pubshare = sum_coms.pubshare(idx) + if not VSSCommitment.verify_secshare(secshare, pubshare): + raise UnknownFaultyPartyError( + "Received invalid secshare, consider blaming to determine faulty party", + ) + + pubshares = [sum_coms.pubshare(i) if i != idx else pubshare for i in range(n)] dkg_output = DKGOutput( secshare.to_bytes(), threshold_pubkey.to_bytes_compressed(), @@ -195,6 +212,45 @@ def participant_step2( return dkg_output, eq_input +def participant_blame( + state: ParticipantState, + secshare: Scalar, + partial_secshares: List[Scalar], + blame_rec: BlameRecord, +) -> NoReturn: + _, n, idx, _ = state + partial_pubshares = blame_rec.partial_pubshares + + if Scalar.sum(*partial_secshares) != secshare: + raise SecshareSumError("Sum of partial secshares not equal to secshare") + + for i in range(n): + if not VSSCommitment.verify_secshare( + partial_secshares[i], partial_pubshares[i] + ): + if i != idx: + raise FaultyParticipantError( + i, "Participant sent invalid partial secshare" + ) + else: + # We are not faulty, so the coordinator must be. + raise FaultyCoordinatorError( + "Coordinator fiddled with the share from me to myself" + ) + + # We now know: + # - The sum of the partial secshares is equal to the secshare. + # - Every partial secshare matches its corresponding partial pubshare. + # - The sum of the partial pubshares is not equal to the pubshare (because + # the caller shouldn't have called us otherwise). + # Therefore, the sum of the partial pubshares is not equal to the pubshare, + # and this is the coordinator's fault. + raise FaultyCoordinatorError( + "Sum of partial pubshares not equal to pubshare (or participant_blame() " + "was called even though participant_step2() was successful)" + ) + + ### ### Coordinator ### @@ -221,6 +277,7 @@ def coordinator_step( sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms, n) threshold_pubkey = sum_coms.commitment_to_secret() pubshares = [sum_coms.pubshare(i) for i in range(n)] + dkg_output = DKGOutput( None, threshold_pubkey.to_bytes_compressed(), @@ -228,3 +285,9 @@ def coordinator_step( ) eq_input = t.to_bytes(4, byteorder="big") + sum_coms.to_bytes() return cmsg, dkg_output, eq_input + + +def coordinator_blame(pmsgs: List[ParticipantMsg]) -> List[BlameRecord]: + n = len(pmsgs) + all_partial_pubshares = [[pmsg.com.pubshare(i) for pmsg in pmsgs] for i in range(n)] + return [BlameRecord(all_partial_pubshares[i]) for i in range(n)] diff --git a/python/example.py b/python/example.py index 3e63ae9..59718bb 100755 --- a/python/example.py +++ b/python/example.py @@ -35,6 +35,10 @@ def __init__(self, n): def set_participant_queues(self, participant_queues): self.participant_queues = participant_queues + def send_to(self, i, m): + assert self.participant_queues is not None + self.participant_queues[i].put_nowait(m) + def send_all(self, m): assert self.participant_queues is not None for i in range(self.n): @@ -65,7 +69,10 @@ async def receive(self): async def participant( - chan: ParticipantChannel, hostseckey: bytes, params: SessionParams + chan: ParticipantChannel, + hostseckey: bytes, + params: SessionParams, + blame: bool = True, ) -> Tuple[DKGOutput, RecoveryData]: # TODO Top-level error handling random = random_bytes(32) @@ -73,6 +80,12 @@ async def participant( chan.send(pmsg1) cmsg1 = await chan.receive() + # TODO + # if blame: + # blame_rec = await chan.receive() + # else: + # blame_rec = None + state2, eq_round1 = participant_step2(hostseckey, state1, cmsg1) chan.send(eq_round1) @@ -93,6 +106,11 @@ async def coordinator( state, cmsg1 = coordinator_step1(pmsgs1, params) chans.send_all(cmsg1) + # TODO + # if blame: + # for i in range(n): + # chans.send_to(i, blame_recs[i]) + sigs = [] for i in range(n): sigs += [await chans.receive_from(i)] @@ -118,6 +136,7 @@ def simulate_chilldkg_full(hostseckeys, t) -> List[Tuple[DKGOutput, RecoveryData params = SessionParams(hostpubkeys, t) async def session(): + # TODO Blame coord_chans = CoordinatorChannels(n) participant_chans = [ ParticipantChannel(coord_chans.queues[i]) for i in range(n) diff --git a/python/tests.py b/python/tests.py index 3783b71..1602b90 100755 --- a/python/tests.py +++ b/python/tests.py @@ -10,7 +10,7 @@ from secp256k1proto.secp256k1 import GE, G, Scalar from secp256k1proto.keys import pubkey_gen_plain -from chilldkg_ref.util import prf +from chilldkg_ref.util import prf, FaultyCoordinatorError from chilldkg_ref.vss import Polynomial, VSS, VSSCommitment import chilldkg_ref.simplpedpop as simplpedpop import chilldkg_ref.encpedpop as encpedpop @@ -40,16 +40,31 @@ def simulate_simplpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: prets = [] for i in range(n): prets += [simplpedpop.participant_step1(seeds[i], t, n, i)] - pmsgs = [ret[1] for ret in prets] + + pstates = [pstate for (pstate, _, _) in prets] + pmsgs = [pmsg for (_, pmsg, _) in prets] cmsg, cout, ceq = simplpedpop.coordinator_step(pmsgs, t, n) + blame_recs = simplpedpop.coordinator_blame(pmsgs) pre_finalize_rets = [(cout, ceq)] for i in range(n): - partial_secshares = [pret[2][i] for pret in prets] - secshare = simplpedpop.participant_step2_prepare_secshare(partial_secshares) - pre_finalize_rets += [ - simplpedpop.participant_step2(prets[i][0], cmsg, secshare) + partial_secshares = [ + partial_secshares_for[i] for (_, _, partial_secshares_for) in prets ] + # TODO Test that the protocol fails when wrong shares are sent. + # if i == n - 1: + # partial_secshares[-1] += Scalar(17) + secshare = simplpedpop.participant_step2_prepare_secshare(partial_secshares) + pre_finalize_rets += [simplpedpop.participant_step2(pstates[i], cmsg, secshare)] + # This was a correct run, so blame should fail. + try: + simplpedpop.participant_blame( + pstates[i], secshare, partial_secshares, blame_recs[i] + ) + except FaultyCoordinatorError: + pass + else: + assert False return pre_finalize_rets @@ -78,12 +93,22 @@ def simulate_encpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: pstates = [pstate for (pstate, _) in enc_prets1] cmsg, cout, ceq, enc_secshares = encpedpop.coordinator_step(pmsgs, t, enckeys) + blame_recs = encpedpop.coordinator_blame(pmsgs) pre_finalize_rets = [(cout, ceq)] for i in range(n): deckey = enc_prets0[i][0] pre_finalize_rets += [ encpedpop.participant_step2(pstates[i], deckey, cmsg, enc_secshares[i]) ] + try: + encpedpop.participant_blame( + pstates[i], deckey, cmsg, enc_secshares[i], blame_recs[i] + ) + # This was a correct run, so blame should fail. + except FaultyCoordinatorError: + pass + else: + assert False return pre_finalize_rets @@ -105,11 +130,21 @@ def simulate_chilldkg( pstates1 = [pret[0] for pret in prets1] pmsgs = [pret[1] for pret in prets1] - cstate, cmsg = chilldkg.coordinator_step1(pmsgs, params) + cstate, cmsg1 = chilldkg.coordinator_step1(pmsgs, params) + blame_recs = chilldkg.coordinator_blame(pmsgs) prets2 = [] for i in range(n): - prets2 += [chilldkg.participant_step2(hostseckeys[i], pstates1[i], cmsg)] + prets2 += [chilldkg.participant_step2(hostseckeys[i], pstates1[i], cmsg1)] + # This was a correct run, so blame should fail. + try: + chilldkg.participant_blame( + hostseckeys[i], pstates1[i], cmsg1, blame_recs[i] + ) + except FaultyCoordinatorError: + pass + else: + assert False cmsg2, cout, crec = chilldkg.coordinator_finalize( cstate, [pret[1] for pret in prets2]