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,139 @@
|
|
|
1
|
+
"""SHA-256 written out as a Merkle-Damgard iteration, and its length-extension flaw.
|
|
2
|
+
|
|
3
|
+
Merkle and Damgard (1989) showed how to hash messages of any length with a
|
|
4
|
+
fixed-size compression function: pad the message to whole blocks, ending with
|
|
5
|
+
its length, then chain ``state = compress(state, block)`` from a fixed initial
|
|
6
|
+
value. The digest *is* the final state. So anyone who knows ``H(m)`` and
|
|
7
|
+
``len(m)`` can keep compressing from it and compute ``H(m || pad || suffix)``
|
|
8
|
+
without knowing ``m``: the length-extension attack. HMAC exists to stop it.
|
|
9
|
+
|
|
10
|
+
This is a readable implementation for study. Use :func:`hashlib.sha256` (or
|
|
11
|
+
:func:`blockchainkit.crypto.systems.hashing.sha256`) for real hashing.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from blockchainkit._validation import integer
|
|
15
|
+
|
|
16
|
+
SHA256_IV = (
|
|
17
|
+
0x6A09E667,
|
|
18
|
+
0xBB67AE85,
|
|
19
|
+
0x3C6EF372,
|
|
20
|
+
0xA54FF53A,
|
|
21
|
+
0x510E527F,
|
|
22
|
+
0x9B05688C,
|
|
23
|
+
0x1F83D9AB,
|
|
24
|
+
0x5BE0CD19,
|
|
25
|
+
)
|
|
26
|
+
"""tuple of int: The initial chaining value (FIPS 180-4, section 5.3.3)."""
|
|
27
|
+
|
|
28
|
+
# Round constants: the first 32 bits of the fractional parts of the cube
|
|
29
|
+
# roots of the first 64 primes (FIPS 180-4, section 4.2.2).
|
|
30
|
+
_K = (
|
|
31
|
+
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
|
32
|
+
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
|
33
|
+
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
|
34
|
+
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
|
35
|
+
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
|
36
|
+
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
|
37
|
+
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
|
38
|
+
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
|
|
39
|
+
) # fmt: skip
|
|
40
|
+
_MASK = 0xFFFFFFFF
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _rotr(x: int, n: int) -> int:
|
|
44
|
+
return ((x >> n) | (x << (32 - n))) & _MASK
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sha256_compress(state: tuple[int, ...], block: bytes) -> tuple[int, ...]:
|
|
48
|
+
"""Apply SHA-256's compression function to one 64-byte block.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
state : tuple of int
|
|
53
|
+
Eight 32-bit words: :data:`SHA256_IV` or the previous output.
|
|
54
|
+
block : bytes
|
|
55
|
+
Exactly 64 bytes.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
tuple of int
|
|
60
|
+
The next chaining state.
|
|
61
|
+
"""
|
|
62
|
+
if not isinstance(block, bytes) or len(block) != 64:
|
|
63
|
+
raise ValueError("a SHA-256 block is 64 bytes")
|
|
64
|
+
if len(state) != 8:
|
|
65
|
+
raise ValueError("a SHA-256 state is eight 32-bit words")
|
|
66
|
+
w = [int.from_bytes(block[4 * i : 4 * i + 4], "big") for i in range(16)]
|
|
67
|
+
for i in range(16, 64):
|
|
68
|
+
s0 = _rotr(w[i - 15], 7) ^ _rotr(w[i - 15], 18) ^ (w[i - 15] >> 3)
|
|
69
|
+
s1 = _rotr(w[i - 2], 17) ^ _rotr(w[i - 2], 19) ^ (w[i - 2] >> 10)
|
|
70
|
+
w.append((w[i - 16] + s0 + w[i - 7] + s1) & _MASK)
|
|
71
|
+
a, b, c, d, e, f, g, h = state
|
|
72
|
+
for i in range(64):
|
|
73
|
+
t1 = h + (_rotr(e, 6) ^ _rotr(e, 11) ^ _rotr(e, 25)) + ((e & f) ^ (~e & g)) + _K[i] + w[i]
|
|
74
|
+
t2 = (_rotr(a, 2) ^ _rotr(a, 13) ^ _rotr(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))
|
|
75
|
+
a, b, c, d, e, f, g, h = (t1 + t2) & _MASK, a, b, c, (d + t1) & _MASK, e, f, g
|
|
76
|
+
return tuple((x + y) & _MASK for x, y in zip(state, (a, b, c, d, e, f, g, h), strict=True))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def sha256_padding(message_length: int) -> bytes:
|
|
80
|
+
"""Return the Merkle-Damgard strengthening for a message of that many bytes.
|
|
81
|
+
|
|
82
|
+
A 0x80 byte, zeros up to 56 bytes modulo 64, then the bit length as a
|
|
83
|
+
64-bit big-endian integer. Encoding the length is what makes the
|
|
84
|
+
construction collision-resistant whenever the compression function is.
|
|
85
|
+
"""
|
|
86
|
+
integer(message_length, "message_length")
|
|
87
|
+
zeros = (55 - message_length) % 64
|
|
88
|
+
return b"\x80" + bytes(zeros) + (8 * message_length).to_bytes(8, "big")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _iterate(state: tuple[int, ...], data: bytes) -> tuple[int, ...]:
|
|
92
|
+
for start in range(0, len(data), 64):
|
|
93
|
+
state = sha256_compress(state, data[start : start + 64])
|
|
94
|
+
return state
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _digest(state: tuple[int, ...]) -> bytes:
|
|
98
|
+
return b"".join(word.to_bytes(4, "big") for word in state)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def merkle_damgard_sha256(data: bytes) -> bytes:
|
|
102
|
+
"""Hash by padding, then chaining the compression function from the IV.
|
|
103
|
+
|
|
104
|
+
>>> import hashlib
|
|
105
|
+
>>> from blockchainkit.crypto import merkle_damgard_sha256
|
|
106
|
+
>>> merkle_damgard_sha256(b"abc") == hashlib.sha256(b"abc").digest()
|
|
107
|
+
True
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(data, bytes):
|
|
110
|
+
raise TypeError("data must be bytes")
|
|
111
|
+
return _digest(_iterate(SHA256_IV, data + sha256_padding(len(data))))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def length_extension(digest: bytes, original_length: int, suffix: bytes) -> tuple[bytes, bytes]:
|
|
115
|
+
"""Extend ``H(original)`` to ``H(original || glue || suffix)`` without knowing ``original``.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
digest : bytes
|
|
120
|
+
SHA-256 of the unknown original message.
|
|
121
|
+
original_length : int
|
|
122
|
+
Its length in bytes (for a secret-prefix MAC, key plus message).
|
|
123
|
+
suffix : bytes
|
|
124
|
+
Data the attacker wants to append.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
tuple of bytes
|
|
129
|
+
``(glue, forged)``: the original message's padding, which becomes
|
|
130
|
+
part of the forged message, and ``SHA-256(original || glue || suffix)``.
|
|
131
|
+
"""
|
|
132
|
+
if not isinstance(digest, bytes) or len(digest) != 32:
|
|
133
|
+
raise ValueError("a SHA-256 digest is 32 bytes")
|
|
134
|
+
if not isinstance(suffix, bytes):
|
|
135
|
+
raise TypeError("suffix must be bytes")
|
|
136
|
+
glue = sha256_padding(original_length)
|
|
137
|
+
state = tuple(int.from_bytes(digest[4 * i : 4 * i + 4], "big") for i in range(8))
|
|
138
|
+
total = original_length + len(glue) + len(suffix)
|
|
139
|
+
return glue, _digest(_iterate(state, suffix + sha256_padding(total)))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""MuSig key aggregation (Maxwell, Poelstra, Seurin and Wuille, 2018).
|
|
2
|
+
|
|
3
|
+
Schnorr signatures add: if every signer uses nonce k_i and key x_i, the sum
|
|
4
|
+
of their responses is a valid signature for the *sum* of their public keys.
|
|
5
|
+
Summing keys naively is unsafe: an attacker who announces
|
|
6
|
+
``Q_rogue = xG - Q_honest`` makes the sum equal ``xG``, a key they control
|
|
7
|
+
alone. MuSig weights each key by ``a_i = H(L, Q_i)``, a hash of the whole
|
|
8
|
+
key list L, so no participant can choose their key to cancel the others.
|
|
9
|
+
|
|
10
|
+
Teaching simplification: :func:`musig_sign` plays every signer in one
|
|
11
|
+
process. Real MuSig exchanges nonce commitments first (or uses MuSig2's two
|
|
12
|
+
nonces); without that round, a malicious co-signer can bias the joint nonce.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from blockchainkit._validation import integer
|
|
18
|
+
from blockchainkit.constants import MUSIG_DOMAIN
|
|
19
|
+
from blockchainkit.crypto.core.base import SchnorrSignature
|
|
20
|
+
from blockchainkit.crypto.systems.curves import (
|
|
21
|
+
SECP256K1,
|
|
22
|
+
Curve,
|
|
23
|
+
add,
|
|
24
|
+
encode_point,
|
|
25
|
+
multiply,
|
|
26
|
+
public_key,
|
|
27
|
+
)
|
|
28
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
29
|
+
from blockchainkit.crypto.systems.signatures import challenge
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def musig_coefficients(
|
|
33
|
+
publics: Sequence[tuple[int, int]], curve: Curve = SECP256K1
|
|
34
|
+
) -> tuple[int, ...]:
|
|
35
|
+
"""Return ``a_i = H(L || Q_i) mod n`` for each key, with L the encoded key list."""
|
|
36
|
+
if not publics:
|
|
37
|
+
raise ValueError("need at least one public key")
|
|
38
|
+
key_list = b"".join(encode_point(q, curve) for q in publics)
|
|
39
|
+
return tuple(
|
|
40
|
+
int.from_bytes(sha256(MUSIG_DOMAIN + key_list + encode_point(q, curve)), "big")
|
|
41
|
+
% curve.order
|
|
42
|
+
for q in publics
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def aggregate_public_keys(
|
|
47
|
+
publics: Sequence[tuple[int, int]], curve: Curve = SECP256K1
|
|
48
|
+
) -> tuple[int, int]:
|
|
49
|
+
"""Return the MuSig aggregate key ``sum(a_i * Q_i)``.
|
|
50
|
+
|
|
51
|
+
>>> from blockchainkit.crypto import aggregate_public_keys, public_key
|
|
52
|
+
>>> len(aggregate_public_keys([public_key(3), public_key(5)]))
|
|
53
|
+
2
|
|
54
|
+
"""
|
|
55
|
+
total = None
|
|
56
|
+
for a, q in zip(musig_coefficients(publics, curve), publics, strict=True):
|
|
57
|
+
total = add(total, multiply(a, q, curve), curve)
|
|
58
|
+
# A weighted sum of honest keys is the point at infinity only with
|
|
59
|
+
# negligible probability (a hash would have to cancel the keys exactly).
|
|
60
|
+
assert total is not None
|
|
61
|
+
return total
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def musig_sign(
|
|
65
|
+
message: bytes,
|
|
66
|
+
privates: Sequence[int],
|
|
67
|
+
nonces: Sequence[int],
|
|
68
|
+
curve: Curve = SECP256K1,
|
|
69
|
+
) -> SchnorrSignature:
|
|
70
|
+
"""Produce one Schnorr signature valid for the aggregate of the signers' keys.
|
|
71
|
+
|
|
72
|
+
Each signer i contributes ``R_i = k_i G`` and ``s_i = k_i + c * a_i * x_i``
|
|
73
|
+
with the shared challenge ``c = H(R, Q_agg, m)``; the signature is
|
|
74
|
+
``(sum R_i, sum s_i)`` and verifies with :func:`~blockchainkit.crypto.systems.signatures.verify`
|
|
75
|
+
against :func:`aggregate_public_keys`.
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
>>> from blockchainkit.crypto import musig_sign, aggregate_public_keys, public_key, verify
|
|
80
|
+
>>> signature = musig_sign(b"m", [3, 5], [11, 13])
|
|
81
|
+
>>> verify(b"m", signature, aggregate_public_keys([public_key(3), public_key(5)]))
|
|
82
|
+
True
|
|
83
|
+
"""
|
|
84
|
+
if len(privates) != len(nonces) or not privates:
|
|
85
|
+
raise ValueError("need one nonce per signer, and at least one signer")
|
|
86
|
+
for nonce in nonces:
|
|
87
|
+
integer(nonce, "nonce", 1)
|
|
88
|
+
publics = [public_key(x, curve) for x in privates]
|
|
89
|
+
coefficients = musig_coefficients(publics, curve)
|
|
90
|
+
aggregate = aggregate_public_keys(publics, curve)
|
|
91
|
+
commitment = None
|
|
92
|
+
for nonce in nonces:
|
|
93
|
+
commitment = add(commitment, public_key(nonce, curve), curve)
|
|
94
|
+
if commitment is None:
|
|
95
|
+
raise ValueError("the joint nonce is the point at infinity; choose other nonces")
|
|
96
|
+
c = challenge(message, commitment, aggregate, curve)
|
|
97
|
+
response = sum(k + c * a * x for k, a, x in zip(nonces, coefficients, privates, strict=True))
|
|
98
|
+
return SchnorrSignature(commitment, response % curve.order)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The one-time pad (Vernam 1917; Shannon 1949): XOR with a key as long as the message."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def xor_bytes(left: bytes, right: bytes) -> bytes:
|
|
5
|
+
"""Return the bytewise XOR of two equally long byte strings.
|
|
6
|
+
|
|
7
|
+
>>> from blockchainkit.crypto import xor_bytes
|
|
8
|
+
>>> xor_bytes(b"\\x0f", b"\\xff")
|
|
9
|
+
b'\\xf0'
|
|
10
|
+
"""
|
|
11
|
+
if not isinstance(left, bytes) or not isinstance(right, bytes):
|
|
12
|
+
raise TypeError("xor_bytes needs bytes")
|
|
13
|
+
if len(left) != len(right):
|
|
14
|
+
raise ValueError("inputs must have equal length")
|
|
15
|
+
return bytes(a ^ b for a, b in zip(left, right, strict=True))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def one_time_pad(message: bytes, key: bytes) -> bytes:
|
|
19
|
+
"""Encrypt or decrypt with a one-time pad: ``message XOR key``.
|
|
20
|
+
|
|
21
|
+
The same call decrypts, because XOR is its own inverse. Shannon proved
|
|
22
|
+
the pad perfectly secret when the key is uniformly random, as long as the
|
|
23
|
+
message, and never reused: every plaintext of that length is then equally
|
|
24
|
+
likely given the ciphertext. Reusing a key leaks ``m1 XOR m2``.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
message : bytes
|
|
29
|
+
Plaintext (or ciphertext, to decrypt).
|
|
30
|
+
key : bytes
|
|
31
|
+
A key exactly as long as the message.
|
|
32
|
+
|
|
33
|
+
Examples
|
|
34
|
+
--------
|
|
35
|
+
>>> from blockchainkit.crypto import one_time_pad
|
|
36
|
+
>>> ciphertext = one_time_pad(b"hi", b"\\x01\\x02")
|
|
37
|
+
>>> one_time_pad(ciphertext, b"\\x01\\x02")
|
|
38
|
+
b'hi'
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(message, bytes) and isinstance(key, bytes) and len(key) != len(message):
|
|
41
|
+
raise ValueError("the key must be exactly as long as the message")
|
|
42
|
+
return xor_bytes(message, key)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Merkle's puzzles (1974-1978): key agreement from symmetric primitives alone.
|
|
2
|
+
|
|
3
|
+
Alice publishes ``count`` puzzles, each a short secret (an identifier and a
|
|
4
|
+
session key) encrypted under a deliberately weak key of ``bits`` bits. Bob
|
|
5
|
+
solves one at random, about ``2**(bits-1)`` trials, and announces only its
|
|
6
|
+
identifier. An eavesdropper does not know which puzzle Bob chose and must
|
|
7
|
+
solve, on average, half of them. With ``count`` near ``2**bits``, honest work
|
|
8
|
+
is linear and the attacker's quadratic: the first public-key idea, with a
|
|
9
|
+
gap that is only polynomial.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import secrets
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
from blockchainkit._validation import integer
|
|
16
|
+
from blockchainkit.constants import PUZZLE_DOMAIN, PUZZLE_MAGIC
|
|
17
|
+
from blockchainkit.crypto.core.base import PuzzleSolution
|
|
18
|
+
from blockchainkit.crypto.systems.hashing import sha256
|
|
19
|
+
from blockchainkit.crypto.systems.one_time_pad import xor_bytes
|
|
20
|
+
|
|
21
|
+
_MAX_BITS = 24
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _keystream(weak_key: int) -> bytes:
|
|
25
|
+
return sha256(PUZZLE_DOMAIN + weak_key.to_bytes(8, "big"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def merkle_puzzles(
|
|
29
|
+
count: int,
|
|
30
|
+
bits: int,
|
|
31
|
+
*,
|
|
32
|
+
randbits: Callable[[int], int] = secrets.randbits,
|
|
33
|
+
) -> tuple[tuple[bytes, ...], dict[bytes, bytes]]:
|
|
34
|
+
"""Create Alice's puzzles and her private table of identifiers to keys.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
count : int
|
|
39
|
+
Number of puzzles to publish.
|
|
40
|
+
bits : int
|
|
41
|
+
Size of each weak key, between 1 and 24; solving takes up to
|
|
42
|
+
``2**bits`` trials.
|
|
43
|
+
randbits : callable
|
|
44
|
+
Randomness source; inject ``random.Random(seed).getrandbits`` only
|
|
45
|
+
for reproducible experiments.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
tuple
|
|
50
|
+
``(puzzles, table)``: 32-byte puzzles to publish, and the mapping
|
|
51
|
+
from each 8-byte identifier to its 16-byte session key.
|
|
52
|
+
"""
|
|
53
|
+
integer(count, "count", 1)
|
|
54
|
+
integer(bits, "bits", 1)
|
|
55
|
+
if bits > _MAX_BITS:
|
|
56
|
+
raise ValueError(f"bits must be at most {_MAX_BITS} to keep solving bounded")
|
|
57
|
+
puzzles, table = [], {}
|
|
58
|
+
for _ in range(count):
|
|
59
|
+
puzzle_id = randbits(64).to_bytes(8, "big")
|
|
60
|
+
key = randbits(128).to_bytes(16, "big")
|
|
61
|
+
weak_key = randbits(bits)
|
|
62
|
+
puzzles.append(xor_bytes(PUZZLE_MAGIC + puzzle_id + key, _keystream(weak_key)))
|
|
63
|
+
table[puzzle_id] = key
|
|
64
|
+
return tuple(puzzles), table
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def solve_puzzle(puzzle: bytes, bits: int) -> PuzzleSolution:
|
|
68
|
+
"""Open a puzzle by brute force over every weak key.
|
|
69
|
+
|
|
70
|
+
A trial succeeds when the decrypted plaintext starts with the known
|
|
71
|
+
marker, ``PUZZLE_MAGIC``.
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
ValueError
|
|
76
|
+
No weak key of that size opens the puzzle.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
>>> from random import Random
|
|
81
|
+
>>> from blockchainkit.crypto import merkle_puzzles, solve_puzzle
|
|
82
|
+
>>> puzzles, table = merkle_puzzles(3, 6, randbits=Random(1).getrandbits)
|
|
83
|
+
>>> solution = solve_puzzle(puzzles[0], bits=6)
|
|
84
|
+
>>> table[solution.puzzle_id] == solution.key
|
|
85
|
+
True
|
|
86
|
+
"""
|
|
87
|
+
if not isinstance(puzzle, bytes) or len(puzzle) != 32:
|
|
88
|
+
raise ValueError("a puzzle is 32 bytes")
|
|
89
|
+
integer(bits, "bits", 1)
|
|
90
|
+
if bits > _MAX_BITS:
|
|
91
|
+
raise ValueError(f"bits must be at most {_MAX_BITS} to keep solving bounded")
|
|
92
|
+
for weak_key in range(2**bits):
|
|
93
|
+
plaintext = xor_bytes(puzzle, _keystream(weak_key))
|
|
94
|
+
if plaintext.startswith(PUZZLE_MAGIC):
|
|
95
|
+
return PuzzleSolution(plaintext[8:16], plaintext[16:], weak_key + 1)
|
|
96
|
+
raise ValueError("no weak key of that size opens this puzzle")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Threshold secret sharing: Shamir (1979) and Feldman's verifiable variant (1987)."""
|
|
2
|
+
|
|
3
|
+
import secrets
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
|
|
6
|
+
from blockchainkit._validation import integer
|
|
7
|
+
from blockchainkit.crypto.core.base import FeldmanShares
|
|
8
|
+
from blockchainkit.crypto.systems.asymmetric import TEACHING_GROUP, DHGroup
|
|
9
|
+
from blockchainkit.crypto.utils.primes import is_prime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def split_secret(
|
|
13
|
+
secret: int,
|
|
14
|
+
threshold: int,
|
|
15
|
+
shares: int,
|
|
16
|
+
prime: int = 2089,
|
|
17
|
+
*,
|
|
18
|
+
randbelow: Callable[[int], int] = secrets.randbelow,
|
|
19
|
+
) -> tuple[tuple[int, int], ...]:
|
|
20
|
+
"""Sample a polynomial with constant term ``secret`` and return its shares.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
secret : int
|
|
25
|
+
Field element in [0, prime).
|
|
26
|
+
threshold : int
|
|
27
|
+
Number of shares needed, between 2 and shares.
|
|
28
|
+
shares : int
|
|
29
|
+
Number of distinct nonzero evaluation points, below prime.
|
|
30
|
+
prime : int
|
|
31
|
+
Prime field modulus below 2**64.
|
|
32
|
+
randbelow : callable
|
|
33
|
+
Uniform randomness source. Inject ``random.Random(seed).randrange``
|
|
34
|
+
only for reproducible teaching experiments.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
tuple
|
|
39
|
+
(x, y) pairs at x = 1, ..., shares.
|
|
40
|
+
"""
|
|
41
|
+
if not is_prime(prime):
|
|
42
|
+
raise ValueError("need a prime modulus below 2**64")
|
|
43
|
+
coefficients = _sample_polynomial(secret, threshold, shares, prime, randbelow)
|
|
44
|
+
return _evaluate(coefficients, shares, prime)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sample_polynomial(
|
|
48
|
+
secret: int, threshold: int, shares: int, prime: int, randbelow: Callable[[int], int]
|
|
49
|
+
) -> list[int]:
|
|
50
|
+
integer(secret, "secret")
|
|
51
|
+
integer(threshold, "threshold", 2)
|
|
52
|
+
integer(shares, "shares", threshold)
|
|
53
|
+
if secret >= prime or shares >= prime:
|
|
54
|
+
raise ValueError("need secret < prime and shares < prime")
|
|
55
|
+
coefficients = [secret]
|
|
56
|
+
for _ in range(threshold - 1):
|
|
57
|
+
coefficient = randbelow(prime)
|
|
58
|
+
integer(coefficient, "random coefficient")
|
|
59
|
+
if coefficient >= prime:
|
|
60
|
+
raise ValueError("randbelow returned a value outside the field")
|
|
61
|
+
coefficients.append(coefficient)
|
|
62
|
+
# Coefficients, including the leading one, may be zero. Sampling every
|
|
63
|
+
# polynomial with this constant term uniformly is what makes any
|
|
64
|
+
# threshold - 1 shares independent of the secret (perfect secrecy).
|
|
65
|
+
return coefficients
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _evaluate(coefficients: list[int], shares: int, prime: int) -> tuple[tuple[int, int], ...]:
|
|
69
|
+
return tuple(
|
|
70
|
+
(x, sum(c * pow(x, i, prime) for i, c in enumerate(coefficients)) % prime)
|
|
71
|
+
for x in range(1, shares + 1)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def feldman_split(
|
|
76
|
+
secret: int,
|
|
77
|
+
threshold: int,
|
|
78
|
+
shares: int,
|
|
79
|
+
group: DHGroup = TEACHING_GROUP,
|
|
80
|
+
*,
|
|
81
|
+
randbelow: Callable[[int], int] = secrets.randbelow,
|
|
82
|
+
) -> FeldmanShares:
|
|
83
|
+
"""Shamir-share a secret modulo the group order, and publish ``g**a_j``.
|
|
84
|
+
|
|
85
|
+
Each commitment hides one polynomial coefficient in the exponent. A share
|
|
86
|
+
holder checks ``g**y == prod(C_j**(x**j))``, which holds exactly when
|
|
87
|
+
their share lies on the committed polynomial, so a cheating dealer is
|
|
88
|
+
caught without anyone learning the secret. The commitment ``g**secret``
|
|
89
|
+
is public, so the secret is only computationally hidden.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
secret : int
|
|
94
|
+
Element of the integers modulo the group order q.
|
|
95
|
+
threshold, shares : int
|
|
96
|
+
As for :func:`split_secret`.
|
|
97
|
+
group : DHGroup
|
|
98
|
+
Prime-order group; shares live modulo its order q.
|
|
99
|
+
randbelow : callable
|
|
100
|
+
Uniform randomness source.
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
FeldmanShares
|
|
105
|
+
The shares and the public coefficient commitments.
|
|
106
|
+
|
|
107
|
+
Examples
|
|
108
|
+
--------
|
|
109
|
+
>>> from random import Random
|
|
110
|
+
>>> from blockchainkit.crypto import feldman_split, feldman_verify, TEACHING_GROUP
|
|
111
|
+
>>> dealt = feldman_split(42, 2, 3, randbelow=Random(0).randrange)
|
|
112
|
+
>>> all(feldman_verify(share, dealt.commitments) for share in dealt.shares)
|
|
113
|
+
True
|
|
114
|
+
"""
|
|
115
|
+
coefficients = _sample_polynomial(secret, threshold, shares, group.q, randbelow)
|
|
116
|
+
commitments = tuple(pow(group.g, a, group.p) for a in coefficients)
|
|
117
|
+
return FeldmanShares(_evaluate(coefficients, shares, group.q), commitments)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def feldman_verify(
|
|
121
|
+
share: tuple[int, int], commitments: Sequence[int], group: DHGroup = TEACHING_GROUP
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Check one share against the dealer's public commitments."""
|
|
124
|
+
x, y = share
|
|
125
|
+
if type(x) is not int or type(y) is not int or not 0 < x < group.q or not 0 <= y < group.q:
|
|
126
|
+
return False
|
|
127
|
+
expected = 1
|
|
128
|
+
for j, commitment in enumerate(commitments):
|
|
129
|
+
expected = expected * pow(commitment, pow(x, j, group.q), group.p) % group.p
|
|
130
|
+
return pow(group.g, y, group.p) == expected
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def recover_secret(shares: Sequence[tuple[int, int]], prime: int = 2089) -> int:
|
|
134
|
+
"""Interpolate at x=0 using Lagrange coefficients.
|
|
135
|
+
|
|
136
|
+
The caller must supply enough authentic shares. Without commitments,
|
|
137
|
+
this function cannot detect too few shares or a malicious participant.
|
|
138
|
+
|
|
139
|
+
>>> from blockchainkit.crypto import recover_secret
|
|
140
|
+
>>> recover_secret([(1, 8), (2, 11)], prime=17)
|
|
141
|
+
5
|
|
142
|
+
"""
|
|
143
|
+
if not shares or not is_prime(prime):
|
|
144
|
+
raise ValueError("need shares and a prime modulus below 2**64")
|
|
145
|
+
for x, y in shares:
|
|
146
|
+
integer(x, "x", 1)
|
|
147
|
+
integer(y, "y")
|
|
148
|
+
if x >= prime or y >= prime:
|
|
149
|
+
raise ValueError("share coordinates must be field elements")
|
|
150
|
+
if len({x for x, _ in shares}) != len(shares):
|
|
151
|
+
raise ValueError("share x coordinates must be distinct")
|
|
152
|
+
result = 0
|
|
153
|
+
for i, (xi, yi) in enumerate(shares):
|
|
154
|
+
weight = 1
|
|
155
|
+
for j, (xj, _) in enumerate(shares):
|
|
156
|
+
if i != j:
|
|
157
|
+
weight = weight * (-xj) * pow(xi - xj, -1, prime) % prime
|
|
158
|
+
result = (result + yi * weight) % prime
|
|
159
|
+
return result
|