[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:
```bash
Version: 0.11.2
Version: 0.12.0
Debug: False
Cashu dir: /home/user/.cashu
Wallet: wallet

View File

@@ -3,9 +3,15 @@ import json
from sqlite3 import Row
from typing import Any, Dict, List, Optional, TypedDict, Union
from loguru import logger
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
# ------- PROOFS -------
@@ -241,6 +247,8 @@ class WalletKeyset:
if public_keys:
self.public_keys = 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:
@@ -267,7 +275,7 @@ class MintKeyset:
active=None,
seed: str = "",
derivation_path: str = "",
version: str = "",
version: str = "1",
):
self.derivation_path = derivation_path
self.id = id
@@ -282,16 +290,26 @@ class MintKeyset:
def generate_keys(self, seed):
"""Generates keys of a keyset from a seed."""
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.id = derive_keyset_id(self.public_keys) # type: ignore
def get_keybase(self):
assert self.id is not None
return {
k: KeyBase(id=self.id, amount=k, pubkey=v.serialize().hex())
for k, v in self.public_keys.items() # type: ignore
}
if backwards_compatibility_pre_0_12:
logger.warning(
f"WARNING: Using weak key derivation for keyset {self.id} (backwards compatibility < 0.12)"
)
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.
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 {
2
** i: PrivateKey(
@@ -33,7 +51,7 @@ def derive_keys(master_key: str, derivation_path: str = ""):
def derive_pubkey(master_key: str):
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,
).pubkey

View File

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

View File

@@ -116,7 +116,7 @@ class LedgerAPI:
"""Returns proofs of promise from promises. Wants secrets and blinding factors rs."""
proofs: List[Proof] = []
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 (
self.keyset_id == promise.id
), "our keyset id does not match promise id."
@@ -278,7 +278,7 @@ class LedgerAPI:
int(amt): PublicKey(bytes.fromhex(val), raw=True)
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
@async_set_requests

View File

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

View File

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

View File

@@ -106,7 +106,7 @@ def test_send(mint, cli_prefix):
@pytest.mark.asyncio
def test_receive_tokenv3(mint, cli_prefix):
runner = CliRunner()
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIkhYUC1SSEZ2Tk5Cb2ZZT1FzY1RURnciLCAiQyI6ICIwMjQxOTgwYzk0NjY1ODU0MDZhODg3ZWI1Y2JkN2Y1N2U4NTc0MzdiNzE0MTkxYmUwOTQyNTJmYjAxZWViNGQ3OTQifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICItcXI2cF80bDNVQWQwSzBib2JSekdRIiwgIkMiOiAiMDIyOTA5YWJhYjg0N2RlODA0NjdmYjhjOTdiMDJiYjFjOGUwNWE0ZTFlYzBiYzI1MDY0MDUzMjg3YTNhYjViZDYyIn1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIld6TEF2VW53SDlRaFYwQU1rMy1oYWciLCAiQyI6ICIwMmZlMzUxYjAyN2FlMGY1ZDkyN2U2ZjFjMTljMjNjNTc3NzRhZTI2M2UyOGExN2E2MTUxNjY1ZjU3NWNhNjMyNWMifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICJDamFTeTcyR2dVOGwzMGV6bE5zZnVBIiwgIkMiOiAiMDNjMzM0OTJlM2ZlNjI4NzFhMWEzMDhiNWUyYjVhZjBkNWI1Mjk5YzI0YmVkNDI2ZjQ1YzZmNDg5N2QzZjc4NGQ5In1dLCAibWludCI6ICJodHRwOi8vbG9jYWxob3N0OjMzMzcifV19"
result = runner.invoke(
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
# therefore, we need to know the mint keyset already and have the mint URL in the db
runner = CliRunner()
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIk1OQzRXM2k3NjNSMFYwdkFncEdJQ1EiLCAiQyI6ICIwMjg2YWIyNDZlMWViZTI5Yjk0ZTMzMDgzMjE0NDZhNTRkOGE3NWEwOTQ2MjU4YmYyMzM1ZmJhOTA5Y2ZjY2VhMWYifSwgeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI5OFM5MGRjRGtKb3Q1V1Z4QVJ3VWdRIiwgIkMiOiAiMDJlMTE0OGRkNzQ3OWJlNzIwMmI5OWVmZDIzNjllZTFhYTBhYTVhYzMyYjM1ODczMzk0YmNjYWU1MmFkZTYzYmUxIn1dfV19"
token = "cashuAeyJ0b2tlbiI6IFt7InByb29mcyI6IFt7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiAyLCAic2VjcmV0IjogIi1oM0ZXMFFoX1FYLW9ac1V2c0RuNlEiLCAiQyI6ICIwMzY5Mzc4MzdlYjg5ZWI4NjMyNWYwOWUyOTIxMWQxYTI4OTRlMzQ2YmM1YzQwZTZhMThlNTk5ZmVjNjEwOGRmMGIifSwgeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogOCwgInNlY3JldCI6ICI3d0VhNUgzZGhSRGRNZl94c1k3c3JnIiwgIkMiOiAiMDJiZmZkM2NlZDkxNjUyMzcxMDg2NjQxMzJiMjgxYjBhZjY1ZTNlZWVkNTY3MmFkZjM0Y2VhNzE5ODhhZWM1NWI1In1dfV19"
result = runner.invoke(
cli,
[
@@ -142,7 +142,7 @@ def test_receive_tokenv3_no_mint(mint, cli_prefix):
@pytest.mark.asyncio
def test_receive_tokenv2(mint, cli_prefix):
runner = CliRunner()
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIyOGo4bnlTTDFOZGQiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJ6dHRyMU9EdXBlZzA5Y1d2ckFVdWxRIiwgIkMiOiAiMDM3ZGQyNDYxZjFlOTg4Y2YxOWQyMmJhOTMxOTdlMmU2YmI5YTJmNjc5NDM4YTFiZjYwYmY0ZWJmZGJkNWUyYmM0In0sIHsiaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiekw5NDlTVzI4ZEpxaUMyOXl0bVZKQSIsICJDIjogIjAzOWIzOGIwM2QxY2NlNTU0NGZlYzM3YTM4ZGViZGZhMjUzMTc2ZTI3MWVlNDU3NjdkOTBkMWYwNWNmZGNhYzE2ZCJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjI4ajhueVNMMU5kZCJdfV19"
token = "eyJwcm9vZnMiOiBbeyJpZCI6ICIxY0NOSUFaMlgvdzEiLCAiYW1vdW50IjogMiwgInNlY3JldCI6ICJhUmREbzlFdW9yZUVfOW90enRNVVpnIiwgIkMiOiAiMDNhMzY5ZmUyN2IxYmVmOTg4MzA3NDQyN2RjMzc1NmU0NThlMmMwYjQ1NWMwYmVmZGM4ZjVmNTA3YmM5MGQxNmU3In0sIHsiaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDgsICJzZWNyZXQiOiAiTEZQbFp6Ui1MWHFfYXFDMGhUeDQyZyIsICJDIjogIjAzNGNiYzQxYWY0ODIxMGFmNjVmYjVjOWIzOTNkMjhmMmQ5ZDZhOWE5MzI2YmI3MzQ2YzVkZmRmMTU5MDk1MzI2YyJ9XSwgIm1pbnRzIjogW3sidXJsIjogImh0dHA6Ly9sb2NhbGhvc3Q6MzMzNyIsICJpZHMiOiBbIjFjQ05JQVoyWC93MSJdfV19"
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],
@@ -155,7 +155,7 @@ def test_receive_tokenv2(mint, cli_prefix):
@pytest.mark.asyncio
def test_receive_tokenv1(mint, cli_prefix):
runner = CliRunner()
token = "W3siaWQiOiAiMjhqOG55U0wxTmRkIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiZDRJQjY3LU1iOGpDS242clZoREMyUSIsICJDIjogIjAzM2E5M2NiNjhjZWZhZjFmNTJkN2NhZTMzNWVhN2ExYmQ4MDFiZTVmZDE5OGI5MWQzM2FmMDJlNjk3NWI0NzdmMiJ9LCB7ImlkIjogIjI4ajhueVNMMU5kZCIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInAtc0QzeW5PSE5ISmdjVlRUZDVVYVEiLCAiQyI6ICIwMzg5NGMwMzdiNGMyMWRmNmNiNzExMmJjZjE0OGMwOTlmYTM2ZDk4MTZhYjcwN2VmMTM0N2ZmODEyZjQ4N2MzNmEifV0="
token = "W3siaWQiOiAiMWNDTklBWjJYL3cxIiwgImFtb3VudCI6IDIsICJzZWNyZXQiOiAiRnVsc2dzMktQV1FMcUlLX200SzgwQSIsICJDIjogIjAzNTc4OThlYzlhMjIxN2VhYWIxZDc3YmM1Mzc2OTUwMjJlMjU2YTljMmMwNjc0ZDJlM2FiM2JiNGI0ZDMzMWZiMSJ9LCB7ImlkIjogIjFjQ05JQVoyWC93MSIsICJhbW91bnQiOiA4LCAic2VjcmV0IjogInJlRDBDazVNS2xBTUQ0dWk2OEtfbEEiLCAiQyI6ICIwMjNkODNkNDE0MDU0NWQ1NTg4NjUyMzU5YjJhMjFhODljODY1ZGIzMzAyZTkzMTZkYTM5NjA0YTA2ZDYwYWQzOGYifV0="
result = runner.invoke(
cli,
[*cli_prefix, "receive", token],

View File

@@ -52,11 +52,37 @@ async def 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
async def test_keysets(ledger: Ledger):
assert len(ledger.keysets.keysets)
assert len(ledger.keysets.get_ids())
assert ledger.keyset.id == "XQM1wwtQbOXE"
assert ledger.keyset.id == "1cCNIAZ2X/w1"
@pytest.mark.asyncio
@@ -79,7 +105,7 @@ async def test_mint(ledger: Ledger):
assert promises[0].amount == 8
assert (
promises[0].C_
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
)
@@ -107,5 +133,5 @@ async def test_generate_promises(ledger: Ledger):
promises = await ledger._generate_promises(blinded_messages_mock)
assert (
promises[0].C_
== "032dfadd74bb3abba8170ecbae5401507e384eafd312defda94148fa37314c0ef0"
== "037074c4f53e326ee14ed67125f387d160e0e729351471b69ad41f7d5d21071e15"
)