diff --git a/.coveragerc b/.coveragerc index 4c832ab..b0320e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,3 +2,11 @@ exclude_lines = if __name__ == .__main__.: + +[paths] +source = + secp256k1 + */site-packages/secp256k1/ + */site-packages/*.egg/secp256k1/ + */dist-packages/secp256k1/ + */dist-packages/*.egg/secp256k1/ diff --git a/.gitignore b/.gitignore index 1538ea4..61c6404 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ _libsecp256k1* .cache/ *.egg htmlcov +libsecp256k1 diff --git a/.travis.yml b/.travis.yml index 30b441e..c81df5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,19 @@ language: python + python: - "2.7" - "3.3" + - "3.4" + +env: + global: + - LD_LIBRARY_PATH=./libsecp256k1_ext/.libs + - LIB_DIR=./libsecp256k1_ext/.libs + - INCLUDE_DIR=./libsecp256k1_ext/include + matrix: + - BUNDLED=0 + - BUNDLED=1 + - BUNDLED=1 SECP_BUNDLED_EXPERIMENTAL=1 sudo: false @@ -12,23 +24,29 @@ addons: - libtool - autoconf - automake + - pkg-config + - libffi-dev + - libgmp-dev before_install: - - git clone git://github.com/bitcoin/secp256k1.git libsecp256k1 - - pushd libsecp256k1 - - ./autogen.sh - - ./configure --enable-module-recovery --enable-module-ecdh --enable-module-schnorr - - make - - popd - - virtualenv ENV - - source ENV/bin/activate - - pip install -U pip setuptools cffi coverage coveralls + - ./travis_install.sh + - pip install -U pip setuptools cffi pytest coverage coveralls install: - - LIB_DIR=./libsecp256k1/.libs INCLUDE_DIR=./libsecp256k1/include python setup.py -q install + - python setup.py install +# This is a bit of a hack: +# We want to ensure that we test the installed version, not the local source. +# For that reason we rename the local source directory before running the +# tests. +# Unfortunately this compilcates using coverage. We use '--parallel' and +# 'combine' to massage it into producing correct paths. For that to work +# we need to re-rename the source back to it's correct name. script: - - LD_LIBRARY_PATH=./libsecp256k1/.libs LIB_DIR=./libsecp256k1/.libs INCLUDE_DIR=./libsecp256k1/include coverage run --source=secp256k1 setup.py pytest + - mv secp256k1 _secp256k1 + - coverage run --parallel --include="*/site-packages/*.egg/secp256k1/*" -m py.test + - mv _secp256k1 secp256k1 + - coverage combine after_success: - coveralls diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8f37ef0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include setup_support.py +recursive-include _cffi_build *.py *.h +graft libsecp256k1 diff --git a/README.md b/README.md index bac5f93..1bac676 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,61 @@ # secp256k1-py [![Build Status](https://travis-ci.org/ludbb/secp256k1-py.svg?branch=master)](https://travis-ci.org/ludbb/secp256k1-py) [![Coverage Status](https://coveralls.io/repos/ludbb/secp256k1-py/badge.svg?branch=master&service=github)](https://coveralls.io/github/ludbb/secp256k1-py?branch=master) -Python FFI bindings for [secp256k1](https://github.com/bitcoin/secp256k1) +Python FFI bindings for [libsecp256k1](https://github.com/bitcoin/secp256k1) (an experimental and optimized C library for EC operations on curve secp256k1). +## Installation + ``` pip install secp256k1 ``` -In case the headers or lib for secp256k1 are not in your path, it's -possible to specify `INCLUDE_DIR` and `LIB_DIR` as in: +There are two modes of installation depending on whether you already have +libsecp256k1 installed on your system: + + +###### Using a system installed libsecp256k1 + +If the library is already installed it should usually be automatically detected +and used. +However if libsecp256k1 is installed in a non standard location you can use the +environment variables `INCLUDE_DIR` and `LIB_DIR` to point the way: + +``` +INCLUDE_DIR=/opt/somewhere/include LIB_DIR=/opt/somewhere/lib pip install secp256k1 +``` + + +###### Using the bundled libsecp256k1 + +If on the other hand you don't have libsecp256k1 installed on your system, a +bundled version will be built and used. In this case only the `recovery` module +will be enabled since it's the only one not currently considered as +"experimental" by the library authors. This can be overridden by setting the +`SECP_BUNDLED_EXPERIMENTAL` environment variable: ``` -INCLUDE_DIR=/usr/local/include LIB_DIR=/usr/local/lib pip install secp256k1 +SECP_BUNDLED_EXPERIMENTAL=1 pip install secp256k1 ``` +For the bundled version to compile successfully you need to have a C compiler +as well as the development headers for `libffi` and `libgmp` installed. + +On Debian / Ubuntu for example the necessary packages are: + +* `build-essential` +* `automake` +* `pkg-config` +* `libtool` +* `libffi-dev` +* `libgmp-dev` + +On OS X the necessary homebrew packages are: + +* `automake` +* `pkg-config` +* `libffi` +* `gmp` + ## Command line usage @@ -287,3 +329,39 @@ pubkey_ser_uncompressed = privkey.pubkey.serialize(compressed=False) assert pubkey_ser == bytes(bytearray.fromhex(pub_compressed)) assert pubkey_ser_uncompressed == bytes(bytearray.fromhex(pub_uncompressed)) ``` + + +## Technical details about the bundled libsecp256k1 + +The bundling of libsecp256k1 is handled by the various setup.py build phases: + +- During 'sdist': + If the directory `libsecp256k1` doesn't exist in the + source directory it is downloaded from the location specified + by the `LIB_TARBALL_URL` constant in `setup.py` and extracted into + a directory called `libsecp256k1` + + To upgrade to a newer version of the bundled libsecp256k1 source + simply delete the `libsecp256k1` directory and update the + `LIB_TARBALL_URL` to point to a newer commit. + +- During 'install': + If an existing (system) installation of libsecp256k1 is found + (either in the default library locations or in the location pointed + to by the environment variable `LIB_DIR`) it is used as before. + + Due to the way the way cffi modules are implemented it is necessary + to perform this detection in the cffi build module + `_cffi_build/build.py` as well as in `setup.py`. For that reason + some utility functions have been moved into a `setup_support.py` + module which is imported from both. + + If however no existing installation can be found the bundled + source code is used to build a library locally that will be + statically linked into the CFFI extension. + + By default only the `recovery` module will be enabled in this bundled + version as it is the only one not considered to be 'experimental' by + the libsecp256k1 authors. It is possible to override this and enable + all modules by setting the environment variable + `SECP_BUNDLED_EXPERIMENTAL`. diff --git a/_cffi_build/build.py b/_cffi_build/build.py new file mode 100644 index 0000000..c6e3c87 --- /dev/null +++ b/_cffi_build/build.py @@ -0,0 +1,93 @@ +import os +import sys +from collections import namedtuple +from itertools import combinations + +from cffi import FFI, VerificationError + +sys.path.append(os.path.abspath(os.path.dirname(__file__))) +from setup_support import has_system_lib, redirect, workdir, absolute + +Source = namedtuple('Source', ('h', 'include')) + + +class Break(Exception): + pass + + +def _mk_ffi(sources, name="_libsecp256k1", bundled=True, **kwargs): + ffi = FFI() + code = [] + if 'INCLUDE_DIR' in os.environ: + kwargs['include_dirs'] = [absolute(os.environ['INCLUDE_DIR'])] + if 'LIB_DIR' in os.environ: + kwargs['library_dirs'] = [absolute(os.environ['LIB_DIR'])] + for source in sources: + with open(source.h, 'rt') as h: + ffi.cdef(h.read()) + code.append(source.include) + if bundled: + code.append("#define PY_USE_BUNDLED") + ffi.set_source(name, "\n".join(code), **kwargs) + return ffi + + +_base = [Source(absolute("_cffi_build/secp256k1.h"), "#include ", )] + +_modules = { + 'ecdh': Source(absolute("_cffi_build/secp256k1_ecdh.h"), "#include ", ), + 'recovery': Source(absolute("_cffi_build/secp256k1_recovery.h"), "#include ", ), + 'schnorr': Source(absolute("_cffi_build/secp256k1_schnorr.h"), "#include ", ), +} + + +ffi = None + +# The following is used to detect whether the library is already installed on +# the system (and if so which modules are enabled) or if we will use the +# bundled one. +if has_system_lib(): + _available = [] + try: + # try all combinations of optional modules that could be enabled + # works downwards from most enabled modules to fewest + for l in range(len(_modules), -1, -1): + for combination in combinations(_modules.items(), l): + try: + _test_ffi = _mk_ffi( + _base + [item[1] for item in combination], + name="_testcompile", + bundled=False, + libraries=['secp256k1'] + ) + with redirect(sys.stderr, os.devnull), workdir(): + _test_ffi.compile() + _available = combination + raise Break() + except VerificationError as ex: + pass + except Break: + ffi = _mk_ffi( + _base + [i[1] for i in _available], + bundled=False, + libraries=['secp256k1'] + ) + print("Using system libsecp256k1 with modules: {}".format( + ", ".join(i[0] for i in _available)) + ) + else: + # We didn't find any functioning combination of modules + # Normally this shouldn't happen but just in case we will fall back + # to the bundled library + print("Installed libsecp256k1 is unusable falling back to bundled version.") + +if ffi is None: + # Library is not installed - use bundled one + print("Using bundled libsecp256k1") + + # By default we only build with recovery enabled since the other modules + # are experimental + if os.environ.get('SECP_BUNDLED_EXPERIMENTAL'): + ffi = _mk_ffi(_base + list(_modules.values()), libraries=['secp256k1']) + else: + ffi = _mk_ffi(_base + [_modules['recovery']], libraries=['secp256k1']) diff --git a/_cffi_build/secp256k1.h b/_cffi_build/secp256k1.h new file mode 100644 index 0000000..0f9d7e1 --- /dev/null +++ b/_cffi_build/secp256k1.h @@ -0,0 +1,170 @@ +typedef struct secp256k1_context_struct secp256k1_context; + +typedef struct { + unsigned char data[64]; +} secp256k1_pubkey; + +typedef struct { + unsigned char data[64]; +} secp256k1_ecdsa_signature; + +typedef int (*secp256k1_nonce_function)( + unsigned char *nonce32, + const unsigned char *msg32, + const unsigned char *key32, + const unsigned char *algo16, + void *data, + unsigned int attempt +); + +#define SECP256K1_FLAGS_TYPE_MASK ... +#define SECP256K1_FLAGS_TYPE_CONTEXT ... +#define SECP256K1_FLAGS_TYPE_COMPRESSION ... +#define SECP256K1_FLAGS_BIT_CONTEXT_VERIFY ... +#define SECP256K1_FLAGS_BIT_CONTEXT_SIGN ... +#define SECP256K1_FLAGS_BIT_COMPRESSION ... + +#define SECP256K1_CONTEXT_VERIFY ... +#define SECP256K1_CONTEXT_SIGN ... +#define SECP256K1_CONTEXT_NONE ... + +#define SECP256K1_EC_COMPRESSED ... +#define SECP256K1_EC_UNCOMPRESSED ... + +secp256k1_context* secp256k1_context_create( + unsigned int flags +); + +secp256k1_context* secp256k1_context_clone( + const secp256k1_context* ctx +); + +void secp256k1_context_destroy( + secp256k1_context* ctx +); + +void secp256k1_context_set_illegal_callback( + secp256k1_context* ctx, + void (*fun)(const char* message, void* data), + const void* data +); + +void secp256k1_context_set_error_callback( + secp256k1_context* ctx, + void (*fun)(const char* message, void* data), + const void* data +); + +int secp256k1_ec_pubkey_parse( + const secp256k1_context* ctx, + secp256k1_pubkey* pubkey, + const unsigned char *input, + size_t inputlen +); + +int secp256k1_ec_pubkey_serialize( + const secp256k1_context* ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_pubkey* pubkey, + unsigned int flags +); + +int secp256k1_ecdsa_signature_parse_compact( + const secp256k1_context* ctx, + secp256k1_ecdsa_signature* sig, + const unsigned char *input64 +); + +int secp256k1_ecdsa_signature_parse_der( + const secp256k1_context* ctx, + secp256k1_ecdsa_signature* sig, + const unsigned char *input, + size_t inputlen +); + +int secp256k1_ecdsa_signature_serialize_der( + const secp256k1_context* ctx, + unsigned char *output, + size_t *outputlen, + const secp256k1_ecdsa_signature* sig +); + +int secp256k1_ecdsa_signature_serialize_compact( + const secp256k1_context* ctx, + unsigned char *output64, + const secp256k1_ecdsa_signature* sig +); + +int secp256k1_ecdsa_verify( + const secp256k1_context* ctx, + const secp256k1_ecdsa_signature *sig, + const unsigned char *msg32, + const secp256k1_pubkey *pubkey +); + +int secp256k1_ecdsa_signature_normalize( + const secp256k1_context* ctx, + secp256k1_ecdsa_signature *sigout, + const secp256k1_ecdsa_signature *sigin +); + +extern const secp256k1_nonce_function secp256k1_nonce_function_rfc6979; + +extern const secp256k1_nonce_function secp256k1_nonce_function_default; + +int secp256k1_ecdsa_sign( + const secp256k1_context* ctx, + secp256k1_ecdsa_signature *sig, + const unsigned char *msg32, + const unsigned char *seckey, + secp256k1_nonce_function noncefp, + const void *ndata +); + +int secp256k1_ec_seckey_verify( + const secp256k1_context* ctx, + const unsigned char *seckey +); + +int secp256k1_ec_pubkey_create( + const secp256k1_context* ctx, + secp256k1_pubkey *pubkey, + const unsigned char *seckey +); + +int secp256k1_ec_privkey_tweak_add( + const secp256k1_context* ctx, + unsigned char *seckey, + const unsigned char *tweak +); + +int secp256k1_ec_pubkey_tweak_add( + const secp256k1_context* ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak +); + +int secp256k1_ec_privkey_tweak_mul( + const secp256k1_context* ctx, + unsigned char *seckey, + const unsigned char *tweak +); + +int secp256k1_ec_pubkey_tweak_mul( + const secp256k1_context* ctx, + secp256k1_pubkey *pubkey, + const unsigned char *tweak +); + +int secp256k1_context_randomize( + secp256k1_context* ctx, + const unsigned char *seed32 +); + +int secp256k1_ec_pubkey_combine( + const secp256k1_context* ctx, + secp256k1_pubkey *out, + const secp256k1_pubkey * const * ins, + size_t n +); diff --git a/_cffi_build/secp256k1_ecdh.h b/_cffi_build/secp256k1_ecdh.h new file mode 100644 index 0000000..293fcea --- /dev/null +++ b/_cffi_build/secp256k1_ecdh.h @@ -0,0 +1,6 @@ +int secp256k1_ecdh( + const secp256k1_context* ctx, + unsigned char *result, + const secp256k1_pubkey *pubkey, + const unsigned char *privkey +); diff --git a/_cffi_build/secp256k1_recovery.h b/_cffi_build/secp256k1_recovery.h new file mode 100644 index 0000000..5319cb7 --- /dev/null +++ b/_cffi_build/secp256k1_recovery.h @@ -0,0 +1,39 @@ +typedef struct { + unsigned char data[65]; +} secp256k1_ecdsa_recoverable_signature; + +int secp256k1_ecdsa_recoverable_signature_parse_compact( + const secp256k1_context* ctx, + secp256k1_ecdsa_recoverable_signature* sig, + const unsigned char *input64, + int recid +); + +int secp256k1_ecdsa_recoverable_signature_convert( + const secp256k1_context* ctx, + secp256k1_ecdsa_signature* sig, + const secp256k1_ecdsa_recoverable_signature* sigin +); + +int secp256k1_ecdsa_recoverable_signature_serialize_compact( + const secp256k1_context* ctx, + unsigned char *output64, + int *recid, + const secp256k1_ecdsa_recoverable_signature* sig +); + +int secp256k1_ecdsa_sign_recoverable( + const secp256k1_context* ctx, + secp256k1_ecdsa_recoverable_signature *sig, + const unsigned char *msg32, + const unsigned char *seckey, + secp256k1_nonce_function noncefp, + const void *ndata +); + +int secp256k1_ecdsa_recover( + const secp256k1_context* ctx, + secp256k1_pubkey *pubkey, + const secp256k1_ecdsa_recoverable_signature *sig, + const unsigned char *msg32 +); diff --git a/_cffi_build/secp256k1_schnorr.h b/_cffi_build/secp256k1_schnorr.h new file mode 100644 index 0000000..909854f --- /dev/null +++ b/_cffi_build/secp256k1_schnorr.h @@ -0,0 +1,48 @@ +int secp256k1_schnorr_sign( + const secp256k1_context* ctx, + unsigned char *sig64, + const unsigned char *msg32, + const unsigned char *seckey, + secp256k1_nonce_function noncefp, + const void *ndata +); + +int secp256k1_schnorr_verify( + const secp256k1_context* ctx, + const unsigned char *sig64, + const unsigned char *msg32, + const secp256k1_pubkey *pubkey +); + +int secp256k1_schnorr_recover( + const secp256k1_context* ctx, + secp256k1_pubkey *pubkey, + const unsigned char *sig64, + const unsigned char *msg32 +); + +int secp256k1_schnorr_generate_nonce_pair( + const secp256k1_context* ctx, + secp256k1_pubkey *pubnonce, + unsigned char *privnonce32, + const unsigned char *msg32, + const unsigned char *sec32, + secp256k1_nonce_function noncefp, + const void* noncedata +); + +int secp256k1_schnorr_partial_sign( + const secp256k1_context* ctx, + unsigned char *sig64, + const unsigned char *msg32, + const unsigned char *sec32, + const secp256k1_pubkey *pubnonce_others, + const unsigned char *secnonce32 +); + +int secp256k1_schnorr_partial_combine( + const secp256k1_context* ctx, + unsigned char *sig64, + const unsigned char * const * sig64sin, + size_t n +); diff --git a/build.py b/build.py deleted file mode 100644 index df0c3f0..0000000 --- a/build.py +++ /dev/null @@ -1,335 +0,0 @@ -import os -from cffi import FFI, ffiplatform - - -definitions = """ - /* secp256k1.h*/ - - typedef struct secp256k1_context_struct secp256k1_context; - - typedef struct { - unsigned char data[64]; - } secp256k1_pubkey; - - typedef struct { - unsigned char data[64]; - } secp256k1_ecdsa_signature; - - typedef int (*secp256k1_nonce_function)( - unsigned char *nonce32, - const unsigned char *msg32, - const unsigned char *key32, - const unsigned char *algo16, - void *data, - unsigned int attempt - ); - - #define SECP256K1_FLAGS_TYPE_MASK 255 - #define SECP256K1_FLAGS_TYPE_CONTEXT 1 - #define SECP256K1_FLAGS_TYPE_COMPRESSION 2 - - #define SECP256K1_FLAGS_BIT_CONTEXT_VERIFY 256 - #define SECP256K1_FLAGS_BIT_CONTEXT_SIGN 512 - #define SECP256K1_FLAGS_BIT_COMPRESSION 256 - - #define SECP256K1_CONTEXT_VERIFY 257 - #define SECP256K1_CONTEXT_SIGN 513 - #define SECP256K1_CONTEXT_NONE 1 - - #define SECP256K1_EC_COMPRESSED 258 - #define SECP256K1_EC_UNCOMPRESSED 2 - - secp256k1_context* secp256k1_context_create( - int flags - ); - - secp256k1_context* secp256k1_context_clone( - const secp256k1_context* ctx - ); - - void secp256k1_context_destroy( - secp256k1_context* ctx - ); - - int secp256k1_ec_pubkey_parse( - const secp256k1_context* ctx, - secp256k1_pubkey* pubkey, - const unsigned char *input, - size_t inputlen - ); - - int secp256k1_ec_pubkey_serialize( - const secp256k1_context* ctx, - unsigned char *output, - size_t *outputlen, - const secp256k1_pubkey* pubkey, - unsigned int flags - ); - - int secp256k1_ecdsa_signature_parse_compact( - const secp256k1_context* ctx, - secp256k1_ecdsa_signature* sig, - const unsigned char *input64 - ); - - int secp256k1_ecdsa_signature_parse_der( - const secp256k1_context* ctx, - secp256k1_ecdsa_signature* sig, - const unsigned char *input, - size_t inputlen - ); - - int secp256k1_ecdsa_signature_serialize_der( - const secp256k1_context* ctx, - unsigned char *output, - size_t *outputlen, - const secp256k1_ecdsa_signature* sig - ); - - int secp256k1_ecdsa_signature_serialize_compact( - const secp256k1_context* ctx, - unsigned char *output64, - const secp256k1_ecdsa_signature* sig - ); - - int secp256k1_ecdsa_verify( - const secp256k1_context* ctx, - const secp256k1_ecdsa_signature *sig, - const unsigned char *msg32, - const secp256k1_pubkey *pubkey - ); - - int secp256k1_ecdsa_signature_normalize( - const secp256k1_context* ctx, - secp256k1_ecdsa_signature *sigout, - const secp256k1_ecdsa_signature *sigin - ); - - extern const secp256k1_nonce_function secp256k1_nonce_function_rfc6979; - extern const secp256k1_nonce_function secp256k1_nonce_function_default; - - - int secp256k1_ecdsa_sign( - const secp256k1_context* ctx, - secp256k1_ecdsa_signature *sig, - const unsigned char *msg32, - const unsigned char *seckey, - secp256k1_nonce_function noncefp, - const void *ndata - ); - - int secp256k1_ec_seckey_verify( - const secp256k1_context* ctx, - const unsigned char *seckey - ); - - int secp256k1_ec_pubkey_create( - const secp256k1_context* ctx, - secp256k1_pubkey *pubkey, - const unsigned char *seckey - ); - - int secp256k1_ec_privkey_tweak_add( - const secp256k1_context* ctx, - unsigned char *seckey, - const unsigned char *tweak - ); - - int secp256k1_ec_pubkey_tweak_add( - const secp256k1_context* ctx, - secp256k1_pubkey *pubkey, - const unsigned char *tweak - ); - - int secp256k1_ec_privkey_tweak_mul( - const secp256k1_context* ctx, - unsigned char *seckey, - const unsigned char *tweak - ); - - int secp256k1_ec_pubkey_tweak_mul( - const secp256k1_context* ctx, - secp256k1_pubkey *pubkey, - const unsigned char *tweak - ); - - int secp256k1_context_randomize( - secp256k1_context* ctx, - const unsigned char *seed32 - ); - - int secp256k1_ec_pubkey_combine( - const secp256k1_context* ctx, - secp256k1_pubkey *out, - const secp256k1_pubkey * const * ins, - size_t n - ); -""" - -definitions_recovery = """ - /* secp256k1_recovery.h */ - - typedef struct { - unsigned char data[65]; - } secp256k1_ecdsa_recoverable_signature; - - int secp256k1_ecdsa_recoverable_signature_parse_compact( - const secp256k1_context* ctx, - secp256k1_ecdsa_recoverable_signature* sig, - const unsigned char *input64, - int recid - ); - - int secp256k1_ecdsa_recoverable_signature_convert( - const secp256k1_context* ctx, - secp256k1_ecdsa_signature* sig, - const secp256k1_ecdsa_recoverable_signature* sigin - ); - - int secp256k1_ecdsa_recoverable_signature_serialize_compact( - const secp256k1_context* ctx, - unsigned char *output64, - int *recid, - const secp256k1_ecdsa_recoverable_signature* sig - ); - - int secp256k1_ecdsa_sign_recoverable( - const secp256k1_context* ctx, - secp256k1_ecdsa_recoverable_signature *sig, - const unsigned char *msg32, - const unsigned char *seckey, - secp256k1_nonce_function noncefp, - const void *ndata - ); - - int secp256k1_ecdsa_recover( - const secp256k1_context* ctx, - secp256k1_pubkey *pubkey, - const secp256k1_ecdsa_recoverable_signature *sig, - const unsigned char *msg32 - ); -""" - -definitions_schnorr = """ - /* secp256k1_schnorr.h */ - - int secp256k1_schnorr_sign( - const secp256k1_context* ctx, - unsigned char *sig64, - const unsigned char *msg32, - const unsigned char *seckey, - secp256k1_nonce_function noncefp, - const void *ndata - ); - - int secp256k1_schnorr_verify( - const secp256k1_context* ctx, - const unsigned char *sig64, - const unsigned char *msg32, - const secp256k1_pubkey *pubkey - ); - - int secp256k1_schnorr_recover( - const secp256k1_context* ctx, - secp256k1_pubkey *pubkey, - const unsigned char *sig64, - const unsigned char *msg32 - ); - - int secp256k1_schnorr_generate_nonce_pair( - const secp256k1_context* ctx, - secp256k1_pubkey *pubnonce, - unsigned char *privnonce32, - const unsigned char *msg32, - const unsigned char *sec32, - secp256k1_nonce_function noncefp, - const void* noncedata - ); - - int secp256k1_schnorr_partial_sign( - const secp256k1_context* ctx, - unsigned char *sig64, - const unsigned char *msg32, - const unsigned char *sec32, - const secp256k1_pubkey *pubnonce_others, - const unsigned char *secnonce32 - ); - - int secp256k1_schnorr_partial_combine( - const secp256k1_context* ctx, - unsigned char *sig64, - const unsigned char * const * sig64sin, - size_t n - ); -""" - -definitions_ecdh = """ - /* secp256k1_ecdh.h */ - - int secp256k1_ecdh( - const secp256k1_context* ctx, - unsigned char *result, - const secp256k1_pubkey *point, - const unsigned char *scalar - ); -""" - - -def build_ffi(include_recovery=False, include_schnorr=False, include_ecdh=False): - ffi = FFI() - - source = "#include " - cdefs = definitions - if include_recovery: - cdefs += definitions_recovery - source += "\n#include " - if include_schnorr: - cdefs += definitions_schnorr - source += "\n#include " - if include_ecdh: - cdefs += definitions_ecdh - source += "\n#include " - - incpath = [os.environ['INCLUDE_DIR']] if 'INCLUDE_DIR' in os.environ else None - libpath = [os.environ['LIB_DIR']] if 'LIB_DIR' in os.environ else None - - ffi.set_source( - "_libsecp256k1", - source, - libraries=["secp256k1"], - library_dirs=libpath, - include_dirs=incpath) - ffi.cdef(cdefs) - - return ffi - - -_modules = { - 'secp256k1_recovery': [False, {'include_recovery': True}], - 'secp256k1_schnorr': [False, {'include_schnorr': True}], - 'secp256k1_ecdh': [False, {'include_ecdh': True}] -} - -# Check which modules are available. -for mod in _modules: - kwargs = _modules[mod][1] - try: - _ffi = build_ffi(**kwargs) - _ffi.compile() - _modules[mod][0] = True - except ffiplatform.VerificationError: - pass - -# Build interface with all active modules. -_kwargs = {} -_not_avail = [] -for mod, val in _modules.items(): - if val[0]: - _kwargs.update(val[1]) - else: - _not_avail.append(mod) - -ffi = build_ffi(**_kwargs) -ffi.compile() - -print('\n'.join('{} not supported'.format(entry) for entry in _not_avail)) diff --git a/secp256k1.py b/secp256k1/__init__.py similarity index 99% rename from secp256k1.py rename to secp256k1/__init__.py index 1b89cb2..61b1b86 100644 --- a/secp256k1.py +++ b/secp256k1/__init__.py @@ -2,7 +2,7 @@ import hashlib import binascii -from _libsecp256k1 import ffi, lib +from ._libsecp256k1 import ffi, lib EC_COMPRESSED = lib.SECP256K1_EC_COMPRESSED diff --git a/setup.py b/setup.py index 882df49..5e15beb 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,267 @@ -from setuptools import setup +import errno +import os +import os.path +import shutil +import subprocess +import tarfile +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 +from distutils.errors import DistutilsError +from io import BytesIO +import sys + +from setuptools import Distribution as _Distribution, setup, find_packages, __version__ as setuptools_version +from setuptools.command.develop import develop as _develop +from setuptools.command.egg_info import egg_info as _egg_info +from setuptools.command.sdist import sdist as _sdist + +try: + from urllib2 import urlopen, URLError +except ImportError: + from urllib.request import urlopen + from urllib.error import URLError + + +sys.path.append(os.path.abspath(os.path.dirname(__file__))) +from setup_support import absolute, build_flags, has_system_lib + + +# Version of libsecp256k1 to download if none exists in the `libsecp256k1` +# directory +LIB_TARBALL_URL = "https://github.com/bitcoin/secp256k1/archive/bd2895fdd92d4dcb0360181082a8d7d078518162.tar.gz" + + +# We require setuptools >= 12.0 +if [int(i) for i in setuptools_version.split('.')] < [12, 0]: + raise SystemExit( + "Your setuptools version ({}) is too old to correctly install this " + "package. Please upgrade to a newer version.".format(setuptools_version) + ) + + +def download_library(command): + if command.dry_run: + return + libdir = absolute("libsecp256k1") + if os.path.exists(os.path.join(libdir, "autogen.sh")): + # Library already downloaded + return + if not os.path.exists(libdir): + command.announce("downloading libsecp256k1 source code", level=log.INFO) + try: + r = urlopen(LIB_TARBALL_URL) + if r.getcode() == 200: + content = BytesIO(r.read()) + content.seek(0) + with tarfile.open(fileobj=content) as tf: + dirname = tf.getnames()[0].partition('/')[0] + tf.extractall() + shutil.move(dirname, libdir) + else: + raise SystemExit( + "Unable to download secp256k1 library: HTTP-Status: %d", + r.getcode() + ) + except URLError as ex: + raise SystemExit("Unable to download secp256k1 library: %s", + ex.message) + + +class egg_info(_egg_info): + def run(self): + # Ensure library has been downloaded (sdist might have been skipped) + download_library(self) + + _egg_info.run(self) + + +class sdist(_sdist): + def run(self): + download_library(self) + _sdist.run(self) + + +class Distribution(_Distribution): + def has_c_libraries(self): + return not has_system_lib() + + +class build_clib(_build_clib): + def initialize_options(self): + _build_clib.initialize_options(self) + self.build_flags = None + + def finalize_options(self): + _build_clib.finalize_options(self) + if self.build_flags is None: + self.build_flags = { + 'include_dirs': [], + 'library_dirs': [], + 'define': [], + } + + def get_source_files(self): + # Ensure library has been downloaded (sdist might have been skipped) + download_library(self) + + return [ + absolute(os.path.join(root, filename)) + for root, _, filenames in os.walk(absolute("libsecp256k1")) + for filename in filenames + ] + + def build_libraries(self, libraries): + raise Exception("build_libraries") + + def check_library_list(self, libraries): + raise Exception("check_library_list") + + def get_library_names(self): + return build_flags('libsecp256k1', 'l', os.path.abspath(self.build_temp)) + + def run(self): + if has_system_lib(): + log.info("Using system library") + return + + build_temp = os.path.abspath(self.build_temp) + + try: + os.makedirs(build_temp) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + if not os.path.exists(absolute("libsecp256k1/configure")): + # configure script hasn't been generated yet + autogen = absolute("libsecp256k1/autogen.sh") + os.chmod(absolute(autogen), 0o755) + subprocess.check_call( + [autogen], + cwd=absolute("libsecp256k1"), + ) + + for filename in [ + "libsecp256k1/configure", + "libsecp256k1/build-aux/compile", + "libsecp256k1/build-aux/config.guess", + "libsecp256k1/build-aux/config.sub", + "libsecp256k1/build-aux/depcomp", + "libsecp256k1/build-aux/install-sh", + "libsecp256k1/build-aux/missing", + "libsecp256k1/build-aux/test-driver", + ]: + try: + os.chmod(absolute(filename), 0o755) + except OSError as e: + # some of these files might not exist depending on autoconf version + if e.errno != errno.ENOENT: + # If the error isn't "No such file or directory" something + # else is wrong and we want to know about it + raise + + cmd = [ + absolute("libsecp256k1/configure"), + "--disable-shared", + "--enable-static", + "--disable-dependency-tracking", + "--with-pic", + "--enable-module-recovery", + "--prefix", + os.path.abspath(self.build_clib), + ] + if os.environ.get('SECP_BUNDLED_EXPERIMENTAL'): + log.info("Building experimental") + cmd.extend([ + "--enable-experimental", + "--enable-module-ecdh", + "--enable-module-schnorr", + ]) + + log.debug("Running configure: {}".format(" ".join(cmd))) + subprocess.check_call( + cmd, + cwd=build_temp, + ) + + subprocess.check_call(["make"], cwd=build_temp) + subprocess.check_call(["make", "install"], cwd=build_temp) + + self.build_flags['include_dirs'].extend(build_flags('libsecp256k1', 'I', build_temp)) + self.build_flags['library_dirs'].extend(build_flags('libsecp256k1', 'L', build_temp)) + if not has_system_lib(): + self.build_flags['define'].append(('CFFI_ENABLE_RECOVERY', None)) + else: + pass + + +class build_ext(_build_ext): + def run(self): + if self.distribution.has_c_libraries(): + build_clib = self.get_finalized_command("build_clib") + self.include_dirs.append( + os.path.join(build_clib.build_clib, "include"), + ) + self.include_dirs.extend(build_clib.build_flags['include_dirs']) + + self.library_dirs.append( + os.path.join(build_clib.build_clib, "lib"), + ) + self.library_dirs.extend(build_clib.build_flags['library_dirs']) + + self.define = build_clib.build_flags['define'] + + return _build_ext.run(self) + + +class develop(_develop): + def run(self): + if not has_system_lib(): + raise DistutilsError( + "This library is not usable in 'develop' mode when using the " + "bundled libsecp256k1. See README for details.") + _develop.run(self) -classifiers = [ - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Topic :: Software Development :: Libraries", - "Topic :: Security :: Cryptography" -] setup( name="secp256k1", version="0.11.1", - description='FFI bindings to secp256k1', + + description='FFI bindings to libsecp256k1', + url='https://github.com/ludbb/secp256k1-py', author='Ludvig Broberg', author_email='lud@tutanota.com', - url='https://github.com/ludbb/secp256k1-py', license='MIT', - classifiers=classifiers, - py_modules=['secp256k1'], - setup_requires=['cffi>=1.0.0', 'pytest-runner'], - cffi_modules=['build.py:ffi'], - install_requires=['cffi>=1.0.0'], - tests_require=['pytest'] + + setup_requires=['cffi>=1.3.0', 'pytest-runner==2.6.2'], + install_requires=['cffi>=1.3.0'], + tests_require=['pytest==2.8.7'], + + packages=find_packages(exclude=('_cffi_build', '_cffi_build.*', 'libsecp256k1')), + ext_package="secp256k1", + cffi_modules=[ + "_cffi_build/build.py:ffi" + ], + + cmdclass={ + 'build_clib': build_clib, + 'build_ext': build_ext, + 'develop': develop, + 'egg_info': egg_info, + 'sdist': sdist, + }, + distclass=Distribution, + zip_safe=False, + + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries", + "Topic :: Security :: Cryptography" + ] ) diff --git a/setup_support.py b/setup_support.py new file mode 100644 index 0000000..68a2a7f --- /dev/null +++ b/setup_support.py @@ -0,0 +1,97 @@ +import glob +import os +import shutil +from contextlib import contextmanager +from tempfile import mkdtemp + +import subprocess + + +@contextmanager +def workdir(): + cwd = os.getcwd() + tmpdir = mkdtemp() + os.chdir(tmpdir) + try: + yield + finally: + os.chdir(cwd) + shutil.rmtree(tmpdir) + + +@contextmanager +def redirect(stdchannel, dest_filename): + oldstdchannel = os.dup(stdchannel.fileno()) + dest_file = open(dest_filename, 'w') + os.dup2(dest_file.fileno(), stdchannel.fileno()) + try: + yield + finally: + if oldstdchannel is not None: + os.dup2(oldstdchannel, stdchannel.fileno()) + if dest_file is not None: + dest_file.close() + + +def absolute(*paths): + op = os.path + return op.realpath(op.abspath(op.join(op.dirname(__file__), *paths))) + + +def build_flags(library, type_, path): + """Return separated build flags from pkg-config output""" + + pkg_config_path = [path] + if "PKG_CONFIG_PATH" in os.environ: + pkg_config_path.append(os.environ['PKG_CONFIG_PATH']) + if "LIB_DIR" in os.environ: + pkg_config_path.append(os.environ['LIB_DIR']) + pkg_config_path.append(os.path.join(os.environ['LIB_DIR'], "pkgconfig")) + + options = [ + "--static", + { + 'I': "--cflags-only-I", + 'L': "--libs-only-L", + 'l': "--libs-only-l" + }[type_] + ] + + return [ + flag.strip("-{}".format(type_)) + for flag + in subprocess.check_output( + ["pkg-config"] + options + [library], + env=dict(os.environ, PKG_CONFIG_PATH=":".join(pkg_config_path)) + ).decode("UTF-8").split() + ] + + +def _find_lib(): + from cffi import FFI + ffi = FFI() + try: + ffi.dlopen("secp256k1") + except OSError: + if 'LIB_DIR' in os.environ: + for path in glob.glob(os.path.join(os.environ['LIB_DIR'], "*secp256k1*")): + try: + FFI().dlopen(path) + return True + except OSError: + pass + # We couldn't locate libsecp256k1 so we'll use the bundled one + return False + else: + # If we got this far then the system library should be good enough + return True + + +_has_system_lib = None + + +def has_system_lib(): + global _has_system_lib + if _has_system_lib is None: + _has_system_lib = _find_lib() + return _has_system_lib diff --git a/tests/test_ecdsa.py b/tests/test_ecdsa.py index ecc8acc..39ab69a 100644 --- a/tests/test_ecdsa.py +++ b/tests/test_ecdsa.py @@ -130,6 +130,9 @@ def test_cli_ecdsa(): assert out.getvalue().strip() == str(False) def test_cli_ecdsa_recover(): + if not secp256k1.HAS_RECOVERABLE: + pytest.skip('secp256k1_recovery not enabled, skipping') + return parser, enc = secp256k1._parse_cli() args = parser.parse_args(['privkey', '-p']) diff --git a/tests/test_err.py b/tests/test_err.py index be16886..c67a5a5 100644 --- a/tests/test_err.py +++ b/tests/test_err.py @@ -95,6 +95,10 @@ def test_ecdsa_compact(): key.ecdsa_deserialize_compact(ser) def test_ecdsa_recoverable(): + if not secp256k1.HAS_RECOVERABLE: + pytest.skip('secp256k1_recovery not enabled, skipping') + return + key = '32a8935ffdb984a498b0f7ac8943e0d2ac084e81c809595fd19fde41522f1837' priv = secp256k1.PrivateKey(bytes(bytearray.fromhex(key))) sig = priv.ecdsa_sign_recoverable(b'hi') diff --git a/tox.ini b/tox.ini index d9de8b1..6a80e36 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,11 @@ [tox] -envlist = py27,py34 +mintoxversion = 2.0 +envlist = py{27,33,34,py}-{default,experimental} + [testenv] -deps=pytest -commands=py.test +setenv = + experimental: SECP_BUNDLED_EXPERIMENTAL=1 +deps = + pytest==2.8.7 +commands = py.test {posargs} + diff --git a/travis_install.sh b/travis_install.sh new file mode 100755 index 0000000..7bd6d2f --- /dev/null +++ b/travis_install.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [[ $BUNDLED -eq 0 ]]; then + git clone git://github.com/bitcoin/secp256k1.git libsecp256k1_ext + pushd libsecp256k1_ext + ./autogen.sh + ./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --enable-module-schnorr + make + popd +fi