diff --git a/cashu/core/base.py b/cashu/core/base.py index 5d23ab55..e04f96c4 100644 --- a/cashu/core/base.py +++ b/cashu/core/base.py @@ -168,11 +168,16 @@ class DLEQWallet(BaseModel): Discrete Log Equality (DLEQ) Proof """ + # DLEQ proof of equality of a (mint private key) e: str s: str - r: str # blinding_factor, unknown to mint but sent from wallet to wallet for DLEQ proof - # B_: Union[str, None] = None # blinded message, sent to the mint by the wallet - # C_: Union[str, None] = None # blinded signature, received by the mint + # r: str # blinding_factor, unknown to mint but sent from wallet to wallet for DLEQ proof + B_: str # blinded message, sent to the mint by the wallet + C_: str # blinded signature, received by the mint + + # schnorr proof of knowledge of r (blinding factor of Alice) + f: str + t: str class Proof(BaseModel): diff --git a/cashu/core/crypto/b_dhke.py b/cashu/core/crypto/b_dhke.py index bfcffbcf..098b5ab5 100644 --- a/cashu/core/crypto/b_dhke.py +++ b/cashu/core/crypto/b_dhke.py @@ -97,12 +97,11 @@ def verify(a: PrivateKey, C: PublicKey, secret_msg: str) -> bool: return C == Y.mult(a) # type: ignore -def hash_e(R1: PublicKey, R2: PublicKey, K: PublicKey, C_: PublicKey) -> bytes: - _R1 = R1.serialize(compressed=False).hex() - _R2 = R2.serialize(compressed=False).hex() - _K = K.serialize(compressed=False).hex() - _C_ = C_.serialize(compressed=False).hex() - e_ = f"{_R1}{_R2}{_K}{_C_}" +def hash_e(*publickeys: PublicKey) -> bytes: + e_ = "" + for p in publickeys: + _p = p.serialize(compressed=False).hex() + e_ += str(_p) e = hashlib.sha256(e_.encode("utf-8")).digest() return e @@ -130,8 +129,13 @@ def step2_bob_dleq( return epk, spk -def alice_verify_dleq( - B_: PublicKey, C_: PublicKey, e: PrivateKey, s: PrivateKey, A: PublicKey +def verify_dleq( + *, + B_: PublicKey, + C_: PublicKey, + e: PrivateKey, + s: PrivateKey, + A: PublicKey, ): R1 = s.pubkey - A.mult(e) # type: ignore R2 = B_.mult(s) - C_.mult(e) # type: ignore @@ -140,6 +144,28 @@ def alice_verify_dleq( def carol_verify_dleq( + *, + B_: PublicKey, + C_: PublicKey, + e: PrivateKey, + s: PrivateKey, + A: PublicKey, + f: PrivateKey, + t: PrivateKey, + C: PublicKey, + secret_msg: str, +): + # verify dleq proof that mint signature is valid + assert verify_dleq(B_=B_, C_=C_, e=e, s=s, A=A) + # verify schnorr proof that Alice sent us valid C_ and B_ + assert carol_schnorr_r_verify( + A=A, B_=B_, secret_msg=secret_msg, C=C, C_=C_, f=f, t=t + ) + return True + + +def alice_verify_dleq( + *, secret_msg: str, r: PrivateKey, C: PublicKey, @@ -150,7 +176,54 @@ def carol_verify_dleq( Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) C_: PublicKey = C + A.mult(r) # type: ignore B_: PublicKey = Y + r.pubkey # type: ignore - return alice_verify_dleq(B_, C_, e, s, A) + return verify_dleq(B_=B_, C_=C_, e=e, s=s, A=A) + + +def alice_schnorr_r( + r: PrivateKey, + A: PublicKey, + B_: PublicKey, + C_: PublicKey, + C: PublicKey, + secret_msg: str, + k_bytes: bytes = b"", +) -> Tuple[PrivateKey, PrivateKey]: + if k_bytes: + # deterministic k for testing + k = PrivateKey(privkey=k_bytes, raw=True) + else: + # normally, we generate a random p + k = PrivateKey() + + K1 = k.pubkey # K1 = kG + assert K1 + K2 = A.mult(k) # K2 = kA # type: ignore + + Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) + f = hash_e(K1, K2, A, B_, C_, Y, C) + t = k.tweak_add(r.tweak_mul(f)) # t = p + fk + tpk = PrivateKey(t, raw=True) + fpk = PrivateKey(f, raw=True) + return fpk, tpk + + +def carol_schnorr_r_verify( + *, + A: PublicKey, + B_: PublicKey, + secret_msg: str, + C: PublicKey, + C_: PublicKey, + f: PrivateKey, + t: PrivateKey, +): + Y: PublicKey = hash_to_curve(secret_msg.encode("utf-8")) + K1 = t.pubkey - B_.mult(f) + Y.mult(f) # type: ignore + K2 = A.mult(t) - C_.mult(f) + C.mult(f) # type: ignore + + f_bytes = f.private_key + + return f_bytes == hash_e(K1, K2, A, B_, C_, Y, C) # Below is a test of a simple positive and negative case diff --git a/cashu/wallet/wallet.py b/cashu/wallet/wallet.py index ba93d3ea..7ae3acd9 100644 --- a/cashu/wallet/wallet.py +++ b/cashu/wallet/wallet.py @@ -142,33 +142,48 @@ async def _init_s(self): # ---------- DLEQ PROOFS ---------- - def verify_proofs_dleq(self, proofs: List[Proof]): + def alice_verify_proofs_dleq(self, proofs: List[Proof], rs: List[PrivateKey]): """Verifies DLEQ proofs in proofs.""" - for proof in proofs: + for r, proof in zip(rs, proofs): if not proof.dleq: logger.trace("No DLEQ proof in proof.") return - logger.trace("Verifying DLEQ proof.") + logger.trace("Alice: Verifying DLEQ proof.") assert self.keys.public_keys - # if not b_dhke.alice_verify_dleq( - # e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), - # s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), - # A=self.keys.public_keys[proof.amount], - # B_=PublicKey(bytes.fromhex(proof.B_), raw=True), - # C_=PublicKey(bytes.fromhex(proof.C_), raw=True), - # ): - # raise Exception("Alice: DLEQ proof invalid.") - if not b_dhke.carol_verify_dleq( + if not b_dhke.alice_verify_dleq( secret_msg=proof.secret, C=PublicKey(bytes.fromhex(proof.C), raw=True), - r=PrivateKey(bytes.fromhex(proof.dleq.r), raw=True), + r=r, e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), A=self.keys.public_keys[proof.amount], ): raise Exception("DLEQ proof invalid.") else: - logger.debug("DLEQ proof valid.") + logger.debug("Alice: DLEQ proof valid.") + + def carol_verify_proofs_dleq(self, proofs: List[Proof]): + """Verifies DLEQ proofs in proofs.""" + for proof in proofs: + if not proof.dleq: + logger.debug("No DLEQ proof in proof.") + return + logger.trace("Carol: Verifying DLEQ proof.") + assert self.keys.public_keys + if not b_dhke.carol_verify_dleq( + e=PrivateKey(bytes.fromhex(proof.dleq.e), raw=True), + s=PrivateKey(bytes.fromhex(proof.dleq.s), raw=True), + A=self.keys.public_keys[proof.amount], + B_=PublicKey(bytes.fromhex(proof.dleq.B_), raw=True), + C_=PublicKey(bytes.fromhex(proof.dleq.C_), raw=True), + C=PublicKey(bytes.fromhex(proof.C), raw=True), + f=PrivateKey(bytes.fromhex(proof.dleq.f), raw=True), + t=PrivateKey(bytes.fromhex(proof.dleq.t), raw=True), + secret_msg=proof.secret, + ): + raise Exception("Carol: DLEQ proof invalid.") + else: + logger.debug("Carol: DLEQ proof valid.") def _construct_proofs( self, @@ -213,18 +228,32 @@ def _construct_proofs( # if the mint returned a dleq proof, we add it to the proof if promise.dleq: + # create schnorr proof for r + f, t = b_dhke.alice_schnorr_r( + r=r, + A=self.public_keys[promise.amount], + B_=B_, + C_=C_, + C=C, + secret_msg=secret, + ) + proof.dleq = DLEQWallet( - e=promise.dleq.e, s=promise.dleq.s, r=r.serialize() + e=promise.dleq.e, + s=promise.dleq.s, + B_=B_.serialize().hex(), + C_=C_.serialize().hex(), + f=f.serialize(), + t=t.serialize(), ) proofs.append(proof) - logger.trace( f"Created proof: {proof}, r: {r.serialize()} out of promise {promise}" ) # DLEQ verify - self.verify_proofs_dleq(proofs) + self.alice_verify_proofs_dleq(proofs, rs) logger.trace(f"Constructed {len(proofs)} proofs.") return proofs @@ -1086,7 +1115,7 @@ async def redeem( """ # verify DLEQ of incoming proofs logger.debug("Verifying DLEQ of incoming proofs.") - self.verify_proofs_dleq(proofs) + self.carol_verify_proofs_dleq(proofs) logger.debug("DLEQ verified.") return await self.split(proofs, sum_proofs(proofs)) diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 59b94849..e6dd1a8c 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -1,12 +1,12 @@ from cashu.core.crypto.b_dhke import ( alice_verify_dleq, - carol_verify_dleq, hash_e, hash_to_curve, step1_alice, step2_bob, step2_bob_dleq, step3_alice, + verify_dleq, ) from cashu.core.crypto.secp import PrivateKey, PublicKey @@ -251,7 +251,7 @@ def test_dleq_alice_verify_dleq(): raw=True, ) - assert alice_verify_dleq(B_, C_, e, s, A) + assert verify_dleq(B_=B_, C_=C_, e=e, s=s, A=A) def test_dleq_alice_direct_verify_dleq(): @@ -274,7 +274,7 @@ def test_dleq_alice_direct_verify_dleq(): ), ) C_, e, s = step2_bob(B_, a) - assert alice_verify_dleq(B_, C_, e, s, A) + assert verify_dleq(B_=B_, C_=C_, e=e, s=s, A=A) def test_dleq_carol_varify_from_bob(): @@ -295,8 +295,8 @@ def test_dleq_carol_varify_from_bob(): ) B_, _ = step1_alice(secret_msg, r) C_, e, s = step2_bob(B_, a) - assert alice_verify_dleq(B_, C_, e, s, A) + assert verify_dleq(B_=B_, C_=C_, e=e, s=s, A=A) C = step3_alice(C_, r, A) # carol does not know B_ and C_, but she receives C and r from Alice - assert carol_verify_dleq(secret_msg=secret_msg, C=C, r=r, e=e, s=s, A=A) + assert alice_verify_dleq(secret_msg=secret_msg, C=C, r=r, e=e, s=s, A=A)