blockchainkit 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.
- blockchainkit/__init__.py +12 -0
- blockchainkit/_validation.py +44 -0
- blockchainkit/consensus/__init__.py +66 -0
- blockchainkit/consensus/core/__init__.py +25 -0
- blockchainkit/consensus/core/base.py +97 -0
- blockchainkit/consensus/systems/__init__.py +46 -0
- blockchainkit/consensus/systems/byzantine.py +91 -0
- blockchainkit/consensus/systems/catch_up.py +60 -0
- blockchainkit/consensus/systems/difficulty.py +74 -0
- blockchainkit/consensus/systems/finality.py +92 -0
- blockchainkit/consensus/systems/fork_choice.py +44 -0
- blockchainkit/consensus/systems/pbft.py +78 -0
- blockchainkit/consensus/systems/pos.py +53 -0
- blockchainkit/consensus/systems/pow.py +58 -0
- blockchainkit/consensus/systems/pricing.py +50 -0
- blockchainkit/consensus/systems/randomized.py +114 -0
- blockchainkit/consensus/systems/selfish.py +81 -0
- blockchainkit/consensus/systems/sortition.py +48 -0
- blockchainkit/consensus/systems/stake_games.py +44 -0
- blockchainkit/consensus/systems/synchrony.py +49 -0
- blockchainkit/consensus/visualizers/__init__.py +9 -0
- blockchainkit/consensus/visualizers/plots.py +80 -0
- blockchainkit/constants.py +66 -0
- blockchainkit/crypto/__init__.py +157 -0
- blockchainkit/crypto/core/__init__.py +21 -0
- blockchainkit/crypto/core/base.py +109 -0
- blockchainkit/crypto/systems/__init__.py +137 -0
- blockchainkit/crypto/systems/asymmetric.py +136 -0
- blockchainkit/crypto/systems/commitments.py +93 -0
- blockchainkit/crypto/systems/curves.py +205 -0
- blockchainkit/crypto/systems/discrete_log.py +156 -0
- blockchainkit/crypto/systems/hashing.py +95 -0
- blockchainkit/crypto/systems/lamport.py +65 -0
- blockchainkit/crypto/systems/mac.py +34 -0
- blockchainkit/crypto/systems/merkle_damgard.py +139 -0
- blockchainkit/crypto/systems/multisig.py +98 -0
- blockchainkit/crypto/systems/one_time_pad.py +42 -0
- blockchainkit/crypto/systems/puzzles.py +96 -0
- blockchainkit/crypto/systems/sharing.py +159 -0
- blockchainkit/crypto/systems/signatures.py +232 -0
- blockchainkit/crypto/utils/__init__.py +5 -0
- blockchainkit/crypto/utils/primes.py +39 -0
- blockchainkit/crypto/visualizers/__init__.py +9 -0
- blockchainkit/crypto/visualizers/plots.py +88 -0
- blockchainkit/network/__init__.py +70 -0
- blockchainkit/network/core/__init__.py +21 -0
- blockchainkit/network/core/base.py +149 -0
- blockchainkit/network/systems/__init__.py +54 -0
- blockchainkit/network/systems/addresses.py +126 -0
- blockchainkit/network/systems/broadcast.py +124 -0
- blockchainkit/network/systems/clocks.py +149 -0
- blockchainkit/network/systems/epidemics.py +111 -0
- blockchainkit/network/systems/gossip.py +201 -0
- blockchainkit/network/systems/kademlia.py +147 -0
- blockchainkit/network/systems/privacy.py +98 -0
- blockchainkit/network/systems/propagation.py +60 -0
- blockchainkit/network/systems/relay.py +123 -0
- blockchainkit/network/systems/replication.py +122 -0
- blockchainkit/network/systems/topology.py +238 -0
- blockchainkit/network/visualizers/__init__.py +14 -0
- blockchainkit/network/visualizers/plots.py +177 -0
- blockchainkit/py.typed +0 -0
- blockchainkit/structures/__init__.py +61 -0
- blockchainkit/structures/core/__init__.py +21 -0
- blockchainkit/structures/core/base.py +100 -0
- blockchainkit/structures/systems/__init__.py +45 -0
- blockchainkit/structures/systems/block.py +144 -0
- blockchainkit/structures/systems/bloom.py +81 -0
- blockchainkit/structures/systems/chain.py +131 -0
- blockchainkit/structures/systems/hash_chain.py +35 -0
- blockchainkit/structures/systems/headers.py +54 -0
- blockchainkit/structures/systems/ledger.py +87 -0
- blockchainkit/structures/systems/merkle.py +273 -0
- blockchainkit/structures/systems/mmr.py +112 -0
- blockchainkit/structures/systems/sparse_merkle.py +139 -0
- blockchainkit/structures/systems/transaction.py +116 -0
- blockchainkit/structures/systems/utxo.py +156 -0
- blockchainkit/structures/utils/__init__.py +6 -0
- blockchainkit/structures/utils/accounts.py +13 -0
- blockchainkit/structures/utils/encoding.py +11 -0
- blockchainkit/structures/visualizers/__init__.py +13 -0
- blockchainkit/structures/visualizers/plots.py +154 -0
- blockchainkit/vm/__init__.py +60 -0
- blockchainkit/vm/core/__init__.py +25 -0
- blockchainkit/vm/core/base.py +171 -0
- blockchainkit/vm/systems/__init__.py +40 -0
- blockchainkit/vm/systems/assembler.py +80 -0
- blockchainkit/vm/systems/expressions.py +91 -0
- blockchainkit/vm/systems/programs.py +143 -0
- blockchainkit/vm/systems/reentrancy.py +70 -0
- blockchainkit/vm/systems/script.py +268 -0
- blockchainkit/vm/systems/stack_machine.py +239 -0
- blockchainkit/vm/systems/turing.py +111 -0
- blockchainkit/vm/systems/verifier.py +81 -0
- blockchainkit/vm/visualizers/__init__.py +9 -0
- blockchainkit/vm/visualizers/plots.py +97 -0
- blockchainkit-0.2.0.dist-info/METADATA +195 -0
- blockchainkit-0.2.0.dist-info/RECORD +101 -0
- blockchainkit-0.2.0.dist-info/WHEEL +5 -0
- blockchainkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- blockchainkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Commitments: salted hash commitments (Blum 1981) and Pedersen commitments (1991).
|
|
2
|
+
|
|
3
|
+
A commitment is a sealed envelope: binding (the committer cannot change the
|
|
4
|
+
contents later) and hiding (nobody can read them before the opening). A hash
|
|
5
|
+
commitment is computationally hiding and binding. A Pedersen commitment is
|
|
6
|
+
*perfectly* hiding and computationally binding, and commitments add.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hmac
|
|
10
|
+
|
|
11
|
+
from blockchainkit._validation import integer
|
|
12
|
+
from blockchainkit.constants import COMMIT_DOMAIN, PEDERSEN_DOMAIN
|
|
13
|
+
from blockchainkit.crypto.systems.asymmetric import TEACHING_GROUP, DHGroup
|
|
14
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def commit(message: bytes, salt: bytes) -> bytes:
|
|
18
|
+
"""Commit to a message with a secret salt of at least 16 bytes.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
message : bytes
|
|
23
|
+
Bytes to commit to.
|
|
24
|
+
salt : bytes
|
|
25
|
+
Random secret salt; fixed salts are only appropriate for experiments.
|
|
26
|
+
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
bytes
|
|
30
|
+
Domain-separated digest. Length framing prevents ambiguous openings.
|
|
31
|
+
|
|
32
|
+
Notes
|
|
33
|
+
-----
|
|
34
|
+
Hiding depends on salt entropy; binding relies on collision resistance.
|
|
35
|
+
"""
|
|
36
|
+
if not isinstance(message, bytes) or not isinstance(salt, bytes):
|
|
37
|
+
raise TypeError("message and salt must be bytes")
|
|
38
|
+
if len(salt) < 16:
|
|
39
|
+
raise ValueError("use at least 16 salt bytes")
|
|
40
|
+
return sha256(COMMIT_DOMAIN + len(salt).to_bytes(8, "big") + salt + message)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def verify_commitment(digest: bytes, message: bytes, salt: bytes) -> bool:
|
|
44
|
+
"""Check an opening against a commitment using constant-time comparison."""
|
|
45
|
+
return hmac.compare_digest(digest, commit(message, salt))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pedersen_generators(group: DHGroup = TEACHING_GROUP) -> tuple[int, int]:
|
|
49
|
+
"""Return ``(g, h)``: the group generator and a second, independent generator.
|
|
50
|
+
|
|
51
|
+
``h`` is derived by hashing the group parameters into the subgroup, so
|
|
52
|
+
nobody knows ``log_g(h)``. Anyone who did could open a commitment to any
|
|
53
|
+
value, which is why ``h`` must not be chosen by the committer.
|
|
54
|
+
"""
|
|
55
|
+
counter = 0
|
|
56
|
+
while True:
|
|
57
|
+
seed = PEDERSEN_DOMAIN + f"{group.p}:{group.q}:{group.g}:{counter}".encode()
|
|
58
|
+
candidate = int.from_bytes(sha256(seed), "big") % group.p
|
|
59
|
+
h = pow(candidate, (group.p - 1) // group.q, group.p)
|
|
60
|
+
if h not in (0, 1, group.g):
|
|
61
|
+
return group.g, h
|
|
62
|
+
counter += 1 # Degenerate candidate (probability about 2/q): hash again.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def pedersen_commit(value: int, blinding: int, group: DHGroup = TEACHING_GROUP) -> int:
|
|
66
|
+
"""Commit to ``value`` as ``g**value * h**blinding mod p``.
|
|
67
|
+
|
|
68
|
+
With a uniformly random blinding factor the commitment is uniformly
|
|
69
|
+
distributed whatever the value (perfect hiding). Opening it to a
|
|
70
|
+
different value would reveal ``log_g(h)`` (computational binding).
|
|
71
|
+
Commitments multiply to a commitment of the sum:
|
|
72
|
+
``C(a, r) * C(b, s) = C(a + b, r + s)``.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
value, blinding : int
|
|
77
|
+
Elements of the integers modulo the group order q.
|
|
78
|
+
group : DHGroup
|
|
79
|
+
A prime-order subgroup; defaults to
|
|
80
|
+
:data:`~blockchainkit.crypto.systems.asymmetric.TEACHING_GROUP`.
|
|
81
|
+
|
|
82
|
+
Examples
|
|
83
|
+
--------
|
|
84
|
+
>>> from blockchainkit.crypto import pedersen_commit, TEACHING_GROUP as G
|
|
85
|
+
>>> pedersen_commit(2, 5) * pedersen_commit(3, 7) % G.p == pedersen_commit(5, 12)
|
|
86
|
+
True
|
|
87
|
+
"""
|
|
88
|
+
for number, name in ((value, "value"), (blinding, "blinding")):
|
|
89
|
+
integer(number, name)
|
|
90
|
+
if number >= group.q:
|
|
91
|
+
raise ValueError(f"{name} must be below the group order q")
|
|
92
|
+
g, h = pedersen_generators(group)
|
|
93
|
+
return pow(g, value, group.p) * pow(h, blinding, group.p) % group.p
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Readable elliptic-curve group arithmetic, not constant-time cryptography.
|
|
2
|
+
|
|
3
|
+
Points are ``(x, y)`` tuples; ``None`` denotes the point at infinity.
|
|
4
|
+
The curve equation is y**2 = x**3 + a*x + b modulo p.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from math import isqrt
|
|
9
|
+
|
|
10
|
+
from blockchainkit._validation import integer
|
|
11
|
+
from blockchainkit.crypto.core.base import Point
|
|
12
|
+
from blockchainkit.crypto.utils.primes import is_prime
|
|
13
|
+
|
|
14
|
+
_SECP_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
|
|
15
|
+
_SECP_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Curve:
|
|
20
|
+
"""A nonsingular prime-field curve with a prime-order generator.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
p, a, b : int
|
|
25
|
+
Field modulus and reduced curve coefficients.
|
|
26
|
+
generator : tuple of int
|
|
27
|
+
Affine coordinates of the base point.
|
|
28
|
+
order : int
|
|
29
|
+
Prime order of the base point.
|
|
30
|
+
name : str
|
|
31
|
+
Human-readable label.
|
|
32
|
+
|
|
33
|
+
Notes
|
|
34
|
+
-----
|
|
35
|
+
Custom field/order primes must be below 2**64, which bounds the
|
|
36
|
+
educational primality check. secp256k1's own field prime and group
|
|
37
|
+
order are also accepted, each only in its own role.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
p: int
|
|
41
|
+
a: int
|
|
42
|
+
b: int
|
|
43
|
+
generator: tuple[int, int]
|
|
44
|
+
order: int
|
|
45
|
+
name: str = "custom"
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
for value, known in ((self.p, _SECP_P), (self.order, _SECP_N)):
|
|
49
|
+
integer(value, "prime", 3)
|
|
50
|
+
if value == known:
|
|
51
|
+
continue
|
|
52
|
+
if value >= 2**64:
|
|
53
|
+
raise ValueError("custom primes must be below 2**64")
|
|
54
|
+
if not is_prime(value):
|
|
55
|
+
raise ValueError("field modulus and generator order must be prime")
|
|
56
|
+
for value in (self.a, self.b):
|
|
57
|
+
integer(value, "coefficient")
|
|
58
|
+
if value >= self.p:
|
|
59
|
+
raise ValueError("coefficients must be reduced modulo p")
|
|
60
|
+
if (4 * self.a**3 + 27 * self.b**2) % self.p == 0:
|
|
61
|
+
raise ValueError("singular curve")
|
|
62
|
+
if self.generator is None or not self.contains(self.generator):
|
|
63
|
+
raise ValueError("generator must be a finite point on the curve")
|
|
64
|
+
if multiply(self.order, self.generator, self) is not None:
|
|
65
|
+
raise ValueError("generator does not have the claimed order")
|
|
66
|
+
|
|
67
|
+
def contains(self, point: Point) -> bool:
|
|
68
|
+
"""Return whether point is infinity or a canonical affine curve point."""
|
|
69
|
+
if point is None:
|
|
70
|
+
return True
|
|
71
|
+
if not isinstance(point, tuple) or len(point) != 2:
|
|
72
|
+
return False
|
|
73
|
+
x, y = point
|
|
74
|
+
return (
|
|
75
|
+
type(x) is int
|
|
76
|
+
and type(y) is int
|
|
77
|
+
and 0 <= x < self.p
|
|
78
|
+
and 0 <= y < self.p
|
|
79
|
+
and (y * y - x**3 - self.a * x - self.b) % self.p == 0
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def cofactor_is_one(self) -> bool:
|
|
84
|
+
"""Whether the generator's subgroup is provably the whole curve group.
|
|
85
|
+
|
|
86
|
+
Hasse's theorem bounds the number of points: #E <= p + 1 + 2*sqrt(p).
|
|
87
|
+
The subgroup order n divides #E, so if 2n exceeds that bound the
|
|
88
|
+
cofactor #E/n must be 1. Every on-curve point then lies in the
|
|
89
|
+
subgroup, and verifiers can skip the n*P = O membership check.
|
|
90
|
+
"""
|
|
91
|
+
return 2 * self.order > self.p + 1 + 2 * (isqrt(self.p) + 1)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _add(left: Point, right: Point, curve: Curve) -> Point:
|
|
95
|
+
if left is None:
|
|
96
|
+
return right
|
|
97
|
+
if right is None:
|
|
98
|
+
return left
|
|
99
|
+
x1, y1 = left
|
|
100
|
+
x2, y2 = right
|
|
101
|
+
p = curve.p
|
|
102
|
+
if x1 == x2 and (y1 + y2) % p == 0:
|
|
103
|
+
return None
|
|
104
|
+
if left == right:
|
|
105
|
+
slope = (3 * x1 * x1 + curve.a) * pow(2 * y1, -1, p) % p
|
|
106
|
+
else:
|
|
107
|
+
slope = (y2 - y1) * pow(x2 - x1, -1, p) % p
|
|
108
|
+
x3 = (slope * slope - x1 - x2) % p
|
|
109
|
+
return x3, (slope * (x1 - x3) - y1) % p
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def add(left: Point, right: Point, curve: Curve) -> Point:
|
|
113
|
+
"""Add two validated points, including infinity and inverse pairs."""
|
|
114
|
+
if not curve.contains(left) or not curve.contains(right):
|
|
115
|
+
raise ValueError("points must lie on the curve")
|
|
116
|
+
return _add(left, right, curve)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def multiply(scalar: int, point: Point, curve: Curve) -> Point:
|
|
120
|
+
"""Multiply a point by a signed integer using double-and-add.
|
|
121
|
+
|
|
122
|
+
Scalars are not reduced modulo the generator order: this also allows
|
|
123
|
+
checking subgroup membership for arbitrary points on a custom curve.
|
|
124
|
+
"""
|
|
125
|
+
if type(scalar) is not int:
|
|
126
|
+
raise TypeError(f"scalar must be an integer, not {type(scalar).__name__}")
|
|
127
|
+
if not curve.contains(point):
|
|
128
|
+
raise ValueError("point must lie on the curve")
|
|
129
|
+
if scalar < 0:
|
|
130
|
+
point = None if point is None else (point[0], -point[1] % curve.p)
|
|
131
|
+
scalar = -scalar
|
|
132
|
+
result = None
|
|
133
|
+
while scalar:
|
|
134
|
+
if scalar & 1:
|
|
135
|
+
result = _add(result, point, curve)
|
|
136
|
+
point = _add(point, point, curve)
|
|
137
|
+
scalar >>= 1
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
TOY_CURVE = Curve(17, 2, 2, (5, 1), 19, "toy17")
|
|
142
|
+
SECP256K1 = Curve(
|
|
143
|
+
_SECP_P,
|
|
144
|
+
0,
|
|
145
|
+
7,
|
|
146
|
+
(
|
|
147
|
+
0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
|
|
148
|
+
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
|
|
149
|
+
),
|
|
150
|
+
_SECP_N,
|
|
151
|
+
"secp256k1",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def public_key(private: int, curve: Curve = SECP256K1) -> tuple[int, int]:
|
|
156
|
+
"""Derive private*G for 1 <= private < generator order.
|
|
157
|
+
|
|
158
|
+
>>> from blockchainkit.crypto import public_key, TOY_CURVE
|
|
159
|
+
>>> public_key(2, TOY_CURVE)
|
|
160
|
+
(6, 3)
|
|
161
|
+
"""
|
|
162
|
+
integer(private, "private", 1)
|
|
163
|
+
if private >= curve.order:
|
|
164
|
+
raise ValueError("private key must be below generator order")
|
|
165
|
+
point = multiply(private, curve.generator, curve)
|
|
166
|
+
assert point is not None
|
|
167
|
+
return point
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def enumerate_points(curve: Curve) -> tuple[tuple[int, int], ...]:
|
|
171
|
+
"""List every finite point of a small curve, sorted, by trying each x.
|
|
172
|
+
|
|
173
|
+
Adding the point at infinity gives the whole group, so
|
|
174
|
+
``len(enumerate_points(curve)) + 1`` is the group order #E. Useful for
|
|
175
|
+
plotting a toy curve and for checking Hasse's bound by hand.
|
|
176
|
+
|
|
177
|
+
Raises
|
|
178
|
+
------
|
|
179
|
+
ValueError
|
|
180
|
+
The field has more than 10,000 elements; enumeration is O(p).
|
|
181
|
+
|
|
182
|
+
Examples
|
|
183
|
+
--------
|
|
184
|
+
>>> from blockchainkit.crypto import TOY_CURVE, enumerate_points
|
|
185
|
+
>>> len(enumerate_points(TOY_CURVE)) + 1 == TOY_CURVE.order
|
|
186
|
+
True
|
|
187
|
+
"""
|
|
188
|
+
if curve.p > 10_000:
|
|
189
|
+
raise ValueError("enumeration is only for small curves (p <= 10000)")
|
|
190
|
+
roots: dict[int, list[int]] = {}
|
|
191
|
+
for y in range(curve.p):
|
|
192
|
+
roots.setdefault(y * y % curve.p, []).append(y)
|
|
193
|
+
return tuple(
|
|
194
|
+
(x, y)
|
|
195
|
+
for x in range(curve.p)
|
|
196
|
+
for y in roots.get((x**3 + curve.a * x + curve.b) % curve.p, ())
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def encode_point(point: Point, curve: Curve = SECP256K1) -> bytes:
|
|
201
|
+
"""Return fixed-width uncompressed encoding (0x04 || x || y)."""
|
|
202
|
+
if point is None or not curve.contains(point):
|
|
203
|
+
raise ValueError("expected a finite on-curve point")
|
|
204
|
+
width = (curve.p.bit_length() + 7) // 8
|
|
205
|
+
return b"\x04" + point[0].to_bytes(width, "big") + point[1].to_bytes(width, "big")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Generic discrete-logarithm algorithms: why group size and structure matter.
|
|
2
|
+
|
|
3
|
+
Baby-step giant-step (Shanks 1971) solves base**x = target in any cyclic
|
|
4
|
+
group of order n with about 2*sqrt(n) multiplications. Pohlig-Hellman (1978)
|
|
5
|
+
splits the problem along the prime factors of n, so its cost is governed by
|
|
6
|
+
the *largest prime factor*, not by n. Together they explain why
|
|
7
|
+
Diffie-Hellman and Schnorr work in a subgroup of large prime order.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from math import isqrt
|
|
11
|
+
|
|
12
|
+
from blockchainkit._validation import integer
|
|
13
|
+
from blockchainkit.crypto.core.base import DiscreteLogResult
|
|
14
|
+
from blockchainkit.crypto.utils.primes import is_prime
|
|
15
|
+
|
|
16
|
+
_TRIAL_DIVISION_BOUND = 2**20
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check(base: int, target: int, modulus: int, order: int) -> None:
|
|
20
|
+
for value, name in ((base, "base"), (target, "target")):
|
|
21
|
+
integer(value, name, 1)
|
|
22
|
+
integer(modulus, "modulus", 3)
|
|
23
|
+
integer(order, "order", 1)
|
|
24
|
+
if base >= modulus or target >= modulus:
|
|
25
|
+
raise ValueError("base and target must be reduced modulo the modulus")
|
|
26
|
+
if pow(base, order, modulus) != 1:
|
|
27
|
+
raise ValueError("base**order must be 1 modulo the modulus")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bsgs(base: int, target: int, modulus: int, order: int) -> tuple[int, int]:
|
|
31
|
+
m = isqrt(order - 1) + 1 if order > 1 else 1
|
|
32
|
+
baby: dict[int, int] = {}
|
|
33
|
+
value = 1
|
|
34
|
+
for j in range(m):
|
|
35
|
+
baby.setdefault(value, j)
|
|
36
|
+
value = value * base % modulus
|
|
37
|
+
giant_step = pow(base, -m, modulus)
|
|
38
|
+
gamma = target
|
|
39
|
+
for i in range(m):
|
|
40
|
+
if gamma in baby:
|
|
41
|
+
return (i * m + baby[gamma]) % order, m + i + 1
|
|
42
|
+
gamma = gamma * giant_step % modulus
|
|
43
|
+
raise ValueError("target is not a power of base")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def baby_step_giant_step(base: int, target: int, modulus: int, order: int) -> DiscreteLogResult:
|
|
47
|
+
"""Solve ``base**x = target (mod modulus)`` in about ``2*sqrt(order)`` steps.
|
|
48
|
+
|
|
49
|
+
Writing ``x = i*m + j`` with ``m = ceil(sqrt(order))``, a table of baby
|
|
50
|
+
steps ``base**j`` is matched against giant steps ``target * base**(-i*m)``.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
base : int
|
|
55
|
+
A group element whose order divides ``order``.
|
|
56
|
+
target : int
|
|
57
|
+
The element whose logarithm is wanted.
|
|
58
|
+
modulus : int
|
|
59
|
+
The modulus of the multiplicative group.
|
|
60
|
+
order : int
|
|
61
|
+
A multiple of the order of ``base`` (the subgroup order).
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
DiscreteLogResult
|
|
66
|
+
The exponent and the number of table multiplications.
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
ValueError
|
|
71
|
+
``target`` is not a power of ``base``.
|
|
72
|
+
|
|
73
|
+
Examples
|
|
74
|
+
--------
|
|
75
|
+
>>> from blockchainkit.crypto import baby_step_giant_step
|
|
76
|
+
>>> baby_step_giant_step(2, pow(2, 77, 1019), 1019, 1018).exponent
|
|
77
|
+
77
|
|
78
|
+
"""
|
|
79
|
+
_check(base, target, modulus, order)
|
|
80
|
+
exponent, steps = _bsgs(base, target, modulus, order)
|
|
81
|
+
return DiscreteLogResult(exponent, steps)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def factor_order(n: int) -> dict[int, int]:
|
|
85
|
+
"""Factor a group order by trial division up to 2**20.
|
|
86
|
+
|
|
87
|
+
A cofactor left over after trial division must itself be prime, which
|
|
88
|
+
is all Pohlig-Hellman needs; otherwise the order is rejected.
|
|
89
|
+
|
|
90
|
+
>>> from blockchainkit.crypto.systems.discrete_log import factor_order
|
|
91
|
+
>>> factor_order(8100)
|
|
92
|
+
{2: 2, 3: 4, 5: 2}
|
|
93
|
+
"""
|
|
94
|
+
integer(n, "n", 1)
|
|
95
|
+
factors: dict[int, int] = {}
|
|
96
|
+
d = 2
|
|
97
|
+
while d * d <= n and d <= _TRIAL_DIVISION_BOUND:
|
|
98
|
+
while n % d == 0:
|
|
99
|
+
factors[d] = factors.get(d, 0) + 1
|
|
100
|
+
n //= d
|
|
101
|
+
d += 1 if d == 2 else 2
|
|
102
|
+
if n > 1:
|
|
103
|
+
if n >= 2**64 or not is_prime(n):
|
|
104
|
+
raise ValueError("cannot factor the order by trial division")
|
|
105
|
+
factors[n] = factors.get(n, 0) + 1
|
|
106
|
+
return factors
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def pohlig_hellman(base: int, target: int, modulus: int, order: int) -> DiscreteLogResult:
|
|
110
|
+
"""Solve a discrete log by splitting the group order into prime powers.
|
|
111
|
+
|
|
112
|
+
For each prime power ``p**e`` dividing ``order``, the problem is mapped
|
|
113
|
+
into the subgroup of order ``p**e`` and solved one base-``p`` digit at a
|
|
114
|
+
time with baby-step giant-step in a subgroup of order ``p``. The Chinese
|
|
115
|
+
Remainder Theorem combines the answers. The cost is about
|
|
116
|
+
``sum(e * sqrt(p))``, tiny when every prime factor is small.
|
|
117
|
+
|
|
118
|
+
Parameters and return value are as for :func:`baby_step_giant_step`.
|
|
119
|
+
|
|
120
|
+
Examples
|
|
121
|
+
--------
|
|
122
|
+
>>> from blockchainkit.crypto import pohlig_hellman
|
|
123
|
+
>>> pohlig_hellman(6, pow(6, 4321, 8101), 8101, 8100).exponent
|
|
124
|
+
4321
|
|
125
|
+
"""
|
|
126
|
+
_check(base, target, modulus, order)
|
|
127
|
+
# Reduce the given multiple to the exact order of base, prime by prime;
|
|
128
|
+
# otherwise a prime-power subproblem can have a trivial generator.
|
|
129
|
+
factors = factor_order(order)
|
|
130
|
+
for prime in factors:
|
|
131
|
+
while factors[prime] and pow(base, order // prime, modulus) == 1:
|
|
132
|
+
order //= prime
|
|
133
|
+
factors[prime] -= 1
|
|
134
|
+
residues, moduli, operations = [], [], 0
|
|
135
|
+
for prime, power in factors.items():
|
|
136
|
+
if power == 0:
|
|
137
|
+
continue
|
|
138
|
+
cofactor = order // prime**power
|
|
139
|
+
g = pow(base, cofactor, modulus) # Order divides prime**power.
|
|
140
|
+
h = pow(target, cofactor, modulus)
|
|
141
|
+
generator = pow(g, prime ** (power - 1), modulus) # Order divides prime.
|
|
142
|
+
digits = 0
|
|
143
|
+
for k in range(power):
|
|
144
|
+
# Strip the digits found so far, then project onto the order-p subgroup.
|
|
145
|
+
residual = h * pow(g, -digits, modulus) % modulus
|
|
146
|
+
projected = pow(residual, prime ** (power - 1 - k), modulus)
|
|
147
|
+
digit, steps = _bsgs(generator, projected, modulus, prime)
|
|
148
|
+
operations += steps
|
|
149
|
+
digits += digit * prime**k
|
|
150
|
+
residues.append(digits)
|
|
151
|
+
moduli.append(prime**power)
|
|
152
|
+
exponent = 0
|
|
153
|
+
for residue, m in zip(residues, moduli, strict=True):
|
|
154
|
+
rest = order // m
|
|
155
|
+
exponent += residue * rest * pow(rest, -1, m)
|
|
156
|
+
return DiscreteLogResult(exponent % order, operations)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""SHA-256 hashing, bit-level digest comparison, and birthday collisions."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
from blockchainkit._validation import integer
|
|
6
|
+
from blockchainkit.crypto.core.base import CollisionResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def sha256(data: bytes) -> bytes:
|
|
10
|
+
"""Return the 32-byte SHA-256 digest of bytes.
|
|
11
|
+
|
|
12
|
+
>>> from blockchainkit.crypto import sha256
|
|
13
|
+
>>> sha256(b"abc").hex()
|
|
14
|
+
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
|
|
15
|
+
"""
|
|
16
|
+
if not isinstance(data, bytes):
|
|
17
|
+
raise TypeError("data must be bytes; encode text explicitly")
|
|
18
|
+
return hashlib.sha256(data).digest()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hash256(data: bytes) -> bytes:
|
|
22
|
+
"""Return SHA-256(SHA-256(data)), as used in Bitcoin hashing."""
|
|
23
|
+
return sha256(sha256(data))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def hamming_distance(left: bytes, right: bytes) -> int:
|
|
27
|
+
"""Count differing bits between equally long byte strings.
|
|
28
|
+
|
|
29
|
+
>>> from blockchainkit.crypto import hamming_distance
|
|
30
|
+
>>> hamming_distance(bytes([0]), bytes([7]))
|
|
31
|
+
3
|
|
32
|
+
"""
|
|
33
|
+
if len(left) != len(right):
|
|
34
|
+
raise ValueError("inputs must have equal length")
|
|
35
|
+
return sum((a ^ b).bit_count() for a, b in zip(left, right, strict=True))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def truncated_hash(data: bytes, bits: int) -> int:
|
|
39
|
+
"""Return the top ``bits`` bits of SHA-256(data) as an integer.
|
|
40
|
+
|
|
41
|
+
Truncation makes collisions findable, to measure the birthday bound.
|
|
42
|
+
|
|
43
|
+
>>> from blockchainkit.crypto import truncated_hash
|
|
44
|
+
>>> truncated_hash(b"abc", 8) == sha256(b"abc")[0]
|
|
45
|
+
True
|
|
46
|
+
"""
|
|
47
|
+
integer(bits, "bits", 1)
|
|
48
|
+
if bits > 256:
|
|
49
|
+
raise ValueError("bits cannot exceed 256")
|
|
50
|
+
return int.from_bytes(sha256(data), "big") >> (256 - bits)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def find_collision(bits: int, *, prefix: bytes = b"", max_trials: int = 1 << 22) -> CollisionResult:
|
|
54
|
+
"""Find two inputs whose ``bits``-bit truncated hashes agree.
|
|
55
|
+
|
|
56
|
+
Hashes ``prefix + counter`` for counter = 0, 1, 2, ... and remembers every
|
|
57
|
+
truncated digest. By the birthday paradox a repeat is expected after
|
|
58
|
+
about ``sqrt(pi/2 * 2**bits)`` trials, far fewer than the ``2**bits``
|
|
59
|
+
needed to hit one *given* digest (Yuval, 1979).
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
bits : int
|
|
64
|
+
Output size to attack, between 1 and 48.
|
|
65
|
+
prefix : bytes
|
|
66
|
+
Shared prefix of every candidate input.
|
|
67
|
+
max_trials : int
|
|
68
|
+
Search bound.
|
|
69
|
+
|
|
70
|
+
Raises
|
|
71
|
+
------
|
|
72
|
+
TimeoutError
|
|
73
|
+
No collision within ``max_trials``.
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> from blockchainkit.crypto import find_collision
|
|
78
|
+
>>> result = find_collision(16)
|
|
79
|
+
>>> result.first != result.second, result.trials < 2**10
|
|
80
|
+
(True, True)
|
|
81
|
+
"""
|
|
82
|
+
integer(bits, "bits", 1)
|
|
83
|
+
if bits > 48:
|
|
84
|
+
raise ValueError("bits must be at most 48 to keep the search bounded")
|
|
85
|
+
integer(max_trials, "max_trials", 1)
|
|
86
|
+
if not isinstance(prefix, bytes):
|
|
87
|
+
raise TypeError("prefix must be bytes")
|
|
88
|
+
seen: dict[int, bytes] = {}
|
|
89
|
+
for counter in range(max_trials):
|
|
90
|
+
candidate = prefix + counter.to_bytes(8, "big")
|
|
91
|
+
digest = truncated_hash(candidate, bits)
|
|
92
|
+
if digest in seen:
|
|
93
|
+
return CollisionResult(seen[digest], candidate, digest, counter + 1)
|
|
94
|
+
seen[digest] = candidate
|
|
95
|
+
raise TimeoutError(f"no {bits}-bit collision in {max_trials} trials")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Lamport one-time signatures (1979): signatures from a hash function alone.
|
|
2
|
+
|
|
3
|
+
The private key is 256 pairs of random preimages; the public key is their
|
|
4
|
+
hashes. To sign, hash the message and, for each digest bit, reveal the
|
|
5
|
+
preimage for that bit's value. Verification hashes each revealed preimage.
|
|
6
|
+
Each signature reveals half the private key, so a key must sign only once.
|
|
7
|
+
Security rests only on the hash being one-way, so the scheme survives
|
|
8
|
+
quantum computers, and its descendants (XMSS, SPHINCS+) are standardized.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import hmac
|
|
12
|
+
|
|
13
|
+
from blockchainkit.constants import LAMPORT_DOMAIN
|
|
14
|
+
from blockchainkit.crypto.core.base import LamportKeyPair
|
|
15
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _bits(message: bytes) -> list[int]:
|
|
19
|
+
digest = int.from_bytes(sha256(message), "big")
|
|
20
|
+
return [(digest >> (255 - i)) & 1 for i in range(256)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def lamport_keypair(seed: bytes) -> LamportKeyPair:
|
|
24
|
+
"""Derive a one-time key pair deterministically from a secret seed.
|
|
25
|
+
|
|
26
|
+
Each private preimage is SHA-256(domain || seed || index || bit). A real
|
|
27
|
+
seed must be at least 32 random bytes and never reused.
|
|
28
|
+
|
|
29
|
+
>>> from blockchainkit.crypto import lamport_keypair
|
|
30
|
+
>>> len(lamport_keypair(b"seed").public)
|
|
31
|
+
256
|
|
32
|
+
"""
|
|
33
|
+
if not isinstance(seed, bytes):
|
|
34
|
+
raise TypeError("seed must be bytes")
|
|
35
|
+
pairs = []
|
|
36
|
+
for i in range(256):
|
|
37
|
+
prefix = LAMPORT_DOMAIN + seed + i.to_bytes(2, "big")
|
|
38
|
+
pairs.append((sha256(prefix + b"\x00"), sha256(prefix + b"\x01")))
|
|
39
|
+
public = tuple((sha256(zero), sha256(one)) for zero, one in pairs)
|
|
40
|
+
return LamportKeyPair(tuple(pairs), public)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def lamport_sign(message: bytes, key: LamportKeyPair) -> tuple[bytes, ...]:
|
|
44
|
+
"""Reveal, for each bit of SHA-256(message), the matching private preimage."""
|
|
45
|
+
if not isinstance(message, bytes):
|
|
46
|
+
raise TypeError("message must be bytes")
|
|
47
|
+
return tuple(pair[bit] for pair, bit in zip(key.private, _bits(message), strict=True))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lamport_verify(
|
|
51
|
+
message: bytes, signature: tuple[bytes, ...], public: tuple[tuple[bytes, bytes], ...]
|
|
52
|
+
) -> bool:
|
|
53
|
+
"""Check that each revealed preimage hashes to the public value for its bit.
|
|
54
|
+
|
|
55
|
+
>>> from blockchainkit.crypto import lamport_keypair, lamport_sign, lamport_verify
|
|
56
|
+
>>> key = lamport_keypair(b"seed")
|
|
57
|
+
>>> lamport_verify(b"hello", lamport_sign(b"hello", key), key.public)
|
|
58
|
+
True
|
|
59
|
+
"""
|
|
60
|
+
if not isinstance(message, bytes) or len(signature) != 256 or len(public) != 256:
|
|
61
|
+
return False
|
|
62
|
+
return all(
|
|
63
|
+
isinstance(revealed, bytes) and hmac.compare_digest(sha256(revealed), pair[bit])
|
|
64
|
+
for revealed, pair, bit in zip(signature, public, _bits(message), strict=True)
|
|
65
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Message authentication codes: the naive secret-prefix hash, and HMAC (1996)."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import hmac
|
|
5
|
+
|
|
6
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def naive_mac(key: bytes, message: bytes) -> bytes:
|
|
10
|
+
"""Return SHA-256(key || message): a tempting MAC that is *broken*.
|
|
11
|
+
|
|
12
|
+
Anyone holding a tag can forge the tag of ``message || glue || suffix``
|
|
13
|
+
with :func:`~blockchainkit.crypto.systems.merkle_damgard.length_extension`.
|
|
14
|
+
Shown for contrast with :func:`hmac_sha256`; never use it.
|
|
15
|
+
"""
|
|
16
|
+
if not isinstance(key, bytes) or not isinstance(message, bytes):
|
|
17
|
+
raise TypeError("key and message must be bytes")
|
|
18
|
+
return sha256(key + message)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hmac_sha256(key: bytes, message: bytes) -> bytes:
|
|
22
|
+
"""Return HMAC-SHA256(key, message) (Bellare, Canetti and Krawczyk, 1996).
|
|
23
|
+
|
|
24
|
+
HMAC hashes twice with two derived keys,
|
|
25
|
+
``H((k XOR opad) || H((k XOR ipad) || m))``. The outer hash hides the
|
|
26
|
+
inner chaining state, so length extension no longer works.
|
|
27
|
+
|
|
28
|
+
>>> from blockchainkit.crypto import hmac_sha256
|
|
29
|
+
>>> hmac_sha256(b"key", b"The quick brown fox jumps over the lazy dog").hex()[:16]
|
|
30
|
+
'f7bc83f430538424'
|
|
31
|
+
"""
|
|
32
|
+
if not isinstance(key, bytes) or not isinstance(message, bytes):
|
|
33
|
+
raise TypeError("key and message must be bytes")
|
|
34
|
+
return hmac.new(key, message, hashlib.sha256).digest()
|