Browse Source

update bip-frost-dkg to commit 0f9e4b95

add_frost_channel_encryption
zebra-lucky 4 months ago
parent
commit
0dce982dea
  1. 2
      src/jmclient/frost_clients.py
  2. 11
      src/jmfrost/__init__.py
  3. 1310
      src/jmfrost/chilldkg_ref/README.md
  4. 66
      src/jmfrost/chilldkg_ref/chilldkg.py
  5. 35
      src/jmfrost/chilldkg_ref/encpedpop.py
  6. 25
      src/jmfrost/chilldkg_ref/simplpedpop.py
  7. 2
      src/jmfrost/chilldkg_ref/util.py
  8. 8
      src/jmfrost/chilldkg_ref/vss.py
  9. 10
      src/jmfrost/secp256k1lab/CHANGELOG.md
  10. 1
      src/jmfrost/secp256k1lab/COPYING
  11. 13
      src/jmfrost/secp256k1lab/README.md
  12. 0
      src/jmfrost/secp256k1lab/__init__.py
  13. 2
      src/jmfrost/secp256k1lab/bip340.py
  14. 2
      src/jmfrost/secp256k1lab/ecdh.py
  15. 0
      src/jmfrost/secp256k1lab/keys.py
  16. 30
      src/jmfrost/secp256k1lab/secp256k1.py
  17. 0
      src/jmfrost/secp256k1lab/util.py
  18. 20
      test/jmfrost/test_chilldkg_ref.py

2
src/jmclient/frost_clients.py

@ -34,7 +34,7 @@ from jmfrost.chilldkg_ref.chilldkg import (
from jmfrost.chilldkg_ref import encpedpop
from jmfrost.chilldkg_ref import simplpedpop
from jmfrost.chilldkg_ref import vss
from jmfrost.secp256k1proto import secp256k1
from jmfrost.secp256k1lab import secp256k1
from jmfrost.frost_ref import reference as frost
from jmfrost.frost_ref.utils.bip340 import schnorr_verify

11
src/jmfrost/__init__.py

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
# chilldkg_ref, secp256k1proto code is from
# chilldkg_ref, secp256k1lab code is from
# https://github.com/BlockstreamResearch/bip-frost-dkg
#
# commit 1731341f04157592e2f184cb00a37c4d331188e3
# Author: Tim Ruffing <me@real-or-random.org>
# Date: Wed Dec 18 23:42:26 2024 +0100
# commit 0f9e4b95b2e1ef4a0d335908e512ddaca60ebd99
# Merge: aff38ce 7ddab85
# Author: Jonas Nick <jonasd.nick@gmail.com>
# Date: Thu May 8 07:40:00 2025 +0000
#
# text: Use links for internal references
# Merge pull request #93 from jonasnick/address-siv-comments
# frost_ref is from
# https://github.com/siv2r/bip-frost-signing

1310
src/jmfrost/chilldkg_ref/README.md

File diff suppressed because it is too large Load Diff

66
src/jmfrost/chilldkg_ref/chilldkg.py

@ -11,10 +11,10 @@ their arguments and return values, and the exceptions they raise; see also the
from secrets import token_bytes as random_bytes
from typing import Any, Tuple, List, NamedTuple, NewType, Optional, NoReturn, Dict
from ..secp256k1proto.secp256k1 import Scalar, GE
from ..secp256k1proto.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1proto.keys import pubkey_gen_plain
from ..secp256k1proto.util import int_from_bytes, bytes_from_int
from ..secp256k1lab.secp256k1 import Scalar, GE
from ..secp256k1lab.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1lab.keys import pubkey_gen_plain
from ..secp256k1lab.util import bytes_from_int
from .vss import VSSCommitment
from . import encpedpop
@ -41,12 +41,15 @@ __all__ = [
"coordinator_investigate",
"recover",
# Exceptions
"InvalidSignatureInCertificateError",
"HostSeckeyError",
"SessionParamsError",
"InvalidHostPubkeyError",
"DuplicateHostPubkeyError",
"ThresholdOrCountError",
"RandomnessError",
"ProtocolError",
"FaultyParticipantError",
"FaultyParticipantOrCoordinatorError",
"FaultyCoordinatorError",
"UnknownFaultyParticipantOrCoordinatorError",
@ -149,16 +152,22 @@ def hostpubkey_gen(hostseckey: bytes) -> bytes:
The host public key (33 bytes).
Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes.
HostSeckeyError: If the length of `hostseckey` is not 32 bytes or if the
key is invalid.
"""
if len(hostseckey) != 32:
raise HostSeckeyError
return pubkey_gen_plain(hostseckey)
try:
return pubkey_gen_plain(hostseckey)
except ValueError:
raise HostSeckeyError
class HostSeckeyError(ValueError):
"""Raised if the length of a host secret key is not 32 bytes."""
"""Raised if the host secret key is invalid.
This incluces the case that its length is not 32 bytes."""
###
@ -392,7 +401,7 @@ def deserialize_recovery_data(
if len(rest) < 32 * n:
raise ValueError
enc_secshares, rest = (
[Scalar(int_from_bytes(rest[i : i + 32])) for i in range(0, 32 * n, 32)],
[Scalar.from_bytes_checked(rest[i : i + 32]) for i in range(0, 32 * n, 32)],
rest[32 * n :],
)
@ -442,12 +451,14 @@ def participant_step1(
ParticipantMsg1: The first message to be sent to the coordinator.
Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes or if
`hostseckey` does not match any entry of `hostpubkeys`.
HostSeckeyError: If the length of `hostseckey` is not 32 bytes, if the
key is invalid, or if the key does not match any entry of
`hostpubkeys`.
InvalidHostPubkeyError: If `hostpubkeys` contains an invalid public key.
DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates.
ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does
not hold.
RandomnessError: If the length of `random` is not 32 bytes.
"""
hostpubkey = hostpubkey_gen(hostseckey) # HostSeckeyError if len(hostseckey) != 32
@ -460,6 +471,9 @@ def participant_step1(
raise HostSeckeyError(
"Host secret key does not match any host public key"
) from e
if len(random) != 32:
raise RandomnessError
enc_state, enc_pmsg = encpedpop.participant_step1(
# We know that EncPedPop uses its seed only by feeding it to a hash
# function. Thus, it is sufficient that the seed has a high entropy,
@ -476,6 +490,10 @@ def participant_step1(
return state1, ParticipantMsg1(enc_pmsg)
class RandomnessError(ValueError):
"""Raised if the length of the provided randomness is not 32 bytes."""
def participant_step2(
hostseckey: bytes,
state1: ParticipantState1,
@ -510,6 +528,8 @@ def participant_step2(
Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes.
FaultyCoordinatorError: If the coordinator is faulty. See the
documentation of the exception for further details.
FaultyParticipantOrCoordinatorError: If another known participant or the
coordinator is faulty. See the documentation of the exception for
further details.
@ -519,6 +539,9 @@ def participant_step2(
suspected participant. See the documentation of the exception for
further details.
"""
if len(hostseckey) != 32:
raise HostSeckeyError
params, idx, enc_state = state1
enc_cmsg, enc_secshares = cmsg1
@ -565,6 +588,7 @@ def participant_finalize(
Arguments:
state2: The participant's state as output by `participant_step2`.
cmsg2: The second message received from the coordinator.
Returns:
DKGOutput: The DKG output.
@ -639,7 +663,8 @@ def coordinator_step1(
"""Perform the coordinator's first step of a ChillDKG session.
Arguments:
pmsgs1: List of first messages received from the participants.
pmsgs1: List of first messages received from the participants. The
list's length must equal the total number of participants.
params: Common session parameters.
Returns:
@ -654,6 +679,8 @@ def coordinator_step1(
DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates.
ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does
not hold.
FaultyParticipantError: If another participant is faulty. See the
documentation of the exception for further details.
"""
params_validate(params)
hostpubkeys, t = params
@ -695,7 +722,8 @@ def coordinator_finalize(
Arguments:
state: The coordinator's session state as output by `coordinator_step1`.
pmsgs2: List of second messages received from the participants.
pmsgs2: List of second messages received from the participants. The
list's length must equal the total number of participants.
Returns:
CoordinatorMsg2: The second message to be sent to all participants.
@ -704,11 +732,13 @@ def coordinator_finalize(
bytes: The serialized recovery data.
Raises:
FaultyParticipantError: If another known participant or the coordinator
is faulty. See the documentation of the exception for further
details.
FaultyParticipantError: If another participant is faulty. See the
documentation of the exception for further details.
"""
params, eq_input, dkg_output = state
if len(pmsgs2) != len(params.hostpubkeys):
raise ValueError
cert = certeq_coordinator_step([pmsg2.sig for pmsg2 in pmsgs2])
try:
certeq_verify(params.hostpubkeys, eq_input, cert)
@ -771,9 +801,9 @@ def recover(
SessionParams: The common parameters of the recovered session.
Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes or if
`hostseckey` does not match the recovery data. (This can also
occur if the recovery data is invalid.)
HostSeckeyError: If the length of `hostseckey` is not 32 bytes, if the
key is invalid, or if the key does not match the recovery data.
(This can also occur if the recovery data is invalid.)
RecoveryDataError: If recovery failed due to invalid recovery data.
"""
try:

35
src/jmfrost/chilldkg_ref/encpedpop.py

@ -1,15 +1,14 @@
from typing import Tuple, List, NamedTuple, NoReturn
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
from ..secp256k1lab.secp256k1 import Scalar, GE
from ..secp256k1lab.ecdh import ecdh_libsecp256k1
from ..secp256k1lab.keys import pubkey_gen_plain
from . import simplpedpop
from .util import (
UnknownFaultyParticipantOrCoordinatorError,
tagged_hash_bip_dkg,
FaultyParticipantOrCoordinatorError,
FaultyParticipantError,
FaultyCoordinatorError,
)
@ -29,16 +28,18 @@ def ecdh(
data += their_pubkey + my_pubkey
assert len(data) == 32 + 2 * 33
data += context
return Scalar(int_from_bytes(tagged_hash_bip_dkg("encpedpop ecdh", data)))
ret: Scalar = Scalar.from_bytes_wrapping(
tagged_hash_bip_dkg("encpedpop ecdh", data)
)
return ret
def self_pad(symkey: bytes, nonce: bytes, context: bytes) -> Scalar:
# Pad for symmetric encryption to ourselves
return Scalar(
int_from_bytes(
tagged_hash_bip_dkg("encaps_multi self_pad", symkey + nonce + context)
)
pad: Scalar = Scalar.from_bytes_wrapping(
tagged_hash_bip_dkg("encaps_multi self_pad", symkey + nonce + context)
)
return pad
def encaps_multi(
@ -84,7 +85,8 @@ def encrypt_multi(
plaintexts: List[Scalar],
) -> List[Scalar]:
pads = encaps_multi(secnonce, pubnonce, deckey, enckeys, context, idx)
assert len(plaintexts) == len(pads)
if len(plaintexts) != len(pads):
raise ValueError
ciphertexts = [plaintext + pad for plaintext, pad in zip(plaintexts, pads)]
return ciphertexts
@ -179,8 +181,10 @@ def participant_step1(
idx: int,
random: bytes,
) -> Tuple[ParticipantState, ParticipantMsg]:
assert t < 2 ** (4 * 8)
assert len(random) == 32
if t >= 2 ** (4 * 8):
raise ValueError
if len(random) != 32:
raise ValueError
n = len(enckeys)
# Derive an encryption nonce and a seed for SimplPedPop.
@ -248,7 +252,8 @@ def participant_investigate(
) -> NoReturn:
simpl_inv_data, enc_secshare, pads = error.inv_data
enc_partial_secshares, partial_pubshares = cinv
assert len(enc_partial_secshares) == len(pads)
if len(enc_partial_secshares) != len(pads):
raise ValueError
partial_secshares = [
enc_partial_secshare - pad
for enc_partial_secshare, pad in zip(enc_partial_secshares, pads)
@ -292,7 +297,7 @@ def coordinator_step(
pubnonces = [pmsg.pubnonce for pmsg in pmsgs]
for i in range(n):
if len(pmsgs[i].enc_shares) != n:
raise FaultyParticipantOrCoordinatorError(
raise FaultyParticipantError(
i, "Participant sent enc_shares with invalid length"
)
enc_secshares = [

25
src/jmfrost/chilldkg_ref/simplpedpop.py

@ -1,8 +1,8 @@
from secrets import token_bytes as random_bytes
from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn
from ..secp256k1proto.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1proto.secp256k1 import GE, Scalar
from ..secp256k1lab.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1lab.secp256k1 import GE, Scalar
from .util import (
BIP_TAG,
FaultyParticipantOrCoordinatorError,
@ -35,7 +35,7 @@ def pop_msg(idx: int) -> bytes:
return idx.to_bytes(4, byteorder="big")
def pop_prove(seckey: bytes, idx: int, aux_rand: bytes = 32 * b"\x00") -> Pop:
def pop_prove(seckey: bytes, idx: int) -> Pop:
sig = schnorr_sign(
pop_msg(idx), seckey, aux_rand=random_bytes(32), tag_prefix=POP_MSG_TAG
)
@ -176,9 +176,12 @@ def participant_step2(
t, n, idx, com_to_secret = state
coms_to_secrets, sum_coms_to_nonconst_terms, pops = cmsg
assert len(coms_to_secrets) == n
assert len(sum_coms_to_nonconst_terms) == t - 1
assert len(pops) == n
if (
len(coms_to_secrets) != n
or len(sum_coms_to_nonconst_terms) != t - 1
or len(pops) != n
):
raise ValueError
if coms_to_secrets[idx] != com_to_secret:
raise FaultyCoordinatorError(
@ -219,7 +222,10 @@ def participant_step2(
threshold_pubkey = sum_coms_tweaked.commitment_to_secret()
pubshares = [
sum_coms_tweaked.pubshare(i) if i != idx else pubshare_tweaked for i in range(n)
sum_coms_tweaked.pubshare(i)
if i != idx
else pubshare_tweaked # We have computed our own pubshare already.
for i in range(n)
]
dkg_output = DKGOutput(
secshare_tweaked.to_bytes(),
@ -236,6 +242,9 @@ def participant_investigate(
partial_secshares: List[Scalar],
) -> NoReturn:
n, idx, secshare, pubshare = error.inv_data
if len(partial_secshares) != n:
raise ValueError
partial_pubshares = cinv.partial_pubshares
if GE.sum(*partial_pubshares) != pubshare:
@ -279,6 +288,8 @@ def participant_investigate(
def coordinator_step(
pmsgs: List[ParticipantMsg], t: int, n: int
) -> Tuple[CoordinatorMsg, DKGOutput, bytes]:
if len(pmsgs) != n:
raise ValueError
# Sum the commitments to the i-th coefficients for i > 0
#
# This procedure corresponds to the one described by Pedersen in Section 5.1

2
src/jmfrost/chilldkg_ref/util.py

@ -1,6 +1,6 @@
from typing import Any
from ..secp256k1proto.util import tagged_hash
from ..secp256k1lab.util import tagged_hash
BIP_TAG = "BIP DKG/"

8
src/jmfrost/chilldkg_ref/vss.py

@ -2,8 +2,8 @@ from __future__ import annotations
from typing import List, Tuple
from ..secp256k1proto.secp256k1 import GE, G, Scalar
from ..secp256k1proto.util import tagged_hash
from ..secp256k1lab.secp256k1 import GE, G, Scalar
from ..secp256k1lab.util import tagged_hash
from .util import tagged_hash_bip_dkg
@ -95,7 +95,7 @@ class VSSCommitment:
# 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(
secshare_tweak = Scalar.from_bytes_checked(
tagged_hash("TapTweak", pk.to_bytes_compressed())
)
pubshare_tweak = secshare_tweak * G
@ -112,7 +112,7 @@ class VSS:
@staticmethod
def generate(seed: bytes, t: int) -> VSS:
coeffs = [
Scalar.from_bytes(
Scalar.from_bytes_checked(
tagged_hash_bip_dkg("vss coeffs", seed + i.to_bytes(4, byteorder="big"))
)
for i in range(t)

10
src/jmfrost/secp256k1lab/CHANGELOG.md

@ -0,0 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-03-31
Initial release.

1
src/jmfrost/secp256k1proto/COPYING → src/jmfrost/secp256k1lab/COPYING

@ -2,6 +2,7 @@ The MIT License (MIT)
Copyright (c) 2009-2024 The Bitcoin Core developers
Copyright (c) 2009-2024 Bitcoin Developers
Copyright (c) 2025- The secp256k1lab Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

13
src/jmfrost/secp256k1lab/README.md

@ -0,0 +1,13 @@
secp256k1lab
============
![Dependencies: None](https://img.shields.io/badge/dependencies-none-success)
An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education.
Features:
* Low-level secp256k1 field and group arithmetic.
* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
* ECDH key exchange.
WARNING: The code in this library is slow and trivially vulnerable to side channel attacks.

0
src/jmfrost/secp256k1proto/__init__.py → src/jmfrost/secp256k1lab/__init__.py

2
src/jmfrost/secp256k1proto/bip340.py → src/jmfrost/secp256k1lab/bip340.py

@ -56,7 +56,7 @@ def schnorr_verify(
if len(sig) != 64:
raise ValueError("The signature must be a 64-byte array.")
try:
P = GE.lift_x(int_from_bytes(pubkey))
P = GE.from_bytes_xonly(pubkey)
except ValueError:
return False
r = int_from_bytes(sig[0:32])

2
src/jmfrost/secp256k1proto/ecdh.py → src/jmfrost/secp256k1lab/ecdh.py

@ -5,7 +5,7 @@ from .secp256k1 import GE, Scalar
def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE:
"""TODO"""
shared_secret = Scalar.from_bytes(seckey) * GE.from_bytes_compressed(pubkey)
shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey)
assert not shared_secret.infinity # prime-order group
return shared_secret

0
src/jmfrost/secp256k1proto/keys.py → src/jmfrost/secp256k1lab/keys.py

30
src/jmfrost/secp256k1proto/secp256k1.py → src/jmfrost/secp256k1lab/secp256k1.py

@ -135,13 +135,29 @@ class APrimeFE:
return int(self).to_bytes(32, 'big')
@classmethod
def from_bytes(cls, b):
"""Convert a 32-byte array to a field element (BE byte order, no overflow allowed)."""
v = int.from_bytes(b, 'big')
def from_int_checked(cls, v):
"""Convert an integer to a field element (no overflow allowed)."""
if v >= cls.SIZE:
raise ValueError
return cls(v)
@classmethod
def from_int_wrapping(cls, v):
"""Convert an integer to a field element (reduced modulo SIZE)."""
return cls(v % cls.SIZE)
@classmethod
def from_bytes_checked(cls, b):
"""Convert a 32-byte array to a field element (BE byte order, no overflow allowed)."""
v = int.from_bytes(b, 'big')
return cls.from_int_checked(v)
@classmethod
def from_bytes_wrapping(cls, b):
"""Convert a 32-byte array to a field element (BE byte order, reduced modulo SIZE)."""
v = int.from_bytes(b, 'big')
return cls.from_int_wrapping(v)
def __str__(self):
"""Convert this field element to a 64 character hex string."""
return f"{int(self):064x}"
@ -345,7 +361,7 @@ class GE:
assert len(b) == 33
if b[0] != 2 and b[0] != 3:
raise ValueError
x = FE.from_bytes(b[1:])
x = FE.from_bytes_checked(b[1:])
r = GE.lift_x(x)
if b[0] == 3:
r = -r
@ -357,8 +373,8 @@ class GE:
assert len(b) == 65
if b[0] != 4:
raise ValueError
x = FE.from_bytes(b[1:33])
y = FE.from_bytes(b[33:])
x = FE.from_bytes_checked(b[1:33])
y = FE.from_bytes_checked(b[33:])
if y**2 != x**3 + 7:
raise ValueError
return GE(x, y)
@ -376,7 +392,7 @@ class GE:
def from_bytes_xonly(b):
"""Convert a point given in xonly encoding to a group element."""
assert len(b) == 32
x = FE.from_bytes(b)
x = FE.from_bytes_checked(b)
r = GE.lift_x(x)
return r

0
src/jmfrost/secp256k1proto/util.py → src/jmfrost/secp256k1lab/util.py

20
test/jmfrost/test_chilldkg_ref.py

@ -8,8 +8,8 @@ from random import randint
from typing import Tuple, List, Optional
from secrets import token_bytes as random_bytes
from jmfrost.secp256k1proto.secp256k1 import GE, G, Scalar
from jmfrost.secp256k1proto.keys import pubkey_gen_plain
from jmfrost.secp256k1lab.secp256k1 import GE, G, Scalar
from jmfrost.secp256k1lab.keys import pubkey_gen_plain
from jmfrost.chilldkg_ref.util import (
FaultyParticipantOrCoordinatorError,
@ -22,8 +22,7 @@ import jmfrost.chilldkg_ref.simplpedpop as simplpedpop
import jmfrost.chilldkg_ref.encpedpop as encpedpop
import jmfrost.chilldkg_ref.chilldkg as chilldkg
from chilldkg_example import (
simulate_chilldkg_full as simulate_chilldkg_full_example)
from chilldkg_example import simulate_chilldkg_full as simulate_chilldkg_full_example
def test_chilldkg_params_validate():
@ -83,6 +82,17 @@ def test_vss_correctness():
for i in range(n)
)
vssc_tweaked, tweak, pubtweak = vss.commit().invalid_taproot_commit()
assert VSSCommitment.verify_secshare(
vss.secret() + tweak, vss.commit().commitment_to_secret() + pubtweak
)
assert all(
VSSCommitment.verify_secshare(
secshares[i] + tweak, vssc_tweaked.pubshare(i)
)
for i in range(n)
)
def simulate_simplpedpop(
seeds, t, investigation: bool
@ -313,7 +323,7 @@ def check_correctness_dkg_output(t, n, dkg_outputs: List[simplpedpop.DKGOutput])
# Check that each secshare matches the corresponding pubshare
secshares_scalar = [
None if secshare is None else Scalar.from_bytes(secshare)
None if secshare is None else Scalar.from_bytes_checked(secshare)
for secshare in secshares
]
for i in range(1, n + 1):

Loading…
Cancel
Save