[Mint] Fix key derivation (#187)

* fix private key derivation

* add backwards compatilibity for old keysets

* bump version

* test pubkeys and private keys

* make format

* reset keys for tests

* fix cli tests
This commit is contained in:
calle
2023-05-01 22:43:51 +02:00
committed by GitHub
parent d201b89df2
commit 321fc733c8
9 changed files with 86 additions and 24 deletions

View File

@@ -115,7 +115,7 @@ cashu info
Returns: Returns:
```bash ```bash
Version: 0.11.2 Version: 0.12.0
Debug: False Debug: False
Cashu dir: /home/user/.cashu Cashu dir: /home/user/.cashu
Wallet: wallet Wallet: wallet

View File

@@ -3,9 +3,15 @@ import json
from sqlite3 import Row from sqlite3 import Row
from typing import Any, Dict, List, Optional, TypedDict, Union from typing import Any, Dict, List, Optional, TypedDict, Union
from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from ..core.crypto import derive_keys, derive_keyset_id, derive_pubkeys from ..core.crypto import (
derive_keys,
derive_keys_backwards_compatible_0_11_insecure,
derive_keyset_id,
derive_pubkeys,
)
from ..core.secp import PrivateKey, PublicKey from ..core.secp import PrivateKey, PublicKey
# ------- PROOFS ------- # ------- PROOFS -------
@@ -241,6 +247,8 @@ class WalletKeyset:
if public_keys: if public_keys:
self.public_keys = public_keys self.public_keys = public_keys
self.id = derive_keyset_id(self.public_keys) self.id = derive_keyset_id(self.public_keys)
if id:
assert id == self.id, "id must match derived id from public keys"
class MintKeyset: class MintKeyset:
@@ -267,7 +275,7 @@ class MintKeyset:
active=None, active=None,
seed: str = "", seed: str = "",
derivation_path: str = "", derivation_path: str = "",
version: str = "", version: str = "1",
): ):
self.derivation_path = derivation_path self.derivation_path = derivation_path
self.id = id self.id = id
@@ -282,16 +290,26 @@ class MintKeyset:
def generate_keys(self, seed): def generate_keys(self, seed):
"""Generates keys of a keyset from a seed.""" """Generates keys of a keyset from a seed."""
self.private_keys = derive_keys(seed, self.derivation_path) backwards_compatibility_pre_0_12 = False
if (
self.version
and len(self.version.split(".")) > 1
and int(self.version.split(".")[0]) == 0
and int(self.version.split(".")[1]) <= 11
):
backwards_compatibility_pre_0_12 = True
# WARNING: Broken key derivation for backwards compatibility with < 0.12
self.private_keys = derive_keys_backwards_compatible_0_11_insecure(
seed, self.derivation_path
)
else:
self.private_keys = derive_keys(seed, self.derivation_path)
self.public_keys = derive_pubkeys(self.private_keys) # type: ignore self.public_keys = derive_pubkeys(self.private_keys) # type: ignore
self.id = derive_keyset_id(self.public_keys) # type: ignore self.id = derive_keyset_id(self.public_keys) # type: ignore
if backwards_compatibility_pre_0_12:
def get_keybase(self): logger.warning(
assert self.id is not None f"WARNING: Using weak key derivation for keyset {self.id} (backwards compatibility < 0.12)"
return { )
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
for k, v in self.public_keys.items() # type: ignore
}
class MintKeysets: class MintKeysets:

View File

