Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions cashu/core/crypto/b_dhke.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"""

import hashlib
import hmac
from typing import Optional, Tuple

from .secp import PrivateKey, PublicKey
Expand Down Expand Up @@ -125,20 +126,47 @@ def hash_e(*publickeys: PublicKey) -> bytes:
return e


SECP256K1_N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141


def deterministic_dleq_nonce(
a: PrivateKey, B_: PublicKey, C_: PublicKey
) -> PrivateKey:
"""Derive deterministic DLEQ nonce r according to NUT-12."""
a_bytes = bytes.fromhex(a.to_hex())
A = a.public_key
assert A
A_bytes = A.format(compressed=False)
B_bytes = B_.format(compressed=False)
C_bytes = C_.format(compressed=False)

ctr = 0
while ctr < 256:
data = b"Cashu_DLEQ_R_v1" + A_bytes + B_bytes + C_bytes + bytes([ctr])
r_digest = hmac.new(a_bytes, data, hashlib.sha256).digest()
r_val = int.from_bytes(r_digest, "big")
if 0 < r_val < SECP256K1_N:
return PrivateKey(r_digest)
ctr += 1
raise ValueError("Could not derive deterministic nonce after 256 iterations")


def step2_bob_dleq(
B_: PublicKey, a: PrivateKey, p_bytes: bytes = b""
) -> Tuple[PrivateKey, PrivateKey]:
# Compute C_ once at the start
C_: PublicKey = B_ * a # type: ignore

if p_bytes:
# deterministic p for testing
p = PrivateKey(p_bytes)
else:
# normally, we generate a random p
p = PrivateKey()
# derive deterministic nonce p using NUT-12 spec
p = deterministic_dleq_nonce(a, B_, C_)

R1 = p.public_key # R1 = pG
assert R1
R2: PublicKey = B_ * p # type: ignore
C_: PublicKey = B_ * a # type: ignore
A = a.public_key
assert A
e = hash_e(R1, R2, A, C_) # e = hash(R1, R2, A, C_)
Expand Down
32 changes: 30 additions & 2 deletions cashu/mint/ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,21 @@ async def get_melt_quote(self, quote_id: str, rollback_unknown=False) -> MeltQuo
if not melt_quote:
raise Exception("quote not found")

# Reconstruct missing DLEQ proofs on-the-fly for melt change signatures
if melt_quote.change:
melt_outputs = await self.crud.get_blinded_messages_melt_id(
melt_id=quote_id, db=self.db
)
outputs_by_amount: Dict[int, List[str]] = {}
for out in melt_outputs:
outputs_by_amount.setdefault(out.amount, []).append(out.B_)

for sig in melt_quote.change:
b_list = outputs_by_amount.get(sig.amount)
if b_list:
B_ = PublicKey(bytes.fromhex(b_list.pop(0)))
self._reconstruct_dleq_proof(sig, B_)

unit, method = self._verify_and_get_unit_method(
melt_quote.unit, melt_quote.method
)
Expand Down Expand Up @@ -1332,6 +1347,8 @@ async def restore(
b_=output.B_, db=self.db, conn=conn
)
if promise is not None:
B_ = PublicKey(bytes.fromhex(output.B_))
self._reconstruct_dleq_proof(promise, B_)
signatures.append(promise)
return_outputs.append(output)
logger.trace(f"promise found: {promise}")
Expand Down Expand Up @@ -1427,8 +1444,8 @@ async def _sign_blinded_messages(
amount=amount,
b_=B_.format().hex(),
c_=C_.format().hex(),
e=e.to_hex(),
s=s.to_hex(),
e="",
s="",
db=self.db,
conn=conn,
)
Expand All @@ -1447,3 +1464,14 @@ async def _sign_blinded_messages(
)

return signatures

def _reconstruct_dleq_proof(self, sig: BlindedSignature, B_: PublicKey) -> None:
"""Derive and populate the DLEQ proof for a signature on-the-fly (NUT-12)."""
if sig.dleq is not None and sig.dleq.e and sig.dleq.s:
return
if sig.id not in self.keysets:
return
keyset = self.keysets[sig.id]
private_key = keyset.private_keys[sig.amount]
e, s = b_dhke.step2_bob_dleq(B_, private_key)
sig.dleq = DLEQ(e=e.to_hex(), s=s.to_hex())
10 changes: 6 additions & 4 deletions cashu/mint/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ async def m007_proofs_and_promises_store_id(db: Database):
async def m008_promises_dleq(db: Database):
"""
Add columns for DLEQ proof to promises table.
DEPRECATED: Since deterministic DLEQ nonces (NUT-12), these columns are unused
for new promises and will be dropped in a future migration.
"""
async with db.connect() as conn:
await conn.execute(
Expand Down Expand Up @@ -696,8 +698,8 @@ async def m017_foreign_keys_proof_tables(db: Database):
id TEXT,
b_ TEXT NOT NULL,
c_ TEXT NOT NULL,
dleq_e TEXT,
dleq_s TEXT,
dleq_e TEXT, -- DEPRECATED (NUT-12)
dleq_s TEXT, -- DEPRECATED (NUT-12)
created TIMESTAMP,
mint_quote TEXT,
swap_id TEXT,
Expand Down Expand Up @@ -1081,8 +1083,8 @@ async def recreate_promises_table(db: Database, conn: Connection):
id TEXT,
b_ TEXT NOT NULL,
c_ TEXT,
dleq_e TEXT,
dleq_s TEXT,
dleq_e TEXT, -- DEPRECATED (NUT-12)
dleq_s TEXT, -- DEPRECATED (NUT-12)
created TIMESTAMP,
signed_at TIMESTAMP,
mint_quote TEXT,
Expand Down
23 changes: 23 additions & 0 deletions tests/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,29 @@ def test_dleq_step2_bob_dleq():
)


def test_dleq_deterministic_nonce_vector():
# Test vector from NUT-12 deterministic nonce derivation spec
a = PrivateKey(
bytes.fromhex(
"0000000000000000000000000000000000000000000000000000000000000002"
)
)
B_ = PublicKey(
bytes.fromhex(
"02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2"
)
)
e, s = step2_bob_dleq(B_, a)
assert (
e.to_hex()
== "2a16ffee280aff3c429045607f9b8e0bf8b35910c44c1b20b9dfaf01b263d7b3"
)
assert (
s.to_hex()
== "9df27731238334718d120d4f74611a7c668233f988e687ac3fb188f0a34a2dab"
)


def test_dleq_alice_verify_dleq():
# e from test_step2_bob_dleq for a=0x1
e = PrivateKey(
Expand Down
Loading