Skip to content

Commit

Permalink
Prevent malicious taproot commitment
Browse files Browse the repository at this point in the history
  • Loading branch information
jonasnick committed Dec 6, 2024
1 parent 76ba82f commit 5b20821
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 15 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ which is common to all participants and does not need to be kept confidential.
Recovering a device that has participated in a DKG session then requires just the device's host secret key and the recovery data,
the latter of which can be obtained from any cooperative participant (or the coordinator) or from an untrusted backup provider.

ChillDKG outputs a threshold public key that can be safely used in [[BIP 341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)] Taproot outputs.
In contrast, a standard PedPop implementation would allow a malicious participant to secretly embed a Taproot commitment to a BIP 341 script path within the threshold public key.
If such a key was used directly in a Taproot output, the malicious participant could spend the output through their hidden script path, bypassing the requirement for `t - 1` additional signatures.
While BIP 341 outlines special precautions for using threshold public keys generated by standard PedPop, ChillDKG eliminates this vulnerability entirely, providing built-in protection against accidental misuse.

These features make ChillDKG usable in a wide range of applications.
As a consequence of this broad applicability, there will necessarily be scenarios in which specialized protocols need less communication overhead and fewer rounds,
e.g., when setting up multiple signing devices in a single location.
Expand All @@ -131,6 +136,7 @@ In summary, we aim for the following design goals:
- **Simple backups**: ChillDKG allows recovering the DKG output using the host secret key and common recovery data shared among all participants and the coordinator. This eliminates the need for session-specific backups, simplifying user experience.
- **Untrusted coordinator**: Like FROST, ChillDKG uses a coordinator that relays messages between the participants. This simplifies the network topology, and the coordinator additionally reduces communication overhead by aggregating some of the messages. A malicious coordinator can force the DKG to fail but cannot negatively affect the security of the DKG.
- **Per-participant public keys**: When ChillDKG is used with FROST, partial signature verification is supported.
- **Taproot-safe threshold public key**: ChillDKG prevents malicious participants from embedding hidden [[BIP 341]](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)] Taproot commitment to a script path in the threshold public key.