@@ -19,6 +19,24 @@ def derive_keys(master_key: str, derivation_path: str = ""):
Deterministic derivation of keys for 2^n values. Deterministic derivation of keys for 2^n values.
TODO: Implement BIP32. TODO: Implement BIP32.
""" """
return {
2
** i: PrivateKey(
hashlib.sha256(
(master_key + derivation_path + str(i)).encode("utf-8")
).digest()[:32],
raw=True,
)
for i in range(settings.max_order)
}
def derive_keys_backwards_compatible_0_11_insecure(
master_key: str, derivation_path: str = ""
):
"""
WARNING: Broken key derivation for backwards compatibility with 0.11.
"""
return { return {
2 2
** i: PrivateKey( ** i: PrivateKey(
@@ -33,7 +51,7 @@ def derive_keys(master_key: str, derivation_path: str = ""):
def derive_pubkey(master_key: str): def derive_pubkey(master_key: str):
return PrivateKey( return PrivateKey(
hashlib.sha256((master_key).encode("utf-8")).hexdigest().encode("utf-8")[:32], hashlib.sha256((master_key).encode("utf-8")).digest()[:32],
raw=True, raw=True,
).pubkey ).pubkey

View File

@@ -8,7 +8,7 @@ from pydantic import BaseSettings, Extra, Field, validator
env = Env() env = Env()
VERSION = "0.11.2" VERSION = "0.12.0"
def find_env_file(): def find_env_file():

View File

@@ -116,7 +116,7 @@ class LedgerAPI:
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs.""" """Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
proofs: List[Proof] = [] proofs: List[Proof] = []
for promise, secret, r in zip(promises, secrets, rs): for promise, secret, r in zip(promises, secrets, rs):
logger.trace(f"Creating proof with keyset {self.keyset_id} =+ {promise.id}") logger.trace(f"Creating proof with keyset {self.keyset_id} = {promise.id}")
assert ( assert (
self.keyset_id == promise.id self.keyset_id == promise.id
), "our keyset id does not match promise id." ), "our keyset id does not match promise id."
@@ -278,7 +278,7 @@ class LedgerAPI:
int(amt): PublicKey(bytes.fromhex(val), raw=True) int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in keys.items() for amt, val in keys.items()
} }
keyset = WalletKeyset(public_keys=keyset_keys, mint_url=url) keyset = WalletKeyset(id=keyset_id, public_keys=keyset_keys, mint_url=url)
return keyset return keyset
@async_set_requests @async_set_requests

View File

@@ -13,7 +13,7 @@ entry_points = {"console_scripts": ["cashu = cashu.wallet.cli.cli:cli"]}
setuptools.setup( setuptools.setup(
name="cashu", name="cashu",
version="0.11.2", version="0.12.0",
description="Ecash wallet and mint for Bitcoin Lightning", description="Ecash wallet and mint for Bitcoin Lightning",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",

View File

@@ -30,8 +30,8 @@ class UvicornServer(multiprocessing.Process):
settings.mint_lightning_backend = "FakeWallet" settings.mint_lightning_backend = "FakeWallet"
settings.mint_listen_port = 3337 settings.mint_listen_port = 3337
settings.mint_database = "data/test_mint" settings.mint_database = "data/test_mint"
settings.mint_private_key = "TEST_PRIVATE_KEY"
settings.mint_derivation_path = "0/0/0/0" settings.mint_derivation_path = "0/0/0/0"
settings.mint_private_key = "privatekeyofthemint"
dirpath = Path(settings.mint_database) dirpath = Path(settings.mint_database)
if dirpath.exists() and dirpath.is_dir(): if dirpath.exists() and dirpath.is_dir():

View File

@@ -106,7 +106,7 @@ def test_send(mint, cli_prefix):
@pytest.mark.asyncio @pytest.mark.asyncio
def test_receive_tokenv3(mint, cli_prefix): def test_receive_tokenv3(mint, cli_prefix):
runner = CliRunner() runner = CliRunner()
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIkhYUC1SSEZ2Tk5Cb2ZZT1FzY1RURnciLCAiQyI6ICIwMjQxOTgwYzk0NjY1ODU0MDZhODg3ZWI1Y2JkN2Y1N2U4NTc0MzdiNzE0MTkxYmUwOTQyNTJmYjAxZWViNGQ3OTQifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICItcXI2cF80bDNVQWQwSzBib2JSekdRIiwgIkMiOiAiMDIyOTA5YWJhYjg0N2RlODA0NjdmYjhjOTdiMDJiYjFjOGUwNWE0ZTFlYzBiYzI1MDY0MDUzMjg3YTNhYjViZDYyIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19" token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLCAiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2ZjQ1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
result = runner.invoke( result = runner.invoke(
cli, cli,
[ [
@@ -125,7 +125,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
# this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token # this test works only if the previous test succeeds because we simulate the case where the mint URL is not in the token
# therefore, we need to know the mint keyset already and have the mint URL in the db # therefore, we need to know the mint keyset already and have the mint URL in the db
runner = CliRunner() runner = CliRunner()
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIk1OQzRXM2k3NjNSMFYwdkFncEdJQ1EiLCAiQyI6ICIwMjg2YWIyNDZlMWViZTI5Yjk0ZTMzMDgzMjE0NDZhNTRkOGE3NWEwOTQ2MjU4YmYyMzM1ZmJhOTA5Y2ZjY2VhMWYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI5OFM5MGRjRGtKb3Q1V1Z4QVJ3VWdRIiwgIkMiOiAiMDJlMTE0OGRkNzQ3OWJlNzIwMmI5OWVmZDIzNjllZTFhYTBhYTVhYzMyYjM1ODczMzk0YmNjYWU1MmFkZTYzYmUxIn1dfV19" token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLCAiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZjM0Y2VhNzE5ODhhZWM1NWI1In1dfV19"
result = runner.invoke( result = runner.invoke(
cli, cli,
[ [
@@ -142,7 +142,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
@pytest.mark.asyncio @pytest.mark.asyncio
def test_receive_tokenv2(mint, cli_prefix): def test_receive_tokenv2(mint, cli_prefix):
runner = CliRunner() runner = CliRunner()
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ6dHRyMU9EdXBlZzA5Y1d2ckFVdWxRIiwgIkMiOiAiMDM3ZGQyNDYxZjFlOTg4Y2YxOWQyMmJhOTMxOTdlMmU2YmI5YTJmNjc5NDM4YTFiZjYwYmY0ZWJmZGJkNWUyYmM0In0sIHsiaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiekw5NDlTVzI4ZEpxaUMyOXl0bVZKQSIsICJDIjogIjAzOWIzOGIwM2QxY2NlNTU0NGZlYzM3YTM4ZGViZGZhMjUzMTc2ZTI3MWVlNDU3NjdkOTBkMWYwNWNmZGNhYzE2ZCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjI4ajhueVNMMU5kZCJdfV19" token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUyN2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19"
result = runner.invoke( result = runner.invoke(
cli, cli,
[*cli_prefix, "receive", token], [*cli_prefix, "receive", token],
@@ -155,7 +155,7 @@ def test_receive_tokenv2(mint, cli_prefix):
@pytest.mark.asyncio @pytest.mark.asyncio
def test_receive_tokenv1(mint, cli_prefix): def test_receive_tokenv1(mint, cli_prefix):
runner = CliRunner() runner = CliRunner()
token = "W3siaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiZDRJQjY3LU1iOGpDS242clZoREMyUSIsICJDIjogIjAzM2E5M2NiNjhjZWZhZjFmNTJkN2NhZTMzNWVhN2ExYmQ4MDFiZTVmZDE5OGI5MWQzM2FmMDJlNjk3NWI0NzdmMiJ9LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInAtc0QzeW5PSE5ISmdjVlRUZDVVYVEiLCAiQyI6ICIwMzg5NGMwMzdiNGMyMWRmNmNiNzExMmJjZjE0OGMwOTlmYTM2ZDk4MTZhYjcwN2VmMTM0N2ZmODEyZjQ4N2MzNmEifV0=" token = "W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIxZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBDazVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0="
result = runner.invoke( result = runner.invoke(
cli, cli,
[*cli_prefix, "receive", token], [*cli_prefix, "receive", token],

View File

@@ -52,11 +52,37 @@ async def ledger():
yield ledger yield ledger
@pytest.mark.asyncio
async def test_pubkeys(ledger: Ledger):
assert ledger.keyset.public_keys
assert (
ledger.keyset.public_keys[1].serialize().hex()
== "03190ebc0c3e2726a5349904f572a2853ea021b0128b269b8b6906501d262edaa8"
)
assert (
ledger.keyset.public_keys[2 ** (settings.max_order - 1)].serialize().hex()
== "032dc008b88b85fdc2301a499bfaaef774c191a6307d8c9434838fc2eaa2e48d51"
)
@pytest.mark.asyncio
async def test_privatekeys(ledger: Ledger):
assert ledger.keyset.private_keys
assert (
ledger.keyset.private_keys[1].serialize()
== "67de62e1bf8b5ccf88dbad6768b7d13fa0f41433b0a89caf915039505f2e00a7"
)
assert (
ledger.keyset.private_keys[2 ** (settings.max_order - 1)].serialize()
== "3b1340c703b02028a11025302d2d9e68d2a6dd721ab1a2770f0942d15eacb8d0"
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_keysets(ledger: Ledger): async def test_keysets(ledger: Ledger):
assert len(ledger.keysets.keysets) assert len(ledger.keysets.keysets)
assert len(ledger.keysets.get_ids()) assert len(ledger.keysets.get_ids())
assert ledger.keyset.id == "XQM1wwtQbOXE" assert ledger.keyset.id == "1cCNIAZ2X/w1"
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -79,7 +105,7 @@ async def test_mint(ledger: Ledger):
assert promises[0].amount == 8 assert promises[0].amount == 8
assert ( assert (
promises[0].C_ promises[0].C_
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0" == "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
) )
@@ -107,5 +133,5 @@ async def test_generate_promises(ledger: Ledger):
promises = await ledger._generate_promises(blinded_messages_mock) promises = await ledger._generate_promises(blinded_messages_mock)
assert ( assert (
promises[0].C_ promises[0].C_
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0" == "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
) )