mirror of
https://github.com/bitcoin/bips.git
synced 2025-05-12 12:03:29 +00:00
103 lines
3.1 KiB
Python
103 lines
3.1 KiB
Python
|
"""Reference implementation of DLEQ BIP for secp256k1 with unit tests."""
|
||
|
|
||
|
from hashlib import sha256
|
||
|
import random
|
||
|
from secp256k1 import G, GE
|
||
|
import sys
|
||
|
import unittest
|
||
|
|
||
|
|
||
|
DLEQ_TAG_AUX = "BIP0???/aux"
|
||
|
DLEQ_TAG_NONCE = "BIP0???/nonce"
|
||
|
DLEQ_TAG_CHALLENGE = "BIP0???/challenge"
|
||
|
|
||
|
|
||
|
def TaggedHash(tag: str, data: bytes) -> bytes:
|
||
|
ss = sha256(tag.encode()).digest()
|
||
|
ss += ss
|
||
|
ss += data
|
||
|
return sha256(ss).digest()
|
||
|
|
||
|
|
||
|
def xor_bytes(lhs: bytes, rhs: bytes) -> bytes:
|
||
|
assert len(lhs) == len(rhs)
|
||
|
return bytes([lhs[i] ^ rhs[i] for i in range(len(lhs))])
|
||
|
|
||
|
|
||
|
def dleq_challenge(A: GE, B: GE, C: GE, R1: GE, R2: GE) -> int:
|
||
|
return int.from_bytes(TaggedHash(DLEQ_TAG_CHALLENGE,
|
||
|
A.to_bytes_compressed() + B.to_bytes_compressed() + C.to_bytes_compressed() +
|
||
|
R1.to_bytes_compressed() + R2.to_bytes_compressed()), 'big')
|
||
|
|
||
|
|
||
|
def dleq_generate_proof(a: int, B: GE, r: bytes) -> bytes | None:
|
||
|
assert len(r) == 32
|
||
|
if not (0 < a < GE.ORDER):
|
||
|
return None
|
||
|
if B.infinity:
|
||
|
return None
|
||
|
A = a * G
|
||
|
C = a * B
|
||
|
t = xor_bytes(a.to_bytes(32, 'big'), TaggedHash(DLEQ_TAG_AUX, r))
|
||
|
rand = TaggedHash(DLEQ_TAG_NONCE, t + A.to_bytes_compressed() + C.to_bytes_compressed())
|
||
|
k = int.from_bytes(rand, 'big') % GE.ORDER
|
||
|
if k == 0:
|
||
|
return None
|
||
|
R1 = k * G
|
||
|
R2 = k * B
|
||
|
e = dleq_challenge(A, B, C, R1, R2)
|
||
|
s = (k + e * a) % GE.ORDER
|
||
|
proof = e.to_bytes(32, 'big') + s.to_bytes(32, 'big')
|
||
|
if not dleq_verify_proof(A, B, C, proof):
|
||
|
return None
|
||
|
return proof
|
||
|
|
||
|
|
||
|
def dleq_verify_proof(A: GE, B: GE, C: GE, proof: bytes) -> bool:
|
||
|
assert len(proof) == 64
|
||
|
e = int.from_bytes(proof[:32], 'big')
|
||
|
s = int.from_bytes(proof[32:], 'big')
|
||
|
if s >= GE.ORDER:
|
||
|
return False
|
||
|
# TODO: implement subtraction operator (__sub__) for GE class to simplify these terms
|
||
|
R1 = s * G + (-e * A)
|
||
|
if R1.infinity:
|
||
|
return False
|
||
|
R2 = s * B + (-e * C)
|
||
|
if R2.infinity:
|
||
|
return False
|
||
|
if e != dleq_challenge(A, B, C, R1, R2):
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
class DLEQTests(unittest.TestCase):
|
||
|
def test_dleq(self):
|
||
|
seed = random.randrange(sys.maxsize)
|
||
|
random.seed(seed)
|
||
|
print(f"PRNG seed is: {seed}")
|
||
|
for _ in range(10):
|
||
|
# generate random keypairs for both parties
|
||
|
a = random.randrange(1, GE.ORDER)
|
||
|
A = a * G
|
||
|
b = random.randrange(1, GE.ORDER)
|
||
|
B = b * G
|
||
|
|
||
|
# create shared secret
|
||
|
C = a * B
|
||
|
|
||
|
# create dleq proof
|
||
|
rand_aux = random.randbytes(32)
|
||
|
proof = dleq_generate_proof(a, B, rand_aux)
|
||
|
self.assertTrue(proof is not None)
|
||
|
# verify dleq proof
|
||
|
success = dleq_verify_proof(A, B, C, proof)
|
||
|
self.assertTrue(success)
|
||
|
|
||
|
# flip a random bit in the dleq proof and check that verification fails
|
||
|
for _ in range(5):
|
||
|
proof_damaged = list(proof)
|
||
|
proof_damaged[random.randrange(len(proof))] ^= (1 << (random.randrange(8)))
|
||
|
success = dleq_verify_proof(A, B, C, bytes(proof_damaged))
|
||
|
self.assertFalse(success)
|