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 encpedpop
from jmfrost.chilldkg_ref import simplpedpop from jmfrost.chilldkg_ref import simplpedpop
from jmfrost.chilldkg_ref import vss 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 import reference as frost
from jmfrost.frost_ref.utils.bip340 import schnorr_verify from jmfrost.frost_ref.utils.bip340 import schnorr_verify

11
src/jmfrost/__init__.py

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# chilldkg_ref, secp256k1proto code is from # chilldkg_ref, secp256k1lab code is from
# https://github.com/BlockstreamResearch/bip-frost-dkg # https://github.com/BlockstreamResearch/bip-frost-dkg
# #
# commit 1731341f04157592e2f184cb00a37c4d331188e3 # commit 0f9e4b95b2e1ef4a0d335908e512ddaca60ebd99
# Author: Tim Ruffing <me@real-or-random.org> # Merge: aff38ce 7ddab85
# Date: Wed Dec 18 23:42:26 2024 +0100 # 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 # frost_ref is from
# https://github.com/siv2r/bip-frost-signing # 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 secrets import token_bytes as random_bytes
from typing import Any, Tuple, List, NamedTuple, NewType, Optional, NoReturn, Dict from typing import Any, Tuple, List, NamedTuple, NewType, Optional, NoReturn, Dict
from ..secp256k1proto.secp256k1 import Scalar, GE from ..secp256k1lab.secp256k1 import Scalar, GE
from ..secp256k1proto.bip340 import schnorr_sign, schnorr_verify from ..secp256k1lab.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1proto.keys import pubkey_gen_plain from ..secp256k1lab.keys import pubkey_gen_plain
from ..secp256k1proto.util import int_from_bytes, bytes_from_int from ..secp256k1lab.util import bytes_from_int
from .vss import VSSCommitment from .vss import VSSCommitment
from . import encpedpop from . import encpedpop
@ -41,12 +41,15 @@ __all__ = [
"coordinator_investigate", "coordinator_investigate",
"recover", "recover",
# Exceptions # Exceptions
"InvalidSignatureInCertificateError",
"HostSeckeyError", "HostSeckeyError",
"SessionParamsError", "SessionParamsError",
"InvalidHostPubkeyError", "InvalidHostPubkeyError",
"DuplicateHostPubkeyError", "DuplicateHostPubkeyError",
"ThresholdOrCountError", "ThresholdOrCountError",
"RandomnessError",
"ProtocolError", "ProtocolError",
"FaultyParticipantError",
"FaultyParticipantOrCoordinatorError", "FaultyParticipantOrCoordinatorError",
"FaultyCoordinatorError", "FaultyCoordinatorError",
"UnknownFaultyParticipantOrCoordinatorError", "UnknownFaultyParticipantOrCoordinatorError",
@ -149,16 +152,22 @@ def hostpubkey_gen(hostseckey: bytes) -> bytes:
The host public key (33 bytes). The host public key (33 bytes).
Raises: 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: if len(hostseckey) != 32:
raise HostSeckeyError raise HostSeckeyError
return pubkey_gen_plain(hostseckey) try:
return pubkey_gen_plain(hostseckey)
except ValueError:
raise HostSeckeyError
class HostSeckeyError(ValueError): 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: if len(rest) < 32 * n:
raise ValueError raise ValueError
enc_secshares, rest = ( 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 :], rest[32 * n :],
) )
@ -442,12 +451,14 @@ def participant_step1(
ParticipantMsg1: The first message to be sent to the coordinator. ParticipantMsg1: The first message to be sent to the coordinator.
Raises: Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes or if HostSeckeyError: If the length of `hostseckey` is not 32 bytes, if the
`hostseckey` does not match any entry of `hostpubkeys`. key is invalid, or if the key does not match any entry of
`hostpubkeys`.
InvalidHostPubkeyError: If `hostpubkeys` contains an invalid public key. InvalidHostPubkeyError: If `hostpubkeys` contains an invalid public key.
DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates. DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates.
ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does
not hold. not hold.
RandomnessError: If the length of `random` is not 32 bytes.
""" """
hostpubkey = hostpubkey_gen(hostseckey) # HostSeckeyError if len(hostseckey) != 32 hostpubkey = hostpubkey_gen(hostseckey) # HostSeckeyError if len(hostseckey) != 32
@ -460,6 +471,9 @@ def participant_step1(
raise HostSeckeyError( raise HostSeckeyError(
"Host secret key does not match any host public key" "Host secret key does not match any host public key"
) from e ) from e
if len(random) != 32:
raise RandomnessError
enc_state, enc_pmsg = encpedpop.participant_step1( enc_state, enc_pmsg = encpedpop.participant_step1(
# We know that EncPedPop uses its seed only by feeding it to a hash # 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, # function. Thus, it is sufficient that the seed has a high entropy,
@ -476,6 +490,10 @@ def participant_step1(
return state1, ParticipantMsg1(enc_pmsg) return state1, ParticipantMsg1(enc_pmsg)
class RandomnessError(ValueError):
"""Raised if the length of the provided randomness is not 32 bytes."""
def participant_step2( def participant_step2(
hostseckey: bytes, hostseckey: bytes,
state1: ParticipantState1, state1: ParticipantState1,
@ -510,6 +528,8 @@ def participant_step2(
Raises: Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes. 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 FaultyParticipantOrCoordinatorError: If another known participant or the
coordinator is faulty. See the documentation of the exception for coordinator is faulty. See the documentation of the exception for
further details. further details.
@ -519,6 +539,9 @@ def participant_step2(
suspected participant. See the documentation of the exception for suspected participant. See the documentation of the exception for
further details. further details.
""" """
if len(hostseckey) != 32:
raise HostSeckeyError
params, idx, enc_state = state1 params, idx, enc_state = state1
enc_cmsg, enc_secshares = cmsg1 enc_cmsg, enc_secshares = cmsg1
@ -565,6 +588,7 @@ def participant_finalize(
Arguments: Arguments:
state2: The participant's state as output by `participant_step2`. state2: The participant's state as output by `participant_step2`.
cmsg2: The second message received from the coordinator.
Returns: Returns:
DKGOutput: The DKG output. DKGOutput: The DKG output.
@ -639,7 +663,8 @@ def coordinator_step1(
"""Perform the coordinator's first step of a ChillDKG session. """Perform the coordinator's first step of a ChillDKG session.
Arguments: 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. params: Common session parameters.
Returns: Returns:
@ -654,6 +679,8 @@ def coordinator_step1(
DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates. DuplicateHostPubkeyError: If `hostpubkeys` contains duplicates.
ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does ThresholdOrCountError: If `1 <= t <= len(hostpubkeys) <= 2**32 - 1` does
not hold. not hold.
FaultyParticipantError: If another participant is faulty. See the
documentation of the exception for further details.
""" """
params_validate(params) params_validate(params)
hostpubkeys, t = params hostpubkeys, t = params
@ -695,7 +722,8 @@ def coordinator_finalize(
Arguments: Arguments:
state: The coordinator's session state as output by `coordinator_step1`. 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: Returns:
CoordinatorMsg2: The second message to be sent to all participants. CoordinatorMsg2: The second message to be sent to all participants.
@ -704,11 +732,13 @@ def coordinator_finalize(
bytes: The serialized recovery data. bytes: The serialized recovery data.
Raises: Raises:
FaultyParticipantError: If another known participant or the coordinator FaultyParticipantError: If another participant is faulty. See the
is faulty. See the documentation of the exception for further documentation of the exception for further details.
details.
""" """
params, eq_input, dkg_output = state params, eq_input, dkg_output = state
if len(pmsgs2) != len(params.hostpubkeys):
raise ValueError
cert = certeq_coordinator_step([pmsg2.sig for pmsg2 in pmsgs2]) cert = certeq_coordinator_step([pmsg2.sig for pmsg2 in pmsgs2])
try: try:
certeq_verify(params.hostpubkeys, eq_input, cert) certeq_verify(params.hostpubkeys, eq_input, cert)
@ -771,9 +801,9 @@ def recover(
SessionParams: The common parameters of the recovered session. SessionParams: The common parameters of the recovered session.
Raises: Raises:
HostSeckeyError: If the length of `hostseckey` is not 32 bytes or if HostSeckeyError: If the length of `hostseckey` is not 32 bytes, if the
`hostseckey` does not match the recovery data. (This can also key is invalid, or if the key does not match the recovery data.
occur if the recovery data is invalid.) (This can also occur if the recovery data is invalid.)
RecoveryDataError: If recovery failed due to invalid recovery data. RecoveryDataError: If recovery failed due to invalid recovery data.
""" """
try: try:

35
src/jmfrost/chilldkg_ref/encpedpop.py

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

25
src/jmfrost/chilldkg_ref/simplpedpop.py

@ -1,8 +1,8 @@
from secrets import token_bytes as random_bytes from secrets import token_bytes as random_bytes
from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn from typing import List, NamedTuple, NewType, Tuple, Optional, NoReturn
from ..secp256k1proto.bip340 import schnorr_sign, schnorr_verify from ..secp256k1lab.bip340 import schnorr_sign, schnorr_verify
from ..secp256k1proto.secp256k1 import GE, Scalar from ..secp256k1lab.secp256k1 import GE, Scalar
from .util import ( from .util import (
BIP_TAG, BIP_TAG,
FaultyParticipantOrCoordinatorError, FaultyParticipantOrCoordinatorError,
@ -35,7 +35,7 @@ def pop_msg(idx: int) -> bytes:
return idx.to_bytes(4, byteorder="big") 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( sig = schnorr_sign(
pop_msg(idx), seckey, aux_rand=random_bytes(32), tag_prefix=POP_MSG_TAG 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 t, n, idx, com_to_secret = state
coms_to_secrets, sum_coms_to_nonconst_terms, pops = cmsg coms_to_secrets, sum_coms_to_nonconst_terms, pops = cmsg
assert len(coms_to_secrets) == n if (
assert len(sum_coms_to_nonconst_terms) == t - 1 len(coms_to_secrets) != n
assert len(pops) == n or len(sum_coms_to_nonconst_terms) != t - 1
or len(pops) != n
):
raise ValueError
if coms_to_secrets[idx] != com_to_secret: if coms_to_secrets[idx] != com_to_secret:
raise FaultyCoordinatorError( raise FaultyCoordinatorError(
@ -219,7 +222,10 @@ def participant_step2(
threshold_pubkey = sum_coms_tweaked.commitment_to_secret() threshold_pubkey = sum_coms_tweaked.commitment_to_secret()
pubshares = [ 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( dkg_output = DKGOutput(
secshare_tweaked.to_bytes(), secshare_tweaked.to_bytes(),
@ -236,6 +242,9 @@ def participant_investigate(
partial_secshares: List[Scalar], partial_secshares: List[Scalar],
) -> NoReturn: ) -> NoReturn:
n, idx, secshare, pubshare = error.inv_data n, idx, secshare, pubshare = error.inv_data
if len(partial_secshares) != n:
raise ValueError
partial_pubshares = cinv.partial_pubshares partial_pubshares = cinv.partial_pubshares
if GE.sum(*partial_pubshares) != pubshare: if GE.sum(*partial_pubshares) != pubshare:
@ -279,6 +288,8 @@ def participant_investigate(
def coordinator_step( def coordinator_step(
pmsgs: List[ParticipantMsg], t: int, n: int pmsgs: List[ParticipantMsg], t: int, n: int
) -> Tuple[CoordinatorMsg, DKGOutput, bytes]: ) -> Tuple[CoordinatorMsg, DKGOutput, bytes]:
if len(pmsgs) != n:
raise ValueError
# Sum the commitments to the i-th coefficients for i > 0 # Sum the commitments to the i-th coefficients for i > 0
# #
# This procedure corresponds to the one described by Pedersen in Section 5.1 # 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 typing import Any
from ..secp256k1proto.util import tagged_hash from ..secp256k1lab.util import tagged_hash
BIP_TAG = "BIP DKG/" 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 typing import List, Tuple
from ..secp256k1proto.secp256k1 import GE, G, Scalar from ..secp256k1lab.secp256k1 import GE, G, Scalar
from ..secp256k1proto.util import tagged_hash from ..secp256k1lab.util import tagged_hash
from .util import tagged_hash_bip_dkg 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 # The function returns the updated VSS commitment and the tweak `t` which
# must be added to all secret shares of the commitment. # must be added to all secret shares of the commitment.
pk = self.commitment_to_secret() pk = self.commitment_to_secret()
secshare_tweak = Scalar.from_bytes( secshare_tweak = Scalar.from_bytes_checked(
tagged_hash("TapTweak", pk.to_bytes_compressed()) tagged_hash("TapTweak", pk.to_bytes_compressed())
) )
pubshare_tweak = secshare_tweak * G pubshare_tweak = secshare_tweak * G
@ -112,7 +112,7 @@ class VSS:
@staticmethod @staticmethod
def generate(seed: bytes, t: int) -> VSS: def generate(seed: bytes, t: int) -> VSS:
coeffs = [ coeffs = [
Scalar.from_bytes( Scalar.from_bytes_checked(
tagged_hash_bip_dkg("vss coeffs", seed + i.to_bytes(4, byteorder="big")) tagged_hash_bip_dkg("vss coeffs", seed + i.to_bytes(4, byteorder="big"))
) )
for i in range(t) 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 The Bitcoin Core developers
Copyright (c) 2009-2024 Bitcoin 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal 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: if len(sig) != 64:
raise ValueError("The signature must be a 64-byte array.") raise ValueError("The signature must be a 64-byte array.")
try: try:
P = GE.lift_x(int_from_bytes(pubkey)) P = GE.from_bytes_xonly(pubkey)
except ValueError: except ValueError:
return False return False
r = int_from_bytes(sig[0:32]) 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: def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE:
"""TODO""" """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 assert not shared_secret.infinity # prime-order group
return shared_secret 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') return int(self).to_bytes(32, 'big')
@classmethod @classmethod
def from_bytes(cls, b): def from_int_checked(cls, v):
"""Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" """Convert an integer to a field element (no overflow allowed)."""
v = int.from_bytes(b, 'big')
if v >= cls.SIZE: if v >= cls.SIZE:
raise ValueError raise ValueError
return cls(v) 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): def __str__(self):
"""Convert this field element to a 64 character hex string.""" """Convert this field element to a 64 character hex string."""
return f"{int(self):064x}" return f"{int(self):064x}"
@ -345,7 +361,7 @@ class GE:
assert len(b) == 33 assert len(b) == 33
if b[0] != 2 and b[0] != 3: if b[0] != 2 and b[0] != 3:
raise ValueError raise ValueError
x = FE.from_bytes(b[1:]) x = FE.from_bytes_checked(b[1:])
r = GE.lift_x(x) r = GE.lift_x(x)
if b[0] == 3: if b[0] == 3:
r = -r r = -r
@ -357,8 +373,8 @@ class GE:
assert len(b) == 65 assert len(b) == 65
if b[0] != 4: if b[0] != 4:
raise ValueError raise ValueError
x = FE.from_bytes(b[1:33]) x = FE.from_bytes_checked(b[1:33])
y = FE.from_bytes(b[33:]) y = FE.from_bytes_checked(b[33:])
if y**2 != x**3 + 7: if y**2 != x**3 + 7:
raise ValueError raise ValueError
return GE(x, y) return GE(x, y)
@ -376,7 +392,7 @@ class GE:
def from_bytes_xonly(b): def from_bytes_xonly(b):
"""Convert a point given in xonly encoding to a group element.""" """Convert a point given in xonly encoding to a group element."""
assert len(b) == 32 assert len(b) == 32
x = FE.from_bytes(b) x = FE.from_bytes_checked(b)
r = GE.lift_x(x) r = GE.lift_x(x)
return r 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 typing import Tuple, List, Optional
from secrets import token_bytes as random_bytes from secrets import token_bytes as random_bytes
from jmfrost.secp256k1proto.secp256k1 import GE, G, Scalar from jmfrost.secp256k1lab.secp256k1 import GE, G, Scalar
from jmfrost.secp256k1proto.keys import pubkey_gen_plain from jmfrost.secp256k1lab.keys import pubkey_gen_plain
from jmfrost.chilldkg_ref.util import ( from jmfrost.chilldkg_ref.util import (
FaultyParticipantOrCoordinatorError, FaultyParticipantOrCoordinatorError,
@ -22,8 +22,7 @@ import jmfrost.chilldkg_ref.simplpedpop as simplpedpop
import jmfrost.chilldkg_ref.encpedpop as encpedpop import jmfrost.chilldkg_ref.encpedpop as encpedpop
import jmfrost.chilldkg_ref.chilldkg as chilldkg import jmfrost.chilldkg_ref.chilldkg as chilldkg
from chilldkg_example import ( from chilldkg_example import simulate_chilldkg_full as simulate_chilldkg_full_example
simulate_chilldkg_full as simulate_chilldkg_full_example)
def test_chilldkg_params_validate(): def test_chilldkg_params_validate():
@ -83,6 +82,17 @@ def test_vss_correctness():
for i in range(n) 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( def simulate_simplpedpop(
seeds, t, investigation: bool 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 # Check that each secshare matches the corresponding pubshare
secshares_scalar = [ 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 secshare in secshares
] ]
for i in range(1, n + 1): for i in range(1, n + 1):

Loading…
Cancel
Save