blockchainkit.crypto#
Cryptographic foundations: hashing, public keys, sharing, curves, and proofs.
Hashing and commitments, Diffie-Hellman and RSA, blind signatures, secret sharing, elliptic curves, and Schnorr proofs and signatures.
Every public name below is re-exported by the subpackage: import it as
bk.crypto.<name>. The plotting helpers are the exception: import them
explicitly from blockchainkit.crypto.visualizers, which loads Matplotlib.
Types and results#
Shared types and result containers for blockchainkit.crypto.
- blockchainkit.crypto.core.base.Point = tuple[int, int] | None#
An affine curve point
(x, y), orNonefor the point at infinity.
- class blockchainkit.crypto.core.base.SchnorrSignature(commitment, response)[source]#
Bases:
objectA finite commitment point R and response scalar s.
- class blockchainkit.crypto.core.base.DiscreteLogResult(exponent, group_operations)[source]#
Bases:
objectAn exponent x with base**x = target, and the work spent finding it.
- Variables:
- Parameters:
- class blockchainkit.crypto.core.base.CollisionResult(first, second, digest, trials)[source]#
Bases:
objectTwo distinct inputs whose truncated hashes agree.
- Variables:
- Parameters:
- class blockchainkit.crypto.core.base.PuzzleSolution(puzzle_id, key, trials)[source]#
Bases:
objectThe contents of one solved Merkle puzzle, and the trials it took.
- Variables:
- Parameters:
- class blockchainkit.crypto.core.base.LamportKeyPair(private, public)[source]#
Bases:
objectA Lamport one-time key: 256 pairs of secret preimages and their hashes.
- Variables:
- Parameters:
Bases:
objectShamir shares plus public commitments that let each holder check theirs.
- Variables:
- Parameters:
Constructions and protocols#
The one-time pad (Vernam 1917; Shannon 1949): XOR with a key as long as the message.
- blockchainkit.crypto.systems.one_time_pad.xor_bytes(left, right)[source]#
Return the bytewise XOR of two equally long byte strings.
>>> from blockchainkit.crypto import xor_bytes >>> xor_bytes(b"\x0f", b"\xff") b'\xf0'
- blockchainkit.crypto.systems.one_time_pad.one_time_pad(message, key)[source]#
Encrypt or decrypt with a one-time pad:
message XOR key.The same call decrypts, because XOR is its own inverse. Shannon proved the pad perfectly secret when the key is uniformly random, as long as the message, and never reused: every plaintext of that length is then equally likely given the ciphertext. Reusing a key leaks
m1 XOR m2.- Parameters:
- Return type:
Examples
>>> from blockchainkit.crypto import one_time_pad >>> ciphertext = one_time_pad(b"hi", b"\x01\x02") >>> one_time_pad(ciphertext, b"\x01\x02") b'hi'
SHA-256 hashing, bit-level digest comparison, and birthday collisions.
- blockchainkit.crypto.systems.hashing.sha256(data)[source]#
Return the 32-byte SHA-256 digest of bytes.
>>> from blockchainkit.crypto import sha256 >>> sha256(b"abc").hex() 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
- blockchainkit.crypto.systems.hashing.hash256(data)[source]#
Return SHA-256(SHA-256(data)), as used in Bitcoin hashing.
- blockchainkit.crypto.systems.hashing.hamming_distance(left, right)[source]#
Count differing bits between equally long byte strings.
>>> from blockchainkit.crypto import hamming_distance >>> hamming_distance(bytes([0]), bytes([7])) 3
- blockchainkit.crypto.systems.hashing.truncated_hash(data, bits)[source]#
Return the top
bitsbits of SHA-256(data) as an integer.Truncation makes collisions findable, to measure the birthday bound.
>>> from blockchainkit.crypto import truncated_hash >>> truncated_hash(b"abc", 8) == sha256(b"abc")[0] True
- blockchainkit.crypto.systems.hashing.find_collision(bits, *, prefix=b'', max_trials=4194304)[source]#
Find two inputs whose
bits-bit truncated hashes agree.Hashes
prefix + counterfor counter = 0, 1, 2, … and remembers every truncated digest. By the birthday paradox a repeat is expected after aboutsqrt(pi/2 * 2**bits)trials, far fewer than the2**bitsneeded to hit one given digest (Yuval, 1979).- Parameters:
- Raises:
TimeoutError – No collision within
max_trials.- Return type:
Examples
>>> from blockchainkit.crypto import find_collision >>> result = find_collision(16) >>> result.first != result.second, result.trials < 2**10 (True, True)
SHA-256 written out as a Merkle-Damgard iteration, and its length-extension flaw.
Merkle and Damgard (1989) showed how to hash messages of any length with a
fixed-size compression function: pad the message to whole blocks, ending with
its length, then chain state = compress(state, block) from a fixed initial
value. The digest is the final state. So anyone who knows H(m) and
len(m) can keep compressing from it and compute H(m || pad || suffix)
without knowing m: the length-extension attack. HMAC exists to stop it.
This is a readable implementation for study. Use hashlib.sha256() (or
blockchainkit.crypto.systems.hashing.sha256()) for real hashing.
- blockchainkit.crypto.systems.merkle_damgard.SHA256_IV = (1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225)#
The initial chaining value (FIPS 180-4, section 5.3.3).
- blockchainkit.crypto.systems.merkle_damgard.sha256_compress(state, block)[source]#
Apply SHA-256’s compression function to one 64-byte block.
- blockchainkit.crypto.systems.merkle_damgard.sha256_padding(message_length)[source]#
Return the Merkle-Damgard strengthening for a message of that many bytes.
A 0x80 byte, zeros up to 56 bytes modulo 64, then the bit length as a 64-bit big-endian integer. Encoding the length is what makes the construction collision-resistant whenever the compression function is.
- blockchainkit.crypto.systems.merkle_damgard.merkle_damgard_sha256(data)[source]#
Hash by padding, then chaining the compression function from the IV.
>>> import hashlib >>> from blockchainkit.crypto import merkle_damgard_sha256 >>> merkle_damgard_sha256(b"abc") == hashlib.sha256(b"abc").digest() True
- blockchainkit.crypto.systems.merkle_damgard.length_extension(digest, original_length, suffix)[source]#
Extend
H(original)toH(original || glue || suffix)without knowingoriginal.- Parameters:
- Returns:
(glue, forged): the original message’s padding, which becomes part of the forged message, andSHA-256(original || glue || suffix).- Return type:
Message authentication codes: the naive secret-prefix hash, and HMAC (1996).
- blockchainkit.crypto.systems.mac.naive_mac(key, message)[source]#
Return SHA-256(key || message): a tempting MAC that is broken.
Anyone holding a tag can forge the tag of
message || glue || suffixwithlength_extension(). Shown for contrast withhmac_sha256(); never use it.
- blockchainkit.crypto.systems.mac.hmac_sha256(key, message)[source]#
Return HMAC-SHA256(key, message) (Bellare, Canetti and Krawczyk, 1996).
HMAC hashes twice with two derived keys,
H((k XOR opad) || H((k XOR ipad) || m)). The outer hash hides the inner chaining state, so length extension no longer works.>>> from blockchainkit.crypto import hmac_sha256 >>> hmac_sha256(b"key", b"The quick brown fox jumps over the lazy dog").hex()[:16] 'f7bc83f430538424'
Commitments: salted hash commitments (Blum 1981) and Pedersen commitments (1991).
A commitment is a sealed envelope: binding (the committer cannot change the contents later) and hiding (nobody can read them before the opening). A hash commitment is computationally hiding and binding. A Pedersen commitment is perfectly hiding and computationally binding, and commitments add.
- blockchainkit.crypto.systems.commitments.commit(message, salt)[source]#
Commit to a message with a secret salt of at least 16 bytes.
- Parameters:
- Returns:
Domain-separated digest. Length framing prevents ambiguous openings.
- Return type:
Notes
Hiding depends on salt entropy; binding relies on collision resistance.
- blockchainkit.crypto.systems.commitments.verify_commitment(digest, message, salt)[source]#
Check an opening against a commitment using constant-time comparison.
- blockchainkit.crypto.systems.commitments.pedersen_generators(group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
Return
(g, h): the group generator and a second, independent generator.his derived by hashing the group parameters into the subgroup, so nobody knowslog_g(h). Anyone who did could open a commitment to any value, which is whyhmust not be chosen by the committer.
- blockchainkit.crypto.systems.commitments.pedersen_commit(value, blinding, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
Commit to
valueasg**value * h**blinding mod p.With a uniformly random blinding factor the commitment is uniformly distributed whatever the value (perfect hiding). Opening it to a different value would reveal
log_g(h)(computational binding). Commitments multiply to a commitment of the sum:C(a, r) * C(b, s) = C(a + b, r + s).- Parameters:
value (
int) – Elements of the integers modulo the group order q.blinding (
int) – Elements of the integers modulo the group order q.group (
blockchainkit.crypto.systems.asymmetric.DHGroup) – A prime-order subgroup; defaults toTEACHING_GROUP.
- Return type:
Examples
>>> from blockchainkit.crypto import pedersen_commit, TEACHING_GROUP as G >>> pedersen_commit(2, 5) * pedersen_commit(3, 7) % G.p == pedersen_commit(5, 12) True
Merkle’s puzzles (1974-1978): key agreement from symmetric primitives alone.
Alice publishes count puzzles, each a short secret (an identifier and a
session key) encrypted under a deliberately weak key of bits bits. Bob
solves one at random, about 2**(bits-1) trials, and announces only its
identifier. An eavesdropper does not know which puzzle Bob chose and must
solve, on average, half of them. With count near 2**bits, honest work
is linear and the attacker’s quadratic: the first public-key idea, with a
gap that is only polynomial.
- blockchainkit.crypto.systems.puzzles.merkle_puzzles(count, bits, *, randbits=<bound method SystemRandom.getrandbits of <random.SystemRandom object>>)[source]#
Create Alice’s puzzles and her private table of identifiers to keys.
- Parameters:
count (
int) – Number of puzzles to publish.bits (
int) – Size of each weak key, between 1 and 24; solving takes up to2**bitstrials.randbits (
collections.abc.Callable) – Randomness source; injectrandom.Random(seed).getrandbitsonly for reproducible experiments.
- Returns:
(puzzles, table): 32-byte puzzles to publish, and the mapping from each 8-byte identifier to its 16-byte session key.- Return type:
- blockchainkit.crypto.systems.puzzles.solve_puzzle(puzzle, bits)[source]#
Open a puzzle by brute force over every weak key.
A trial succeeds when the decrypted plaintext starts with the known marker,
PUZZLE_MAGIC.- Raises:
ValueError – No weak key of that size opens the puzzle.
- Parameters:
- Return type:
Examples
>>> from random import Random >>> from blockchainkit.crypto import merkle_puzzles, solve_puzzle >>> puzzles, table = merkle_puzzles(3, 6, randbits=Random(1).getrandbits) >>> solution = solve_puzzle(puzzles[0], bits=6) >>> table[solution.puzzle_id] == solution.key True
Textbook Diffie–Hellman, RSA, and RSA blinding for small experiments.
These deliberately expose the mathematics. They are not encryption APIs for sensitive data: textbook RSA has no padding and DH has no authentication.
- class blockchainkit.crypto.systems.asymmetric.DHGroup(p=23, q=11, g=2)[source]#
Bases:
objectA prime-order subgroup of the multiplicative group modulo
p.- Parameters:
Examples
>>> from blockchainkit.crypto import DHGroup >>> group = DHGroup() >>> group.shared(group.public(7), 3) == group.shared(group.public(3), 7) True
- blockchainkit.crypto.systems.asymmetric.TEACHING_GROUP = DHGroup(p=4611686018427377339, q=2305843009213688669, g=4)#
The order-q subgroup of a 62-bit safe prime p = 2q + 1, generated by 4.
Large enough that its discrete logs take real work by hand-written baby-step giant-step (about 2**31 steps), small enough for exact arithmetic to stay instant. Still far too small for real security.
- Type:
- class blockchainkit.crypto.systems.asymmetric.RSAKeyPair(n, e, d)[source]#
Bases:
objectTextbook RSA exponents: modulus n, public e, and private d.
Construct with
rsa_keypair(); tiny factors make the algebra visible.
- blockchainkit.crypto.systems.asymmetric.rsa_keypair(p=61, q=53, e=17)[source]#
Build a textbook key pair from distinct primes below 2**64.
>>> from blockchainkit.crypto import rsa_keypair >>> key = rsa_keypair() >>> key.encrypt(65), key.decrypt(2790) (2790, 65)
- Parameters:
- Return type:
- blockchainkit.crypto.systems.asymmetric.rsa_blind(message, factor, key)[source]#
Return m*r**e mod n for a caller-selected invertible blinding factor.
The signer applies its private exponent to this blinded integer.
- Parameters:
message (int)
factor (int)
key (RSAKeyPair)
- Return type:
- blockchainkit.crypto.systems.asymmetric.rsa_unblind(blind_signature, factor, key)[source]#
Remove blinding so signature**e mod n equals the original message.
- Parameters:
blind_signature (int)
factor (int)
key (RSAKeyPair)
- Return type:
Generic discrete-logarithm algorithms: why group size and structure matter.
Baby-step giant-step (Shanks 1971) solves base**x = target in any cyclic group of order n with about 2*sqrt(n) multiplications. Pohlig-Hellman (1978) splits the problem along the prime factors of n, so its cost is governed by the largest prime factor, not by n. Together they explain why Diffie-Hellman and Schnorr work in a subgroup of large prime order.
- blockchainkit.crypto.systems.discrete_log.baby_step_giant_step(base, target, modulus, order)[source]#
Solve
base**x = target (mod modulus)in about2*sqrt(order)steps.Writing
x = i*m + jwithm = ceil(sqrt(order)), a table of baby stepsbase**jis matched against giant stepstarget * base**(-i*m).- Parameters:
- Returns:
The exponent and the number of table multiplications.
- Return type:
- Raises:
ValueError –
targetis not a power ofbase.
Examples
>>> from blockchainkit.crypto import baby_step_giant_step >>> baby_step_giant_step(2, pow(2, 77, 1019), 1019, 1018).exponent 77
- blockchainkit.crypto.systems.discrete_log.factor_order(n)[source]#
Factor a group order by trial division up to 2**20.
A cofactor left over after trial division must itself be prime, which is all Pohlig-Hellman needs; otherwise the order is rejected.
>>> from blockchainkit.crypto.systems.discrete_log import factor_order >>> factor_order(8100) {2: 2, 3: 4, 5: 2}
- blockchainkit.crypto.systems.discrete_log.pohlig_hellman(base, target, modulus, order)[source]#
Solve a discrete log by splitting the group order into prime powers.
For each prime power
p**edividingorder, the problem is mapped into the subgroup of orderp**eand solved one base-pdigit at a time with baby-step giant-step in a subgroup of orderp. The Chinese Remainder Theorem combines the answers. The cost is aboutsum(e * sqrt(p)), tiny when every prime factor is small.Parameters and return value are as for
baby_step_giant_step().Examples
>>> from blockchainkit.crypto import pohlig_hellman >>> pohlig_hellman(6, pow(6, 4321, 8101), 8101, 8100).exponent 4321
- Parameters:
- Return type:
Threshold secret sharing: Shamir (1979) and Feldman’s verifiable variant (1987).
- blockchainkit.crypto.systems.sharing.split_secret(secret, threshold, shares, prime=2089, *, randbelow=<function randbelow>)[source]#
Sample a polynomial with constant term
secretand return its shares.- Parameters:
secret (
int) – Field element in [0, prime).threshold (
int) – Number of shares needed, between 2 and shares.shares (
int) – Number of distinct nonzero evaluation points, below prime.prime (
int) – Prime field modulus below 2**64.randbelow (
collections.abc.Callable) – Uniform randomness source. Injectrandom.Random(seed).randrangeonly for reproducible teaching experiments.
- Returns:
(x, y) pairs at x = 1, …, shares.
- Return type:
- blockchainkit.crypto.systems.sharing.feldman_split(secret, threshold, shares, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4), *, randbelow=<function randbelow>)[source]#
Shamir-share a secret modulo the group order, and publish
g**a_j.Each commitment hides one polynomial coefficient in the exponent. A share holder checks
g**y == prod(C_j**(x**j)), which holds exactly when their share lies on the committed polynomial, so a cheating dealer is caught without anyone learning the secret. The commitmentg**secretis public, so the secret is only computationally hidden.- Parameters:
secret (
int) – Element of the integers modulo the group order q.threshold (
int) – As forsplit_secret().shares (
int) – As forsplit_secret().group (
blockchainkit.crypto.systems.asymmetric.DHGroup) – Prime-order group; shares live modulo its order q.randbelow (
collections.abc.Callable) – Uniform randomness source.
- Returns:
The shares and the public coefficient commitments.
- Return type:
Examples
>>> from random import Random >>> from blockchainkit.crypto import feldman_split, feldman_verify, TEACHING_GROUP >>> dealt = feldman_split(42, 2, 3, randbelow=Random(0).randrange) >>> all(feldman_verify(share, dealt.commitments) for share in dealt.shares) True
- blockchainkit.crypto.systems.sharing.feldman_verify(share, commitments, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#
Check one share against the dealer’s public commitments.
- blockchainkit.crypto.systems.sharing.recover_secret(shares, prime=2089)[source]#
Interpolate at x=0 using Lagrange coefficients.
The caller must supply enough authentic shares. Without commitments, this function cannot detect too few shares or a malicious participant.
>>> from blockchainkit.crypto import recover_secret >>> recover_secret([(1, 8), (2, 11)], prime=17) 5
Lamport one-time signatures (1979): signatures from a hash function alone.
The private key is 256 pairs of random preimages; the public key is their hashes. To sign, hash the message and, for each digest bit, reveal the preimage for that bit’s value. Verification hashes each revealed preimage. Each signature reveals half the private key, so a key must sign only once. Security rests only on the hash being one-way, so the scheme survives quantum computers, and its descendants (XMSS, SPHINCS+) are standardized.
- blockchainkit.crypto.systems.lamport.lamport_keypair(seed)[source]#
Derive a one-time key pair deterministically from a secret seed.
Each private preimage is SHA-256(domain || seed || index || bit). A real seed must be at least 32 random bytes and never reused.
>>> from blockchainkit.crypto import lamport_keypair >>> len(lamport_keypair(b"seed").public) 256
- Parameters:
seed (bytes)
- Return type:
- blockchainkit.crypto.systems.lamport.lamport_sign(message, key)[source]#
Reveal, for each bit of SHA-256(message), the matching private preimage.
- Parameters:
message (bytes)
key (LamportKeyPair)
- Return type:
- blockchainkit.crypto.systems.lamport.lamport_verify(message, signature, public)[source]#
Check that each revealed preimage hashes to the public value for its bit.
>>> from blockchainkit.crypto import lamport_keypair, lamport_sign, lamport_verify >>> key = lamport_keypair(b"seed") >>> lamport_verify(b"hello", lamport_sign(b"hello", key), key.public) True
Readable elliptic-curve group arithmetic, not constant-time cryptography.
Points are (x, y) tuples; None denotes the point at infinity.
The curve equation is y**2 = x**3 + a*x + b modulo p.
- class blockchainkit.crypto.systems.curves.Curve(p, a, b, generator, order, name='custom')[source]#
Bases:
objectA nonsingular prime-field curve with a prime-order generator.
- Parameters:
p (
int) – Field modulus and reduced curve coefficients.a (
int) – Field modulus and reduced curve coefficients.b (
int) – Field modulus and reduced curve coefficients.generator (
tupleofint) – Affine coordinates of the base point.order (
int) – Prime order of the base point.name (
str) – Human-readable label.
Notes
Custom field/order primes must be below 2**64, which bounds the educational primality check. secp256k1’s own field prime and group order are also accepted, each only in its own role.
- property cofactor_is_one: bool#
Whether the generator’s subgroup is provably the whole curve group.
Hasse’s theorem bounds the number of points: #E <= p + 1 + 2*sqrt(p). The subgroup order n divides #E, so if 2n exceeds that bound the cofactor #E/n must be 1. Every on-curve point then lies in the subgroup, and verifiers can skip the n*P = O membership check.
- blockchainkit.crypto.systems.curves.add(left, right, curve)[source]#
Add two validated points, including infinity and inverse pairs.
- blockchainkit.crypto.systems.curves.multiply(scalar, point, curve)[source]#
Multiply a point by a signed integer using double-and-add.
Scalars are not reduced modulo the generator order: this also allows checking subgroup membership for arbitrary points on a custom curve.
- blockchainkit.crypto.systems.curves.public_key(private, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Derive private*G for 1 <= private < generator order.
>>> from blockchainkit.crypto import public_key, TOY_CURVE >>> public_key(2, TOY_CURVE) (6, 3)
- blockchainkit.crypto.systems.curves.enumerate_points(curve)[source]#
List every finite point of a small curve, sorted, by trying each x.
Adding the point at infinity gives the whole group, so
len(enumerate_points(curve)) + 1is the group order #E. Useful for plotting a toy curve and for checking Hasse’s bound by hand.- Raises:
ValueError – The field has more than 10,000 elements; enumeration is O(p).
- Parameters:
curve (Curve)
- Return type:
Examples
>>> from blockchainkit.crypto import TOY_CURVE, enumerate_points >>> len(enumerate_points(TOY_CURVE)) + 1 == TOY_CURVE.order True
- blockchainkit.crypto.systems.curves.encode_point(point, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Return fixed-width uncompressed encoding (0x04 || x || y).
Educational Schnorr signatures and interactive proof transcripts.
This is a domain-separated teaching scheme, not Bitcoin BIP-340. Python curve arithmetic is variable-time. Explicit nonces exist for experiments; reusing a nonce reveals the private key.
- blockchainkit.crypto.systems.signatures.challenge(message, commitment, public, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Hash the curve parameters, public key, commitment, and message to a scalar.
- blockchainkit.crypto.systems.signatures.sign(message, private, *, nonce=None, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Sign with s = k + H(R, Q, m)*x modulo the subgroup order.
- Parameters:
message (
bytes) – Exact bytes to authenticate.private (
int) – Secret scalar x.nonce (
int, optional) – Experiment-only scalar k. Defaults to system randomness.curve (
blockchainkit.crypto.systems.curves.Curve) – Defaults to secp256k1; tiny curves illustrate algebra, not security.
- Returns:
Commitment R=kG and response s.
- Return type:
Examples
>>> from blockchainkit.crypto import sign, verify, public_key >>> signature = sign(b"lesson", 7, nonce=11) >>> verify(b"lesson", signature, public_key(7)) True
- blockchainkit.crypto.systems.signatures.verify_transcript(public, commitment, challenge_scalar, response, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Check the interactive Schnorr equation sG = R + cQ.
An accepting transcript is not itself proof of a live interaction: choosing c and s first permits simulation via R=sG-cQ. Soundness needs an unpredictable verifier challenge after the prover commits to R.
- blockchainkit.crypto.systems.signatures.simulate_transcript(public, challenge_scalar, response, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Forge an accepting transcript without the private key: R = sG - cQ.
Choosing the challenge and response first, then solving for the commitment, produces
(R, c, s)thatverify_transcript()accepts. This is the simulator in the zero-knowledge proof (Goldwasser, Micali and Rackoff, 1985): real transcripts and simulated ones look the same, so a transcript teaches the verifier nothing. A live proof is sound only because the verifier picks c after seeing R.
- blockchainkit.crypto.systems.signatures.deterministic_nonce(private, message, order=115792089237316195423570985008687907852837564279074904382605163141518161494337)[source]#
Derive a signing nonce from the key and message (RFC 6979, HMAC-SHA256).
Random nonces fail when the randomness does: a repeated or predictable nonce reveals the private key (see
recover_reused_nonce_key()). RFC 6979 removes the random number generator from signing: the nonce is an HMAC-DRBG output seeded with the private key and the message hash, so it is unpredictable without the key, and distinct messages get distinct nonces. This implements section 3.2 with SHA-256, and reproduces the RFC’s published nonces when given the same order.- Parameters:
- Return type:
Examples
>>> from blockchainkit.crypto import deterministic_nonce, sign, verify, public_key >>> k = deterministic_nonce(7, b"lesson") >>> verify(b"lesson", sign(b"lesson", 7, nonce=k), public_key(7)) True
- blockchainkit.crypto.systems.signatures.verify(message, signature, public, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Verify a Schnorr signature; malformed points or scalars return False.
- blockchainkit.crypto.systems.signatures.recover_reused_nonce_key(first, second, first_challenge, second_challenge, order=115792089237316195423570985008687907852837564279074904382605163141518161494337)[source]#
Recover x=(s1-s2)/(c1-c2) when two signatures reuse their nonce.
This demonstrates special soundness and why nonce reuse is catastrophic. The caller supplies the actual challenges derived from the two messages.
- Parameters:
first (SchnorrSignature)
second (SchnorrSignature)
first_challenge (int)
second_challenge (int)
order (int)
- Return type:
MuSig key aggregation (Maxwell, Poelstra, Seurin and Wuille, 2018).
Schnorr signatures add: if every signer uses nonce k_i and key x_i, the sum
of their responses is a valid signature for the sum of their public keys.
Summing keys naively is unsafe: an attacker who announces
Q_rogue = xG - Q_honest makes the sum equal xG, a key they control
alone. MuSig weights each key by a_i = H(L, Q_i), a hash of the whole
key list L, so no participant can choose their key to cancel the others.
Teaching simplification: musig_sign() plays every signer in one
process. Real MuSig exchanges nonce commitments first (or uses MuSig2’s two
nonces); without that round, a malicious co-signer can bias the joint nonce.
- blockchainkit.crypto.systems.multisig.musig_coefficients(publics, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Return
a_i = H(L || Q_i) mod nfor each key, with L the encoded key list.
- blockchainkit.crypto.systems.multisig.aggregate_public_keys(publics, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Return the MuSig aggregate key
sum(a_i * Q_i).>>> from blockchainkit.crypto import aggregate_public_keys, public_key >>> len(aggregate_public_keys([public_key(3), public_key(5)])) 2
- blockchainkit.crypto.systems.multisig.musig_sign(message, privates, nonces, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#
Produce one Schnorr signature valid for the aggregate of the signers’ keys.
Each signer i contributes
R_i = k_i Gands_i = k_i + c * a_i * x_iwith the shared challengec = H(R, Q_agg, m); the signature is(sum R_i, sum s_i)and verifies withverify()againstaggregate_public_keys().Examples
>>> from blockchainkit.crypto import musig_sign, aggregate_public_keys, public_key, verify >>> signature = musig_sign(b"m", [3, 5], [11, 13]) >>> verify(b"m", signature, aggregate_public_keys([public_key(3), public_key(5)])) True
Helpers#
Deterministic primality testing for the bounded parameters used in experiments.
- blockchainkit.crypto.utils.primes.is_prime(value)[source]#
Test primality deterministically for integers below 2**64.
Uses trial division followed by deterministic Miller–Rabin bases for this bounded domain. Larger numbers are deliberately not accepted.
>>> from blockchainkit.crypto.utils import is_prime >>> is_prime(7919), is_prime(561) (True, False)
Plotting#
Plotting helpers for blockchainkit.crypto: small curve groups and hash avalanche.
- blockchainkit.crypto.visualizers.plots.plot_curve_points(curve, *, label_multiples=True, ax=None)[source]#
Scatter every finite point of a small curve and label the multiples kG.
- Parameters:
curve (
blockchainkit.crypto.systems.curves.Curve) – A curve with p <= 10,000 (seeenumerate_points()).label_multiples (
bool) – Annotate each point kG of the generator’s subgroup with its k, which shows that scalar multiplication jumps around the plane with no visible pattern: the discrete-log problem.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.crypto.visualizers.plots.plot_hamming_distances(distances, bits=256, ax=None)[source]#
Histogram of output-bit differences against the ideal Binomial(bits, 1/2).
- Parameters:
distances (
collections.abc.Sequenceofint) – Hamming distances between digests of related inputs, e.g. fromhamming_distance()after a one-bit flip.bits (
int) – Digest length in bits.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: