From 4f5d87adc8bed41db534e2a3ec31096eb24a0851 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Wed, 23 Oct 2024 21:41:26 -0400 Subject: [PATCH 01/23] Bip Draft: DLEQ --- bip-DLEQ.mediawiki | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 bip-DLEQ.mediawiki diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki new file mode 100644 index 0000000000..bb2735f01f --- /dev/null +++ b/bip-DLEQ.mediawiki @@ -0,0 +1,92 @@ +
+  BIP: ?
+  Layer: Applications
+  Title: Discrete Log Equality Proofs
+  Author: Andrew Toth 
+          Ruben Somsen 
+  Comments-URI: TBD
+  Status: Draft
+  Type: Standards Track
+  License: BSD-2-Clause
+  Created: 2024-06-29
+  Post-History: TBD
+
+ +== Introduction == + +=== Abstract === + +This document proposes a standard for 64-byte zero-knowledge ''discrete logarithm equality proofs'' (DLEQ proofs) over an elliptic curve. For given elliptic curve points ''A'', ''B'', ''C'', and ''G'', the prover proves knowledge of a scalar ''a'' such that ''A = a⋅G'' and ''C = a⋅B'' without revealing anything about ''a''. This can, for instance, be useful in ECDH: if ''A'' and ''B'' are ECDH public keys, and ''C'' is their ECDH shared secret computed as ''C = a⋅B'', the proof establishes that the same secret key ''a'' is used for generating both ''A'' and ''C'' without revealing ''a''. + +=== Copyright === + +This document is licensed under the 2-clause BSD license. + +=== Motivation === + +[https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki#specification BIP352] requires senders to compute output scripts using ECDH shared secrets from the same secret keys used to sign the inputs. Generating an incorrect signature will produce an invalid transaction that will be rejected by consensus. An incorrectly generated output script can still be consensus-valid, meaning funds may be lost if it gets broadcast. +By producing a DLEQ proof for the generated ECDH shared secrets, the signing entity can prove to other entities that the output scripts have been generated correctly without revealing the private keys. + +== Specification == + +All conventions and notations are used as defined in [https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Notation BIP327]. + +=== DLEQ Proof Generation === + +Input: +* The secret key ''a'': a 256-bit unsigned integer +* The public key ''B'': a point on the curve +* The generator point ''G'': a point on the curve +* Auxiliary random data ''r'': a 32-byte array + +The algorithm ''GenerateProof(a, B, r)'' is defined as: +* Fail if ''a = 0'' or ''a ≥ n''. +* Fail if ''is_infinite(B)''. +* Let ''A = a⋅G''. +* Let ''C = a⋅B''. +* Let ''t'' be the byte-wise xor of ''bytes(32, a)'' and ''hashBIP0???/aux(r)''. +* Let ''rand = hashBIP0???/nonce(t || cbytes(A) || cbytes(C))''. +* Let ''k = int(rand) mod n''. +* Fail if ''k = 0''. +* Let ''R1 = k⋅G''. +* Let ''R2 = k⋅B''. +* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2)))''. +* Let ''s = (k + e⋅a) mod n''. +* Let ''proof = bytes(32, e) || bytes(32, s)''. +* If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort. +* Return the proof ''proof''. + +=== DLEQ Proof Verification === + +Input: +* The public key of the secret key used in the proof generation ''A'': a point on the curve +* The public key used in the proof generation ''B'': a point on the curve +* The result of multiplying the secret and public keys used in the proof generation ''C'': a point on the curve +* The generator point used in the proof generation ''G'': a point on the curve +* A proof ''proof'': a 64-byte array + +The algorithm ''VerifyProof(A, B, C, G, proof)'' is defined as: +* Let ''e = int(proof[0:32])''. +* Let ''s = int(proof[32:64])''; fail if ''s ≥ n''. +* Let ''R1 = s⋅G - e⋅A''. +* Fail if ''is_infinite(R1)''. +* Let ''R2 = s⋅B - e⋅C''. +* Fail if ''is_infinite(R2)''. +* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2)))''. +* Return success iff no failure occurred before reaching this point. + +== Test Vectors and Reference Code == + +TBD + +== Changelog == + +TBD + +== Footnotes == + + + +== Acknowledgements == + +TBD \ No newline at end of file From 0c7e54d780d059ebbab345946d7c6adbc61fef15 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Mon, 18 Nov 2024 12:14:43 +0100 Subject: [PATCH 02/23] BIP-DLEQ: add reference implementation for secp256k1 --- bip-DLEQ/reference.py | 102 ++++++++++++ bip-DLEQ/secp256k1.py | 354 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 456 insertions(+) create mode 100644 bip-DLEQ/reference.py create mode 100644 bip-DLEQ/secp256k1.py diff --git a/bip-DLEQ/reference.py b/bip-DLEQ/reference.py new file mode 100644 index 0000000000..f508776c6a --- /dev/null +++ b/bip-DLEQ/reference.py @@ -0,0 +1,102 @@ +"""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) diff --git a/bip-DLEQ/secp256k1.py b/bip-DLEQ/secp256k1.py new file mode 100644 index 0000000000..50a46dce37 --- /dev/null +++ b/bip-DLEQ/secp256k1.py @@ -0,0 +1,354 @@ +# Copyright (c) 2022-2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +"""Test-only implementation of low-level secp256k1 field and group arithmetic + +It is designed for ease of understanding, not performance. + +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for +anything but tests. + +Exports: +* FE: class for secp256k1 field elements +* GE: class for secp256k1 group elements +* G: the secp256k1 generator point +""" + +import unittest +from hashlib import sha256 + +class FE: + """Objects of this class represent elements of the field GF(2**256 - 2**32 - 977). + + They are represented internally in numerator / denominator form, in order to delay inversions. + """ + + # The size of the field (also its modulus and characteristic). + SIZE = 2**256 - 2**32 - 977 + + def __init__(self, a=0, b=1): + """Initialize a field element a/b; both a and b can be ints or field elements.""" + if isinstance(a, FE): + num = a._num + den = a._den + else: + num = a % FE.SIZE + den = 1 + if isinstance(b, FE): + den = (den * b._num) % FE.SIZE + num = (num * b._den) % FE.SIZE + else: + den = (den * b) % FE.SIZE + assert den != 0 + if num == 0: + den = 1 + self._num = num + self._den = den + + def __add__(self, a): + """Compute the sum of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._den + self._den * a._num, self._den * a._den) + return FE(self._num + self._den * a, self._den) + + def __radd__(self, a): + """Compute the sum of an integer and a field element.""" + return FE(a) + self + + def __sub__(self, a): + """Compute the difference of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._den - self._den * a._num, self._den * a._den) + return FE(self._num - self._den * a, self._den) + + def __rsub__(self, a): + """Compute the difference of an integer and a field element.""" + return FE(a) - self + + def __mul__(self, a): + """Compute the product of two field elements (second may be int).""" + if isinstance(a, FE): + return FE(self._num * a._num, self._den * a._den) + return FE(self._num * a, self._den) + + def __rmul__(self, a): + """Compute the product of an integer with a field element.""" + return FE(a) * self + + def __truediv__(self, a): + """Compute the ratio of two field elements (second may be int).""" + return FE(self, a) + + def __pow__(self, a): + """Raise a field element to an integer power.""" + return FE(pow(self._num, a, FE.SIZE), pow(self._den, a, FE.SIZE)) + + def __neg__(self): + """Negate a field element.""" + return FE(-self._num, self._den) + + def __int__(self): + """Convert a field element to an integer in range 0..p-1. The result is cached.""" + if self._den != 1: + self._num = (self._num * pow(self._den, -1, FE.SIZE)) % FE.SIZE + self._den = 1 + return self._num + + def sqrt(self): + """Compute the square root of a field element if it exists (None otherwise). + + Due to the fact that our modulus is of the form (p % 4) == 3, the Tonelli-Shanks + algorithm (https://en.wikipedia.org/wiki/Tonelli-Shanks_algorithm) is simply + raising the argument to the power (p + 1) / 4. + + To see why: (p-1) % 2 = 0, so 2 divides the order of the multiplicative group, + and thus only half of the non-zero field elements are squares. An element a is + a (nonzero) square when Euler's criterion, a^((p-1)/2) = 1 (mod p), holds. We're + looking for x such that x^2 = a (mod p). Given a^((p-1)/2) = 1, that is equivalent + to x^2 = a^(1 + (p-1)/2) mod p. As (1 + (p-1)/2) is even, this is equivalent to + x = a^((1 + (p-1)/2)/2) mod p, or x = a^((p+1)/4) mod p.""" + v = int(self) + s = pow(v, (FE.SIZE + 1) // 4, FE.SIZE) + if s**2 % FE.SIZE == v: + return FE(s) + return None + + def is_square(self): + """Determine if this field element has a square root.""" + # A more efficient algorithm is possible here (Jacobi symbol). + return self.sqrt() is not None + + def is_even(self): + """Determine whether this field element, represented as integer in 0..p-1, is even.""" + return int(self) & 1 == 0 + + def __eq__(self, a): + """Check whether two field elements are equal (second may be an int).""" + if isinstance(a, FE): + return (self._num * a._den - self._den * a._num) % FE.SIZE == 0 + return (self._num - self._den * a) % FE.SIZE == 0 + + def to_bytes(self): + """Convert a field element to a 32-byte array (BE byte order).""" + return int(self).to_bytes(32, 'big') + + @staticmethod + def from_bytes(b): + """Convert a 32-byte array to a field element (BE byte order, no overflow allowed).""" + v = int.from_bytes(b, 'big') + if v >= FE.SIZE: + return None + return FE(v) + + def __str__(self): + """Convert this field element to a 64 character hex string.""" + return f"{int(self):064x}" + + def __repr__(self): + """Get a string representation of this field element.""" + return f"FE(0x{int(self):x})" + + +class GE: + """Objects of this class represent secp256k1 group elements (curve points or infinity) + + Normal points on the curve have fields: + * x: the x coordinate (a field element) + * y: the y coordinate (a field element, satisfying y^2 = x^3 + 7) + * infinity: False + + The point at infinity has field: + * infinity: True + """ + + # Order of the group (number of points on the curve, plus 1 for infinity) + ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + + # Number of valid distinct x coordinates on the curve. + ORDER_HALF = ORDER // 2 + + def __init__(self, x=None, y=None): + """Initialize a group element with specified x and y coordinates, or infinity.""" + if x is None: + # Initialize as infinity. + assert y is None + self.infinity = True + else: + # Initialize as point on the curve (and check that it is). + fx = FE(x) + fy = FE(y) + assert fy**2 == fx**3 + 7 + self.infinity = False + self.x = fx + self.y = fy + + def __add__(self, a): + """Add two group elements together.""" + # Deal with infinity: a + infinity == infinity + a == a. + if self.infinity: + return a + if a.infinity: + return self + if self.x == a.x: + if self.y != a.y: + # A point added to its own negation is infinity. + assert self.y + a.y == 0 + return GE() + else: + # For identical inputs, use the tangent (doubling formula). + lam = (3 * self.x**2) / (2 * self.y) + else: + # For distinct inputs, use the line through both points (adding formula). + lam = (self.y - a.y) / (self.x - a.x) + # Determine point opposite to the intersection of that line with the curve. + x = lam**2 - (self.x + a.x) + y = lam * (self.x - x) - self.y + return GE(x, y) + + @staticmethod + def mul(*aps): + """Compute a (batch) scalar group element multiplication. + + GE.mul((a1, p1), (a2, p2), (a3, p3)) is identical to a1*p1 + a2*p2 + a3*p3, + but more efficient.""" + # Reduce all the scalars modulo order first (so we can deal with negatives etc). + naps = [(a % GE.ORDER, p) for a, p in aps] + # Start with point at infinity. + r = GE() + # Iterate over all bit positions, from high to low. + for i in range(255, -1, -1): + # Double what we have so far. + r = r + r + # Add then add the points for which the corresponding scalar bit is set. + for (a, p) in naps: + if (a >> i) & 1: + r += p + return r + + def __rmul__(self, a): + """Multiply an integer with a group element.""" + if self == G: + return FAST_G.mul(a) + return GE.mul((a, self)) + + def __neg__(self): + """Compute the negation of a group element.""" + if self.infinity: + return self + return GE(self.x, -self.y) + + def to_bytes_compressed(self): + """Convert a non-infinite group element to 33-byte compressed encoding.""" + assert not self.infinity + return bytes([3 - self.y.is_even()]) + self.x.to_bytes() + + def to_bytes_uncompressed(self): + """Convert a non-infinite group element to 65-byte uncompressed encoding.""" + assert not self.infinity + return b'\x04' + self.x.to_bytes() + self.y.to_bytes() + + def to_bytes_xonly(self): + """Convert (the x coordinate of) a non-infinite group element to 32-byte xonly encoding.""" + assert not self.infinity + return self.x.to_bytes() + + @staticmethod + def lift_x(x): + """Return group element with specified field element as x coordinate (and even y).""" + y = (FE(x)**3 + 7).sqrt() + if y is None: + return None + if not y.is_even(): + y = -y + return GE(x, y) + + @staticmethod + def from_bytes(b): + """Convert a compressed or uncompressed encoding to a group element.""" + assert len(b) in (33, 65) + if len(b) == 33: + if b[0] != 2 and b[0] != 3: + return None + x = FE.from_bytes(b[1:]) + if x is None: + return None + r = GE.lift_x(x) + if r is None: + return None + if b[0] == 3: + r = -r + return r + else: + if b[0] != 4: + return None + x = FE.from_bytes(b[1:33]) + y = FE.from_bytes(b[33:]) + if y**2 != x**3 + 7: + return None + return GE(x, y) + + @staticmethod + def from_bytes_xonly(b): + """Convert a point given in xonly encoding to a group element.""" + assert len(b) == 32 + x = FE.from_bytes(b) + if x is None: + return None + return GE.lift_x(x) + + @staticmethod + def is_valid_x(x): + """Determine whether the provided field element is a valid X coordinate.""" + return (FE(x)**3 + 7).is_square() + + def __str__(self): + """Convert this group element to a string.""" + if self.infinity: + return "(inf)" + return f"({self.x},{self.y})" + + def __repr__(self): + """Get a string representation for this group element.""" + if self.infinity: + return "GE()" + return f"GE(0x{int(self.x):x},0x{int(self.y):x})" + +# The secp256k1 generator point +G = GE.lift_x(0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798) + + +class FastGEMul: + """Table for fast multiplication with a constant group element. + + Speed up scalar multiplication with a fixed point P by using a precomputed lookup table with + its powers of 2: + + table = [P, 2*P, 4*P, (2^3)*P, (2^4)*P, ..., (2^255)*P] + + During multiplication, the points corresponding to each bit set in the scalar are added up, + i.e. on average ~128 point additions take place. + """ + + def __init__(self, p): + self.table = [p] # table[i] = (2^i) * p + for _ in range(255): + p = p + p + self.table.append(p) + + def mul(self, a): + result = GE() + a = a % GE.ORDER + for bit in range(a.bit_length()): + if a & (1 << bit): + result += self.table[bit] + return result + +# Precomputed table with multiples of G for fast multiplication +FAST_G = FastGEMul(G) + +class TestFrameworkSecp256k1(unittest.TestCase): + def test_H(self): + H = sha256(G.to_bytes_uncompressed()).digest() + assert GE.lift_x(FE.from_bytes(H)) is not None + self.assertEqual(H.hex(), "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0") From cc7bb12b245bd73712cab58a6fbea32506fef417 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Mon, 9 Dec 2024 13:19:57 -0500 Subject: [PATCH 03/23] Add optional message to DLEQ --- bip-DLEQ.mediawiki | 8 +++-- bip-DLEQ/reference.py | 70 +++++++++++++++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index bb2735f01f..726daea142 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -38,6 +38,7 @@ Input: * The public key ''B'': a point on the curve * The generator point ''G'': a point on the curve * Auxiliary random data ''r'': a 32-byte array +* An optional message ''m'': a 32-byte array The algorithm ''GenerateProof(a, B, r)'' is defined as: * Fail if ''a = 0'' or ''a ≥ n''. @@ -50,7 +51,8 @@ The algorithm ''GenerateProof(a, B, r)'' is defined as: * Fail if ''k = 0''. * Let ''R1 = k⋅G''. * Let ''R2 = k⋅B''. -* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2)))''. +* Let ''m' = m if m is provided, otherwise an empty byte array''. +* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || cbytes(m')))''. * Let ''s = (k + e⋅a) mod n''. * Let ''proof = bytes(32, e) || bytes(32, s)''. * If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort. @@ -64,6 +66,7 @@ Input: * The result of multiplying the secret and public keys used in the proof generation ''C'': a point on the curve * The generator point used in the proof generation ''G'': a point on the curve * A proof ''proof'': a 64-byte array +* An optional message ''m'': a 32-byte array The algorithm ''VerifyProof(A, B, C, G, proof)'' is defined as: * Let ''e = int(proof[0:32])''. @@ -72,7 +75,8 @@ The algorithm ''VerifyProof(A, B, C, G, proof)'' is defined as: * Fail if ''is_infinite(R1)''. * Let ''R2 = s⋅B - e⋅C''. * Fail if ''is_infinite(R2)''. -* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2)))''. +* Let ''m' = m if m is provided, otherwise an empty byte array''. +* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || cbytes(m')))''. * Return success iff no failure occurred before reaching this point. == Test Vectors and Reference Code == diff --git a/bip-DLEQ/reference.py b/bip-DLEQ/reference.py index f508776c6a..231617acde 100644 --- a/bip-DLEQ/reference.py +++ b/bip-DLEQ/reference.py @@ -24,13 +24,30 @@ def xor_bytes(lhs: bytes, rhs: bytes) -> bytes: 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: +def dleq_challenge( + A: GE, B: GE, C: GE, R1: GE, R2: GE, G: GE = G, m: bytes | None = None +) -> int: + if m is not None: + assert len(m) == 32 + m = bytes([]) if m is None else m.to_bytes(32, "big") + return int.from_bytes( + TaggedHash( + DLEQ_TAG_CHALLENGE, + A.to_bytes_compressed() + + B.to_bytes_compressed() + + C.to_bytes_compressed() + + G.to_bytes_compressed() + + R1.to_bytes_compressed() + + R2.to_bytes_compressed() + + m, + ), + "big", + ) + + +def dleq_generate_proof( + a: int, B: GE, r: bytes, G: GE = G, m: bytes | None = None +) -> bytes | None: assert len(r) == 32 if not (0 < a < GE.ORDER): return None @@ -38,25 +55,29 @@ def dleq_generate_proof(a: int, B: GE, r: bytes) -> bytes | None: 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 + 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') + 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: +def dleq_verify_proof( + A: GE, B: GE, C: GE, proof: bytes, G: GE = G, m: bytes | None = None +) -> bool: assert len(proof) == 64 - e = int.from_bytes(proof[:32], 'big') - s = int.from_bytes(proof[32:], 'big') + 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 @@ -97,6 +118,25 @@ def test_dleq(self): # 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))) + proof_damaged[random.randrange(len(proof))] ^= 1 << ( + random.randrange(8) + ) + success = dleq_verify_proof(A, B, C, bytes(proof_damaged)) + self.assertFalse(success) + + # create the same dleq proof with a message + message = random.randbytes(32) + proof = dleq_generate_proof(a, B, rand_aux, m=message) + self.assertTrue(proof is not None) + # verify dleq proof with a message + success = dleq_verify_proof(A, B, C, proof, m=message) + 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) From ed98dc7b028095767469cc4d7d716698abaddfb2 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Mon, 9 Dec 2024 14:00:22 -0500 Subject: [PATCH 04/23] Add some more commentary --- bip-DLEQ.mediawiki | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 726daea142..a9c73b7c6d 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -33,14 +33,16 @@ All conventions and notations are used as defined in [https://github.com/bitcoin === DLEQ Proof Generation === +The following generates a proof that the result of ''a⋅B'' and the result of ''a⋅G'' are both generated from the same scalar ''a'' without having to reveal ''a''. + Input: * The secret key ''a'': a 256-bit unsigned integer * The public key ''B'': a point on the curve -* The generator point ''G'': a point on the curve * Auxiliary random data ''r'': a 32-byte array -* An optional message ''m'': a 32-byte array +* The generator point ''G'': a point on the curve ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from Secp256k1, passing it as an input here lets this algorithm be used for other curves. +* An optional message ''m'': a 32-byte array ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement. -The algorithm ''GenerateProof(a, B, r)'' is defined as: +The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as: * Fail if ''a = 0'' or ''a ≥ n''. * Fail if ''is_infinite(B)''. * Let ''A = a⋅G''. @@ -60,15 +62,17 @@ The algorithm ''GenerateProof(a, B, r)'' is defined as: === DLEQ Proof Verification === +The following verifies the proof generated in the previous section. If the following algorithm succeeds, the points ''A'' and ''C'' were both generated from the same scalar. The former from multiplying by ''G'', and the latter from multiplying by ''B''. + Input: * The public key of the secret key used in the proof generation ''A'': a point on the curve * The public key used in the proof generation ''B'': a point on the curve * The result of multiplying the secret and public keys used in the proof generation ''C'': a point on the curve -* The generator point used in the proof generation ''G'': a point on the curve * A proof ''proof'': a 64-byte array -* An optional message ''m'': a 32-byte array +* The generator point used in the proof generation ''G'': a point on the curve ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from Secp256k1, passing it as an input here lets this algorithm be used for other curves. +* An optional message ''m'': a 32-byte array ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement. -The algorithm ''VerifyProof(A, B, C, G, proof)'' is defined as: +The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as: * Let ''e = int(proof[0:32])''. * Let ''s = int(proof[32:64])''; fail if ''s ≥ n''. * Let ''R1 = s⋅G - e⋅A''. @@ -79,9 +83,13 @@ The algorithm ''VerifyProof(A, B, C, G, proof)'' is defined as: * Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || cbytes(m')))''. * Return success iff no failure occurred before reaching this point. +==Backwards Compatibility== + +This proposal is compatible with all older clients. + == Test Vectors and Reference Code == -TBD +A reference python implementation is included [./bip-DLEQ/reference.py here]. == Changelog == From b5d47dfef9078811074b16760fd681d390e09ee8 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Mon, 9 Dec 2024 16:26:10 -0500 Subject: [PATCH 05/23] add theStack as co-author --- bip-DLEQ.mediawiki | 1 + 1 file changed, 1 insertion(+) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index a9c73b7c6d..ba5353440f 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -4,6 +4,7 @@ Title: Discrete Log Equality Proofs Author: Andrew Toth Ruben Somsen + Sebastian Falbesoner Comments-URI: TBD Status: Draft Type: Standards Track From 597004acef977e6905038e559799a8e755920b03 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Tue, 10 Dec 2024 19:17:46 -0500 Subject: [PATCH 06/23] Lowercase secp Co-authored-by: Sebastian Falbesoner --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index ba5353440f..28220cf178 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -40,7 +40,7 @@ Input: * The secret key ''a'': a 256-bit unsigned integer * The public key ''B'': a point on the curve * Auxiliary random data ''r'': a 32-byte array -* The generator point ''G'': a point on the curve ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from Secp256k1, passing it as an input here lets this algorithm be used for other curves. +* The generator point ''G'': a point on the curve ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from secp256k1, passing it as an input here lets this algorithm be used for other curves. * An optional message ''m'': a 32-byte array ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement. The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as: From e4f1d7bb8e16e8ffa4b7d469b2a8b7222389284f Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Tue, 10 Dec 2024 19:18:16 -0500 Subject: [PATCH 07/23] Remove cbytes wrapper from m' Co-authored-by: Sebastian Falbesoner --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 28220cf178..5726059175 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -55,7 +55,7 @@ The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as: * Let ''R1 = k⋅G''. * Let ''R2 = k⋅B''. * Let ''m' = m if m is provided, otherwise an empty byte array''. -* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || cbytes(m')))''. +* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. * Let ''s = (k + e⋅a) mod n''. * Let ''proof = bytes(32, e) || bytes(32, s)''. * If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort. From b838696c97b9dce48a817578927410466ab1a24c Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Tue, 10 Dec 2024 19:20:50 -0500 Subject: [PATCH 08/23] Remove cbytes wrapper from m' --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 5726059175..90de1e15c9 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -81,7 +81,7 @@ The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as: * Let ''R2 = s⋅B - e⋅C''. * Fail if ''is_infinite(R2)''. * Let ''m' = m if m is provided, otherwise an empty byte array''. -* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || cbytes(m')))''. +* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. * Return success iff no failure occurred before reaching this point. ==Backwards Compatibility== From dab5571c37252cd170de073eb36ae7b9138dffed Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Sat, 21 Dec 2024 01:01:10 +0100 Subject: [PATCH 09/23] bugfix: respect message m in DLEQ proof generation/verification --- bip-DLEQ/reference.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bip-DLEQ/reference.py b/bip-DLEQ/reference.py index 231617acde..ac431985fc 100644 --- a/bip-DLEQ/reference.py +++ b/bip-DLEQ/reference.py @@ -25,11 +25,11 @@ def xor_bytes(lhs: bytes, rhs: bytes) -> bytes: def dleq_challenge( - A: GE, B: GE, C: GE, R1: GE, R2: GE, G: GE = G, m: bytes | None = None + A: GE, B: GE, C: GE, R1: GE, R2: GE, m: bytes | None, G: GE = G, ) -> int: if m is not None: assert len(m) == 32 - m = bytes([]) if m is None else m.to_bytes(32, "big") + m = bytes([]) if m is None else m return int.from_bytes( TaggedHash( DLEQ_TAG_CHALLENGE, @@ -64,10 +64,10 @@ def dleq_generate_proof( return None R1 = k * G R2 = k * B - e = dleq_challenge(A, B, C, R1, R2) + e = dleq_challenge(A, B, C, R1, R2, m) 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): + if not dleq_verify_proof(A, B, C, proof, m=m): return None return proof @@ -87,7 +87,7 @@ def dleq_verify_proof( R2 = s * B + (-e * C) if R2.infinity: return False - if e != dleq_challenge(A, B, C, R1, R2): + if e != dleq_challenge(A, B, C, R1, R2, m): return False return True From 6b169524225e0bc3421f131d930ccb6f715f1068 Mon Sep 17 00:00:00 2001 From: Sebastian Falbesoner Date: Fri, 20 Dec 2024 16:36:37 +0100 Subject: [PATCH 10/23] Add test vectors for DLEQ proof generation/verification Squashed from the following commits: - Add skeleton for generating DLEQ proof test vectors - Add run_test_vectors.py counterpart for generated DLEQ proofs - Add DLEQ test vectors for proof verification --- bip-DLEQ/gen_test_vectors.py | 122 +++++++++++++++++++++++++++++++++++ bip-DLEQ/run_test_vectors.py | 74 +++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100755 bip-DLEQ/gen_test_vectors.py create mode 100755 bip-DLEQ/run_test_vectors.py diff --git a/bip-DLEQ/gen_test_vectors.py b/bip-DLEQ/gen_test_vectors.py new file mode 100755 index 0000000000..9c49744bb7 --- /dev/null +++ b/bip-DLEQ/gen_test_vectors.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Generate the BIP-DLEQ test vectors (limited to secp256k1 generator right now).""" +import csv +import os +import sys +from reference import ( + TaggedHash, + dleq_generate_proof, + dleq_verify_proof, +) +from secp256k1 import G, GE + + +NUM_SUCCESS_TEST_VECTORS = 5 +DLEQ_TAG_TESTVECTORS_RNG = "BIP0???/testvectors_rng" + +FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') +FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') + + +def random_scalar_int(vector_i, purpose): + rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) + return int.from_bytes(rng_out, 'big') % GE.ORDER + + +def random_bytes(vector_i, purpose): + rng_out = TaggedHash(DLEQ_TAG_TESTVECTORS_RNG, purpose.encode() + vector_i.to_bytes(4, 'little')) + return rng_out + + +def create_test_vector_data(vector_i): + a = random_scalar_int(vector_i, "scalar_a") + A = a * G + b = random_scalar_int(vector_i, "scalar_b") + B = b * G + C = a * B # shared secret + assert C.to_bytes_compressed() == (b * A).to_bytes_compressed() + auxrand = random_bytes(vector_i, "auxrand") + msg = random_bytes(vector_i, "message") + proof = dleq_generate_proof(a, B, auxrand, m=msg) + return (a, A, b, B, C, auxrand, msg, proof) + + +TEST_VECTOR_DATA = [create_test_vector_data(i) for i in range(NUM_SUCCESS_TEST_VECTORS)] + + +def gen_all_generate_proof_vectors(f): + writer = csv.writer(f) + writer.writerow(("index", "secret_a", "point_B", "auxrand_r", "message", "result_proof", "comment")) + + # success cases with random values + idx = 0 + for i in range(NUM_SUCCESS_TEST_VECTORS): + a, A, b, B, C, auxrand, msg, proof = TEST_VECTOR_DATA[i] + assert proof is not None and len(proof) == 64 + writer.writerow((idx, f"{a:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), proof.hex(), f"Success case {i+1}")) + idx += 1 + + # failure cases: a is not within group order (a=0, a=N) + a_invalid = 0 + assert dleq_generate_proof(a_invalid, B, auxrand, m=msg) is None + writer.writerow((idx, f"{a_invalid:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=0)")) + idx += 1 + a_invalid = GE.ORDER + assert dleq_generate_proof(a_invalid, B, auxrand, m=msg) is None + writer.writerow((idx, f"{a_invalid:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=N [group order])")) + idx += 1 + + # failure case: B is point at infinity + B_infinity = GE() + B_infinity_str = "INFINITY" + assert dleq_generate_proof(a, B_infinity, auxrand, m=msg) is None + writer.writerow((idx, f"{a:02x}", B_infinity_str, auxrand.hex(), msg.hex(), "INVALID", f"Failure case (B is point at infinity)")) + idx += 1 + + +def gen_all_verify_proof_vectors(f): + writer = csv.writer(f) + writer.writerow(("index", "point_A", "point_B", "point_C", "proof", "message", "result_success", "comment")) + + # success cases (same as above) + idx = 0 + for i in range(NUM_SUCCESS_TEST_VECTORS): + _, A, _, B, C, _, msg, proof = TEST_VECTOR_DATA[i] + assert dleq_verify_proof(A, B, C, proof, m=msg) + writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + C.to_bytes_compressed().hex(), proof.hex(), msg.hex(), "TRUE", f"Success case {i+1}")) + idx += 1 + + # other permutations of A, B, C should always fail + for i, points in enumerate(([A, C, B], [B, A, C], [B, C, A], [C, A, B], [C, B, A])): + assert not dleq_verify_proof(points[0], points[1], points[2], proof, m=msg) + writer.writerow((idx, points[0].to_bytes_compressed().hex(), points[1].to_bytes_compressed().hex(), + points[2].to_bytes_compressed().hex(), proof.hex(), msg.hex(), "FALSE", f"Swapped points case {i+1}")) + idx += 1 + + # modifying proof should fail (flip one bit) + proof_damage_pos = random_scalar_int(idx, "damage_pos") % 256 + proof_damaged = list(proof) + proof_damaged[proof_damage_pos // 8] ^= (1 << (proof_damage_pos % 8)) + proof_damaged = bytes(proof_damaged) + writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + C.to_bytes_compressed().hex(), proof_damaged.hex(), msg.hex(), "FALSE", f"Tampered proof (random bit-flip)")) + idx += 1 + + # modifying message should fail (flip one bit) + msg_damage_pos = random_scalar_int(idx, "damage_pos") % 256 + msg_damaged = list(msg) + msg_damaged[proof_damage_pos // 8] ^= (1 << (msg_damage_pos % 8)) + msg_damaged = bytes(msg_damaged) + writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + C.to_bytes_compressed().hex(), proof.hex(), msg_damaged.hex(), "FALSE", f"Tampered message (random bit-flip)")) + idx += 1 + + +if __name__ == "__main__": + print(f"Generating {FILENAME_GENERATE_PROOF_TEST}...") + with open(FILENAME_GENERATE_PROOF_TEST, "w", encoding="utf-8") as fil_generate_proof: + gen_all_generate_proof_vectors(fil_generate_proof) + print(f"Generating {FILENAME_VERIFY_PROOF_TEST}...") + with open(FILENAME_VERIFY_PROOF_TEST, "w", encoding="utf-8") as fil_verify_proof: + gen_all_verify_proof_vectors(fil_verify_proof) diff --git a/bip-DLEQ/run_test_vectors.py b/bip-DLEQ/run_test_vectors.py new file mode 100755 index 0000000000..242b797ede --- /dev/null +++ b/bip-DLEQ/run_test_vectors.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Run the BIP-DLEQ test vectors.""" +import csv +import os +import sys +from reference import ( + dleq_generate_proof, + dleq_verify_proof, +) +from secp256k1 import GE + + +FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') +FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') + + +all_passed = True +print("-----------------------------------------") +print("----- Proof generation test vectors -----") +print("-----------------------------------------") +with open(FILENAME_GENERATE_PROOF_TEST, newline='') as csvfile: + reader = csv.reader(csvfile) + reader.__next__() + for row in reader: + (index, seckey_a_hex, point_B_hex, aux_rand_hex, msg_hex, result_str, comment) = row + a = int.from_bytes(bytes.fromhex(seckey_a_hex), 'big') + B = GE() if point_B_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_B_hex)) + aux_rand = bytes.fromhex(aux_rand_hex) + msg = bytes.fromhex(msg_hex) + print('Test vector', ('#' + index).rjust(3, ' ') + ':' + f' ({comment})') + expected_result = None if result_str == 'INVALID' else bytes.fromhex(result_str) + actual_result = dleq_generate_proof(a, B, aux_rand, m=msg) + if expected_result == actual_result: + print(' * Passed proof generation test.') + else: + print(' * Failed proof generation test.') + print(' Expected proof: ', expected_result.hex() if expected_result is not None else 'INVALID') + print(' Actual proof: ', actual_result.hex() if actual_result is not None else 'INVALID') + all_passed = False + print() + + +print("-------------------------------------------") +print("----- Proof verification test vectors -----") +print("-------------------------------------------") +with open(FILENAME_VERIFY_PROOF_TEST, newline='') as csvfile: + reader = csv.reader(csvfile) + reader.__next__() + for row in reader: + (index, point_A_hex, point_B_hex, point_C_hex, proof_hex, msg_hex, result_success, comment) = row + A = GE() if point_A_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_A_hex)) + B = GE() if point_B_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_B_hex)) + C = GE() if point_C_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_C_hex)) + proof = bytes.fromhex(proof_hex) + msg = bytes.fromhex(msg_hex) + print('Test vector', ('#' + index).rjust(3, ' ') + ':' + f' ({comment})') + expected_result = result_success == 'TRUE' + actual_result = dleq_verify_proof(A, B, C, proof, m=msg) + if expected_result == actual_result: + print(' * Passed proof verification test.') + else: + print(' * Failed proof verification test.') + print(' Expected verification result: ', expected_result) + print(' Actual verification result: ', actual_result) + all_passed = False + + +print() +if all_passed: + print('All test vectors passed.') + sys.exit(0) +else: + print('Some test vectors failed.') + sys.exit(1) From 1f875a3706c561aaf89171ee3580e41c9169947e Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 12:52:28 -0500 Subject: [PATCH 11/23] Add note about generating and running test vectors --- bip-DLEQ.mediawiki | 1 + 1 file changed, 1 insertion(+) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 90de1e15c9..90c9cd1832 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -91,6 +91,7 @@ This proposal is compatible with all older clients. == Test Vectors and Reference Code == A reference python implementation is included [./bip-DLEQ/reference.py here]. +Test vectors can be generated by running `./bip-DLEQ/gen_test_vectors.py` which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with `./bip-DLEQ/run_test_vectors.py`. == Changelog == From 687198d72bf47501f0135083ff231a5e291a89a6 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 12:52:54 -0500 Subject: [PATCH 12/23] Fail if any point is infinity when verifying --- bip-DLEQ.mediawiki | 1 + bip-DLEQ/reference.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 90c9cd1832..12f2eb6866 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -74,6 +74,7 @@ Input: * An optional message ''m'': a 32-byte array ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement. The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as: +* Fail if any of ''is_infinite(A)'', ''is_infinite(B)'', ''is_infinite(C)'', ''is_infinite(G)'' * Let ''e = int(proof[0:32])''. * Let ''s = int(proof[32:64])''; fail if ''s ≥ n''. * Let ''R1 = s⋅G - e⋅A''. diff --git a/bip-DLEQ/reference.py b/bip-DLEQ/reference.py index ac431985fc..f7e69c7ba1 100644 --- a/bip-DLEQ/reference.py +++ b/bip-DLEQ/reference.py @@ -75,6 +75,8 @@ def dleq_generate_proof( def dleq_verify_proof( A: GE, B: GE, C: GE, proof: bytes, G: GE = G, m: bytes | None = None ) -> bool: + if A.infinity or B.infinity or C.infinity or G.infinity: + return False assert len(proof) == 64 e = int.from_bytes(proof[:32], "big") s = int.from_bytes(proof[32:], "big") From f5d1c12aa9d0388880300c9fe95a53d6cf9f251c Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 12:53:36 -0500 Subject: [PATCH 13/23] Add acknowledgements --- bip-DLEQ.mediawiki | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 12f2eb6866..8d717731ee 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -104,4 +104,5 @@ TBD == Acknowledgements == -TBD \ No newline at end of file +Thanks to josibake, Tim Ruffing, benma, stratospher, waxwing, Yuval Kogman and all others who +participated in discussions on this topic. From fd60d8eded89a80cce0bfb9aefd3ac7d0469bdc8 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 16:11:12 -0500 Subject: [PATCH 14/23] Add description of proof --- bip-DLEQ.mediawiki | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 8d717731ee..c5412b2fa3 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -32,6 +32,29 @@ By producing a DLEQ proof for the generated ECDH shared secrets, the signing ent All conventions and notations are used as defined in [https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-Notation BIP327]. +=== Description === + +The basic proof generation uses a random scalar ''k'', the secret ''a'', and the point being proven ''C = a⋅B''. + +* Let ''R1 = k⋅G''. +* Let ''R2 = k⋅B''. +* Let ''e = hash(R1 || R2)''. +* Let ''s = (k + e⋅a)''. + +Providing only the point being proven ''C'', and ''e'' and ''s'' as a proof does not reveal ''a'' or ''k''. + +Verifying the proof involves recreating ''R1'' and ''R2'' with only ''e'' and ''s'' as follows: + +* Let ''R1 = s⋅G - e⋅A''. +* Let ''R2 = s⋅B - e⋅C''. + +This can be verified by substituing ''s = (k + e⋅a)'': + +* ''s⋅G - e⋅A = (k + e⋅a)⋅G - e⋅A = k⋅G + e⋅(a⋅G) - e⋅A = k⋅G + e⋅A - e⋅A = k⋅G''. +* ''s⋅B - e⋅C = (k + e⋅a)⋅B - e⋅C = k⋅B + e⋅(a⋅B) - e⋅C = k⋅B + e⋅C - e⋅C = k⋅B''. + +Thus verifying ''e = hash(R1 || R2)'' proves the discrete logarithm equivalency of ''A'' and ''C''. + === DLEQ Proof Generation === The following generates a proof that the result of ''a⋅B'' and the result of ''a⋅G'' are both generated from the same scalar ''a'' without having to reveal ''a''. From 90e7027f1957e9483d78ac6aae193c9dfdcb5128 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 16:11:46 -0500 Subject: [PATCH 15/23] Remove changelog --- bip-DLEQ.mediawiki | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index c5412b2fa3..b6ed6e4a91 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -41,7 +41,7 @@ The basic proof generation uses a random scalar ''k'', the secret ''a'', and the * Let ''e = hash(R1 || R2)''. * Let ''s = (k + e⋅a)''. -Providing only the point being proven ''C'', and ''e'' and ''s'' as a proof does not reveal ''a'' or ''k''. +Providing only ''C'', ''e'' and ''s'' as a proof does not reveal ''a'' or ''k''. Verifying the proof involves recreating ''R1'' and ''R2'' with only ''e'' and ''s'' as follows: @@ -117,10 +117,6 @@ This proposal is compatible with all older clients. A reference python implementation is included [./bip-DLEQ/reference.py here]. Test vectors can be generated by running `./bip-DLEQ/gen_test_vectors.py` which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with `./bip-DLEQ/run_test_vectors.py`. -== Changelog == - -TBD - == Footnotes == From 0b590d0d5d94f428b39b7d62332abebe7d236b40 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 16:17:11 -0500 Subject: [PATCH 16/23] Add footnote recommending using fresh randomness for each proof --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index b6ed6e4a91..c03260bed7 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -62,7 +62,7 @@ The following generates a proof that the result of ''a⋅B'' and the result of ' Input: * The secret key ''a'': a 256-bit unsigned integer * The public key ''B'': a point on the curve -* Auxiliary random data ''r'': a 32-byte array +* Auxiliary random data ''r'': a 32-byte array ''' Why include auxiliary random data?''' The auxiliary random data should be set to fresh randomness for each proof. The same rationale and recommendations from [https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing BIP340] should be applied. * The generator point ''G'': a point on the curve ''' Why include the generator point G as an input?''' While all other BIPs have used the generator point from secp256k1, passing it as an input here lets this algorithm be used for other curves. * An optional message ''m'': a 32-byte array ''' Why include a message as an input?''' This could be useful for protocols that want to authorize on a compound statement, not just knowledge of a scalar. This allows the protocol to combine knowledge of the scalar and the statement. From a0d8aad1dfee73da6ec82cbbaed073fc643dc914 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Dec 2024 16:18:24 -0500 Subject: [PATCH 17/23] Fix typo --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index c03260bed7..71ed2637a7 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -48,7 +48,7 @@ Verifying the proof involves recreating ''R1'' and ''R2'' * Let ''R1 = s⋅G - e⋅A''. * Let ''R2 = s⋅B - e⋅C''. -This can be verified by substituing ''s = (k + e⋅a)'': +This can be verified by substituting ''s = (k + e⋅a)'': * ''s⋅G - e⋅A = (k + e⋅a)⋅G - e⋅A = k⋅G + e⋅(a⋅G) - e⋅A = k⋅G + e⋅A - e⋅A = k⋅G''. * ''s⋅B - e⋅C = (k + e⋅a)⋅B - e⋅C = k⋅B + e⋅(a⋅B) - e⋅C = k⋅B + e⋅C - e⋅C = k⋅B''. From 5799659da0a2086c66f78df3b0f1be863e8d16bb Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 26 Dec 2024 11:52:39 -0500 Subject: [PATCH 18/23] Update bip-DLEQ.mediawiki Co-authored-by: Mark "Murch" Erhardt --- bip-DLEQ.mediawiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki index 71ed2637a7..6b47055275 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-DLEQ.mediawiki @@ -1,5 +1,5 @@
-  BIP: ?
+  BIP: 374
   Layer: Applications
   Title: Discrete Log Equality Proofs
   Author: Andrew Toth 

From b533b92ed3a57d93dfe8bf1e6db5ace212876d59 Mon Sep 17 00:00:00 2001
From: Andrew Toth 
Date: Thu, 26 Dec 2024 11:52:51 -0500
Subject: [PATCH 19/23] Update bip-DLEQ.mediawiki

Co-authored-by: Mark "Murch" Erhardt 
---
 bip-DLEQ.mediawiki | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/bip-DLEQ.mediawiki b/bip-DLEQ.mediawiki
index 6b47055275..dd1f29c0e2 100644
--- a/bip-DLEQ.mediawiki
+++ b/bip-DLEQ.mediawiki
@@ -9,7 +9,7 @@
   Status: Draft
   Type: Standards Track
   License: BSD-2-Clause
-  Created: 2024-06-29
+  Created: 2024-12-26
   Post-History: TBD
 
From 1350bc423e1417efb1b47e3e0c36ebc346d7388e Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 26 Dec 2024 12:06:44 -0500 Subject: [PATCH 20/23] BIP374 --- bip-DLEQ.mediawiki => bip-0374.mediawiki | 8 ++++---- bip-DLEQ/gen_test_vectors.py | 2 +- bip-DLEQ/reference.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) rename bip-DLEQ.mediawiki => bip-0374.mediawiki (97%) diff --git a/bip-DLEQ.mediawiki b/bip-0374.mediawiki similarity index 97% rename from bip-DLEQ.mediawiki rename to bip-0374.mediawiki index dd1f29c0e2..17947d7f63 100644 --- a/bip-DLEQ.mediawiki +++ b/bip-0374.mediawiki @@ -71,14 +71,14 @@ The algorithm ''GenerateProof(a, B, r, G, m)'' is defined as: * Fail if ''is_infinite(B)''. * Let ''A = a⋅G''. * Let ''C = a⋅B''. -* Let ''t'' be the byte-wise xor of ''bytes(32, a)'' and ''hashBIP0???/aux(r)''. -* Let ''rand = hashBIP0???/nonce(t || cbytes(A) || cbytes(C))''. +* Let ''t'' be the byte-wise xor of ''bytes(32, a)'' and ''hashBIP0374/aux(r)''. +* Let ''rand = hashBIP0374/nonce(t || cbytes(A) || cbytes(C))''. * Let ''k = int(rand) mod n''. * Fail if ''k = 0''. * Let ''R1 = k⋅G''. * Let ''R2 = k⋅B''. * Let ''m' = m if m is provided, otherwise an empty byte array''. -* Let ''e = int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. +* Let ''e = int(hashBIP0374/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. * Let ''s = (k + e⋅a) mod n''. * Let ''proof = bytes(32, e) || bytes(32, s)''. * If ''VerifyProof(A, B, C, proof)'' (see below) returns failure, abort. @@ -105,7 +105,7 @@ The algorithm ''VerifyProof(A, B, C, proof, G, m)'' is defined as: * Let ''R2 = s⋅B - e⋅C''. * Fail if ''is_infinite(R2)''. * Let ''m' = m if m is provided, otherwise an empty byte array''. -* Fail if ''e ≠ int(hashBIP0???/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. +* Fail if ''e ≠ int(hashBIP0374/challenge(cbytes(A) || cbytes(B) || cbytes(C) || cbytes(G) || cbytes(R1) || cbytes(R2) || m'))''. * Return success iff no failure occurred before reaching this point. ==Backwards Compatibility== diff --git a/bip-DLEQ/gen_test_vectors.py b/bip-DLEQ/gen_test_vectors.py index 9c49744bb7..5d73757d9a 100755 --- a/bip-DLEQ/gen_test_vectors.py +++ b/bip-DLEQ/gen_test_vectors.py @@ -12,7 +12,7 @@ NUM_SUCCESS_TEST_VECTORS = 5 -DLEQ_TAG_TESTVECTORS_RNG = "BIP0???/testvectors_rng" +DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng" FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv') FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv') diff --git a/bip-DLEQ/reference.py b/bip-DLEQ/reference.py index f7e69c7ba1..e1e29cc77d 100644 --- a/bip-DLEQ/reference.py +++ b/bip-DLEQ/reference.py @@ -7,9 +7,9 @@ import unittest -DLEQ_TAG_AUX = "BIP0???/aux" -DLEQ_TAG_NONCE = "BIP0???/nonce" -DLEQ_TAG_CHALLENGE = "BIP0???/challenge" +DLEQ_TAG_AUX = "BIP0374/aux" +DLEQ_TAG_NONCE = "BIP0374/nonce" +DLEQ_TAG_CHALLENGE = "BIP0374/challenge" def TaggedHash(tag: str, data: bytes) -> bytes: From 9d6dc6b6819b98698810b3c2c6c302b119ad31fe Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 26 Dec 2024 12:10:52 -0500 Subject: [PATCH 21/23] Update README table, post-history, and comments-uri --- README.mediawiki | 7 +++++++ bip-0374.mediawiki | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.mediawiki b/README.mediawiki index 6c48d0e237..ed40f4a0f8 100644 --- a/README.mediawiki +++ b/README.mediawiki @@ -1177,6 +1177,13 @@ Those proposing changes should consider that ultimately consent may rest with th | Standard | Draft |- +| [[bip-0374.mediawiki|374]] +| Applications +| Discrete Log Equality Proofs +| Andrew Toth, Ruben Somsen, Sebastian Falbesoner +| Standard +| Draft +|- | [[bip-0379.md|379]] | Applications | Miniscript diff --git a/bip-0374.mediawiki b/bip-0374.mediawiki index 17947d7f63..0b04405ff5 100644 --- a/bip-0374.mediawiki +++ b/bip-0374.mediawiki @@ -5,12 +5,13 @@ Author: Andrew Toth Ruben Somsen Sebastian Falbesoner - Comments-URI: TBD + Comments-URI: https://github.com/bitcoin/bips/wiki/Comments:BIP-0374 Status: Draft Type: Standards Track License: BSD-2-Clause Created: 2024-12-26 - Post-History: TBD + Post-History: https://gist.github.com/andrewtoth/df97c3260cc8d12f09d3855ee61322ea + https://groups.google.com/g/bitcoindev/c/MezoKV5md7s == Introduction == From 18421209071ecb14f2913c9573276b874313403c Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 26 Dec 2024 14:16:57 -0500 Subject: [PATCH 22/23] Clarify restraints on given points --- bip-0374.mediawiki | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bip-0374.mediawiki b/bip-0374.mediawiki index 0b04405ff5..d04f3fc1bc 100644 --- a/bip-0374.mediawiki +++ b/bip-0374.mediawiki @@ -18,7 +18,7 @@ === Abstract === -This document proposes a standard for 64-byte zero-knowledge ''discrete logarithm equality proofs'' (DLEQ proofs) over an elliptic curve. For given elliptic curve points ''A'', ''B'', ''C'', and ''G'', the prover proves knowledge of a scalar ''a'' such that ''A = a⋅G'' and ''C = a⋅B'' without revealing anything about ''a''. This can, for instance, be useful in ECDH: if ''A'' and ''B'' are ECDH public keys, and ''C'' is their ECDH shared secret computed as ''C = a⋅B'', the proof establishes that the same secret key ''a'' is used for generating both ''A'' and ''C'' without revealing ''a''. +This document proposes a standard for 64-byte zero-knowledge ''discrete logarithm equality proofs'' (DLEQ proofs) over an elliptic curve. For given elliptic curve points ''A'', ''B'', ''C'', ''G'', and a scalar ''a'' known on ly to the prover where ''A = a⋅G'' and ''C = a⋅B'', the prover proves knowledge of ''a'' without revealing anything about ''a''. This can, for instance, be useful in ECDH: if ''A'' and ''B'' are ECDH public keys, and ''C'' is their ECDH shared secret computed as ''C = a⋅B'', the proof establishes that the same secret key ''a'' is used for generating both ''A'' and ''C'' without revealing ''a''. === Copyright === @@ -115,8 +115,8 @@ This proposal is compatible with all older clients. == Test Vectors and Reference Code == -A reference python implementation is included [./bip-DLEQ/reference.py here]. -Test vectors can be generated by running `./bip-DLEQ/gen_test_vectors.py` which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with `./bip-DLEQ/run_test_vectors.py`. +A reference python implementation is included [./bip-0374/reference.py here]. +Test vectors can be generated by running `./bip-0374/gen_test_vectors.py` which will produce a CSV file of random test vectors for both generating and verifying proofs. These can be run against the reference implementation with `./bip-0374/run_test_vectors.py`. == Footnotes == From cb3afee8502b7811b5789c197f3202062d8cfb6e Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Thu, 26 Dec 2024 14:17:52 -0500 Subject: [PATCH 23/23] Move test vectors to bip-0374 directory, add tests for G --- {bip-DLEQ => bip-0374}/gen_test_vectors.py | 42 ++++++++++++---------- {bip-DLEQ => bip-0374}/reference.py | 2 +- {bip-DLEQ => bip-0374}/run_test_vectors.py | 11 +++--- {bip-DLEQ => bip-0374}/secp256k1.py | 0 4 files changed, 31 insertions(+), 24 deletions(-) rename {bip-DLEQ => bip-0374}/gen_test_vectors.py (64%) rename {bip-DLEQ => bip-0374}/reference.py (98%) rename {bip-DLEQ => bip-0374}/run_test_vectors.py (82%) rename {bip-DLEQ => bip-0374}/secp256k1.py (100%) diff --git a/bip-DLEQ/gen_test_vectors.py b/bip-0374/gen_test_vectors.py similarity index 64% rename from bip-DLEQ/gen_test_vectors.py rename to bip-0374/gen_test_vectors.py index 5d73757d9a..aaa090a453 100755 --- a/bip-DLEQ/gen_test_vectors.py +++ b/bip-0374/gen_test_vectors.py @@ -8,7 +8,7 @@ dleq_generate_proof, dleq_verify_proof, ) -from secp256k1 import G, GE +from secp256k1 import G as GENERATOR, GE NUM_SUCCESS_TEST_VECTORS = 5 @@ -29,6 +29,11 @@ def random_bytes(vector_i, purpose): def create_test_vector_data(vector_i): + g = random_scalar_int(vector_i, "scalar_g") + assert g < GE.ORDER + assert g > 0 + G = g * GENERATOR + assert not G.infinity a = random_scalar_int(vector_i, "scalar_a") A = a * G b = random_scalar_int(vector_i, "scalar_b") @@ -37,60 +42,59 @@ def create_test_vector_data(vector_i): assert C.to_bytes_compressed() == (b * A).to_bytes_compressed() auxrand = random_bytes(vector_i, "auxrand") msg = random_bytes(vector_i, "message") - proof = dleq_generate_proof(a, B, auxrand, m=msg) - return (a, A, b, B, C, auxrand, msg, proof) - + proof = dleq_generate_proof(a, B, auxrand, G=G, m=msg) + return (G, a, A, b, B, C, auxrand, msg, proof) TEST_VECTOR_DATA = [create_test_vector_data(i) for i in range(NUM_SUCCESS_TEST_VECTORS)] def gen_all_generate_proof_vectors(f): writer = csv.writer(f) - writer.writerow(("index", "secret_a", "point_B", "auxrand_r", "message", "result_proof", "comment")) + writer.writerow(("index", "point_G", "scalar_a", "point_B", "auxrand_r", "message", "result_proof", "comment")) # success cases with random values idx = 0 for i in range(NUM_SUCCESS_TEST_VECTORS): - a, A, b, B, C, auxrand, msg, proof = TEST_VECTOR_DATA[i] + G, a, A, b, B, C, auxrand, msg, proof = TEST_VECTOR_DATA[i] assert proof is not None and len(proof) == 64 - writer.writerow((idx, f"{a:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), proof.hex(), f"Success case {i+1}")) + writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), proof.hex(), f"Success case {i+1}")) idx += 1 # failure cases: a is not within group order (a=0, a=N) a_invalid = 0 - assert dleq_generate_proof(a_invalid, B, auxrand, m=msg) is None - writer.writerow((idx, f"{a_invalid:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=0)")) + assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None + writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a_invalid:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=0)")) idx += 1 a_invalid = GE.ORDER - assert dleq_generate_proof(a_invalid, B, auxrand, m=msg) is None - writer.writerow((idx, f"{a_invalid:02x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=N [group order])")) + assert dleq_generate_proof(a_invalid, B, auxrand, G=G, m=msg) is None + writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a_invalid:064x}", B.to_bytes_compressed().hex(), auxrand.hex(), msg.hex(), "INVALID", f"Failure case (a=N [group order])")) idx += 1 # failure case: B is point at infinity B_infinity = GE() B_infinity_str = "INFINITY" assert dleq_generate_proof(a, B_infinity, auxrand, m=msg) is None - writer.writerow((idx, f"{a:02x}", B_infinity_str, auxrand.hex(), msg.hex(), "INVALID", f"Failure case (B is point at infinity)")) + writer.writerow((idx, G.to_bytes_compressed().hex(), f"{a:064x}", B_infinity_str, auxrand.hex(), msg.hex(), "INVALID", f"Failure case (B is point at infinity)")) idx += 1 def gen_all_verify_proof_vectors(f): writer = csv.writer(f) - writer.writerow(("index", "point_A", "point_B", "point_C", "proof", "message", "result_success", "comment")) + writer.writerow(("index", "point_G", "point_A", "point_B", "point_C", "proof", "message", "result_success", "comment")) # success cases (same as above) idx = 0 for i in range(NUM_SUCCESS_TEST_VECTORS): - _, A, _, B, C, _, msg, proof = TEST_VECTOR_DATA[i] - assert dleq_verify_proof(A, B, C, proof, m=msg) - writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + G, _, A, _, B, C, _, msg, proof = TEST_VECTOR_DATA[i] + assert dleq_verify_proof(A, B, C, proof, G=G, m=msg) + writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), C.to_bytes_compressed().hex(), proof.hex(), msg.hex(), "TRUE", f"Success case {i+1}")) idx += 1 # other permutations of A, B, C should always fail for i, points in enumerate(([A, C, B], [B, A, C], [B, C, A], [C, A, B], [C, B, A])): assert not dleq_verify_proof(points[0], points[1], points[2], proof, m=msg) - writer.writerow((idx, points[0].to_bytes_compressed().hex(), points[1].to_bytes_compressed().hex(), + writer.writerow((idx, G.to_bytes_compressed().hex(), points[0].to_bytes_compressed().hex(), points[1].to_bytes_compressed().hex(), points[2].to_bytes_compressed().hex(), proof.hex(), msg.hex(), "FALSE", f"Swapped points case {i+1}")) idx += 1 @@ -99,7 +103,7 @@ def gen_all_verify_proof_vectors(f): proof_damaged = list(proof) proof_damaged[proof_damage_pos // 8] ^= (1 << (proof_damage_pos % 8)) proof_damaged = bytes(proof_damaged) - writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), C.to_bytes_compressed().hex(), proof_damaged.hex(), msg.hex(), "FALSE", f"Tampered proof (random bit-flip)")) idx += 1 @@ -108,7 +112,7 @@ def gen_all_verify_proof_vectors(f): msg_damaged = list(msg) msg_damaged[proof_damage_pos // 8] ^= (1 << (msg_damage_pos % 8)) msg_damaged = bytes(msg_damaged) - writer.writerow((idx, A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), + writer.writerow((idx, G.to_bytes_compressed().hex(), A.to_bytes_compressed().hex(), B.to_bytes_compressed().hex(), C.to_bytes_compressed().hex(), proof.hex(), msg_damaged.hex(), "FALSE", f"Tampered message (random bit-flip)")) idx += 1 diff --git a/bip-DLEQ/reference.py b/bip-0374/reference.py similarity index 98% rename from bip-DLEQ/reference.py rename to bip-0374/reference.py index e1e29cc77d..edb7efda4f 100644 --- a/bip-DLEQ/reference.py +++ b/bip-0374/reference.py @@ -67,7 +67,7 @@ def dleq_generate_proof( e = dleq_challenge(A, B, C, R1, R2, m) 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, m=m): + if not dleq_verify_proof(A, B, C, proof, G=G, m=m): return None return proof diff --git a/bip-DLEQ/run_test_vectors.py b/bip-0374/run_test_vectors.py similarity index 82% rename from bip-DLEQ/run_test_vectors.py rename to bip-0374/run_test_vectors.py index 242b797ede..44b4f196db 100755 --- a/bip-DLEQ/run_test_vectors.py +++ b/bip-0374/run_test_vectors.py @@ -22,14 +22,16 @@ reader = csv.reader(csvfile) reader.__next__() for row in reader: - (index, seckey_a_hex, point_B_hex, aux_rand_hex, msg_hex, result_str, comment) = row + (index, point_G_hex, seckey_a_hex, point_B_hex, aux_rand_hex, msg_hex, result_str, comment) = row + print(seckey_a_hex) + G = GE() if point_G_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_G_hex)) a = int.from_bytes(bytes.fromhex(seckey_a_hex), 'big') B = GE() if point_B_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_B_hex)) aux_rand = bytes.fromhex(aux_rand_hex) msg = bytes.fromhex(msg_hex) print('Test vector', ('#' + index).rjust(3, ' ') + ':' + f' ({comment})') expected_result = None if result_str == 'INVALID' else bytes.fromhex(result_str) - actual_result = dleq_generate_proof(a, B, aux_rand, m=msg) + actual_result = dleq_generate_proof(a, B, aux_rand, G=G, m=msg) if expected_result == actual_result: print(' * Passed proof generation test.') else: @@ -47,7 +49,8 @@ reader = csv.reader(csvfile) reader.__next__() for row in reader: - (index, point_A_hex, point_B_hex, point_C_hex, proof_hex, msg_hex, result_success, comment) = row + (index, point_G_hex, point_A_hex, point_B_hex, point_C_hex, proof_hex, msg_hex, result_success, comment) = row + G = GE() if point_G_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_G_hex)) A = GE() if point_A_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_A_hex)) B = GE() if point_B_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_B_hex)) C = GE() if point_C_hex == 'INFINITY' else GE.from_bytes(bytes.fromhex(point_C_hex)) @@ -55,7 +58,7 @@ msg = bytes.fromhex(msg_hex) print('Test vector', ('#' + index).rjust(3, ' ') + ':' + f' ({comment})') expected_result = result_success == 'TRUE' - actual_result = dleq_verify_proof(A, B, C, proof, m=msg) + actual_result = dleq_verify_proof(A, B, C, proof, G=G, m=msg) if expected_result == actual_result: print(' * Passed proof verification test.') else: diff --git a/bip-DLEQ/secp256k1.py b/bip-0374/secp256k1.py similarity index 100% rename from bip-DLEQ/secp256k1.py rename to bip-0374/secp256k1.py