Merge pull request #9 from callebtc/dep/libsecp256k1

Dep/libsecp256k1
This commit is contained in:
calle
2022-09-17 11:21:39 +03:00
committed by GitHub
12 changed files with 154 additions and 175 deletions

View File

@@ -1,40 +1,32 @@
# Don't trust me with cryptography.
"""
Implementation of https://gist.github.com/RubenSomsen/be7a4760dd4596d06963d67baf140406
Alice:
A = a*G
return A
Bob:
Y = hash_to_curve(secret_message)
r = random blinding factor
B'= Y + r*G
return B'
Alice:
C' = a*B'
(= a*Y + a*r*G)
return C'
Bob:
C = C' - r*A
(= C' - a*r*G)
(= a*Y)
return C, secret_message
Alice:
Y = hash_to_curve(secret_message)
C == a*Y
If true, C must have originated from Alice
"""
import hashlib
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
G = secp256k1.G
from secp256k1 import PrivateKey, PublicKey
def hash_to_curve(secret_msg):
@@ -43,13 +35,15 @@ def hash_to_curve(secret_msg):
point = None
msg = secret_msg
while point is None:
x_coord = int(hashlib.sha256(msg).hexdigest().encode("utf-8"), 16)
y_coord = secp256k1.compute_y(x_coord)
_hash = hashlib.sha256(msg).hexdigest().encode("utf-8")
try:
# Fails if the point is not on the curve
point = Point(x_coord, y_coord, secp256k1)
# We construct compressed pub which has x coordinate encoded with even y
_hash = list(_hash[:33]) # take the 33 bytes and get a list of bytes
_hash[0] = 0x02 # set first byte to represent even y coord
_hash = bytes(_hash)
point = PublicKey(_hash, raw=True)
except:
msg = str(x_coord).encode("utf-8")
msg = _hash
return point
@@ -57,35 +51,42 @@ def hash_to_curve(secret_msg):
def step1_bob(secret_msg):
secret_msg = secret_msg.encode("utf-8")
Y = hash_to_curve(secret_msg)
r, _ = gen_keypair(secp256k1)
B_ = Y + r * G
r = PrivateKey()
B_ = Y + r.pubkey
return B_, r
def step2_alice(B_, a):
C_ = a * B_
C_ = B_.mult(a)
return C_
def step3_bob(C_, r, A):
C = C_ - r * A
C = C_ - A.mult(r)
return C
def verify(a, C, secret_msg):
Y = hash_to_curve(secret_msg.encode("utf-8"))
return C == a * Y
return C == Y.mult(a)
### Below is a test of a simple positive and negative case
# # Alice private key
# a, A = gen_keypair(secp256k1)
# # Alice's keys
# a = PrivateKey()
# A = a.pubkey
# secret_msg = "test"
# B_, r = step1_bob(secret_msg)
# C_ = step2_alice(B_, a)
# C = step3_bob(C_, r, A)
# print("C:{}, secret_msg:{}".format(C, secret_msg))
# assert verify(a, C, secret_msg)
# assert verify(a, C + 1*G, secret_msg) == False # adding 1*G shouldn't pass
# assert verify(a, C + C, secret_msg) == False # adding C twice shouldn't pass
# assert verify(a, A, secret_msg) == False # A shouldn't pass
# # Test operations
# b = PrivateKey()
# B = b.pubkey
# assert -A -A + A == -A # neg
# assert B.mult(a) == A.mult(b) # a*B = A*b

View File

@@ -4,16 +4,9 @@ from typing import List
from pydantic import BaseModel
class BasePoint(BaseModel):
"""Named BasePoint because it conflicts with ecc.curve.Point"""
x: int
y: int
class Proof(BaseModel):
amount: int
C: BasePoint
C: str
secret: str
reserved: bool = False # whether this proof is reserved for sending
@@ -21,22 +14,16 @@ class Proof(BaseModel):
def from_row(cls, row: Row):
return cls(
amount=row[0],
C=dict(
x=int(row[1]),
y=int(row[2]),
),
secret=row[3],
reserved=row[4] or False,
C=row[1],
secret=row[2],
reserved=row[3] or False,
)
@classmethod
def from_dict(cls, d: dict):
return cls(
amount=d["amount"],
C=dict(
x=int(d["C"]["x"]),
y=int(d["C"]["y"]),
),
C=d["C"],
secret=d["secret"],
reserved=d["reserved"] or False,
)
@@ -72,21 +59,18 @@ class Invoice(BaseModel):
class BlindedMessage(BaseModel):
amount: int
B_: BasePoint
B_: str
class BlindedSignature(BaseModel):
amount: int
C_: BasePoint
C_: str
@classmethod
def from_dict(cls, d: dict):
return cls(
amount=d["amount"],
C_=dict(
x=int(d["C_"]["x"]),
y=int(d["C_"]["y"]),
),
C_=d["C_"],
)

