Skip to content

RFC: Integrate secp256k1lab v1.0.0 as subtree, use it for BIP-374 #1855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bip-0374/gen_test_vectors.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
#!/usr/bin/env python3
"""Generate the BIP-0374 test vectors."""
import csv
import os
from pathlib import Path
import sys
from reference import (
TaggedHash,
dleq_generate_proof,
dleq_verify_proof,
)
from secp256k1 import G as GENERATOR, GE
from secp256k1lab.secp256k1 import G as GENERATOR, GE


NUM_SUCCESS_TEST_VECTORS = 8
DLEQ_TAG_TESTVECTORS_RNG = "BIP0374/testvectors_rng"

FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')
FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'


def random_scalar_int(vector_i, purpose):
Expand Down
6 changes: 5 additions & 1 deletion bip-0374/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
"""Reference implementation of DLEQ BIP for secp256k1 with unit tests."""

from hashlib import sha256
from pathlib import Path
import random
from secp256k1 import G, GE
import sys
import unittest

# Prefer the vendored copy of secp256k1lab
sys.path.insert(0, str(Path(__file__).parent / "../python/secp256k1lab/src"))
from secp256k1lab.secp256k1 import G, GE


DLEQ_TAG_AUX = "BIP0374/aux"
DLEQ_TAG_NONCE = "BIP0374/nonce"
Expand Down
8 changes: 4 additions & 4 deletions bip-0374/run_test_vectors.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
#!/usr/bin/env python3
"""Run the BIP-DLEQ test vectors."""
import csv
import os
from pathlib import Path
import sys
from reference import (
dleq_generate_proof,
dleq_verify_proof,
)
from secp256k1 import GE
from secp256k1lab.secp256k1 import GE


FILENAME_GENERATE_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_generate_proof.csv')
FILENAME_VERIFY_PROOF_TEST = os.path.join(sys.path[0], 'test_vectors_verify_proof.csv')
FILENAME_GENERATE_PROOF_TEST = Path(__file__).parent / 'test_vectors_generate_proof.csv'
FILENAME_VERIFY_PROOF_TEST = Path(__file__).parent / 'test_vectors_verify_proof.csv'


all_passed = True
Expand Down
17 changes: 17 additions & 0 deletions python/secp256k1lab/.github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Tests
on: [push, pull_request]
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
- run: uvx ruff check .
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v5
- run: uvx mypy .
1 change: 1 addition & 0 deletions python/secp256k1lab/.python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.9
10 changes: 10 additions & 0 deletions python/secp256k1lab/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2025-03-31

Initial release.
23 changes: 23 additions & 0 deletions python/secp256k1lab/COPYING
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
The MIT License (MIT)

Copyright (c) 2009-2024 The Bitcoin Core developers
Copyright (c) 2009-2024 Bitcoin Developers
Copyright (c) 2025- The secp256k1lab Developers

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
13 changes: 13 additions & 0 deletions python/secp256k1lab/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
secp256k1lab
============

![Dependencies: None](https://img.shields.io/badge/dependencies-none-success)

An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education.

Features:
* Low-level secp256k1 field and group arithmetic.
* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
* ECDH key exchange.

WARNING: The code in this library is slow and trivially vulnerable to side channel attacks.
34 changes: 34 additions & 0 deletions python/secp256k1lab/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[project]
name = "secp256k1lab"
version = "1.0.0"
description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education"
readme = "README.md"
authors = [
{ name = "Pieter Wuille", email = "[email protected]" },
{ name = "Tim Ruffing", email = "[email protected]" },
{ name = "Jonas Nick", email = "[email protected]" },
{ name = "Sebastian Falbesoner", email = "[email protected]" }
]
maintainers = [
{ name = "Tim Ruffing", email = "[email protected]" },
{ name = "Jonas Nick", email = "[email protected]" },
{ name = "Sebastian Falbesoner", email = "[email protected]" }
]
requires-python = ">=3.9"
license = "MIT"
license-files = ["COPYING"]
keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Topic :: Security :: Cryptography",
]
dependencies = []

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Empty file.
73 changes: 73 additions & 0 deletions python/secp256k1lab/src/secp256k1lab/bip340.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# The following functions are based on the BIP 340 reference implementation:
# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py

from .secp256k1 import FE, GE, G
from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash


def pubkey_gen(seckey: bytes) -> bytes:
d0 = int_from_bytes(seckey)
if not (1 <= d0 <= GE.ORDER - 1):
raise ValueError("The secret key must be an integer in the range 1..n-1.")
P = d0 * G
assert not P.infinity
return P.to_bytes_xonly()


def schnorr_sign(
msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340"
) -> bytes:
d0 = int_from_bytes(seckey)
if not (1 <= d0 <= GE.ORDER - 1):
raise ValueError("The secret key must be an integer in the range 1..n-1.")
if len(aux_rand) != 32:
raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand))
P = d0 * G
assert not P.infinity
d = d0 if P.has_even_y() else GE.ORDER - d0
t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand))
k0 = (
int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg))
% GE.ORDER
)
if k0 == 0:
raise RuntimeError("Failure. This happens only with negligible probability.")
R = k0 * G
assert not R.infinity
k = k0 if R.has_even_y() else GE.ORDER - k0
e = (
int_from_bytes(
tagged_hash(
tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg
)
)
% GE.ORDER
)
sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER)
assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix)
return sig


def schnorr_verify(
msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340"
) -> bool:
if len(pubkey) != 32:
raise ValueError("The public key must be a 32-byte array.")
if len(sig) != 64:
raise ValueError("The signature must be a 64-byte array.")
try:
P = GE.from_bytes_xonly(pubkey)
except ValueError:
return False
r = int_from_bytes(sig[0:32])
s = int_from_bytes(sig[32:64])
if (r >= FE.SIZE) or (s >= GE.ORDER):
return False
e = (
int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg))
% GE.ORDER
)
R = s * G - e * P
if R.infinity or (not R.has_even_y()) or (R.x != r):
return False
return True
16 changes: 16 additions & 0 deletions python/secp256k1lab/src/secp256k1lab/ecdh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import hashlib

from .secp256k1 import GE, Scalar


def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE:
"""TODO"""
shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey)
assert not shared_secret.infinity # prime-order group
return shared_secret


def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes:
"""TODO"""
shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey)
return hashlib.sha256(shared_secret.to_bytes_compressed()).digest()
15 changes: 15 additions & 0 deletions python/secp256k1lab/src/secp256k1lab/keys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from .secp256k1 import GE, G
from .util import int_from_bytes

# The following function is based on the BIP 327 reference implementation
# https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py


# Return the plain public key corresponding to a given secret key
def pubkey_gen_plain(seckey: bytes) -> bytes:
d0 = int_from_bytes(seckey)
if not (1 <= d0 <= GE.ORDER - 1):
raise ValueError("The secret key must be an integer in the range 1..n-1.")
P = d0 * G
assert not P.infinity
return P.to_bytes_compressed()
Empty file.
Loading