Skip to content

Commit 6815a0d

Browse files
committed
bip-XXXX: Add vendored secp256k1lab library
1 parent c021a5f commit 6815a0d

19 files changed

Lines changed: 1025 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Tests
2+
on: [push, pull_request]
3+
jobs:
4+
ruff:
5+
runs-on: ubuntu-latest
6+
steps:
7+
- uses: actions/checkout@v4
8+
- name: Install the latest version of uv
9+
uses: astral-sh/setup-uv@v5
10+
- run: uvx ruff check .
11+
mypy:
12+
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
python-version: ["3.11", "3.12", "3.13", "3.14"]
16+
steps:
17+
- uses: actions/checkout@v4
18+
- name: Install the latest version of uv, setup Python ${{ matrix.python-version }}
19+
uses: astral-sh/setup-uv@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- run: uvx mypy .
23+
unittest:
24+
runs-on: ubuntu-latest
25+
strategy:
26+
matrix:
27+
python-version: ["3.11", "3.12", "3.13", "3.14"]
28+
steps:
29+
- uses: actions/checkout@v4
30+
- name: Setup Python ${{ matrix.python-version }}
31+
uses: actions/setup-python@v5
32+
with:
33+
python-version: ${{ matrix.python-version }}
34+
- run: python3 -m unittest

bip-XXXX/secp256k1lab/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
9+
# Virtual environments
10+
.venv
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

bip-XXXX/secp256k1lab/CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
#### Added
11+
- Added new methods `Scalar.from_int_nonzero_checked` and `Scalar.from_bytes_nonzero_checked`
12+
that ensure a constructed scalar is in the range `0 < s < N` (i.e. is non-zero and within the
13+
group order) and throw a `ValueError` otherwise. This is e.g. useful for ensuring that newly
14+
generated secret keys or nonces are valid without having to do the non-zero check manually.
15+
The already existing methods `Scalar.from_int_checked` and `Scalar.from_bytes_checked` error
16+
on overflow, but not on zero, i.e. they only ensure `0 <= s < N`.
17+
18+
- Added a new method `GE.from_bytes_compressed_with_infinity` to parse a compressed
19+
public key (33 bytes) to a group element, where the all-zeros bytestring maps to the
20+
point at infinity. This is the counterpart to the already existing serialization
21+
method `GE.to_bytes_compressed_with_infinity`.
22+
23+
## [1.0.0] - 2025-03-31
24+
25+
Initial release.

bip-XXXX/secp256k1lab/COPYING

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2009-2024 The Bitcoin Core developers
4+
Copyright (c) 2009-2024 Bitcoin Developers
5+
Copyright (c) 2025- The secp256k1lab Developers
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.