52
core/secp.py Normal file
View File

@@ -0,0 +1,52 @@
from secp256k1 import PrivateKey, PublicKey
# We extend the public key to define some operations on points
# Picked from https://github.com/WTRMQDev/secp256k1-zkp-py/blob/master/secp256k1_zkp/__init__.py
class PublicKeyExt(PublicKey):
def __add__(self, pubkey2):
if isinstance(pubkey2, PublicKey):
new_pub = PublicKey()
new_pub.combine([self.public_key, pubkey2.public_key])
return new_pub
else:
raise TypeError("Cant add pubkey and %s" % pubkey2.__class__)
def __neg__(self):
serialized = self.serialize()
first_byte, remainder = serialized[:1], serialized[1:]
# flip odd/even byte
first_byte = {b"\x03": b"\x02", b"\x02": b"\x03"}[first_byte]
return PublicKey(first_byte + remainder, raw=True)
def __sub__(self, pubkey2):
if isinstance(pubkey2, PublicKey):
return self + (-pubkey2)
else:
raise TypeError("Can't add pubkey and %s" % pubkey2.__class__)
def mult(self, privkey):
if isinstance(privkey, PrivateKey):
return self.tweak_mul(privkey.private_key)
else:
raise TypeError("Can't multiply with non privatekey")
def __eq__(self, pubkey2):
if isinstance(pubkey2, PublicKey):
seq1 = self.to_data()
seq2 = pubkey2.to_data()
return seq1 == seq2
else:
raise TypeError("Can't compare pubkey and %s" % pubkey2.__class__)
def to_data(self):
return [self.public_key.data[i] for i in range(64)]
# Horrible monkeypatching
PublicKey.__add__ = PublicKeyExt.__add__
PublicKey.__neg__ = PublicKeyExt.__neg__
PublicKey.__sub__ = PublicKeyExt.__sub__
PublicKey.mult = PublicKeyExt.mult
PublicKey.__eq__ = PublicKeyExt.__eq__
PublicKey.to_data = PublicKeyExt.to_data

View File

@@ -5,10 +5,11 @@ from typing import Union
import click
import uvicorn
from ecc.curve import Point, secp256k1
from fastapi import FastAPI
from loguru import logger
from secp256k1 import PublicKey
import core.settings as settings
from core.base import MintPayloads, SplitPayload, MeltPayload, CheckPayload
from core.settings import MINT_PRIVATE_KEY, MINT_SERVER_HOST, MINT_SERVER_PORT
@@ -124,12 +125,8 @@ async def mint(payloads: MintPayloads, payment_hash: Union[str, None] = None):
amounts = []
B_s = []
for payload in payloads.blinded_messages:
v = payload.dict()
amounts.append(v["amount"])
x = int(v["B_"]["x"])
y = int(v["B_"]["y"])
B_ = Point(x, y, secp256k1)
B_s.append(B_)
amounts.append(payload.amount)
B_s.append(PublicKey(bytes.fromhex(payload.B_), raw=True))
try:
promises = await ledger.mint(B_s, amounts, payment_hash=payment_hash)
return promises

View File

