eddsa-threshold 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eddsa_threshold/__init__.py +1 -0
- eddsa_threshold/eddsa/__init__.py +1 -0
- eddsa_threshold/eddsa/algorithms/__init__.py +1 -0
- eddsa_threshold/eddsa/algorithms/ed25519.py +84 -0
- eddsa_threshold/eddsa/algorithms/ed25519ctx.py +23 -0
- eddsa_threshold/eddsa/algorithms/ed25519ph.py +23 -0
- eddsa_threshold/eddsa/algorithms/ed448.py +84 -0
- eddsa_threshold/eddsa/algorithms/ed448ph.py +23 -0
- eddsa_threshold/eddsa/curves/__init__.py +1 -0
- eddsa_threshold/eddsa/curves/base/__init__.py +1 -0
- eddsa_threshold/eddsa/curves/base/edwards_curve.py +91 -0
- eddsa_threshold/eddsa/curves/base/encoding.py +54 -0
- eddsa_threshold/eddsa/curves/base/field_ops.py +48 -0
- eddsa_threshold/eddsa/curves/base/scalar_ops.py +34 -0
- eddsa_threshold/eddsa/curves/ed25519/__init__.py +1 -0
- eddsa_threshold/eddsa/curves/ed25519/constants.py +37 -0
- eddsa_threshold/eddsa/curves/ed25519/ed25519_curve.py +79 -0
- eddsa_threshold/eddsa/curves/ed25519/encoding.py +80 -0
- eddsa_threshold/eddsa/curves/ed25519/field_ops.py +12 -0
- eddsa_threshold/eddsa/curves/ed25519/scalar_ops.py +12 -0
- eddsa_threshold/eddsa/curves/ed448/__init__.py +1 -0
- eddsa_threshold/eddsa/curves/ed448/constants.py +37 -0
- eddsa_threshold/eddsa/curves/ed448/ed448_curve.py +76 -0
- eddsa_threshold/eddsa/curves/ed448/encoding.py +77 -0
- eddsa_threshold/eddsa/curves/ed448/field_ops.py +12 -0
- eddsa_threshold/eddsa/curves/ed448/scalar_ops.py +12 -0
- eddsa_threshold/eddsa/keys/__init__.py +1 -0
- eddsa_threshold/eddsa/keys/ed25519_keypair.py +46 -0
- eddsa_threshold/eddsa/keys/ed448_keypair.py +45 -0
- eddsa_threshold/eddsa/keys/keypair.py +50 -0
- eddsa_threshold/eddsa/util/__init__.py +1 -0
- eddsa_threshold/eddsa/util/dom.py +17 -0
- eddsa_threshold/eddsa/util/hash_bindings.py +12 -0
- eddsa_threshold/frost/__init__.py +1 -0
- eddsa_threshold/frost/coordinator.py +185 -0
- eddsa_threshold/frost/core/__init__.py +1 -0
- eddsa_threshold/frost/core/base/__init__.py +1 -0
- eddsa_threshold/frost/core/base/frost_hashing.py +27 -0
- eddsa_threshold/frost/core/ed25519/__init__.py +1 -0
- eddsa_threshold/frost/core/ed25519/frost_hashing.py +26 -0
- eddsa_threshold/frost/core/ed448/__init__.py +1 -0
- eddsa_threshold/frost/core/ed448/frost_hashing.py +27 -0
- eddsa_threshold/frost/core/frost_types.py +53 -0
- eddsa_threshold/frost/core/polynomial.py +39 -0
- eddsa_threshold/frost/core/secrets/__init__.py +1 -0
- eddsa_threshold/frost/core/secrets/secret_sharing.py +26 -0
- eddsa_threshold/frost/core/secrets/shamir_secret_sharing.py +41 -0
- eddsa_threshold/frost/core/util.py +109 -0
- eddsa_threshold/frost/participant.py +140 -0
- eddsa_threshold/frost/trusted_dealer.py +72 -0
- eddsa_threshold-0.2.0.dist-info/METADATA +39 -0
- eddsa_threshold-0.2.0.dist-info/RECORD +55 -0
- eddsa_threshold-0.2.0.dist-info/WHEEL +5 -0
- eddsa_threshold-0.2.0.dist-info/licenses/LICENSE +14 -0
- eddsa_threshold-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
from eddsa_threshold.eddsa.curves.base.edwards_curve import EdwardsCurve
|
|
4
|
+
from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
|
|
5
|
+
from eddsa_threshold.frost.core.frost_types import GroupInfo, NonceCommitment, ParticipantId, SecretValue, SessionId, SigningPackage, SigningSession, VSSCommitment
|
|
6
|
+
from eddsa_threshold.frost.core.util import check_participant_bounds, compute_binding_factors, compute_group_commitment, derive_group_info
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FrostCoordinator:
|
|
10
|
+
"""
|
|
11
|
+
Coordinator-side implementation for a 2-round FROST signing flow.
|
|
12
|
+
|
|
13
|
+
Cryptographic operations are delegated through callback hooks so this class can stay curve/algorithm agnostic.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, threshold: int, participant_ids: list[ParticipantId], hashing: FrostHashing, curve: EdwardsCurve):
|
|
17
|
+
if threshold <= 0:
|
|
18
|
+
raise ValueError("threshold must be positive")
|
|
19
|
+
|
|
20
|
+
check_participant_bounds(threshold, participant_ids, curve.scalar_ops)
|
|
21
|
+
|
|
22
|
+
self.THRESHOLD: Final[int] = threshold
|
|
23
|
+
self.PARTICIPANT_IDS: Final[list[ParticipantId]] = participant_ids
|
|
24
|
+
self.MAX_PARTICIPANTS: Final[int] = len(participant_ids)
|
|
25
|
+
|
|
26
|
+
self._dealer_info_set: bool = False
|
|
27
|
+
|
|
28
|
+
self._HASHING: Final[FrostHashing] = hashing
|
|
29
|
+
self._CURVE: Final[EdwardsCurve] = curve
|
|
30
|
+
|
|
31
|
+
self._signing_sessions: dict[SessionId, SigningSession] = {}
|
|
32
|
+
|
|
33
|
+
def set_dealer_info(self, vss_commitment: list[VSSCommitment]) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Set the group info after receiving it from the trusted dealer.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if self._dealer_info_set:
|
|
39
|
+
raise ValueError(f"coordinator has already set their dealer info")
|
|
40
|
+
|
|
41
|
+
self._GROUP_INFO = derive_group_info(self.THRESHOLD, self.MAX_PARTICIPANTS, vss_commitment, self._CURVE)
|
|
42
|
+
self._dealer_info_set = True
|
|
43
|
+
|
|
44
|
+
def create_signing_session(self, message: bytes) -> SessionId:
|
|
45
|
+
"""
|
|
46
|
+
Initializes a signing session for the given message.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
if not self._dealer_info_set:
|
|
50
|
+
raise ValueError("dealer info has not been set yet")
|
|
51
|
+
|
|
52
|
+
# for now, session id is a hash of the message + randomness (allow for multiple signing sessions for the same message)
|
|
53
|
+
session_id = self._HASHING.h2(message) + self._CURVE.scalar_ops.random_scalar()
|
|
54
|
+
|
|
55
|
+
signing_session = SigningSession(session_id, message)
|
|
56
|
+
self._signing_sessions[session_id] = signing_session
|
|
57
|
+
|
|
58
|
+
return session_id
|
|
59
|
+
|
|
60
|
+
def register_participant_to_session(self, session_id: SessionId, participant_id: ParticipantId) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Registers a participant to a signing session.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if session_id not in self._signing_sessions:
|
|
66
|
+
raise ValueError("signing session not found")
|
|
67
|
+
|
|
68
|
+
signing_session = self._signing_sessions[session_id]
|
|
69
|
+
|
|
70
|
+
if signing_session.signing_in_progress:
|
|
71
|
+
raise ValueError("cannot register participant to session after signing has started")
|
|
72
|
+
|
|
73
|
+
if participant_id not in self.PARTICIPANT_IDS:
|
|
74
|
+
raise ValueError("participant id not recognized")
|
|
75
|
+
|
|
76
|
+
if participant_id in signing_session.participant_ids:
|
|
77
|
+
raise ValueError("participant already registered to session")
|
|
78
|
+
|
|
79
|
+
signing_session.participant_ids.append(participant_id)
|
|
80
|
+
|
|
81
|
+
def start_signing_session(self, session_id: SessionId) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Marks the signing session as started, preventing further participant registrations.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if session_id not in self._signing_sessions:
|
|
87
|
+
raise ValueError("signing session not found")
|
|
88
|
+
|
|
89
|
+
signing_session = self._signing_sessions[session_id]
|
|
90
|
+
|
|
91
|
+
if len(signing_session.participant_ids) < self.THRESHOLD:
|
|
92
|
+
raise ValueError("not enough participants registered to start signing session")
|
|
93
|
+
|
|
94
|
+
signing_session.signing_in_progress = True
|
|
95
|
+
|
|
96
|
+
def receive_commitment(self, session_id: SessionId, participant_id: ParticipantId, commitment: NonceCommitment) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Receives a nonce commitment from a participant and stores it in the signing session.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
if session_id not in self._signing_sessions:
|
|
102
|
+
raise ValueError("signing session not found")
|
|
103
|
+
|
|
104
|
+
signing_session = self._signing_sessions[session_id]
|
|
105
|
+
|
|
106
|
+
if not signing_session.signing_in_progress:
|
|
107
|
+
raise ValueError("signing session has not started yet")
|
|
108
|
+
|
|
109
|
+
if participant_id not in signing_session.participant_ids:
|
|
110
|
+
raise ValueError("participant id not registered to this signing session")
|
|
111
|
+
|
|
112
|
+
if participant_id in signing_session.commitments:
|
|
113
|
+
raise ValueError("commitment already received from this participant")
|
|
114
|
+
|
|
115
|
+
signing_session.commitments[participant_id] = commitment
|
|
116
|
+
|
|
117
|
+
if len(signing_session.commitments) == len(signing_session.participant_ids):
|
|
118
|
+
signing_session.round_one_completed = True
|
|
119
|
+
|
|
120
|
+
def create_signing_package(self, session_id: SessionId) -> SigningPackage:
|
|
121
|
+
"""
|
|
122
|
+
Creates the signing package to be sent to the participants after round one is complete.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if session_id not in self._signing_sessions:
|
|
126
|
+
raise ValueError("signing session not found")
|
|
127
|
+
|
|
128
|
+
signing_session = self._signing_sessions[session_id]
|
|
129
|
+
|
|
130
|
+
if not signing_session.round_one_completed:
|
|
131
|
+
raise ValueError("round one not completed yet")
|
|
132
|
+
|
|
133
|
+
return SigningPackage(session_id, signing_session.message, signing_session.participant_ids, signing_session.commitments)
|
|
134
|
+
|
|
135
|
+
def receive_signature_share(self, session_id: SessionId, participant_id: ParticipantId, signature_share: SecretValue) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Receives a signature share from a participant and stores it in the signing session.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
if session_id not in self._signing_sessions:
|
|
141
|
+
raise ValueError("signing session not found")
|
|
142
|
+
|
|
143
|
+
signing_session = self._signing_sessions[session_id]
|
|
144
|
+
|
|
145
|
+
if not signing_session.round_one_completed:
|
|
146
|
+
raise ValueError("round one not completed yet")
|
|
147
|
+
|
|
148
|
+
if participant_id not in signing_session.participant_ids:
|
|
149
|
+
raise ValueError("participant id not registered to this signing session")
|
|
150
|
+
|
|
151
|
+
if participant_id in signing_session.signature_shares:
|
|
152
|
+
raise ValueError("signature share already received from this participant")
|
|
153
|
+
|
|
154
|
+
signing_session.signature_shares[participant_id] = signature_share
|
|
155
|
+
|
|
156
|
+
if len(signing_session.signature_shares) == len(signing_session.participant_ids):
|
|
157
|
+
signing_session.round_two_completed = True
|
|
158
|
+
|
|
159
|
+
def aggregate(self, session_id: SessionId) -> bytes:
|
|
160
|
+
"""
|
|
161
|
+
Aggregates the signature shares into a final signature.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
if session_id not in self._signing_sessions:
|
|
165
|
+
raise ValueError("signing session not found")
|
|
166
|
+
|
|
167
|
+
signing_session = self._signing_sessions[session_id]
|
|
168
|
+
|
|
169
|
+
if not signing_session.round_two_completed:
|
|
170
|
+
raise ValueError("round two not completed yet")
|
|
171
|
+
|
|
172
|
+
commitments = list(signing_session.commitments.values())
|
|
173
|
+
signature_shares = list(signing_session.signature_shares.values())
|
|
174
|
+
|
|
175
|
+
binding_factors = compute_binding_factors(self._GROUP_INFO.group_public_key, commitments, signing_session.message, self._HASHING, self._CURVE.encoding)
|
|
176
|
+
|
|
177
|
+
group_commitment = compute_group_commitment(commitments, binding_factors, self._CURVE)
|
|
178
|
+
|
|
179
|
+
z = 0
|
|
180
|
+
for z_i in signature_shares:
|
|
181
|
+
z = z + z_i
|
|
182
|
+
|
|
183
|
+
z = self._CURVE.scalar_ops.reduce(z)
|
|
184
|
+
|
|
185
|
+
return self._CURVE.encoding.encode_point(group_commitment) + self._CURVE.encoding.encode_scalar(z)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FROST core primitives."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Base FROST interfaces."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FrostHashing(ABC):
|
|
5
|
+
"""
|
|
6
|
+
FrostHashing is the base class that ensures that all hashing functions used in the FROST protocol are present.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def h1(self, m: bytes) -> int:
|
|
11
|
+
raise NotImplementedError()
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def h2(self, m: bytes) -> int:
|
|
15
|
+
raise NotImplementedError()
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def h3(self, m: bytes) -> int:
|
|
19
|
+
raise NotImplementedError()
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def h4(self, m: bytes) -> bytes:
|
|
23
|
+
raise NotImplementedError()
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def h5(self, m: bytes) -> bytes:
|
|
27
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ed25519-specific FROST helpers."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from eddsa_threshold.eddsa.curves.ed25519.constants import L
|
|
2
|
+
from eddsa_threshold.eddsa.util.hash_bindings import sha512
|
|
3
|
+
from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Ed25519FrostHashing(FrostHashing):
|
|
7
|
+
"""
|
|
8
|
+
Ed25519FrostHashing is the implementation of the FrostHashing interface for Ed25519.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
_CONTEXT_STRING = b"FROST-ED25519-SHA512-v1"
|
|
12
|
+
|
|
13
|
+
def h1(self, m: bytes) -> int:
|
|
14
|
+
return int.from_bytes(sha512(self._CONTEXT_STRING + b"rho" + m), "little") % L
|
|
15
|
+
|
|
16
|
+
def h2(self, m: bytes) -> int:
|
|
17
|
+
return int.from_bytes(sha512(m), "little") % L
|
|
18
|
+
|
|
19
|
+
def h3(self, m: bytes) -> int:
|
|
20
|
+
return int.from_bytes(sha512(self._CONTEXT_STRING + b"nonce" + m), "little") % L
|
|
21
|
+
|
|
22
|
+
def h4(self, m: bytes) -> bytes:
|
|
23
|
+
return sha512(self._CONTEXT_STRING + b"msg" + m)
|
|
24
|
+
|
|
25
|
+
def h5(self, m: bytes) -> bytes:
|
|
26
|
+
return sha512(self._CONTEXT_STRING + b"com" + m)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ed448-specific FROST helpers."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from eddsa_threshold.eddsa.curves.ed448.constants import L
|
|
2
|
+
from eddsa_threshold.eddsa.util.hash_bindings import shake256
|
|
3
|
+
from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Ed448FrostHashing(FrostHashing):
|
|
7
|
+
"""
|
|
8
|
+
Ed448FrostHashing is the implementation of the FrostHashing interface for Ed448.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
_CONTEXT_STRING = b"FROST-ED448-SHAKE256-v1"
|
|
12
|
+
_DIGEST_SIZE = 114
|
|
13
|
+
|
|
14
|
+
def h1(self, m: bytes) -> int:
|
|
15
|
+
return int.from_bytes(shake256(self._CONTEXT_STRING + b"rho" + m, self._DIGEST_SIZE), "little") % L
|
|
16
|
+
|
|
17
|
+
def h2(self, m: bytes) -> int:
|
|
18
|
+
return int.from_bytes(shake256(b"SigEd448" + b'\x00' + b'\x00' + m, self._DIGEST_SIZE), "little") % L
|
|
19
|
+
|
|
20
|
+
def h3(self, m: bytes) -> int:
|
|
21
|
+
return int.from_bytes(shake256(self._CONTEXT_STRING + b"nonce" + m, self._DIGEST_SIZE), "little") % L
|
|
22
|
+
|
|
23
|
+
def h4(self, m: bytes) -> bytes:
|
|
24
|
+
return shake256(self._CONTEXT_STRING + b"msg" + m, self._DIGEST_SIZE)
|
|
25
|
+
|
|
26
|
+
def h5(self, m: bytes) -> bytes:
|
|
27
|
+
return shake256(self._CONTEXT_STRING + b"com" + m, self._DIGEST_SIZE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
|
|
5
|
+
ParticipantId = int
|
|
6
|
+
SessionId = int
|
|
7
|
+
SecretValue = int
|
|
8
|
+
VSSCommitment = Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class SecretShare:
|
|
13
|
+
index: ParticipantId
|
|
14
|
+
value: SecretValue
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class GroupInfo:
|
|
19
|
+
group_public_key: bytes
|
|
20
|
+
public_keys: dict[ParticipantId, bytes]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class NonceCommitment:
|
|
25
|
+
participant_id: ParticipantId
|
|
26
|
+
hiding_nonce_commitment: Tuple[int, int]
|
|
27
|
+
binding_nonce_commitment: Tuple[int, int]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BindingFactor:
|
|
32
|
+
participant_id: ParticipantId
|
|
33
|
+
binding_factor: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class SigningPackage:
|
|
38
|
+
session_id: SessionId
|
|
39
|
+
message: bytes
|
|
40
|
+
participant_ids: list[ParticipantId]
|
|
41
|
+
commitments: dict[ParticipantId, NonceCommitment]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class SigningSession:
|
|
46
|
+
session_id: SessionId
|
|
47
|
+
message: bytes
|
|
48
|
+
participant_ids: list[ParticipantId] = field(default_factory=list)
|
|
49
|
+
commitments: dict[ParticipantId, NonceCommitment] = field(default_factory=dict)
|
|
50
|
+
signature_shares: dict[ParticipantId, SecretValue] = field(default_factory=dict)
|
|
51
|
+
signing_in_progress: bool = False
|
|
52
|
+
round_one_completed: bool = False
|
|
53
|
+
round_two_completed: bool = False
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def evaluate_polynomial(coeffs: list[int], x: int, scalar_ops: ScalarOps) -> int:
|
|
5
|
+
"""
|
|
6
|
+
Evaluates a polynomial at a given x using Horner's method.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# https://en.wikipedia.org/wiki/Horner's_method
|
|
10
|
+
result = 0
|
|
11
|
+
for coeff in reversed(coeffs):
|
|
12
|
+
result = result * x + coeff
|
|
13
|
+
|
|
14
|
+
return scalar_ops.reduce(result)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def derive_interpolating_value(L: list[int], x_i: int, x: int, scalar_ops: ScalarOps) -> int:
|
|
18
|
+
"""
|
|
19
|
+
Compute Lagrange basis coefficient λ_i(x) for arbitrary interpolation point x.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
if x_i not in L:
|
|
23
|
+
raise ValueError("x_i not in set")
|
|
24
|
+
|
|
25
|
+
if len(set(L)) != len(L):
|
|
26
|
+
raise ValueError("duplicate x values")
|
|
27
|
+
|
|
28
|
+
numerator = 1
|
|
29
|
+
denominator = 1
|
|
30
|
+
|
|
31
|
+
# Compute the Lagrange basis polynomial
|
|
32
|
+
# https://en.wikipedia.org/wiki/Lagrange_polynomial
|
|
33
|
+
for x_j in L:
|
|
34
|
+
if x_j == x_i:
|
|
35
|
+
continue
|
|
36
|
+
numerator = numerator * (x - x_j)
|
|
37
|
+
denominator = denominator * (x_i - x_j)
|
|
38
|
+
|
|
39
|
+
return scalar_ops.reduce(numerator * scalar_ops.inv(scalar_ops.reduce(denominator)))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Secret sharing helpers."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Final, Tuple
|
|
3
|
+
|
|
4
|
+
from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
|
|
5
|
+
from eddsa_threshold.frost.core.frost_types import SecretShare
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SecretSharing(ABC):
|
|
9
|
+
def __init__(self, threshold: int, num_shares: int, scalar_ops: ScalarOps):
|
|
10
|
+
self.T: Final[int] = threshold
|
|
11
|
+
self.N: Final[int] = num_shares
|
|
12
|
+
self.scalar_ops: Final[ScalarOps] = scalar_ops
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def split(self, secret: int) -> Tuple[list[SecretShare], list[int]]:
|
|
16
|
+
"""
|
|
17
|
+
Split the secret into N shares with a threshold of T.
|
|
18
|
+
"""
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def reconstruct(self, shares: list[SecretShare]) -> int:
|
|
23
|
+
"""
|
|
24
|
+
Reconstruct the secret from at least T shares.
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
|
|
4
|
+
from eddsa_threshold.frost.core.polynomial import evaluate_polynomial, derive_interpolating_value
|
|
5
|
+
from eddsa_threshold.frost.core.secrets.secret_sharing import SecretSharing
|
|
6
|
+
from eddsa_threshold.frost.core.frost_types import SecretShare
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ShamirSecretSharing(SecretSharing):
|
|
10
|
+
def split(self, secret: int) -> Tuple[list[SecretShare], list[int]]:
|
|
11
|
+
"""
|
|
12
|
+
Split the secret into N shares with a threshold of T.
|
|
13
|
+
Uses Shamir's Secret Sharing with random coefficients.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
# Generate random coefficients for the polynomial f(x) = secret + a1*x + a2*x^2 + ... + a_{T-1}*x^{T-1}
|
|
17
|
+
# Uses .random_scalar(), NOT PRODUCTION SECURE, but fine for this project.
|
|
18
|
+
coeffs = [secret] + [self.scalar_ops.random_scalar() for _ in range(1, self.T)]
|
|
19
|
+
|
|
20
|
+
shares = []
|
|
21
|
+
for i in range(1, self.N + 1):
|
|
22
|
+
secret_key_share_i = evaluate_polynomial(coeffs, i, self.scalar_ops)
|
|
23
|
+
shares.append(SecretShare(i, secret_key_share_i))
|
|
24
|
+
|
|
25
|
+
return shares, coeffs
|
|
26
|
+
|
|
27
|
+
def reconstruct(self, shares: list[SecretShare]) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Reconstruct the secret from at least T shares using Lagrange interpolation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
if len(shares) < self.T:
|
|
33
|
+
raise ValueError("Not enough shares to reconstruct the secret")
|
|
34
|
+
|
|
35
|
+
secret = self.scalar_ops.identity
|
|
36
|
+
for share_i in shares:
|
|
37
|
+
# Add share_i contribution to the secret
|
|
38
|
+
lagrange_coeff = derive_interpolating_value([share_j.index for share_j in shares], share_i.index, 0, self.scalar_ops)
|
|
39
|
+
secret += share_i.value * lagrange_coeff
|
|
40
|
+
|
|
41
|
+
return self.scalar_ops.reduce(secret)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
from eddsa_threshold.eddsa.curves.base.edwards_curve import EdwardsCurve
|
|
4
|
+
from eddsa_threshold.eddsa.curves.base.encoding import Encoding
|
|
5
|
+
from eddsa_threshold.eddsa.curves.base.scalar_ops import ScalarOps
|
|
6
|
+
from eddsa_threshold.frost.core.base.frost_hashing import FrostHashing
|
|
7
|
+
from eddsa_threshold.frost.core.frost_types import BindingFactor, GroupInfo, NonceCommitment, ParticipantId, VSSCommitment
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def encode_group_commitments(commitments: list[NonceCommitment], encoding: Encoding) -> bytes:
|
|
11
|
+
"""
|
|
12
|
+
Encodes a list of NonceCommitments into bytes.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Sort by participant id for deterministic encoding
|
|
16
|
+
group_commitments = sorted(commitments, key=lambda c: c.participant_id)
|
|
17
|
+
|
|
18
|
+
encoded_group_commitment = b""
|
|
19
|
+
|
|
20
|
+
for commitment in group_commitments:
|
|
21
|
+
encoded_commitment = encoding.encode_scalar(commitment.participant_id) + encoding.encode_point(
|
|
22
|
+
commitment.hiding_nonce_commitment) + encoding.encode_point(commitment.binding_nonce_commitment)
|
|
23
|
+
encoded_group_commitment = encoded_group_commitment + encoded_commitment
|
|
24
|
+
|
|
25
|
+
return encoded_group_commitment
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compute_binding_factors(group_public_key: bytes, commitments: list[NonceCommitment], message: bytes, hashing: FrostHashing, encoding: Encoding) -> list[BindingFactor]:
|
|
29
|
+
"""
|
|
30
|
+
Computes the binding factors for a signing session given the group public key, the list of NonceCommitments, and the message.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
message_hash = hashing.h4(message)
|
|
34
|
+
encoded_commitments_hash = hashing.h5(encode_group_commitments(commitments, encoding))
|
|
35
|
+
|
|
36
|
+
rho_prefix = group_public_key + message_hash + encoded_commitments_hash
|
|
37
|
+
|
|
38
|
+
binding_factors = []
|
|
39
|
+
for commitment in commitments:
|
|
40
|
+
rho_input = rho_prefix + encoding.encode_scalar(commitment.participant_id)
|
|
41
|
+
binding_factor = hashing.h1(rho_input)
|
|
42
|
+
binding_factors.append(BindingFactor(commitment.participant_id, binding_factor))
|
|
43
|
+
return binding_factors
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def binding_factor_for_participant(participant_id: ParticipantId, binding_factors: list[BindingFactor]) -> int:
|
|
47
|
+
"""
|
|
48
|
+
Retrieves the binding factor for a given participant id from a list of BindingFactors.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
for factor in binding_factors:
|
|
52
|
+
if factor.participant_id == participant_id:
|
|
53
|
+
return factor.binding_factor
|
|
54
|
+
|
|
55
|
+
raise ValueError(f"binding factor not found for participant {participant_id}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# EdwardsCurve for now, because this project only implements FROST for EdDSA, but this can be made more generic if needed.
|
|
59
|
+
def compute_group_commitment(commitments: list[NonceCommitment], binding_factors: list[BindingFactor], curve: EdwardsCurve) -> Tuple:
|
|
60
|
+
"""
|
|
61
|
+
Computes the group commitment from a list of NonceCommitments and BindingFactors.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
group_commitment = (0, 1, 1, 0) # Neutral element in extended coordinates
|
|
65
|
+
|
|
66
|
+
for commitment in commitments:
|
|
67
|
+
binding_factor = binding_factor_for_participant(commitment.participant_id, binding_factors)
|
|
68
|
+
binding_nonce = curve.scalar_mult(binding_factor, curve.affine_to_extended(commitment.binding_nonce_commitment))
|
|
69
|
+
|
|
70
|
+
group_commitment = curve.add(group_commitment, curve.affine_to_extended(commitment.hiding_nonce_commitment))
|
|
71
|
+
group_commitment = curve.add(group_commitment, binding_nonce)
|
|
72
|
+
|
|
73
|
+
return curve.extended_to_affine(group_commitment)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_participant_bounds(threshold: int, participant_ids: list[ParticipantId], scalar_ops: ScalarOps) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Checks that participant ids are within valid bounds
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if threshold <= 0:
|
|
82
|
+
raise ValueError("threshold must be positive")
|
|
83
|
+
if len(participant_ids) < threshold:
|
|
84
|
+
raise ValueError("number of participants must be >= threshold")
|
|
85
|
+
if len(participant_ids) <= 0:
|
|
86
|
+
raise ValueError("number of participants must be positive")
|
|
87
|
+
if len(participant_ids) >= scalar_ops.order:
|
|
88
|
+
raise ValueError("number of participants must be less than the curve order")
|
|
89
|
+
|
|
90
|
+
unique_ids = set(participant_ids)
|
|
91
|
+
if len(unique_ids) != len(participant_ids):
|
|
92
|
+
raise ValueError("participant ids must be unique")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def derive_group_info(threshold: int, max_participants: int, vss_commitment: list[VSSCommitment], curve: EdwardsCurve) -> GroupInfo:
|
|
96
|
+
"""
|
|
97
|
+
Derives the group public key and participant public keys from the VSS commitment.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
group_public_key = curve.encode_extended_point(vss_commitment[0]) # the first commitment is the group public key
|
|
101
|
+
|
|
102
|
+
participant_public_keys: dict[ParticipantId, bytes] = {}
|
|
103
|
+
|
|
104
|
+
for i in range(1, max_participants + 1):
|
|
105
|
+
participant_i_pk = (0, 1, 1, 0)
|
|
106
|
+
for j in range(0, threshold):
|
|
107
|
+
participant_i_pk = curve.add(curve.scalar_mult(pow(i, j), vss_commitment[j]), participant_i_pk)
|
|
108
|
+
participant_public_keys[i] = curve.encode_extended_point(participant_i_pk)
|
|
109
|
+
return GroupInfo(group_public_key, participant_public_keys)
|