ecc 2026.10.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecc/__init__.py +101 -0
- ecc/_libsecp256k1.py +336 -0
- ecc/_utils.py +442 -0
- ecc/alias.py +211 -0
- ecc/curves/__init__.py +161 -0
- ecc/curves/_data/ec_Brainpool.json +79 -0
- ecc/curves/_data/ec_NIST.json +57 -0
- ecc/curves/_data/ec_SEC2v1_insecure.json +79 -0
- ecc/curves/_data/ec_SEC2v2.json +90 -0
- ecc/curves/curve.py +1681 -0
- ecc/curves/curve_group.py +1905 -0
- ecc/curves/curve_group_2.py +754 -0
- ecc/curves/curve_group_f.py +63 -0
- ecc/curves/sec_point.py +461 -0
- ecc/ecc/__init__.py +108 -0
- ecc/ecc/bip340_nonce.py +177 -0
- ecc/ecc/borromean.py +871 -0
- ecc/ecc/commit_nonce.py +193 -0
- ecc/ecc/dh.py +135 -0
- ecc/ecc/dlc_oracle.py +76 -0
- ecc/ecc/dleq.py +290 -0
- ecc/ecc/dsa.py +2624 -0
- ecc/ecc/ecdsa_adaptor.py +318 -0
- ecc/ecc/ecies.py +426 -0
- ecc/ecc/ellswift.py +329 -0
- ecc/ecc/frost.py +1170 -0
- ecc/ecc/musig2.py +1220 -0
- ecc/ecc/pedersen.py +577 -0
- ecc/ecc/rangeproof.py +1729 -0
- ecc/ecc/rfc6979_nonce.py +182 -0
- ecc/ecc/ssa.py +1774 -0
- ecc/ecc/ssa_adaptor.py +260 -0
- ecc/exceptions.py +139 -0
- ecc/hashes.py +109 -0
- ecc/kdf.py +219 -0
- ecc/number_theory.py +648 -0
- ecc/py.typed +0 -0
- ecc-2026.10.20.dist-info/METADATA +243 -0
- ecc-2026.10.20.dist-info/RECORD +42 -0
- ecc-2026.10.20.dist-info/WHEEL +4 -0
- ecc-2026.10.20.dist-info/licenses/AUTHORS.md +9 -0
- ecc-2026.10.20.dist-info/licenses/LICENSE +21 -0
ecc/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Copyright (c) The btclib developers
|
|
2
|
+
# Distributed under the MIT software license, see the accompanying
|
|
3
|
+
# LICENSE file or https://opensource.org/license/mit for the full text.
|
|
4
|
+
|
|
5
|
+
"""The ecc package: what it publishes, and the version metadata.
|
|
6
|
+
|
|
7
|
+
Elliptic curve arithmetic and the signature, commitment and key-agreement
|
|
8
|
+
schemes built on it, over any curve in short Weierstrass form, with
|
|
9
|
+
libsecp256k1 accelerating secp256k1 where the optional `btclib-secp256k1`
|
|
10
|
+
bindings are installed.
|
|
11
|
+
|
|
12
|
+
`__all__` here is the root of the package's public tree: the packages and
|
|
13
|
+
top-level modules a caller reaches from this name. Each of those, and each
|
|
14
|
+
module below them, declares its own `__all__`, so a walk that starts here
|
|
15
|
+
has a declared surface at every node -- which is why the list is not
|
|
16
|
+
`pkgutil.iter_modules`: discovery would answer the file tree, and a module
|
|
17
|
+
added to the directory would publish itself rather than being published.
|
|
18
|
+
|
|
19
|
+
`name` is not in it, nor are the metadata dunders. `name` is the
|
|
20
|
+
distribution's name and not a member of the tree, `__version__` bound by
|
|
21
|
+
a star import would overwrite the importing module's own, and each is
|
|
22
|
+
still an attribute here: `ecc.__version__` is how a caller
|
|
23
|
+
reads the version and `ecc.name` how it reads the name.
|
|
24
|
+
|
|
25
|
+
Nothing is imported eagerly. A module is imported when it is first asked
|
|
26
|
+
for, through the `__getattr__` at the bottom of this file, so `import
|
|
27
|
+
ecc` is the metadata lookup below and nothing else, and the
|
|
28
|
+
import graph keeps its shape: importing every module here would put the
|
|
29
|
+
whole package in `sys.modules` before any single module of it could be
|
|
30
|
+
imported first, which is the situation tests/imports_test.py exists to
|
|
31
|
+
make impossible.
|
|
32
|
+
|
|
33
|
+
What that costs is worth stating: mypy reads a module-level `__getattr__`
|
|
34
|
+
as a promise that any attribute may exist, so `ecc.cruves` is
|
|
35
|
+
`Any` to it and a misspelling on this package is a runtime
|
|
36
|
+
`AttributeError` rather than a reported error. The spellings a caller
|
|
37
|
+
actually writes -- `from ecc import curves`, `import
|
|
38
|
+
ecc.curves`, `from ecc.curves import mult` --
|
|
39
|
+
resolve against the real modules and stay checked.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from importlib import import_module
|
|
43
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
44
|
+
from types import ModuleType
|
|
45
|
+
|
|
46
|
+
# the one literal the metadata lookups here and in _libsecp256k1 read: a
|
|
47
|
+
# rehearsal publishes to TestPyPI as `ecc-test`, and the dev-version
|
|
48
|
+
# action rewrites this line with pyproject.toml's `name`
|
|
49
|
+
name = "ecc"
|
|
50
|
+
try:
|
|
51
|
+
__version__ = version(name)
|
|
52
|
+
except PackageNotFoundError:
|
|
53
|
+
# a source tree with no metadata beside it: git clone and import, or
|
|
54
|
+
# read the docs, which builds without installing this package. Any
|
|
55
|
+
# number here would be a guess, and reading pyproject.toml back is not
|
|
56
|
+
# the way to stop guessing: the file is not in the wheel. Importing has
|
|
57
|
+
# to keep working, so the version says it does not know
|
|
58
|
+
__version__ = "unknown"
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"alias",
|
|
62
|
+
"curves",
|
|
63
|
+
"ecc",
|
|
64
|
+
"exceptions",
|
|
65
|
+
"hashes",
|
|
66
|
+
"kdf",
|
|
67
|
+
"number_theory",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def __getattr__(published: str) -> ModuleType:
|
|
72
|
+
"""Import a published module the first time it is asked for.
|
|
73
|
+
|
|
74
|
+
PEP 562: this runs only for a name the package does not already have,
|
|
75
|
+
so it answers `ecc.curves` once and the import machinery's
|
|
76
|
+
own attribute answers it from then on -- and it never runs for
|
|
77
|
+
`import ecc.curves` or `from ecc import curves`,
|
|
78
|
+
which import the submodule themselves. What it makes work is
|
|
79
|
+
`getattr(ecc, "curves")` on a fresh interpreter, which is
|
|
80
|
+
how a walker reading `__all__` descends, and `from ecc
|
|
81
|
+
import *`, which asks for each name in the list.
|
|
82
|
+
|
|
83
|
+
Anything not in `__all__` raises `AttributeError`, private modules
|
|
84
|
+
included: `import ecc._utils` still reaches one. The
|
|
85
|
+
message is the interpreter's own wording, so a typo reads as it does
|
|
86
|
+
anywhere else.
|
|
87
|
+
"""
|
|
88
|
+
if published in __all__:
|
|
89
|
+
return import_module(f"{__name__}.{published}")
|
|
90
|
+
raise AttributeError(f"module {__name__!r} has no attribute {published!r}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def __dir__() -> list[str]:
|
|
94
|
+
"""Answer with the published tree beside what the package already has.
|
|
95
|
+
|
|
96
|
+
`dir(ecc)` consults this rather than the namespace, so
|
|
97
|
+
without it a module not yet imported is missing from the completion a
|
|
98
|
+
caller gets at an interactive prompt -- the same names `__getattr__`
|
|
99
|
+
above will answer for.
|
|
100
|
+
"""
|
|
101
|
+
return sorted({*__all__, *globals()})
|
ecc/_libsecp256k1.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Copyright (c) The btclib developers
|
|
2
|
+
# Distributed under the MIT software license, see the accompanying
|
|
3
|
+
# LICENSE file or https://opensource.org/license/mit for the full text.
|
|
4
|
+
|
|
5
|
+
"""Everything this package asks of libsecp256k1, imported in one place.
|
|
6
|
+
|
|
7
|
+
The modules that delegate to the bindings import none of them directly:
|
|
8
|
+
they import from here, so the question "are the bindings installed" is
|
|
9
|
+
asked once, at one import, and answered by `INSTALLED` rather than by a
|
|
10
|
+
`try` block in each of them, which could drift apart. `ENABLED` is that
|
|
11
|
+
answer with `ECC_NO_LIBSECP256K1` applied -- installed, and not
|
|
12
|
+
refused -- and `curves.curve` reads it into `_libsecp256k1_available`,
|
|
13
|
+
which is the seam every dispatch in the package consults, so an absent
|
|
14
|
+
binding is a dispatch that declines rather than an `ImportError` raised
|
|
15
|
+
before any dispatch is reached.
|
|
16
|
+
|
|
17
|
+
Two names because the seam moves and this import does not. `ENABLED` is
|
|
18
|
+
only the state `curves.curve` starts in, and `set_libsecp256k1_serving`
|
|
19
|
+
may have moved it since; `INSTALLED` is settled here and stays, so it is
|
|
20
|
+
what that function reads to refuse `serving=True` where there is nothing
|
|
21
|
+
to serve, and what the suite skips its `bindings` marker on. The
|
|
22
|
+
difference is this module's and the seam's, not a caller's:
|
|
23
|
+
`curves.is_libsecp256k1_serving` publishes one answer -- whether the next
|
|
24
|
+
call goes to libsecp256k1 or to the Python arithmetic -- which is the
|
|
25
|
+
only difference a caller can act on.
|
|
26
|
+
|
|
27
|
+
A module that imports nothing of this package but `exceptions` and the
|
|
28
|
+
root's `name`, and is therefore below every module that reads it. What it
|
|
29
|
+
imports from the bindings is the whole of the surface this package uses, so
|
|
30
|
+
this file is also the answer to "what does this package need these bindings
|
|
31
|
+
for", which no reader has to assemble from the delegating modules' own
|
|
32
|
+
imports.
|
|
33
|
+
|
|
34
|
+
The names are re-exported under the bindings' own spelling and the caller
|
|
35
|
+
aliases them as it always did: a caller holding `keys` and reaching
|
|
36
|
+
through it, and a caller naming `pubkey_sum` directly, both keep the call
|
|
37
|
+
they had. Neither shape costs an attribute lookup it did not cost before,
|
|
38
|
+
which matters where `curves.curve` counts tenths of a microsecond.
|
|
39
|
+
|
|
40
|
+
With the bindings absent every name here is None. Nothing may call one:
|
|
41
|
+
`_libsecp256k1_serves` is False in that configuration, and it is the
|
|
42
|
+
predicate in front of every delegation -- which is a rule about the
|
|
43
|
+
package rather than about this file, and is what `tests/no_bindings_test.py`
|
|
44
|
+
checks by importing this package with the bindings out of reach.
|
|
45
|
+
|
|
46
|
+
Absent and installed-but-too-old are different failures, and only the
|
|
47
|
+
first one is this fallback (btclib-org/ecc#25). `btclib_secp256k1`
|
|
48
|
+
itself failing to import is `ModuleNotFoundError` with its `name` naming
|
|
49
|
+
the top-level package -- nothing else is. A name this module asks for that
|
|
50
|
+
an installed, too-old package does not have raises a plain `ImportError`
|
|
51
|
+
instead (the package is found; the attribute inside it is not), or a
|
|
52
|
+
`ModuleNotFoundError` naming a submodule rather than the top-level package
|
|
53
|
+
(an old package missing a whole submodule this floor needs). Reading
|
|
54
|
+
either of those as "absent" is the defect: it is what let
|
|
55
|
+
`btclib-secp256k1` 0.8.0.6, which has no `btclib_secp256k1.ecdh.shared_point`,
|
|
56
|
+
answer `INSTALLED = False` instead of raising. So those two raise, which is
|
|
57
|
+
a loud failure at import time rather than a silent, slower fallback nobody
|
|
58
|
+
is told about.
|
|
59
|
+
|
|
60
|
+
What they raise is an `ImportError` that says what to do about it, chained
|
|
61
|
+
from the one the import system raised (btclib-org/ecc#56): the
|
|
62
|
+
installed `btclib-secp256k1` version, the floor this package declares for it,
|
|
63
|
+
and the two ways to a release that meets it. The floor is read back from this
|
|
64
|
+
distribution's own metadata, where `pyproject.toml`'s `secp256k1` extra put
|
|
65
|
+
it, so no second copy of it is kept here to drift; a source tree with no
|
|
66
|
+
metadata beside it is told to look in the extra. Only an `ImportError` naming
|
|
67
|
+
`btclib_secp256k1` or one of its submodules is taken for a bindings mismatch:
|
|
68
|
+
a dependency of the bindings that is missing, or a shared library that will
|
|
69
|
+
not load, propagates as it was raised.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
from __future__ import annotations
|
|
73
|
+
|
|
74
|
+
import os
|
|
75
|
+
import re
|
|
76
|
+
from hashlib import sha256
|
|
77
|
+
from importlib import metadata
|
|
78
|
+
|
|
79
|
+
from ecc import name as _distribution
|
|
80
|
+
from ecc.exceptions import EccRuntimeError
|
|
81
|
+
|
|
82
|
+
# the environment variable that refuses the bindings without uninstalling
|
|
83
|
+
# them, read once and here, when this module is first imported. `import
|
|
84
|
+
# ecc` alone does not import it, the root importing lazily; the first
|
|
85
|
+
# import of `ecc.curves`, of `ecc.ecc` or of a module under them does, so a
|
|
86
|
+
# caller settling the variable after that has settled it after this module
|
|
87
|
+
# answered. The package's name as its prefix, and "set to ask for it": an
|
|
88
|
+
# empty value is not set, so `ECC_NO_LIBSECP256K1=` leaves them
|
|
89
|
+
# serving
|
|
90
|
+
NO_LIBSECP256K1 = "ECC_NO_LIBSECP256K1"
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"ENABLED",
|
|
94
|
+
"INSTALLED",
|
|
95
|
+
"NO_LIBSECP256K1",
|
|
96
|
+
"PubkeyTweakChain",
|
|
97
|
+
"dsa",
|
|
98
|
+
"ellswift",
|
|
99
|
+
"ffi",
|
|
100
|
+
"keys",
|
|
101
|
+
"musig",
|
|
102
|
+
"pubkey_from_prvkey",
|
|
103
|
+
"pubkey_sum",
|
|
104
|
+
"pubkey_tweak_add",
|
|
105
|
+
"pubkey_tweak_mul_sum",
|
|
106
|
+
"recovery",
|
|
107
|
+
"shared_point",
|
|
108
|
+
"ssa",
|
|
109
|
+
"xonly_prvkey_tweak_add",
|
|
110
|
+
"xonly_pubkey_verify",
|
|
111
|
+
"xonly_to_pubkey",
|
|
112
|
+
"xonly_tweak_add",
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
_BINDINGS_MODULE = "btclib_secp256k1"
|
|
116
|
+
_BINDINGS_DISTRIBUTION = "btclib-secp256k1"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _floor() -> str | None:
|
|
120
|
+
"""Return the specifier the `secp256k1` extra puts on the bindings.
|
|
121
|
+
|
|
122
|
+
Read from this distribution's own metadata, which is where
|
|
123
|
+
`pyproject.toml` declares it, and `None` where there is no metadata to
|
|
124
|
+
read: a source tree that was never installed, or an installed one whose
|
|
125
|
+
`Requires-Dist` no longer lists the extra.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
requirements = metadata.requires(_distribution)
|
|
129
|
+
except metadata.PackageNotFoundError:
|
|
130
|
+
return None
|
|
131
|
+
for requirement in requirements or ():
|
|
132
|
+
head, _, marker = requirement.partition(";")
|
|
133
|
+
if not re.search(r"""extra\s*==\s*["']secp256k1["']""", marker):
|
|
134
|
+
continue
|
|
135
|
+
name = re.match(r"[A-Za-z0-9._-]+", head.strip())
|
|
136
|
+
if name is None:
|
|
137
|
+
continue
|
|
138
|
+
if re.sub(r"[-_.]+", "-", name.group()).lower() != _BINDINGS_DISTRIBUTION:
|
|
139
|
+
continue
|
|
140
|
+
specifier = head.strip()[name.end() :].strip()
|
|
141
|
+
return specifier or None
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _mismatch(exc: ImportError) -> ImportError | None:
|
|
146
|
+
"""Return the error to raise for a bindings mismatch, `None` for any other.
|
|
147
|
+
|
|
148
|
+
A mismatch is an `ImportError` that names the bindings or one of their
|
|
149
|
+
submodules: the package was found, and something this module asks of it
|
|
150
|
+
was not there. Anything else -- a dependency of the bindings that is
|
|
151
|
+
missing, a shared library that will not load -- is not this module's to
|
|
152
|
+
reword, and the caller re-raises it as it came.
|
|
153
|
+
"""
|
|
154
|
+
if exc.name != _BINDINGS_MODULE and not (exc.name or "").startswith(
|
|
155
|
+
_BINDINGS_MODULE + "."
|
|
156
|
+
):
|
|
157
|
+
return None
|
|
158
|
+
try:
|
|
159
|
+
installed = f"version {metadata.version(_BINDINGS_DISTRIBUTION)}"
|
|
160
|
+
except metadata.PackageNotFoundError:
|
|
161
|
+
installed = "a version it could not read"
|
|
162
|
+
floor = _floor()
|
|
163
|
+
required = (
|
|
164
|
+
f"{_BINDINGS_DISTRIBUTION}{floor}"
|
|
165
|
+
if floor
|
|
166
|
+
else f"the release the `secp256k1` extra of {_distribution} names"
|
|
167
|
+
)
|
|
168
|
+
return ImportError(
|
|
169
|
+
f"{_BINDINGS_DISTRIBUTION} is installed, {installed}, but it lacks what "
|
|
170
|
+
f"{_distribution} imports from it ({exc}); {_distribution} requires {required}. "
|
|
171
|
+
f"Upgrade it with `pip install --upgrade {_BINDINGS_DISTRIBUTION}` or "
|
|
172
|
+
f"install `{_distribution}[secp256k1]`",
|
|
173
|
+
name=exc.name,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# `ffi` is the one object here that is not a wrapped call: issue
|
|
179
|
+
# btclib-org/btclib#1009 is why it is needed at all -- `ecc.ecc.dsa.Signer`
|
|
180
|
+
# builds its own `unsigned char[32]` with it, to hold a private key in
|
|
181
|
+
# memory this package can overwrite, the way `ssa.Signer` already can
|
|
182
|
+
# through the keypair `ssa` itself builds. Nothing else here needs it,
|
|
183
|
+
# `dsa.sign` and every other wrapped call taking that buffer as the `prvkey`
|
|
184
|
+
# a caller may already hold (btclib-org/btclib-secp256k1#253)
|
|
185
|
+
from btclib_secp256k1 import (
|
|
186
|
+
dsa,
|
|
187
|
+
ellswift,
|
|
188
|
+
ffi,
|
|
189
|
+
keys,
|
|
190
|
+
musig,
|
|
191
|
+
recovery,
|
|
192
|
+
ssa,
|
|
193
|
+
)
|
|
194
|
+
from btclib_secp256k1.ecdh import shared_point
|
|
195
|
+
from btclib_secp256k1.keys import (
|
|
196
|
+
PubkeyTweakChain,
|
|
197
|
+
pubkey_from_prvkey,
|
|
198
|
+
pubkey_sum,
|
|
199
|
+
pubkey_tweak_add,
|
|
200
|
+
pubkey_tweak_mul_sum,
|
|
201
|
+
)
|
|
202
|
+
from btclib_secp256k1.xonly import prvkey_tweak_add as xonly_prvkey_tweak_add
|
|
203
|
+
from btclib_secp256k1.xonly import pubkey_verify as xonly_pubkey_verify
|
|
204
|
+
from btclib_secp256k1.xonly import to_pubkey as xonly_to_pubkey
|
|
205
|
+
from btclib_secp256k1.xonly import tweak_add as xonly_tweak_add
|
|
206
|
+
|
|
207
|
+
INSTALLED = True
|
|
208
|
+
# issue btclib-org/btclib#1002 measured this branch rather than assuming it
|
|
209
|
+
# stays transitional: `test.yml`'s `no-bindings` job executes it every run, and
|
|
210
|
+
# `coverage-union` combines that run's data with the `coverage` job's. The
|
|
211
|
+
# combined report is 100% with this pragma removed -- so the branch is reached
|
|
212
|
+
# and is not dead code -- and the pragma still belongs here regardless, because
|
|
213
|
+
# `coverage-union` is a second gate beside the `coverage` job's, not instead of
|
|
214
|
+
# it: that job's own report, `pytest --cov` on this configuration alone, has
|
|
215
|
+
# bindings installed by construction, a `ModuleNotFoundError` only reachable by
|
|
216
|
+
# actually removing them, and a subprocess that does
|
|
217
|
+
# (`tests/no_bindings_test.py`) whose coverage that job does not collect. So
|
|
218
|
+
# this branch is a structural miss in that report regardless of the union, and
|
|
219
|
+
# removing the pragma would fail the one gate this issue chose to leave
|
|
220
|
+
# unchanged
|
|
221
|
+
except (
|
|
222
|
+
ImportError
|
|
223
|
+
) as exc: # pragma: no cover -- only a child interpreter or no-bindings reaches this
|
|
224
|
+
# `exc.name` is the top-level package's own name, on a
|
|
225
|
+
# `ModuleNotFoundError`, only when the import system never found it at
|
|
226
|
+
# all. A submodule of an installed package failing to import names that
|
|
227
|
+
# submodule instead (`btclib_secp256k1.ecdh`, say), and a name missing
|
|
228
|
+
# from a module that *was* found is a plain `ImportError`. Either of
|
|
229
|
+
# those is an installed package too old for what this module asks of it
|
|
230
|
+
# (btclib-org/ecc#25), and the caller is told so and what to do
|
|
231
|
+
# (btclib-org/ecc#56) rather than silently handed the slower
|
|
232
|
+
# Python arithmetic
|
|
233
|
+
if not (isinstance(exc, ModuleNotFoundError) and exc.name == _BINDINGS_MODULE):
|
|
234
|
+
mismatch = _mismatch(exc)
|
|
235
|
+
if mismatch is None:
|
|
236
|
+
raise
|
|
237
|
+
raise mismatch from exc
|
|
238
|
+
# None and not a callable that raises: what would raise is never
|
|
239
|
+
# called, so the object would be a second thing to keep true. The
|
|
240
|
+
# ignore is on the assignment and not on the module: every other
|
|
241
|
+
# name here keeps the type the try branch gave it
|
|
242
|
+
dsa = ellswift = ffi = keys = musig = recovery = ssa = None # type: ignore[assignment]
|
|
243
|
+
shared_point = None # type: ignore[assignment]
|
|
244
|
+
# a class rather than a function, so mypy calls it an assignment to
|
|
245
|
+
# a type and wants the second code as well
|
|
246
|
+
PubkeyTweakChain = None # type: ignore[misc, assignment]
|
|
247
|
+
pubkey_from_prvkey = pubkey_sum = None # type: ignore[assignment]
|
|
248
|
+
pubkey_tweak_add = pubkey_tweak_mul_sum = None # type: ignore[assignment]
|
|
249
|
+
xonly_pubkey_verify = xonly_to_pubkey = None # type: ignore[assignment]
|
|
250
|
+
xonly_prvkey_tweak_add = xonly_tweak_add = None # type: ignore[assignment]
|
|
251
|
+
|
|
252
|
+
INSTALLED = False
|
|
253
|
+
|
|
254
|
+
# installed and not refused, which is one state and not two: a caller
|
|
255
|
+
# asking whether the bindings serve has no use for the difference, and
|
|
256
|
+
# `curves.curve` starts its seam from this
|
|
257
|
+
ENABLED = INSTALLED and not os.environ.get(NO_LIBSECP256K1)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _wrong_bindings(what: str, *, setter: bool) -> EccRuntimeError:
|
|
261
|
+
"""Return the error for a wrong answer of the bindings.
|
|
262
|
+
|
|
263
|
+
At import the variable is the way out; from the setter it is already
|
|
264
|
+
set, and not asking for the bindings is.
|
|
265
|
+
"""
|
|
266
|
+
pure = "keep `serving=False`" if setter else f"set {NO_LIBSECP256K1}"
|
|
267
|
+
return EccRuntimeError(
|
|
268
|
+
f"the libsecp256k1 bindings {what}; ecc cannot use them. "
|
|
269
|
+
f"Reinstall them with `pip install --force-reinstall --no-cache-dir "
|
|
270
|
+
f"{_BINDINGS_DISTRIBUTION}`, or {pure} to use the pure Python arithmetic"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _check_bindings(*, setter: bool) -> None:
|
|
275
|
+
"""Raise unless the bindings answer fixed vectors correctly.
|
|
276
|
+
|
|
277
|
+
A misbuilt or mismatched wheel could derive keys or verify signatures
|
|
278
|
+
wrongly with no error, so the bindings, when first selected, are asked a
|
|
279
|
+
public key, one ECDSA verification and its refusal over another message,
|
|
280
|
+
and one BIP340 verification. The answers are published, not taken from
|
|
281
|
+
the bindings. A `raise`, not an `assert`, so
|
|
282
|
+
that `python -O` keeps it.
|
|
283
|
+
"""
|
|
284
|
+
# the public key of the private key 1 is the generator, SEC 2 section 2.4.1
|
|
285
|
+
g = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
|
|
286
|
+
if pubkey_from_prvkey(1, True).hex() != g:
|
|
287
|
+
raise _wrong_bindings("derive a wrong public key", setter=setter)
|
|
288
|
+
|
|
289
|
+
# the first valid vector of the first group of Wycheproof 0.9rc5,
|
|
290
|
+
# tests/schemes/_data/ecdsa_secp256k1_sha256_bitcoin_test.json, with its key
|
|
291
|
+
# compressed and its signature as r || s, as
|
|
292
|
+
# `dsa.verify` passes it: `compact=True, normalize=True`
|
|
293
|
+
pub_key = bytes.fromhex(
|
|
294
|
+
"03b838ff44e5bc177bf21189d0766082fc9d843226887fc9760371100b7ee20a6f"
|
|
295
|
+
)
|
|
296
|
+
sig = bytes.fromhex(
|
|
297
|
+
"813ef79ccefa9a56f7ba805f0e478584fe5f0dd5f567bc09b5123ccbc9832365"
|
|
298
|
+
"6ff18a52dcc0336f7af62400a6dd9b810732baf1ff758000d6f613a556eb31ba"
|
|
299
|
+
)
|
|
300
|
+
msg_hash = sha256(bytes.fromhex("313233343030")).digest()
|
|
301
|
+
if not dsa.verify(msg_hash, pub_key, sig, normalize=True, compact=True):
|
|
302
|
+
raise _wrong_bindings("do not verify a known ECDSA signature", setter=setter)
|
|
303
|
+
if dsa.verify(
|
|
304
|
+
sha256(b"another message").digest(), pub_key, sig, normalize=True, compact=True
|
|
305
|
+
):
|
|
306
|
+
raise _wrong_bindings(
|
|
307
|
+
"verify a known ECDSA signature over another message", setter=setter
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# BIP340 test vector 0, tests/schemes/_data/bip340_test_vectors.csv
|
|
311
|
+
x_only = bytes.fromhex(
|
|
312
|
+
"F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"
|
|
313
|
+
)
|
|
314
|
+
sig = bytes.fromhex(
|
|
315
|
+
"E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA8215"
|
|
316
|
+
"25F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0"
|
|
317
|
+
)
|
|
318
|
+
if not ssa.verify(bytes(32), x_only, sig):
|
|
319
|
+
raise _wrong_bindings("do not verify a known BIP340 signature", setter=setter)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
_checked = False
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _check_once(*, setter: bool) -> None:
|
|
326
|
+
"""Run `_check_bindings` unless it has already passed."""
|
|
327
|
+
global _checked # noqa: PLW0603
|
|
328
|
+
if not _checked:
|
|
329
|
+
_check_bindings(setter=setter)
|
|
330
|
+
_checked = True
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# once, when the bindings are first selected: here, or in
|
|
334
|
+
# `curves.curve.set_libsecp256k1_serving`
|
|
335
|
+
if ENABLED:
|
|
336
|
+
_check_once(setter=False)
|