In summary, ChillDKG incorporates solutions for both secure channels and consensus and simplifies backups in practice.
As a result, it fits a wide range of application scenarios,
Expand Down Expand Up @@ -241,21 +247,25 @@ Our variant of the SimplPedPop protocol then works as follows:
the vector `sum_coms` is now the complete component-wise sum of the `coms[j]` vectors from every participant `j`.
It acts as a VSS commitment to the sum `f = f_0 + ... + f_{n-1}` of the polynomials of all participants.)
To prevent malicious participants from embedding a [[BIP 341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)] Taproot script path in the threshold public key,
each participant creates a modified VSS commitment called `sum_coms_tweaked` from `sum_coms`, such that the public key generated from `sum_coms_tweaked` has an unspendable script path.
This is achieved by computing a Taproot tweak `t` for an unspendable script path and adding the point `t * G` to `sum_coms[0]`.
Participant `i` computes the public share of every participant `j` as follows:
```
pubshares[j] = (j+1)^0 * sum_coms[0] + ... + (j+1)^(t-1) * sum_coms[t-1]
pubshares[j] = (j+1)^0 * sum_coms_tweaked[0] + ... + (j+1)^(t-1) * sum_coms_tweaked[t-1]
```
Let `secshare` be the sum of VSS shares privately obtained from each participant.
Participant `i` checks the validity of `secshare` against `sum_coms`
Let `secshare` be the sum of VSS shares privately obtained from each participant and Taproot tweak `t`.
Participant `i` checks the validity of `secshare` against `sum_coms_tweaked`
by checking if the equation `secshare * G = pubshares[i]` holds.
(Assuming `secshare` is the sum of the VSS shares created by other participants, it will be equal to `f(i+1)`.)
If the check fails, participant `i` aborts.
Otherwise, participant `i` sets the DKG output consisting of
this participant's secret share `secshare`,
the threshold public key `threshold_pubkey = sum_coms[0]`, and
the threshold public key `threshold_pubkey = sum_coms_tweaked[0]`, and
all participants' public shares `pubshares`.
As a final step, participant `i` enters a session of an external equality check protocol
Expand Down
2 changes: 2 additions & 0 deletions python/chilldkg_ref/chilldkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ def recover(
certeq_verify(hostpubkeys, eq_input, cert)

# Compute threshold pubkey and individual pubshares
sum_coms, secshare_tweak = sum_coms.invalid_taproot_commit()
threshold_pubkey = sum_coms.commitment_to_secret()
pubshares = [sum_coms.pubshare(i) for i in range(n)]

Expand All @@ -706,6 +707,7 @@ def recover(
idx,
enc_secshares[idx],
)
secshare += secshare_tweak

# This is just a sanity check. Our signature is valid, so we have done
# this check already during the actual session.
Expand Down
26 changes: 16 additions & 10 deletions python/chilldkg_ref/simplpedpop.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn

from secp256k1proto.bip340 import schnorr_sign, schnorr_verify
from secp256k1proto.secp256k1 import GE, Scalar
from secp256k1proto.secp256k1 import G, GE, Scalar
from .util import (
BIP_TAG,
SecretKeyError,
Expand Down Expand Up @@ -112,6 +112,7 @@ class ParticipantBlameState(NamedTuple):
n: int
idx: int
secshare: Scalar
secshare_tweak: Scalar
pubshare: GE


Expand Down Expand Up @@ -204,16 +205,20 @@ def participant_step2(
i, "Participant sent invalid proof-of-knowledge"
)
sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms)
threshold_pubkey = sum_coms.commitment_to_secret()
pubshare = sum_coms.pubshare(idx)
sum_coms_tweaked, secshare_tweak = sum_coms.invalid_taproot_commit()
secshare += secshare_tweak
threshold_pubkey = sum_coms_tweaked.commitment_to_secret()
pubshare = sum_coms_tweaked.pubshare(idx)

if not VSSCommitment.verify_secshare(secshare, pubshare):
raise UnknownFaultyParticipantOrCoordinatorError(
ParticipantBlameState(n, idx, secshare, pubshare),
ParticipantBlameState(n, idx, secshare, secshare_tweak, pubshare),
"Received invalid secshare, consider blaming to determine faulty party",
)

pubshares = [sum_coms.pubshare(i) if i != idx else pubshare for i in range(n)]
pubshares = [
sum_coms_tweaked.pubshare(i) if i != idx else pubshare for i in range(n)
]
dkg_output = DKGOutput(
secshare.to_bytes(),
threshold_pubkey.to_bytes_compressed(),
Expand All @@ -228,13 +233,13 @@ def participant_blame(
cblame: CoordinatorBlameMsg,
partial_secshares: List[Scalar],
) -> NoReturn:
n, idx, secshare, pubshare = blame_state
n, idx, secshare, secshare_tweak, pubshare = blame_state
partial_pubshares = cblame.partial_pubshares

if GE.sum(*partial_pubshares) != pubshare:
if GE.sum(*partial_pubshares) + secshare_tweak * G != pubshare:
raise FaultyCoordinatorError("Sum of partial pubshares not equal to pubshare")

if Scalar.sum(*partial_secshares) != secshare:
if Scalar.sum(*partial_secshares) + secshare_tweak != secshare:
raise SecshareSumError("Sum of partial secshares not equal to secshare")

for i in range(n):
Expand Down Expand Up @@ -286,8 +291,9 @@ def coordinator_step(
cmsg = CoordinatorMsg(coms_to_secrets, sum_coms_to_nonconst_terms, pops)

sum_coms = assemble_sum_coms(coms_to_secrets, sum_coms_to_nonconst_terms)
threshold_pubkey = sum_coms.commitment_to_secret()
pubshares = [sum_coms.pubshare(i) for i in range(n)]
sum_coms_tweaked, secshare_tweak = sum_coms.invalid_taproot_commit()
threshold_pubkey = sum_coms_tweaked.commitment_to_secret()
pubshares = [sum_coms_tweaked.pubshare(i) for i in range(n)]

dkg_output = DKGOutput(
None,
Expand Down
29 changes: 28 additions & 1 deletion python/chilldkg_ref/vss.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

from typing import List
from typing import List, Tuple

from secp256k1proto.secp256k1 import GE, G, Scalar
from secp256k1proto.util import tagged_hash

from .util import tagged_hash_bip_dkg

Expand Down Expand Up @@ -74,6 +75,32 @@ def commitment_to_secret(self) -> GE:
def commitment_to_nonconst_terms(self) -> List[GE]:
return self.ges[1 : self.t()]

def invalid_taproot_commit(self) -> Tuple[VSSCommitment, Scalar]:
# Return a modified VSS commitment such that the threshold public key
# generated from it has an unspendable BIP 341 Taproot script path.
#
# Specifically, for a VSS commitment `com`, we have:
# `com.add_invalid_taproot_commitment().commitment_to_secret() = com.commitment_to_secret() + t*G`.
#
# The tweak 't' commits to an empty message, which is invalid according
# to BIP 341 for Taproot script spends. This follows BIP 341's
# recommended approach for committing to an unspendable script path.
#
# This prevents a malicious participant from secretly inserting a *valid*
# Taproot commitment to a script path into the summed VSS commitment during
# the DKG protocol. If the resulting threshold public key was used directly
# in a BIP 341 Taproot output, the malicious participant would be able to
# spend the output using their hidden script path.
#
# The function returns the updated VSS commitment and the tweak `t` which
# must be added to all secret shares of the commitment.
pk = self.commitment_to_secret()
secshare_tweak = Scalar.from_bytes(
tagged_hash("TapTweak", pk.to_bytes_compressed())
)
vss_tweak = VSSCommitment([secshare_tweak * G] + [GE()] * (self.t() - 1))
return (self + vss_tweak, secshare_tweak)


class VSS:
f: Polynomial
Expand Down

0 comments on commit 5b20821

Please sign in to comment.