Skip to content

Commit

Permalink
feat: add coincurve instead of secp256k1
Browse files Browse the repository at this point in the history
- changed secp256k1 to coincurve
- add testcase for verifying pubkey
lightning/bolts#1184
- added keep_payee flag for encoding
- added tests for signature model
- added test for invalid signature
  • Loading branch information
dni committed Jul 18, 2024
1 parent ef22967 commit e291d8a
Show file tree
Hide file tree
Showing 9 changed files with 545 additions and 424 deletions.
5 changes: 4 additions & 1 deletion bolt11/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" bolt11 CLI """
"""bolt11 CLI"""

import json
import sys
Expand Down Expand Up @@ -52,10 +52,12 @@ def decode(bolt11, ignore_exceptions, strict):
@click.argument("private_key", type=str, default=None, required=False)
@click.argument("ignore_exceptions", type=bool, default=True)
@click.argument("strict", type=bool, default=False)
@click.argument("keep_payee", type=bool, default=False)
def encode(
json_string,
ignore_exceptions: bool = True,
strict: bool = False,
keep_payee: bool = False,
private_key: Optional[str] = None,
):
"""
Expand Down Expand Up @@ -92,6 +94,7 @@ def encode(
private_key,
ignore_exceptions=ignore_exceptions,
strict=strict,
keep_payee=keep_payee,
)
click.echo(encoded)
except Bolt11Exception as exc:
Expand Down
13 changes: 9 additions & 4 deletions bolt11/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ def decode(
timestamp = data_part.read(35).uint

tags = Tags()
payee = None

while data_part.pos != data_part.len:
tag, tagdata, data_part = _pull_tagged(data_part)
data_length = int(len(tagdata or []) / 5)

# MUST skip over unknown fields, OR an f field with unknown version, OR p, h,
# s or n fields that do NOT have data_lengths of 52, 52, 52 or 53, respectively.

if (
tag == TagChar.payment_hash.value
and data_length == 52
Expand Down Expand Up @@ -93,9 +95,10 @@ def decode(
and data_length == 53
and not tags.has(TagChar.payee)
):
payee = trim_to_bytes(tagdata).hex()
tags.add(
TagChar.payee,
trim_to_bytes(tagdata).hex(),
payee,
)
elif (
tag == TagChar.description.value
Expand Down Expand Up @@ -133,6 +136,10 @@ def decode(
elif tag == TagChar.route_hint.value:
tags.add(TagChar.route_hint, RouteHint.from_bitstring(tagdata))

else:
# skip unknown fields
pass

signature = Signature(
signature_data=signature_data,
signing_data=hrp.encode() + data_part.tobytes(),
Expand All @@ -141,11 +148,9 @@ def decode(
# A reader MUST check that the `signature` is valid (see the `n` tagged field
# specified below). A reader MUST use the `n` field to validate the signature
# instead of performing signature recovery if a valid `n` field is provided.
payee = tags.get(TagChar.payee)
if payee:
# TODO: research why no test runs this?
try:
signature.verify(payee.data)
signature.verify(payee)
except Exception as exc:
raise Bolt11SignatureVerifyException() from exc
else:
Expand Down
7 changes: 3 additions & 4 deletions bolt11/encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def encode(
private_key: Optional[str] = None,
ignore_exceptions: bool = False,
strict: bool = False,
keep_payee: bool = False,
) -> str:
try:
if invoice.description_hash:
Expand All @@ -75,10 +76,8 @@ def encode(
tags += _tagged_bytes(tag.bech32, bytes.fromhex(tag.data))
elif tag.char == TagChar.metadata:
tags += _tagged_bytes(tag.bech32, bytes.fromhex(tag.data))
# TODO: why uncommented?
# payee is not needed, needs more research
# elif tag.char == TagChar.payee:
# tags += _tagged_bytes(tag.bech32, bytes.fromhex(tag.data))
elif tag.char == TagChar.payee and keep_payee:
tags += _tagged_bytes(tag.bech32, bytes.fromhex(tag.data))
elif tag.char == TagChar.features:
tags += _tagged_bytes(tag.bech32, tag.data.data)
elif tag.char == TagChar.fallback:
Expand Down
8 changes: 3 additions & 5 deletions bolt11/models/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
from typing import Optional

from bitstring import Bits
from coincurve import PrivateKey
from ecdsa import SECP256k1, VerifyingKey
from ecdsa.util import sigdecode_string
from secp256k1 import PrivateKey


@dataclass
Expand All @@ -19,12 +19,10 @@ class Signature:
def from_private_key(
cls, private_key: str, hrp: str, signing_data: Bits
) -> "Signature":
key = PrivateKey(bytes.fromhex(private_key))
sig = key.ecdsa_sign_recoverable(
key: PrivateKey = PrivateKey.from_hex(private_key)
signature_data = key.sign_recoverable(
bytearray([ord(c) for c in hrp]) + signing_data.tobytes()
)
sig, recid = key.ecdsa_recoverable_serialize(sig)
signature_data = bytes(sig) + bytes([recid])
return cls(signing_data=signing_data.tobytes(), signature_data=signature_data)

def verify(self, payee: str) -> bool:
Expand Down
838 changes: 431 additions & 407 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ python = ">=3.8.1"
click = "*"
base58 = "*"
ecdsa = "*"
secp256k1 = "*"
coincurve = "*"
bech32 = "*"
bitstring = "*"

Expand Down
34 changes: 32 additions & 2 deletions tests/test_bolt11_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -991,8 +991,6 @@ def test_example_12(self):
)

encoded = encode(invoice, ex["private_key"])
print(encoded)
print(ex["payment_request"])
assert encoded == ex["payment_request"].lower()

def test_example_13(self):
Expand Down Expand Up @@ -1125,3 +1123,35 @@ def test_example_14(self):
)
encoded = encode(invoice, ex["private_key"])
assert encoded == ex["payment_request"]

def test_example_15(self):
"""
Verify payee signature
"""
ex = {
"payment_request": (
"lnbc10n1p0v27vqpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqnp4q0n326hr8v9zprg8"
"gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3z"
"ygsdqjv3jhxcmjd9c8g6t0dcjctywagxza9lahzzf8yrd4m4dn8lx7q9dtf5896pfx2jc30dv2w8vw38j2kpr7trh"
"fuqdkavr2925n2f85g0uzansyy5pusrwansemqp0ux0x3"
),
"payment_secret": (
"1111111111111111111111111111111111111111111111111111111111111111"
),
"description": "description",
"payee": (
"03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad"
),
"private_key": (
"e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734"
),
}

decoded = decode(ex["payment_request"])
assert decoded.payment_secret == ex["payment_secret"]
assert decoded.description == ex["description"]
assert decoded.payee == ex["payee"]
assert decoded.signature

re_encoded = encode(decoded, keep_payee=True)
assert re_encoded == ex["payment_request"]
39 changes: 39 additions & 0 deletions tests/test_signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from bitstring import Bits

from bolt11 import Signature

ex = {
"private_key": "e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734",
"public_key": "031a7a2db2e0c0fbfcfe62e23b6262b9f2b76bc01b022d17b2f9b49c7bc22310fd",
"signature": (
"202bea309bab7aeff223ad2890d42ca00bc5673c88cab47420387100f8939fb26a"
"dea33c4e1748b3a56f87b3f9b1b001ffdc82169809782a02bd03ae2d1cbdc700"
),
}


class TestBolt11Signature:
def test_recovers_public_key(self):
signature = Signature.from_private_key(
ex["private_key"],
"lnbc",
Bits(b"1234567890"),
)
assert signature.recover_public_key() == ex["public_key"]

def test_signature_from_private_key(self):
signature = Signature.from_private_key(
ex["private_key"],
"lnbc",
Bits(b"1234567890"),
)
assert signature.signing_data == b"1234567890"
assert signature.signature_data == bytes.fromhex(ex["signature"])

def test_signature_verify(self):
signature = Signature.from_private_key(
ex["private_key"],
"lnbc",
Bits(b"1234567890"),
)
assert signature.verify(ex["public_key"])
23 changes: 23 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Bolt11NoMinFinalCltvException,
Bolt11NoPaymentHashException,
Bolt11NoPaymentSecretException,
Bolt11SignatureVerifyException,
)

ex = {
Expand Down Expand Up @@ -178,3 +179,25 @@ def test_validate_strict_encoding(self):

with pytest.raises(Bolt11NoMinFinalCltvException):
decode(bolt11, strict=True)

def test_validate_signature_verification(self):
invoice = Bolt11(
currency=ex["currency"],
amount_msat=ex["amount_msat"],
date=ex["date"],
tags=Tags.from_dict(
{
"p": ex["payment_hash"],
"s": ex["payment_secret"],
"h": ex["description_hash"],
# invalid pubkey
"n": (
"03b1c1a3dd064c7b4386b688c1f0950fddb28"
"f61f2c3be8bcaf4ef3c78429ffe4e"
),
}
),
)
bolt11 = encode(invoice, ex["private_key"], keep_payee=True)
with pytest.raises(Bolt11SignatureVerifyException):
decode(bolt11)

0 comments on commit e291d8a

Please sign in to comment.