diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 112719c0..61c8d941 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,47 +24,36 @@ jobs: python-version: "3.10" tox-env: py310 - name: py2.7 - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: py27 - name: py2.7 with old gmpy - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: py27_old_gmpy - name: py2.7 with old gmpy2 - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: py27_old_gmpy2 - name: py2.7 with old six - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: py27_old_six - name: py2.7 with gmpy - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: gmpypy27 - name: py2.7 with gmpy2 - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 tox-env: gmpy2py27 - - name: py3.3 - os: ubuntu-18.04 - python-version: 3.3 - tox-env: py33 - - name: py3.4 - os: ubuntu-18.04 - python-version: 3.4 - tox-env: py34 - - name: py3.5 - os: ubuntu-18.04 - python-version: 3.5 - tox-env: py35 - name: py3.6 - os: ubuntu-18.04 + os: ubuntu-latest + container: centos:8 python-version: 3.6 tox-env: py36 - name: py3.7 - os: ubuntu-latest + os: ubuntu-22.04 python-version: 3.7 tox-env: py37 - name: py3.8 @@ -89,25 +78,37 @@ jobs: tox-env: gmpy2py310 - name: py3.11 os: ubuntu-latest - python-version: '3.11.0-beta.3' + python-version: '3.11' tox-env: py311 + - name: py3.12 + os: ubuntu-latest + python-version: '3.12' + tox-env: py312 + - name: py3.13 + os: ubuntu-latest + python-version: '3.13' + tox-env: py313 - name: pypy os: ubuntu-latest python-version: pypy-2.7 tox-env: pypy - name: pypy3 - os: ubuntu-latest + os: ubuntu-22.04 python-version: pypy-3.7 tox-env: pypy3 # special configurations - name: py2.7 with instrumental - os: ubuntu-18.04 + os: ubuntu-22.04 python-version: 2.7 opt-deps: ['instrumental'] - name: code checks os: ubuntu-latest python-version: 3.9 tox-env: codechecks + - name: mutation testing + os: ubuntu-latest + python-version: '3.11' + mutation: 'true' steps: - uses: actions/checkout@v2 if: ${{ !matrix.container }} @@ -118,7 +119,7 @@ jobs: if: ${{ matrix.container }} with: fetch-depth: 50 - - name: Ensure dependencies on CentOS + - name: Ensure dependencies on CentOS:6 if: ${{ matrix.container == 'centos:6' }} run: | ls /etc/yum.repos.d/ @@ -147,12 +148,59 @@ jobs: echo installing yum clean all yum repolist all - yum install -y git make python curl gcc libffi-devel python-devel glibc-devel openssl-devel wget + yum install -y make python curl gcc libffi-devel python-devel glibc-devel openssl-devel wget curl-config curl-devel expat-devel gettext-devel zlib-devel perl-ExtUtils-MakeMaker + cd /usr/src + wget https://www.kernel.org/pub/software/scm/git/git-2.31.0.tar.gz + tar xzf git-2.31.0.tar.gz + cd git-2.31.0 + make prefix=/usr/local/git all + make prefix=/usr/local/git install + ln -fs /usr/local/git/bin/git /usr/bin/git + - name: Ensere dependenceis on CentOS:8 + if: ${{ matrix.container == 'centos:8' }} + run: | + ls /etc/yum.repos.d/ + cat /etc/yum.repos.d/CentOS-Linux-BaseOS.repo + cat /etc/yum.repos.d/CentOS-Linux-AppStream.repo + cat /etc/yum.repos.d/CentOS-Linux-Extras.repo + rm /etc/yum.repos.d/CentOS-Linux-BaseOS.repo + rm /etc/yum.repos.d/CentOS-Linux-AppStream.repo + rm /etc/yum.repos.d/CentOS-Linux-Extras.repo + cat > /etc/yum.repos.d/CentOS-Linux-BaseOS.repo < /etc/yum.repos.d/CentOS-Linux-AppStream.repo < /etc/yum.repos.d/CentOS-Linux-Extras.repo < cosmic-ray.html + - name: Archive mutation testing results + if: ${{ matrix.mutation == 'true' && github.event.pull_request }} + uses: actions/upload-artifact@v4 + with: + name: mutation-PR-coverage-report + path: cosmic-ray.html + - name: Check test coverage for PR + if: ${{ matrix.mutation == 'true' && github.event.pull_request }} + run: | + # check if executed have at most 50% survival rate + cr-rate --estimate --confidence 99.9 --fail-over 50 session-vs-master.sqlite - name: instrumental test coverage on PR if: ${{ contains(matrix.opt-deps, 'instrumental') && github.event.pull_request }} env: @@ -314,8 +425,13 @@ jobs: COVERALLS_FLAG_NAME: ${{ matrix.name }} COVERALLS_PARALLEL: true COVERALLS_SERVICE_NAME: github - run: coveralls - + PY_VERSION: ${{ matrix.python-version }} + run: | + if [[ $PY_VERSION == "2.6" ]]; then + COVERALLS_SKIP_SSL_VERIFY=1 coveralls + else + coveralls + fi coveralls: name: Indicate completion to coveralls.io needs: test @@ -331,3 +447,291 @@ jobs: COVERALLS_SERVICE_NAME: github run: | coveralls --finish + + mutation-prepare: + name: Prepare job files for the mutation runners + # use runner minutes on mutation testing only after the PR passed basic + # testing + needs: coveralls + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + if: ${{ !matrix.container }} + with: + fetch-depth: 50 + - name: save session objects + uses: actions/cache@v3 + with: + path: | + sessions/ + key: sessions-${{ github.sha }} + - name: Install cosmic-ray + run: | + pip3 install cosmic-ray + pip install pytest-timeout + - name: Install dependencies + run: | + sudo apt-get install -y sqlite3 + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: Create list of mutations + run: | + cosmic-ray init cosmic-ray.toml session.sqlite + - name: Log number of jobs created + run: | + cr-report session.sqlite | tail -n 3 + - name: Split up mutations to workers + run: | + cp session.sqlite session-to_del.sqlite + sqlite3 session-to_del.sqlite "$(cat sql/create_to_del.sql)" + mkdir sessions + for i in $(seq 0 19); do + sed "s/%SHARD%/$i/" < sql/shard-db.sql > shard.sql + cp session-to_del.sqlite session-$i.sqlite + sqlite3 session-$i.sqlite "$(cat shard.sql)" + mv session-$i.sqlite sessions/ + done + mutation-execute: + name: Execute mutation testing + needs: mutation-prepare + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: 0 + - name: 1 + - name: 2 + - name: 3 + - name: 4 + - name: 5 + - name: 6 + - name: 7 + - name: 8 + - name: 9 + - name: 10 + - name: 11 + - name: 12 + - name: 13 + - name: 14 + - name: 15 + - name: 16 + - name: 17 + - name: 18 + - name: 19 + steps: + - uses: actions/checkout@v2 + if: ${{ !matrix.container }} + with: + fetch-depth: 1 + - name: Session objects + uses: actions/cache@v3 + with: + path: | + sessions/ + key: sessions-${{ github.sha }} + - name: Session done objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-${{ matrix.name }}-done.sqlite + key: sessions-${{ github.sha }}-${{ matrix.name }}-done + - name: Install gmpy2 dependencies + run: sudo apt-get install -y libmpfr-dev libmpc-dev + - name: Install gmpy2 + run: pip install gmpy2 + - name: Install build dependencies + run: | + pip install -r build-requirements.txt + pip install cosmic-ray + pip install pytest-timeout + - name: Run mutation testing + run: | + cp sessions/session-${{ matrix.name }}.sqlite session.sqlite + systemd-run --user --scope -p MemoryMax=2G -p MemoryHigh=2G cosmic-ray exec cosmic-ray.toml session.sqlite & + cosmic_pid=$! + sleep 1 + for i in $(seq 1 10); do + if ! kill -s 0 $cosmic_pid; then + break + fi + echo $i + sleep 60 + done + kill -s 0 $cosmic_pid && kill $cosmic_pid + mkdir sessions-done/ + cp session.sqlite sessions-done/session-${{ matrix.name }}-done.sqlite + - name: Report executed + run: | + cr-report session.sqlite | tail -n 3 + mutation-combine: + name: Combine mutation testing results + needs: mutation-execute + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + if: ${{ !matrix.container }} + with: + fetch-depth: 1 + - name: Session done 0 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-0-done.sqlite + key: sessions-${{ github.sha }}-0-done + - name: Session done 1 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-1-done.sqlite + key: sessions-${{ github.sha }}-1-done + - name: Session done 2 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-2-done.sqlite + key: sessions-${{ github.sha }}-2-done + - name: Session done 3 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-3-done.sqlite + key: sessions-${{ github.sha }}-3-done + - name: Session done 4 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-4-done.sqlite + key: sessions-${{ github.sha }}-4-done + - name: Session done 5 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-5-done.sqlite + key: sessions-${{ github.sha }}-5-done + - name: Session done 6 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-6-done.sqlite + key: sessions-${{ github.sha }}-6-done + - name: Session done 7 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-7-done.sqlite + key: sessions-${{ github.sha }}-7-done + - name: Session done 8 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-8-done.sqlite + key: sessions-${{ github.sha }}-8-done + - name: Session done 9 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-9-done.sqlite + key: sessions-${{ github.sha }}-9-done + - name: Session done 10 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-10-done.sqlite + key: sessions-${{ github.sha }}-10-done + - name: Session done 11 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-11-done.sqlite + key: sessions-${{ github.sha }}-11-done + - name: Session done 12 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-12-done.sqlite + key: sessions-${{ github.sha }}-12-done + - name: Session done 13 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-13-done.sqlite + key: sessions-${{ github.sha }}-13-done + - name: Session done 14 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-14-done.sqlite + key: sessions-${{ github.sha }}-14-done + - name: Session done 15 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-15-done.sqlite + key: sessions-${{ github.sha }}-15-done + - name: Session done 16 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-16-done.sqlite + key: sessions-${{ github.sha }}-16-done + - name: Session done 17 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-17-done.sqlite + key: sessions-${{ github.sha }}-17-done + - name: Session done 18 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-18-done.sqlite + key: sessions-${{ github.sha }}-18-done + - name: Session done 19 objects + uses: actions/cache@v3 + with: + path: | + sessions-done/session-19-done.sqlite + key: sessions-${{ github.sha }}-19-done + - name: Install cosmic-ray + run: | + pip3 install cosmic-ray + pip install pytest-timeout + - name: Install dependencies + run: | + sudo apt-get install -y sqlite3 + - name: Combine worker results + run: | + cp sessions-done/session-0-done.sqlite session.sqlite + for i in $(seq 1 19); do + cp sessions-done/session-$i-done.sqlite session-to_merge.sqlite && sqlite3 session.sqlite "$(cat sql/combine.sql)" || true + done + - name: Report executed + run: | + cr-report session.sqlite | tail -n 3 + - name: Generate html report + run: | + cr-html session.sqlite > cosmic-ray.html + - name: Archive mutation testing results + uses: actions/upload-artifact@v4 + with: + name: mutation-coverage-report + path: cosmic-ray.html + - name: Get mutation score + run: | + echo "print('{0:.2f}'.format(100-$(cr-rate session.sqlite)))" > print-score.py + echo "MUT_SCORE=$(python print-score.py)" >> $GITHUB_ENV + - name: Create mutation score badge + if: ${{ !github.event.pull_request }} + uses: schneegans/dynamic-badges-action@v1.4.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: 9b6ca1f3410207fbeca785a178781651 + filename: python-ecdsa-mutation-score.json + label: mutation score + message: ${{ env.MUT_SCORE }}% + valColorRange: ${{ env.MUT_SCORE }} + maxColorRange: 100 + minColorRange: 0 + - name: Check survival estimate + run: cr-rate --estimate --fail-over 32 --confidence 99.9 session.sqlite diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..c671375c --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "59 21 * * 0" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ python ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3fcbd5ca..14dd391c 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,17 +7,13 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" - # You can also specify other tool versions: - # nodejs: "16" - # rust: "1.55" - # golang: "1.17" + python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: - configuration: docs/source/conf.py + configuration: docs/source/conf.py # If using Sphinx, optionally build your docs in additional formats such as PDF # formats: @@ -26,4 +22,5 @@ sphinx: # Optionally declare the Python requirements required to build your docs python: install: + - requirements: requirements.txt - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml index f31a7197..8729ca80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,13 @@ addons: - libmpc-dev before_cache: - rm -f $HOME/.cache/pip/log/debug.log -# place the slowest (instrumental and py2.6) first +# place the slowest (instrumental, mutation and py2.6) first matrix: include: + - python: 3.9 + dist: bionic + sudo: true + env: MUTATION=yes - python: 2.7 env: INSTRUMENTAL=yes dist: bionic @@ -125,15 +129,16 @@ install: else travis_retry pip install -r build-requirements.txt; fi - - if [[ $TOX_ENV =~ gmpy2 ]] || [[ $INSTRUMENTAL ]]; then travis_retry pip install gmpy2; fi + - if [[ $TOX_ENV =~ gmpy2 ]] || [[ $INSTRUMENTAL ]] || [[ $MUTATION ]]; then travis_retry pip install gmpy2; fi - if [[ $TOX_ENV =~ gmpyp ]]; then travis_retry pip install gmpy; fi - if [[ $INSTRUMENTAL ]]; then travis_retry pip install instrumental; fi + - if [[ $MUTATION ]]; then travis_retry pip install cosmic-ray; fi - pip list script: - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi - - if [[ $TOX_ENV =~ gmpy2 ]]; then tox -e speedgmpy2; fi - - if [[ $TOX_ENV =~ gmpyp ]]; then tox -e speedgmpy; fi - - if ! [[ $TOX_ENV =~ gmpy ]]; then tox -e speed; fi + - if [[ $TOX_ENV =~ gmpy2 ]] && [[ -z $MUTATION ]]; then tox -e speedgmpy2; fi + - if [[ $TOX_ENV =~ gmpyp ]] && [[ -z $MUTATION ]]; then tox -e speedgmpy; fi + - if ! [[ $TOX_ENV =~ gmpy ]] && [[ -z $MUTATION ]]; then tox -e speed; fi - | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then git checkout $PR_FIRST^ @@ -155,6 +160,26 @@ script: if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then instrumental -f .instrumental.cov -s | python diff-instrumental.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 fi + # cosmic-ray (mutation testing) runs + - if [[ $MUTATION ]]; then cosmic-ray init cosmic-ray.toml session.sqlite; fi + - if [[ $MUTATION ]]; then cosmic-ray baseline --report session.sqlite; fi + - if [[ $MUTATION ]]; then cr-report --show-output session.baseline.sqlite; fi + - | + if [[ $MUTATION ]]; then + cosmic-ray exec session.sqlite & + COSMIC_PID=$! + # make sure that we output something every 5 minutes (otherwise travis will kill us) + while kill -s 0 $COSMIC_PID; do + sleep 300 + cr-report session.sqlite | tail -n 3; + done & + REPORT_PID=$! + # kill exec after 40 minutes + (sleep $((60*40)); kill $COSMIC_PID) & + fi + - if [[ $MUTATION ]]; then wait $COSMIC_PID ; kill $REPORT_PID ; true; fi + - if [[ $MUTATION ]]; then cr-report --show-output session.sqlite | tail -n 40; fi + - if [[ $MUTATION ]]; then cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite; fi after_success: - - if [[ -z $INSTRUMENTAL ]]; then coveralls; fi + - if [[ -z $INSTRUMENTAL && -z $MUTATION ]]; then coveralls; fi diff --git a/NEWS b/NEWS index cba5f930..0d066f4b 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,56 @@ +* Release 0.19.1 (13 Mar 2025) + +New API: +* ``der.remove_implitic`` and ``der.encode_implicit`` for decoding and + encoding DER IMPLICIT values with custom tag values and arbitrary + classes + +Bug fixes: +* Minor fixes around arithmetic with curves that have non-prime order + (useful for experimentation, not practical deployments) +* Fix arithmetic to work with curves that have (0, 0) on the curve +* Fix canonicalization of signatures when ``s`` is just slightly + above half of curve order + +Maintenance: +* Dropped official support for Python 3.5 (again, issues with CI, support + for Python 2.6 and Python 2.7 is unchanged) +* Officialy support Python 3.12 and 3.13 (add them to CI) +* Removal of few more unnecessary `six.b` literals (Alexandre Detiste) +* Fix typos in warning messages + + +* Release 0.19.0 (08 Apr 2024) + +New API: +* `to_ssh` in `VerifyingKey` and `SigningKey`, supports Ed25519 keys only + (Pablo Mazzini) + +New features: +* Support for twisted Brainpool curves + +Doc fix: +* Fix curve equation in glossary +* Documentation for signature encoding and signature decoding functions + +Maintenance: +* Dropped official support for 3.3 and 3.4 (because of problems running them + in CI, not because it's actually incompatible; support for 2.6 and 2.7 is + unaffected) +* Fixes aroung hypothesis parameters +* Officially support Python 3.11 and 3.12 +* Small updates to test suite to make it work with 3.11 and 3.12 and new + releases of test dependencies +* Dropped the internal `_rwlock` module as it's unused +* Added mutation testing to CI, lots of speed-ups to the test suite + to make it happen +* Removal of unnecessary `six.b` literals (Alexandre Detiste) + +Deprecations: +* `int_to_string`, `string_to_int`, and `digest_integer` from `ecdsa.ecdsa` + module are now considered deprecated, they will be removed in a future + release + * Release 0.18.0 (09 Jul 2022) New API: diff --git a/README.md b/README.md index 11a00160..0bf02bde 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # Pure-Python ECDSA and ECDH -[![Build Status](https://github.com/tlsfuzzer/python-ecdsa/workflows/GitHub%20CI/badge.svg?branch=master)](https://github.com/tlsfuzzer/python-ecdsa/actions?query=workflow%3A%22GitHub+CI%22+branch%3Amaster) +[![GitHub CI](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml/badge.svg)](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/ci.yml) [![Documentation Status](https://readthedocs.org/projects/ecdsa/badge/?version=latest)](https://ecdsa.readthedocs.io/en/latest/?badge=latest) [![Coverage Status](https://coveralls.io/repos/github/tlsfuzzer/python-ecdsa/badge.svg?branch=master)](https://coveralls.io/github/tlsfuzzer/python-ecdsa?branch=master) ![condition coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomato42/9b6ca1f3410207fbeca785a178781651/raw/python-ecdsa-condition-coverage.json) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/tlsfuzzer/python-ecdsa.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tlsfuzzer/python-ecdsa/context:python) -[![Total alerts](https://img.shields.io/lgtm/alerts/g/tlsfuzzer/python-ecdsa.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tlsfuzzer/python-ecdsa/alerts/) +![mutation score](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/tomato42/9b6ca1f3410207fbeca785a178781651/raw/python-ecdsa-mutation-score.json) +[![CodeQL](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/codeql.yml/badge.svg)](https://github.com/tlsfuzzer/python-ecdsa/actions/workflows/codeql.yml) [![Latest Version](https://img.shields.io/pypi/v/ecdsa.svg?style=flat)](https://pypi.python.org/pypi/ecdsa/) ![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg?style=flat) @@ -45,7 +45,7 @@ curves over prime fields. ## Dependencies This library uses only Python and the 'six' package. It is compatible with -Python 2.6, 2.7, and 3.3+. It also supports execution on alternative +Python 2.6, 2.7, and 3.6+. It also supports execution on alternative implementations like pypy and pypy3. If `gmpy2` or `gmpy` is installed, they will be used for faster arithmetic. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..f5dbcb48 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,48 @@ +# Security Policy + +## Supported Versions + +Only the latest released version is supported. +Alpha and beta releases are always unsupported with security fixes. + +The project uses semantic versioning, as such, minor version changes are API compatible. + +| Version | Supported | +| -------- | ------------------ | +| 0.18.x | :white_check_mark: | +| < 0.18 | :x: | + +## Support Scope + +This library was not designed with security in mind. If you are processing data that needs +to be protected we suggest you use a quality wrapper around OpenSSL. +[`pyca/cryptography`](https://cryptography.io/) is one example of such a wrapper. +The primary use-case of this library is as a portable library for interoperability testing +and as a teaching tool. + +**This library does not protect against side-channel attacks.** + +Do not allow attackers to measure how long it takes you to generate a key pair or sign a message. +Do not allow attackers to run code on the same physical machine when key pair generation or +signing is taking place (this includes virtual machines). +Do not allow attackers to measure how much power your computer uses while generating the key pair +or signing a message. Do not allow attackers to measure RF interference coming from your computer +while generating a key pair or signing a message. Note: just loading the private key will cause +key pair generation. Other operations or attack vectors may also be vulnerable to attacks. +For a sophisticated attacker observing just one operation with a private key will be sufficient +to completely reconstruct the private key. + +Fixes for side-channel vulerabilities will not be developed. + +Please also note that any Pure-python cryptographic library will be vulnerable to the same +side-channel attacks. This is because Python does not provide side-channel secure primitives +(with the exception of [`hmac.compare_digest()`](https://docs.python.org/3/library/hmac.html#hmac.compare_digest)), +making side-channel secure programming impossible. + +This library depends upon a strong source of random numbers. Do not use it on a system +where `os.urandom()` does not provide cryptographically secure random numbers. + +## Reporting a Vulnerability + +If you find a security vulnerability in this library, you can report it using the "Report a vulnerability" button on the Security tab in github UI. +Alternatively, you can contact the project maintainer at hkario at redhat dot com. diff --git a/build-requirements-3.3.txt b/build-requirements-3.3.txt deleted file mode 100644 index d817d2b3..00000000 --- a/build-requirements-3.3.txt +++ /dev/null @@ -1,9 +0,0 @@ -git+https://github.com/tomato42/coveralls-python.git@add-py26#egg=coveralls -pluggy<0.6 -tox<3 -wheel<0.30 -virtualenv==15.2.0 -enum34 -hypothesis<3.44 -coverage<5.0 -urllib3<=1.25.8 diff --git a/build-requirements-3.4.txt b/build-requirements-3.4.txt deleted file mode 100644 index ee734d11..00000000 --- a/build-requirements-3.4.txt +++ /dev/null @@ -1,7 +0,0 @@ -tox -git+https://github.com/tomato42/coveralls-python.git@add-py26#egg=coveralls -hypothesis -pytest>=4.6.0 -PyYAML<5.3 -coverage -attrs<21 diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..6c163acf --- /dev/null +++ b/conftest.py @@ -0,0 +1,11 @@ +def pytest_addoption(parser): + parser.addoption( + "--fast", action="store_true", default=False, help="run tests fast" + ) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", + "slow: mark test as slow to run (deselect with '-m \"not slow\"')", + ) diff --git a/cosmic-ray-12way.sh b/cosmic-ray-12way.sh new file mode 100644 index 00000000..7b16fb2d --- /dev/null +++ b/cosmic-ray-12way.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -e + +cosmic-ray init cosmic-ray-12way.toml session.sqlite +cosmic-ray baseline --session-file session.baseline.sqlite cosmic-ray-12way.toml +cr-report --show-output session.baseline.sqlite +# some mutations cause huge memory use, so put it in a cgroup +# systemd-run --user --scope -p MemoryMax=8G -p MemoryHigh=8G cr-http-workers cosmic-ray-12way.toml . +cr-http-workers cosmic-ray-12way.toml . +cosmic-ray exec cosmic-ray-12way.toml session.sqlite +cr-report session.sqlite +cr-html session.sqlite > session.html +cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite diff --git a/cosmic-ray-12way.toml b/cosmic-ray-12way.toml new file mode 100644 index 00000000..c1e8e55c --- /dev/null +++ b/cosmic-ray-12way.toml @@ -0,0 +1,28 @@ +[cosmic-ray] +module-path = "src" +timeout = 20.0 +excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*'] +test-command = "pytest --timeout=30 -x --fast -m 'not slow' src/" + +[cosmic-ray.distributor] +name = "http" + +[cosmic-ray.distributor.http] +worker-urls = [ + "http://localhost:9870", + "http://localhost:9871", + "http://localhost:9872", + "http://localhost:9873", + "http://localhost:9874", + "http://localhost:9875", + "http://localhost:9876", + "http://localhost:9877", + "http://localhost:9878", + "http://localhost:9879", + "http://localhost:9880", + "http://localhost:9881", + "http://localhost:9882" +] + +[cosmic-ray.filters.git-filter] +branch = "master" diff --git a/cosmic-ray.sh b/cosmic-ray.sh new file mode 100644 index 00000000..51181a3a --- /dev/null +++ b/cosmic-ray.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e + +cosmic-ray init cosmic-ray.toml session.sqlite +cosmic-ray baseline --session-file session.baseline.sqlite cosmic-ray.toml +cr-report --show-output session.baseline.sqlite +# some mutations cause huge memory use, so put it in a cgroup +# systemd-run --user --scope -p MemoryMax=2G -p MemoryHigh=2G cosmic-ray exec cosmic-ray.toml session.sqlite +cosmic-ray exec cosmic-ray.toml session.sqlite +cr-report session.sqlite +cr-html session.sqlite > session.html +cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite diff --git a/cosmic-ray.toml b/cosmic-ray.toml new file mode 100644 index 00000000..af40c3cb --- /dev/null +++ b/cosmic-ray.toml @@ -0,0 +1,11 @@ +[cosmic-ray] +module-path = "src" +timeout = 20.0 +excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*'] +test-command = "pytest --timeout 30 -x --fast -m 'not slow' src/" + +[cosmic-ray.distributor] +name = "local" + +[cosmic-ray.filters.git-filter] +branch = "master" diff --git a/docs/requirements.txt b/docs/requirements.txt index ffe2fce4..6c5d5d44 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1 @@ -six +sphinx-rtd-theme diff --git a/docs/source/conf.py b/docs/source/conf.py index 0eaea411..d86ea492 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,11 +62,11 @@ html_static_path = ["_static"] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {"https://docs.python.org/": None} +intersphinx_mapping = { + "python": ("https://docs.python.org/", None), +} autodoc_default_options = { "undoc-members": True, "inherited-members": True, } - -intersphinx_mapping = {"https://docs.python.org/": None} diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst index 54866730..4aebb532 100644 --- a/docs/source/glossary.rst +++ b/docs/source/glossary.rst @@ -88,5 +88,5 @@ Glossary ``tuple``, ``set``, ``frozenset``, etc. short Weierstrass curve - A curve with the curve equation: :math:`x^2=y^3+ax+b`. Most popular + A curve with the curve equation: :math:`y^2=x^3+ax+b`. Most popular curves use equation of this format (e.g. NIST256p). diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..ffe2fce4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +six diff --git a/setup.py b/setup.py index f6a1dd1e..618f5e6d 100755 --- a/setup.py +++ b/setup.py @@ -27,22 +27,22 @@ package_dir={"": "src"}, license="MIT", cmdclass=commands, - python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*", + python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " + "!=3.5.*", classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], install_requires=["six>=1.9.0"], extras_require={"gmpy2": "gmpy2", "gmpy": "gmpy"}, diff --git a/speed.py b/speed.py index 3ed72c49..a3f9e59c 100644 --- a/speed.py +++ b/speed.py @@ -1,4 +1,3 @@ -import six import timeit from ecdsa.curves import curves @@ -43,9 +42,9 @@ def do(setup_statements, statement): ) for curve in [i.name for i in curves]: - S1 = "import six; from ecdsa import SigningKey, %s" % curve + S1 = "from ecdsa import SigningKey, %s" % curve S2 = "sk = SigningKey.generate(%s)" % curve - S3 = "msg = six.b('msg')" + S3 = "msg = b'msg'" S4 = "sig = sk.sign(msg)" S5 = "vk = sk.get_verifying_key()" S6 = "vk.precompute()" @@ -61,7 +60,7 @@ def do(setup_statements, statement): import ecdsa c = getattr(ecdsa, curve) - sig = ecdsa.SigningKey.generate(c).sign(six.b("msg")) + sig = ecdsa.SigningKey.generate(c).sign(b"msg") print( prnt_form.format( name=curve, diff --git a/sql/combine.sql b/sql/combine.sql new file mode 100644 index 00000000..d2a4490d --- /dev/null +++ b/sql/combine.sql @@ -0,0 +1,7 @@ +attach 'session-to_merge.sqlite' as toMerge; +BEGIN; + insert into work_items select * from toMerge.work_items; + insert into mutation_specs select * from toMerge.mutation_specs; + insert into work_results select * from toMerge.work_results; +COMMIT; +detach toMerge; diff --git a/sql/create_to_del.sql b/sql/create_to_del.sql new file mode 100644 index 00000000..956d1a28 --- /dev/null +++ b/sql/create_to_del.sql @@ -0,0 +1,2 @@ +create table to_del (job_id VARCHAR NOT NULL, id INTEGER PRIMARY KEY); +insert into to_del select *, ROWID from work_items; diff --git a/sql/shard-db.sql b/sql/shard-db.sql new file mode 100644 index 00000000..7460bd31 --- /dev/null +++ b/sql/shard-db.sql @@ -0,0 +1,3 @@ +delete from mutation_specs where job_id in (select job_id from to_del where to_del.ID % 20 != %SHARD%); +delete from work_items where job_id in (select job_id from to_del where to_del.ID % 20 != %SHARD%); +drop table to_del; diff --git a/src/ecdsa/__init__.py b/src/ecdsa/__init__.py index ce8749aa..342538e0 100644 --- a/src/ecdsa/__init__.py +++ b/src/ecdsa/__init__.py @@ -28,6 +28,13 @@ SECP160r1, Ed25519, Ed448, + BRAINPOOLP160t1, + BRAINPOOLP192t1, + BRAINPOOLP224t1, + BRAINPOOLP256t1, + BRAINPOOLP320t1, + BRAINPOOLP384t1, + BRAINPOOLP512t1, ) from .ecdh import ( ECDH, @@ -84,6 +91,13 @@ Ed25519, Ed448, six.b(""), + BRAINPOOLP160t1, + BRAINPOOLP192t1, + BRAINPOOLP224t1, + BRAINPOOLP256t1, + BRAINPOOLP320t1, + BRAINPOOLP384t1, + BRAINPOOLP512t1, ] del _hush_pyflakes diff --git a/src/ecdsa/_compat.py b/src/ecdsa/_compat.py index 83d41a5f..4558e33a 100644 --- a/src/ecdsa/_compat.py +++ b/src/ecdsa/_compat.py @@ -91,28 +91,13 @@ def int_to_bytes(val, length=None, byteorder="big"): raise ValueError("Only 'big' or 'little' endian supported") else: - if sys.version_info < (3, 4): # pragma: no branch - # on python 3.3 hmac.hmac.update() accepts only bytes, on newer - # versions it does accept memoryview() also - def hmac_compat(data): - if not isinstance(data, bytes): # pragma: no branch - return bytes(data) - return data - - def normalise_bytes(buffer_object): - """Cast the input into array of bytes.""" - if not buffer_object: - return b"" - return memoryview(buffer_object).cast("B") - else: - - def hmac_compat(data): - return data + def hmac_compat(data): + return data - def normalise_bytes(buffer_object): - """Cast the input into array of bytes.""" - return memoryview(buffer_object).cast("B") + def normalise_bytes(buffer_object): + """Cast the input into array of bytes.""" + return memoryview(buffer_object).cast("B") def compat26_str(val): return val @@ -142,7 +127,7 @@ def int_to_bytes(val, length=None, byteorder="big"): if length is None: length = byte_length(val) # for gmpy we need to convert back to native int - if type(val) != int: + if not isinstance(val, int): val = int(val) return bytearray(val.to_bytes(length=length, byteorder=byteorder)) diff --git a/src/ecdsa/_rwlock.py b/src/ecdsa/_rwlock.py deleted file mode 100644 index 010e4981..00000000 --- a/src/ecdsa/_rwlock.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright Mateusz Kobos, (c) 2011 -# https://code.activestate.com/recipes/577803-reader-writer-lock-with-priority-for-writers/ -# released under the MIT licence - -import threading - - -__author__ = "Mateusz Kobos" - - -class RWLock: - """ - Read-Write locking primitive - - Synchronization object used in a solution of so-called second - readers-writers problem. In this problem, many readers can simultaneously - access a share, and a writer has an exclusive access to this share. - Additionally, the following constraints should be met: - 1) no reader should be kept waiting if the share is currently opened for - reading unless a writer is also waiting for the share, - 2) no writer should be kept waiting for the share longer than absolutely - necessary. - - The implementation is based on [1, secs. 4.2.2, 4.2.6, 4.2.7] - with a modification -- adding an additional lock (C{self.__readers_queue}) - -- in accordance with [2]. - - Sources: - [1] A.B. Downey: "The little book of semaphores", Version 2.1.5, 2008 - [2] P.J. Courtois, F. Heymans, D.L. Parnas: - "Concurrent Control with 'Readers' and 'Writers'", - Communications of the ACM, 1971 (via [3]) - [3] http://en.wikipedia.org/wiki/Readers-writers_problem - """ - - def __init__(self): - """ - A lock giving an even higher priority to the writer in certain - cases (see [2] for a discussion). - """ - self.__read_switch = _LightSwitch() - self.__write_switch = _LightSwitch() - self.__no_readers = threading.Lock() - self.__no_writers = threading.Lock() - self.__readers_queue = threading.Lock() - - def reader_acquire(self): - self.__readers_queue.acquire() - self.__no_readers.acquire() - self.__read_switch.acquire(self.__no_writers) - self.__no_readers.release() - self.__readers_queue.release() - - def reader_release(self): - self.__read_switch.release(self.__no_writers) - - def writer_acquire(self): - self.__write_switch.acquire(self.__no_readers) - self.__no_writers.acquire() - - def writer_release(self): - self.__no_writers.release() - self.__write_switch.release(self.__no_readers) - - -class _LightSwitch: - """An auxiliary "light switch"-like object. The first thread turns on the - "switch", the last one turns it off (see [1, sec. 4.2.2] for details).""" - - def __init__(self): - self.__counter = 0 - self.__mutex = threading.Lock() - - def acquire(self, lock): - self.__mutex.acquire() - self.__counter += 1 - if self.__counter == 1: - lock.acquire() - self.__mutex.release() - - def release(self, lock): - self.__mutex.acquire() - self.__counter -= 1 - if self.__counter == 0: - lock.release() - self.__mutex.release() diff --git a/src/ecdsa/curves.py b/src/ecdsa/curves.py index 1119ee5a..38e3a758 100644 --- a/src/ecdsa/curves.py +++ b/src/ecdsa/curves.py @@ -26,12 +26,19 @@ "curve_by_name", "SECP256k1", "BRAINPOOLP160r1", + "BRAINPOOLP160t1", "BRAINPOOLP192r1", + "BRAINPOOLP192t1", "BRAINPOOLP224r1", + "BRAINPOOLP224t1", "BRAINPOOLP256r1", + "BRAINPOOLP256t1", "BRAINPOOLP320r1", + "BRAINPOOLP320t1", "BRAINPOOLP384r1", + "BRAINPOOLP384t1", "BRAINPOOLP512r1", + "BRAINPOOLP512t1", "PRIME_FIELD_OID", "CHARACTERISTIC_TWO_FIELD_OID", "Ed25519", @@ -375,6 +382,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP160t1 = Curve( + "BRAINPOOLP160t1", + ecdsa.curve_brainpoolp160t1, + ecdsa.generator_brainpoolp160t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 2), + "brainpoolP160t1", +) + + BRAINPOOLP192r1 = Curve( "BRAINPOOLP192r1", ecdsa.curve_brainpoolp192r1, @@ -384,6 +400,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP192t1 = Curve( + "BRAINPOOLP192t1", + ecdsa.curve_brainpoolp192t1, + ecdsa.generator_brainpoolp192t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 4), + "brainpoolP192t1", +) + + BRAINPOOLP224r1 = Curve( "BRAINPOOLP224r1", ecdsa.curve_brainpoolp224r1, @@ -393,6 +418,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP224t1 = Curve( + "BRAINPOOLP224t1", + ecdsa.curve_brainpoolp224t1, + ecdsa.generator_brainpoolp224t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 6), + "brainpoolP224t1", +) + + BRAINPOOLP256r1 = Curve( "BRAINPOOLP256r1", ecdsa.curve_brainpoolp256r1, @@ -402,6 +436,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP256t1 = Curve( + "BRAINPOOLP256t1", + ecdsa.curve_brainpoolp256t1, + ecdsa.generator_brainpoolp256t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 8), + "brainpoolP256t1", +) + + BRAINPOOLP320r1 = Curve( "BRAINPOOLP320r1", ecdsa.curve_brainpoolp320r1, @@ -411,6 +454,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP320t1 = Curve( + "BRAINPOOLP320t1", + ecdsa.curve_brainpoolp320t1, + ecdsa.generator_brainpoolp320t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 10), + "brainpoolP320t1", +) + + BRAINPOOLP384r1 = Curve( "BRAINPOOLP384r1", ecdsa.curve_brainpoolp384r1, @@ -420,6 +472,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP384t1 = Curve( + "BRAINPOOLP384t1", + ecdsa.curve_brainpoolp384t1, + ecdsa.generator_brainpoolp384t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 12), + "brainpoolP384t1", +) + + BRAINPOOLP512r1 = Curve( "BRAINPOOLP512r1", ecdsa.curve_brainpoolp512r1, @@ -429,6 +490,15 @@ def from_pem(cls, string, valid_encodings=None): ) +BRAINPOOLP512t1 = Curve( + "BRAINPOOLP512t1", + ecdsa.curve_brainpoolp512t1, + ecdsa.generator_brainpoolp512t1, + (1, 3, 36, 3, 3, 2, 8, 1, 1, 14), + "brainpoolP512t1", +) + + Ed25519 = Curve( "Ed25519", eddsa.curve_ed25519, @@ -466,6 +536,13 @@ def from_pem(cls, string, valid_encodings=None): SECP160r1, Ed25519, Ed448, + BRAINPOOLP160t1, + BRAINPOOLP192t1, + BRAINPOOLP224t1, + BRAINPOOLP256t1, + BRAINPOOLP320t1, + BRAINPOOLP384t1, + BRAINPOOLP512t1, ] diff --git a/src/ecdsa/der.py b/src/ecdsa/der.py index 8b27941c..5d35d698 100644 --- a/src/ecdsa/der.py +++ b/src/ecdsa/der.py @@ -4,8 +4,8 @@ import base64 import warnings from itertools import chain -from six import int2byte, b, text_type -from ._compat import str_idx_as_int +from six import int2byte, text_type +from ._compat import compat26_str, str_idx_as_int class UnexpectedDER(Exception): @@ -16,20 +16,46 @@ def encode_constructed(tag, value): return int2byte(0xA0 + tag) + encode_length(len(value)) + value +def encode_implicit(tag, value, cls="context-specific"): + """ + Encode and IMPLICIT value using :term:`DER`. + + :param int tag: the tag value to encode, must be between 0 an 31 inclusive + :param bytes value: the data to encode + :param str cls: the class of the tag to encode: "application", + "context-specific", or "private" + :rtype: bytes + """ + if cls not in ("application", "context-specific", "private"): + raise ValueError("invalid tag class") + if tag > 31: + raise ValueError("Long tags not supported") + + if cls == "application": + tag_class = 0b01000000 + elif cls == "context-specific": + tag_class = 0b10000000 + else: + assert cls == "private" + tag_class = 0b11000000 + + return int2byte(tag_class + tag) + encode_length(len(value)) + value + + def encode_integer(r): assert r >= 0 # can't support negative numbers yet h = ("%x" % r).encode() if len(h) % 2: - h = b("0") + h + h = b"0" + h s = binascii.unhexlify(h) num = str_idx_as_int(s, 0) if num <= 0x7F: - return b("\x02") + encode_length(len(s)) + s + return b"\x02" + encode_length(len(s)) + s else: # DER integers are two's complement, so if the first byte is # 0x80-0xff then we need an extra 0x00 byte to prevent it from # looking negative. - return b("\x02") + encode_length(len(s) + 1) + b("\x00") + s + return b"\x02" + encode_length(len(s) + 1) + b"\x00" + s # sentry object to check if an argument was specified (used to detect @@ -91,11 +117,11 @@ def encode_bitstring(s, unused=_sentry): raise ValueError("unused bits must be zeros in DER") encoded_unused = int2byte(unused) len_extra = 1 - return b("\x03") + encode_length(len(s) + len_extra) + encoded_unused + s + return b"\x03" + encode_length(len(s) + len_extra) + encoded_unused + s def encode_octet_string(s): - return b("\x04") + encode_length(len(s)) + s + return b"\x04" + encode_length(len(s)) + s def encode_oid(first, second, *pieces): @@ -111,7 +137,7 @@ def encode_oid(first, second, *pieces): def encode_sequence(*encoded_pieces): total_len = sum([len(p) for p in encoded_pieces]) - return b("\x30") + encode_length(total_len) + b("").join(encoded_pieces) + return b"\x30" + encode_length(total_len) + b"".join(encoded_pieces) def encode_number(n): @@ -122,7 +148,7 @@ def encode_number(n): if not b128_digits: b128_digits.append(0) b128_digits[-1] &= 0x7F - return b("").join([int2byte(d) for d in b128_digits]) + return b"".join([int2byte(d) for d in b128_digits]) def is_sequence(string): @@ -137,6 +163,53 @@ def remove_constructed(string): ) tag = s0 & 0x1F length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") + body = string[1 + llen : 1 + llen + length] + rest = string[1 + llen + length :] + return tag, body, rest + + +def remove_implicit(string, exp_class="context-specific"): + """ + Removes an IMPLICIT tagged value from ``string`` following :term:`DER`. + + :param bytes string: a byte string that can have one or more + DER elements. + :param str exp_class: the expected tag class of the implicitly + encoded value. Possible values are: "context-specific", "application", + and "private". + :return: a tuple with first value being the tag without indicator bits, + second being the raw bytes of the value and the third one being + remaining bytes (or an empty string if there are none) + :rtype: tuple(int,bytes,bytes) + """ + if exp_class not in ("context-specific", "application", "private"): + raise ValueError("invalid `exp_class` value") + if exp_class == "application": + tag_class = 0b01000000 + elif exp_class == "context-specific": + tag_class = 0b10000000 + else: + assert exp_class == "private" + tag_class = 0b11000000 + tag_mask = 0b11000000 + + s0 = str_idx_as_int(string, 0) + + if (s0 & tag_mask) != tag_class: + raise UnexpectedDER( + "wanted class {0}, got 0x{1:02x} tag".format(exp_class, s0) + ) + if s0 & 0b00100000 != 0: + raise UnexpectedDER( + "wanted type primitive, got 0x{0:02x} tag".format(s0) + ) + + tag = s0 & 0x1F + length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") body = string[1 + llen : 1 + llen + length] rest = string[1 + llen + length :] return tag, body, rest @@ -160,6 +233,8 @@ def remove_octet_string(string): n = str_idx_as_int(string, 0) raise UnexpectedDER("wanted type 'octetstring' (0x04), got 0x%02x" % n) length, llen = read_length(string[1:]) + if length > len(string) - 1 - llen: + raise UnexpectedDER("Length longer than the provided buffer") body = string[1 + llen : 1 + llen + length] rest = string[1 + llen + length :] return body, rest @@ -254,7 +329,7 @@ def encode_length(l): return int2byte(l) s = ("%x" % l).encode() if len(s) % 2: - s = b("0") + s + s = b"0" + s s = binascii.unhexlify(s) llen = len(s) return int2byte(0x80 | llen) + s @@ -389,21 +464,21 @@ def unpem(pem): if isinstance(pem, text_type): # pragma: no branch pem = pem.encode() - d = b("").join( + d = b"".join( [ l.strip() - for l in pem.split(b("\n")) - if l and not l.startswith(b("-----")) + for l in pem.split(b"\n") + if l and not l.startswith(b"-----") ] ) return base64.b64decode(d) def topem(der, name): - b64 = base64.b64encode(der) + b64 = base64.b64encode(compat26_str(der)) lines = [("-----BEGIN %s-----\n" % name).encode()] lines.extend( - [b64[start : start + 64] + b("\n") for start in range(0, len(b64), 64)] + [b64[start : start + 76] + b"\n" for start in range(0, len(b64), 76)] ) lines.append(("-----END %s-----\n" % name).encode()) - return b("").join(lines) + return b"".join(lines) diff --git a/src/ecdsa/ecdsa.py b/src/ecdsa/ecdsa.py index 33282816..f7109659 100644 --- a/src/ecdsa/ecdsa.py +++ b/src/ecdsa/ecdsa.py @@ -40,16 +40,16 @@ # Verifying a signature for a hash value: if pubkey.verifies( hash, signature ): - print_("Demo verification succeeded.") + print("Demo verification succeeded.") else: - print_("*** Demo verification failed.") + print("*** Demo verification failed.") # Verification fails if the hash value is modified: if pubkey.verifies( hash-1, signature ): - print_("**** Demo verification failed to reject tampered hash.") + print("**** Demo verification failed to reject tampered hash.") else: - print_("Demo verification correctly rejected tampered hash.") + print("Demo verification correctly rejected tampered hash.") Revision history: 2005.12.31 - Initial version. @@ -64,7 +64,8 @@ modified as part of the python-ecdsa package. """ -from six import int2byte, b +import warnings +from six import int2byte from . import ellipticcurve from . import numbertheory from .util import bit_length @@ -269,11 +270,17 @@ def sign(self, hash, random_k): return Signature(r, s) -def int_to_string(x): +def int_to_string(x): # pragma: no cover """Convert integer x into a string of bytes, as per X9.62.""" + # deprecated in 0.19 + warnings.warn( + "Function is unused in library code. If you use this code, " + "change to util.number_to_string.", + DeprecationWarning, + ) assert x >= 0 if x == 0: - return b("\0") + return b"\0" result = [] while x: ordinal = x & 0xFF @@ -281,11 +288,17 @@ def int_to_string(x): x >>= 8 result.reverse() - return b("").join(result) + return b"".join(result) -def string_to_int(s): +def string_to_int(s): # pragma: no cover """Convert a string of bytes into an integer, as per X9.62.""" + # deprecated in 0.19 + warnings.warn( + "Function is unused in library code. If you use this code, " + "change to util.string_to_number.", + DeprecationWarning, + ) result = 0 for c in s: if not isinstance(c, int): @@ -294,9 +307,16 @@ def string_to_int(s): return result -def digest_integer(m): +def digest_integer(m): # pragma: no cover """Convert an integer into a string of bytes, compute its SHA-1 hash, and convert the result to an integer.""" + # deprecated in 0.19 + warnings.warn( + "Function is unused in library code. If you use this code, " + "change to a one-liner with util.number_to_string and " + "util.string_to_number methods.", + DeprecationWarning, + ) # # I don't expect this function to be used much. I wrote # it in order to be able to duplicate the examples @@ -654,6 +674,18 @@ def point_is_valid(generator, x, y): curve_brainpoolp160r1, _Gx, _Gy, 1, _q, generator=True ) +# Brainpool P-160-t1 +_a = 0xE95E4A5F737059DC60DFC7AD95B3D8139515620C +_b = 0x7A556B6DAE535B7B51ED2C4D7DAA7A0B5C55F380 +# _z = 0x24DBFF5DEC9B986BBFE5295A29BFBAE45E0F5D0B +_Gx = 0xB199B13B9B34EFC1397E64BAEB05ACC265FF2378 +_Gy = 0xADD6718B7C7C1961F0991B842443772152C9E0AD +_q = 0xE95E4A5F737059DC60DF5991D45029409E60FC09 +curve_brainpoolp160t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp160t1 = ellipticcurve.PointJacobi( + curve_brainpoolp160t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-192-r1 _a = 0x6A91174076B1E0E19C39C031FE8685C1CAE040E5C69A28EF _b = 0x469A28EF7C28CCA3DC721D044F4496BCCA7EF4146FBF25C9 @@ -667,6 +699,19 @@ def point_is_valid(generator, x, y): curve_brainpoolp192r1, _Gx, _Gy, 1, _q, generator=True ) +# Brainpool P-192-t1 +_a = 0xC302F41D932A36CDA7A3463093D18DB78FCE476DE1A86294 +_b = 0x13D56FFAEC78681E68F9DEB43B35BEC2FB68542E27897B79 +# _z = 0x1B6F5CC8DB4DC7AF19458A9CB80DC2295E5EB9C3732104CB +_Gx = 0x3AE9E58C82F63C30282E1FE7BBF43FA72C446AF6F4618129 +_Gy = 0x097E2C5667C2223A902AB5CA449D0084B7E5B3DE7CCC01C9 +_q = 0xC302F41D932A36CDA7A3462F9E9E916B5BE8F1029AC4ACC1 + +curve_brainpoolp192t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp192t1 = ellipticcurve.PointJacobi( + curve_brainpoolp192t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-224-r1 _a = 0x68A5E62CA9CE6C1C299803A6C1530B514E182AD8B0042A59CAD29F43 _b = 0x2580F63CCFE44138870713B1A92369E33E2135D266DBB372386C400B @@ -680,6 +725,19 @@ def point_is_valid(generator, x, y): curve_brainpoolp224r1, _Gx, _Gy, 1, _q, generator=True ) +# Brainpool P-224-t1 +_a = 0xD7C134AA264366862A18302575D1D787B09F075797DA89F57EC8C0FC +_b = 0x4B337D934104CD7BEF271BF60CED1ED20DA14C08B3BB64F18A60888D +# _z = 0x2DF271E14427A346910CF7A2E6CFA7B3F484E5C2CCE1C8B730E28B3F +_Gx = 0x6AB1E344CE25FF3896424E7FFE14762ECB49F8928AC0C76029B4D580 +_Gy = 0x0374E9F5143E568CD23F3F4D7C0D4B1E41C8CC0D1C6ABD5F1A46DB4C +_q = 0xD7C134AA264366862A18302575D0FB98D116BC4B6DDEBCA3A5A7939F + +curve_brainpoolp224t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp224t1 = ellipticcurve.PointJacobi( + curve_brainpoolp224t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-256-r1 _a = 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9 _b = 0x26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6 @@ -693,6 +751,19 @@ def point_is_valid(generator, x, y): curve_brainpoolp256r1, _Gx, _Gy, 1, _q, generator=True ) +# Brainpool P-256-t1 +_a = 0xA9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5374 +_b = 0x662C61C430D84EA4FE66A7733D0B76B7BF93EBC4AF2F49256AE58101FEE92B04 +# _z = 0x3E2D4BD9597B58639AE7AA669CAB9837CF5CF20A2C852D10F655668DFC150EF0 +_Gx = 0xA3E8EB3CC1CFE7B7732213B23A656149AFA142C47AAFBC2B79A191562E1305F4 +_Gy = 0x2D996C823439C56D7F7B22E14644417E69BCB6DE39D027001DABE8F35B25C9BE +_q = 0xA9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7 + +curve_brainpoolp256t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp256t1 = ellipticcurve.PointJacobi( + curve_brainpoolp256t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-320-r1 _a = int( remove_whitespace( @@ -748,6 +819,61 @@ def point_is_valid(generator, x, y): curve_brainpoolp320r1, _Gx, _Gy, 1, _q, generator=True ) +# Brainpool P-320-t1 +_a = int( + remove_whitespace( + """ + D35E472036BC4FB7E13C785ED201E065F98FCFA6F6F40DEF4F92B9EC7893EC + 28FCD412B1F1B32E24""" + ), + 16, +) +_b = int( + remove_whitespace( + """ + A7F561E038EB1ED560B3D147DB782013064C19F27ED27C6780AAF77FB8A547 + CEB5B4FEF422340353""" + ), + 16, +) +# _z = int( +# remove_whitespace( +# """ +# 15F75CAF668077F7E85B42EB01F0A81FF56ECD6191D55CB82B7D861458A18F +# EFC3E5AB7496F3C7B1""" +# ), +# 16, +# ) +_Gx = int( + remove_whitespace( + """ + 925BE9FB01AFC6FB4D3E7D4990010F813408AB106C4F09CB7EE07868CC136F + FF3357F624A21BED52""" + ), + 16, +) +_Gy = int( + remove_whitespace( + """ + 63BA3A7A27483EBF6671DBEF7ABB30EBEE084E58A0B077AD42A5A0989D1EE7 + 1B1B9BC0455FB0D2C3""" + ), + 16, +) +_q = int( + remove_whitespace( + """ + D35E472036BC4FB7E13C785ED201E065F98FCFA5B68F12A32D482EC7EE8658 + E98691555B44C59311""" + ), + 16, +) + +curve_brainpoolp320t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp320t1 = ellipticcurve.PointJacobi( + curve_brainpoolp320t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-384-r1 _a = int( remove_whitespace( @@ -803,6 +929,60 @@ def point_is_valid(generator, x, y): curve_brainpoolp384r1, _Gx, _Gy, 1, _q, generator=True ) +_a = int( + remove_whitespace( + """ + 8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB711 + 23ACD3A729901D1A71874700133107EC50""" + ), + 16, +) +_b = int( + remove_whitespace( + """ + 7F519EADA7BDA81BD826DBA647910F8C4B9346ED8CCDC64E4B1ABD11756DCE + 1D2074AA263B88805CED70355A33B471EE""" + ), + 16, +) +# _z = int( +# remove_whitespace( +# """ +# 41DFE8DD399331F7166A66076734A89CD0D2BCDB7D068E44E1F378F41ECBAE +# 97D2D63DBC87BCCDDCCC5DA39E8589291C""" +# ), +# 16, +# ) +_Gx = int( + remove_whitespace( + """ + 18DE98B02DB9A306F2AFCD7235F72A819B80AB12EBD653172476FECD462AAB + FFC4FF191B946A5F54D8D0AA2F418808CC""" + ), + 16, +) +_Gy = int( + remove_whitespace( + """ + 25AB056962D30651A114AFD2755AD336747F93475B7A1FCA3B88F2B6A208CC + FE469408584DC2B2912675BF5B9E582928""" + ), + 16, +) +_q = int( + remove_whitespace( + """ + 8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425 + A7CF3AB6AF6B7FC3103B883202E9046565""" + ), + 16, +) + +curve_brainpoolp384t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp384t1 = ellipticcurve.PointJacobi( + curve_brainpoolp384t1, _Gx, _Gy, 1, _q, generator=True +) + # Brainpool P-512-r1 _a = int( remove_whitespace( @@ -857,3 +1037,58 @@ def point_is_valid(generator, x, y): generator_brainpoolp512r1 = ellipticcurve.PointJacobi( curve_brainpoolp512r1, _Gx, _Gy, 1, _q, generator=True ) + +# Brainpool P-512-t1 +_a = int( + remove_whitespace( + """ + AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308 + 717D4D9B009BC66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F0""" + ), + 16, +) +_b = int( + remove_whitespace( + """ + 7CBBBCF9441CFAB76E1890E46884EAE321F70C0BCB4981527897504BEC3E36 + A62BCDFA2304976540F6450085F2DAE145C22553B465763689180EA2571867423E""" + ), + 16, +) +# _z = int( +# remove_whitespace( +# """ +# 12EE58E6764838B69782136F0F2D3BA06E27695716054092E60A80BEDB212B +# 64E585D90BCE13761F85C3F1D2A64E3BE8FEA2220F01EBA5EEB0F35DBD29D922AB""" +# ), +# 16, +# ) +_Gx = int( + remove_whitespace( + """ + 640ECE5C12788717B9C1BA06CBC2A6FEBA85842458C56DDE9DB1758D39C031 + 3D82BA51735CDB3EA499AA77A7D6943A64F7A3F25FE26F06B51BAA2696FA9035DA""" + ), + 16, +) +_Gy = int( + remove_whitespace( + """ + 5B534BD595F5AF0FA2C892376C84ACE1BB4E3019B71634C01131159CAE03CE + E9D9932184BEEF216BD71DF2DADF86A627306ECFF96DBB8BACE198B61E00F8B332""" + ), + 16, +) +_q = int( + remove_whitespace( + """ + AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308 + 70553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069""" + ), + 16, +) + +curve_brainpoolp512t1 = ellipticcurve.CurveFp(_p, _a, _b, 1) +generator_brainpoolp512t1 = ellipticcurve.PointJacobi( + curve_brainpoolp512t1, _Gx, _Gy, 1, _q, generator=True +) diff --git a/src/ecdsa/ellipticcurve.py b/src/ecdsa/ellipticcurve.py index d6f71463..a982c1e4 100644 --- a/src/ecdsa/ellipticcurve.py +++ b/src/ecdsa/ellipticcurve.py @@ -136,11 +136,17 @@ def contains_point(self, x, y): return (y * y - ((x * x + self.__a) * x + self.__b)) % self.__p == 0 def __str__(self): - return "CurveFp(p=%d, a=%d, b=%d, h=%d)" % ( + if self.__h is not None: + return "CurveFp(p={0}, a={1}, b={2}, h={3})".format( + self.__p, + self.__a, + self.__b, + self.__h, + ) + return "CurveFp(p={0}, a={1}, b={2})".format( self.__p, self.__a, self.__b, - self.__h, ) @@ -219,11 +225,17 @@ def cofactor(self): return self.__h def __str__(self): - return "CurveEdTw(p={0}, a={1}, d={2}, h={3})".format( + if self.__h is not None: + return "CurveEdTw(p={0}, a={1}, d={2}, h={3})".format( + self.__p, + self.__a, + self.__d, + self.__h, + ) + return "CurveEdTw(p={0}, a={1}, d={2})".format( self.__p, self.__a, self.__d, - self.__h, ) @@ -621,7 +633,7 @@ def __eq__(self, other): """ x1, y1, z1 = self.__coords if other is INFINITY: - return not y1 or not z1 + return not z1 if isinstance(other, Point): x2, y2, z2 = other.x(), other.y(), 1 elif isinstance(other, PointJacobi): @@ -711,11 +723,13 @@ def scale(self): def to_affine(self): """Return point in affine form.""" - _, y, z = self.__coords - if not y or not z: + _, _, z = self.__coords + p = self.__curve.p() + if not (z % p): return INFINITY self.scale() x, y, z = self.__coords + assert z == 1 return Point(self.__curve, x, y, self.__order) @staticmethod @@ -747,7 +761,7 @@ def _double_with_z_1(self, X1, Y1, p, a): # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-mdbl-2007-bl XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return 0, 0, 1 + return 0, 0, 0 YYYY = YY * YY % p S = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p M = 3 * XX + a @@ -761,13 +775,13 @@ def _double(self, X1, Y1, Z1, p, a): """Add a point to itself, arbitrary z.""" if Z1 == 1: return self._double_with_z_1(X1, Y1, p, a) - if not Y1 or not Z1: - return 0, 0, 1 + if not Z1: + return 0, 0, 0 # after: # http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-2007-bl XX, YY = X1 * X1 % p, Y1 * Y1 % p if not YY: - return 0, 0, 1 + return 0, 0, 0 YYYY = YY * YY % p ZZ = Z1 * Z1 % p S = 2 * ((X1 + YY) ** 2 - XX - YYYY) % p @@ -783,14 +797,14 @@ def double(self): """Add a point to itself.""" X1, Y1, Z1 = self.__coords - if not Y1: + if not Z1: return INFINITY p, a = self.__curve.p(), self.__curve.a() X3, Y3, Z3 = self._double(X1, Y1, Z1, p, a) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -874,10 +888,10 @@ def __radd__(self, other): def _add(self, X1, Y1, Z1, X2, Y2, Z2, p): """add two points, select fastest method.""" - if not Y1 or not Z1: - return X2, Y2, Z2 - if not Y2 or not Z2: - return X1, Y1, Z1 + if not Z1: + return X2 % p, Y2 % p, Z2 % p + if not Z2: + return X1 % p, Y1 % p, Z1 % p if Z1 == Z2: if Z1 == 1: return self._add_with_z_1(X1, Y1, X2, Y2, p) @@ -905,7 +919,7 @@ def __add__(self, other): X3, Y3, Z3 = self._add(X1, Y1, Z1, X2, Y2, Z2, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -915,7 +929,7 @@ def __rmul__(self, other): def _mul_precompute(self, other): """Multiply point by integer with precomputation table.""" - X3, Y3, Z3, p = 0, 0, 1, self.__curve.p() + X3, Y3, Z3, p = 0, 0, 0, self.__curve.p() _add = self._add for X2, Y2 in self.__precompute: if other % 2: @@ -928,7 +942,7 @@ def _mul_precompute(self, other): else: other //= 2 - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -947,7 +961,7 @@ def __mul__(self, other): self = self.scale() X2, Y2, _ = self.__coords - X3, Y3, Z3 = 0, 0, 1 + X3, Y3, Z3 = 0, 0, 0 p, a = self.__curve.p(), self.__curve.a() _double = self._double _add = self._add @@ -960,7 +974,7 @@ def __mul__(self, other): elif i > 0: X3, Y3, Z3 = _add(X3, Y3, Z3, X2, Y2, 1, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -989,7 +1003,7 @@ def mul_add(self, self_mul, other, other_mul): other_mul = other_mul % self.__order # (X3, Y3, Z3) is the accumulator - X3, Y3, Z3 = 0, 0, 1 + X3, Y3, Z3 = 0, 0, 0 p, a = self.__curve.p(), self.__curve.a() # as we have 6 unique points to work with, we can't scale all of them, @@ -1008,12 +1022,12 @@ def mul_add(self, self_mul, other, other_mul): # so we need 4 combined points: mAmB_X, mAmB_Y, mAmB_Z = _add(X1, -Y1, Z1, X2, -Y2, Z2, p) pAmB_X, pAmB_Y, pAmB_Z = _add(X1, Y1, Z1, X2, -Y2, Z2, p) - mApB_X, mApB_Y, mApB_Z = _add(X1, -Y1, Z1, X2, Y2, Z2, p) - pApB_X, pApB_Y, pApB_Z = _add(X1, Y1, Z1, X2, Y2, Z2, p) + mApB_X, mApB_Y, mApB_Z = pAmB_X, -pAmB_Y, pAmB_Z + pApB_X, pApB_Y, pApB_Z = mAmB_X, -mAmB_Y, mAmB_Z # when the self and other sum to infinity, we need to add them # one by one to get correct result but as that's very unlikely to # happen in regular operation, we don't need to optimise this case - if not pApB_Y or not pApB_Z: + if not pApB_Z: return self * self_mul + other * other_mul # gmp object creation has cumulatively higher overhead than the @@ -1058,7 +1072,7 @@ def mul_add(self, self_mul, other, other_mul): assert B > 0 X3, Y3, Z3 = _add(X3, Y3, Z3, pApB_X, pApB_Y, pApB_Z, p) - if not Y3 or not Z3: + if not Z3: return INFINITY return PointJacobi(self.__curve, X3, Y3, Z3, self.__order) @@ -1142,6 +1156,8 @@ def __eq__(self, other): Note: only points that lay on the same curve can be equal. """ + if other is INFINITY: + return self.__x is None or self.__y is None if isinstance(other, Point): return ( self.__curve == other.__curve @@ -1208,17 +1224,22 @@ def leftmost_bit(x): # From X9.62 D.3.2: e3 = 3 * e - negative_self = Point(self.__curve, self.__x, -self.__y, self.__order) + negative_self = Point( + self.__curve, + self.__x, + (-self.__y) % self.__curve.p(), + self.__order, + ) i = leftmost_bit(e3) // 2 result = self - # print_("Multiplying %s by %d (e3 = %d):" % (self, other, e3)) + # print("Multiplying %s by %d (e3 = %d):" % (self, other, e3)) while i > 1: result = result.double() if (e3 & i) != 0 and (e & i) == 0: result = result + self if (e3 & i) == 0 and (e & i) != 0: result = result + negative_self - # print_(". . . i = %d, result = %s" % ( i, result )) + # print(". . . i = %d, result = %s" % ( i, result )) i = i // 2 return result @@ -1235,7 +1256,6 @@ def __str__(self): def double(self): """Return a new point that is twice the old.""" - if self == INFINITY: return INFINITY @@ -1249,6 +1269,9 @@ def double(self): * numbertheory.inverse_mod(2 * self.__y, p) ) % p + if not l: + return INFINITY + x3 = (l * l - 2 * self.__x) % p y3 = (l * (self.__x - x3) - self.__y) % p @@ -1520,7 +1543,9 @@ def double(self): X3, Y3, Z3, T3 = self._double(X1, Y1, Z1, T1, p, a) - if not X3 or not T3: + # both Ed25519 and Ed448 have prime order, so no point added to + # itself will equal zero + if not X3 or not T3: # pragma: no branch return INFINITY return PointEdwards(self.__curve, X3, Y3, Z3, T3, self.__order) diff --git a/src/ecdsa/keys.py b/src/ecdsa/keys.py index 2b7d3168..f74252c7 100644 --- a/src/ecdsa/keys.py +++ b/src/ecdsa/keys.py @@ -5,9 +5,9 @@ import binascii from hashlib import sha1 import os -from six import PY2, b +from six import PY2 from . import ecdsa, eddsa -from . import der +from . import der, ssh from . import rfc6979 from . import ellipticcurve from .curves import NIST192p, Curve, Ed25519, Ed448 @@ -614,6 +614,18 @@ def to_der( der.encode_bitstring(point_str, 0), ) + def to_ssh(self): + """ + Convert the public key to the SSH format. + + :return: SSH encoding of the public key + :rtype: bytes + """ + return ssh.serialize_public( + self.curve.name, + self.to_string(), + ) + def verify( self, signature, @@ -1032,7 +1044,7 @@ def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None): curve = None s, empty = der.remove_sequence(s) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER( "trailing junk after DER privkey: %s" % binascii.hexlify(empty) ) @@ -1090,12 +1102,6 @@ def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None): curve = Curve.from_der(algorithm_identifier, valid_curve_encodings) - if empty != b"": - raise der.UnexpectedDER( - "unexpected data after algorithm identifier: %s" - % binascii.hexlify(empty) - ) - # Up next is an octet string containing an ECPrivateKey. Ignore # the optional "attributes" and "publicKey" fields that come after. s, _ = der.remove_octet_string(s) @@ -1103,7 +1109,7 @@ def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None): # Unpack the ECPrivateKey to get to the key data octet string, # and rejoin the ssleay parsing path. s, empty = der.remove_sequence(s) - if empty != b(""): + if empty != b"": raise der.UnexpectedDER( "trailing junk after DER privkey: %s" % binascii.hexlify(empty) @@ -1143,7 +1149,7 @@ def from_der(cls, string, hashfunc=sha1, valid_curve_encodings=None): # our from_string method likes fixed-length privkey strings if len(privkey_str) < curve.baselen: privkey_str = ( - b("\x00") * (curve.baselen - len(privkey_str)) + privkey_str + b"\x00" * (curve.baselen - len(privkey_str)) + privkey_str ) return cls.from_string(privkey_str, curve, hashfunc) @@ -1281,6 +1287,19 @@ def to_der( der.encode_octet_string(ec_private_key), ) + def to_ssh(self): + """ + Convert the private key to the SSH format. + + :return: SSH encoded private key + :rtype: bytes + """ + return ssh.serialize_private( + self.curve.name, + self.verifying_key.to_string(), + self.to_string(), + ) + def get_verifying_key(self): """ Return the VerifyingKey associated with this private key. diff --git a/src/ecdsa/numbertheory.py b/src/ecdsa/numbertheory.py index d3500c70..fe974f8e 100644 --- a/src/ecdsa/numbertheory.py +++ b/src/ecdsa/numbertheory.py @@ -20,11 +20,11 @@ except NameError: xrange = range try: - from gmpy2 import powmod + from gmpy2 import powmod, mpz GMPY2 = True GMPY = False -except ImportError: +except ImportError: # pragma: no branch GMPY2 = False try: from gmpy import mpz @@ -33,8 +33,15 @@ except ImportError: GMPY = False + +if GMPY2 or GMPY: # pragma: no branch + integer_types = tuple(integer_types + (type(mpz(1)),)) + + import math import warnings +import random +from .util import bit_length class Error(Exception): @@ -216,14 +223,15 @@ def square_root_mod_prime(a, p): range_top = min(0x7FFFFFFF, p) else: range_top = p - for b in xrange(2, range_top): + for b in xrange(2, range_top): # pragma: no branch if jacobi(b * b - 4 * a, p) == -1: f = (a, -b, 1) ff = polynomial_exp_mod((0, 1), (p + 1) // 2, f, p) if ff[1]: raise SquareRootError("p is not prime") return ff[0] - raise RuntimeError("No b found.") + # just an assertion + raise RuntimeError("No b found.") # pragma: no cover # because all the inverse_mod code is arch/environment specific, and coveralls @@ -346,7 +354,7 @@ def factorization(n): q, r = divmod(n, d) if r == 0: count = 1 - while d <= n: + while d <= n: # pragma: no branch n = q q, r = divmod(n, d) if r != 0: @@ -370,7 +378,8 @@ def factorization(n): if r == 0: # d divides n. How many times? count = 1 n = q - while d <= n: # As long as d might still divide n, + # As long as d might still divide n, + while d <= n: # pragma: no branch q, r = divmod(n, d) # see if it does. if r != 0: break @@ -555,8 +564,8 @@ def is_prime(n): return True else: return False - - if gcd(n, 2 * 3 * 5 * 7 * 11) != 1: + # 2310 = 2 * 3 * 5 * 7 * 11 + if gcd(n, 2310) != 1: return False # Choose a number of iterations sufficient to reduce the @@ -564,7 +573,8 @@ def is_prime(n): # (from Menezes et al. Table 4.4): t = 40 - n_bits = 1 + int(math.log(n, 2)) + n_bits = 1 + bit_length(n) + assert 11 <= n_bits <= 16384 for k, tt in ( (100, 27), (150, 18), @@ -591,7 +601,7 @@ def is_prime(n): s = s + 1 r = r // 2 for i in xrange(t): - a = smallprimes[i] + a = random.choice(smallprimes) y = pow(a, r, n) if y != 1 and y != n - 1: j = 1 diff --git a/src/ecdsa/ssh.py b/src/ecdsa/ssh.py new file mode 100644 index 00000000..64e94030 --- /dev/null +++ b/src/ecdsa/ssh.py @@ -0,0 +1,83 @@ +import binascii +from . import der +from ._compat import compat26_str, int_to_bytes + +_SSH_ED25519 = b"ssh-ed25519" +_SK_MAGIC = b"openssh-key-v1\0" +_NONE = b"none" + + +def _get_key_type(name): + if name == "Ed25519": + return _SSH_ED25519 + else: + raise ValueError("Unsupported key type") + + +class _Serializer: + def __init__(self): + self.bytes = b"" + + def put_raw(self, val): + self.bytes += val + + def put_u32(self, val): + self.bytes += int_to_bytes(val, length=4, byteorder="big") + + def put_str(self, val): + self.put_u32(len(val)) + self.bytes += val + + def put_pad(self, blklen=8): + padlen = blklen - (len(self.bytes) % blklen) + self.put_raw(bytearray(range(1, 1 + padlen))) + + def encode(self): + return binascii.b2a_base64(compat26_str(self.bytes)) + + def tobytes(self): + return self.bytes + + def topem(self): + return der.topem(self.bytes, "OPENSSH PRIVATE KEY") + + +def serialize_public(name, pub): + serial = _Serializer() + ktype = _get_key_type(name) + serial.put_str(ktype) + serial.put_str(pub) + return b" ".join([ktype, serial.encode()]) + + +def serialize_private(name, pub, priv): + # encode public part + spub = _Serializer() + ktype = _get_key_type(name) + spub.put_str(ktype) + spub.put_str(pub) + + # encode private part + spriv = _Serializer() + checksum = 0 + spriv.put_u32(checksum) + spriv.put_u32(checksum) + spriv.put_raw(spub.tobytes()) + spriv.put_str(priv + pub) + comment = b"" + spriv.put_str(comment) + spriv.put_pad() + + # top-level structure + main = _Serializer() + main.put_raw(_SK_MAGIC) + ciphername = kdfname = _NONE + main.put_str(ciphername) + main.put_str(kdfname) + nokdf = 0 + main.put_u32(nokdf) + nkeys = 1 + main.put_u32(nkeys) + main.put_str(spub.tobytes()) + main.put_str(spriv.tobytes()) + return main.topem() diff --git a/src/ecdsa/test_der.py b/src/ecdsa/test_der.py index 0ca5bd7f..33a2dca6 100644 --- a/src/ecdsa/test_der.py +++ b/src/ecdsa/test_der.py @@ -7,9 +7,9 @@ import unittest2 as unittest except ImportError: import unittest -from six import b +import sys import hypothesis.strategies as st -from hypothesis import given +from hypothesis import given, settings import pytest from ._compat import str_idx_as_int from .curves import NIST256p, NIST224p @@ -22,8 +22,10 @@ remove_object, encode_oid, remove_constructed, + remove_implicit, remove_octet_string, remove_sequence, + encode_implicit, ) @@ -32,44 +34,44 @@ class TestRemoveInteger(unittest.TestCase): # interpreted as negative, check if those errors are detected def test_non_minimal_encoding(self): with self.assertRaises(UnexpectedDER): - remove_integer(b("\x02\x02\x00\x01")) + remove_integer(b"\x02\x02\x00\x01") def test_negative_with_high_bit_set(self): with self.assertRaises(UnexpectedDER): - remove_integer(b("\x02\x01\x80")) + remove_integer(b"\x02\x01\x80") def test_minimal_with_high_bit_set(self): - val, rem = remove_integer(b("\x02\x02\x00\x80")) + val, rem = remove_integer(b"\x02\x02\x00\x80") self.assertEqual(val, 0x80) self.assertEqual(rem, b"") def test_two_zero_bytes_with_high_bit_set(self): with self.assertRaises(UnexpectedDER): - remove_integer(b("\x02\x03\x00\x00\xff")) + remove_integer(b"\x02\x03\x00\x00\xff") def test_zero_length_integer(self): with self.assertRaises(UnexpectedDER): - remove_integer(b("\x02\x00")) + remove_integer(b"\x02\x00") def test_empty_string(self): with self.assertRaises(UnexpectedDER): - remove_integer(b("")) + remove_integer(b"") def test_encoding_of_zero(self): - val, rem = remove_integer(b("\x02\x01\x00")) + val, rem = remove_integer(b"\x02\x01\x00") self.assertEqual(val, 0) self.assertEqual(rem, b"") def test_encoding_of_127(self): - val, rem = remove_integer(b("\x02\x01\x7f")) + val, rem = remove_integer(b"\x02\x01\x7f") self.assertEqual(val, 127) self.assertEqual(rem, b"") def test_encoding_of_128(self): - val, rem = remove_integer(b("\x02\x02\x00\x80")) + val, rem = remove_integer(b"\x02\x02\x00\x80") self.assertEqual(val, 128) self.assertEqual(rem, b"") @@ -92,37 +94,37 @@ class TestReadLength(unittest.TestCase): # form and lengths above that encoded with minimal number of bytes # necessary def test_zero_length(self): - self.assertEqual((0, 1), read_length(b("\x00"))) + self.assertEqual((0, 1), read_length(b"\x00")) def test_two_byte_zero_length(self): with self.assertRaises(UnexpectedDER): - read_length(b("\x81\x00")) + read_length(b"\x81\x00") def test_two_byte_small_length(self): with self.assertRaises(UnexpectedDER): - read_length(b("\x81\x7f")) + read_length(b"\x81\x7f") def test_long_form_with_zero_length(self): with self.assertRaises(UnexpectedDER): - read_length(b("\x80")) + read_length(b"\x80") def test_smallest_two_byte_length(self): - self.assertEqual((128, 2), read_length(b("\x81\x80"))) + self.assertEqual((128, 2), read_length(b"\x81\x80")) def test_zero_padded_length(self): with self.assertRaises(UnexpectedDER): - read_length(b("\x82\x00\x80")) + read_length(b"\x82\x00\x80") def test_two_three_byte_length(self): self.assertEqual((256, 3), read_length(b"\x82\x01\x00")) def test_empty_string(self): with self.assertRaises(UnexpectedDER): - read_length(b("")) + read_length(b"") def test_length_overflow(self): with self.assertRaises(UnexpectedDER): - read_length(b("\x83\x01\x00")) + read_length(b"\x83\x01\x00") class TestEncodeBitstring(unittest.TestCase): @@ -144,26 +146,22 @@ def test_old_call_convention(self): def test_new_call_convention(self): """This is how it should be called now.""" - warnings.simplefilter("always") - with pytest.warns(None) as warns: + # make sure no warnings are raised + with warnings.catch_warnings(): + warnings.simplefilter("error") der = encode_bitstring(b"\xff", 0) - # verify that new call convention doesn't raise Warnings - self.assertEqual(len(warns), 0) - self.assertEqual(der, b"\x03\x02\x00\xff") def test_implicit_unused_bits(self): """ Writing bit string with already included the number of unused bits. """ - warnings.simplefilter("always") - with pytest.warns(None) as warns: + # make sure no warnings are raised + with warnings.catch_warnings(): + warnings.simplefilter("error") der = encode_bitstring(b"\x00\xff", None) - # verify that new call convention doesn't raise Warnings - self.assertEqual(len(warns), 0) - self.assertEqual(der, b"\x03\x02\x00\xff") def test_explicit_unused_bits(self): @@ -203,22 +201,20 @@ def test_old_call_convention(self): self.assertEqual(rest, b"") def test_new_call_convention(self): - warnings.simplefilter("always") - with pytest.warns(None) as warns: + # make sure no warnings are raised + with warnings.catch_warnings(): + warnings.simplefilter("error") bits, rest = remove_bitstring(b"\x03\x02\x00\xff", 0) - self.assertEqual(len(warns), 0) - self.assertEqual(bits, b"\xff") self.assertEqual(rest, b"") def test_implicit_unexpected_unused(self): - warnings.simplefilter("always") - with pytest.warns(None) as warns: + # make sure no warnings are raised + with warnings.catch_warnings(): + warnings.simplefilter("error") bits, rest = remove_bitstring(b"\x03\x02\x00\xff", None) - self.assertEqual(len(warns), 0) - self.assertEqual(bits, (b"\xff", 0)) self.assertEqual(rest, b"") @@ -275,10 +271,10 @@ def test_bytearray(self): class TestEncodeOid(unittest.TestCase): def test_pub_key_oid(self): oid_ecPublicKey = encode_oid(1, 2, 840, 10045, 2, 1) - self.assertEqual(hexlify(oid_ecPublicKey), b("06072a8648ce3d0201")) + self.assertEqual(hexlify(oid_ecPublicKey), b"06072a8648ce3d0201") def test_nist224p_oid(self): - self.assertEqual(hexlify(NIST224p.encoded_oid), b("06052b81040021")) + self.assertEqual(hexlify(NIST224p.encoded_oid), b"06052b81040021") def test_nist256p_oid(self): self.assertEqual( @@ -402,6 +398,128 @@ def test_with_malformed_tag(self): self.assertIn("constructed tag", str(e.exception)) +class TestRemoveImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.exp_tag = 6 + cls.exp_data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_simple(self): + tag, body, rest = remove_implicit(self.data_context_specific) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_wrong_expected_class(self): + with self.assertRaises(ValueError) as e: + remove_implicit(self.data_context_specific, "foobar") + + self.assertIn("invalid `exp_class` value", str(e.exception)) + + def test_with_wrong_class(self): + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(self.data_application) + + self.assertIn( + "wanted class context-specific, got 0x46 tag", str(e.exception) + ) + + def test_with_application_class(self): + tag, body, rest = remove_implicit(self.data_application, "application") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_private_class(self): + tag, body, rest = remove_implicit(self.data_private, "private") + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, b"") + + def test_with_data_following(self): + extra_data = b"\x00\x01" + + tag, body, rest = remove_implicit( + self.data_context_specific + extra_data + ) + + self.assertEqual(tag, self.exp_tag) + self.assertEqual(body, self.exp_data) + self.assertEqual(rest, extra_data) + + def test_with_constructed(self): + data = b"\xa6\x02\x0a\x0b" + + with self.assertRaises(UnexpectedDER) as e: + remove_implicit(data) + + self.assertIn("wanted type primitive, got 0xa6 tag", str(e.exception)) + + def test_encode_decode(self): + data = b"some longish string" + + tag, body, rest = remove_implicit( + encode_implicit(6, data, "application"), "application" + ) + + self.assertEqual(tag, 6) + self.assertEqual(body, data) + self.assertEqual(rest, b"") + + +class TestEncodeImplicit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.data = b"\x0a\x0b" + # data with application tag class + cls.data_application = b"\x46\x02\x0a\x0b" + # data with context-specific tag class + cls.data_context_specific = b"\x86\x02\x0a\x0b" + # data with private tag class + cls.data_private = b"\xc6\x02\x0a\x0b" + + def test_encode_with_default_class(self): + ret = encode_implicit(6, self.data) + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_application_class(self): + ret = encode_implicit(6, self.data, "application") + + self.assertEqual(ret, self.data_application) + + def test_encode_with_context_specific_class(self): + ret = encode_implicit(6, self.data, "context-specific") + + self.assertEqual(ret, self.data_context_specific) + + def test_encode_with_private_class(self): + ret = encode_implicit(6, self.data, "private") + + self.assertEqual(ret, self.data_private) + + def test_encode_with_invalid_class(self): + with self.assertRaises(ValueError) as e: + encode_implicit(6, self.data, "foobar") + + self.assertIn("invalid tag class", str(e.exception)) + + def test_encode_with_too_large_tag(self): + with self.assertRaises(ValueError) as e: + encode_implicit(32, self.data) + + self.assertIn("Long tags not supported", str(e.exception)) + + class TestRemoveOctetString(unittest.TestCase): def test_simple(self): data = b"\x04\x03\xaa\xbb\xcc" @@ -468,9 +586,37 @@ def st_oid(draw, max_value=2**512, max_size=50): return (first, second) + tuple(rest) +HYP_SETTINGS = {} + + +if "--fast" in sys.argv: # pragma: no cover + HYP_SETTINGS["max_examples"] = 2 + + +@settings(**HYP_SETTINGS) @given(st_oid()) def test_oids(ids): encoded_oid = encode_oid(*ids) decoded_oid, rest = remove_object(encoded_oid) assert rest == b"" assert decoded_oid == ids + +def test_remove_octet_string_rejects_truncated_length(): + # OCTET STRING: declared length 4096, but only 3 bytes present + bad = b"\x04\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_octet_string(bad) + +def test_remove_constructed_rejects_truncated_length(): + # Constructed tag: 0xA0 (context-specific constructed, tag=0) + # declared length 4096, but only 3 bytes present + bad = b"\xA0\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_constructed(bad) + +def test_remove_implicit_rejects_truncated_length(): + # IMPLICIT primitive context-specific tag 0: 0x80 + # declared length 4096, but only 3 bytes present + bad = b"\x80\x82\x10\x00" + b"ABC" + with pytest.raises(UnexpectedDER, match="Length longer than the provided buffer"): + remove_implicit(bad) diff --git a/src/ecdsa/test_ecdh.py b/src/ecdsa/test_ecdh.py index 872d4d14..cb225803 100644 --- a/src/ecdsa/test_ecdh.py +++ b/src/ecdsa/test_ecdh.py @@ -1,4 +1,5 @@ import os +import sys import shutil import subprocess import pytest @@ -16,6 +17,8 @@ NIST384p, NIST521p, BRAINPOOLP160r1, + SECP112r2, + SECP128r1, ) from .curves import curves from .ecdh import ( @@ -29,6 +32,10 @@ from .ellipticcurve import CurveEdTw +if "--fast" in sys.argv: # pragma: no cover + curves = [SECP112r2, SECP128r1] + + @pytest.mark.parametrize( "vcurve", curves, @@ -366,6 +373,7 @@ def run_openssl(cmd): ) +@pytest.mark.slow @pytest.mark.parametrize( "vcurve", curves, diff --git a/src/ecdsa/test_ecdsa.py b/src/ecdsa/test_ecdsa.py index dbc4a6eb..c1e25829 100644 --- a/src/ecdsa/test_ecdsa.py +++ b/src/ecdsa/test_ecdsa.py @@ -27,6 +27,7 @@ generator_112r2, int_to_string, ) +from .ellipticcurve import Point HYP_SETTINGS = {} @@ -76,6 +77,19 @@ def test_verification(self): def test_rejection(self): assert not self.pubk.verifies(self.msg - 1, self.sig) + def test_verification_with_regular_point(self): + pubk = Public_key( + Point( + generator_192.curve(), + generator_192.x(), + generator_192.y(), + generator_192.order(), + ), + self.pubk.point, + ) + + assert pubk.verifies(self.msg, self.sig) + class TestPublicKey(unittest.TestCase): def test_equality_public_keys(self): @@ -584,7 +598,13 @@ def test_signature_validity(gen, msg, qx, qy, r, s, expected): elliptic curve of `gen`, `r` and `s` are the signature, and `expected` is True iff the signature is expected to be valid.""" pubk = Public_key(gen, ellipticcurve.Point(gen.curve(), qx, qy)) - assert expected == pubk.verifies(digest_integer(msg), Signature(r, s)) + with pytest.warns(DeprecationWarning) as warns: + msg_dgst = digest_integer(msg) + assert len(warns) == 3 + assert "unused" in warns[0].message.args[0] + assert "unused" in warns[1].message.args[0] + assert "unused" in warns[2].message.args[0] + assert expected == pubk.verifies(msg_dgst, Signature(r, s)) @pytest.mark.parametrize( @@ -593,7 +613,13 @@ def test_signature_validity(gen, msg, qx, qy, r, s, expected): def test_pk_recovery(gen, msg, r, s, qx, qy, expected): del expected sign = Signature(r, s) - pks = sign.recover_public_keys(digest_integer(msg), gen) + with pytest.warns(DeprecationWarning) as warns: + msg_dgst = digest_integer(msg) + assert len(warns) == 3 + assert "unused" in warns[0].message.args[0] + assert "unused" in warns[1].message.args[0] + assert "unused" in warns[2].message.args[0] + pks = sign.recover_public_keys(msg_dgst, gen) assert pks @@ -622,19 +648,22 @@ def st_random_gen_key_msg_nonce(draw): name = draw(st.sampled_from(sorted(name_gen.keys()))) note("Generator used: {0}".format(name)) generator = name_gen[name] - order = int(generator.order()) + order = int(generator.order()) - 1 key = draw(st.integers(min_value=1, max_value=order)) msg = draw(st.integers(min_value=1, max_value=order)) nonce = draw( - st.integers(min_value=1, max_value=order + 1) + st.integers(min_value=1, max_value=order) | st.integers(min_value=order >> 1, max_value=order) ) return generator, key, msg, nonce SIG_VER_SETTINGS = dict(HYP_SETTINGS) -SIG_VER_SETTINGS["max_examples"] = 10 +if "--fast" in sys.argv: # pragma: no cover + SIG_VER_SETTINGS["max_examples"] = 1 +else: + SIG_VER_SETTINGS["max_examples"] = 10 @settings(**SIG_VER_SETTINGS) @@ -658,4 +687,8 @@ def test_sig_verify(args): def test_int_to_string_with_zero(): - assert int_to_string(0) == b"\x00" + with pytest.warns(DeprecationWarning) as warns: + assert int_to_string(0) == b"\x00" + + assert len(warns) == 1 + assert "unused" in warns[0].message.args[0] diff --git a/src/ecdsa/test_eddsa.py b/src/ecdsa/test_eddsa.py index 7a09ad7f..6821b3bc 100644 --- a/src/ecdsa/test_eddsa.py +++ b/src/ecdsa/test_eddsa.py @@ -1,3 +1,4 @@ +import sys import pickle import hashlib import pytest @@ -162,6 +163,27 @@ def test_ed25519_eq_x_different_y(): assert a != b +def test_ed25519_mul_by_order(): + g = PointEdwards( + curve_ed25519, + generator_ed25519.x(), + generator_ed25519.y(), + 1, + generator_ed25519.x() * generator_ed25519.y(), + ) + + assert g * generator_ed25519.order() == INFINITY + + +def test_radd(): + + a = PointEdwards(curve_ed25519, 1, 1, 1, 1) + + p = INFINITY + a + + assert p == a + + def test_ed25519_test_normalisation_and_scaling(): x = generator_ed25519.x() y = generator_ed25519.y() @@ -259,6 +281,22 @@ def test_add_wrong_point_type(self): self.assertIn("different curve", str(e.exception)) +def test_generate_with_point(): + x1 = int( + "427838232691226969392843410947554224151809796397784248136826" + "78720006717057747" + ) + y1 = int( + "463168356949264781694283940034751631413079938662562256157830" + "33603165251855960" + ) + p = PointEdwards(curve_ed25519, x1, y1, 1, x1 * y1) + + pk = PublicKey(generator_ed25519, b"0" * 32, public_point=p) + + assert pk.public_point() == p + + def test_ed25519_mul_to_order_min_1(): x1 = int( "427838232691226969392843410947554224151809796397784248136826" @@ -500,6 +538,7 @@ def test_equal_public_points(self): key2 = PublicKey(generator_ed25519, b"\x01" * 32) self.assertEqual(key1, key2) + # verify that `__ne__` works as expected self.assertFalse(key1 != key2) def test_unequal_public_points(self): @@ -519,6 +558,7 @@ def test_unequal_publickey_curves(self): key2 = PublicKey(generator_ed448, b"\x03" * 56 + b"\x00") self.assertNotEqual(key1, key2) + # verify that `__ne__` works as expected self.assertTrue(key1 != key2) def test_equal_private_keys(self): @@ -526,6 +566,7 @@ def test_equal_private_keys(self): key2 = PrivateKey(generator_ed25519, b"\x01" * 32) self.assertEqual(key1, key2) + # verify that `__ne__` works as expected self.assertFalse(key1 != key2) def test_unequal_private_keys(self): @@ -533,6 +574,7 @@ def test_unequal_private_keys(self): key2 = PrivateKey(generator_ed25519, b"\x02" * 32) self.assertNotEqual(key1, key2) + # verify that `__ne__` works as expected self.assertTrue(key1 != key2) def test_unequal_privatekey_to_string(self): @@ -643,7 +685,10 @@ def test_invalid_r_value(self): HYP_SETTINGS = dict() -HYP_SETTINGS["max_examples"] = 10 +if "--fast" in sys.argv: # pragma: no cover + HYP_SETTINGS["max_examples"] = 2 +else: + HYP_SETTINGS["max_examples"] = 10 @settings(**HYP_SETTINGS) diff --git a/src/ecdsa/test_ellipticcurve.py b/src/ecdsa/test_ellipticcurve.py index 85faef4d..864cf108 100644 --- a/src/ecdsa/test_ellipticcurve.py +++ b/src/ecdsa/test_ellipticcurve.py @@ -14,7 +14,7 @@ except ImportError: # pragma: no cover HC_PRESENT = False from .numbertheory import inverse_mod -from .ellipticcurve import CurveFp, INFINITY, Point +from .ellipticcurve import CurveFp, INFINITY, Point, CurveEdTw HYP_SETTINGS = {} @@ -40,11 +40,11 @@ HYP_SLOW_SETTINGS = dict(HYP_SETTINGS) -HYP_SLOW_SETTINGS["max_examples"] = 10 +HYP_SLOW_SETTINGS["max_examples"] = 2 @settings(**HYP_SLOW_SETTINGS) -@given(st.integers(min_value=1, max_value=r + 1)) +@given(st.integers(min_value=1, max_value=r - 1)) def test_p192_mult_tests(multiple): inv_m = inverse_mod(multiple, r) @@ -83,6 +83,11 @@ def test_inequality_curves(self): c192 = CurveFp(p, -3, b) self.assertNotEqual(self.c_23, c192) + def test_inequality_curves_by_b_only(self): + a = CurveFp(23, 1, 0) + b = CurveFp(23, 1, 1) + self.assertNotEqual(a, b) + def test_usability_in_a_hashed_collection_curves(self): {self.c_23: None} @@ -97,6 +102,32 @@ def test_conflation_curves(self): self.assertDictEqual({c_23: None}, {eq1: None}) self.assertIn(eq2, {eq3: None}) + def test___str__(self): + self.assertEqual(str(self.c_23), "CurveFp(p=23, a=1, b=1)") + + def test___str___with_cofactor(self): + c = CurveFp(23, 1, 1, 4) + self.assertEqual(str(c), "CurveFp(p=23, a=1, b=1, h=4)") + + +class TestCurveEdTw(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.c_23 = CurveEdTw(23, 1, 1) + + def test___str__(self): + self.assertEqual(str(self.c_23), "CurveEdTw(p=23, a=1, d=1)") + + def test___str___with_cofactor(self): + c = CurveEdTw(23, 1, 1, 4) + self.assertEqual(str(c), "CurveEdTw(p=23, a=1, d=1, h=4)") + + def test_usability_in_a_hashed_collection_curves(self): + {self.c_23: None} + + def test_hashability_curves(self): + hash(self.c_23) + class TestPoint(unittest.TestCase): @classmethod @@ -158,6 +189,33 @@ def test_double(self): self.assertEqual(p3.x(), x3) self.assertEqual(p3.y(), y3) + def test_double_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1.double() + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2.double() + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_add_self_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1 + p1 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 + p2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_mul_to_infinity(self): + p1 = Point(self.c_23, 11, 20) + p2 = p1 * 2 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 * 2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + def test_multiply(self): x1, y1, m, x3, y3 = (3, 10, 2, 7, 12) p1 = Point(self.c_23, x1, y1) @@ -197,3 +255,40 @@ def test_inequality_points(self): def test_inequality_points_diff_types(self): c = CurveFp(100, -3, 100) self.assertNotEqual(self.g_23, c) + + def test_inequality_diff_y(self): + p1 = Point(self.c_23, 6, 4) + p2 = Point(self.c_23, 6, 19) + + self.assertNotEqual(p1, p2) + + def test_to_bytes_from_bytes(self): + p = Point(self.c_23, 3, 10) + + self.assertEqual(p, Point.from_bytes(self.c_23, p.to_bytes())) + + def test_add_to_neg_self(self): + p = Point(self.c_23, 3, 10) + + self.assertEqual(INFINITY, p + (-p)) + + def test_add_to_infinity(self): + p = Point(self.c_23, 3, 10) + + self.assertIs(p, p + INFINITY) + + def test_mul_infinity_by_scalar(self): + self.assertIs(INFINITY, INFINITY * 10) + + def test_mul_by_negative(self): + p = Point(self.c_23, 3, 10) + + self.assertEqual(p * -5, (-p) * 5) + + def test_str_infinity(self): + self.assertEqual(str(INFINITY), "infinity") + + def test_str_point(self): + p = Point(self.c_23, 3, 10) + + self.assertEqual(str(p), "(3,10)") diff --git a/src/ecdsa/test_jacobi.py b/src/ecdsa/test_jacobi.py index 1f528048..f811b922 100644 --- a/src/ecdsa/test_jacobi.py +++ b/src/ecdsa/test_jacobi.py @@ -1,4 +1,5 @@ import pickle +import sys try: import unittest2 as unittest @@ -6,7 +7,6 @@ import unittest import os -import sys import signal import pytest import threading @@ -14,7 +14,7 @@ import hypothesis.strategies as st from hypothesis import given, assume, settings, example -from .ellipticcurve import CurveFp, PointJacobi, INFINITY +from .ellipticcurve import CurveFp, PointJacobi, INFINITY, Point from .ecdsa import ( generator_256, curve_256, @@ -22,6 +22,7 @@ generator_brainpoolp160r1, curve_brainpoolp160r1, generator_112r2, + curve_112r2, ) from .numbertheory import inverse_mod from .util import randrange @@ -32,6 +33,13 @@ NO_OLD_SETTINGS["deadline"] = 5000 +SLOW_SETTINGS = {} +if "--fast" in sys.argv: # pragma: no cover + SLOW_SETTINGS["max_examples"] = 2 +else: + SLOW_SETTINGS["max_examples"] = 10 + + class TestJacobi(unittest.TestCase): def test___init__(self): curve = object() @@ -50,7 +58,7 @@ def test_add_with_different_curves(self): p_a = PointJacobi.from_affine(generator_256) p_b = PointJacobi.from_affine(generator_224) - with self.assertRaises(ValueError): + with self.assertRaises(ValueError): # pragma: no branch p_a + p_b def test_compare_different_curves(self): @@ -84,14 +92,21 @@ def test_double_with_zero_point(self): self.assertIs(pj, INFINITY) def test_double_with_zero_equivalent_point(self): - pj = PointJacobi(curve_256, 0, curve_256.p(), 1) + pj = PointJacobi(curve_256, 0, 0, 0) pj = pj.double() self.assertIs(pj, INFINITY) - def test_double_with_zero_equivalent_point_non_1_z(self): - pj = PointJacobi(curve_256, 0, curve_256.p(), 2) + def test_double_with_zero_equivalent_point_non_zero_z_non_zero_y(self): + pj = PointJacobi(curve_256, 0, 1, curve_256.p()) + + pj = pj.double() + + self.assertIs(pj, INFINITY) + + def test_double_with_zero_equivalent_point_non_zero_z(self): + pj = PointJacobi(curve_256, 0, 0, curve_256.p()) pj = pj.double() @@ -105,7 +120,7 @@ def test_compare_with_affine_point(self): self.assertEqual(pa, pj) def test_to_affine_with_zero_point(self): - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) pa = pj.to_affine() @@ -136,7 +151,7 @@ def test_add_with_infinity(self): def test_add_zero_point_to_affine(self): pa = PointJacobi.from_affine(generator_256).to_affine() - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) s = pj + pa @@ -187,8 +202,35 @@ def test_compare_non_zero_with_infinity(self): self.assertNotEqual(pj, INFINITY) + def test_compare_non_zero_bad_scale_with_infinity(self): + pj = PointJacobi(curve_256, 1, 1, 0) + self.assertEqual(pj, INFINITY) + + def test_eq_x_0_on_curve_with_infinity(self): + c_23 = CurveFp(23, 1, 1) + pj = PointJacobi(c_23, 0, 1, 1) + + self.assertTrue(c_23.contains_point(0, 1)) + + self.assertNotEqual(pj, INFINITY) + + def test_eq_y_0_on_curve_with_infinity(self): + c_23 = CurveFp(23, 1, 1) + pj = PointJacobi(c_23, 4, 0, 1) + + self.assertTrue(c_23.contains_point(4, 0)) + + self.assertNotEqual(pj, INFINITY) + + def test_eq_with_same_x_different_y(self): + c_23 = CurveFp(23, 1, 1) + p_a = PointJacobi(c_23, 0, 22, 1) + p_b = PointJacobi(c_23, 0, 1, 1) + + self.assertNotEqual(p_a, p_b) + def test_compare_zero_point_with_infinity(self): - pj = PointJacobi(curve_256, 0, 0, 1) + pj = PointJacobi(curve_256, 0, 0, 0) self.assertEqual(pj, INFINITY) @@ -199,10 +241,10 @@ def test_compare_double_with_multiply(self): self.assertEqual(dbl, mlpl) - @settings(max_examples=10) + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=0, max_value=int(generator_brainpoolp160r1.order()) + min_value=0, max_value=int(generator_brainpoolp160r1.order() - 1) ) ) def test_multiplications(self, mul): @@ -214,10 +256,10 @@ def test_multiplications(self, mul): self.assertEqual((pj.x(), pj.y()), (pw.x(), pw.y())) self.assertEqual(pj, pw) - @settings(max_examples=10) + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=0, max_value=int(generator_brainpoolp160r1.order()) + min_value=0, max_value=int(generator_brainpoolp160r1.order() - 1) ) ) @example(0) @@ -232,13 +274,13 @@ def test_precompute(self, mul): self.assertEqual(a, b) - @settings(max_examples=10) + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), ) @example(3, 3) @@ -251,13 +293,13 @@ def test_add_scaled_points(self, a_mul, b_mul): self.assertEqual(c, j_g * (a_mul + b_mul)) - @settings(max_examples=10) + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers(min_value=1, max_value=int(curve_brainpoolp160r1.p() - 1)), ) @@ -283,13 +325,14 @@ def test_add_one_scaled_point(self, a_mul, b_mul, new_z): self.assertEqual(c, j_g * (a_mul + b_mul)) - @settings(max_examples=10) + @pytest.mark.slow + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers(min_value=1, max_value=int(curve_brainpoolp160r1.p() - 1)), ) @@ -348,13 +391,14 @@ def test_add_same_scale_points_static(self): self.assertEqual(c, x + y) - @settings(max_examples=14) + @pytest.mark.slow + @settings(**SLOW_SETTINGS) @given( st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.integers( - min_value=1, max_value=int(generator_brainpoolp160r1.order()) + min_value=1, max_value=int(generator_brainpoolp160r1.order() - 1) ), st.lists( st.integers( @@ -423,6 +467,78 @@ def test_add_different_scale_points_static(self): self.assertEqual(c, x + y) + def test_add_different_points_same_scale_static(self): + j_g = generator_brainpoolp160r1 + p = curve_brainpoolp160r1.p() + a = j_g * 11 + a.scale() + b = j_g * 12 + z = 13 + x = PointJacobi( + curve_brainpoolp160r1, + a.x() * z**2 % p, + a.y() * z**3 % p, + z, + ) + y = PointJacobi( + curve_brainpoolp160r1, + b.x() * z**2 % p, + b.y() * z**3 % p, + z, + ) + + c = a + b + + self.assertEqual(c, x + y) + + def test_add_same_point_different_scale_second_z_1_static(self): + j_g = generator_112r2 + p = curve_112r2.p() + z = 11 + a = j_g * z + a.scale() + + x = PointJacobi( + curve_112r2, + a.x() * z**2 % p, + a.y() * z**3 % p, + z, + ) + y = PointJacobi( + curve_112r2, + a.x(), + a.y(), + 1, + ) + + c = a + a + + self.assertEqual(c, x + y) + + def test_add_to_infinity_static(self): + j_g = generator_112r2 + + z = 11 + a = j_g * z + a.scale() + + b = -a + + x = PointJacobi( + curve_112r2, + a.x(), + a.y(), + 1, + ) + y = PointJacobi( + curve_112r2, + b.x(), + b.y(), + 1, + ) + + self.assertEqual(INFINITY, x + y) + def test_add_point_3_times(self): j_g = PointJacobi.from_affine(generator_256) @@ -497,6 +613,18 @@ def test_mul_add(self): self.assertEqual(ret.to_affine(), w_a + w_b) + def test_mul_add_zero(self): + j_g = PointJacobi.from_affine(generator_256) + + w_a = generator_256 * 255 + w_b = generator_256 * (0 * 0xA8) + + j_b = j_g * 0xA8 + + ret = j_g.mul_add(255, j_b, 0) + + self.assertEqual(ret.to_affine(), w_a + w_b) + def test_mul_add_large(self): j_g = PointJacobi.from_affine(generator_256) b = PointJacobi.from_affine(j_g * 255) @@ -537,6 +665,20 @@ def test_mul_add_with_doubled_negation_of_itself(self): self.assertEqual(j_g.mul_add(4, dbl_neg, 2), INFINITY) + @given( + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + st.integers(min_value=0, max_value=int(generator_112r2.order() - 1)), + ) + @example(693, 2, 3293) # values that will hit all the conditions for NAF + def test_mul_add_random(self, mul1, mul2, mul3): + p_a = PointJacobi.from_affine(generator_112r2) + p_b = generator_112r2 * mul2 + + res = p_a.mul_add(mul1, p_b, mul3) + + self.assertEqual(res, p_a * mul1 + p_b * mul3) + def test_equality(self): pj1 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) pj2 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) @@ -553,13 +695,96 @@ def test_equality_with_wrong_curves(self): self.assertNotEqual(p_a, p_b) + def test_add_with_point_at_infinity(self): + pj1 = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) + x, y, z = pj1._add(2, 3, 1, 5, 5, 0, 23) + + self.assertEqual((x, y, z), (2, 3, 1)) + + def test_double_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p.double() + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2.double() + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_double_to_x_0(self): + c_23_2 = CurveFp(23, 1, 2) + p = PointJacobi(c_23_2, 9, 2, 1) + p2 = p.double() + + self.assertEqual((p2.x(), p2.y()), (0, 18)) + + def test_mul_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p * 2 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 * 2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_add_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 11, 20, 1) + p2 = p + p + self.assertEqual((p2.x(), p2.y()), (4, 0)) + self.assertNotEqual(p2, INFINITY) + p3 = p2 + p2 + self.assertEqual(p3, INFINITY) + self.assertIs(p3, INFINITY) + + def test_mul_to_x_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 13 + self.assertEqual((p2.x(), p2.y()), (0, 22)) + + def test_mul_to_y_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 14 + self.assertEqual((p2.x(), p2.y()), (4, 0)) + + def test_add_to_x_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 12 + p + self.assertEqual((p2.x(), p2.y()), (0, 22)) + + def test_add_to_y_0(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + p2 = p * 13 + p + self.assertEqual((p2.x(), p2.y()), (4, 0)) + + def test_add_diff_z_to_infinity(self): + c_23 = CurveFp(23, 1, 1) + p = PointJacobi(c_23, 9, 7, 1) + + c = p * 20 + p * 8 + self.assertIs(c, INFINITY) + def test_pickle(self): pj = PointJacobi(curve=CurveFp(23, 1, 1, 1), x=2, y=3, z=1, order=1) self.assertEqual(pickle.loads(pickle.dumps(pj)), pj) + @pytest.mark.slow @settings(**NO_OLD_SETTINGS) + @pytest.mark.skipif( + platform.python_implementation() == "PyPy", + reason="threading on PyPy breaks coverage", + ) @given(st.integers(min_value=1, max_value=10)) - def test_multithreading(self, thread_num): + def test_multithreading(self, thread_num): # pragma: no cover # ensure that generator's precomputation table is filled generator_112r2 * 2 @@ -591,11 +816,14 @@ def runner(generator): generator_112r2._PointJacobi__precompute, ) + @pytest.mark.slow @pytest.mark.skipif( - platform.system() == "Windows", - reason="there are no signals on Windows", + platform.system() == "Windows" + or platform.python_implementation() == "PyPy", + reason="there are no signals on Windows, and threading breaks coverage" + " on PyPy", ) - def test_multithreading_with_interrupts(self): + def test_multithreading_with_interrupts(self): # pragma: no cover thread_num = 10 # ensure that generator's precomputation table is filled generator_112r2 * 2 @@ -655,3 +883,52 @@ def interrupter(barrier_start, barrier_end, lock_exit): gen._PointJacobi__precompute, generator_112r2._PointJacobi__precompute, ) + + +class TestZeroCurve(unittest.TestCase): + """Tests with curve that has (0, 0) on the curve.""" + + def setUp(self): + self.curve = CurveFp(23, 1, 0) + + def test_zero_point_on_curve(self): + self.assertTrue(self.curve.contains_point(0, 0)) + + def test_double_to_0_0_point(self): + p = PointJacobi(self.curve, 1, 18, 1) + + d = p.double() + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_double_to_0_0_point_with_non_one_z(self): + z = 2 + p = PointJacobi(self.curve, 1 * z**2, 18 * z**3, z) + + d = p.double() + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_mul_to_0_0_point(self): + p = PointJacobi(self.curve, 11, 13, 1) + + d = p * 12 + + self.assertNotEqual(d, INFINITY) + self.assertEqual((0, 0), (d.x(), d.y())) + + def test_double_of_0_0_point(self): + p = PointJacobi(self.curve, 0, 0, 1) + + d = p.double() + + self.assertIs(d, INFINITY) + + def test_compare_to_old_implementation(self): + p = PointJacobi(self.curve, 11, 13, 1) + p_c = Point(self.curve, 11, 13) + + for i in range(24): + self.assertEqual(p * i, p_c * i) diff --git a/src/ecdsa/test_keys.py b/src/ecdsa/test_keys.py index 25386b17..348475e2 100644 --- a/src/ecdsa/test_keys.py +++ b/src/ecdsa/test_keys.py @@ -13,13 +13,20 @@ import pytest import hashlib -from .keys import VerifyingKey, SigningKey, MalformedPointError +from .keys import ( + VerifyingKey, + SigningKey, + MalformedPointError, + BadSignatureError, +) from .der import ( unpem, UnexpectedDER, encode_sequence, encode_oid, encode_bitstring, + encode_integer, + encode_octet_string, ) from .util import ( sigencode_string, @@ -120,6 +127,10 @@ def test_bytearray_compressed(self): self.assertEqual(self.vk.to_string(), vk.to_string()) + def test_ed25519_VerifyingKey_from_string_imported(self): + with self.assertRaises(MalformedPointError): + VerifyingKey.from_string(b"AAA", Ed25519) + class TestVerifyingKeyFromDer(unittest.TestCase): """ @@ -249,13 +260,13 @@ def test_array_array_of_bytes_memoryview(self): self.assertEqual(self.vk.to_string(), vk.to_string()) def test_equality_on_verifying_keys(self): - self.assertEqual(self.vk, self.sk.get_verifying_key()) + self.assertTrue(self.vk == self.sk.get_verifying_key()) def test_inequality_on_verifying_keys(self): - self.assertNotEqual(self.vk, self.vk2) + self.assertFalse(self.vk == self.vk2) def test_inequality_on_verifying_keys_not_implemented(self): - self.assertNotEqual(self.vk, None) + self.assertFalse(self.vk == None) def test_VerifyingKey_inequality_on_same_curve(self): self.assertNotEqual(self.vk, self.sk2.verifying_key) @@ -264,7 +275,7 @@ def test_SigningKey_inequality_on_same_curve(self): self.assertNotEqual(self.sk, self.sk2) def test_inequality_on_wrong_types(self): - self.assertNotEqual(self.vk, self.sk) + self.assertFalse(self.vk == self.sk) def test_from_public_point_old(self): pj = self.vk.pubkey.point @@ -272,7 +283,7 @@ def test_from_public_point_old(self): vk = VerifyingKey.from_public_point(point, self.vk.curve) - self.assertEqual(vk, self.vk) + self.assertTrue(vk == self.vk) def test_ed25519_VerifyingKey_repr__(self): sk = SigningKey.from_string(Ed25519.generator.to_bytes(), Ed25519) @@ -364,6 +375,18 @@ def test_export_ed255_to_pem(self): self.assertEqual(vk_pem, vk.to_pem()) + def test_export_ed255_to_ssh(self): + vk_str = ( + b"\x23\x00\x50\xd0\xd6\x64\x22\x28\x8e\xe3\x55\x89\x7e\x6e\x41\x57" + b"\x8d\xae\xde\x44\x26\xee\x56\x27\xbc\x85\xe6\x0b\x2f\x2a\xcb\x65" + ) + + vk = VerifyingKey.from_string(vk_str, Ed25519) + + vk_ssh = b"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstl\n" + + self.assertEqual(vk_ssh, vk.to_ssh()) + def test_ed25519_export_import(self): sk = SigningKey.generate(Ed25519) vk = sk.verifying_key @@ -393,6 +416,28 @@ def test_ed25519_sig_verify(self): self.assertTrue(vk.verify(sig, data)) + def test_ed25519_sig_verify_malformed(self): + vk_pem = ( + "-----BEGIN PUBLIC KEY-----\n" + "MCowBQYDK2VwAyEAIwBQ0NZkIiiO41WJfm5BV42u3kQm7lYnvIXmCy8qy2U=\n" + "-----END PUBLIC KEY-----\n" + ) + + vk = VerifyingKey.from_pem(vk_pem) + + data = b"data\n" + + # modified signature from test_ed25519_sig_verify + sig = ( + b"\xAA\x47\xab\x6a\x33\xcd\x79\x45\xad\x98\x11\x6c\xb9\xf2\x20\xeb" + b"\x90\xd6\x50\xe3\xc7\x8f\x9f\x60\x10\xec\x75\xe0\x2f\x27\xd3\x96" + b"\xda\xe8\x58\x7f\xe0\xfe\x46\x5c\x81\xef\x50\xec\x29\x9f\xae\xd5" + b"\xad\x46\x3c\x91\x68\x83\x4d\xea\x8d\xa8\x19\x04\x04\x79\x03\x0b" + ) + + with self.assertRaises(BadSignatureError): + vk.verify(sig, data) + def test_ed448_from_pem(self): pem_str = ( "-----BEGIN PUBLIC KEY-----\n" @@ -428,8 +473,8 @@ def test_ed448_to_pem(self): vk_pem = ( b"-----BEGIN PUBLIC KEY-----\n" - b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0\n" - b"dTdYD2ll94g58MhSnBiBQB9A1MMA\n" + b"MEMwBQYDK2VxAzoAeQtetSu7CMEzE+XWB10Bg47LCA0giNikOxHzdp+tZ/eK/En0dTdYD2ll94g5\n" + b"8MhSnBiBQB9A1MMA\n" b"-----END PUBLIC KEY-----\n" ) @@ -505,6 +550,17 @@ def setUpClass(cls): ) cls.sk2 = SigningKey.from_pem(prv_key_str) + def test_to_der_pkcs8(self): + self.assertEqual( + self.sk1.to_der(format="pkcs8"), + b"0o\x02\x01\x010\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H" + b"\xce=\x03\x01\x01\x04U0S\x02\x01\x01\x04\x18^\xc8B\x0b\xd6\xef" + b"\x92R\xa9B\xe9\x89\x04<\xa2\x9fV\x1f\xa5%w\x0e\xb1\xc5\xa14\x03" + b"2\x00\x04\xb8\x81w\xd0\x84\xef\x17\xf5\xe4V9@\x80(6\x0f\x9fY" + b"\xb4\xa4\xd7&Nb\xda\x06Q\xdc\xe4z5\xa4\xc5\xb4\\\xf5\x15\x93B:" + b"\x8bU{\x9c \x99\xf3l", + ) + def test_decoding_explicit_curve_parameters(self): prv_key_str = ( "-----BEGIN PRIVATE KEY-----\n" @@ -614,6 +670,99 @@ def test_ed25519_from_pem(self): self.assertEqual(sk, sk_str) + def test_ed25519_from_der_bad_alg_id_params(self): + der_str = encode_sequence( + encode_integer(1), + encode_sequence(encode_oid(*Ed25519.oid), encode_integer(1)), + encode_octet_string(encode_octet_string(b"A" * 32)), + ) + + with self.assertRaises(UnexpectedDER) as e: + SigningKey.from_der(der_str) + + self.assertIn("Non NULL parameters", str(e.exception)) + + def test_ed25519_from_der_junk_after_priv_key(self): + der_str = encode_sequence( + encode_integer(1), + encode_sequence( + encode_oid(*Ed25519.oid), + ), + encode_octet_string(encode_octet_string(b"A" * 32) + b"B"), + ) + + with self.assertRaises(UnexpectedDER) as e: + SigningKey.from_der(der_str) + + self.assertIn( + "trailing junk after the encoded private key", str(e.exception) + ) + + def test_ed25519_sign(self): + sk_str = SigningKey.from_string( + b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" + b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4", + Ed25519, + ) + + msg = b"message" + + sig = sk_str.sign(msg, sigencode=sigencode_der) + + self.assertEqual( + sig, + b"\xe1,v\xc9>%\xda\xd2~>\xc3&\na\xf4@|\x9e`X\x11\x13@<\x987\xd4" + b"\r\xb1\xf5\xb3\x15\x7f%i{\xdf}\xdd\xb1\xf3\x02\x7f\x80\x02\xc2" + b'|\xe5\xd6\x06\xc4\n\xa3\xb0\xf6}\xc0\xed)"+E\xaf\x00', + ) + + def test_ed25519_sign_digest_deterministic(self): + sk_str = SigningKey.from_string( + b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" + b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4", + Ed25519, + ) + with self.assertRaises(ValueError) as e: + sk_str.sign_digest_deterministic(b"a" * 20) + + self.assertIn("Method unsupported for Edwards", str(e.exception)) + + def test_ed25519_sign_digest(self): + sk_str = SigningKey.from_string( + b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" + b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4", + Ed25519, + ) + with self.assertRaises(ValueError) as e: + sk_str.sign_digest(b"a" * 20) + + self.assertIn("Method unsupported for Edwards", str(e.exception)) + + def test_ed25519_sign_number(self): + sk_str = SigningKey.from_string( + b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" + b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4", + Ed25519, + ) + with self.assertRaises(ValueError) as e: + sk_str.sign_number(20) + + self.assertIn("Method unsupported for Edwards", str(e.exception)) + + def test_ed25519_to_der_ssleay(self): + pem_str = ( + "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VwBCIEIDS6x9FO1PG8T4xIPg8Zd0z8uL6sVGZFEZrX17gHC/XU\n" + "-----END PRIVATE KEY-----\n" + ) + + sk = SigningKey.from_pem(pem_str) + + with self.assertRaises(ValueError) as e: + sk.to_der(format="ssleay") + + self.assertIn("Only PKCS#8 format", str(e.exception)) + def test_ed25519_to_pem(self): sk = SigningKey.from_string( b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" @@ -629,6 +778,25 @@ def test_ed25519_to_pem(self): self.assertEqual(sk.to_pem(format="pkcs8"), pem_str) + def test_ed25519_to_ssh(self): + sk = SigningKey.from_string( + b"\x34\xBA\xC7\xD1\x4E\xD4\xF1\xBC\x4F\x8C\x48\x3E\x0F\x19\x77\x4C" + b"\xFC\xB8\xBE\xAC\x54\x66\x45\x11\x9A\xD7\xD7\xB8\x07\x0B\xF5\xD4", + Ed25519, + ) + + ssh_str = ( + b"-----BEGIN OPENSSH PRIVATE KEY-----\n" + b"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUx\n" + b"OQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAIgAAAAAAAAAAAAAAAtzc2gt\n" + b"ZWQyNTUxOQAAACAjAFDQ1mQiKI7jVYl+bkFXja7eRCbuVie8heYLLyrLZQAAAEA0usfRTtTxvE+M\n" + b"SD4PGXdM/Li+rFRmRRGa19e4Bwv11CMAUNDWZCIojuNViX5uQVeNrt5EJu5WJ7yF5gsvKstlAAAA\n" + b"AAECAwQF\n" + b"-----END OPENSSH PRIVATE KEY-----\n" + ) + + self.assertEqual(sk.to_ssh(), ssh_str) + def test_ed25519_to_and_from_pem(self): sk = SigningKey.generate(Ed25519) @@ -636,6 +804,17 @@ def test_ed25519_to_and_from_pem(self): self.assertEqual(sk, decoded) + def test_ed25519_custom_entropy(self): + sk = SigningKey.generate(Ed25519, entropy=os.urandom) + + self.assertIsNotNone(sk) + + def test_ed25519_from_secret_exponent(self): + with self.assertRaises(ValueError) as e: + SigningKey.from_secret_exponent(1234567890, curve=Ed25519) + + self.assertIn("don't support setting the secret", str(e.exception)) + def test_ed448_from_pem(self): pem_str = ( "-----BEGIN PRIVATE KEY-----\n" @@ -665,8 +844,8 @@ def test_ed448_to_pem(self): ) pem_str = ( b"-----BEGIN PRIVATE KEY-----\n" - b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmP\n" - b"OP0JMYaLGlTzwovmvCDJ2zLaezu9NLz9aQ==\n" + b"MEcCAQAwBQYDK2VxBDsEOTyFuXqFLXgJlV8uDqcOw9nG4IqzLiZ/i5NfBDoHPzmPOP0JMYaLGlTz\n" + b"wovmvCDJ2zLaezu9NLz9aQ==\n" b"-----END PRIVATE KEY-----\n" ) @@ -769,8 +948,8 @@ def test_deterministic_sign_that_rises_S_zero_error(self): verifiers = [] for modifier, fun in [ ("bytes", lambda x: x), - ("bytes memoryview", lambda x: buffer(x)), - ("bytearray", lambda x: bytearray(x)), + ("bytes memoryview", buffer), + ("bytearray", bytearray), ("bytearray memoryview", lambda x: buffer(bytearray(x))), ("array.array of bytes", lambda x: array.array("B", x)), ("array.array of bytes memoryview", lambda x: buffer(array.array("B", x))), @@ -939,14 +1118,14 @@ def test_VerifyingKey_inequality_with_different_curves(): sk1 = SigningKey.from_secret_exponent(2, BRAINPOOLP160r1) sk2 = SigningKey.from_secret_exponent(2, NIST256p) - assert sk1.verifying_key != sk2.verifying_key + assert not (sk1.verifying_key == sk2.verifying_key) def test_VerifyingKey_inequality_with_different_secret_points(): sk1 = SigningKey.from_secret_exponent(2, BRAINPOOLP160r1) sk2 = SigningKey.from_secret_exponent(3, BRAINPOOLP160r1) - assert sk1.verifying_key != sk2.verifying_key + assert not (sk1.verifying_key == sk2.verifying_key) def test_SigningKey_from_pem_pkcs8v2_EdDSA(): diff --git a/src/ecdsa/test_malformed_sigs.py b/src/ecdsa/test_malformed_sigs.py index 8e1b611d..e5a87c28 100644 --- a/src/ecdsa/test_malformed_sigs.py +++ b/src/ecdsa/test_malformed_sigs.py @@ -31,7 +31,7 @@ from .keys import BadSignatureError from .util import sigencode_der, sigencode_string from .util import sigdecode_der, sigdecode_string -from .curves import curves +from .curves import curves, SECP112r2, SECP128r1 from .der import ( encode_integer, encode_bitstring, @@ -55,6 +55,10 @@ bigger than order sizes of curves.""" +if "--fast" in sys.argv: # pragma: no cover + curves = [SECP112r2, SECP128r1] + + keys_and_sigs = [] """Name of the curve+hash combination, VerifyingKey and DER signature.""" @@ -91,7 +95,7 @@ def test_signatures(verifying_key, signature): @st.composite -def st_fuzzed_sig(draw, keys_and_sigs): +def st_fuzzed_sig(draw, keys_and_sigs): # pragma: no cover """ Hypothesis strategy that generates pairs of VerifyingKey and malformed signatures created by fuzzing of a valid signature. @@ -111,6 +115,7 @@ def st_fuzzed_sig(draw, keys_and_sigs): note("Remove bytes: {0}".format(to_remove)) # decide which bytes of the original signature should be changed + xors = None if sig: # pragma: no branch xors = draw( st.dictionaries( @@ -153,12 +158,17 @@ def st_fuzzed_sig(draw, keys_and_sigs): HealthCheck.filter_too_much, HealthCheck.too_slow, ] +if "--fast" in sys.argv: # pragma: no cover + params["max_examples"] = 20 slow_params = dict(params) -slow_params["max_examples"] = 10 +if "--fast" in sys.argv: # pragma: no cover + slow_params["max_examples"] = 1 +else: + slow_params["max_examples"] = 10 -@settings(**params) +@settings(**slow_params) @given(st_fuzzed_sig(keys_and_sigs)) def test_fuzzed_der_signatures(args): verifying_key, sig = args @@ -168,7 +178,7 @@ def test_fuzzed_der_signatures(args): @st.composite -def st_random_der_ecdsa_sig_value(draw): +def st_random_der_ecdsa_sig_value(draw): # pragma: no cover """ Hypothesis strategy for selecting random values and encoding them to ECDSA-Sig-Value object:: @@ -214,7 +224,7 @@ def test_random_der_ecdsa_sig_value(params): verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) -def st_der_integer(*args, **kwargs): +def st_der_integer(*args, **kwargs): # pragma: no cover """ Hypothesis strategy that returns a random positive integer as DER INTEGER. @@ -226,7 +236,7 @@ def st_der_integer(*args, **kwargs): @st.composite -def st_der_bit_string(draw, *args, **kwargs): +def st_der_bit_string(draw, *args, **kwargs): # pragma: no cover """ Hypothesis strategy that returns a random DER BIT STRING. Parameters are passed to hypothesis.strategy.binary. @@ -242,7 +252,7 @@ def st_der_bit_string(draw, *args, **kwargs): return encode_bitstring(data, unused) -def st_der_octet_string(*args, **kwargs): +def st_der_octet_string(*args, **kwargs): # pragma: no cover """ Hypothesis strategy that returns a random DER OCTET STRING object. Parameters are passed to hypothesis.strategy.binary @@ -250,7 +260,7 @@ def st_der_octet_string(*args, **kwargs): return st.builds(encode_octet_string, st.binary(*args, **kwargs)) -def st_der_null(): +def st_der_null(): # pragma: no cover """ Hypothesis strategy that returns DER NULL object. """ @@ -258,7 +268,7 @@ def st_der_null(): @st.composite -def st_der_oid(draw): +def st_der_oid(draw): # pragma: no cover """ Hypothesis strategy that returns DER OBJECT IDENTIFIER objects. """ @@ -273,7 +283,7 @@ def st_der_oid(draw): return encode_oid(first, second, *rest) -def st_der(): +def st_der(): # pragma: no cover """ Hypothesis strategy that returns random DER structures. @@ -281,22 +291,20 @@ def st_der(): of a valid DER structure, sequence of valid DER objects or a constructed encoding of any of the above. """ - return st.recursive( + return st.recursive( # pragma: no branch st.just(b"") | st_der_integer(max_value=2**4096) | st_der_bit_string(max_size=1024**2) | st_der_octet_string(max_size=1024**2) | st_der_null() | st_der_oid(), - lambda children: st.builds( - lambda x: encode_octet_string(x), st.one_of(children) - ) + lambda children: st.builds(encode_octet_string, st.one_of(children)) | st.builds(lambda x: encode_bitstring(x, 0), st.one_of(children)) | st.builds( lambda x: encode_sequence(*x), st.lists(children, max_size=200) ) | st.builds( - lambda tag, x: encode_constructed(tag, x), + encode_constructed, st.integers(min_value=0, max_value=0x3F), st.one_of(children), ), @@ -304,7 +312,7 @@ def st_der(): ) -@settings(**params) +@settings(**slow_params) @given(st.sampled_from(keys_and_sigs), st_der()) def test_random_der_as_signature(params, der): """Check if random DER structures are rejected as signature""" @@ -314,7 +322,7 @@ def test_random_der_as_signature(params, der): verifying_key.verify(der, example_data, sigdecode=sigdecode_der) -@settings(**params) +@settings(**slow_params) @given(st.sampled_from(keys_and_sigs), st.binary(max_size=1024**2)) @example( keys_and_sigs[0], encode_sequence(encode_integer(0), encode_integer(0)) @@ -361,7 +369,7 @@ def test_random_bytes_as_signature(params, der): ] -@settings(**params) +@settings(**slow_params) @given(st_fuzzed_sig(keys_and_string_sigs)) def test_fuzzed_string_signatures(params): verifying_key, sig = params diff --git a/src/ecdsa/test_numbertheory.py b/src/ecdsa/test_numbertheory.py index 8bc787f1..966eca29 100644 --- a/src/ecdsa/test_numbertheory.py +++ b/src/ecdsa/test_numbertheory.py @@ -1,5 +1,6 @@ import operator from functools import reduce +import sys try: import unittest2 as unittest @@ -29,6 +30,16 @@ square_root_mod_prime, ) +try: + from gmpy2 import mpz +except ImportError: + try: + from gmpy import mpz + except ImportError: + + def mpz(x): + return x + BIGPRIMES = ( 999671, @@ -66,6 +77,7 @@ def test_next_prime_with_nums_less_2(val): assert next_prime(val) == 2 +@pytest.mark.slow @pytest.mark.parametrize("prime", smallprimes) def test_square_root_mod_prime_for_small_primes(prime): squares = set() @@ -182,7 +194,7 @@ def st_comp_with_com_fac(draw): # select at most 20 lists (returned numbers), # each having at most 30 primes (factors) including none (then the number # will be 1) - comp_primes = draw( + comp_primes = draw( # pragma: no branch st.integers(min_value=1, max_value=20).flatmap( lambda n: st.lists( st.lists(st.sampled_from(primes), max_size=30), @@ -225,7 +237,7 @@ def st_comp_no_com_fac(draw): # select at most 20 lists, each having at most 30 primes # selected from the leftover_primes list - number_primes = draw( + number_primes = draw( # pragma: no branch st.integers(min_value=1, max_value=20).flatmap( lambda n: st.lists( st.lists(st.sampled_from(leftover_primes), max_size=30), @@ -251,9 +263,15 @@ def st_comp_no_com_fac(draw): # the factorization() sometimes takes a long time to finish HYP_SETTINGS["deadline"] = 5000 +if "--fast" in sys.argv: # pragma: no cover + HYP_SETTINGS["max_examples"] = 20 + HYP_SLOW_SETTINGS = dict(HYP_SETTINGS) -HYP_SLOW_SETTINGS["max_examples"] = 10 +if "--fast" in sys.argv: # pragma: no cover + HYP_SLOW_SETTINGS["max_examples"] = 1 +else: + HYP_SLOW_SETTINGS["max_examples"] = 20 class TestIsPrime(unittest.TestCase): @@ -285,7 +303,30 @@ def test_medium_non_trivial_composite(self): def test_large_prime(self): # nextPrime[2^2048] - assert is_prime(2**2048 + 0x3D5) + assert is_prime(mpz(2) ** 2048 + 0x3D5) + + def test_pseudoprime_base_19(self): + assert not is_prime(1543267864443420616877677640751301) + + def test_pseudoprime_base_300(self): + # F. Arnault "Constructing Carmichael Numbers Which Are Strong + # Pseudoprimes to Several Bases". Journal of Symbolic + # Computation. 20 (2): 151-161. doi:10.1006/jsco.1995.1042. + # Section 4.4 Large Example (a pseudoprime to all bases up to + # 300) + p = int( + "29 674 495 668 685 510 550 154 174 642 905 332 730 " + "771 991 799 853 043 350 995 075 531 276 838 753 171 " + "770 199 594 238 596 428 121 188 033 664 754 218 345 " + "562 493 168 782 883".replace(" ", "") + ) + + assert is_prime(p) + for _ in range(10): + if not is_prime(p * (313 * (p - 1) + 1) * (353 * (p - 1) + 1)): + break + else: + assert False, "composite not detected" class TestNumbertheory(unittest.TestCase): @@ -301,6 +342,7 @@ def test_gcd(self): "case times-out on it", ) @settings(**HYP_SLOW_SETTINGS) + @example([877 * 1151, 877 * 1009]) @given(st_comp_with_com_fac()) def test_gcd_with_com_factor(self, numbers): n = gcd(numbers) @@ -315,11 +357,13 @@ def test_gcd_with_com_factor(self, numbers): "case times-out on it", ) @settings(**HYP_SLOW_SETTINGS) + @example([1151, 1069, 1009]) @given(st_comp_no_com_fac()) def test_gcd_with_uncom_factor(self, numbers): n = gcd(numbers) assert n == 1 + @settings(**HYP_SLOW_SETTINGS) @given( st.lists( st.integers(min_value=1, max_value=2**8192), @@ -338,6 +382,7 @@ def test_lcm(self): assert lcm([3, 5 * 3, 7 * 3]) == 3 * 5 * 7 assert lcm(3) == 3 + @settings(**HYP_SLOW_SETTINGS) @given( st.lists( st.integers(min_value=1, max_value=2**8192), @@ -356,7 +401,7 @@ def test_lcm_with_random_numbers(self, numbers): "meet requirements (like `is_prime()`), the test " "case times-out on it", ) - @settings(**HYP_SETTINGS) + @settings(**HYP_SLOW_SETTINGS) @given(st_num_square_prime()) def test_square_root_mod_prime(self, vals): square, prime = vals @@ -364,7 +409,8 @@ def test_square_root_mod_prime(self, vals): calc = square_root_mod_prime(square, prime) assert calc * calc % prime == square - @settings(**HYP_SETTINGS) + @pytest.mark.slow + @settings(**HYP_SLOW_SETTINGS) @given(st.integers(min_value=1, max_value=10**12)) @example(265399 * 1526929) @example(373297**2 * 553991) @@ -401,16 +447,19 @@ def test_jacobi_with_zero(self): def test_jacobi_with_one(self): assert jacobi(1, 3) == 1 - @settings(**HYP_SETTINGS) + @settings(**HYP_SLOW_SETTINGS) @given(st.integers(min_value=3, max_value=1000).filter(lambda x: x % 2)) def test_jacobi(self, mod): + mod = mpz(mod) if is_prime(mod): squares = set() for root in range(1, mod): + root = mpz(root) assert jacobi(root * root, mod) == 1 squares.add(root * root % mod) for i in range(1, mod): if i not in squares: + i = mpz(i) assert jacobi(i, mod) == -1 else: factors = factorization(mod) @@ -420,6 +469,7 @@ def test_jacobi(self, mod): c *= jacobi(a, i[0]) ** i[1] assert c == jacobi(a, mod) + @settings(**HYP_SLOW_SETTINGS) @given(st_two_nums_rel_prime()) def test_inverse_mod(self, nums): num, mod = nums diff --git a/src/ecdsa/test_pyecdsa.py b/src/ecdsa/test_pyecdsa.py index d61f5083..799e9b74 100644 --- a/src/ecdsa/test_pyecdsa.py +++ b/src/ecdsa/test_pyecdsa.py @@ -1,28 +1,33 @@ -from __future__ import with_statement, division +from __future__ import with_statement, division, print_function try: import unittest2 as unittest except ImportError: import unittest import os -import sys import shutil import subprocess import pytest +import sys from binascii import hexlify, unhexlify -from hashlib import sha1, sha256, sha384, sha512 import hashlib from functools import partial -from hypothesis import given +from hypothesis import given, settings import hypothesis.strategies as st -from six import b, print_, binary_type +from six import binary_type from .keys import SigningKey, VerifyingKey from .keys import BadSignatureError, MalformedPointError, BadDigestError from . import util -from .util import sigencode_der, sigencode_strings -from .util import sigdecode_der, sigdecode_strings +from .util import ( + sigencode_der, + sigencode_strings, + sigencode_strings_canonize, + sigencode_string_canonize, + sigencode_der_canonize, +) +from .util import sigdecode_der, sigdecode_strings, sigdecode_string from .util import number_to_string, encoded_oid_ecPublicKey, MalformedSignature from .curves import Curve, UnknownCurveError from .curves import ( @@ -43,6 +48,13 @@ BRAINPOOLP320r1, BRAINPOOLP384r1, BRAINPOOLP512r1, + BRAINPOOLP160t1, + BRAINPOOLP192t1, + BRAINPOOLP224t1, + BRAINPOOLP256t1, + BRAINPOOLP320t1, + BRAINPOOLP384t1, + BRAINPOOLP512t1, Ed25519, Ed448, curves, @@ -63,6 +75,13 @@ class SubprocessError(Exception): pass +HYP_SETTINGS = {} + + +if "--fast" in sys.argv: # pragma: no cover + HYP_SETTINGS["max_examples"] = 2 + + def run_openssl(cmd): OPENSSL = "openssl" p = subprocess.Popen( @@ -84,24 +103,29 @@ def test_basic(self): priv = SigningKey.generate() pub = priv.get_verifying_key() - data = b("blahblah") + data = b"blahblah" sig = priv.sign(data) self.assertTrue(pub.verify(sig, data)) - self.assertRaises(BadSignatureError, pub.verify, sig, data + b("bad")) + self.assertRaises(BadSignatureError, pub.verify, sig, data + b"bad") pub2 = VerifyingKey.from_string(pub.to_string()) self.assertTrue(pub2.verify(sig, data)) def test_deterministic(self): - data = b("blahblah") + data = b"blahblah" secexp = int("9d0219792467d7d37b4d43298a7d0c05", 16) - priv = SigningKey.from_secret_exponent(secexp, SECP256k1, sha256) + priv = SigningKey.from_secret_exponent( + secexp, SECP256k1, hashlib.sha256 + ) pub = priv.get_verifying_key() k = rfc6979.generate_k( - SECP256k1.generator.order(), secexp, sha256, sha256(data).digest() + SECP256k1.generator.order(), + secexp, + hashlib.sha256, + hashlib.sha256(data).digest(), ) sig1 = priv.sign(data, k=k) @@ -110,7 +134,7 @@ def test_deterministic(self): sig2 = priv.sign(data, k=k) self.assertTrue(pub.verify(sig2, data)) - sig3 = priv.sign_deterministic(data, sha256) + sig3 = priv.sign_deterministic(data, hashlib.sha256) self.assertTrue(pub.verify(sig3, data)) self.assertEqual(sig1, sig2) @@ -121,37 +145,16 @@ def test_bad_usage(self): self.assertRaises(TypeError, SigningKey) self.assertRaises(TypeError, VerifyingKey) - def test_lengths(self): + def test_lengths_default(self): default = NIST192p priv = SigningKey.generate() pub = priv.get_verifying_key() self.assertEqual(len(pub.to_string()), default.verifying_key_length) - sig = priv.sign(b("data")) + sig = priv.sign(b"data") self.assertEqual(len(sig), default.signature_length) - for curve in ( - NIST192p, - NIST224p, - NIST256p, - NIST384p, - NIST521p, - BRAINPOOLP160r1, - BRAINPOOLP192r1, - BRAINPOOLP224r1, - BRAINPOOLP256r1, - BRAINPOOLP320r1, - BRAINPOOLP384r1, - BRAINPOOLP512r1, - ): - priv = SigningKey.generate(curve=curve) - pub1 = priv.get_verifying_key() - pub2 = VerifyingKey.from_string(pub1.to_string(), curve) - self.assertEqual(pub1.to_string(), pub2.to_string()) - self.assertEqual(len(pub1.to_string()), curve.verifying_key_length) - sig = priv.sign(b("data")) - self.assertEqual(len(sig), curve.signature_length) def test_serialize(self): - seed = b("secret") + seed = b"secret" curve = NIST192p secexp1 = util.randrange_from_seed__trytryagain(seed, curve.order) secexp2 = util.randrange_from_seed__trytryagain(seed, curve.order) @@ -164,7 +167,7 @@ def test_serialize(self): self.assertEqual(priv1.to_pem(), priv2.to_pem()) pub1 = priv1.get_verifying_key() pub2 = priv2.get_verifying_key() - data = b("data") + data = b"data" sig1 = priv1.sign(data) sig2 = priv2.sign(data) self.assertTrue(pub1.verify(sig1, data)) @@ -174,7 +177,7 @@ def test_serialize(self): self.assertEqual(hexlify(pub1.to_string()), hexlify(pub2.to_string())) def test_nonrandom(self): - s = b("all the entropy in the entire world, compressed into one line") + s = b"all the entropy in the entire world, compressed into one line" def not_much_entropy(numbytes): return s[:numbytes] @@ -190,8 +193,8 @@ def not_much_entropy(numbytes): # want to do this with keys you care about, because the secrecy of # the private key depends upon using different random numbers for # each signature - sig1 = priv1.sign(b("data"), entropy=not_much_entropy) - sig2 = priv2.sign(b("data"), entropy=not_much_entropy) + sig1 = priv1.sign(b"data", entropy=not_much_entropy) + sig2 = priv2.sign(b"data", entropy=not_much_entropy) self.assertEqual(hexlify(sig1), hexlify(sig2)) def assertTruePrivkeysEqual(self, priv1, priv2): @@ -204,7 +207,7 @@ def assertTruePrivkeysEqual(self, priv1, priv2): ) def test_privkey_creation(self): - s = b("all the entropy in the entire world, compressed into one line") + s = b"all the entropy in the entire world, compressed into one line" def not_much_entropy(numbytes): return s[:numbytes] @@ -239,8 +242,8 @@ def test_privkey_strings(self): s1 = priv1.to_pem() self.assertEqual(type(s1), binary_type) - self.assertTrue(s1.startswith(b("-----BEGIN EC PRIVATE KEY-----"))) - self.assertTrue(s1.strip().endswith(b("-----END EC PRIVATE KEY-----"))) + self.assertTrue(s1.startswith(b"-----BEGIN EC PRIVATE KEY-----")) + self.assertTrue(s1.strip().endswith(b"-----END EC PRIVATE KEY-----")) priv2 = SigningKey.from_pem(s1) self.assertTruePrivkeysEqual(priv1, priv2) @@ -252,8 +255,8 @@ def test_privkey_strings(self): priv1 = SigningKey.generate(curve=NIST256p) s1 = priv1.to_pem() self.assertEqual(type(s1), binary_type) - self.assertTrue(s1.startswith(b("-----BEGIN EC PRIVATE KEY-----"))) - self.assertTrue(s1.strip().endswith(b("-----END EC PRIVATE KEY-----"))) + self.assertTrue(s1.startswith(b"-----BEGIN EC PRIVATE KEY-----")) + self.assertTrue(s1.strip().endswith(b"-----END EC PRIVATE KEY-----")) priv2 = SigningKey.from_pem(s1) self.assertTruePrivkeysEqual(priv1, priv2) @@ -266,8 +269,8 @@ def test_privkey_strings_brainpool(self): priv1 = SigningKey.generate(curve=BRAINPOOLP512r1) s1 = priv1.to_pem() self.assertEqual(type(s1), binary_type) - self.assertTrue(s1.startswith(b("-----BEGIN EC PRIVATE KEY-----"))) - self.assertTrue(s1.strip().endswith(b("-----END EC PRIVATE KEY-----"))) + self.assertTrue(s1.startswith(b"-----BEGIN EC PRIVATE KEY-----")) + self.assertTrue(s1.strip().endswith(b"-----END EC PRIVATE KEY-----")) priv2 = SigningKey.from_pem(s1) self.assertTruePrivkeysEqual(priv1, priv2) @@ -304,7 +307,7 @@ def test_pubkey_strings(self): self.assertTruePubkeysEqual(pub1, pub2) self.assertRaises( - der.UnexpectedDER, VerifyingKey.from_der, pub1_der + b("junk") + der.UnexpectedDER, VerifyingKey.from_der, pub1_der + b"junk" ) badpub = VerifyingKey.from_der(pub1_der) @@ -328,10 +331,8 @@ def p(self): pem = pub1.to_pem() self.assertEqual(type(pem), binary_type) - self.assertTrue(pem.startswith(b("-----BEGIN PUBLIC KEY-----")), pem) - self.assertTrue( - pem.strip().endswith(b("-----END PUBLIC KEY-----")), pem - ) + self.assertTrue(pem.startswith(b"-----BEGIN PUBLIC KEY-----"), pem) + self.assertTrue(pem.strip().endswith(b"-----END PUBLIC KEY-----"), pem) pub2 = VerifyingKey.from_pem(pem) self.assertTruePubkeysEqual(pub1, pub2) @@ -364,8 +365,8 @@ def test_sk_to_der_with_invalid_point_encoding(self): def test_vk_from_der_garbage_after_curve_oid(self): type_oid_der = encoded_oid_ecPublicKey - curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b( - "garbage" + curve_oid_der = ( + der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) + b"garbage" ) enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) point_der = der.encode_bitstring(b"\x00\xff", None) @@ -388,7 +389,7 @@ def test_vk_from_der_garbage_after_point_string(self): type_oid_der = encoded_oid_ecPublicKey curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) enc_type_der = der.encode_sequence(type_oid_der, curve_oid_der) - point_der = der.encode_bitstring(b"\x00\xff", None) + b("garbage") + point_der = der.encode_bitstring(b"\x00\xff", None) + b"garbage" to_decode = der.encode_sequence(enc_type_der, point_der) with self.assertRaises(der.UnexpectedDER): @@ -427,7 +428,7 @@ def test_vk_from_der_with_raw_encoding(self): def test_signature_strings(self): priv1 = SigningKey.generate() pub1 = priv1.get_verifying_key() - data = b("data") + data = b"data" sig = priv1.sign(data) self.assertEqual(type(sig), binary_type) @@ -447,24 +448,113 @@ def test_signature_strings(self): self.assertEqual(type(sig_der), binary_type) self.assertTrue(pub1.verify(sig_der, data, sigdecode=sigdecode_der)) + def test_sigencode_string_canonize_no_change(self): + r = 12 + s = 400 + order = SECP112r1.order + + new_r, new_s = sigdecode_string( + sigencode_string_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(s, new_s) + + def test_sigencode_string_canonize(self): + r = 12 + order = SECP112r1.order + s = order - 10 + + new_r, new_s = sigdecode_string( + sigencode_string_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(order - s, new_s) + + def test_sigencode_strings_canonize_no_change(self): + r = 12 + s = 400 + order = SECP112r1.order + + new_r, new_s = sigdecode_strings( + sigencode_strings_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(s, new_s) + + def test_sigencode_strings_canonize(self): + r = 12 + order = SECP112r1.order + s = order - 10 + + new_r, new_s = sigdecode_strings( + sigencode_strings_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(order - s, new_s) + + def test_sigencode_der_canonize_no_change(self): + r = 13 + s = 200 + order = SECP112r1.order + + new_r, new_s = sigdecode_der( + sigencode_der_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(s, new_s) + + def test_sigencode_der_canonize(self): + r = 13 + order = SECP112r1.order + s = order - 14 + + new_r, new_s = sigdecode_der( + sigencode_der_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(order - s, new_s) + + def test_sigencode_der_canonize_with_close_to_half_order(self): + r = 13 + order = SECP112r1.order + s = order // 2 + 1 + + regular_encode = sigencode_der(r, s, order) + canonical_encode = sigencode_der_canonize(r, s, order) + + self.assertNotEqual(regular_encode, canonical_encode) + + new_r, new_s = sigdecode_der( + sigencode_der_canonize(r, s, order), order + ) + + self.assertEqual(r, new_r) + self.assertEqual(order - s, new_s) + def test_sig_decode_strings_with_invalid_count(self): with self.assertRaises(MalformedSignature): - sigdecode_strings([b("one"), b("two"), b("three")], 0xFF) + sigdecode_strings([b"one", b"two", b"three"], 0xFF) def test_sig_decode_strings_with_wrong_r_len(self): with self.assertRaises(MalformedSignature): - sigdecode_strings([b("one"), b("two")], 0xFF) + sigdecode_strings([b"one", b"two"], 0xFF) def test_sig_decode_strings_with_wrong_s_len(self): with self.assertRaises(MalformedSignature): - sigdecode_strings([b("\xa0"), b("\xb0\xff")], 0xFF) + sigdecode_strings([b"\xa0", b"\xb0\xff"], 0xFF) def test_verify_with_too_long_input(self): sk = SigningKey.generate() vk = sk.verifying_key with self.assertRaises(BadDigestError): - vk.verify_digest(None, b("\x00") * 128) + vk.verify_digest(None, b"\x00" * 128) def test_sk_from_secret_exponent_with_wrong_sec_exponent(self): with self.assertRaises(MalformedPointError): @@ -472,11 +562,11 @@ def test_sk_from_secret_exponent_with_wrong_sec_exponent(self): def test_sk_from_string_with_wrong_len_string(self): with self.assertRaises(MalformedPointError): - SigningKey.from_string(b("\x01")) + SigningKey.from_string(b"\x01") def test_sk_from_der_with_junk_after_sequence(self): ver_der = der.encode_integer(1) - to_decode = der.encode_sequence(ver_der) + b("garbage") + to_decode = der.encode_sequence(ver_der) + b"garbage" with self.assertRaises(der.UnexpectedDER): SigningKey.from_der(to_decode) @@ -490,7 +580,7 @@ def test_sk_from_der_with_wrong_version(self): def test_sk_from_der_invalid_const_tag(self): ver_der = der.encode_integer(1) - privkey_der = der.encode_octet_string(b("\x00\xff")) + privkey_der = der.encode_octet_string(b"\x00\xff") curve_oid_der = der.encode_oid(*(1, 2, 3)) const_der = der.encode_constructed(1, curve_oid_der) to_decode = der.encode_sequence( @@ -502,8 +592,8 @@ def test_sk_from_der_invalid_const_tag(self): def test_sk_from_der_garbage_after_privkey_oid(self): ver_der = der.encode_integer(1) - privkey_der = der.encode_octet_string(b("\x00\xff")) - curve_oid_der = der.encode_oid(*(1, 2, 3)) + b("garbage") + privkey_der = der.encode_octet_string(b"\x00\xff") + curve_oid_der = der.encode_oid(*(1, 2, 3)) + b"garbage" const_der = der.encode_constructed(0, curve_oid_der) to_decode = der.encode_sequence( ver_der, privkey_der, const_der, curve_oid_der @@ -514,7 +604,7 @@ def test_sk_from_der_garbage_after_privkey_oid(self): def test_sk_from_der_with_short_privkey(self): ver_der = der.encode_integer(1) - privkey_der = der.encode_octet_string(b("\x00\xff")) + privkey_der = der.encode_octet_string(b"\x00\xff") curve_oid_der = der.encode_oid(*(1, 2, 840, 10045, 3, 1, 1)) const_der = der.encode_constructed(0, curve_oid_der) to_decode = der.encode_sequence( @@ -598,30 +688,32 @@ def test_sign_with_too_long_hash(self): sk = SigningKey.from_secret_exponent(12) with self.assertRaises(BadDigestError): - sk.sign_digest(b("\xff") * 64) + sk.sign_digest(b"\xff" * 64) def test_hashfunc(self): - sk = SigningKey.generate(curve=NIST256p, hashfunc=sha256) - data = b("security level is 128 bits") + sk = SigningKey.generate(curve=NIST256p, hashfunc=hashlib.sha256) + data = b"security level is 128 bits" sig = sk.sign(data) vk = VerifyingKey.from_string( - sk.get_verifying_key().to_string(), curve=NIST256p, hashfunc=sha256 + sk.get_verifying_key().to_string(), + curve=NIST256p, + hashfunc=hashlib.sha256, ) self.assertTrue(vk.verify(sig, data)) sk2 = SigningKey.generate(curve=NIST256p) - sig2 = sk2.sign(data, hashfunc=sha256) + sig2 = sk2.sign(data, hashfunc=hashlib.sha256) vk2 = VerifyingKey.from_string( sk2.get_verifying_key().to_string(), curve=NIST256p, - hashfunc=sha256, + hashfunc=hashlib.sha256, ) self.assertTrue(vk2.verify(sig2, data)) vk3 = VerifyingKey.from_string( sk.get_verifying_key().to_string(), curve=NIST256p ) - self.assertTrue(vk3.verify(sig, data, hashfunc=sha256)) + self.assertTrue(vk3.verify(sig, data, hashfunc=hashlib.sha256)) def test_public_key_recovery(self): # Create keys @@ -631,7 +723,7 @@ def test_public_key_recovery(self): vk = sk.get_verifying_key() # Sign a message - data = b("blahblah") + data = b"blahblah" signature = sk.sign(data) # Recover verifying keys @@ -660,16 +752,20 @@ def test_public_key_recovery_with_custom_hash(self): # Create keys curve = BRAINPOOLP160r1 - sk = SigningKey.generate(curve=curve, hashfunc=sha256) + sk = SigningKey.generate(curve=curve, hashfunc=hashlib.sha256) vk = sk.get_verifying_key() # Sign a message - data = b("blahblah") + data = b"blahblah" signature = sk.sign(data) # Recover verifying keys recovered_vks = VerifyingKey.from_public_key_recovery( - signature, data, curve, hashfunc=sha256, allow_truncate=True + signature, + data, + curve, + hashfunc=hashlib.sha256, + allow_truncate=True, ) # Test if each pk is valid @@ -679,7 +775,7 @@ def test_public_key_recovery_with_custom_hash(self): # Test if properties are equal self.assertEqual(vk.curve, recovered_vk.curve) - self.assertEqual(sha256, recovered_vk.default_hashfunc) + self.assertEqual(hashlib.sha256, recovered_vk.default_hashfunc) # Test if original vk is the list of recovered keys self.assertIn( @@ -691,45 +787,45 @@ def test_encoding(self): sk = SigningKey.from_secret_exponent(123456789) vk = sk.verifying_key - exp = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + exp = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) self.assertEqual(vk.to_string(), exp) self.assertEqual(vk.to_string("raw"), exp) - self.assertEqual(vk.to_string("uncompressed"), b("\x04") + exp) - self.assertEqual(vk.to_string("compressed"), b("\x02") + exp[:24]) - self.assertEqual(vk.to_string("hybrid"), b("\x06") + exp) + self.assertEqual(vk.to_string("uncompressed"), b"\x04" + exp) + self.assertEqual(vk.to_string("compressed"), b"\x02" + exp[:24]) + self.assertEqual(vk.to_string("hybrid"), b"\x06" + exp) def test_decoding(self): sk = SigningKey.from_secret_exponent(123456789) vk = sk.verifying_key - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) from_raw = VerifyingKey.from_string(enc) self.assertEqual(from_raw.pubkey.point, vk.pubkey.point) - from_uncompressed = VerifyingKey.from_string(b("\x04") + enc) + from_uncompressed = VerifyingKey.from_string(b"\x04" + enc) self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point) - from_compressed = VerifyingKey.from_string(b("\x02") + enc[:24]) + from_compressed = VerifyingKey.from_string(b"\x02" + enc[:24]) self.assertEqual(from_compressed.pubkey.point, vk.pubkey.point) - from_uncompressed = VerifyingKey.from_string(b("\x06") + enc) + from_uncompressed = VerifyingKey.from_string(b"\x06" + enc) self.assertEqual(from_uncompressed.pubkey.point, vk.pubkey.point) def test_uncompressed_decoding_as_only_alowed(self): - enc = b( - "\x04" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x04" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) vk = VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) sk = SigningKey.from_secret_exponent(123456789) @@ -737,10 +833,10 @@ def test_uncompressed_decoding_as_only_alowed(self): self.assertEqual(vk, sk.verifying_key) def test_raw_decoding_with_blocked_format(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) @@ -754,11 +850,11 @@ def test_decoding_with_unknown_format(self): self.assertIn("Only uncompressed, compressed", str(e.exception)) def test_uncompressed_decoding_with_blocked_format(self): - enc = b( - "\x04" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x04" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) @@ -766,23 +862,39 @@ def test_uncompressed_decoding_with_blocked_format(self): self.assertIn("Invalid X9.62 encoding", str(exp.exception)) def test_hybrid_decoding_with_blocked_format(self): - enc = b( - "\x06" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x06" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("uncompressed",)) self.assertIn("Invalid X9.62 encoding", str(exp.exception)) + def test_hybrid_decoding_with_inconsistent_encoding_and_no_validation( + self, + ): + sk = SigningKey.from_secret_exponent(123456789) + vk = sk.verifying_key + + enc = vk.to_string("hybrid") + self.assertEqual(enc[:1], b"\x06") + enc = b"\x07" + enc[1:] + + b = VerifyingKey.from_string( + enc, valid_encodings=("hybrid",), validate_point=False + ) + + self.assertEqual(vk, b) + def test_compressed_decoding_with_blocked_format(self): - enc = b( - "\x02" - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x02" + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" )[:25] with self.assertRaises(MalformedPointError) as exp: VerifyingKey.from_string(enc, valid_encodings=("hybrid", "raw")) @@ -790,55 +902,66 @@ def test_compressed_decoding_with_blocked_format(self): self.assertIn("(hybrid, raw)", str(exp.exception)) def test_decoding_with_malformed_uncompressed(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x02") + enc) + VerifyingKey.from_string(b"\x02" + enc) def test_decoding_with_malformed_compressed(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x01") + enc[:24]) + VerifyingKey.from_string(b"\x01" + enc[:24]) def test_decoding_with_inconsistent_hybrid(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x07") + enc) + VerifyingKey.from_string(b"\x07" + enc) + + def test_decoding_with_inconsistent_hybrid_odd_point(self): + sk = SigningKey.from_secret_exponent(123456791) + vk = sk.verifying_key + + enc = vk.to_string("hybrid") + self.assertEqual(enc[:1], b"\x07") + enc = b"\x06" + enc[1:] + + with self.assertRaises(MalformedPointError): + b = VerifyingKey.from_string(enc, valid_encodings=("hybrid",)) def test_decoding_with_point_not_on_curve(self): - enc = b( - "\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" - "\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" - "z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" + enc = ( + b"\x0c\xe0\x1d\xe0d\x1c\x8eS\x8a\xc0\x9eK\xa8x !\xd5\xc2\xc3" + b"\xfd\xc8\xa0c\xff\xfb\x02\xb9\xc4\x84)\x1a\x0f\x8b\x87\xa4" + b"z\x8a#\xb5\x97\xecO\xb6\xa0HQ\x89*" ) with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(enc[:47] + b("\x00")) + VerifyingKey.from_string(enc[:47] + b"\x00") def test_decoding_with_point_at_infinity(self): # decoding it is unsupported, as it's not necessary to encode it with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x00")) + VerifyingKey.from_string(b"\x00") def test_not_lying_on_curve(self): enc = number_to_string(NIST192p.curve.p(), NIST192p.curve.p() + 1) with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x02") + enc) + VerifyingKey.from_string(b"\x02" + enc) def test_from_string_with_invalid_curve_too_short_ver_key_len(self): # both verifying_key_length and baselen are calculated internally @@ -849,7 +972,7 @@ def test_from_string_with_invalid_curve_too_short_ver_key_len(self): curve.baselen = 32 with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x00") * 16, curve) + VerifyingKey.from_string(b"\x00" * 16, curve) def test_from_string_with_invalid_curve_too_long_ver_key_len(self): # both verifying_key_length and baselen are calculated internally @@ -860,7 +983,7 @@ def test_from_string_with_invalid_curve_too_long_ver_key_len(self): curve.baselen = 16 with self.assertRaises(MalformedPointError): - VerifyingKey.from_string(b("\x00") * 16, curve) + VerifyingKey.from_string(b"\x00" * 16, curve) @pytest.mark.parametrize( @@ -870,9 +993,9 @@ def test_VerifyingKey_decode_with_small_values(val, even): enc = number_to_string(val, NIST192p.order) if even: - enc = b("\x02") + enc + enc = b"\x02" + enc else: - enc = b("\x03") + enc + enc = b"\x03" + enc # small values can both be actual valid public keys and not, verify that # only expected exceptions are raised if they are not @@ -903,6 +1026,24 @@ def test_VerifyingKey_encode_decode(curve, encoding): assert vk.pubkey.point == from_enc.pubkey.point +if "--fast" in sys.argv: # pragma: no cover + params = [NIST192p, BRAINPOOLP160r1] +else: + params = curves + + +@pytest.mark.parametrize("curve", params) +def test_lengths(curve): + priv = SigningKey.generate(curve=curve) + pub1 = priv.get_verifying_key() + pub2 = VerifyingKey.from_string(pub1.to_string(), curve) + assert pub1.to_string() == pub2.to_string() + assert len(pub1.to_string()) == curve.verifying_key_length + sig = priv.sign(b"data") + assert len(sig) == curve.signature_length + + +@pytest.mark.slow class OpenSSL(unittest.TestCase): # test interoperability with OpenSSL tools. Note that openssl's ECDSA # sign/verify arguments changed between 0.9.8 and 1.0.0: the early @@ -939,6 +1080,7 @@ def get_openssl_messagedigest_arg(self, hash_name): # vk: 3:OpenSSL->python 4:python->OpenSSL # sig: 5:OpenSSL->python 6:python->OpenSSL + @pytest.mark.slow @pytest.mark.skipif( "secp112r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp112r1", @@ -946,6 +1088,7 @@ def get_openssl_messagedigest_arg(self, hash_name): def test_from_openssl_secp112r1(self): return self.do_test_from_openssl(SECP112r1) + @pytest.mark.slow @pytest.mark.skipif( "secp112r2" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp112r2", @@ -953,6 +1096,7 @@ def test_from_openssl_secp112r1(self): def test_from_openssl_secp112r2(self): return self.do_test_from_openssl(SECP112r2) + @pytest.mark.slow @pytest.mark.skipif( "secp128r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp128r1", @@ -960,6 +1104,7 @@ def test_from_openssl_secp112r2(self): def test_from_openssl_secp128r1(self): return self.do_test_from_openssl(SECP128r1) + @pytest.mark.slow @pytest.mark.skipif( "secp160r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp160r1", @@ -967,6 +1112,7 @@ def test_from_openssl_secp128r1(self): def test_from_openssl_secp160r1(self): return self.do_test_from_openssl(SECP160r1) + @pytest.mark.slow @pytest.mark.skipif( "prime192v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime192v1", @@ -974,6 +1120,7 @@ def test_from_openssl_secp160r1(self): def test_from_openssl_nist192p(self): return self.do_test_from_openssl(NIST192p) + @pytest.mark.slow @pytest.mark.skipif( "prime192v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime192v1", @@ -981,6 +1128,7 @@ def test_from_openssl_nist192p(self): def test_from_openssl_nist192p_sha256(self): return self.do_test_from_openssl(NIST192p, "SHA256") + @pytest.mark.slow @pytest.mark.skipif( "secp224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp224r1", @@ -988,6 +1136,7 @@ def test_from_openssl_nist192p_sha256(self): def test_from_openssl_nist224p(self): return self.do_test_from_openssl(NIST224p) + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -995,6 +1144,7 @@ def test_from_openssl_nist224p(self): def test_from_openssl_nist256p(self): return self.do_test_from_openssl(NIST256p) + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -1002,6 +1152,7 @@ def test_from_openssl_nist256p(self): def test_from_openssl_nist256p_sha384(self): return self.do_test_from_openssl(NIST256p, "SHA384") + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -1009,6 +1160,7 @@ def test_from_openssl_nist256p_sha384(self): def test_from_openssl_nist256p_sha512(self): return self.do_test_from_openssl(NIST256p, "SHA512") + @pytest.mark.slow @pytest.mark.skipif( "secp384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp384r1", @@ -1016,6 +1168,7 @@ def test_from_openssl_nist256p_sha512(self): def test_from_openssl_nist384p(self): return self.do_test_from_openssl(NIST384p) + @pytest.mark.slow @pytest.mark.skipif( "secp521r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp521r1", @@ -1023,6 +1176,7 @@ def test_from_openssl_nist384p(self): def test_from_openssl_nist521p(self): return self.do_test_from_openssl(NIST521p) + @pytest.mark.slow @pytest.mark.skipif( "secp256k1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp256k1", @@ -1030,6 +1184,7 @@ def test_from_openssl_nist521p(self): def test_from_openssl_secp256k1(self): return self.do_test_from_openssl(SECP256k1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP160r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP160r1", @@ -1037,6 +1192,7 @@ def test_from_openssl_secp256k1(self): def test_from_openssl_brainpoolp160r1(self): return self.do_test_from_openssl(BRAINPOOLP160r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP192r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP192r1", @@ -1044,6 +1200,7 @@ def test_from_openssl_brainpoolp160r1(self): def test_from_openssl_brainpoolp192r1(self): return self.do_test_from_openssl(BRAINPOOLP192r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP224r1", @@ -1051,6 +1208,7 @@ def test_from_openssl_brainpoolp192r1(self): def test_from_openssl_brainpoolp224r1(self): return self.do_test_from_openssl(BRAINPOOLP224r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP256r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP256r1", @@ -1058,6 +1216,7 @@ def test_from_openssl_brainpoolp224r1(self): def test_from_openssl_brainpoolp256r1(self): return self.do_test_from_openssl(BRAINPOOLP256r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP320r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP320r1", @@ -1065,6 +1224,7 @@ def test_from_openssl_brainpoolp256r1(self): def test_from_openssl_brainpoolp320r1(self): return self.do_test_from_openssl(BRAINPOOLP320r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP384r1", @@ -1072,6 +1232,7 @@ def test_from_openssl_brainpoolp320r1(self): def test_from_openssl_brainpoolp384r1(self): return self.do_test_from_openssl(BRAINPOOLP384r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP512r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP512r1", @@ -1079,6 +1240,62 @@ def test_from_openssl_brainpoolp384r1(self): def test_from_openssl_brainpoolp512r1(self): return self.do_test_from_openssl(BRAINPOOLP512r1) + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP160t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP160t1", + ) + def test_from_openssl_brainpoolp160t1(self): + return self.do_test_from_openssl(BRAINPOOLP160t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP192t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP192t1", + ) + def test_from_openssl_brainpoolp192t1(self): + return self.do_test_from_openssl(BRAINPOOLP192t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP224t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP224t1", + ) + def test_from_openssl_brainpoolp224t1(self): + return self.do_test_from_openssl(BRAINPOOLP224t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP256t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP256t1", + ) + def test_from_openssl_brainpoolp256t1(self): + return self.do_test_from_openssl(BRAINPOOLP256t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP320t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP320t1", + ) + def test_from_openssl_brainpoolp320t1(self): + return self.do_test_from_openssl(BRAINPOOLP320t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP384t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP384t1", + ) + def test_from_openssl_brainpoolp384t1(self): + return self.do_test_from_openssl(BRAINPOOLP384t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP512t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP512t1", + ) + def test_from_openssl_brainpoolp512t1(self): + return self.do_test_from_openssl(BRAINPOOLP512t1) + def do_test_from_openssl(self, curve, hash_name="SHA1"): curvename = curve.openssl_name assert curvename @@ -1090,7 +1307,7 @@ def do_test_from_openssl(self, curve, hash_name="SHA1"): os.mkdir("t") run_openssl("ecparam -name %s -genkey -out t/privkey.pem" % curvename) run_openssl("ec -in t/privkey.pem -pubout -out t/pubkey.pem") - data = b("data") + data = b"data" with open("t/data.txt", "wb") as e: e.write(data) run_openssl( @@ -1131,6 +1348,7 @@ def do_test_from_openssl(self, curve, hash_name="SHA1"): sk_from_p8 = SigningKey.from_pem(privkey_p8_pem) self.assertEqual(sk, sk_from_p8) + @pytest.mark.slow @pytest.mark.skipif( "secp112r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp112r1", @@ -1138,6 +1356,7 @@ def do_test_from_openssl(self, curve, hash_name="SHA1"): def test_to_openssl_secp112r1(self): self.do_test_to_openssl(SECP112r1) + @pytest.mark.slow @pytest.mark.skipif( "secp112r2" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp112r2", @@ -1145,6 +1364,7 @@ def test_to_openssl_secp112r1(self): def test_to_openssl_secp112r2(self): self.do_test_to_openssl(SECP112r2) + @pytest.mark.slow @pytest.mark.skipif( "secp128r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp128r1", @@ -1152,6 +1372,7 @@ def test_to_openssl_secp112r2(self): def test_to_openssl_secp128r1(self): self.do_test_to_openssl(SECP128r1) + @pytest.mark.slow @pytest.mark.skipif( "secp160r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp160r1", @@ -1159,6 +1380,7 @@ def test_to_openssl_secp128r1(self): def test_to_openssl_secp160r1(self): self.do_test_to_openssl(SECP160r1) + @pytest.mark.slow @pytest.mark.skipif( "prime192v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime192v1", @@ -1166,6 +1388,7 @@ def test_to_openssl_secp160r1(self): def test_to_openssl_nist192p(self): self.do_test_to_openssl(NIST192p) + @pytest.mark.slow @pytest.mark.skipif( "prime192v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime192v1", @@ -1173,6 +1396,7 @@ def test_to_openssl_nist192p(self): def test_to_openssl_nist192p_sha256(self): self.do_test_to_openssl(NIST192p, "SHA256") + @pytest.mark.slow @pytest.mark.skipif( "secp224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp224r1", @@ -1180,6 +1404,7 @@ def test_to_openssl_nist192p_sha256(self): def test_to_openssl_nist224p(self): self.do_test_to_openssl(NIST224p) + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -1187,6 +1412,7 @@ def test_to_openssl_nist224p(self): def test_to_openssl_nist256p(self): self.do_test_to_openssl(NIST256p) + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -1194,6 +1420,7 @@ def test_to_openssl_nist256p(self): def test_to_openssl_nist256p_sha384(self): self.do_test_to_openssl(NIST256p, "SHA384") + @pytest.mark.slow @pytest.mark.skipif( "prime256v1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support prime256v1", @@ -1201,6 +1428,7 @@ def test_to_openssl_nist256p_sha384(self): def test_to_openssl_nist256p_sha512(self): self.do_test_to_openssl(NIST256p, "SHA512") + @pytest.mark.slow @pytest.mark.skipif( "secp384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp384r1", @@ -1208,6 +1436,7 @@ def test_to_openssl_nist256p_sha512(self): def test_to_openssl_nist384p(self): self.do_test_to_openssl(NIST384p) + @pytest.mark.slow @pytest.mark.skipif( "secp521r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp521r1", @@ -1215,6 +1444,7 @@ def test_to_openssl_nist384p(self): def test_to_openssl_nist521p(self): self.do_test_to_openssl(NIST521p) + @pytest.mark.slow @pytest.mark.skipif( "secp256k1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support secp256k1", @@ -1222,6 +1452,7 @@ def test_to_openssl_nist521p(self): def test_to_openssl_secp256k1(self): self.do_test_to_openssl(SECP256k1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP160r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP160r1", @@ -1229,6 +1460,7 @@ def test_to_openssl_secp256k1(self): def test_to_openssl_brainpoolp160r1(self): self.do_test_to_openssl(BRAINPOOLP160r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP192r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP192r1", @@ -1236,6 +1468,7 @@ def test_to_openssl_brainpoolp160r1(self): def test_to_openssl_brainpoolp192r1(self): self.do_test_to_openssl(BRAINPOOLP192r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP224r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP224r1", @@ -1243,6 +1476,7 @@ def test_to_openssl_brainpoolp192r1(self): def test_to_openssl_brainpoolp224r1(self): self.do_test_to_openssl(BRAINPOOLP224r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP256r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP256r1", @@ -1250,6 +1484,7 @@ def test_to_openssl_brainpoolp224r1(self): def test_to_openssl_brainpoolp256r1(self): self.do_test_to_openssl(BRAINPOOLP256r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP320r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP320r1", @@ -1257,6 +1492,7 @@ def test_to_openssl_brainpoolp256r1(self): def test_to_openssl_brainpoolp320r1(self): self.do_test_to_openssl(BRAINPOOLP320r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP384r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP384r1", @@ -1264,6 +1500,7 @@ def test_to_openssl_brainpoolp320r1(self): def test_to_openssl_brainpoolp384r1(self): self.do_test_to_openssl(BRAINPOOLP384r1) + @pytest.mark.slow @pytest.mark.skipif( "brainpoolP512r1" not in OPENSSL_SUPPORTED_CURVES, reason="system openssl does not support brainpoolP512r1", @@ -1271,6 +1508,62 @@ def test_to_openssl_brainpoolp384r1(self): def test_to_openssl_brainpoolp512r1(self): self.do_test_to_openssl(BRAINPOOLP512r1) + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP160t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP160t1", + ) + def test_to_openssl_brainpoolp160t1(self): + self.do_test_to_openssl(BRAINPOOLP160t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP192t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP192t1", + ) + def test_to_openssl_brainpoolp192t1(self): + self.do_test_to_openssl(BRAINPOOLP192t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP224t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP224t1", + ) + def test_to_openssl_brainpoolp224t1(self): + self.do_test_to_openssl(BRAINPOOLP224t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP256t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP256t1", + ) + def test_to_openssl_brainpoolp256t1(self): + self.do_test_to_openssl(BRAINPOOLP256t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP320t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP320t1", + ) + def test_to_openssl_brainpoolp320t1(self): + self.do_test_to_openssl(BRAINPOOLP320t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP384t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP384t1", + ) + def test_to_openssl_brainpoolp384t1(self): + self.do_test_to_openssl(BRAINPOOLP384t1) + + @pytest.mark.slow + @pytest.mark.skipif( + "brainpoolP512t1" not in OPENSSL_SUPPORTED_CURVES, + reason="system openssl does not support brainpoolP512t1", + ) + def test_to_openssl_brainpoolp512t1(self): + self.do_test_to_openssl(BRAINPOOLP512t1) + def do_test_to_openssl(self, curve, hash_name="SHA1"): curvename = curve.openssl_name assert curvename @@ -1282,7 +1575,7 @@ def do_test_to_openssl(self, curve, hash_name="SHA1"): os.mkdir("t") sk = SigningKey.generate(curve=curve) vk = sk.get_verifying_key() - data = b("data") + data = b"data" with open("t/pubkey.der", "wb") as e: e.write(vk.to_der()) # 4 with open("t/pubkey.pem", "wb") as e: @@ -1298,7 +1591,7 @@ def do_test_to_openssl(self, curve, hash_name="SHA1"): with open("t/data.txt", "wb") as e: e.write(data) with open("t/baddata.txt", "wb") as e: - e.write(data + b("corrupt")) + e.write(data + b"corrupt") self.assertRaises( SubprocessError, @@ -1359,17 +1652,15 @@ def do_test_to_openssl(self, curve, hash_name="SHA1"): OPENSSL_SUPPORTED_TYPES = set() try: if "-rawin" in run_openssl("pkeyutl -help"): - OPENSSL_SUPPORTED_TYPES = set( + OPENSSL_SUPPORTED_TYPES = set( # pragma: no branch c.lower() for c in ("ED25519", "ED448") if c in run_openssl("list -public-key-methods") ) - except SubprocessError: + except SubprocessError: # pragma: no cover pass def do_eddsa_test_to_openssl(self, curve): - curvename = curve.name.upper() - if os.path.isdir("t"): shutil.rmtree("t") os.mkdir("t") @@ -1406,6 +1697,7 @@ def do_eddsa_test_to_openssl(self, curve): # in practice at least OpenSSL 3.0.0 is needed to make EdDSA signatures # earlier versions support EdDSA only in X.509 certificates + @pytest.mark.slow @pytest.mark.skipif( "ed25519" not in OPENSSL_SUPPORTED_TYPES, reason="system openssl does not support signing with Ed25519", @@ -1413,6 +1705,7 @@ def do_eddsa_test_to_openssl(self, curve): def test_to_openssl_ed25519(self): return self.do_eddsa_test_to_openssl(Ed25519) + @pytest.mark.slow @pytest.mark.skipif( "ed448" not in OPENSSL_SUPPORTED_TYPES, reason="system openssl does not support signing with Ed448", @@ -1456,6 +1749,7 @@ def do_eddsa_test_from_openssl(self, curve): shutil.rmtree("t") + @pytest.mark.slow @pytest.mark.skipif( "ed25519" not in OPENSSL_SUPPORTED_TYPES, reason="system openssl does not support signing with Ed25519", @@ -1463,6 +1757,7 @@ def do_eddsa_test_from_openssl(self, curve): def test_from_openssl_ed25519(self): return self.do_eddsa_test_from_openssl(Ed25519) + @pytest.mark.slow @pytest.mark.skipif( "ed448" not in OPENSSL_SUPPORTED_TYPES, reason="system openssl does not support signing with Ed448", @@ -1483,7 +1778,7 @@ class TooSmallCurve(unittest.TestCase): ) def test_sign_too_small_curve_dont_allow_truncate_raises(self): sk = SigningKey.generate(curve=NIST192p) - data = b("data") + data = b"data" with self.assertRaises(BadDigestError): sk.sign( data, @@ -1499,7 +1794,7 @@ def test_sign_too_small_curve_dont_allow_truncate_raises(self): def test_verify_too_small_curve_dont_allow_truncate_raises(self): sk = SigningKey.generate(curve=NIST192p) vk = sk.get_verifying_key() - data = b("data") + data = b"data" sig_der = sk.sign( data, hashfunc=partial(hashlib.new, "SHA256"), @@ -1518,69 +1813,70 @@ def test_verify_too_small_curve_dont_allow_truncate_raises(self): class DER(unittest.TestCase): def test_integer(self): - self.assertEqual(der.encode_integer(0), b("\x02\x01\x00")) - self.assertEqual(der.encode_integer(1), b("\x02\x01\x01")) - self.assertEqual(der.encode_integer(127), b("\x02\x01\x7f")) - self.assertEqual(der.encode_integer(128), b("\x02\x02\x00\x80")) - self.assertEqual(der.encode_integer(256), b("\x02\x02\x01\x00")) - # self.assertEqual(der.encode_integer(-1), b("\x02\x01\xff")) + self.assertEqual(der.encode_integer(0), b"\x02\x01\x00") + self.assertEqual(der.encode_integer(1), b"\x02\x01\x01") + self.assertEqual(der.encode_integer(127), b"\x02\x01\x7f") + self.assertEqual(der.encode_integer(128), b"\x02\x02\x00\x80") + self.assertEqual(der.encode_integer(256), b"\x02\x02\x01\x00") + # self.assertEqual(der.encode_integer(-1), b"\x02\x01\xff") def s(n): - return der.remove_integer(der.encode_integer(n) + b("junk")) + return der.remove_integer(der.encode_integer(n) + b"junk") - self.assertEqual(s(0), (0, b("junk"))) - self.assertEqual(s(1), (1, b("junk"))) - self.assertEqual(s(127), (127, b("junk"))) - self.assertEqual(s(128), (128, b("junk"))) - self.assertEqual(s(256), (256, b("junk"))) + self.assertEqual(s(0), (0, b"junk")) + self.assertEqual(s(1), (1, b"junk")) + self.assertEqual(s(127), (127, b"junk")) + self.assertEqual(s(128), (128, b"junk")) + self.assertEqual(s(256), (256, b"junk")) self.assertEqual( s(1234567890123456789012345678901234567890), - (1234567890123456789012345678901234567890, b("junk")), + (1234567890123456789012345678901234567890, b"junk"), ) def test_number(self): - self.assertEqual(der.encode_number(0), b("\x00")) - self.assertEqual(der.encode_number(127), b("\x7f")) - self.assertEqual(der.encode_number(128), b("\x81\x00")) - self.assertEqual(der.encode_number(3 * 128 + 7), b("\x83\x07")) + self.assertEqual(der.encode_number(0), b"\x00") + self.assertEqual(der.encode_number(127), b"\x7f") + self.assertEqual(der.encode_number(128), b"\x81\x00") + self.assertEqual(der.encode_number(3 * 128 + 7), b"\x83\x07") # self.assertEqual(der.read_number("\x81\x9b" + "more"), (155, 2)) - # self.assertEqual(der.encode_number(155), b("\x81\x9b")) + # self.assertEqual(der.encode_number(155), b"\x81\x9b") for n in (0, 1, 2, 127, 128, 3 * 128 + 7, 840, 10045): # , 155): - x = der.encode_number(n) + b("more") + x = der.encode_number(n) + b"more" n1, llen = der.read_number(x) self.assertEqual(n1, n) - self.assertEqual(x[llen:], b("more")) + self.assertEqual(x[llen:], b"more") def test_length(self): - self.assertEqual(der.encode_length(0), b("\x00")) - self.assertEqual(der.encode_length(127), b("\x7f")) - self.assertEqual(der.encode_length(128), b("\x81\x80")) - self.assertEqual(der.encode_length(255), b("\x81\xff")) - self.assertEqual(der.encode_length(256), b("\x82\x01\x00")) - self.assertEqual(der.encode_length(3 * 256 + 7), b("\x82\x03\x07")) - self.assertEqual(der.read_length(b("\x81\x9b") + b("more")), (155, 2)) - self.assertEqual(der.encode_length(155), b("\x81\x9b")) + self.assertEqual(der.encode_length(0), b"\x00") + self.assertEqual(der.encode_length(127), b"\x7f") + self.assertEqual(der.encode_length(128), b"\x81\x80") + self.assertEqual(der.encode_length(255), b"\x81\xff") + self.assertEqual(der.encode_length(256), b"\x82\x01\x00") + self.assertEqual(der.encode_length(3 * 256 + 7), b"\x82\x03\x07") + self.assertEqual(der.read_length(b"\x81\x9b" + b"more"), (155, 2)) + self.assertEqual(der.encode_length(155), b"\x81\x9b") for n in (0, 1, 2, 127, 128, 255, 256, 3 * 256 + 7, 155): - x = der.encode_length(n) + b("more") + x = der.encode_length(n) + b"more" n1, llen = der.read_length(x) self.assertEqual(n1, n) - self.assertEqual(x[llen:], b("more")) + self.assertEqual(x[llen:], b"more") def test_sequence(self): - x = der.encode_sequence(b("ABC"), b("DEF")) + b("GHI") - self.assertEqual(x, b("\x30\x06ABCDEFGHI")) + x = der.encode_sequence(b"ABC", b"DEF") + b"GHI" + self.assertEqual(x, b"\x30\x06ABCDEFGHI") x1, rest = der.remove_sequence(x) - self.assertEqual(x1, b("ABCDEF")) - self.assertEqual(rest, b("GHI")) + self.assertEqual(x1, b"ABCDEF") + self.assertEqual(rest, b"GHI") def test_constructed(self): x = der.encode_constructed(0, NIST224p.encoded_oid) - self.assertEqual(hexlify(x), b("a007") + b("06052b81040021")) - x = der.encode_constructed(1, unhexlify(b("0102030a0b0c"))) - self.assertEqual(hexlify(x), b("a106") + b("0102030a0b0c")) + self.assertEqual(hexlify(x), b"a007" + b"06052b81040021") + x = der.encode_constructed(1, unhexlify(b"0102030a0b0c")) + self.assertEqual(hexlify(x), b"a106" + b"0102030a0b0c") class Util(unittest.TestCase): + @pytest.mark.slow def test_trytryagain(self): tta = util.randrange_from_seed__trytryagain for i in range(1000): @@ -1599,7 +1895,7 @@ def test_trytryagain(self): # this trytryagain *does* provide long-term stability self.assertEqual( ("%x" % (tta("seed", NIST224p.order))).encode(), - b("6fa59d73bf0446ae8743cf748fc5ac11d5585a90356417e97155c3bc"), + b"6fa59d73bf0446ae8743cf748fc5ac11d5585a90356417e97155c3bc", ) def test_trytryagain_single(self): @@ -1610,9 +1906,10 @@ def test_trytryagain_single(self): # known issue: https://github.com/warner/python-ecdsa/issues/221 if sys.version_info < (3, 0): # pragma: no branch self.assertEqual(n, 228) - else: + else: # pragma: no branch self.assertEqual(n, 18) + @settings(**HYP_SETTINGS) @given(st.integers(min_value=0, max_value=10**200)) def test_randrange(self, i): # util.randrange does not provide long-term stability: we might @@ -1641,7 +1938,7 @@ def OFF_test_prove_uniformity(self): # pragma: no cover # this technique should use the full range self.assertTrue(counts[order - 1]) for i in range(1, order): - print_("%3d: %s" % (i, "*" * (counts[i] // 100))) + print("%3d: %s" % (i, "*" * (counts[i] // 100))) class RFC6979(unittest.TestCase): @@ -1656,8 +1953,8 @@ def test_SECP256k1(self): self._do( generator=SECP256k1.generator, secexp=int("9d0219792467d7d37b4d43298a7d0c05", 16), - hsh=sha256(b("sample")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"sample").digest(), + hash_func=hashlib.sha256, expected=int( "8fa1f95d514760e498f28957b824ee6ec39ed64826ff4fecc2b5739ec45b91cd", 16, @@ -1671,8 +1968,8 @@ def test_SECP256k1_2(self): "cca9fbcc1b41e5a95d369eaa6ddcff73b61a4efaa279cfc6567e8daa39cbaf50", 16, ), - hsh=sha256(b("sample")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"sample").digest(), + hash_func=hashlib.sha256, expected=int( "2df40ca70e639d89528a6b670d9d48d9165fdc0febc0974056bdce192b8e16a3", 16, @@ -1683,8 +1980,8 @@ def test_SECP256k1_3(self): self._do( generator=SECP256k1.generator, secexp=0x1, - hsh=sha256(b("Satoshi Nakamoto")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"Satoshi Nakamoto").digest(), + hash_func=hashlib.sha256, expected=0x8F8A276C19F4149656B280621E358CCE24F5F52542772691EE69063B74F15D15, ) @@ -1692,12 +1989,10 @@ def test_SECP256k1_4(self): self._do( generator=SECP256k1.generator, secexp=0x1, - hsh=sha256( - b( - "All those moments will be lost in time, like tears in rain. Time to die..." - ) + hsh=hashlib.sha256( + b"All those moments will be lost in time, like tears in rain. Time to die..." ).digest(), - hash_func=sha256, + hash_func=hashlib.sha256, expected=0x38AA22D72376B4DBC472E06C3BA403EE0A394DA63FC58D88686C611ABA98D6B3, ) @@ -1705,8 +2000,8 @@ def test_SECP256k1_5(self): self._do( generator=SECP256k1.generator, secexp=0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140, - hsh=sha256(b("Satoshi Nakamoto")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"Satoshi Nakamoto").digest(), + hash_func=hashlib.sha256, expected=0x33A19B60E25FB6F4435AF53A3D42D493644827367E6453928554F43E49AA6F90, ) @@ -1714,8 +2009,8 @@ def test_SECP256k1_6(self): self._do( generator=SECP256k1.generator, secexp=0xF8B8AF8CE3C7CCA5E300D33939540C10D45CE001B8F252BFBC57BA0342904181, - hsh=sha256(b("Alan Turing")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"Alan Turing").digest(), + hash_func=hashlib.sha256, expected=0x525A82B70E67874398067543FD84C83D30C175FDC45FDEEE082FE13B1D7CFDF1, ) @@ -1730,11 +2025,9 @@ def test_1(self): ), secexp=int("09A4D6792295A7F730FC3F2B49CBC0F62E862272F", 16), hsh=unhexlify( - b( - "AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" - ) + b"AF2BDBE1AA9B6EC1E2ADE1D694F41FC71A831D0268E9891562113D8A62ADD1BF" ), - hash_func=sha256, + hash_func=hashlib.sha256, expected=int("23AF4074C90A02B3FE61D286D5C87F425E6BDD81B", 16), ) @@ -1742,8 +2035,8 @@ def test_2(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha1(b("sample")).digest(), - hash_func=sha1, + hsh=hashlib.sha1(b"sample").digest(), + hash_func=hashlib.sha1, expected=int( "37D7CA00D2C7B0E5E412AC03BD44BA837FDD5B28CD3B0021", 16 ), @@ -1753,8 +2046,8 @@ def test_3(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha256(b("sample")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"sample").digest(), + hash_func=hashlib.sha256, expected=int( "32B1B6D7D42A05CB449065727A84804FB1A3E34D8F261496", 16 ), @@ -1764,8 +2057,8 @@ def test_4(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha512(b("sample")).digest(), - hash_func=sha512, + hsh=hashlib.sha512(b"sample").digest(), + hash_func=hashlib.sha512, expected=int( "A2AC7AB055E4F20692D49209544C203A7D1F2C0BFBC75DB1", 16 ), @@ -1775,8 +2068,8 @@ def test_5(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha1(b("test")).digest(), - hash_func=sha1, + hsh=hashlib.sha1(b"test").digest(), + hash_func=hashlib.sha1, expected=int( "D9CF9C3D3297D3260773A1DA7418DB5537AB8DD93DE7FA25", 16 ), @@ -1786,8 +2079,8 @@ def test_6(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha256(b("test")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"test").digest(), + hash_func=hashlib.sha256, expected=int( "5C4CE89CF56D9E7C77C8585339B006B97B5F0680B4306C6C", 16 ), @@ -1797,8 +2090,8 @@ def test_7(self): self._do( generator=NIST192p.generator, secexp=int("6FAB034934E4C0FC9AE67F5B5659A9D7D1FEFD187EE09FD4", 16), - hsh=sha512(b("test")).digest(), - hash_func=sha512, + hsh=hashlib.sha512(b"test").digest(), + hash_func=hashlib.sha512, expected=int( "0758753A5254759C7CFBAD2E2D9B0792EEE44136C9480527", 16 ), @@ -1811,8 +2104,8 @@ def test_8(self): "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538", 16, ), - hsh=sha1(b("sample")).digest(), - hash_func=sha1, + hsh=hashlib.sha1(b"sample").digest(), + hash_func=hashlib.sha1, expected=int( "089C071B419E1C2820962321787258469511958E80582E95D8378E0C2CCDB3CB42BEDE42F50E3FA3C71F5A76724281D31D9C89F0F91FC1BE4918DB1C03A5838D0F9", 16, @@ -1826,8 +2119,8 @@ def test_9(self): "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538", 16, ), - hsh=sha256(b("sample")).digest(), - hash_func=sha256, + hsh=hashlib.sha256(b"sample").digest(), + hash_func=hashlib.sha256, expected=int( "0EDF38AFCAAECAB4383358B34D67C9F2216C8382AAEA44A3DAD5FDC9C32575761793FEF24EB0FC276DFC4F6E3EC476752F043CF01415387470BCBD8678ED2C7E1A0", 16, @@ -1841,8 +2134,8 @@ def test_10(self): "0FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538", 16, ), - hsh=sha512(b("test")).digest(), - hash_func=sha512, + hsh=hashlib.sha512(b"test").digest(), + hash_func=hashlib.sha512, expected=int( "16200813020EC986863BEDFC1B121F605C1215645018AEA1A7B215A564DE9EB1B38A67AA1128B80CE391C4FB71187654AAA3431027BFC7F395766CA988C964DC56D", 16, @@ -1945,6 +2238,7 @@ def test_brainpoolP256r1(self): ), ) + @pytest.mark.slow def test_brainpoolP384r1(self): self._do( curve=curve_brainpoolp384r1, @@ -1991,6 +2285,7 @@ def test_brainpoolP384r1(self): ), ) + @pytest.mark.slow def test_brainpoolP512r1(self): self._do( curve=curve_brainpoolp512r1, @@ -2095,6 +2390,7 @@ def test_brainpoolP256r1(self): ), ) + @pytest.mark.slow def test_brainpoolP384r1(self): self._do( curve=curve_brainpoolp384r1, @@ -2141,6 +2437,7 @@ def test_brainpoolP384r1(self): ), ) + @pytest.mark.slow def test_brainpoolP512r1(self): self._do( curve=curve_brainpoolp512r1, @@ -2206,7 +2503,7 @@ def test_brainpoolP512r1(self): "6FC98BD7E50211A4A27102FA3549DF79EBCB4BF246B80945CDDFE7D509BBFD7D", "9E56F509196784D963D1C0A401510EE7ADA3DCC5DEE04B154BF61AF1D5A6DECE", b"abc", - sha256, + hashlib.sha256, "CB28E0999B9C7715FD0A80D8E47A77079716CBBF917DD72E97566EA1C066957C", "86FA3BB4E26CAD5BF90B7F81899256CE7594BB1EA0C89212748BFF3B3D5B0315", NIST256p, @@ -2222,7 +2519,7 @@ def test_brainpoolP512r1(self): "B4B74E44D71A13D568003D7489908D564C7761E229C58CBFA18950096EB7463B" "854D7FA992F934D927376285E63414FA", b"abc", - sha384, + hashlib.sha384, "FB017B914E29149432D8BAC29A514640B46F53DDAB2C69948084E2930F1C8F7E" "08E07C9C63F2D21A07DCB56A6AF56EB3", "B263A1305E057F984D38726A1B46874109F417BCA112674C528262A40A629AF1" @@ -2244,7 +2541,7 @@ def test_brainpoolP512r1(self): "373778F9DE6B6497B1EF825FF24F42F9B4A4BD7382CFC3378A540B1B7F0C1B95" "6C2F", b"abc", - sha512, + hashlib.sha512, "0154FD3836AF92D0DCA57DD5341D3053988534FDE8318FC6AAAAB68E2E6F4339" "B19F2F281A7E0B22C269D93CF8794A9278880ED7DBB8D9362CAEACEE54432055" "2251", diff --git a/src/ecdsa/test_rw_lock.py b/src/ecdsa/test_rw_lock.py deleted file mode 100644 index 0a84b9c7..00000000 --- a/src/ecdsa/test_rw_lock.py +++ /dev/null @@ -1,180 +0,0 @@ -# Copyright Mateusz Kobos, (c) 2011 -# https://code.activestate.com/recipes/577803-reader-writer-lock-with-priority-for-writers/ -# released under the MIT licence - -try: - import unittest2 as unittest -except ImportError: - import unittest -import threading -import time -import copy -from ._rwlock import RWLock - - -class Writer(threading.Thread): - def __init__( - self, buffer_, rw_lock, init_sleep_time, sleep_time, to_write - ): - """ - @param buffer_: common buffer_ shared by the readers and writers - @type buffer_: list - @type rw_lock: L{RWLock} - @param init_sleep_time: sleep time before doing any action - @type init_sleep_time: C{float} - @param sleep_time: sleep time while in critical section - @type sleep_time: C{float} - @param to_write: data that will be appended to the buffer - """ - threading.Thread.__init__(self) - self.__buffer = buffer_ - self.__rw_lock = rw_lock - self.__init_sleep_time = init_sleep_time - self.__sleep_time = sleep_time - self.__to_write = to_write - self.entry_time = None - """Time of entry to the critical section""" - self.exit_time = None - """Time of exit from the critical section""" - - def run(self): - time.sleep(self.__init_sleep_time) - self.__rw_lock.writer_acquire() - self.entry_time = time.time() - time.sleep(self.__sleep_time) - self.__buffer.append(self.__to_write) - self.exit_time = time.time() - self.__rw_lock.writer_release() - - -class Reader(threading.Thread): - def __init__(self, buffer_, rw_lock, init_sleep_time, sleep_time): - """ - @param buffer_: common buffer shared by the readers and writers - @type buffer_: list - @type rw_lock: L{RWLock} - @param init_sleep_time: sleep time before doing any action - @type init_sleep_time: C{float} - @param sleep_time: sleep time while in critical section - @type sleep_time: C{float} - """ - threading.Thread.__init__(self) - self.__buffer = buffer_ - self.__rw_lock = rw_lock - self.__init_sleep_time = init_sleep_time - self.__sleep_time = sleep_time - self.buffer_read = None - """a copy of a the buffer read while in critical section""" - self.entry_time = None - """Time of entry to the critical section""" - self.exit_time = None - """Time of exit from the critical section""" - - def run(self): - time.sleep(self.__init_sleep_time) - self.__rw_lock.reader_acquire() - self.entry_time = time.time() - time.sleep(self.__sleep_time) - self.buffer_read = copy.deepcopy(self.__buffer) - self.exit_time = time.time() - self.__rw_lock.reader_release() - - -class RWLockTestCase(unittest.TestCase): - def test_readers_nonexclusive_access(self): - (buffer_, rw_lock, threads) = self.__init_variables() - - threads.append(Reader(buffer_, rw_lock, 0, 0)) - threads.append(Writer(buffer_, rw_lock, 0.2, 0.4, 1)) - threads.append(Reader(buffer_, rw_lock, 0.3, 0.3)) - threads.append(Reader(buffer_, rw_lock, 0.5, 0)) - - self.__start_and_join_threads(threads) - - ## The third reader should enter after the second one but it should - ## exit before the second one exits - ## (i.e. the readers should be in the critical section - ## at the same time) - - self.assertEqual([], threads[0].buffer_read) - self.assertEqual([1], threads[2].buffer_read) - self.assertEqual([1], threads[3].buffer_read) - self.assertTrue(threads[1].exit_time <= threads[2].entry_time) - self.assertTrue(threads[2].entry_time <= threads[3].entry_time) - self.assertTrue(threads[3].exit_time < threads[2].exit_time) - - def test_writers_exclusive_access(self): - (buffer_, rw_lock, threads) = self.__init_variables() - - threads.append(Writer(buffer_, rw_lock, 0, 0.4, 1)) - threads.append(Writer(buffer_, rw_lock, 0.1, 0, 2)) - threads.append(Reader(buffer_, rw_lock, 0.2, 0)) - - self.__start_and_join_threads(threads) - - ## The second writer should wait for the first one to exit - - self.assertEqual([1, 2], threads[2].buffer_read) - self.assertTrue(threads[0].exit_time <= threads[1].entry_time) - self.assertTrue(threads[1].exit_time <= threads[2].exit_time) - - def test_writer_priority(self): - (buffer_, rw_lock, threads) = self.__init_variables() - - threads.append(Writer(buffer_, rw_lock, 0, 0, 1)) - threads.append(Reader(buffer_, rw_lock, 0.1, 0.4)) - threads.append(Writer(buffer_, rw_lock, 0.2, 0, 2)) - threads.append(Reader(buffer_, rw_lock, 0.3, 0)) - threads.append(Reader(buffer_, rw_lock, 0.3, 0)) - - self.__start_and_join_threads(threads) - - ## The second writer should go before the second and the third reader - - self.assertEqual([1], threads[1].buffer_read) - self.assertEqual([1, 2], threads[3].buffer_read) - self.assertEqual([1, 2], threads[4].buffer_read) - self.assertTrue(threads[0].exit_time < threads[1].entry_time) - self.assertTrue(threads[1].exit_time <= threads[2].entry_time) - self.assertTrue(threads[2].exit_time <= threads[3].entry_time) - self.assertTrue(threads[2].exit_time <= threads[4].entry_time) - - def test_many_writers_priority(self): - (buffer_, rw_lock, threads) = self.__init_variables() - - threads.append(Writer(buffer_, rw_lock, 0, 0, 1)) - threads.append(Reader(buffer_, rw_lock, 0.1, 0.6)) - threads.append(Writer(buffer_, rw_lock, 0.2, 0.1, 2)) - threads.append(Reader(buffer_, rw_lock, 0.3, 0)) - threads.append(Reader(buffer_, rw_lock, 0.4, 0)) - threads.append(Writer(buffer_, rw_lock, 0.5, 0.1, 3)) - - self.__start_and_join_threads(threads) - - ## The two last writers should go first -- after the first reader and - ## before the second and the third reader - - self.assertEqual([1], threads[1].buffer_read) - self.assertEqual([1, 2, 3], threads[3].buffer_read) - self.assertEqual([1, 2, 3], threads[4].buffer_read) - self.assertTrue(threads[0].exit_time < threads[1].entry_time) - self.assertTrue(threads[1].exit_time <= threads[2].entry_time) - self.assertTrue(threads[1].exit_time <= threads[5].entry_time) - self.assertTrue(threads[2].exit_time <= threads[3].entry_time) - self.assertTrue(threads[2].exit_time <= threads[4].entry_time) - self.assertTrue(threads[5].exit_time <= threads[3].entry_time) - self.assertTrue(threads[5].exit_time <= threads[4].entry_time) - - @staticmethod - def __init_variables(): - buffer_ = [] - rw_lock = RWLock() - threads = [] - return (buffer_, rw_lock, threads) - - @staticmethod - def __start_and_join_threads(threads): - for t in threads: - t.start() - for t in threads: - t.join() diff --git a/src/ecdsa/test_sha3.py b/src/ecdsa/test_sha3.py index 2c6bd15c..d30381d7 100644 --- a/src/ecdsa/test_sha3.py +++ b/src/ecdsa/test_sha3.py @@ -8,7 +8,7 @@ from gmpy2 import mpz GMPY = True -except ImportError: +except ImportError: # pragma: no cover try: from gmpy import mpz @@ -43,7 +43,7 @@ def test_int_to_bytes_wrong_endian(self): int_to_bytes(0, byteorder="middle") -@pytest.mark.skipif(GMPY == False, reason="requites gmpy or gmpy2") +@pytest.mark.skipif(GMPY == False, reason="requires gmpy or gmpy2") def test_int_to_bytes_with_gmpy(): assert int_to_bytes(mpz(1)) == b"\x01" diff --git a/src/ecdsa/util.py b/src/ecdsa/util.py index 9a561105..1aff5bf5 100644 --- a/src/ecdsa/util.py +++ b/src/ecdsa/util.py @@ -1,3 +1,16 @@ +""" +This module includes some utility functions. + +The methods most typically used are the sigencode and sigdecode functions +to be used with :func:`~ecdsa.keys.SigningKey.sign` and +:func:`~ecdsa.keys.VerifyingKey.verify` +respectively. See the :func:`sigencode_strings`, :func:`sigdecode_string`, +:func:`sigencode_der`, :func:`sigencode_strings_canonize`, +:func:`sigencode_string_canonize`, :func:`sigencode_der_canonize`, +:func:`sigdecode_strings`, :func:`sigdecode_string`, and +:func:`sigdecode_der` functions. +""" + from __future__ import division import os @@ -5,10 +18,11 @@ import binascii import sys from hashlib import sha256 -from six import PY2, int2byte, b, next +from six import PY2, int2byte, next from . import der from ._compat import normalise_bytes + # RFC5480: # The "unrestricted" algorithm identifier is: # id-ecPublicKey OBJECT IDENTIFIER ::= { @@ -188,7 +202,7 @@ def randrange_from_seed__trytryagain(seed, order): bits, bytes, extrabits = bits_and_bytes(order) generate = PRNG(seed) while True: - extrabyte = b("") + extrabyte = b"" if extrabits: extrabyte = int2byte(ord(generate(1)) & lsb_of_ones(extrabits)) guess = string_to_number(extrabyte + generate(bytes)) + 1 @@ -221,12 +235,24 @@ def string_to_number_fixedlen(string, order): return int(binascii.hexlify(string), 16) -# these methods are useful for the sigencode= argument to SK.sign() and the -# sigdecode= argument to VK.verify(), and control how the signature is packed -# or unpacked. +def sigencode_strings(r, s, order): + """ + Encode the signature to a pair of strings in a tuple + Encodes signature into raw encoding (:term:`raw encoding`) with the + ``r`` and ``s`` parts of the signature encoded separately. -def sigencode_strings(r, s, order): + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + + :return: raw encoding of ECDSA signature + :rtype: tuple(bytes, bytes) + """ r_str = number_to_string(r, order) s_str = number_to_string(s, order) return (r_str, s_str) @@ -236,7 +262,7 @@ def sigencode_string(r, s, order): """ Encode the signature to raw format (:term:`raw encoding`) - It's expected that this function will be used as a `sigencode=` parameter + It's expected that this function will be used as a ``sigencode=`` parameter in :func:`ecdsa.keys.SigningKey.sign` method. :param int r: first parameter of the signature @@ -264,7 +290,7 @@ def sigencode_der(r, s, order): s INTEGER } - It's expected that this function will be used as a `sigencode=` parameter + It's expected that this function will be used as a ``sigencode=`` parameter in :func:`ecdsa.keys.SigningKey.sign` method. :param int r: first parameter of the signature @@ -278,25 +304,99 @@ def sigencode_der(r, s, order): return der.encode_sequence(der.encode_integer(r), der.encode_integer(s)) -# canonical versions of sigencode methods -# these enforce low S values, by negating the value (modulo the order) if -# above order/2 see CECKey::Sign() -# https://github.com/bitcoin/bitcoin/blob/master/src/key.cpp#L214 -def sigencode_strings_canonize(r, s, order): - if s > order / 2: +def _canonize(s, order): + """ + Internal function for ensuring that the ``s`` value of a signature is in + the "canonical" format. + + :param int s: the second parameter of ECDSA signature + :param int order: the order of the curve over which the signatures was + computed + + :return: canonical value of s + :rtype: int + """ + if s > order // 2: s = order - s + return s + + +def sigencode_strings_canonize(r, s, order): + """ + Encode the signature to a pair of strings in a tuple + + Encodes signature into raw encoding (:term:`raw encoding`) with the + ``r`` and ``s`` parts of the signature encoded separately. + + Makes sure that the signature is encoded in the canonical format, where + the ``s`` parameter is always smaller than ``order / 2``. + Most commonly used in bitcoin. + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + + :return: raw encoding of ECDSA signature + :rtype: tuple(bytes, bytes) + """ + s = _canonize(s, order) return sigencode_strings(r, s, order) def sigencode_string_canonize(r, s, order): - if s > order / 2: - s = order - s + """ + Encode the signature to raw format (:term:`raw encoding`) + + Makes sure that the signature is encoded in the canonical format, where + the ``s`` parameter is always smaller than ``order / 2``. + Most commonly used in bitcoin. + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + + :return: raw encoding of ECDSA signature + :rtype: bytes + """ + s = _canonize(s, order) return sigencode_string(r, s, order) def sigencode_der_canonize(r, s, order): - if s > order / 2: - s = order - s + """ + Encode the signature into the ECDSA-Sig-Value structure using :term:`DER`. + + Makes sure that the signature is encoded in the canonical format, where + the ``s`` parameter is always smaller than ``order / 2``. + Most commonly used in bitcoin. + + Encodes the signature to the following :term:`ASN.1` structure:: + + Ecdsa-Sig-Value ::= SEQUENCE { + r INTEGER, + s INTEGER + } + + It's expected that this function will be used as a ``sigencode=`` parameter + in :func:`ecdsa.keys.SigningKey.sign` method. + + :param int r: first parameter of the signature + :param int s: second parameter of the signature + :param int order: the order of the curve over which the signature was + computed + + :return: DER encoding of ECDSA signature + :rtype: bytes + """ + s = _canonize(s, order) return sigencode_der(r, s, order) @@ -321,7 +421,7 @@ def sigdecode_string(signature, order): the signature, with each encoded using the same amount of bytes depending on curve size/order. - It's expected that this function will be used as the `sigdecode=` + It's expected that this function will be used as the ``sigdecode=`` parameter to the :func:`ecdsa.keys.VerifyingKey.verify` method. :param signature: encoded signature @@ -331,7 +431,7 @@ def sigdecode_string(signature, order): :raises MalformedSignature: when the encoding of the signature is invalid - :return: tuple with decoded 'r' and 's' values of signature + :return: tuple with decoded ``r`` and ``s`` values of signature :rtype: tuple of ints """ signature = normalise_bytes(signature) @@ -350,10 +450,10 @@ def sigdecode_strings(rs_strings, order): """ Decode the signature from two strings. - First string needs to be a big endian encoding of 'r', second needs to - be a big endian encoding of the 's' parameter of an ECDSA signature. + First string needs to be a big endian encoding of ``r``, second needs to + be a big endian encoding of the ``s`` parameter of an ECDSA signature. - It's expected that this function will be used as the `sigdecode=` + It's expected that this function will be used as the ``sigdecode=`` parameter to the :func:`ecdsa.keys.VerifyingKey.verify` method. :param list rs_strings: list of two bytes-like objects, each encoding one @@ -362,7 +462,7 @@ def sigdecode_strings(rs_strings, order): :raises MalformedSignature: when the encoding of the signature is invalid - :return: tuple with decoded 'r' and 's' values of signature + :return: tuple with decoded ``r`` and ``s`` values of signature :rtype: tuple of ints """ if not len(rs_strings) == 2: @@ -404,7 +504,7 @@ def sigdecode_der(sig_der, order): s INTEGER } - It's expected that this function will be used as as the `sigdecode=` + It's expected that this function will be used as as the ``sigdecode=`` parameter to the :func:`ecdsa.keys.VerifyingKey.verify` method. :param sig_der: encoded signature @@ -414,7 +514,7 @@ def sigdecode_der(sig_der, order): :raises UnexpectedDER: when the encoding of signature is invalid - :return: tuple with decoded 'r' and 's' values of signature + :return: tuple with decoded ``r`` and ``s`` values of signature :rtype: tuple of ints """ sig_der = normalise_bytes(sig_der) diff --git a/tox.ini b/tox.ini index 46864866..19c03175 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,17 @@ [tox] -envlist = py26, py27, py33, py34, py35, py36, py37, py38, py39, py310, py311, py, pypy, pypy3, gmpy2py27, gmpy2py39, gmpy2py310, gmpypy27, gmpypy39, gmpypy310, codechecks +envlist = py26, py27, py35, py36, py37, py38, py39, py310, py311, py312, py313, py, pypy, pypy3, gmpy2py27, gmpy2py39, gmpy2py310, gmpypy27, gmpypy39, gmpypy310, codechecks [testenv] deps = - py{33}: py<1.5 - py{33}: pytest<3.3 - py{33}: enum34 - py{33}: hypothesis<3.44 py{26}: unittest2 py{26}: hypothesis<3 - py{34}: attrs<21 - py{26,27,34,35,36,37,38,39,310,311,py,py3}: pytest - py{27,34,35,36,37,38,39,310,311,py,py3}: hypothesis - gmpy2py{27,39,310,311}: gmpy2 - gmpypy{27,39,310,311}: gmpy - gmpy{2py27,2py39,2py310,2py311,py27,py39,py310,py311}: pytest - gmpy{2py27,2py39,2py310,2py311,py27,py39,py310,py311}: hypothesis + py{26,27,35,36,37,38,39,310,311,312,313,py,py3}: pytest + py{27,35,36,37,38,39,310,311,312,313,py,py3}: hypothesis + gmpy2py{27,39,310,311,312,313}: gmpy2 + gmpypy{27,39,310,311,312,313}: gmpy + gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: pytest + gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: hypothesis # six==1.9.0 comes from setup.py install_requires py27_old_six: six==1.9.0 py27_old_six: pytest @@ -31,7 +26,6 @@ deps = py27_old_gmpy2: hypothesis py: pytest py: hypothesis - py{33}: wheel<0.30 coverage commands = coverage run --branch -m pytest {posargs:src/ecdsa} @@ -56,6 +50,12 @@ basepython=python3.10 [testenv:gmpypy311] basepython=python3.11 +[testenv:gmpypy312] +basepython=python3.12 + +[testenv:gmpypy313] +basepython=python3.13 + [testenv:gmpy2py27] basepython=python2.7 @@ -68,6 +68,12 @@ basepython=python3.10 [testenv:gmpy2py311] basepython=python3.11 +[testenv:gmpy2py312] +basepython=python3.12 + +[testenv:gmpy2py313] +basepython=python3.13 + [testenv:instrumental] basepython = python2.7 deps = @@ -104,7 +110,7 @@ commands = {envpython} speed.py basepython = python3 deps = black==22.3.0 - flake8 + flake8==6.1.0 commands = flake8 setup.py speed.py src black --check --line-length 79 .