@@ -7,10 +7,8 @@ from core.db import Connection, Database
async def store_promise(
amount: int,
B_x: str,
B_y: str,
C_x: str,
C_y: str,
B_: str,
C_: str,
db: Database,
conn: Optional[Connection] = None,
):
@@ -18,15 +16,13 @@ async def store_promise(
await (conn or db).execute(
"""
INSERT INTO promises
(amount, B_x, B_y, C_x, C_y)
VALUES (?, ?, ?, ?, ?)
(amount, B_b, C_b)
VALUES (?, ?, ?)
""",
(
amount,
str(B_x),
str(B_y),
str(C_x),
str(C_y),
str(B_),
str(C_),
),
)
@@ -54,13 +50,12 @@ async def invalidate_proof(
await (conn or db).execute(
"""
INSERT INTO proofs_used
(amount, C_x, C_y, secret)
VALUES (?, ?, ?, ?)
(amount, C, secret)
VALUES (?, ?, ?)
""",
(
proof.amount,
str(proof.C.x),
str(proof.C.y),
str(proof.C),
str(proof.secret),
),
)

View File

@@ -3,12 +3,10 @@ Implementation of https://gist.github.com/phyro/935badc682057f418842c72961cf096c
"""
import hashlib
import math
from ecc.curve import Point, secp256k1
from ecc.key import gen_keypair
from core.secp import PrivateKey, PublicKey
from typing import List
from core.base import Proof, BlindedMessage, BlindedSignature, BasePoint
from typing import List, Set
from core.base import Proof, BlindedMessage, BlindedSignature
import core.b_dhke as b_dhke
from core.base import Invoice
@@ -30,49 +28,49 @@ from mint.crud import (
class Ledger:
def __init__(self, secret_key: str, db: str):
self.proofs_used = set()
self.proofs_used: Set[str] = set()
self.master_key = secret_key
self.keys = self._derive_keys(self.master_key)
self.pub_keys = self._derive_pubkeys(self.keys)
self.db = Database("mint", db)
self.master_key: str = secret_key
self.keys: List[PrivateKey] = self._derive_keys(self.master_key)
self.pub_keys: List[PublicKey] = self._derive_pubkeys(self.keys)
self.db: Database = Database("mint", db)
async def load_used_proofs(self):
self.proofs_used = set(await get_proofs_used(db=self.db))
@staticmethod
def _derive_keys(master_key):
def _derive_keys(master_key: str):
"""Deterministic derivation of keys for 2^n values."""
return {
2
** i: int(
** i: PrivateKey(
hashlib.sha256((str(master_key) + str(i)).encode("utf-8"))
.hexdigest()
.encode("utf-8"),
16,
.encode("utf-8")[:32],
raw=True,
)
for i in range(MAX_ORDER)
}
@staticmethod
def _derive_pubkeys(keys):
return {
amt: keys[amt] * secp256k1.G for amt in [2**i for i in range(MAX_ORDER)]
}
def _derive_pubkeys(keys: List[PrivateKey]):
return {amt: keys[amt].pubkey for amt in [2**i for i in range(MAX_ORDER)]}
async def _generate_promises(self, amounts, B_s):
"""Generates promises that sum to the given amount."""
return [
await self._generate_promise(amount, Point(B_.x, B_.y, secp256k1))
await self._generate_promise(amount, PublicKey(bytes.fromhex(B_), raw=True))
for (amount, B_) in zip(amounts, B_s)
]
async def _generate_promise(self, amount: int, B_):
async def _generate_promise(self, amount: int, B_: PublicKey):
"""Generates a promise for given amount and returns a pair (amount, C')."""
secret_key = self.keys[amount] # Get the correct key
C_ = b_dhke.step2_alice(B_, secret_key)
await store_promise(amount, B_x=B_.x, B_y=B_.y, C_x=C_.x, C_y=C_.y, db=self.db)
return BlindedSignature(amount=amount, C_=BasePoint(x=C_.x, y=C_.y))
await store_promise(
amount, B_=B_.serialize().hex(), C_=C_.serialize().hex(), db=self.db
)
return BlindedSignature(amount=amount, C_=C_.serialize().hex())
def _check_spendable(self, proof: Proof):
"""Checks whether the proof was already spent."""
@@ -83,10 +81,12 @@ class Ledger:
if not self._check_spendable(proof):
raise Exception(f"tokens already spent. Secret: {proof.secret}")
secret_key = self.keys[proof.amount] # Get the correct key to check against
C = Point(proof.C.x, proof.C.y, secp256k1)
C = PublicKey(bytes.fromhex(proof.C), raw=True)
return b_dhke.verify(secret_key, C, proof.secret)
def _verify_outputs(self, total: int, amount: int, output_data):
def _verify_outputs(
self, total: int, amount: int, output_data: List[BlindedMessage]
):
"""Verifies the expected split was correctly computed"""
fst_amt, snd_amt = total - amount, amount # we have two amounts to split to
fst_outputs = amount_split(fst_amt)
@@ -95,16 +95,18 @@ class Ledger:
given = [o.amount for o in output_data]
return given == expected
def _verify_no_duplicates(self, proofs: List[Proof], output_data):
def _verify_no_duplicates(
self, proofs: List[Proof], output_data: List[BlindedMessage]
):
secrets = [p.secret for p in proofs]
if len(secrets) != len(list(set(secrets))):
return False
B_xs = [od.B_.x for od in output_data]
if len(B_xs) != len(list(set(B_xs))):
B_s = [od.B_ for od in output_data]
if len(B_s) != len(list(set(B_s))):
return False
return True
def _verify_split_amount(self, amount):
def _verify_split_amount(self, amount: int):
"""Split amount like output amount can't be negative or too big."""
try:
self._verify_amount(amount)
@@ -179,7 +181,7 @@ class Ledger:
# Public methods
def get_pubkeys(self):
"""Returns public keys for possible amounts."""
return self.pub_keys
return {a: p.serialize().hex() for a, p in self.pub_keys.items()}
async def request_mint(self, amount):
"""Returns Lightning invoice and stores it in the db."""
@@ -239,7 +241,7 @@ class Ledger:
if not all([self._verify_proof(p) for p in proofs]):
return False
total = sum([p["amount"] for p in proofs])
total = sum([p.amount for p in proofs])
if not self._verify_no_duplicates(proofs, output_data):
raise Exception("duplicate proofs or promises")

View File

@@ -17,12 +17,10 @@ async def m001_initial(db: Database):
"""
CREATE TABLE IF NOT EXISTS promises (
amount INTEGER NOT NULL,
B_x TEXT NOT NULL,
B_y TEXT NOT NULL,
C_x TEXT NOT NULL,
C_y TEXT NOT NULL,
B_b TEXT NOT NULL,
C_b TEXT NOT NULL,
UNIQUE (B_x, B_y)
UNIQUE (B_b)
);
"""
@@ -32,8 +30,7 @@ async def m001_initial(db: Database):
"""
CREATE TABLE IF NOT EXISTS proofs_used (
amount INTEGER NOT NULL,
C_x TEXT NOT NULL,
C_y TEXT NOT NULL,
C TEXT NOT NULL,
secret TEXT NOT NULL,
UNIQUE (secret)

44
poetry.lock generated
View File

@@ -127,40 +127,6 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "dataclasses"
version = "0.6"
description = "A backport of the dataclasses module for Python 3.6"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ecc"
version = "0.0.1"
description = "Pure Python implementation of an elliptic curve cryptosystem based on FIPS 186-3"
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ecc-pycrypto"
version = "1.0.0"
description = ""
category = "main"
optional = false
python-versions = ">=3.6"
develop = false
[package.dependencies]
dataclasses = "*"
[package.source]
type = "git"
url = "https://github.com/lc6chang/ecc-pycrypto.git"
reference = "v1.0.1"
resolved_reference = "eb8b8c19a81a52d9cf705d90a597a78cdaf2b6f6"
[[package]]
name = "ecdsa"
version = "0.18.0"
@@ -707,7 +673,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=
[metadata]
lock-version = "1.1"
python-versions = "^3.8"
content-hash = "27d48020dabbc74117941ab884e1794d502d38c1380c777ee0edb203ada3c7b1"
content-hash = "8046f708fe138fcdb9c1e39e18e4c466292f183f0d1736ecee66ec4854ad54cc"
[metadata.files]
anyio = [
@@ -838,14 +804,6 @@ colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
dataclasses = [
{file = "dataclasses-0.6-py3-none-any.whl", hash = "sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f"},
{file = "dataclasses-0.6.tar.gz", hash = "sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"},
]
ecc = [
{file = "ecc-0.0.1.zip", hash = "sha256:4bbcd46e9963ca37422d3244ab503af9dce95cbd35f676f7f9a4dd6306e23538"},
]
ecc-pycrypto = []
ecdsa = [
{file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"},
{file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"},

View File

@@ -8,7 +8,6 @@ license = "MIT"
[tool.poetry.dependencies]
python = "^3.8"
pycrypto = "^2.6.1"
ecc = "0.0.1"
requests = "2.27.1"
pytest-asyncio = "0.19.0"
SQLAlchemy = "1.3.24"
@@ -22,7 +21,6 @@ Jinja2 = "3.0.3"
MarkupSafe = "2.1.1"
urllib3 = "1.23"
Werkzeug = "2.2.2"
ecc-pycrypto = {git = "https://github.com/lc6chang/ecc-pycrypto.git", rev = "v1.0.1"}
asgiref = "^3.5.2"
pydantic = "^1.10.2"
bech32 = "^1.2.0"

View File

@@ -13,13 +13,12 @@ async def store_proof(
await (conn or db).execute(
"""
INSERT INTO proofs
(amount, C_x, C_y, secret)
VALUES (?, ?, ?, ?)
(amount, C, secret)
VALUES (?, ?, ?)
""",
(
proof.amount,
str(proof.C.x),
str(proof.C.y),
str(proof.C),
str(proof.secret),
),
)
@@ -55,13 +54,12 @@ async def invalidate_proof(
await (conn or db).execute(
"""
INSERT INTO proofs_used
(amount, C_x, C_y, secret)
VALUES (?, ?, ?, ?)
(amount, C, secret)
VALUES (?, ?, ?)
""",
(
proof.amount,
str(proof.C.x),
str(proof.C.y),
str(proof.C),
str(proof.secret),
),
)

View File

@@ -17,8 +17,7 @@ async def m001_initial(db: Database):
"""
CREATE TABLE IF NOT EXISTS proofs (
amount INTEGER NOT NULL,
C_x TEXT NOT NULL,
C_y TEXT NOT NULL,
C TEXT NOT NULL,
secret TEXT NOT NULL,
UNIQUE (secret)
@@ -31,8 +30,7 @@ async def m001_initial(db: Database):
"""
CREATE TABLE IF NOT EXISTS proofs_used (
amount INTEGER NOT NULL,
C_x TEXT NOT NULL,
C_y TEXT NOT NULL,
C TEXT NOT NULL,
secret TEXT NOT NULL,
UNIQUE (secret)

View File

@@ -2,11 +2,10 @@ import random
from typing import List
import requests
from ecc.curve import Point, secp256k1
from core.secp import PublicKey
import core.b_dhke as b_dhke
from core.base import (
BasePoint,
BlindedMessage,
MintPayloads,
Proof,
@@ -29,7 +28,8 @@ class LedgerAPI:
def _get_keys(url):
resp = requests.get(url + "/keys").json()
return {
int(amt): Point(val["x"], val["y"], secp256k1) for amt, val in resp.items()
int(amt): PublicKey(bytes.fromhex(val), raw=True)
for amt, val in resp.items()
}
@staticmethod
@@ -46,10 +46,9 @@ class LedgerAPI:
"""Returns proofs of promise from promises."""
proofs = []
for promise, (r, secret) in zip(promises, secrets):
C_ = Point(promise.C_.x, promise.C_.y, secp256k1)
C_ = PublicKey(bytes.fromhex(promise.C_), raw=True)
C = b_dhke.step3_bob(C_, r, self.keys[promise.amount])
c_point = BasePoint(x=C.x, y=C.y)
proof = Proof(amount=promise.amount, C=c_point, secret=secret)
proof = Proof(amount=promise.amount, C=C.serialize().hex(), secret=secret)
proofs.append(proof)
return proofs
@@ -63,13 +62,14 @@ class LedgerAPI:
payloads: MintPayloads = MintPayloads()
secrets = []
rs = []
for i, amount in enumerate(amounts):
for amount in amounts:
secret = str(random.getrandbits(128))
secrets.append(secret)
B_, r = b_dhke.step1_bob(secret)
rs.append(r)
blinded_point = BasePoint(x=str(B_.x), y=str(B_.y))
payload: BlindedMessage = BlindedMessage(amount=amount, B_=blinded_point)
payload: BlindedMessage = BlindedMessage(
amount=amount, B_=B_.serialize().hex()
)
payloads.blinded_messages.append(payload)
promises_dict = requests.post(
self.url + "/mint",
@@ -94,9 +94,8 @@ class LedgerAPI:
secret = str(random.getrandbits(128))
B_, r = b_dhke.step1_bob(secret)
secrets.append((r, secret))
blinded_point = BasePoint(x=str(B_.x), y=str(B_.y))
payload: BlindedMessage = BlindedMessage(
amount=output_amt, B_=blinded_point
amount=output_amt, B_=B_.serialize().hex()
)
payloads.blinded_messages.append(payload)
split_payload = SplitPayload(proofs=proofs, amount=amount, output_data=payloads)