mirror of
https://github.com/aljazceru/nutshell.git
synced 2025-12-20 02:24:20 +01:00
[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:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
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.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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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",
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user