diff --git a/README.md b/README.md index 892c912..96f5642 100644 --- a/README.md +++ b/README.md @@ -754,7 +754,7 @@ Perform a participant's first step of a ChillDKG session. #### participant\_step2 ```python -def participant_step2(hostseckey: bytes, state1: ParticipantState1, cmsg1: CoordinatorMsg1) -> Tuple[ParticipantState2, ParticipantMsg2] +def participant_step2(hostseckey: bytes, state1: ParticipantState1, cmsg1: CoordinatorMsg1, blame_rec: Optional[encpedpop.BlameRecord]) -> Tuple[ParticipantState2, ParticipantMsg2] ``` Perform a participant's second step of a ChillDKG session. @@ -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`. @@ -837,7 +838,7 @@ of the success of the DKG session by presenting recovery data to us. #### coordinator\_step1 ```python -def coordinator_step1(pmsgs1: List[ParticipantMsg1], params: SessionParams) -> Tuple[CoordinatorState, CoordinatorMsg1] +def coordinator_step1(pmsgs1: List[ParticipantMsg1], params: SessionParams, blame: bool = True) -> Tuple[CoordinatorState, CoordinatorMsg1, List[Optional[encpedpop.BlameRecord]]] ``` Perform the coordinator's first step of a ChillDKG session. diff --git a/python/chilldkg_ref/chilldkg.py b/python/chilldkg_ref/chilldkg.py index a0b2cd3..371d364 100644 --- a/python/chilldkg_ref/chilldkg.py +++ b/python/chilldkg_ref/chilldkg.py @@ -430,6 +430,7 @@ def participant_step2( hostseckey: bytes, state1: ParticipantState1, cmsg1: CoordinatorMsg1, + blame_rec: Optional[encpedpop.BlameRecord], ) -> Tuple[ParticipantState2, ParticipantMsg2]: """Perform a participant's second step of a ChillDKG session. @@ -448,6 +449,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`. @@ -468,6 +470,7 @@ def participant_step2( deckey=hostseckey, cmsg=enc_cmsg, enc_secshare=enc_secshares[idx], + blame_rec=blame_rec, ) # Include the enc_shares in eq_input to ensure that participants agree on all # shares, which in turn ensures that they have the right recovery data. @@ -531,8 +534,8 @@ class CoordinatorState(NamedTuple): def coordinator_step1( - pmsgs1: List[ParticipantMsg1], params: SessionParams -) -> Tuple[CoordinatorState, CoordinatorMsg1]: + pmsgs1: List[ParticipantMsg1], params: SessionParams, blame: bool = True +) -> Tuple[CoordinatorState, CoordinatorMsg1, List[Optional[encpedpop.BlameRecord]]]: """Perform the coordinator's first step of a ChillDKG session. Arguments: @@ -555,14 +558,19 @@ def coordinator_step1( params_validate(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 + enc_cmsg, enc_dkg_output, eq_input, enc_secshares, blame_recs = ( + encpedpop.coordinator_step( + pmsgs=[pmsg1.enc_pmsg for pmsg1 in pmsgs1], + t=t, + enckeys=hostpubkeys, + blame=blame, + ) ) 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 state = CoordinatorState(params, eq_input, dkg_output) cmsg1 = CoordinatorMsg1(enc_cmsg, enc_secshares) - return state, cmsg1 + return state, cmsg1, blame_recs def coordinator_finalize( diff --git a/python/chilldkg_ref/encpedpop.py b/python/chilldkg_ref/encpedpop.py index 5eecadf..30fcaab 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, Optional, cast -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 @@ -159,6 +159,11 @@ class CoordinatorMsg(NamedTuple): pubnonces: List[bytes] +class BlameRecord(NamedTuple): + enc_partial_secshares: List[Scalar] + partial_pubshares: List[GE] + + ### ### Participant ### @@ -223,9 +228,11 @@ def participant_step2( deckey: bytes, cmsg: CoordinatorMsg, enc_secshare: Scalar, + blame_rec: Optional[BlameRecord] = None, ) -> Tuple[simplpedpop.DKGOutput, bytes]: simpl_state, pubnonce, enckeys, idx = state simpl_cmsg, pubnonces = cmsg + n = len(enckeys) reported_pubnonce = pubnonces[idx] if reported_pubnonce != pubnonce: @@ -235,9 +242,38 @@ 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 - ) + + if blame_rec is not None: + enc_partial_secshares, partial_pubshares = blame_rec + partial_secshares = [ + decrypt( + deckey, + enckeys[idx], + pubnonces[i], + enc_context, + idx, + i, + enc_partial_secshares[i], + ) + for i in range(n) + ] + simpl_blame_rec = simplpedpop.BlameRecord(partial_secshares, partial_pubshares) + else: + simpl_blame_rec = None + + try: + dkg_output, eq_input = simplpedpop.participant_step2( + simpl_state, simpl_cmsg, secshare, simpl_blame_rec + ) + except simplpedpop.InconsistentSecsharesError 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. + assert Scalar.sum(*enc_partial_secshares) != enc_secshare + raise FaultyCoordinatorError( + "Sum of encrypted partial secshares not equal to encrypted secshare" + ) from e eq_input += b"".join(enckeys) + b"".join(pubnonces) return dkg_output, eq_input @@ -251,12 +287,19 @@ def coordinator_step( pmsgs: List[ParticipantMsg], t: int, enckeys: List[bytes], -) -> Tuple[CoordinatorMsg, simplpedpop.DKGOutput, bytes, List[Scalar]]: + blame: bool = True, +) -> Tuple[ + CoordinatorMsg, + simplpedpop.DKGOutput, + bytes, + List[Scalar], + List[Optional[BlameRecord]], +]: 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_cmsg, dkg_output, eq_input, all_partial_pubshares = ( + simplpedpop.coordinator_step([pmsg.simpl_pmsg for pmsg in pmsgs], t, n, blame) ) pubnonces = [pmsg.pubnonce for pmsg in pmsgs] for i in range(n): @@ -267,6 +310,30 @@ def coordinator_step( enc_secshares = [ Scalar.sum(*([pmsg.enc_shares[i] for pmsg in pmsgs])) for i in range(n) ] + + blame_recs: List[Optional[BlameRecord]] + if blame: + # We called simplpedpop.coordinator_step(..., blame=True), which is + # supposed to return proper partial_pubshares. + assert all( + [ + all_partial_pubshares[i][j] is not None + for i in range(n) + for j in range(n) + ] + ) + all_enc_partial_secshares = [ + [pmsg.enc_shares[i] for pmsg in pmsgs] for i in range(n) + ] + blame_recs = [ + BlameRecord( + all_enc_partial_secshares[i], cast(List[GE], all_partial_pubshares[i]) + ) + for i in range(n) + ] + else: + blame_recs = [None 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 @@ -277,4 +344,10 @@ def coordinator_step( # chilldkg.coordinator_step can pick it up. Implementations of pure # EncPedPop will need to decide how to transmit enc_secshares[i] to # participant i; we leave this unspecified. - return CoordinatorMsg(simpl_cmsg, pubnonces), dkg_output, eq_input, enc_secshares + return ( + CoordinatorMsg(simpl_cmsg, pubnonces), + dkg_output, + eq_input, + enc_secshares, + blame_recs, + ) diff --git a/python/chilldkg_ref/simplpedpop.py b/python/chilldkg_ref/simplpedpop.py index de89622..9b3b526 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, cast from secp256k1proto.bip340 import schnorr_sign, schnorr_verify from secp256k1proto.secp256k1 import GE, Scalar @@ -13,6 +13,15 @@ from .vss import VSS, VSSCommitment +### +### Exceptions +### + + +class InconsistentSecsharesError(ValueError): + pass + + ### ### Proofs of possession (pops) ### @@ -62,6 +71,11 @@ def to_bytes(self) -> bytes: ) + b"".join(self.pops) +class BlameRecord(NamedTuple): + partial_secshares: List[Scalar] + partial_pubshares: List[GE] + + ### ### Other common definitions ### @@ -100,7 +114,7 @@ class ParticipantState(NamedTuple): def participant_step1( - seed: bytes, t: int, n: int, idx: int + seed: bytes, t: int, n: int, idx: int, blame: bool = True ) -> Tuple[ ParticipantState, ParticipantMsg, @@ -125,12 +139,16 @@ def participant_step1( com_to_secret = com.commitment_to_secret() msg = ParticipantMsg(com, pop) state = ParticipantState(t, n, idx, com_to_secret) + return state, msg, partial_secshares_from_me # Helper function to prepare the secret side inputs for participant idx's -# participant_step2() from the partial_secshares returned by all participants' -# participant_step1(). +# participant_step2() from +# - the list of all partial_secshares[idx] values from participants' +# participant_step1(), and # FIXME terms are wrong here +# - the partial_pubshares list from the coordinator's coordinator_step() +# (if not blaming, this is a list containing n times None). # # This computation cannot be done entirely by the SimplPedPop coordinator # because it involves secret shares. In a pure run of SimplPedPop where secret @@ -141,16 +159,28 @@ def participant_step1( # take care of this preparation by exploiting the homomorphic property of the # encryption. def participant_step2_prepare_secret_side_inputs( - partial_secshares: List[Scalar], -) -> Scalar: + partial_secshares: List[Scalar], partial_pubshares: List[Optional[GE]] +) -> Tuple[Scalar, Optional[BlameRecord]]: + ## FIXME take n from state, amend other commit + n = len(partial_secshares) secshare = Scalar.sum(*partial_secshares) - return secshare + if not len(partial_secshares) == len(partial_pubshares) == n: + raise ValueError + # blame_rec: Optional[BlameRecord] + if partial_pubshares[0] is not None: + if not all([p is not None for p in partial_pubshares]): + raise ValueError + blame_rec = BlameRecord(partial_secshares, cast(List[GE], partial_pubshares)) + else: + blame_rec = None + return secshare, blame_rec def participant_step2( state: ParticipantState, cmsg: CoordinatorMsg, secshare: Scalar, + blame_rec: Optional[BlameRecord] = None, ) -> Tuple[DKGOutput, bytes]: t, n, idx, com_to_secret = state coms_to_secrets, sum_coms_to_nonconst_terms, pops = cmsg @@ -181,8 +211,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 FaultyParticipantError(None, "Received invalid secshare") + if blame_rec is not None: + _participant_step2_blame(secshare, pubshares, idx, blame_rec) + else: + raise FaultyParticipantError( + None, "Received invalid secshare, consider rerunning in blame mode" + ) dkg_output = DKGOutput( secshare.to_bytes(), @@ -193,15 +229,47 @@ def participant_step2( return dkg_output, eq_input +def _participant_step2_blame( + secshare: Scalar, pubshares: List[GE], idx: int, blame_rec: BlameRecord +) -> NoReturn: + partial_secshares, partial_pubshares = blame_rec + n = len(pubshares) + if Scalar.sum(*partial_secshares) != secshare: + raise InconsistentSecsharesError + # The following check can safely be omitted, because we trust the + # coordinator for computing the partial_pubshares correctly anyway. Or, in + # other words, the coordinator can anyway make us blame some innocent + # participant. We keep it because it may help debugging benign failures. + if GE.sum(*partial_pubshares) != pubshares[idx]: + raise FaultyCoordinatorError("Sum of partial pubshares not equal to pubshare") + 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 it must be the coordinator. + raise FaultyCoordinatorError( + "Coordinator fiddled with the share from me to myself" + ) + assert False, "unreachable" + + ### ### Coordinator ### +## FIXME document the last return value. Or can we make it an +## Optional[List[List[GE]]] instead? That's more elegant but the type checker +## didn't like me when I had tried this earlier. def coordinator_step( - pmsgs: List[ParticipantMsg], t: int, n: int -) -> Tuple[CoordinatorMsg, DKGOutput, bytes]: - # Sum the commitments to the i-th coefficients for i > 0 + pmsgs: List[ParticipantMsg], t: int, n: int, blame: bool = True +) -> Tuple[CoordinatorMsg, DKGOutput, bytes, List[List[Optional[GE]]]]: + # Sum the commitments to the i-th coefficients for i > 0 # FIXME # # This procedure is introduced by Pedersen in Section 5.1 of # 'Non-Interactive and Information-Theoretic Secure Verifiable Secret @@ -221,10 +289,17 @@ 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)] + + partial_pubshares: List[List[Optional[GE]]] + if blame: + partial_pubshares = [[pmsg.com.pubshare(i) for pmsg in pmsgs] for i in range(n)] + else: + partial_pubshares = [[None for pmsg in pmsgs] for i in range(n)] + dkg_output = DKGOutput( None, threshold_pubkey.to_bytes_compressed(), [pubshare.to_bytes_compressed() for pubshare in pubshares], ) eq_input = t.to_bytes(4, byteorder="big") + sum_coms.to_bytes() - return cmsg, dkg_output, eq_input + return cmsg, dkg_output, eq_input, partial_pubshares diff --git a/python/example.py b/python/example.py index 3e63ae9..ed3726a 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,7 +80,12 @@ async def participant( chan.send(pmsg1) cmsg1 = await chan.receive() - state2, eq_round1 = participant_step2(hostseckey, state1, cmsg1) + if blame: + blame_rec = await chan.receive() + else: + blame_rec = None + + state2, eq_round1 = participant_step2(hostseckey, state1, cmsg1, blame_rec) chan.send(eq_round1) cmsg2 = await chan.receive() @@ -82,7 +94,7 @@ async def participant( async def coordinator( - chans: CoordinatorChannels, params: SessionParams + chans: CoordinatorChannels, params: SessionParams, blame: bool = True ) -> Tuple[DKGOutput, RecoveryData]: (hostpubkeys, t) = params n = len(hostpubkeys) @@ -90,9 +102,13 @@ async def coordinator( pmsgs1 = [] for i in range(n): pmsgs1.append(await chans.receive_from(i)) - state, cmsg1 = coordinator_step1(pmsgs1, params) + state, cmsg1, blame_recs = coordinator_step1(pmsgs1, params, blame) chans.send_all(cmsg1) + 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)] @@ -107,7 +123,9 @@ async def coordinator( # -def simulate_chilldkg_full(hostseckeys, t) -> List[Tuple[DKGOutput, RecoveryData]]: +def simulate_chilldkg_full( + hostseckeys, t, blame=True +) -> List[Tuple[DKGOutput, RecoveryData]]: # Generate common inputs for all participants and coordinator n = len(hostseckeys) hostpubkeys = [] @@ -125,8 +143,9 @@ async def session(): coord_chans.set_participant_queues( [participant_chans[i].queue for i in range(n)] ) - coroutines = [coordinator(coord_chans, params)] + [ - participant(participant_chans[i], hostseckeys[i], params) for i in range(n) + coroutines = [coordinator(coord_chans, params, blame)] + [ + participant(participant_chans[i], hostseckeys[i], params, blame) + for i in range(n) ] return await asyncio.gather(*coroutines) diff --git a/python/tests.py b/python/tests.py index 946af33..c171eb0 100755 --- a/python/tests.py +++ b/python/tests.py @@ -35,22 +35,26 @@ def rand_polynomial(t): ) -def simulate_simplpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: +def simulate_simplpedpop(seeds, t, blame) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: n = len(seeds) prets = [] for i in range(n): - prets += [simplpedpop.participant_step1(seeds[i], t, n, i)] + prets += [simplpedpop.participant_step1(seeds[i], t, n, i, blame)] pmsgs = [ret[1] for ret in prets] - cmsg, cout, ceq = simplpedpop.coordinator_step(pmsgs, t, n) + cmsg, cout, ceq, all_partial_pubshares = simplpedpop.coordinator_step(pmsgs, t, n) pre_finalize_rets = [(cout, ceq)] for i in range(n): partial_secshares = [pret[2][i] for pret in prets] - secshare = simplpedpop.participant_step2_prepare_secret_side_inputs( - partial_secshares + partial_pubshares = all_partial_pubshares[i] + # TODO Test that the protocol fails when wrong shares are sent. + # if i == n - 1: + # partial_secshares[-1] += Scalar(17) + secshare, blame_rec = simplpedpop.participant_step2_prepare_secret_side_inputs( + partial_secshares, partial_pubshares ) pre_finalize_rets += [ - simplpedpop.participant_step2(prets[i][0], cmsg, secshare) + simplpedpop.participant_step2(prets[i][0], cmsg, secshare, blame_rec) ] return pre_finalize_rets @@ -61,7 +65,7 @@ def encpedpop_keys(seed: bytes) -> Tuple[bytes, bytes]: return deckey, enckey -def simulate_encpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: +def simulate_encpedpop(seeds, t, blame) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: n = len(seeds) enc_prets0 = [] enc_prets1 = [] @@ -79,18 +83,22 @@ def simulate_encpedpop(seeds, t) -> List[Tuple[simplpedpop.DKGOutput, bytes]]: pmsgs = [pmsg for (_, pmsg) in enc_prets1] pstates = [pstate for (pstate, _) in enc_prets1] - cmsg, cout, ceq, enc_secshares = encpedpop.coordinator_step(pmsgs, t, enckeys) + cmsg, cout, ceq, enc_secshares, blame_recs = encpedpop.coordinator_step( + pmsgs, t, enckeys, blame + ) 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]) + encpedpop.participant_step2( + pstates[i], deckey, cmsg, enc_secshares[i], blame_recs[i] + ) ] return pre_finalize_rets def simulate_chilldkg( - hostseckeys, t + hostseckeys, t, blame ) -> List[Tuple[chilldkg.DKGOutput, chilldkg.RecoveryData]]: n = len(hostseckeys) @@ -107,11 +115,13 @@ 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, cmsg, blame_recs = chilldkg.coordinator_step1(pmsgs, params, blame) prets2 = [] for i in range(n): - prets2 += [chilldkg.participant_step2(hostseckeys[i], pstates1[i], cmsg)] + prets2 += [ + chilldkg.participant_step2(hostseckeys[i], pstates1[i], cmsg, blame_recs[i]) + ] cmsg2, cout, crec = chilldkg.coordinator_finalize( cstate, [pret[1] for pret in prets2] @@ -188,12 +198,12 @@ def test_correctness_dkg_output(t, n, dkg_outputs: List[simplpedpop.DKGOutput]): assert recovered * G == GE.from_bytes_compressed(threshold_pubkey) -def test_correctness(t, n, simulate_dkg, recovery=False): +def test_correctness(t, n, simulate_dkg, recovery=False, blame=True): seeds = [None] + [random_bytes(32) for _ in range(n)] # rets[0] are the return values from the coordinator # rets[1 : n + 1] are from the participants - rets = simulate_dkg(seeds[1:], t) + rets = simulate_dkg(seeds[1:], t, blame=blame) assert len(rets) == n + 1 dkg_outputs = [ret[0] for ret in rets] @@ -216,7 +226,8 @@ def test_correctness(t, n, simulate_dkg, recovery=False): test_vss_correctness() test_recover_secret() for t, n in [(1, 1), (1, 2), (2, 2), (2, 3), (2, 5)]: - test_correctness(t, n, simulate_simplpedpop) - test_correctness(t, n, simulate_encpedpop) - test_correctness(t, n, simulate_chilldkg, recovery=True) - test_correctness(t, n, simulate_chilldkg_full, recovery=True) + for blame in [False, True]: + test_correctness(t, n, simulate_simplpedpop, blame=blame) + test_correctness(t, n, simulate_encpedpop, blame=blame) + test_correctness(t, n, simulate_chilldkg, blame=blame, recovery=True) + test_correctness(t, n, simulate_chilldkg_full, blame=blame, recovery=True)