Compare commits

...

22 Commits

Author SHA1 Message Date
tecnovert ec7eb48a95
Update secp256k1 version. 5 months ago
tecnovert 654be83506
Add windows definitions. 1 year ago
tecnovert c5821d1402
Merge remote-tracking branch 'ofek/master' into anonswap 1 year ago
Ofek Lev a3c2fdb643 Update index.md 2 years ago
Ofek Lev ed638bd850
Update link (#121) 2 years ago
Ofek Lev 0121668d05 Switch to Ruff 2 years ago
Ofek Lev 1dd0675add update doc 2 years ago
Ofek Lev 063a313e4d release v18.0.0 2 years ago
Ofek Lev 367ee22857
updates (#118) 2 years ago
Antoine Poinsot a7284cd4d6
BIP340 x-only keys and Schnorr signatures (#116) 2 years ago
Antoine Poinsot a57bd24702
Upgrade libsecp256k1 (#115) 2 years ago
Antoine Poinsot 8a2c89dc3c
CI fixups (#117) 2 years ago
Weiliang Li f13612f377
Add __all__ (#113) 2 years ago
tecnovert baa97e7a21
Pin secp256k1 src to tag. 2 years ago
tecnovert d79f30b84e
Add COINCURVE_CHECK_SYSTEM_LIB to disable checking for system libsecp256 by default. 2 years ago
tecnovert 2c53babe78
Merge branch 'master' into anonswap 2 years ago
tecnovert 4bb26b7b35
Remove obsolete secp256k1 with-bignum option. 2 years ago
tecnovert 4374483b64
Expose more functions. 3 years ago
tecnovert 45ce99b6ab
Add verify_compact() 3 years ago
tecnovert 12e3621070
Expose verify point, allow specifying nonce for dleag_prove. 4 years ago
tecnovert 9fd55f7b32
ed25519_get_pubkey 4 years ago
tecnovert 92cce4287a
Add dleag and ecdsa otves modules. 4 years ago
  1. 2
      .github/scripts/build-windows-wheels.sh
  2. 10
      .github/workflows/build.yml
  3. 1
      .gitignore
  4. 35
      .travis/build_windows_wheels.sh
  5. 34
      Dockerfile_compile_dll
  6. 1
      README.md
  7. 8
      _cffi_build/build.py
  8. 51
      _cffi_build/secp256k1_dleag.h
  9. 32
      _cffi_build/secp256k1_ecdsaotves.h
  10. 19
      _cffi_build/secp256k1_ed25519.h
  11. 78
      _cffi_build/secp256k1_extrakeys.h
  12. 7
      _cffi_build/secp256k1_generator.h
  13. 51
      _cffi_build/secp256k1_schnorrsig.h
  14. 11
      coincurve/__init__.py
  15. 230
      coincurve/_windows_libsecp256k1.py
  16. 3
      coincurve/context.py
  17. 82
      coincurve/dleag.py
  18. 17
      coincurve/ecdsa.py
  19. 89
      coincurve/ecdsaotves.py
  20. 57
      coincurve/ed25519.py
  21. 158
      coincurve/keys.py
  22. 4
      coincurve/utils.py
  23. 13
      docs/api.md
  24. 6
      docs/history.md
  25. 1
      docs/index.md
  26. 2
      docs/install.md
  27. 2
      docs/users.md
  28. 43
      pyproject.toml
  29. 28
      setup.py
  30. 2
      setup_support.py
  31. 3
      tests/samples.py
  32. 12
      tests/test_dleag.py
  33. 33
      tests/test_ecdsaotves.py
  34. 61
      tests/test_keys.py
  35. 25
      tox.ini

2
.github/scripts/build-windows-wheels.sh

@ -3,7 +3,7 @@ set -ex
build_dll() {
./autogen.sh
./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic
./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-module-extrakeys --enable-module-schnorrsig --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic
make
}

10
.github/workflows/build.yml

@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true
env:
COINCURVE_UPSTREAM_REF: d8a246324650c3df8d54d133a8ac3c1b857a7a4e
COINCURVE_UPSTREAM_REF: ddf2b2910eb19032f8dd657c66735115ae24bfba
COINCURVE_IGNORE_SYSTEM_LIB: '1'
CIBW_BEFORE_ALL_MACOS: ./.github/scripts/install-macos-build-deps.sh
CIBW_ENVIRONMENT_PASS_LINUX: >
@ -80,7 +80,7 @@ jobs:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
uses: pypa/cibuildwheel@v2.11.2
- uses: actions/upload-artifact@v2
with:
@ -98,7 +98,7 @@ jobs:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
uses: pypa/cibuildwheel@v2.11.2
env:
CIBW_ARCHS_MACOS: x86_64
@ -118,7 +118,7 @@ jobs:
- uses: actions/checkout@v2
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
uses: pypa/cibuildwheel@v2.11.2
env:
CIBW_ARCHS_MACOS: arm64
COINCURVE_CROSS_HOST: aarch64-apple-darwin
@ -173,7 +173,7 @@ jobs:
platforms: arm64
- name: Build wheels
uses: pypa/cibuildwheel@v2.3.1
uses: pypa/cibuildwheel@v2.11.2
env:
CIBW_ARCHS_LINUX: aarch64

1
.gitignore

@ -15,3 +15,4 @@
/coincurve/_libsecp256k1.py
/coincurve/libsecp256k1.dll
libsecp256k1

35
.travis/build_windows_wheels.sh

@ -0,0 +1,35 @@
#!/bin/bash
set -e -x
build_dll() {
./autogen.sh
echo "LDFLAGS = -no-undefined" >> Makefile.am
./configure --host=$1 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-endomorphism --disable-jni --enable-module-ed25519 --enable-module-generator --enable-module-dleag --enable-module-ecdsaotves
make
}
cd ..
#git clone https://github.com/bitcoin-core/secp256k1.git
#mv secp256k1 64bit
wget -O secp256k1_anonswap.zip https://github.com/tecnovert/secp256k1/archive/refs/tags/anonswap_v0.2.zip
unzip secp256k1_anonswap.zip
mv secp256k1-anonswap_v0.2 64bit
cp 64bit 32bit -R
cd 64bit
build_dll x86_64-w64-mingw32
mv .libs/libsecp256k1-0.dll ../clean/coincurve/libsecp256k1.dll
cd ../clean
python setup.py bdist_wheel --universal --plat-name=win_amd64
rm coincurve/libsecp256k1.dll
cd ../32bit
build_dll i686-w64-mingw32
mv .libs/libsecp256k1-0.dll ../clean/coincurve/libsecp256k1.dll
cd ../clean
python setup.py bdist_wheel --universal --plat-name=win32
mv dist/* ../coincurve/dist/
cd ../coincurve

34
Dockerfile_compile_dll

@ -0,0 +1,34 @@
FROM ubuntu:bionic as build
# Install required system packages
RUN apt-get update && apt-get install -y \
build-essential \
mingw-w64 \
autoconf \
libtool \
wget \
unzip
ARG UNAME=user
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID -o $UNAME
RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME
USER $UNAME
WORKDIR /home/user
RUN wget -O secp256k1_anonswap.zip https://github.com/tecnovert/secp256k1/archive/refs/tags/anonswap_v0.2.zip && \
unzip secp256k1_anonswap.zip && \
mv secp256k1-anonswap_v0.2 64bit
RUN cd 64bit && \
./autogen.sh && \
echo "LDFLAGS = -no-undefined" >> Makefile.am && \
./configure --host=x86_64-w64-mingw32 --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-module-extrakeys --enable-module-schnorrsig --enable-benchmark=no --enable-tests=no --enable-openssl-tests=no --enable-exhaustive-tests=no --enable-static --disable-dependency-tracking --with-pic --enable-module-ed25519 --enable-module-generator --enable-module-dleag --enable-module-ecdsaotves && \
make && \
mv .libs/libsecp256k1-0.dll .
FROM scratch as artifact
COPY --from=build /home/user/64bit/libsecp256k1-0.dll /coincurve/libsecp256k1.dll

1
README.md

@ -18,7 +18,6 @@ Feel free to read the [documentation](https://ofek.dev/coincurve/)!
- [Ethereum](https://ethereum.org)
- [LBRY](https://lbry.com)
- [ZeroNet](https://zeronet.io)
- [libp2p](https://libp2p.io)
and [many more](https://ofek.dev/coincurve/users/)!

8
_cffi_build/build.py

@ -13,7 +13,7 @@ def _mk_ffi(sources, name='_libsecp256k1', **kwargs):
code = []
for source in sources:
with open(os.path.join(here, source.h), 'rt') as h:
with open(os.path.join(here, source.h)) as h:
_ffi.cdef(h.read())
code.append(source.include)
@ -26,7 +26,13 @@ def _mk_ffi(sources, name='_libsecp256k1', **kwargs):
modules = [
Source('secp256k1.h', '#include <secp256k1.h>'),
Source('secp256k1_ecdh.h', '#include <secp256k1_ecdh.h>'),
Source('secp256k1_extrakeys.h', '#include <secp256k1_extrakeys.h>'),
Source('secp256k1_recovery.h', '#include <secp256k1_recovery.h>'),
Source('secp256k1_schnorrsig.h', '#include <secp256k1_schnorrsig.h>'),
Source('secp256k1_generator.h', '#include <secp256k1_generator.h>'),
Source('secp256k1_ed25519.h', '#include <secp256k1_ed25519.h>'),
Source('secp256k1_dleag.h', '#include <secp256k1_dleag.h>'),
Source('secp256k1_ecdsaotves.h', '#include <secp256k1_ecdsaotves.h>'),
]
ffi = _mk_ffi(modules, libraries=['secp256k1'])

51
_cffi_build/secp256k1_dleag.h

@ -0,0 +1,51 @@
size_t secp256k1_dleag_size(size_t n_bits);
int secp256k1_dleag_prove(
const secp256k1_context *ctx,
unsigned char *proof_out,
size_t *proof_len, /* Input length of proof_out buffer, output length of proof. */
const unsigned char *key, /* 32 bytes */
size_t n_bits,
const unsigned char *nonce, /* 32 bytes */
const secp256k1_generator *gen_s_a,
const secp256k1_generator *gen_s_b,
const unsigned char *gen_e_a,
const unsigned char *gen_e_b
);
int secp256k1_dleag_verify(
const secp256k1_context *ctx,
const unsigned char *proof,
size_t proof_len,
const secp256k1_generator *gen_s_a,
const secp256k1_generator *gen_s_b,
const unsigned char *gen_e_a,
const unsigned char *gen_e_b
);
int secp256k1_dleag_verify_secp256k1_point(
const secp256k1_context *ctx,
const unsigned char *p
);
int secp256k1_dleag_verify_ed25519_point(
const secp256k1_context *ctx,
const unsigned char *p
);
int secp256k1_ed25519_dleag_prove(
const secp256k1_context *ctx,
unsigned char *proof_out,
size_t *proof_len, /* Input length of proof_out buffer, output length of proof. */
const unsigned char *key, /* 32 bytes */
size_t n_bits,
const unsigned char *nonce /* 32 bytes */
);
int secp256k1_ed25519_dleag_verify(
const secp256k1_context *ctx,
const unsigned char *proof,
size_t proof_len
);

32
_cffi_build/secp256k1_ecdsaotves.h

@ -0,0 +1,32 @@
int ecdsaotves_enc_sign(
const secp256k1_context *ctx,
unsigned char *ct_out,
const unsigned char *skS,
const unsigned char *pkE,
const unsigned char *msg32
);
int ecdsaotves_enc_verify(
const secp256k1_context *ctx,
const unsigned char *pkS,
const unsigned char *pkE,
const unsigned char *msg32,
const unsigned char *ct
);
int ecdsaotves_dec_sig(
const secp256k1_context *ctx,
unsigned char *sig_out,
size_t *sig_length,
const unsigned char *skE,
const unsigned char *ct
);
int ecdsaotves_rec_enc_key(
const secp256k1_context *ctx,
unsigned char *key_out,
const unsigned char *pkE,
const unsigned char *ct,
const unsigned char *dersig,
size_t sig_length
);

19
_cffi_build/secp256k1_ed25519.h

@ -0,0 +1,19 @@
extern const unsigned char ed25519_gen[32];
extern const unsigned char ed25519_gen2[32];
int crypto_scalarmult_ed25519_base_noclamp(
unsigned char *q,
const unsigned char *n
);
int crypto_core_ed25519_add(
unsigned char *r,
const unsigned char *p,
const unsigned char *q
);
void crypto_core_ed25519_scalar_add(
unsigned char *z,
const unsigned char *x,
const unsigned char *y
);

78
_cffi_build/secp256k1_extrakeys.h

@ -0,0 +1,78 @@
typedef struct {
unsigned char data[64];
} secp256k1_xonly_pubkey;
typedef struct {
unsigned char data[96];
} secp256k1_keypair;
int secp256k1_xonly_pubkey_parse(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey* pubkey,
const unsigned char *input32
);
int secp256k1_xonly_pubkey_serialize(
const secp256k1_context* ctx,
unsigned char *output32,
const secp256k1_xonly_pubkey* pubkey
);
int secp256k1_xonly_pubkey_cmp(
const secp256k1_context* ctx,
const secp256k1_xonly_pubkey* pk1,
const secp256k1_xonly_pubkey* pk2
);
int secp256k1_xonly_pubkey_from_pubkey(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *xonly_pubkey,
int *pk_parity,
const secp256k1_pubkey *pubkey
);
int secp256k1_xonly_pubkey_tweak_add(
const secp256k1_context* ctx,
secp256k1_pubkey *output_pubkey,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_xonly_pubkey_tweak_add_check(
const secp256k1_context* ctx,
const unsigned char *tweaked_pubkey32,
int tweaked_pk_parity,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_keypair_create(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *seckey
);
int secp256k1_keypair_sec(
const secp256k1_context* ctx,
unsigned char *seckey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_pub(
const secp256k1_context* ctx,
secp256k1_pubkey *pubkey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_pub(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *pubkey,
int *pk_parity,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_tweak_add(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *tweak32
);

7
_cffi_build/secp256k1_generator.h

@ -0,0 +1,7 @@
typedef struct {
unsigned char data[64];
} secp256k1_generator;
extern const secp256k1_generator secp256k1_generator_const_g;
extern const secp256k1_generator secp256k1_generator_const_h;

51
_cffi_build/secp256k1_schnorrsig.h

@ -0,0 +1,51 @@
typedef int (*secp256k1_nonce_function_hardened)(
unsigned char *nonce32,
const unsigned char *msg,
size_t msglen,
const unsigned char *key32,
const unsigned char *xonly_pk32,
const unsigned char *algo,
size_t algolen,
void *data
);
extern const secp256k1_nonce_function_hardened secp256k1_nonce_function_bip340;
typedef struct {
unsigned char magic[4];
secp256k1_nonce_function_hardened noncefp;
void* ndata;
} secp256k1_schnorrsig_extraparams;
int secp256k1_schnorrsig_sign(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign_custom(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_keypair *keypair,
secp256k1_schnorrsig_extraparams *extraparams
);
int secp256k1_schnorrsig_verify(
const secp256k1_context* ctx,
const unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_xonly_pubkey *pubkey
);

11
coincurve/__init__.py

@ -1,3 +1,12 @@
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.keys import PrivateKey, PublicKey
from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import verify_signature
__all__ = [
'GLOBAL_CONTEXT',
'Context',
'PrivateKey',
'PublicKey',
'PublicKeyXOnly',
'verify_signature',
]

230
coincurve/_windows_libsecp256k1.py

@ -176,6 +176,87 @@ int secp256k1_ec_pubkey_combine(
);
"""
EXTRAKEYS_DEFINITIONS = """
typedef struct {
unsigned char data[64];
} secp256k1_xonly_pubkey;
typedef struct {
unsigned char data[96];
} secp256k1_keypair;
int secp256k1_xonly_pubkey_parse(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey* pubkey,
const unsigned char *input32
);
int secp256k1_xonly_pubkey_serialize(
const secp256k1_context* ctx,
unsigned char *output32,
const secp256k1_xonly_pubkey* pubkey
);
int secp256k1_xonly_pubkey_cmp(
const secp256k1_context* ctx,
const secp256k1_xonly_pubkey* pk1,
const secp256k1_xonly_pubkey* pk2
);
int secp256k1_xonly_pubkey_from_pubkey(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *xonly_pubkey,
int *pk_parity,
const secp256k1_pubkey *pubkey
);
int secp256k1_xonly_pubkey_tweak_add(
const secp256k1_context* ctx,
secp256k1_pubkey *output_pubkey,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_xonly_pubkey_tweak_add_check(
const secp256k1_context* ctx,
const unsigned char *tweaked_pubkey32,
int tweaked_pk_parity,
const secp256k1_xonly_pubkey *internal_pubkey,
const unsigned char *tweak32
);
int secp256k1_keypair_create(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *seckey
);
int secp256k1_keypair_sec(
const secp256k1_context* ctx,
unsigned char *seckey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_pub(
const secp256k1_context* ctx,
secp256k1_pubkey *pubkey,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_pub(
const secp256k1_context* ctx,
secp256k1_xonly_pubkey *pubkey,
int *pk_parity,
const secp256k1_keypair *keypair
);
int secp256k1_keypair_xonly_tweak_add(
const secp256k1_context* ctx,
secp256k1_keypair *keypair,
const unsigned char *tweak32
);
"""
RECOVERY_DEFINITIONS = """
typedef struct {
unsigned char data[65];
@ -218,6 +299,60 @@ int secp256k1_ecdsa_recover(
);
"""
SCHNORRSIG_DEFINITIONS = """
typedef int (*secp256k1_nonce_function_hardened)(
unsigned char *nonce32,
const unsigned char *msg,
size_t msglen,
const unsigned char *key32,
const unsigned char *xonly_pk32,
const unsigned char *algo,
size_t algolen,
void *data
);
extern const secp256k1_nonce_function_hardened secp256k1_nonce_function_bip340;
typedef struct {
unsigned char magic[4];
secp256k1_nonce_function_hardened noncefp;
void* ndata;
} secp256k1_schnorrsig_extraparams;
int secp256k1_schnorrsig_sign(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign32(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg32,
const secp256k1_keypair *keypair,
const unsigned char *aux_rand32
);
int secp256k1_schnorrsig_sign_custom(
const secp256k1_context* ctx,
unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_keypair *keypair,
secp256k1_schnorrsig_extraparams *extraparams
);
int secp256k1_schnorrsig_verify(
const secp256k1_context* ctx,
const unsigned char *sig64,
const unsigned char *msg,
size_t msglen,
const secp256k1_xonly_pubkey *pubkey
);
"""
ECDH_DEFINITIONS = """
int secp256k1_ecdh(
const secp256k1_context* ctx,
@ -229,11 +364,106 @@ int secp256k1_ecdh(
);
"""
DLEAG_DEFINITIONS = """
typedef struct {
unsigned char data[64];
} secp256k1_generator;
size_t secp256k1_dleag_size(size_t n_bits);
int secp256k1_ed25519_dleag_prove(
const secp256k1_context *ctx,
unsigned char *proof_out,
size_t *proof_len, /* Input length of proof_out buffer, output length of proof. */
const unsigned char *key, /* 32 bytes */
size_t n_bits,
const unsigned char *nonce /* 32 bytes */
);
int secp256k1_ed25519_dleag_verify(
const secp256k1_context *ctx,
const unsigned char *proof,
size_t proof_len
);
int secp256k1_dleag_verify_secp256k1_point(
const secp256k1_context *ctx,
const unsigned char *p
);
int secp256k1_dleag_verify_ed25519_point(
const secp256k1_context *ctx,
const unsigned char *p
);
"""
ECDSA_OTVES_DEFINITIONS = """
int ecdsaotves_enc_sign(
const secp256k1_context *ctx,
unsigned char *ct_out,
const unsigned char *skS,
const unsigned char *pkE,
const unsigned char *msg32
);
int ecdsaotves_enc_verify(
const secp256k1_context *ctx,
const unsigned char *pkS,
const unsigned char *pkE,
const unsigned char *msg32,
const unsigned char *ct
);
int ecdsaotves_dec_sig(
const secp256k1_context *ctx,
unsigned char *sig_out,
size_t *sig_length,
const unsigned char *skE,
const unsigned char *ct
);
int ecdsaotves_rec_enc_key(
const secp256k1_context *ctx,
unsigned char *key_out,
const unsigned char *pkE,
const unsigned char *ct,
const unsigned char *dersig,
size_t sig_length
);
"""
ED25519_DEFINITIONS = """
extern const unsigned char ed25519_gen[32];
extern const unsigned char ed25519_gen2[32];
int crypto_scalarmult_ed25519_base_noclamp(
unsigned char *q,
const unsigned char *n
);
int crypto_core_ed25519_add(
unsigned char *r,
const unsigned char *p,
const unsigned char *q
);
void crypto_core_ed25519_scalar_add(
unsigned char *z,
const unsigned char *x,
const unsigned char *y
);
"""
ffi = FFI()
ffi.cdef(BASE_DEFINITIONS)
ffi.cdef(EXTRAKEYS_DEFINITIONS)
ffi.cdef(RECOVERY_DEFINITIONS)
ffi.cdef(SCHNORRSIG_DEFINITIONS)
ffi.cdef(ECDH_DEFINITIONS)
ffi.cdef(DLEAG_DEFINITIONS)
ffi.cdef(ECDSA_OTVES_DEFINITIONS)
ffi.cdef(ED25519_DEFINITIONS)
here = os.path.dirname(os.path.abspath(__file__))
lib = ffi.dlopen(os.path.join(here, 'libsecp256k1.dll'))

3
coincurve/context.py

@ -24,7 +24,8 @@ class Context:
with self._lock:
seed = urandom(32) if not seed or len(seed) != 32 else seed
res = lib.secp256k1_context_randomize(self.ctx, ffi.new('unsigned char [32]', seed))
assert res == 1
if not res:
raise ValueError('secp256k1_context_randomize')
def __repr__(self):
return self.name or super().__repr__()

82
coincurve/dleag.py

@ -0,0 +1,82 @@
from coincurve.context import GLOBAL_CONTEXT
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from ._libsecp256k1 import ffi, lib
def dleag_proof_len(bits=252):
return lib.secp256k1_dleag_size(bits)
def get_nonce():
try:
import secrets
return secrets.token_bytes(32)
except Exception:
from os import urandom
return urandom(32)
def dleag_prove(private_key, nonce_bytes=None, context=GLOBAL_CONTEXT):
proof_length = dleag_proof_len()
proof_output = ffi.new('unsigned char[{}]'.format(proof_length))
proof_length_p = ffi.new('size_t *')
proof_length_p[0] = proof_length
# nonce_bytes = ffi.from_buffer(secrets.token_bytes(32))
if nonce_bytes is None:
nonce_bytes = get_nonce()
rv = lib.secp256k1_ed25519_dleag_prove(
context.ctx,
proof_output,
proof_length_p,
private_key.secret,
252,
nonce_bytes,
)
if rv != 1:
raise ValueError('secp256k1_ed25519_dleag_prove failed')
# TODO: How to clear memory? Add random module to secp256k1?
# ffi.memmove(nonce_bytes, bytes([0] * 32), 32)
return bytes(ffi.buffer(proof_output, proof_length))
def dleag_verify(proof, context=GLOBAL_CONTEXT):
proof_bytes = ffi.from_buffer(proof)
proof_length = len(proof)
rv = lib.secp256k1_ed25519_dleag_verify(
context.ctx,
proof_bytes,
proof_length,
)
return True if rv == 1 else False
def verify_secp256k1_point(pubkey_bytes, context=GLOBAL_CONTEXT):
if len(pubkey_bytes) != 33:
raise ValueError('Invalid pubkey length')
rv = lib.secp256k1_dleag_verify_secp256k1_point(
context.ctx,
pubkey_bytes
)
return True if rv == 1 else False
def verify_ed25519_point(pubkey_bytes, context=GLOBAL_CONTEXT):
if len(pubkey_bytes) != 32:
raise ValueError('Invalid pubkey length')
rv = lib.secp256k1_dleag_verify_ed25519_point(
context.ctx,
pubkey_bytes
)
return True if rv == 1 else False

17
coincurve/ecdsa.py

@ -27,6 +27,17 @@ def der_to_cdata(der: bytes, context: Context = GLOBAL_CONTEXT):
return cdata
def parse_compact(data, context=GLOBAL_CONTEXT):
cdata = ffi.new('secp256k1_ecdsa_signature *')
assert(len(data) == 64)
parsed = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, cdata, data)
if not parsed:
raise ValueError('The DER-encoded signature could not be parsed.')
return cdata
def recover(message: bytes, recover_sig, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT):
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32:
@ -76,7 +87,8 @@ def serialize_compact(raw_sig, context: Context = GLOBAL_CONTEXT): # no cov
output = ffi.new('unsigned char[%d]' % CDATA_SIG_LENGTH)
res = lib.secp256k1_ecdsa_signature_serialize_compact(context.ctx, output, raw_sig)
assert res == 1
if not res:
raise ValueError('secp256k1_ecdsa_signature_serialize_compact')
return bytes(ffi.buffer(output, CDATA_SIG_LENGTH))
@ -87,7 +99,8 @@ def deserialize_compact(ser_sig: bytes, context: Context = GLOBAL_CONTEXT): # n
raw_sig = ffi.new('secp256k1_ecdsa_signature *')
res = lib.secp256k1_ecdsa_signature_parse_compact(context.ctx, raw_sig, ser_sig)
assert res == 1
if not res:
raise ValueError('secp256k1_ecdsa_signature_parse_compact')
return raw_sig

89
coincurve/ecdsaotves.py

@ -0,0 +1,89 @@
from coincurve.context import GLOBAL_CONTEXT
from coincurve.utils import bytes_to_int, int_to_bytes, sha256
from ._libsecp256k1 import ffi, lib
def ecdsaotves_enc_sign(private_key_sign, public_key_encrypt, msg, context=GLOBAL_CONTEXT):
ct_length = 196
ct_output = ffi.new('unsigned char[{}]'.format(ct_length))
if len(private_key_sign) != 32:
raise ValueError('private_key_sign must be 32 bytes')
if len(public_key_encrypt) != 33:
raise ValueError('public_key_encrypt must be 33 bytes')
if len(msg) != 32:
raise ValueError('msg must be 32 bytes')
rv = lib.ecdsaotves_enc_sign(
context.ctx,
ct_output,
private_key_sign,
public_key_encrypt,
msg,
)
if rv != 1:
raise ValueError('ecdsaotves_enc_sign failed')
return bytes(ffi.buffer(ct_output, ct_length))
def ecdsaotves_enc_verify(public_key_sign, public_key_encrypt, msg, ct, context=GLOBAL_CONTEXT):
if len(public_key_sign) != 33:
raise ValueError('public_key_sign must be 33 bytes')
if len(public_key_encrypt) != 33:
raise ValueError('public_key_encrypt must be 33 bytes')
if len(msg) != 32:
raise ValueError('msg must be 32 bytes')
if len(ct) != 196:
raise ValueError('ciphertext must be 196 bytes')
rv = lib.ecdsaotves_enc_verify(
context.ctx,
public_key_sign,
public_key_encrypt,
msg,
ct,
)
return True if rv == 1 else False
def ecdsaotves_dec_sig(private_key_encrypt, ct, context=GLOBAL_CONTEXT):
if len(private_key_encrypt) != 32:
raise ValueError('private_key_encrypt must be 32 bytes')
if len(ct) != 196:
raise ValueError('ciphertext must be 196 bytes')
output_length = ffi.new('size_t *')
output_length[0] = 100
sig_output = ffi.new('unsigned char[{}]'.format(100))
rv = lib.ecdsaotves_dec_sig(
context.ctx,
sig_output,
output_length,
private_key_encrypt,
ct,
)
if rv != 1:
raise ValueError('ecdsaotves_dec_sig failed')
return bytes(ffi.buffer(sig_output, output_length[0]))
def ecdsaotves_rec_enc_key(public_key_encrypt, ct, sig_der, context=GLOBAL_CONTEXT):
if len(public_key_encrypt) != 33:
raise ValueError('public_key_encrypt must be 33 bytes')
if len(ct) != 196:
raise ValueError('ciphertext must be 196 bytes')
key_output = ffi.new('unsigned char[{}]'.format(32))
sig_length = len(sig_der)
rv = lib.ecdsaotves_rec_enc_key(context.ctx, key_output, public_key_encrypt, ct, sig_der, sig_length)
if rv != 1:
raise ValueError('ecdsaotves_rec_enc_key failed')
return bytes(ffi.buffer(key_output, 32))

57
coincurve/ed25519.py

@ -0,0 +1,57 @@
from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm, PrivateKeyInfo
from coincurve.context import GLOBAL_CONTEXT
from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from coincurve.utils import bytes_to_int, int_to_bytes_padded
from ._libsecp256k1 import ffi, lib
GROUP_ORDER_INT = 2 ** 252 + 27742317777372353535851937790883648493
def get_valid_secret():
try:
import secrets
return int_to_bytes_padded(9 + secrets.randbelow(GROUP_ORDER_INT - 9))
except Exception:
from os import urandom
while True:
secret = urandom(32)
if 9 < bytes_to_int(secret) < GROUP_ORDER_INT:
return secret
def ed25519_get_pubkey(privkey):
pubkey_output = ffi.new('unsigned char[{}]'.format(32))
privkey_le = privkey[::-1]
rv = lib.crypto_scalarmult_ed25519_base_noclamp(pubkey_output, privkey_le)
assert(rv == 0)
return bytes(ffi.buffer(pubkey_output, 32))
def ed25519_scalar_add(x, y):
output = ffi.new('unsigned char[{}]'.format(32))
x_le = x[::-1]
y_le = y[::-1]
lib.crypto_core_ed25519_scalar_add(output, x_le, y_le)
return bytes(ffi.buffer(output, 32))[::-1]
def ed25519_add(x, y):
output = ffi.new('unsigned char[{}]'.format(32))
rv = lib.crypto_core_ed25519_add(output, x, y)
assert(rv == 0)
return bytes(ffi.buffer(output, 32))
class Ed25519PrivateKey:
def __init__(self, secret=None, context=GLOBAL_CONTEXT):
self.context = context
class Ed25519PublicKey:
def __init__(self, data, context=GLOBAL_CONTEXT):
self.context = context

158
coincurve/keys.py

@ -1,9 +1,10 @@
import os
from typing import Tuple
from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, PrivateKeyAlgorithm, PrivateKeyInfo
from coincurve.context import GLOBAL_CONTEXT, Context
from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
from coincurve.ecdsa import parse_compact, cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable
from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED
from coincurve.types import Hasher, Nonce
from coincurve.utils import (
@ -31,6 +32,7 @@ class PrivateKey:
self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret()
self.context = context
self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context)
self.public_key_xonly: PublicKeyXOnly = PublicKeyXOnly.from_valid_secret(self.secret, self.context)
def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
@ -59,6 +61,43 @@ class PrivateKey:
return cdata_to_der(signature, self.context)
def sign_schnorr(self, message: bytes, aux_randomness: bytes = b'') -> bytes:
"""Create a Schnorr signature.
:param message: The message to sign.
:param aux_randomness: An optional 32 bytes of fresh randomness. By default (empty bytestring), this
will be generated automatically. Set to `None` to disable this behavior.
:return: The Schnorr signature.
:raises ValueError: If the message was not 32 bytes long, the optional auxiliary random data was not
32 bytes long, signing failed, or the signature was invalid.
"""
if len(message) != 32:
raise ValueError('Message must be 32 bytes long.')
elif aux_randomness == b'':
aux_randomness = os.urandom(32)
elif aux_randomness is None:
aux_randomness = ffi.NULL
elif len(aux_randomness) != 32:
raise ValueError('Auxiliary random data must be 32 bytes long.')
keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(self.context.ctx, keypair, self.secret)
if not res:
raise ValueError('Secret was invalid')
signature = ffi.new('unsigned char[64]')
res = lib.secp256k1_schnorrsig_sign32(self.context.ctx, signature, message, keypair, aux_randomness)
if not res:
raise ValueError('Signing failed')
res = lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key_xonly.public_key
)
if not res:
raise ValueError('Invalid signature')
return bytes(ffi.buffer(signature))
def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes:
"""
Create a recoverable ECDSA signature.
@ -368,7 +407,7 @@ class PublicKey:
return PublicKey(public_key, context)
def format(self, compressed: bool = True) -> bytes:
def format(self, compressed: bool = True) -> bytes: # noqa: A003
"""
Format the public key.
@ -410,6 +449,16 @@ class PublicKey:
# A performance hack to avoid global bool() lookup.
return not not verified
def verify_compact(self, signature, message, hasher=sha256):
msg_hash = hasher(message) if hasher is not None else message
if len(msg_hash) != 32:
raise ValueError('Message hash must be 32 bytes long.')
verified = lib.secp256k1_ecdsa_verify(self.context.ctx, parse_compact(signature), msg_hash, self.public_key)
# A performance hack to avoid global bool() lookup.
return not not verified
def add(self, scalar: bytes, update: bool = False):
"""
Add a scalar to the public key.
@ -484,3 +533,108 @@ class PublicKey:
def __eq__(self, other) -> bool:
return self.format(compressed=False) == other.format(compressed=False)
class PublicKeyXOnly:
def __init__(self, data, parity: bool = False, context: Context = GLOBAL_CONTEXT):
"""A BIP340 `x-only` public key.
:param data: The formatted public key.
:type data: bytes
:param parity: Whether the encoded point is the negation of the public key.
:param context:
"""
if not isinstance(data, bytes):
self.public_key = data
else:
public_key = ffi.new('secp256k1_xonly_pubkey *')
parsed = lib.secp256k1_xonly_pubkey_parse(context.ctx, public_key, data)
if not parsed:
raise ValueError('The public key could not be parsed or is invalid.')
self.public_key = public_key
self.parity = parity
self.context = context
@classmethod
def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
"""Derive an x-only public key from a private key secret.
:param secret: The private key secret.
:param context:
:return: The x-only public key.
"""
keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(context.ctx, keypair, validate_secret(secret))
if not res:
raise ValueError('Secret was invalid')
xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *')
pk_parity = ffi.new('int *')
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
return cls(xonly_pubkey, parity=not not pk_parity[0], context=context)
@classmethod
def from_valid_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT):
keypair = ffi.new('secp256k1_keypair *')
res = lib.secp256k1_keypair_create(context.ctx, keypair, secret)
if not res:
raise ValueError('Secret was invalid')
xonly_pubkey = ffi.new('secp256k1_xonly_pubkey *')
pk_parity = ffi.new('int *')
res = lib.secp256k1_keypair_xonly_pub(context.ctx, xonly_pubkey, pk_parity, keypair)
return cls(xonly_pubkey, parity=not not pk_parity[0], context=context)
def format(self) -> bytes: # noqa: A003
"""Serialize the public key.
:return: The public key serialized as 32 bytes.
"""
output32 = ffi.new('unsigned char [32]')
res = lib.secp256k1_xonly_pubkey_serialize(self.context.ctx, output32, self.public_key)
if not res:
raise ValueError('Public key in self.public_key must be valid')
return bytes(ffi.buffer(output32, 32))
def verify(self, signature: bytes, message: bytes) -> bool:
"""Verify a Schnorr signature over a given message.
:param signature: The 64-byte Schnorr signature to verify.
:param message: The message to be verified.
:return: A boolean indicating whether or not the signature is correct.
"""
if len(signature) != 64:
raise ValueError('Signature must be 32 bytes long.')
return not not lib.secp256k1_schnorrsig_verify(
self.context.ctx, signature, message, len(message), self.public_key
)
def tweak_add(self, scalar: bytes):
"""Add a scalar to the public key.
:param scalar: The scalar with which to add.
:return: The modified public key.
:rtype: PublicKeyXOnly
:raises ValueError: If the tweak was out of range or the resulting public key was invalid.
"""
scalar = pad_scalar(scalar)
out_pubkey = ffi.new('secp256k1_pubkey *')
res = lib.secp256k1_xonly_pubkey_tweak_add(self.context.ctx, out_pubkey, self.public_key, scalar)
if not res:
raise ValueError('The tweak was out of range, or the resulting public key would be invalid')
pk_parity = ffi.new('int *')
lib.secp256k1_xonly_pubkey_from_pubkey(self.context.ctx, self.public_key, pk_parity, out_pubkey)
self.parity = not not pk_parity[0]
def __eq__(self, other) -> bool:
res = lib.secp256k1_xonly_pubkey_cmp(self.context.ctx, self.public_key, other.public_key)
return res == 0

4
coincurve/utils.py

@ -27,11 +27,11 @@ if environ.get('COINCURVE_BUILDING_DOCS') != 'true':
else: # no cov
class __Nonce(tuple):
class __Nonce(tuple): # noqa: N801
def __repr__(self):
return '(ffi.NULL, ffi.NULL)'
class __HasherSHA256:
class __HasherSHA256: # noqa: N801
def __call__(self, bytestr: bytes) -> bytes:
return _sha256(bytestr).digest()

13
docs/api.md

@ -19,6 +19,7 @@ All objects are available directly under the root namespace `coincurve`.
- __init__
- sign
- sign_recoverable
- sign_schnorr
- ecdh
- add
- multiply
@ -48,3 +49,15 @@ All objects are available directly under the root namespace `coincurve`.
- from_signature_and_message
- from_secret
- from_point
::: coincurve.PublicKeyXOnly
rendering:
show_root_full_path: false
selection:
docstring_style: restructured-text
members:
- __init__
- verify
- format
- tweak_add
- from_secret

6
docs/history.md

@ -8,6 +8,12 @@ Important changes are emphasized.
## Unreleased
## 18.0.0
- Support Schnorr signatures
- Add support for Python 3.11
- Upgrade [libsecp256k1][] to the latest available version
## 17.0.0
- **Breaking:** Drop support for Python 3.6

1
docs/index.md

@ -26,7 +26,6 @@ C library used by [Bitcoin Core][] for operations on the elliptic curve [secp256
- [Ethereum](https://ethereum.org)
- [LBRY](https://lbry.com)
- [ZeroNet](https://zeronet.io)
- [libp2p](https://libp2p.io)
and [many more](users.md)!

2
docs/install.md

@ -19,6 +19,7 @@ Binary wheels are available for most platforms and require at least version `19.
| CPython 3.8 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.9 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.10 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
| CPython 3.11 | <ul><li>x86_64</li><li>ARM64</li></ul> | <ul><li>x86_64</li><li>x86</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> | <ul><li>x86_64</li><li>i686</li><li>AArch64</li></ul> |
## Source
@ -28,6 +29,7 @@ A few environment variables influence the build:
- `COINCURVE_UPSTREAM_REF` - This is the Git reference of [libsecp256k1][] to use rather than the (frequently updated) default.
- `COINCURVE_IGNORE_SYSTEM_LIB` - The presence of this will force fetching of [libsecp256k1][] even if it's already detected at the system level.
- `COINCURVE_CHECK_SYSTEM_LIB` - Must be set to detect [libsecp256k1][] at the system level.
!!! tip
To avoid installing the binary wheels on compatible distributions, use the `--no-binary` option.

2
docs/users.md

@ -40,4 +40,4 @@
- [python-idex](https://github.com/sammchardy/python-idex/blob/24cee970172491a7f7d5f52558727a77384cce26/requirements.txt#L2)
- [Rotki](https://github.com/rotki/rotki/blob/70508f99f890bcbd520f1efe7776194d6a5e5e06/requirements.txt#L8)
- [Vyper](https://github.com/vyperlang/vyper/blob/3bd0bf96856554810065fa9cfb89afef7625d436/Dockerfile#L15)
- [ZeroNet](https://github.com/HelloZeroNet/ZeroNet/blob/454c0b2e7e000fda7000cba49027541fbf327b96/requirements.txt#L12)
- [ZeroNet](https://github.com/zeronet-conservancy/zeronet-conservancy/blob/b6e18fd3738b4725726c5e170040deb3048c9048/requirements.txt#L12)

43
pyproject.toml

@ -1,9 +1,7 @@
[tool.black]
target-version = ["py37"]
line-length = 120
py36 = true
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
@ -24,12 +22,33 @@ exclude = '''
)
'''
[tool.isort]
default_section = 'THIRDPARTY'
force_grid_wrap = 0
include_trailing_comma = true
known_first_party = 'coincurve'
line_length = 120
multi_line_output = 3
skip_glob = 'setup.py'
use_parentheses = true
[tool.ruff]
target-version = "py37"
line-length = 120
select = ["A", "B", "C", "E", "F", "I", "M", "N", "Q", "RUF", "S", "T", "U", "W", "YTT"]
ignore = [
# Allow non-abstract empty methods in abstract base classes
"B027",
# Ignore McCabe complexity
"C901",
# Allow boolean positional values in function calls, like `dict.get(... True)`
"FBT003",
# Ignore checks for possible passwords
"S105", "S106", "S107",
]
unfixable = [
# Don't touch unused imports
"F401",
]
[tool.ruff.isort]
known-first-party = ["coincurve"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
[tool.ruff.per-file-ignores]
"setup.py" = ["B", "C", "I", "N", "U"]
# Tests can use assertions
"tests/*" = ["S101"]
"tests/**/*" = ["S101"]

28
setup.py

@ -5,6 +5,7 @@ import platform
import shutil
import subprocess
import tarfile
import zipfile
from distutils import log
from distutils.command.build_clib import build_clib as _build_clib
from distutils.command.build_ext import build_ext as _build_ext
@ -35,7 +36,7 @@ MAKE = 'gmake' if platform.system() in ['FreeBSD', 'OpenBSD'] else 'make'
# IMPORTANT: keep in sync with .github/workflows/build.yml
#
# Version of libsecp256k1 to download if none exists in the `libsecp256k1` directory
UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'd8a246324650c3df8d54d133a8ac3c1b857a7a4e'
UPSTREAM_REF = os.getenv('COINCURVE_UPSTREAM_REF') or 'ddf2b2910eb19032f8dd657c66735115ae24bfba'
LIB_TARBALL_URL = f'https://github.com/bitcoin-core/secp256k1/archive/{UPSTREAM_REF}.tar.gz'
@ -52,6 +53,7 @@ def download_library(command):
if command.dry_run:
return
libdir = absolute('libsecp256k1')
zipdir = absolute('secp256k1-anonswap_v0.2')
if os.path.exists(os.path.join(libdir, 'autogen.sh')):
# Library already downloaded
return
@ -59,7 +61,16 @@ def download_library(command):
command.announce('downloading libsecp256k1 source code', level=log.INFO)
try:
import requests
zip_url = 'https://github.com/tecnovert/secp256k1/archive/refs/tags/anonswap_v0.2.zip'
r = requests.get(zip_url, stream=True)
status_code = r.status_code
if status_code == 200:
content = BytesIO(r.raw.read())
content.seek(0)
with zipfile.ZipFile(content) as zip_ref:
zip_ref.extractall(absolute())
shutil.move(zipdir, libdir)
"""
r = requests.get(LIB_TARBALL_URL, stream=True)
status_code = r.status_code
if status_code == 200:
@ -71,6 +82,7 @@ def download_library(command):
shutil.move(dirname, libdir)
else:
raise SystemExit('Unable to download secp256k1 library: HTTP-Status: %d', status_code)
"""
except requests.exceptions.RequestException as e:
raise SystemExit('Unable to download secp256k1 library: %s', str(e))
@ -178,15 +190,20 @@ class build_clib(_build_clib):
'--enable-static',
'--disable-dependency-tracking',
'--with-pic',
'--enable-module-extrakeys',
'--enable-module-recovery',
'--enable-module-schnorrsig',
'--prefix',
os.path.abspath(self.build_clib),
'--enable-experimental',
'--enable-module-ecdh',
'--enable-benchmark=no',
'--enable-tests=no',
'--enable-openssl-tests=no',
'--enable-exhaustive-tests=no',
'--enable-module-ed25519',
'--enable-module-generator',
'--enable-module-dleag',
'--enable-module-ecdsaotves',
'--with-valgrind=no'
]
if 'COINCURVE_CROSS_HOST' in os.environ:
cmd.append('--host={}'.format(os.environ['COINCURVE_CROSS_HOST']))
@ -263,7 +280,7 @@ else:
setup(
name='coincurve',
version='17.0.0',
version='18.0.3',
description='Cross-platform Python CFFI bindings for libsecp256k1',
long_description=open('README.md', 'r').read(),
@ -305,6 +322,7 @@ setup(
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: Libraries',

2
setup_support.py

@ -60,6 +60,8 @@ def build_flags(library, type_, path):
def _find_lib():
if 'COINCURVE_CHECK_SYSTEM_LIB' not in os.environ:
return False
if 'COINCURVE_IGNORE_SYSTEM_LIB' in os.environ:
return False

3
tests/samples.py

@ -48,3 +48,6 @@ RECOVERABLE_SIGNATURE = (
b'\x92G\x80&8\x1cVz%2\xb0\x8a\xd0l\x0b4\x9c~\x93\x18\xad'
b'\xe4J\x9c-\n\x00'
)
X_ONLY_PUBKEY = b"Ncx\x00\xf1_'BV\x9ac\x0b\xec)\x0eH\xdf\xebc\xa9\\\x85\x19:\xf9L{B\xe6\x14\xfe\xa8"
X_ONLY_PUBKEY_INVALID = bytes(32)

12
tests/test_dleag.py

@ -0,0 +1,12 @@
from coincurve.dleag import dleag_prove, dleag_verify
from coincurve.ed25519 import get_valid_secret
from coincurve.keys import PrivateKey, PublicKey
class TestDLEAG:
def test_dleag(self):
secret = get_valid_secret()
private_key = PrivateKey(secret)
proof = dleag_prove(private_key)
assert True == dleag_verify(proof)

33
tests/test_ecdsaotves.py

@ -0,0 +1,33 @@
import sys
from coincurve.ecdsaotves import ecdsaotves_enc_sign, ecdsaotves_enc_verify, ecdsaotves_dec_sig, ecdsaotves_rec_enc_key
from coincurve.keys import PrivateKey, PublicKey
from coincurve.utils import get_valid_secret, sha256
class TestECDSAOTVES:
def test_ecdsaotves(self):
secret_sign = get_valid_secret()
secret_encrypt = get_valid_secret()
pk_sign = PublicKey.from_secret(secret_sign)
pk_encrypt = PublicKey.from_secret(secret_encrypt)
pk_sb = pk_sign.format()
pk_eb = pk_encrypt.format()
message = 'otves message'
if sys.version_info[0] > 2:
message_hash = sha256(bytes(message, 'utf-8'))
else:
message_hash = sha256(message)
ct = ecdsaotves_enc_sign(secret_sign, pk_eb, message_hash)
assert ecdsaotves_enc_verify(pk_sb, pk_eb, message_hash, ct)
sig = ecdsaotves_dec_sig(secret_encrypt, ct)
assert pk_sign.verify(sig, message_hash, hasher=None)
secret_rec = ecdsaotves_rec_enc_key(pk_eb, ct, sig)
assert secret_rec == secret_encrypt

61
tests/test_keys.py

@ -4,7 +4,7 @@ from os import urandom
import pytest
from coincurve.ecdsa import deserialize_recoverable, recover
from coincurve.keys import PrivateKey, PublicKey
from coincurve.keys import PrivateKey, PublicKey, PublicKeyXOnly
from coincurve.utils import bytes_to_int, int_to_bytes_padded, verify_signature
from .samples import (
@ -20,6 +20,8 @@ from .samples import (
PUBLIC_KEY_Y,
RECOVERABLE_SIGNATURE,
SIGNATURE,
X_ONLY_PUBKEY,
X_ONLY_PUBKEY_INVALID,
)
G = PublicKey(
@ -35,6 +37,9 @@ class TestPrivateKey:
def test_public_key(self):
assert PrivateKey(PRIVATE_KEY_BYTES).public_key.format() == PUBLIC_KEY_COMPRESSED
def test_xonly_pubkey(self):
assert PrivateKey(PRIVATE_KEY_BYTES).public_key_xonly.format() == PUBLIC_KEY_COMPRESSED[1:]
def test_signature_correct(self):
private_key = PrivateKey()
public_key = private_key.public_key
@ -59,6 +64,22 @@ class TestPrivateKey:
== PublicKey(recover(MESSAGE, deserialize_recoverable(private_key.sign_recoverable(MESSAGE)))).format()
)
def test_schnorr_signature(self):
private_key = PrivateKey()
message = urandom(32)
# Message must be 32 bytes
with pytest.raises(ValueError):
private_key.sign_schnorr(message + b'\x01')
# We can provide supplementary randomness
sig = private_key.sign_schnorr(message, urandom(32))
assert private_key.public_key_xonly.verify(sig, message)
# Or not
sig = private_key.sign_schnorr(message)
assert private_key.public_key_xonly.verify(sig, message)
def test_to_hex(self):
assert PrivateKey(PRIVATE_KEY_BYTES).to_hex() == PRIVATE_KEY_HEX
@ -146,3 +167,41 @@ class TestPublicKey:
b = PrivateKey().public_key
assert PublicKey.combine_keys([a, b]) == a.combine([b])
class TestXonlyPubKey:
def test_parse_invalid(self):
# Must be 32 bytes
with pytest.raises(ValueError):
PublicKeyXOnly.from_secret(bytes(33))
# Must be an x coordinate for a valid point
with pytest.raises(ValueError):
PublicKeyXOnly(X_ONLY_PUBKEY_INVALID)
def test_roundtrip(self):
assert PublicKeyXOnly(X_ONLY_PUBKEY).format() == X_ONLY_PUBKEY
assert PublicKeyXOnly(PUBLIC_KEY_COMPRESSED[1:]).format() == PUBLIC_KEY_COMPRESSED[1:]
# Test __eq__
assert PublicKeyXOnly(X_ONLY_PUBKEY) == PublicKeyXOnly(X_ONLY_PUBKEY)
def test_tweak(self):
# Taken from BIP341 test vectors.
# See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json
pubkey = PublicKeyXOnly(bytes.fromhex('d6889cb081036e0faefa3a35157ad71086b123b2b144b649798b494c300a961d'))
pubkey.tweak_add(bytes.fromhex('b86e7be8f39bab32a6f2c0443abbc210f0edac0e2c53d501b36b64437d9c6c70'))
assert pubkey.format() == bytes.fromhex('53a1f6e454df1aa2776a2814a721372d6258050de330b3c6d10ee8f4e0dda343')
def test_parity(self):
# Taken from BIP341 test vectors.
# See github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0341/wallet-test-vectors.json
pubkey = PublicKeyXOnly(bytes.fromhex('187791b6f712a8ea41c8ecdd0ee77fab3e85263b37e1ec18a3651926b3a6cf27'))
pubkey.tweak_add(bytes.fromhex('cbd8679ba636c1110ea247542cfbd964131a6be84f873f7f3b62a777528ed001'))
assert pubkey.format() == bytes.fromhex('147c9c57132f6e7ecddba9800bb0c4449251c92a1e60371ee77557b6620f3ea3')
assert pubkey.parity
pubkey = PublicKeyXOnly(bytes.fromhex('93478e9488f956df2396be2ce6c5cced75f900dfa18e7dabd2428aae78451820'))
pubkey.tweak_add(bytes.fromhex('6af9e28dbf9d6aaf027696e2598a5b3d056f5fd2355a7fd5a37a0e5008132d30'))
assert pubkey.format() == bytes.fromhex('e4d810fd50586274face62b8a807eb9719cef49c04177cc6b76a9a4251d5450e')
assert not pubkey.parity

25
tox.ini

@ -5,6 +5,7 @@ envlist =
3.8
3.9
3.10
3.11
pypy3
bench
lint
@ -35,23 +36,19 @@ commands =
envdir = {toxworkdir}/lint
skip_install = true
deps =
flake8>=3.9
flake8-bugbear>=20.1.4
flake8-quotes>=3.2.0
black>=21.12b0
isort[pyproject]>=5
ruff
commands =
flake8 .
ruff .
black --check --diff .
isort --check-only --diff .
[testenv:fmt]
envdir = {[testenv:lint]envdir}
skip_install = true
deps = {[testenv:lint]deps}
commands =
isort .
black .
ruff --fix .
{[testenv:lint]commands}
[testenv:typing]
@ -70,19 +67,19 @@ setenv =
; See https://reproducible-builds.org/specs/source-date-epoch/
SOURCE_DATE_EPOCH=1580601600
deps =
mkdocs~=1.2.2
mkdocs~=1.3.1
; theme
mkdocs-material~=7.3.1
mkdocs-material~=8.3.9
; plugins
mkdocs-minify-plugin~=0.4.1
mkdocs-git-revision-date-localized-plugin~=0.10.0
mkdocstrings~=0.16.2
mkdocs-minify-plugin~=0.5.0
mkdocs-git-revision-date-localized-plugin~=1.1.0
mkdocstrings~=0.18.1
; Extensions
pymdown-extensions~=9.0
pymdown-extensions~=9.5.0
mkdocs-material-extensions~=1.0.3
mkpatcher~=1.0.2
; Necessary for syntax highlighting in code blocks
Pygments~=2.10.0
Pygments~=2.12.0
commands =
python -m mkdocs {posargs}

Loading…
Cancel
Save