bip-XXXX/secp256k1lab/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
secp256k1lab
2+
============
3+
4+
![Dependencies: None](https://img.shields.io/badge/dependencies-none-success)
5+
6+
An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes written in Python, intended for prototyping, experimentation and education.
7+
8+
Features:
9+
* Low-level secp256k1 field and group arithmetic.
10+
* Schnorr signing/verification and key generation according to [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki).
11+
* ECDH key exchange.
12+
13+
WARNING: The code in this library is slow and trivially vulnerable to side channel attacks.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[project]
2+
name = "secp256k1lab"
3+
version = "1.0.0"
4+
description = "An INSECURE implementation of the secp256k1 elliptic curve and related cryptographic schemes, intended for prototyping, experimentation and education"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Pieter Wuille", email = "pieter@wuille.net" },
8+
{ name = "Tim Ruffing", email = "me@real-or-random.org" },
9+
{ name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
10+
{ name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
11+
]
12+
maintainers = [
13+
{ name = "Tim Ruffing", email = "me@real-or-random.org" },
14+
{ name = "Jonas Nick", email = "jonasd.nick@gmail.com" },
15+
{ name = "Sebastian Falbesoner", email = "sebastian.falbesoner@gmail.com" }
16+
]
17+
requires-python = ">=3.11"
18+
license = "MIT"
19+
license-files = ["COPYING"]
20+
keywords = ["secp256k1", "elliptic curves", "cryptography", "Bitcoin"]
21+
classifiers = [
22+
"Development Status :: 5 - Production/Stable",
23+
"Intended Audience :: Developers",
24+
"Intended Audience :: Education",
25+
"Intended Audience :: Science/Research",
26+
"License :: OSI Approved :: MIT License",
27+
"Programming Language :: Python",
28+
"Topic :: Security :: Cryptography",
29+
]
30+
dependencies = []
31+
32+
[build-system]
33+
requires = ["hatchling"]
34+
build-backend = "hatchling.build"

bip-XXXX/secp256k1lab/src/secp256k1lab/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# The following functions are based on the BIP 340 reference implementation:
2+
# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py
3+
4+
from .secp256k1 import FE, GE, G
5+
from .util import int_from_bytes, bytes_from_int, xor_bytes, tagged_hash
6+
7+
8+
def pubkey_gen(seckey: bytes) -> bytes:
9+
d0 = int_from_bytes(seckey)
10+
if not (1 <= d0 <= GE.ORDER - 1):
11+
raise ValueError("The secret key must be an integer in the range 1..n-1.")
12+
P = d0 * G
13+
assert not P.infinity
14+
return P.to_bytes_xonly()
15+
16+
17+
def schnorr_sign(
18+
msg: bytes, seckey: bytes, aux_rand: bytes, tag_prefix: str = "BIP0340"
19+
) -> bytes:
20+
d0 = int_from_bytes(seckey)
21+
if not (1 <= d0 <= GE.ORDER - 1):
22+
raise ValueError("The secret key must be an integer in the range 1..n-1.")
23+
if len(aux_rand) != 32:
24+
raise ValueError("aux_rand must be 32 bytes instead of %i." % len(aux_rand))
25+
P = d0 * G
26+
assert not P.infinity
27+
d = d0 if P.has_even_y() else GE.ORDER - d0
28+
t = xor_bytes(bytes_from_int(d), tagged_hash(tag_prefix + "/aux", aux_rand))
29+
k0 = (
30+
int_from_bytes(tagged_hash(tag_prefix + "/nonce", t + P.to_bytes_xonly() + msg))
31+
% GE.ORDER
32+
)
33+
if k0 == 0:
34+
raise RuntimeError("Failure. This happens only with negligible probability.")
35+
R = k0 * G
36+
assert not R.infinity
37+
k = k0 if R.has_even_y() else GE.ORDER - k0
38+
e = (
39+
int_from_bytes(
40+
tagged_hash(
41+
tag_prefix + "/challenge", R.to_bytes_xonly() + P.to_bytes_xonly() + msg
42+
)
43+
)
44+
% GE.ORDER
45+
)
46+
sig = R.to_bytes_xonly() + bytes_from_int((k + e * d) % GE.ORDER)
47+
assert schnorr_verify(msg, P.to_bytes_xonly(), sig, tag_prefix=tag_prefix)
48+
return sig
49+
50+
51+
def schnorr_verify(
52+
msg: bytes, pubkey: bytes, sig: bytes, tag_prefix: str = "BIP0340"
53+
) -> bool:
54+
if len(pubkey) != 32:
55+
raise ValueError("The public key must be a 32-byte array.")
56+
if len(sig) != 64:
57+
raise ValueError("The signature must be a 64-byte array.")
58+
try:
59+
P = GE.from_bytes_xonly(pubkey)
60+
except ValueError:
61+
return False
62+
r = int_from_bytes(sig[0:32])
63+
s = int_from_bytes(sig[32:64])
64+
if (r >= FE.SIZE) or (s >= GE.ORDER):
65+
return False
66+
e = (
67+
int_from_bytes(tagged_hash(tag_prefix + "/challenge", sig[0:32] + pubkey + msg))
68+
% GE.ORDER
69+
)
70+
R = s * G - e * P
71+
if R.infinity or (not R.has_even_y()) or (R.x != r):
72+
return False
73+
return True
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import hashlib
2+
3+
from .secp256k1 import GE, Scalar
4+
5+
6+
def ecdh_compressed_in_raw_out(seckey: bytes, pubkey: bytes) -> GE:
7+
"""TODO"""
8+
shared_secret = Scalar.from_bytes_checked(seckey) * GE.from_bytes_compressed(pubkey)
9+
assert not shared_secret.infinity # prime-order group
10+
return shared_secret
11+
12+
13+
def ecdh_libsecp256k1(seckey: bytes, pubkey: bytes) -> bytes:
14+
"""TODO"""
15+
shared_secret = ecdh_compressed_in_raw_out(seckey, pubkey)
16+
return hashlib.sha256(shared_secret.to_bytes_compressed()).digest()

0 commit comments

Comments
 (0)