├── .coveragerc ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .gitleaks.toml ├── .readthedocs.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── NEWS ├── README.md ├── SECURITY.md ├── build-requirements-2.6.txt ├── build-requirements-2.7.txt ├── build-requirements.txt ├── conftest.py ├── cosmic-ray-12way.sh ├── cosmic-ray-12way.toml ├── cosmic-ray.sh ├── cosmic-ray.toml ├── diff-instrumental.py ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── basics.rst │ ├── conf.py │ ├── ec_arithmetic.rst │ ├── ecdsa.curves.rst │ ├── ecdsa.der.rst │ ├── ecdsa.ecdh.rst │ ├── ecdsa.ecdsa.rst │ ├── ecdsa.eddsa.rst │ ├── ecdsa.ellipticcurve.rst │ ├── ecdsa.errors.rst │ ├── ecdsa.keys.rst │ ├── ecdsa.numbertheory.rst │ ├── ecdsa.rfc6979.rst │ ├── ecdsa.rst │ ├── ecdsa.util.rst │ ├── glossary.rst │ ├── index.rst │ ├── modules.rst │ └── quickstart.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── speed.py ├── sql ├── combine.sql ├── create_to_del.sql └── shard-db.sql ├── src └── ecdsa │ ├── __init__.py │ ├── _compat.py │ ├── _sha3.py │ ├── _version.py │ ├── curves.py │ ├── der.py │ ├── ecdh.py │ ├── ecdsa.py │ ├── eddsa.py │ ├── ellipticcurve.py │ ├── errors.py │ ├── keys.py │ ├── numbertheory.py │ ├── rfc6979.py │ ├── ssh.py │ ├── test_curves.py │ ├── test_der.py │ ├── test_ecdh.py │ ├── test_ecdsa.py │ ├── test_eddsa.py │ ├── test_ellipticcurve.py │ ├── test_jacobi.py │ ├── test_keys.py │ ├── test_malformed_sigs.py │ ├── test_numbertheory.py │ ├── test_pyecdsa.py │ ├── test_sha3.py │ └── util.py ├── tox.ini └── versioneer.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # -*- conf -*- 2 | 3 | [run] 4 | include = 5 | src/ecdsa/* 6 | omit = 7 | src/ecdsa/_version.py 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/ecdsa/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "59 21 * * 0" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ python ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | MANIFEST 3 | htmlcov 4 | 5 | # C extensions 6 | *.so 7 | *.dylib 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | bin 17 | var 18 | sdist 19 | develop-eggs 20 | .installed.cfg 21 | lib 22 | lib64 23 | __pycache__ 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Other logs 29 | *.log 30 | 31 | # Unit test / coverage reports 32 | .coverage 33 | coverage-html 34 | .tox 35 | nosetests.xml 36 | t/ 37 | .hypothesis/ 38 | 39 | # Translations 40 | *.mo 41 | 42 | # Mr Developer 43 | .mr.developer.cfg 44 | .project 45 | .pydevproject 46 | 47 | #vscode 48 | .vscode 49 | 50 | # Backup files 51 | *.swp 52 | *~ 53 | .idea 54 | .cache 55 | -------------------------------------------------------------------------------- /.gitleaks.toml: -------------------------------------------------------------------------------- 1 | [allowlist] 2 | description = "Ignore private keys in test files" 3 | files = [ '''test_.*''' ] 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/source/conf.py 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | # formats: 20 | # - pdf 21 | 22 | # Optionally declare the Python requirements required to build your docs 23 | python: 24 | install: 25 | - requirements: requirements.txt 26 | - requirements: docs/requirements.txt 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # workaround for 3.7 not available in default configuration 2 | # travis-ci/travis-ci#9815 3 | dist: trusty 4 | sudo: false 5 | language: python 6 | cache: pip 7 | addons: 8 | apt_packages: 9 | # needed for gmpy and gmpy2 10 | - libgmp-dev 11 | - libmpfr-dev 12 | - libmpc-dev 13 | before_cache: 14 | - rm -f $HOME/.cache/pip/log/debug.log 15 | # place the slowest (instrumental, mutation and py2.6) first 16 | matrix: 17 | include: 18 | - python: 3.9 19 | dist: bionic 20 | sudo: true 21 | env: MUTATION=yes 22 | - python: 2.7 23 | env: INSTRUMENTAL=yes 24 | dist: bionic 25 | sudo: true 26 | - python: 2.6 27 | env: TOX_ENV=py26 28 | - python: 2.7 29 | env: TOX_ENV=py27 30 | - python: 2.7 31 | env: TOX_ENV=py27_old_gmpy 32 | - python: 2.7 33 | env: TOX_ENV=py27_old_gmpy2 34 | - python: 2.7 35 | env: TOX_ENV=py27_old_six 36 | - python: 2.7 37 | env: TOX_ENV=gmpypy27 38 | - python: 2.7 39 | env: TOX_ENV=gmpy2py27 40 | - python: 3.3 41 | env: TOX_ENV=py33 42 | - python: 3.4 43 | env: TOX_ENV=py34 44 | - python: 3.5 45 | env: TOX_ENV=py35 46 | - python: 3.6 47 | env: TOX_ENV=py36 48 | - python: 3.7 49 | env: TOX_ENV=py37 50 | dist: bionic 51 | sudo: true 52 | - python: 3.8 53 | env: TOX_ENV=py38 54 | dist: bionic 55 | sudo: true 56 | - python: 3.9 57 | env: TOX_ENV=codechecks 58 | dist: bionic 59 | sudo: true 60 | - python: 3.9 61 | env: TOX_ENV=py39 62 | dist: bionic 63 | sudo: true 64 | - python: 3.9 65 | env: TOX_ENV=gmpypy39 66 | dist: bionic 67 | sudo: true 68 | - python: 3.9 69 | env: TOX_ENV=gmpy2py39 70 | dist: bionic 71 | sudo: true 72 | - python: nightly 73 | env: TOX_ENV=py 74 | dist: bionic 75 | sudo: true 76 | - python: pypy 77 | env: TOX_ENV=pypy 78 | - python: pypy3 79 | env: TOX_ENV=pypy3 80 | # We use explicit version as the PATH needs major-minor part 81 | - name: "Python3.8.0 on Windows" 82 | os: windows 83 | language: shell 84 | before_install: 85 | - choco install python --version 3.8.0 86 | - python -m pip install --upgrade pip 87 | env: PATH=/c/Python38:/c/Python38/Scripts:$PATH 88 | install: 89 | - pip list 90 | - pip install six 91 | - pip install -r build-requirements.txt 92 | - pip list 93 | script: 94 | - coverage run --branch -m pytest src/ecdsa 95 | after_success: 96 | - coveralls 97 | 98 | allow_failures: 99 | - python: nightly 100 | 101 | # for instrumental we're checking if the coverage changed from base branch 102 | # so collect that info 103 | before_install: 104 | - | 105 | echo -e "TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST\n" \ 106 | "TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG\n" \ 107 | "TRAVIS_PULL_REQUEST=$TRAVIS_PULL_REQUEST\n" \ 108 | "TRAVIS_COMMIT=$TRAVIS_COMMIT\n" \ 109 | "TRAVIS_PYTHON_VERSION=$TRAVIS_PYTHON_VERSION" 110 | - | 111 | # workaround https://github.com/travis-ci/travis-ci/issues/2666 112 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 113 | URL="https://github.com/${TRAVIS_REPO_SLUG}/pull/${TRAVIS_PULL_REQUEST}.patch" 114 | # `--location` makes curl follow redirects 115 | PR_FIRST=$(curl --silent --show-error --location $URL | head -1 | grep -o -E '\b[0-9a-f]{40}\b' | tr -d '\n') 116 | TRAVIS_COMMIT_RANGE=$PR_FIRST^..$TRAVIS_COMMIT 117 | fi 118 | # sanity check current commit 119 | - BRANCH=$(git rev-parse HEAD) 120 | - echo "TRAVIS_COMMIT_RANGE=$TRAVIS_COMMIT_RANGE" 121 | - git fetch origin master:refs/remotes/origin/master 122 | 123 | 124 | install: 125 | - pip list 126 | - | 127 | if [[ -e build-requirements-${TRAVIS_PYTHON_VERSION}.txt ]]; then 128 | travis_retry pip install -r build-requirements-${TRAVIS_PYTHON_VERSION}.txt; 129 | else 130 | travis_retry pip install -r build-requirements.txt; 131 | fi 132 | - if [[ $TOX_ENV =~ gmpy2 ]] || [[ $INSTRUMENTAL ]] || [[ $MUTATION ]]; then travis_retry pip install gmpy2; fi 133 | - if [[ $TOX_ENV =~ gmpyp ]]; then travis_retry pip install gmpy; fi 134 | - if [[ $INSTRUMENTAL ]]; then travis_retry pip install instrumental; fi 135 | - if [[ $MUTATION ]]; then travis_retry pip install cosmic-ray; fi 136 | - pip list 137 | script: 138 | - if [[ $TOX_ENV ]]; then tox -e $TOX_ENV; fi 139 | - if [[ $TOX_ENV =~ gmpy2 ]] && [[ -z $MUTATION ]]; then tox -e speedgmpy2; fi 140 | - if [[ $TOX_ENV =~ gmpyp ]] && [[ -z $MUTATION ]]; then tox -e speedgmpy; fi 141 | - if ! [[ $TOX_ENV =~ gmpy ]] && [[ -z $MUTATION ]]; then tox -e speed; fi 142 | - | 143 | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then 144 | git checkout $PR_FIRST^ 145 | instrumental -t ecdsa -i 'test.*|.*_version|.*_compat' `which pytest` src/ecdsa/test*.py 146 | instrumental -f .instrumental.cov -s 147 | instrumental -f .instrumental.cov -s | python diff-instrumental.py --save .diff-instrumental 148 | git checkout $BRANCH 149 | instrumental -t ecdsa -i 'test.*|.*_version|.*_compat' `which pytest` src/ecdsa/test*.py 150 | instrumental -f .instrumental.cov -sr 151 | fi 152 | - | 153 | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST == "false" ]]; then 154 | instrumental -t ecdsa -i 'test.*|.*_version|.*_compat' `which pytest` src/ecdsa 155 | instrumental -f .instrumental.cov -s 156 | # just log the values when merging 157 | instrumental -f .instrumental.cov -s | python diff-instrumental.py 158 | fi 159 | - | 160 | if [[ $INSTRUMENTAL && $TRAVIS_PULL_REQUEST != "false" ]]; then 161 | instrumental -f .instrumental.cov -s | python diff-instrumental.py --read .diff-instrumental --fail-under 70 --max-difference -0.1 162 | fi 163 | # cosmic-ray (mutation testing) runs 164 | - if [[ $MUTATION ]]; then cosmic-ray init cosmic-ray.toml session.sqlite; fi 165 | - if [[ $MUTATION ]]; then cosmic-ray baseline --report session.sqlite; fi 166 | - if [[ $MUTATION ]]; then cr-report --show-output session.baseline.sqlite; fi 167 | - | 168 | if [[ $MUTATION ]]; then 169 | cosmic-ray exec session.sqlite & 170 | COSMIC_PID=$! 171 | # make sure that we output something every 5 minutes (otherwise travis will kill us) 172 | while kill -s 0 $COSMIC_PID; do 173 | sleep 300 174 | cr-report session.sqlite | tail -n 3; 175 | done & 176 | REPORT_PID=$! 177 | # kill exec after 40 minutes 178 | (sleep $((60*40)); kill $COSMIC_PID) & 179 | fi 180 | - if [[ $MUTATION ]]; then wait $COSMIC_PID ; kill $REPORT_PID ; true; fi 181 | - if [[ $MUTATION ]]; then cr-report --show-output session.sqlite | tail -n 40; fi 182 | - if [[ $MUTATION ]]; then cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite; fi 183 | after_success: 184 | - if [[ -z $INSTRUMENTAL && -z $MUTATION ]]; then coveralls; fi 185 | 186 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | "python-ecdsa" Copyright (c) 2010 Brian Warner 2 | 3 | Portions written in 2005 by Peter Pearson and placed in the public domain. 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # basic metadata 2 | include MANIFEST.in LICENSE NEWS README.md versioneer.py 3 | include src/ecdsa/_version.py 4 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest released version is supported. 6 | Alpha and beta releases are always unsupported with security fixes. 7 | 8 | The project uses semantic versioning, as such, minor version changes are API compatible. 9 | 10 | | Version | Supported | 11 | | -------- | ------------------ | 12 | | 0.18.x | :white_check_mark: | 13 | | < 0.18 | :x: | 14 | 15 | ## Support Scope 16 | 17 | This library was not designed with security in mind. If you are processing data that needs 18 | to be protected we suggest you use a quality wrapper around OpenSSL. 19 | [`pyca/cryptography`](https://cryptography.io/) is one example of such a wrapper. 20 | The primary use-case of this library is as a portable library for interoperability testing 21 | and as a teaching tool. 22 | 23 | **This library does not protect against side-channel attacks.** 24 | 25 | Do not allow attackers to measure how long it takes you to generate a key pair or sign a message. 26 | Do not allow attackers to run code on the same physical machine when key pair generation or 27 | signing is taking place (this includes virtual machines). 28 | Do not allow attackers to measure how much power your computer uses while generating the key pair 29 | or signing a message. Do not allow attackers to measure RF interference coming from your computer 30 | while generating a key pair or signing a message. Note: just loading the private key will cause 31 | key pair generation. Other operations or attack vectors may also be vulnerable to attacks. 32 | For a sophisticated attacker observing just one operation with a private key will be sufficient 33 | to completely reconstruct the private key. 34 | 35 | Fixes for side-channel vulerabilities will not be developed. 36 | 37 | Please also note that any Pure-python cryptographic library will be vulnerable to the same 38 | side-channel attacks. This is because Python does not provide side-channel secure primitives 39 | (with the exception of [`hmac.compare_digest()`](https://docs.python.org/3/library/hmac.html#hmac.compare_digest)), 40 | making side-channel secure programming impossible. 41 | 42 | This library depends upon a strong source of random numbers. Do not use it on a system 43 | where `os.urandom()` does not provide cryptographically secure random numbers. 44 | 45 | ## Reporting a Vulnerability 46 | 47 | 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. 48 | Alternatively, you can contact the project maintainer at hkario at redhat dot com. 49 | -------------------------------------------------------------------------------- /build-requirements-2.6.txt: -------------------------------------------------------------------------------- 1 | tox 2 | inflect<0.3.1 3 | pyopenssl<18 4 | cffi<1.14 5 | git+https://github.com/tomato42/coveralls-python.git@add-py26#egg=coveralls 6 | idna<2.8 7 | unittest2 8 | hypothesis<3 9 | coverage 10 | mock==2.0.0 11 | -------------------------------------------------------------------------------- /build-requirements-2.7.txt: -------------------------------------------------------------------------------- 1 | tox 2 | git+https://github.com/tomato42/coveralls-python.git@add-py26#egg=coveralls 3 | hypothesis 4 | pytest>=4.6.0 5 | coverage 6 | -------------------------------------------------------------------------------- /build-requirements.txt: -------------------------------------------------------------------------------- 1 | tox 2 | coveralls 3 | hypothesis 4 | pytest>=4.6.0 5 | coverage 6 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_addoption(parser): 2 | parser.addoption( 3 | "--fast", action="store_true", default=False, help="run tests fast" 4 | ) 5 | 6 | 7 | def pytest_configure(config): 8 | config.addinivalue_line( 9 | "markers", 10 | "slow: mark test as slow to run (deselect with '-m \"not slow\"')", 11 | ) 12 | -------------------------------------------------------------------------------- /cosmic-ray-12way.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cosmic-ray init cosmic-ray-12way.toml session.sqlite 5 | cosmic-ray baseline --session-file session.baseline.sqlite cosmic-ray-12way.toml 6 | cr-report --show-output session.baseline.sqlite 7 | # some mutations cause huge memory use, so put it in a cgroup 8 | # systemd-run --user --scope -p MemoryMax=8G -p MemoryHigh=8G cr-http-workers cosmic-ray-12way.toml . 9 | cr-http-workers cosmic-ray-12way.toml . 10 | cosmic-ray exec cosmic-ray-12way.toml session.sqlite 11 | cr-report session.sqlite 12 | cr-html session.sqlite > session.html 13 | cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite 14 | -------------------------------------------------------------------------------- /cosmic-ray-12way.toml: -------------------------------------------------------------------------------- 1 | [cosmic-ray] 2 | module-path = "src" 3 | timeout = 20.0 4 | excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*'] 5 | test-command = "pytest --timeout=30 -x --fast -m 'not slow' src/" 6 | 7 | [cosmic-ray.distributor] 8 | name = "http" 9 | 10 | [cosmic-ray.distributor.http] 11 | worker-urls = [ 12 | "http://localhost:9870", 13 | "http://localhost:9871", 14 | "http://localhost:9872", 15 | "http://localhost:9873", 16 | "http://localhost:9874", 17 | "http://localhost:9875", 18 | "http://localhost:9876", 19 | "http://localhost:9877", 20 | "http://localhost:9878", 21 | "http://localhost:9879", 22 | "http://localhost:9880", 23 | "http://localhost:9881", 24 | "http://localhost:9882" 25 | ] 26 | 27 | [cosmic-ray.filters.git-filter] 28 | branch = "master" 29 | -------------------------------------------------------------------------------- /cosmic-ray.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cosmic-ray init cosmic-ray.toml session.sqlite 5 | cosmic-ray baseline --session-file session.baseline.sqlite cosmic-ray.toml 6 | cr-report --show-output session.baseline.sqlite 7 | # some mutations cause huge memory use, so put it in a cgroup 8 | # systemd-run --user --scope -p MemoryMax=2G -p MemoryHigh=2G cosmic-ray exec cosmic-ray.toml session.sqlite 9 | cosmic-ray exec cosmic-ray.toml session.sqlite 10 | cr-report session.sqlite 11 | cr-html session.sqlite > session.html 12 | cr-rate --estimate --fail-over 29 --confidence 99.9 session.sqlite 13 | -------------------------------------------------------------------------------- /cosmic-ray.toml: -------------------------------------------------------------------------------- 1 | [cosmic-ray] 2 | module-path = "src" 3 | timeout = 20.0 4 | excluded-modules = ['src/ecdsa/_sha3.py', 'src/ecdsa/_version.py', 'src/ecdsa/test*'] 5 | test-command = "pytest --timeout 30 -x --fast -m 'not slow' src/" 6 | 7 | [cosmic-ray.distributor] 8 | name = "local" 9 | 10 | [cosmic-ray.filters.git-filter] 11 | branch = "master" 12 | -------------------------------------------------------------------------------- /diff-instrumental.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import sys 3 | import getopt 4 | 5 | fail_under = None 6 | max_difference = 0 7 | read_location = None 8 | save_location = None 9 | raw = False 10 | 11 | argv = sys.argv[1:] 12 | 13 | opts, args = getopt.getopt( 14 | argv, "s:r:", ["fail-under=", "max-difference=", "save=", "read=", "raw"] 15 | ) 16 | if args: 17 | raise ValueError("Unexpected parameters: {0}".format(args)) 18 | for opt, arg in opts: 19 | if opt == "-s" or opt == "--save": 20 | save_location = arg 21 | elif opt == "-r" or opt == "--read": 22 | read_location = arg 23 | elif opt == "--fail-under": 24 | fail_under = float(arg) / 100.0 25 | elif opt == "--max-difference": 26 | max_difference = float(arg) / 100.0 27 | elif opt == "--raw": 28 | raw = True 29 | else: 30 | raise ValueError("Unknown option: {0}".format(opt)) 31 | 32 | total_hits = 0 33 | total_count = 0 34 | 35 | for line in sys.stdin.readlines(): 36 | if not line.startswith("ecdsa"): 37 | continue 38 | 39 | fields = line.split() 40 | hit, count = fields[1].split("/") 41 | total_hits += int(hit) 42 | total_count += int(count) 43 | 44 | coverage = total_hits * 1.0 / total_count 45 | 46 | if read_location: 47 | with open(read_location, "r") as f: 48 | old_coverage = float(f.read()) 49 | print("Old coverage: {0:6.2f}%".format(old_coverage * 100)) 50 | 51 | if save_location: 52 | with open(save_location, "w") as f: 53 | f.write("{0:1.40f}".format(coverage)) 54 | 55 | if raw: 56 | print("{0:6.2f}".format(coverage * 100)) 57 | else: 58 | print("Coverage: {0:6.2f}%".format(coverage * 100)) 59 | 60 | if read_location: 61 | print("Difference: {0:6.2f}%".format((old_coverage - coverage) * 100)) 62 | 63 | if fail_under and coverage < fail_under: 64 | print("ERROR: Insufficient coverage.", file=sys.stderr) 65 | sys.exit(1) 66 | 67 | if read_location and coverage - old_coverage < max_difference: 68 | print("ERROR: Too big decrease in coverage", file=sys.stderr) 69 | sys.exit(1) 70 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme 2 | -------------------------------------------------------------------------------- /docs/source/basics.rst: -------------------------------------------------------------------------------- 1 | ====================== 2 | Basics of ECC handling 3 | ====================== 4 | 5 | The :term:`ECC`, as any asymmetric cryptography system, deals with private 6 | keys and public keys. Private keys are generally used to create signatures, 7 | and are kept, as the name suggest, private. That's because possession of a 8 | private key allows creating a signature that can be verified with a public key. 9 | If the public key is associated with an identity (like a person or an 10 | institution), possession of the private key will allow to impersonate 11 | that identity. 12 | 13 | The public keys on the other hand are widely distributed, and they don't 14 | have to be kept private. The primary purpose of them, is to allow 15 | checking if a given signature was made with the associated private key. 16 | 17 | Number representations 18 | ====================== 19 | 20 | On a more low level, the private key is a single number, usually the 21 | size of the curve size: a NIST P-256 private key will have a size of 256 bits, 22 | though as it needs to be selected randomly, it may be a slightly smaller 23 | number (255-bit, 248-bit, etc.). 24 | Public points are a pair of numbers. That pair specifies a point on an 25 | elliptic curve (a pair of integers that satisfy the curve equation). 26 | Those two numbers are similarly close in size to the curve size, so both the 27 | ``x`` and ``y`` coordinate of a NIST P-256 curve will also be around 256 bit in 28 | size. 29 | 30 | .. note:: 31 | To be more precise, the size of the private key is related to the 32 | curve *order*, i.e. the number of points on a curve. The coordinates 33 | of the curve depend on the *field* of the curve, which usually means the 34 | size of the *prime* used for operations on points. While the *order* and 35 | the *prime* size are related and fairly close in size, it's possible 36 | to have a curve where either of them is larger by a bit (i.e. 37 | it's possible to have a curve that uses a 256 bit *prime* that has a 257 bit 38 | *order*). 39 | 40 | Since normally computers work with much smaller numbers, like 32 bit or 64 bit, 41 | we need to use special approaches to represent numbers that are hundreds of 42 | bits large. 43 | 44 | First is to decide if the numbers should be stored in a big 45 | endian format, or in little endian format. In big endian, the most 46 | significant bits are stored first, so a number like :math:`2^{16}` is saved 47 | as a three bytes: byte with value 1 and two bytes with value 0. 48 | In little endian format the least significant bits are stored first, so 49 | the number like :math:`2^{16}` would be stored as three bytes: 50 | first two bytes with value 0, than a byte with value 1. 51 | 52 | For :term:`ECDSA` big endian encoding is usually used, for :term:`EdDSA` 53 | little endian encoding is usually used. 54 | 55 | Secondly, we need to decide if the numbers need to be stored as fixed length 56 | strings (zero padded if necessary), or if they should be stored with 57 | minimal number of bytes necessary. 58 | That depends on the format and place it's used, some require strict 59 | sizes (so even if the number encoded is 1, but the curve used is 128 bit large, 60 | that number 1 still needs to be encoded with 16 bytes, with fifteen most 61 | significant bytes equal zero). 62 | 63 | Public key encoding 64 | =================== 65 | 66 | Generally, public keys (i.e. points) are expressed as fixed size byte strings. 67 | 68 | While public keys can be saved as two integers, one to represent the 69 | ``x`` coordinate and one to represent ``y`` coordinate, that actually 70 | provides a lot of redundancy. Because of the specifics of elliptic curves, 71 | for every valid ``x`` value there are only two valid ``y`` values. 72 | Moreover, if you have an ``x`` value, you can compute those two possible 73 | ``y`` values (if they exist). 74 | As such, it's possible to save just the ``x`` coordinate and the sign 75 | of the ``y`` coordinate (as the two possible values are negatives of 76 | each-other: :math:`y_1 == -y_2`). 77 | 78 | That gives us few options to represent the public point, the most common are: 79 | 80 | 1. As a concatenation of two fixed-length big-endian integers, so called 81 | :term:`raw encoding`. 82 | 2. As a concatenation of two fixed-length big-endian integers prefixed with 83 | the type of the encoding, so called :term:`uncompressed` point 84 | representation (the type is represented by a 0x04 byte). 85 | 3. As a fixed-length big-endian integer representing the ``x`` coordinate 86 | prefixed with the byte representing the combined type of the encoding 87 | and the sign of the ``y`` coordinate, so called :term:`compressed` 88 | point representation (the type is then represented by a 0x02 or a 0x03 89 | byte). 90 | 91 | Interoperable file formats 92 | ========================== 93 | 94 | Now, while we can save the byte strings as-is and "remember" which curve 95 | was used to generate those private and public keys, interoperability usually 96 | requires to also save information about the curve together with the 97 | corresponding key. Here too there are many ways to do it: 98 | save the parameters of the used curve explicitly, use the name of the 99 | well-known curve as a string, use a numerical identifier of the well-known 100 | curve, etc. 101 | 102 | For public keys the most interoperable format is the one described 103 | in RFC5912 (look for SubjectPublicKeyInfo structure). 104 | For private keys, the RFC5915 format (also known as the ssleay format) 105 | and the PKCS#8 format (described in RFC5958) are the most popular. 106 | 107 | All three formats effectively support two ways of providing the information 108 | about the curve used: by specifying the curve parameters explicitly or 109 | by specifying the curve using ASN.1 OBJECT IDENTIFIER (OID), which is 110 | called ``named_curve``. ASN.1 OIDs are a hierarchical system of representing 111 | types of objects, for example, NIST P-256 curve is identified by the 112 | 1.2.840.10045.3.1.7 OID (in dotted-decimal formatting of the OID, also 113 | known by the ``prime256v1`` OID node name or short name). Those OIDs 114 | uniquely, identify a particular curve, but the receiver needs to know 115 | which numerical OID maps to which curve parameters. Thus the prospect of 116 | using the explicit encoding, where all the needed parameters are provided 117 | is tempting, the downside is that curve parameters may specify a *weak* 118 | curve, which is easy to attack and break (that is to deduce the private key 119 | from the public key). To verify curve parameters is complex and computationally 120 | expensive, thus generally protocols use few specific curves and require 121 | all implementations to carry the parameters of them. As such, use of 122 | ``named_curve`` parameters is generally recommended. 123 | 124 | All of the mentioned formats specify a binary encoding, called DER. That 125 | encoding uses bytes with all possible numerical values, which means it's not 126 | possible to embed it directly in text files. For uses where it's useful to 127 | limit bytes to printable characters, so that the keys can be embedded in text 128 | files or text-only protocols (like email), the PEM formatting of the 129 | DER-encoded data can be used. The PEM formatting is just a base64 encoding 130 | with appropriate header and footer. 131 | 132 | Signature formats 133 | ================= 134 | 135 | Finally, ECDSA signatures at the lowest level are a pair of numbers, usually 136 | called ``r`` and ``s``. While they are the ``x`` coordinates of special 137 | points on the curve, they are saved modulo *order* of the curve, not 138 | modulo *prime* of the curve (as a coordinate needs to be). 139 | 140 | That again means we have multiple ways of encoding those two numbers. 141 | The two most popular formats are to save them as a concatenation of big-endian 142 | integers of fixed size (determined by the curve *order*) or as a DER 143 | structure with two INTEGERS. 144 | The first of those is called the :term:``raw encoding`` inside the Python 145 | ecdsa library. 146 | 147 | As ASN.1 signature format requires the encoding of INTEGERS, and DER INTEGERs 148 | must use the fewest possible number of bytes, a numerically small value of 149 | ``r`` or ``s`` will require fewer 150 | bytes to represent in the DER structure. Thus, DER encoding isn't fixed 151 | size for a given curve, but has a maximum possible size. 152 | 153 | .. note:: 154 | 155 | As DER INTEGER uses so-called two's complement representation of 156 | numbers, the most significant bit of the most significant byte 157 | represents the *sign* of the number. If that bit is set, then the 158 | number is considered to be negative. Thus, to represent a number like 159 | 255, which in binary representation is 0b11111111 (i.e. a byte with all 160 | bits set high), the DER encoding of it will require two bytes, one 161 | zero byte to make sure the sign bit is 0, and a byte with value 255 to 162 | encode the numerical value of the integer. 163 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("../../src")) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "python-ecdsa" 22 | copyright = "2021, Brian Warner and Hubert Kario" 23 | author = "Brian Warner and Hubert Kario" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = "0.17.0" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.intersphinx", 37 | "sphinx.ext.coverage", 38 | "sphinx.ext.imgmath", 39 | "sphinx.ext.viewcode", 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ["_templates"] 44 | 45 | # List of patterns, relative to source directory, that match files and 46 | # directories to ignore when looking for source files. 47 | # This pattern also affects html_static_path and html_extra_path. 48 | exclude_patterns = [] 49 | 50 | todo_include_todos = False 51 | 52 | # -- Options for HTML output ------------------------------------------------- 53 | 54 | # The theme to use for HTML and HTML Help pages. See the documentation for 55 | # a list of builtin themes. 56 | # 57 | html_theme = "sphinx_rtd_theme" 58 | 59 | # Add any paths that contain custom static files (such as style sheets) here, 60 | # relative to this directory. They are copied after the builtin static files, 61 | # so a file named "default.css" will overwrite the builtin "default.css". 62 | html_static_path = ["_static"] 63 | 64 | # Example configuration for intersphinx: refer to the Python standard library. 65 | intersphinx_mapping = { 66 | "python": ("https://docs.python.org/", None), 67 | } 68 | 69 | autodoc_default_options = { 70 | "undoc-members": True, 71 | "inherited-members": True, 72 | } 73 | -------------------------------------------------------------------------------- /docs/source/ec_arithmetic.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | Elliptic Curve arithmetic 3 | ========================= 4 | 5 | The python-ecdsa also provides generic API for performing operations on 6 | elliptic curve points. 7 | 8 | .. warning:: 9 | 10 | This is documentation of a very low-level API, if you want to 11 | handle keys or signatures you should look at documentation of 12 | the :py:mod:`~ecdsa.keys` module. 13 | 14 | Short Weierstrass curves 15 | ======================== 16 | 17 | There are two low-level implementations for 18 | :term:`short Weierstrass curves `: 19 | :py:class:`~ecdsa.ellipticcurve.Point` and 20 | :py:class:`~ecdsa.ellipticcurve.PointJacobi`. 21 | 22 | Both of them use the curves specified using the 23 | :py:class:`~ecdsa.ellipticcurve.CurveFp` object. 24 | 25 | You can either provide your own curve parameters or use one of the predefined 26 | curves. 27 | For example, to define a curve :math:`y^2 = x^3 + 1 * x + 4 \text{ mod } 5` use 28 | code like this: 29 | 30 | .. code:: python 31 | 32 | from ecdsa.ellipticcurve import CurveFp 33 | custom_curve = CurveFp(5, 1, 4) 34 | 35 | The predefined curves are specified in the :py:mod:`~ecdsa.ecdsa` module, 36 | but it's much easier to use the helper functions (and proper names) 37 | from the :py:mod:`~ecdsa.curves` module. 38 | 39 | For example, to get the curve parameters for the NIST P-256 curve use this 40 | code: 41 | 42 | .. code:: python 43 | 44 | from ecdsa.curves import NIST256p 45 | curve = NIST256p.curve 46 | 47 | .. tip:: 48 | 49 | You can also use :py:class:`~ecdsa.curves.Curve` to get the curve 50 | parameters from a PEM or DER file. You can also use 51 | :py:func:`~ecdsa.curves.curve_by_name` to get a curve by specifying its 52 | name. 53 | Or use the 54 | :py:func:`~ecdsa.curves.find_curve` to get a curve by specifying its 55 | ASN.1 object identifier (OID). 56 | 57 | Affine coordinates 58 | ------------------ 59 | 60 | After taking hold of curve parameters you can create a point on the 61 | curve. The :py:class:`~ecdsa.ellipticcurve.Point` uses affine coordinates, 62 | i.e. the :math:`x` and :math:`y` from the curve equation directly. 63 | 64 | To specify a point (1, 1) on the ``custom_curve`` you can use this code: 65 | 66 | .. code:: python 67 | 68 | from ecdsa.ellipticcurve import Point 69 | point_a = Point(custom_curve, 1, 1) 70 | 71 | Then it's possible to either perform scalar multiplication: 72 | 73 | .. code:: python 74 | 75 | point_b = point_a * 3 76 | 77 | Or specify other points and perform addition: 78 | 79 | .. code:: python 80 | 81 | point_b = Point(custom_curve, 3, 2) 82 | point_c = point_a + point_b 83 | 84 | To get the affine coordinates of the point, call the ``x()`` and ``y()`` 85 | methods of the object: 86 | 87 | .. code:: python 88 | 89 | print("x: {0}, y: {1}".format(point_c.x(), point_c.y())) 90 | 91 | Projective coordinates 92 | ---------------------- 93 | 94 | When using the Jacobi coordinates, the point is defined by 3 integers, 95 | which are related to the :math:`x` and :math:`y` in the following way: 96 | 97 | .. math:: 98 | 99 | x = X/Z^2 \\ 100 | y = Y/Z^3 101 | 102 | That means that if you have point in affine coordinates, it's possible 103 | to convert them to Jacobi by simply assuming :math:`Z = 1`. 104 | 105 | So the same points can be specified as so: 106 | 107 | .. code:: python 108 | 109 | from ecdsa.ellipticcurve import PointJacobi 110 | point_a = PointJacobi(custom_curve, 1, 1, 1) 111 | point_b = PointJacobi(custom_curve, 3, 2, 1) 112 | 113 | 114 | .. note:: 115 | 116 | Unlike the :py:class:`~ecdsa.ellipticcurve.Point`, the 117 | :py:class:`~ecdsa.ellipticcurve.PointJacobi` does **not** check if the 118 | coordinates specify a valid point on the curve as that operation is 119 | computationally expensive for Jacobi coordinates. 120 | If you want to verify if they specify a valid 121 | point, you need to convert the point to affine coordinates and use the 122 | :py:meth:`~ecdsa.ellipticcurve.CurveFp.contains_point` method. 123 | 124 | Then all the operations work exactly the same as with regular 125 | :py:class:`~ecdsa.ellipticcurve.Point` implementation. 126 | While it's not possible to get the internal :math:`X`, :math:`Y`, and :math:`Z` 127 | coordinates, it's possible to get the affine projection just like with 128 | the regular implementation: 129 | 130 | .. code:: python 131 | 132 | point_c = point_a + point_b 133 | print("x: {0}, y: {1}".format(point_c.x(), point_c.y())) 134 | 135 | All the other operations, like scalar multiplication or point addition work 136 | on projective points the same as with affine representation, but they 137 | are much more effective computationally. 138 | -------------------------------------------------------------------------------- /docs/source/ecdsa.curves.rst: -------------------------------------------------------------------------------- 1 | ecdsa.curves module 2 | =================== 3 | 4 | .. automodule:: ecdsa.curves 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.der.rst: -------------------------------------------------------------------------------- 1 | ecdsa.der module 2 | ================ 3 | 4 | .. automodule:: ecdsa.der 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.ecdh.rst: -------------------------------------------------------------------------------- 1 | ecdsa.ecdh module 2 | ================= 3 | 4 | .. automodule:: ecdsa.ecdh 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.ecdsa.rst: -------------------------------------------------------------------------------- 1 | ecdsa.ecdsa module 2 | ================== 3 | 4 | .. automodule:: ecdsa.ecdsa 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.eddsa.rst: -------------------------------------------------------------------------------- 1 | ecdsa.eddsa module 2 | ================== 3 | 4 | .. automodule:: ecdsa.eddsa 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.ellipticcurve.rst: -------------------------------------------------------------------------------- 1 | ecdsa.ellipticcurve module 2 | ========================== 3 | 4 | .. automodule:: ecdsa.ellipticcurve 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.errors.rst: -------------------------------------------------------------------------------- 1 | ecdsa.errors module 2 | =================== 3 | 4 | .. automodule:: ecdsa.errors 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.keys.rst: -------------------------------------------------------------------------------- 1 | ecdsa.keys module 2 | ================= 3 | 4 | .. automodule:: ecdsa.keys 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.numbertheory.rst: -------------------------------------------------------------------------------- 1 | ecdsa.numbertheory module 2 | ========================= 3 | 4 | .. automodule:: ecdsa.numbertheory 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.rfc6979.rst: -------------------------------------------------------------------------------- 1 | ecdsa.rfc6979 module 2 | ==================== 3 | 4 | .. automodule:: ecdsa.rfc6979 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/ecdsa.rst: -------------------------------------------------------------------------------- 1 | ecdsa package 2 | ============= 3 | 4 | .. automodule:: ecdsa 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | Submodules 10 | ---------- 11 | 12 | .. toctree:: 13 | :maxdepth: 4 14 | 15 | ecdsa.curves 16 | ecdsa.der 17 | ecdsa.ecdh 18 | ecdsa.ecdsa 19 | ecdsa.eddsa 20 | ecdsa.ellipticcurve 21 | ecdsa.errors 22 | ecdsa.keys 23 | ecdsa.numbertheory 24 | ecdsa.rfc6979 25 | ecdsa.util 26 | -------------------------------------------------------------------------------- /docs/source/ecdsa.util.rst: -------------------------------------------------------------------------------- 1 | ecdsa.util module 2 | ================= 3 | 4 | .. automodule:: ecdsa.util 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/source/glossary.rst: -------------------------------------------------------------------------------- 1 | .. _glossary: 2 | 3 | Glossary 4 | ======== 5 | 6 | .. glossary:: 7 | :sorted: 8 | 9 | ECC 10 | Elliptic Curve Cryptography, a term for all the different ways of using 11 | elliptic curves in cryptography. Also combined term for :term:`ECDSA`, 12 | :term:`EdDSA`, :term:`ECDH`. 13 | 14 | ECDSA 15 | Elliptic Curve Digital Signature Algorithm 16 | 17 | EdDSA 18 | Edwards curve based Digital Signature Algorithm, the alternative 19 | digital signature algorithm that's used for Curve25519 or Curve448 20 | 21 | ECDH 22 | Elliptic Curve Diffie-Hellman 23 | 24 | raw encoding 25 | Conversion of public, private keys and signatures (which in 26 | mathematical sense are integers or pairs of integers) to strings of 27 | bytes that does not use any special tags or encoding rules. 28 | For any given curve, all keys of the same type or signatures will be 29 | encoded to byte strings of the same length. In more formal sense, 30 | the integers are encoded as big-endian, constant length byte strings, 31 | where the string length is determined by the curve order (e.g. 32 | for NIST256p the order is 256 bits long, so the private key will be 32 33 | bytes long while public key will be 64 bytes long). The encoding of a 34 | single integer is zero-padded on the left if the numerical value is 35 | low. In case of public keys and signatures, which are comprised of two 36 | integers, the integers are simply concatenated. 37 | 38 | uncompressed 39 | The most common formatting specified in PKIX standards. Specified in 40 | X9.62 and SEC1 standards. The only difference between it and 41 | :term:`raw encoding` is the prepending of a 0x04 byte. Thus an 42 | uncompressed NIST256p public key encoding will be 65 bytes long. 43 | 44 | compressed 45 | The public point representation that uses half of bytes of the 46 | :term:`uncompressed` encoding (rounded up). It uses the first byte of 47 | the encoding to specify the sign of the y coordinate and encodes the 48 | x coordinate as-is. The first byte of the encoding is equal to 49 | 0x02 or 0x03. Compressed encoding of NIST256p public key will be 33 50 | bytes long. 51 | 52 | hybrid 53 | A combination of :term:`uncompressed` and :term:`compressed` encodings. 54 | Both x and y coordinates are stored just as in :term:`compressed` 55 | encoding, but the first byte reflects the sign of the y coordinate. The 56 | first byte of the encoding will be equal to 0x06 or 0x7. Hybrid 57 | encoding of NIST256p public key will be 65 bytes long. 58 | 59 | PEM 60 | The acronym stands for Privacy Enhanced Mail, but currently it is used 61 | primarily as the way to encode :term:`DER` objects into text that can 62 | be either easily copy-pasted or transferred over email. 63 | It uses headers like ``-----BEGIN -----`` and footers 64 | like ``-----END -----`` to separate multiple 65 | types of objects in the same file or the object from the surrounding 66 | comments. The actual object stored is base64 encoded. 67 | 68 | DER 69 | Distinguished Encoding Rules, the way to encode :term:`ASN.1` objects 70 | deterministically and uniquely into byte strings. 71 | 72 | ASN.1 73 | Abstract Syntax Notation 1 is a standard description language for 74 | specifying serialisation and deserialisation of data structures in a 75 | portable and cross-platform way. 76 | 77 | bytes-like object 78 | All the types that implement the buffer protocol. That includes 79 | ``str`` (only on python2), ``bytes``, ``bytearray``, ``array.array`` 80 | and ``memoryview`` of those objects. 81 | Please note that ``array.array`` serialisation (converting it to byte 82 | string) is endianess dependant! Signature computed over ``array.array`` 83 | of integers on a big-endian system will not be verified on a 84 | little-endian system and vice-versa. 85 | 86 | set-like object 87 | All the types that support the ``in`` operator, like ``list``, 88 | ``tuple``, ``set``, ``frozenset``, etc. 89 | 90 | short Weierstrass curve 91 | A curve with the curve equation: :math:`y^2=x^3+ax+b`. Most popular 92 | curves use equation of this format (e.g. NIST256p). 93 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. python-ecdsa documentation master file, created by 2 | sphinx-quickstart on Sat May 29 18:34:49 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-ecdsa's documentation! 7 | ======================================== 8 | 9 | ``ecdsa`` implements 10 | `elliptic-curve cryptography (ECC) `_, 11 | more specifically the 12 | `Elliptic Curve Digital Signature Algorithm (ECDSA) `_, 13 | `Edwards-curve Digital Signature Algorithm (EdDSA) `_ 14 | and the 15 | `Elliptic Curve Diffie-Hellman (ECDH) `_ 16 | algorithms. 17 | All of those algorithms are used in many protocols in practice, like 18 | in 19 | `TLS `_ 20 | or 21 | `SSH `_. 22 | 23 | This library provides key generation, signing, verifying, and shared secret 24 | derivation for five 25 | popular NIST "Suite B" GF(p) (*prime field*) curves, with key lengths of 192, 26 | 224, 256, 384, and 521 bits. The "short names" for these curves, as known by 27 | the OpenSSL tool (``openssl ecparam -list_curves``), are: ``prime192v1``, 28 | ``secp224r1``, ``prime256v1``, ``secp384r1``, and ``secp521r1``. It includes 29 | the 30 | 256-bit curve ``secp256k1`` used by Bitcoin. There is also support for the 31 | regular (non-twisted) variants of Brainpool curves from 160 to 512 bits. The 32 | "short names" of those curves are: ``brainpoolP160r1``, ``brainpoolP192r1``, 33 | ``brainpoolP224r1``, ``brainpoolP256r1``, ``brainpoolP320r1``, 34 | ``brainpoolP384r1``, 35 | ``brainpoolP512r1``. Few of the small curves from SEC standard are also 36 | included (mainly to speed-up testing of the library), those are: 37 | ``secp112r1``, ``secp112r2``, ``secp128r1``, and ``secp160r1``. 38 | Key generation, signing and verifying is also supported for Ed25519 and Ed448 39 | curves. 40 | No other curves are included, but it is not too hard to add support for more 41 | curves over prime fields. 42 | 43 | .. toctree:: 44 | :maxdepth: 2 45 | :caption: Contents: 46 | :hidden: 47 | 48 | quickstart 49 | basics 50 | ec_arithmetic 51 | glossary 52 | modules 53 | 54 | 55 | Indices and tables 56 | ================== 57 | 58 | * :ref:`genindex` 59 | * :ref:`modindex` 60 | * :ref:`glossary` 61 | * :ref:`search` 62 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | python-ecdsa API 2 | ================ 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | ecdsa 8 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting started 3 | =============== 4 | 5 | The library has just one mandatory dependency: ``six``. 6 | If you install ``python-ecdsa`` through pip, it should automatically 7 | install ``six`` too. 8 | 9 | To install it you can run the following command: 10 | 11 | .. code:: bash 12 | 13 | pip install ecdsa 14 | 15 | The high level API provided by the library is primarily in the 16 | :py:class:`~ecdsa.keys` module. 17 | There you will find the :py:class:`~ecdsa.keys.SigningKey` (the class 18 | that enables handling of the private keys) and the 19 | :py:class:`~ecdsa.keys.VerifyingKey` (the class that enables handling of 20 | the public keys). 21 | 22 | To handle shared key derivation, the :py:class:`~ecdsa.ecdh.ECDH` class 23 | is used. 24 | 25 | Finally, in case use of custom elliptic curves is necessary, the 26 | :py:class:`~ecdsa.curves.Curve` class may be needed. 27 | 28 | Key generation 29 | ============== 30 | 31 | To generate a key, import the :py:class:`~ecdsa.keys.SigningKey` and 32 | call the :py:func:`~ecdsa.keys.SigningKey.generate` function in it: 33 | 34 | .. code:: python 35 | 36 | from ecdsa.keys import SigningKey 37 | 38 | key = SigningKey.generate() 39 | 40 | By default, that will create a key that uses the NIST P-192 curve. To 41 | select a more secure curve, like NIST P-256, import it from the 42 | :py:mod:`ecdsa.curves` or from the :py:mod:`ecdsa` module: 43 | 44 | .. code:: python 45 | 46 | from ecdsa import SigningKey, NIST256p 47 | 48 | key = SigningKey.generate(curve=NIST256p) 49 | 50 | Private key storage and retrieval 51 | ================================= 52 | 53 | To store a key as string or file, you can serialise it using many formats, 54 | in general we recommend the PKCS#8 PEM encoding. 55 | 56 | If you have a :py:class:`~ecdsa.keys.SigningKey` object in ``key`` and 57 | want to save it to a file like ``priv_key.pem`` you can run the following 58 | code: 59 | 60 | .. code:: python 61 | 62 | with open("priv_key.pem", "wb") as f: 63 | f.write(key.to_pem(format="pkcs8")) 64 | 65 | .. warning:: 66 | 67 | Not specifying the ``format=pkcs8`` will create a file that uses the legacy 68 | ``ssleay`` file format which is most commonly used by applications 69 | that use OpenSSL, as that was originally the only format supported by it. 70 | For a long time though OpenSSL supports the PKCS# 8 format too. 71 | 72 | To read that file back, you can run code like this: 73 | 74 | .. code:: python 75 | 76 | from ecdsa import SigningKey 77 | 78 | with open("priv_key.pem") as f: 79 | key = SigningKey.from_pem(f.read()) 80 | 81 | .. tip:: 82 | 83 | As the format is self-describing, the parser will automatically detect 84 | if the provided file is in the ``ssleay`` or the ``pkcs8`` format 85 | and process it accordingly. 86 | 87 | Public key derivation 88 | ===================== 89 | 90 | To get the public key associated with the given private key, either 91 | call the :py:func:`~ecdsa.keys.SigningKey.get_verifying_key` method or 92 | access the ``verifying_key`` attribute in 93 | :py:class:`~ecdsa.keys.SigningKey` directly: 94 | 95 | .. code:: python 96 | 97 | from ecdsa import SigningKey, NIST256p 98 | 99 | private_key = SigningKey.generate(curve=NIST256p) 100 | 101 | public_key = private_key.verifying_key 102 | 103 | Public key storage and retrieval 104 | ================================ 105 | 106 | Similarly to private keys, public keys can be stored in files: 107 | 108 | .. code:: python 109 | 110 | from ecdsa import SigningKey 111 | 112 | private_key = SigningKey.generate() 113 | 114 | public_key = private_key.verifying_key 115 | 116 | with open("pub_key.pem", "wb") as f: 117 | f.write(public_key.to_pem()) 118 | 119 | And read from files: 120 | 121 | .. code:: python 122 | 123 | from ecdsa import VerifyingKey 124 | 125 | with open("pub_key.pem") as f: 126 | public_key = VerifyingKey.from_pem(f.read()) 127 | 128 | Signing 129 | ======= 130 | 131 | To sign a byte string stored in variable ``message`` using SigningKey in 132 | ``private_key``, SHA-256, get a signature in the DER format and save it to a 133 | file, you can use the following code: 134 | 135 | .. code:: python 136 | 137 | from hashlib import sha256 138 | from ecdsa.util import sigencode_der 139 | 140 | sig = private_key.sign_deterministic( 141 | message, 142 | hashfunc=sha256, 143 | sigencode=sigencode_der 144 | ) 145 | 146 | with open("message.sig", "wb") as f: 147 | f.write(sig) 148 | 149 | .. note:: 150 | 151 | As cryptographic hashes (SHA-256, SHA3-256, etc.) operate on *bytes* not 152 | text strings, any text needs to be serialised into *bytes* before it can 153 | be signed. This is because encoding of string "text" results in very 154 | different bytes when it's encoded using UTF-8 and when it's encoded using 155 | UCS-2. 156 | 157 | Verifying 158 | ========= 159 | 160 | To verify a signature of a byte string in ``message`` using a VerifyingKey 161 | in ``public_key``, SHA-256 and a DER signature in a ``message.sig`` file, 162 | you can use the following code: 163 | 164 | .. code:: python 165 | 166 | from hashlib import sha256 167 | from ecdsa import BadSignatureError 168 | from ecdsa.util import sigdecode_der 169 | 170 | with open("message.sig", "rb") as f: 171 | sig = f.read() 172 | 173 | try: 174 | ret = public_key.verify(sig, message, sha256, sigdecode=sigdecode_der) 175 | assert ret 176 | print("Valid signature") 177 | except BadSignatureError: 178 | print("Incorrect signature") 179 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | # See the docstring in versioneer.py for instructions. Note that you must 5 | # re-run 'versioneer.py setup' after changing this section, and commit the 6 | # resulting files. 7 | 8 | [versioneer] 9 | VCS = git 10 | style = pep440 11 | versionfile_source = src/ecdsa/_version.py 12 | versionfile_build = ecdsa/_version.py 13 | tag_prefix = python-ecdsa- 14 | parentdir_prefix = ecdsa- 15 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import os 5 | 6 | from setuptools import setup 7 | import versioneer 8 | 9 | commands = versioneer.get_cmdclass().copy() 10 | 11 | # Use README.md to set markdown long_description 12 | directory = os.path.abspath(os.path.dirname(__file__)) 13 | readme_path = os.path.join(directory, "README.md") 14 | with io.open(readme_path, encoding="utf-8") as read_file: 15 | long_description = read_file.read() 16 | 17 | setup( 18 | name="ecdsa", 19 | version=versioneer.get_version(), 20 | description="ECDSA cryptographic signature library (pure python)", 21 | long_description=long_description, 22 | long_description_content_type="text/markdown", 23 | author="Brian Warner", 24 | author_email="warner@lothar.com", 25 | url="http://github.com/tlsfuzzer/python-ecdsa", 26 | packages=["ecdsa"], 27 | package_dir={"": "src"}, 28 | license="MIT", 29 | cmdclass=commands, 30 | python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, " 31 | "!=3.5.*", 32 | classifiers=[ 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 2", 35 | "Programming Language :: Python :: 2.6", 36 | "Programming Language :: Python :: 2.7", 37 | "Programming Language :: Python :: 3", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3.8", 41 | "Programming Language :: Python :: 3.9", 42 | "Programming Language :: Python :: 3.10", 43 | "Programming Language :: Python :: 3.11", 44 | "Programming Language :: Python :: 3.12", 45 | "Programming Language :: Python :: 3.13", 46 | ], 47 | install_requires=["six>=1.9.0"], 48 | extras_require={"gmpy2": "gmpy2", "gmpy": "gmpy"}, 49 | ) 50 | -------------------------------------------------------------------------------- /speed.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | from ecdsa.curves import curves 3 | 4 | 5 | def do(setup_statements, statement): 6 | # extracted from timeit.py 7 | t = timeit.Timer(stmt=statement, setup="\n".join(setup_statements)) 8 | # determine number so that 0.2 <= total time < 2.0 9 | for i in range(1, 10): 10 | number = 10**i 11 | x = t.timeit(number) 12 | if x >= 0.2: 13 | break 14 | return x / number 15 | 16 | 17 | prnt_form = ( 18 | "{name:>16}{sep:1} {siglen:>6} {keygen:>9{form}}{unit:1} " 19 | "{keygen_inv:>9{form_inv}} {sign:>9{form}}{unit:1} " 20 | "{sign_inv:>9{form_inv}} {verify:>9{form}}{unit:1} " 21 | "{verify_inv:>9{form_inv}} {verify_single:>13{form}}{unit:1} " 22 | "{verify_single_inv:>14{form_inv}}" 23 | ) 24 | 25 | print( 26 | prnt_form.format( 27 | siglen="siglen", 28 | keygen="keygen", 29 | keygen_inv="keygen/s", 30 | sign="sign", 31 | sign_inv="sign/s", 32 | verify="verify", 33 | verify_inv="verify/s", 34 | verify_single="no PC verify", 35 | verify_single_inv="no PC verify/s", 36 | name="", 37 | sep="", 38 | unit="", 39 | form="", 40 | form_inv="", 41 | ) 42 | ) 43 | 44 | for curve in [i.name for i in curves]: 45 | S1 = "from ecdsa import SigningKey, %s" % curve 46 | S2 = "sk = SigningKey.generate(%s)" % curve 47 | S3 = "msg = b'msg'" 48 | S4 = "sig = sk.sign(msg)" 49 | S5 = "vk = sk.get_verifying_key()" 50 | S6 = "vk.precompute()" 51 | S7 = "vk.verify(sig, msg)" 52 | # We happen to know that .generate() also calculates the 53 | # verifying key, which is the time-consuming part. If the code 54 | # were changed to lazily calculate vk, we'd need to change this 55 | # benchmark to loop over S5 instead of S2 56 | keygen = do([S1], S2) 57 | sign = do([S1, S2, S3], S4) 58 | verf = do([S1, S2, S3, S4, S5, S6], S7) 59 | verf_single = do([S1, S2, S3, S4, S5], S7) 60 | import ecdsa 61 | 62 | c = getattr(ecdsa, curve) 63 | sig = ecdsa.SigningKey.generate(c).sign(b"msg") 64 | print( 65 | prnt_form.format( 66 | name=curve, 67 | sep=":", 68 | siglen=len(sig), 69 | unit="s", 70 | keygen=keygen, 71 | keygen_inv=1.0 / keygen, 72 | sign=sign, 73 | sign_inv=1.0 / sign, 74 | verify=verf, 75 | verify_inv=1.0 / verf, 76 | verify_single=verf_single, 77 | verify_single_inv=1.0 / verf_single, 78 | form=".5f", 79 | form_inv=".2f", 80 | ) 81 | ) 82 | 83 | print("") 84 | 85 | ecdh_form = "{name:>16}{sep:1} {ecdh:>9{form}}{unit:1} {ecdh_inv:>9{form_inv}}" 86 | 87 | print( 88 | ecdh_form.format( 89 | ecdh="ecdh", 90 | ecdh_inv="ecdh/s", 91 | name="", 92 | sep="", 93 | unit="", 94 | form="", 95 | form_inv="", 96 | ) 97 | ) 98 | 99 | for curve in [i.name for i in curves]: 100 | if curve == "Ed25519" or curve == "Ed448": 101 | continue 102 | S1 = "from ecdsa import SigningKey, ECDH, {0}".format(curve) 103 | S2 = "our = SigningKey.generate({0})".format(curve) 104 | S3 = "remote = SigningKey.generate({0}).verifying_key".format(curve) 105 | S4 = "ecdh = ECDH(private_key=our, public_key=remote)" 106 | S5 = "ecdh.generate_sharedsecret_bytes()" 107 | ecdh = do([S1, S2, S3, S4], S5) 108 | print( 109 | ecdh_form.format( 110 | name=curve, 111 | sep=":", 112 | unit="s", 113 | form=".5f", 114 | form_inv=".2f", 115 | ecdh=ecdh, 116 | ecdh_inv=1.0 / ecdh, 117 | ) 118 | ) 119 | -------------------------------------------------------------------------------- /sql/combine.sql: -------------------------------------------------------------------------------- 1 | attach 'session-to_merge.sqlite' as toMerge; 2 | BEGIN; 3 | insert into work_items select * from toMerge.work_items; 4 | insert into mutation_specs select * from toMerge.mutation_specs; 5 | insert into work_results select * from toMerge.work_results; 6 | COMMIT; 7 | detach toMerge; 8 | -------------------------------------------------------------------------------- /sql/create_to_del.sql: -------------------------------------------------------------------------------- 1 | create table to_del (job_id VARCHAR NOT NULL, id INTEGER PRIMARY KEY); 2 | insert into to_del select *, ROWID from work_items; 3 | -------------------------------------------------------------------------------- /sql/shard-db.sql: -------------------------------------------------------------------------------- 1 | delete from mutation_specs where job_id in (select job_id from to_del where to_del.ID % 20 != %SHARD%); 2 | delete from work_items where job_id in (select job_id from to_del where to_del.ID % 20 != %SHARD%); 3 | drop table to_del; 4 | -------------------------------------------------------------------------------- /src/ecdsa/__init__.py: -------------------------------------------------------------------------------- 1 | # while we don't use six in this file, we did bundle it for a long time, so 2 | # keep as part of module in a virtual way (through __all__) 3 | import six 4 | from .keys import ( 5 | SigningKey, 6 | VerifyingKey, 7 | BadSignatureError, 8 | BadDigestError, 9 | MalformedPointError, 10 | ) 11 | from .curves import ( 12 | NIST192p, 13 | NIST224p, 14 | NIST256p, 15 | NIST384p, 16 | NIST521p, 17 | SECP256k1, 18 | BRAINPOOLP160r1, 19 | BRAINPOOLP192r1, 20 | BRAINPOOLP224r1, 21 | BRAINPOOLP256r1, 22 | BRAINPOOLP320r1, 23 | BRAINPOOLP384r1, 24 | BRAINPOOLP512r1, 25 | SECP112r1, 26 | SECP112r2, 27 | SECP128r1, 28 | SECP160r1, 29 | Ed25519, 30 | Ed448, 31 | BRAINPOOLP160t1, 32 | BRAINPOOLP192t1, 33 | BRAINPOOLP224t1, 34 | BRAINPOOLP256t1, 35 | BRAINPOOLP320t1, 36 | BRAINPOOLP384t1, 37 | BRAINPOOLP512t1, 38 | ) 39 | from .ecdh import ( 40 | ECDH, 41 | NoKeyError, 42 | NoCurveError, 43 | InvalidCurveError, 44 | InvalidSharedSecretError, 45 | ) 46 | from .der import UnexpectedDER 47 | from . import _version 48 | 49 | # This code comes from http://github.com/tlsfuzzer/python-ecdsa 50 | __all__ = [ 51 | "curves", 52 | "der", 53 | "ecdsa", 54 | "ellipticcurve", 55 | "keys", 56 | "numbertheory", 57 | "test_pyecdsa", 58 | "util", 59 | "six", 60 | ] 61 | 62 | _hush_pyflakes = [ 63 | SigningKey, 64 | VerifyingKey, 65 | BadSignatureError, 66 | BadDigestError, 67 | MalformedPointError, 68 | UnexpectedDER, 69 | InvalidCurveError, 70 | NoKeyError, 71 | InvalidSharedSecretError, 72 | ECDH, 73 | NoCurveError, 74 | NIST192p, 75 | NIST224p, 76 | NIST256p, 77 | NIST384p, 78 | NIST521p, 79 | SECP256k1, 80 | BRAINPOOLP160r1, 81 | BRAINPOOLP192r1, 82 | BRAINPOOLP224r1, 83 | BRAINPOOLP256r1, 84 | BRAINPOOLP320r1, 85 | BRAINPOOLP384r1, 86 | BRAINPOOLP512r1, 87 | SECP112r1, 88 | SECP112r2, 89 | SECP128r1, 90 | SECP160r1, 91 | Ed25519, 92 | Ed448, 93 | six.b(""), 94 | BRAINPOOLP160t1, 95 | BRAINPOOLP192t1, 96 | BRAINPOOLP224t1, 97 | BRAINPOOLP256t1, 98 | BRAINPOOLP320t1, 99 | BRAINPOOLP384t1, 100 | BRAINPOOLP512t1, 101 | ] 102 | del _hush_pyflakes 103 | 104 | __version__ = _version.get_versions()["version"] 105 | -------------------------------------------------------------------------------- /src/ecdsa/_compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Common functions for providing cross-python version compatibility. 3 | """ 4 | import sys 5 | import re 6 | import binascii 7 | from six import integer_types 8 | 9 | 10 | def str_idx_as_int(string, index): 11 | """Take index'th byte from string, return as integer""" 12 | val = string[index] 13 | if isinstance(val, integer_types): 14 | return val 15 | return ord(val) 16 | 17 | 18 | if sys.version_info < (3, 0): # pragma: no branch 19 | import platform 20 | 21 | def normalise_bytes(buffer_object): 22 | """Cast the input into array of bytes.""" 23 | # flake8 runs on py3 where `buffer` indeed doesn't exist... 24 | return buffer(buffer_object) # noqa: F821 25 | 26 | def hmac_compat(ret): 27 | return ret 28 | 29 | if ( 30 | sys.version_info < (2, 7) 31 | or sys.version_info < (2, 7, 4) 32 | or platform.system() == "Java" 33 | ): # pragma: no branch 34 | 35 | def remove_whitespace(text): 36 | """Removes all whitespace from passed in string""" 37 | return re.sub(r"\s+", "", text) 38 | 39 | def compat26_str(val): 40 | return str(val) 41 | 42 | def bit_length(val): 43 | if val == 0: 44 | return 0 45 | return len(bin(val)) - 2 46 | 47 | else: 48 | 49 | def remove_whitespace(text): 50 | """Removes all whitespace from passed in string""" 51 | return re.sub(r"\s+", "", text, flags=re.UNICODE) 52 | 53 | def compat26_str(val): 54 | return val 55 | 56 | def bit_length(val): 57 | """Return number of bits necessary to represent an integer.""" 58 | return val.bit_length() 59 | 60 | def b2a_hex(val): 61 | return binascii.b2a_hex(compat26_str(val)) 62 | 63 | def a2b_hex(val): 64 | try: 65 | return bytearray(binascii.a2b_hex(val)) 66 | except Exception as e: 67 | raise ValueError("base16 error: %s" % e) 68 | 69 | def bytes_to_int(val, byteorder): 70 | """Convert bytes to an int.""" 71 | if not val: 72 | return 0 73 | if byteorder == "big": 74 | return int(b2a_hex(val), 16) 75 | if byteorder == "little": 76 | return int(b2a_hex(val[::-1]), 16) 77 | raise ValueError("Only 'big' and 'little' endian supported") 78 | 79 | def int_to_bytes(val, length=None, byteorder="big"): 80 | """Return number converted to bytes""" 81 | if length is None: 82 | length = byte_length(val) 83 | if byteorder == "big": 84 | return bytearray( 85 | (val >> i) & 0xFF for i in reversed(range(0, length * 8, 8)) 86 | ) 87 | if byteorder == "little": 88 | return bytearray( 89 | (val >> i) & 0xFF for i in range(0, length * 8, 8) 90 | ) 91 | raise ValueError("Only 'big' or 'little' endian supported") 92 | 93 | else: 94 | 95 | def hmac_compat(data): 96 | return data 97 | 98 | def normalise_bytes(buffer_object): 99 | """Cast the input into array of bytes.""" 100 | return memoryview(buffer_object).cast("B") 101 | 102 | def compat26_str(val): 103 | return val 104 | 105 | def remove_whitespace(text): 106 | """Removes all whitespace from passed in string""" 107 | return re.sub(r"\s+", "", text, flags=re.UNICODE) 108 | 109 | def a2b_hex(val): 110 | try: 111 | return bytearray(binascii.a2b_hex(bytearray(val, "ascii"))) 112 | except Exception as e: 113 | raise ValueError("base16 error: %s" % e) 114 | 115 | # pylint: disable=invalid-name 116 | # pylint is stupid here and doesn't notice it's a function, not 117 | # constant 118 | bytes_to_int = int.from_bytes 119 | # pylint: enable=invalid-name 120 | 121 | def bit_length(val): 122 | """Return number of bits necessary to represent an integer.""" 123 | return val.bit_length() 124 | 125 | def int_to_bytes(val, length=None, byteorder="big"): 126 | """Convert integer to bytes.""" 127 | if length is None: 128 | length = byte_length(val) 129 | # for gmpy we need to convert back to native int 130 | if not isinstance(val, int): 131 | val = int(val) 132 | return bytearray(val.to_bytes(length=length, byteorder=byteorder)) 133 | 134 | 135 | def byte_length(val): 136 | """Return number of bytes necessary to represent an integer.""" 137 | length = bit_length(val) 138 | return (length + 7) // 8 139 | -------------------------------------------------------------------------------- /src/ecdsa/_sha3.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementation of the SHAKE-256 algorithm for Ed448 3 | """ 4 | 5 | try: 6 | import hashlib 7 | 8 | hashlib.new("shake256").digest(64) 9 | 10 | def shake_256(msg, outlen): 11 | return hashlib.new("shake256", msg).digest(outlen) 12 | 13 | except (TypeError, ValueError): 14 | 15 | from ._compat import bytes_to_int, int_to_bytes 16 | 17 | # From little endian. 18 | def _from_le(s): 19 | return bytes_to_int(s, byteorder="little") 20 | 21 | # Rotate a word x by b places to the left. 22 | def _rol(x, b): 23 | return ((x << b) | (x >> (64 - b))) & (2**64 - 1) 24 | 25 | # Do the SHA-3 state transform on state s. 26 | def _sha3_transform(s): 27 | ROTATIONS = [ 28 | 0, 29 | 1, 30 | 62, 31 | 28, 32 | 27, 33 | 36, 34 | 44, 35 | 6, 36 | 55, 37 | 20, 38 | 3, 39 | 10, 40 | 43, 41 | 25, 42 | 39, 43 | 41, 44 | 45, 45 | 15, 46 | 21, 47 | 8, 48 | 18, 49 | 2, 50 | 61, 51 | 56, 52 | 14, 53 | ] 54 | PERMUTATION = [ 55 | 1, 56 | 6, 57 | 9, 58 | 22, 59 | 14, 60 | 20, 61 | 2, 62 | 12, 63 | 13, 64 | 19, 65 | 23, 66 | 15, 67 | 4, 68 | 24, 69 | 21, 70 | 8, 71 | 16, 72 | 5, 73 | 3, 74 | 18, 75 | 17, 76 | 11, 77 | 7, 78 | 10, 79 | ] 80 | RC = [ 81 | 0x0000000000000001, 82 | 0x0000000000008082, 83 | 0x800000000000808A, 84 | 0x8000000080008000, 85 | 0x000000000000808B, 86 | 0x0000000080000001, 87 | 0x8000000080008081, 88 | 0x8000000000008009, 89 | 0x000000000000008A, 90 | 0x0000000000000088, 91 | 0x0000000080008009, 92 | 0x000000008000000A, 93 | 0x000000008000808B, 94 | 0x800000000000008B, 95 | 0x8000000000008089, 96 | 0x8000000000008003, 97 | 0x8000000000008002, 98 | 0x8000000000000080, 99 | 0x000000000000800A, 100 | 0x800000008000000A, 101 | 0x8000000080008081, 102 | 0x8000000000008080, 103 | 0x0000000080000001, 104 | 0x8000000080008008, 105 | ] 106 | 107 | for rnd in range(0, 24): 108 | # AddColumnParity (Theta) 109 | c = [0] * 5 110 | d = [0] * 5 111 | for i in range(0, 25): 112 | c[i % 5] ^= s[i] 113 | for i in range(0, 5): 114 | d[i] = c[(i + 4) % 5] ^ _rol(c[(i + 1) % 5], 1) 115 | for i in range(0, 25): 116 | s[i] ^= d[i % 5] 117 | # RotateWords (Rho) 118 | for i in range(0, 25): 119 | s[i] = _rol(s[i], ROTATIONS[i]) 120 | # PermuteWords (Pi) 121 | t = s[PERMUTATION[0]] 122 | for i in range(0, len(PERMUTATION) - 1): 123 | s[PERMUTATION[i]] = s[PERMUTATION[i + 1]] 124 | s[PERMUTATION[-1]] = t 125 | # NonlinearMixRows (Chi) 126 | for i in range(0, 25, 5): 127 | t = [ 128 | s[i], 129 | s[i + 1], 130 | s[i + 2], 131 | s[i + 3], 132 | s[i + 4], 133 | s[i], 134 | s[i + 1], 135 | ] 136 | for j in range(0, 5): 137 | s[i + j] = t[j] ^ ((~t[j + 1]) & (t[j + 2])) 138 | # AddRoundConstant (Iota) 139 | s[0] ^= RC[rnd] 140 | 141 | # Reinterpret octet array b to word array and XOR it to state s. 142 | def _reinterpret_to_words_and_xor(s, b): 143 | for j in range(0, len(b) // 8): 144 | s[j] ^= _from_le(b[8 * j : 8 * j + 8]) 145 | 146 | # Reinterpret word array w to octet array and return it. 147 | def _reinterpret_to_octets(w): 148 | mp = bytearray() 149 | for j in range(0, len(w)): 150 | mp += int_to_bytes(w[j], 8, byteorder="little") 151 | return mp 152 | 153 | def _sha3_raw(msg, r_w, o_p, e_b): 154 | """Semi-generic SHA-3 implementation""" 155 | r_b = 8 * r_w 156 | s = [0] * 25 157 | # Handle whole blocks. 158 | idx = 0 159 | blocks = len(msg) // r_b 160 | for i in range(0, blocks): 161 | _reinterpret_to_words_and_xor(s, msg[idx : idx + r_b]) 162 | idx += r_b 163 | _sha3_transform(s) 164 | # Handle last block padding. 165 | m = bytearray(msg[idx:]) 166 | m.append(o_p) 167 | while len(m) < r_b: 168 | m.append(0) 169 | m[len(m) - 1] |= 128 170 | # Handle padded last block. 171 | _reinterpret_to_words_and_xor(s, m) 172 | _sha3_transform(s) 173 | # Output. 174 | out = bytearray() 175 | while len(out) < e_b: 176 | out += _reinterpret_to_octets(s[:r_w]) 177 | _sha3_transform(s) 178 | return out[:e_b] 179 | 180 | def shake_256(msg, outlen): 181 | return _sha3_raw(msg, 17, 31, outlen) 182 | -------------------------------------------------------------------------------- /src/ecdsa/curves.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | from six import PY2 4 | from . import der, ecdsa, ellipticcurve, eddsa 5 | from .util import orderlen, number_to_string, string_to_number 6 | from ._compat import normalise_bytes, bit_length 7 | 8 | 9 | # orderlen was defined in this module previously, so keep it in __all__, 10 | # will need to mark it as deprecated later 11 | __all__ = [ 12 | "UnknownCurveError", 13 | "orderlen", 14 | "Curve", 15 | "SECP112r1", 16 | "SECP112r2", 17 | "SECP128r1", 18 | "SECP160r1", 19 | "NIST192p", 20 | "NIST224p", 21 | "NIST256p", 22 | "NIST384p", 23 | "NIST521p", 24 | "curves", 25 | "find_curve", 26 | "curve_by_name", 27 | "SECP256k1", 28 | "BRAINPOOLP160r1", 29 | "BRAINPOOLP160t1", 30 | "BRAINPOOLP192r1", 31 | "BRAINPOOLP192t1", 32 | "BRAINPOOLP224r1", 33 | "BRAINPOOLP224t1", 34 | "BRAINPOOLP256r1", 35 | "BRAINPOOLP256t1", 36 | "BRAINPOOLP320r1", 37 | "BRAINPOOLP320t1", 38 | "BRAINPOOLP384r1", 39 | "BRAINPOOLP384t1", 40 | "BRAINPOOLP512r1", 41 | "BRAINPOOLP512t1", 42 | "PRIME_FIELD_OID", 43 | "CHARACTERISTIC_TWO_FIELD_OID", 44 | "Ed25519", 45 | "Ed448", 46 | ] 47 | 48 | 49 | PRIME_FIELD_OID = (1, 2, 840, 10045, 1, 1) 50 | CHARACTERISTIC_TWO_FIELD_OID = (1, 2, 840, 10045, 1, 2) 51 | 52 | 53 | class UnknownCurveError(Exception): 54 | pass 55 | 56 | 57 | class Curve: 58 | def __init__(self, name, curve, generator, oid, openssl_name=None): 59 | self.name = name 60 | self.openssl_name = openssl_name # maybe None 61 | self.curve = curve 62 | self.generator = generator 63 | self.order = generator.order() 64 | if isinstance(curve, ellipticcurve.CurveEdTw): 65 | # EdDSA keys are special in that both private and public 66 | # are the same size (as it's defined only with compressed points) 67 | 68 | # +1 for the sign bit and then round up 69 | self.baselen = (bit_length(curve.p()) + 1 + 7) // 8 70 | self.verifying_key_length = self.baselen 71 | else: 72 | self.baselen = orderlen(self.order) 73 | self.verifying_key_length = 2 * orderlen(curve.p()) 74 | self.signature_length = 2 * self.baselen 75 | self.oid = oid 76 | if oid: 77 | self.encoded_oid = der.encode_oid(*oid) 78 | 79 | def __eq__(self, other): 80 | if isinstance(other, Curve): 81 | return ( 82 | self.curve == other.curve and self.generator == other.generator 83 | ) 84 | return NotImplemented 85 | 86 | def __ne__(self, other): 87 | return not self == other 88 | 89 | def __repr__(self): 90 | return self.name 91 | 92 | def to_der(self, encoding=None, point_encoding="uncompressed"): 93 | """Serialise the curve parameters to binary string. 94 | 95 | :param str encoding: the format to save the curve parameters in. 96 | Default is ``named_curve``, with fallback being the ``explicit`` 97 | if the OID is not set for the curve. 98 | :param str point_encoding: the point encoding of the generator when 99 | explicit curve encoding is used. Ignored for ``named_curve`` 100 | format. 101 | 102 | :return: DER encoded ECParameters structure 103 | :rtype: bytes 104 | """ 105 | if encoding is None: 106 | if self.oid: 107 | encoding = "named_curve" 108 | else: 109 | encoding = "explicit" 110 | 111 | if encoding not in ("named_curve", "explicit"): 112 | raise ValueError( 113 | "Only 'named_curve' and 'explicit' encodings supported" 114 | ) 115 | 116 | if encoding == "named_curve": 117 | if not self.oid: 118 | raise UnknownCurveError( 119 | "Can't encode curve using named_curve encoding without " 120 | "associated curve OID" 121 | ) 122 | return der.encode_oid(*self.oid) 123 | elif isinstance(self.curve, ellipticcurve.CurveEdTw): 124 | assert encoding == "explicit" 125 | raise UnknownCurveError( 126 | "Twisted Edwards curves don't support explicit encoding" 127 | ) 128 | 129 | # encode the ECParameters sequence 130 | curve_p = self.curve.p() 131 | version = der.encode_integer(1) 132 | field_id = der.encode_sequence( 133 | der.encode_oid(*PRIME_FIELD_OID), der.encode_integer(curve_p) 134 | ) 135 | curve = der.encode_sequence( 136 | der.encode_octet_string( 137 | number_to_string(self.curve.a() % curve_p, curve_p) 138 | ), 139 | der.encode_octet_string( 140 | number_to_string(self.curve.b() % curve_p, curve_p) 141 | ), 142 | ) 143 | base = der.encode_octet_string(self.generator.to_bytes(point_encoding)) 144 | order = der.encode_integer(self.generator.order()) 145 | seq_elements = [version, field_id, curve, base, order] 146 | if self.curve.cofactor(): 147 | cofactor = der.encode_integer(self.curve.cofactor()) 148 | seq_elements.append(cofactor) 149 | 150 | return der.encode_sequence(*seq_elements) 151 | 152 | def to_pem(self, encoding=None, point_encoding="uncompressed"): 153 | """ 154 | Serialise the curve parameters to the :term:`PEM` format. 155 | 156 | :param str encoding: the format to save the curve parameters in. 157 | Default is ``named_curve``, with fallback being the ``explicit`` 158 | if the OID is not set for the curve. 159 | :param str point_encoding: the point encoding of the generator when 160 | explicit curve encoding is used. Ignored for ``named_curve`` 161 | format. 162 | 163 | :return: PEM encoded ECParameters structure 164 | :rtype: str 165 | """ 166 | return der.topem( 167 | self.to_der(encoding, point_encoding), "EC PARAMETERS" 168 | ) 169 | 170 | @staticmethod 171 | def from_der(data, valid_encodings=None): 172 | """Decode the curve parameters from DER file. 173 | 174 | :param data: the binary string to decode the parameters from 175 | :type data: :term:`bytes-like object` 176 | :param valid_encodings: set of names of allowed encodings, by default 177 | all (set by passing ``None``), supported ones are ``named_curve`` 178 | and ``explicit`` 179 | :type valid_encodings: :term:`set-like object` 180 | """ 181 | if not valid_encodings: 182 | valid_encodings = set(("named_curve", "explicit")) 183 | if not all(i in ["named_curve", "explicit"] for i in valid_encodings): 184 | raise ValueError( 185 | "Only named_curve and explicit encodings supported" 186 | ) 187 | data = normalise_bytes(data) 188 | if not der.is_sequence(data): 189 | if "named_curve" not in valid_encodings: 190 | raise der.UnexpectedDER( 191 | "named_curve curve parameters not allowed" 192 | ) 193 | oid, empty = der.remove_object(data) 194 | if empty: 195 | raise der.UnexpectedDER("Unexpected data after OID") 196 | return find_curve(oid) 197 | 198 | if "explicit" not in valid_encodings: 199 | raise der.UnexpectedDER("explicit curve parameters not allowed") 200 | 201 | seq, empty = der.remove_sequence(data) 202 | if empty: 203 | raise der.UnexpectedDER( 204 | "Unexpected data after ECParameters structure" 205 | ) 206 | # decode the ECParameters sequence 207 | version, rest = der.remove_integer(seq) 208 | if version != 1: 209 | raise der.UnexpectedDER("Unknown parameter encoding format") 210 | field_id, rest = der.remove_sequence(rest) 211 | curve, rest = der.remove_sequence(rest) 212 | base_bytes, rest = der.remove_octet_string(rest) 213 | order, rest = der.remove_integer(rest) 214 | cofactor = None 215 | if rest: 216 | # the ASN.1 specification of ECParameters allows for future 217 | # extensions of the sequence, so ignore the remaining bytes 218 | cofactor, _ = der.remove_integer(rest) 219 | 220 | # decode the ECParameters.fieldID sequence 221 | field_type, rest = der.remove_object(field_id) 222 | if field_type == CHARACTERISTIC_TWO_FIELD_OID: 223 | raise UnknownCurveError("Characteristic 2 curves unsupported") 224 | if field_type != PRIME_FIELD_OID: 225 | raise UnknownCurveError( 226 | "Unknown field type: {0}".format(field_type) 227 | ) 228 | prime, empty = der.remove_integer(rest) 229 | if empty: 230 | raise der.UnexpectedDER( 231 | "Unexpected data after ECParameters.fieldID.Prime-p element" 232 | ) 233 | 234 | # decode the ECParameters.curve sequence 235 | curve_a_bytes, rest = der.remove_octet_string(curve) 236 | curve_b_bytes, rest = der.remove_octet_string(rest) 237 | # seed can be defined here, but we don't parse it, so ignore `rest` 238 | 239 | curve_a = string_to_number(curve_a_bytes) 240 | curve_b = string_to_number(curve_b_bytes) 241 | 242 | curve_fp = ellipticcurve.CurveFp(prime, curve_a, curve_b, cofactor) 243 | 244 | # decode the ECParameters.base point 245 | 246 | base = ellipticcurve.PointJacobi.from_bytes( 247 | curve_fp, 248 | base_bytes, 249 | valid_encodings=("uncompressed", "compressed", "hybrid"), 250 | order=order, 251 | generator=True, 252 | ) 253 | tmp_curve = Curve("unknown", curve_fp, base, None) 254 | 255 | # if the curve matches one of the well-known ones, use the well-known 256 | # one in preference, as it will have the OID and name associated 257 | for i in curves: 258 | if tmp_curve == i: 259 | return i 260 | return tmp_curve 261 | 262 | @classmethod 263 | def from_pem(cls, string, valid_encodings=None): 264 | """Decode the curve parameters from PEM file. 265 | 266 | :param str string: the text string to decode the parameters from 267 | :param valid_encodings: set of names of allowed encodings, by default 268 | all (set by passing ``None``), supported ones are ``named_curve`` 269 | and ``explicit`` 270 | :type valid_encodings: :term:`set-like object` 271 | """ 272 | if not PY2 and isinstance(string, str): # pragma: no branch 273 | string = string.encode() 274 | 275 | ec_param_index = string.find(b"-----BEGIN EC PARAMETERS-----") 276 | if ec_param_index == -1: 277 | raise der.UnexpectedDER("EC PARAMETERS PEM header not found") 278 | 279 | return cls.from_der( 280 | der.unpem(string[ec_param_index:]), valid_encodings 281 | ) 282 | 283 | 284 | # the SEC curves 285 | SECP112r1 = Curve( 286 | "SECP112r1", 287 | ecdsa.curve_112r1, 288 | ecdsa.generator_112r1, 289 | (1, 3, 132, 0, 6), 290 | "secp112r1", 291 | ) 292 | 293 | 294 | SECP112r2 = Curve( 295 | "SECP112r2", 296 | ecdsa.curve_112r2, 297 | ecdsa.generator_112r2, 298 | (1, 3, 132, 0, 7), 299 | "secp112r2", 300 | ) 301 | 302 | 303 | SECP128r1 = Curve( 304 | "SECP128r1", 305 | ecdsa.curve_128r1, 306 | ecdsa.generator_128r1, 307 | (1, 3, 132, 0, 28), 308 | "secp128r1", 309 | ) 310 | 311 | 312 | SECP160r1 = Curve( 313 | "SECP160r1", 314 | ecdsa.curve_160r1, 315 | ecdsa.generator_160r1, 316 | (1, 3, 132, 0, 8), 317 | "secp160r1", 318 | ) 319 | 320 | 321 | # the NIST curves 322 | NIST192p = Curve( 323 | "NIST192p", 324 | ecdsa.curve_192, 325 | ecdsa.generator_192, 326 | (1, 2, 840, 10045, 3, 1, 1), 327 | "prime192v1", 328 | ) 329 | 330 | 331 | NIST224p = Curve( 332 | "NIST224p", 333 | ecdsa.curve_224, 334 | ecdsa.generator_224, 335 | (1, 3, 132, 0, 33), 336 | "secp224r1", 337 | ) 338 | 339 | 340 | NIST256p = Curve( 341 | "NIST256p", 342 | ecdsa.curve_256, 343 | ecdsa.generator_256, 344 | (1, 2, 840, 10045, 3, 1, 7), 345 | "prime256v1", 346 | ) 347 | 348 | 349 | NIST384p = Curve( 350 | "NIST384p", 351 | ecdsa.curve_384, 352 | ecdsa.generator_384, 353 | (1, 3, 132, 0, 34), 354 | "secp384r1", 355 | ) 356 | 357 | 358 | NIST521p = Curve( 359 | "NIST521p", 360 | ecdsa.curve_521, 361 | ecdsa.generator_521, 362 | (1, 3, 132, 0, 35), 363 | "secp521r1", 364 | ) 365 | 366 | 367 | SECP256k1 = Curve( 368 | "SECP256k1", 369 | ecdsa.curve_secp256k1, 370 | ecdsa.generator_secp256k1, 371 | (1, 3, 132, 0, 10), 372 | "secp256k1", 373 | ) 374 | 375 | 376 | BRAINPOOLP160r1 = Curve( 377 | "BRAINPOOLP160r1", 378 | ecdsa.curve_brainpoolp160r1, 379 | ecdsa.generator_brainpoolp160r1, 380 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 1), 381 | "brainpoolP160r1", 382 | ) 383 | 384 | 385 | BRAINPOOLP160t1 = Curve( 386 | "BRAINPOOLP160t1", 387 | ecdsa.curve_brainpoolp160t1, 388 | ecdsa.generator_brainpoolp160t1, 389 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 2), 390 | "brainpoolP160t1", 391 | ) 392 | 393 | 394 | BRAINPOOLP192r1 = Curve( 395 | "BRAINPOOLP192r1", 396 | ecdsa.curve_brainpoolp192r1, 397 | ecdsa.generator_brainpoolp192r1, 398 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 3), 399 | "brainpoolP192r1", 400 | ) 401 | 402 | 403 | BRAINPOOLP192t1 = Curve( 404 | "BRAINPOOLP192t1", 405 | ecdsa.curve_brainpoolp192t1, 406 | ecdsa.generator_brainpoolp192t1, 407 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 4), 408 | "brainpoolP192t1", 409 | ) 410 | 411 | 412 | BRAINPOOLP224r1 = Curve( 413 | "BRAINPOOLP224r1", 414 | ecdsa.curve_brainpoolp224r1, 415 | ecdsa.generator_brainpoolp224r1, 416 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 5), 417 | "brainpoolP224r1", 418 | ) 419 | 420 | 421 | BRAINPOOLP224t1 = Curve( 422 | "BRAINPOOLP224t1", 423 | ecdsa.curve_brainpoolp224t1, 424 | ecdsa.generator_brainpoolp224t1, 425 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 6), 426 | "brainpoolP224t1", 427 | ) 428 | 429 | 430 | BRAINPOOLP256r1 = Curve( 431 | "BRAINPOOLP256r1", 432 | ecdsa.curve_brainpoolp256r1, 433 | ecdsa.generator_brainpoolp256r1, 434 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 7), 435 | "brainpoolP256r1", 436 | ) 437 | 438 | 439 | BRAINPOOLP256t1 = Curve( 440 | "BRAINPOOLP256t1", 441 | ecdsa.curve_brainpoolp256t1, 442 | ecdsa.generator_brainpoolp256t1, 443 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 8), 444 | "brainpoolP256t1", 445 | ) 446 | 447 | 448 | BRAINPOOLP320r1 = Curve( 449 | "BRAINPOOLP320r1", 450 | ecdsa.curve_brainpoolp320r1, 451 | ecdsa.generator_brainpoolp320r1, 452 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 9), 453 | "brainpoolP320r1", 454 | ) 455 | 456 | 457 | BRAINPOOLP320t1 = Curve( 458 | "BRAINPOOLP320t1", 459 | ecdsa.curve_brainpoolp320t1, 460 | ecdsa.generator_brainpoolp320t1, 461 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 10), 462 | "brainpoolP320t1", 463 | ) 464 | 465 | 466 | BRAINPOOLP384r1 = Curve( 467 | "BRAINPOOLP384r1", 468 | ecdsa.curve_brainpoolp384r1, 469 | ecdsa.generator_brainpoolp384r1, 470 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 11), 471 | "brainpoolP384r1", 472 | ) 473 | 474 | 475 | BRAINPOOLP384t1 = Curve( 476 | "BRAINPOOLP384t1", 477 | ecdsa.curve_brainpoolp384t1, 478 | ecdsa.generator_brainpoolp384t1, 479 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 12), 480 | "brainpoolP384t1", 481 | ) 482 | 483 | 484 | BRAINPOOLP512r1 = Curve( 485 | "BRAINPOOLP512r1", 486 | ecdsa.curve_brainpoolp512r1, 487 | ecdsa.generator_brainpoolp512r1, 488 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 13), 489 | "brainpoolP512r1", 490 | ) 491 | 492 | 493 | BRAINPOOLP512t1 = Curve( 494 | "BRAINPOOLP512t1", 495 | ecdsa.curve_brainpoolp512t1, 496 | ecdsa.generator_brainpoolp512t1, 497 | (1, 3, 36, 3, 3, 2, 8, 1, 1, 14), 498 | "brainpoolP512t1", 499 | ) 500 | 501 | 502 | Ed25519 = Curve( 503 | "Ed25519", 504 | eddsa.curve_ed25519, 505 | eddsa.generator_ed25519, 506 | (1, 3, 101, 112), 507 | ) 508 | 509 | 510 | Ed448 = Curve( 511 | "Ed448", 512 | eddsa.curve_ed448, 513 | eddsa.generator_ed448, 514 | (1, 3, 101, 113), 515 | ) 516 | 517 | 518 | # no order in particular, but keep previously added curves first 519 | curves = [ 520 | NIST192p, 521 | NIST224p, 522 | NIST256p, 523 | NIST384p, 524 | NIST521p, 525 | SECP256k1, 526 | BRAINPOOLP160r1, 527 | BRAINPOOLP192r1, 528 | BRAINPOOLP224r1, 529 | BRAINPOOLP256r1, 530 | BRAINPOOLP320r1, 531 | BRAINPOOLP384r1, 532 | BRAINPOOLP512r1, 533 | SECP112r1, 534 | SECP112r2, 535 | SECP128r1, 536 | SECP160r1, 537 | Ed25519, 538 | Ed448, 539 | BRAINPOOLP160t1, 540 | BRAINPOOLP192t1, 541 | BRAINPOOLP224t1, 542 | BRAINPOOLP256t1, 543 | BRAINPOOLP320t1, 544 | BRAINPOOLP384t1, 545 | BRAINPOOLP512t1, 546 | ] 547 | 548 | 549 | def find_curve(oid_curve): 550 | """Select a curve based on its OID 551 | 552 | :param tuple[int,...] oid_curve: ASN.1 Object Identifier of the 553 | curve to return, like ``(1, 2, 840, 10045, 3, 1, 7)`` for ``NIST256p``. 554 | 555 | :raises UnknownCurveError: When the oid doesn't match any of the supported 556 | curves 557 | 558 | :rtype: ~ecdsa.curves.Curve 559 | """ 560 | for c in curves: 561 | if c.oid == oid_curve: 562 | return c 563 | raise UnknownCurveError( 564 | "I don't know about the curve with oid %s." 565 | "I only know about these: %s" % (oid_curve, [c.name for c in curves]) 566 | ) 567 | 568 | 569 | def curve_by_name(name): 570 | """Select a curve based on its name. 571 | 572 | Returns a :py:class:`~ecdsa.curves.Curve` object with a ``name`` name. 573 | Note that ``name`` is case-sensitve. 574 | 575 | :param str name: Name of the curve to return, like ``NIST256p`` or 576 | ``prime256v1`` 577 | 578 | :raises UnknownCurveError: When the name doesn't match any of the supported 579 | curves 580 | 581 | :rtype: ~ecdsa.curves.Curve 582 | """ 583 | for c in curves: 584 | if name == c.name or (c.openssl_name and name == c.openssl_name): 585 | return c 586 | raise UnknownCurveError( 587 | "Curve with name {0!r} unknown, only curves supported: {1}".format( 588 | name, [c.name for c in curves] 589 | ) 590 | ) 591 | -------------------------------------------------------------------------------- /src/ecdsa/der.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import binascii 4 | import base64 5 | import warnings 6 | from itertools import chain 7 | from six import int2byte, text_type 8 | from ._compat import compat26_str, str_idx_as_int 9 | 10 | 11 | class UnexpectedDER(Exception): 12 | pass 13 | 14 | 15 | def encode_constructed(tag, value): 16 | return int2byte(0xA0 + tag) + encode_length(len(value)) + value 17 | 18 | 19 | def encode_implicit(tag, value, cls="context-specific"): 20 | """ 21 | Encode and IMPLICIT value using :term:`DER`. 22 | 23 | :param int tag: the tag value to encode, must be between 0 an 31 inclusive 24 | :param bytes value: the data to encode 25 | :param str cls: the class of the tag to encode: "application", 26 | "context-specific", or "private" 27 | :rtype: bytes 28 | """ 29 | if cls not in ("application", "context-specific", "private"): 30 | raise ValueError("invalid tag class") 31 | if tag > 31: 32 | raise ValueError("Long tags not supported") 33 | 34 | if cls == "application": 35 | tag_class = 0b01000000 36 | elif cls == "context-specific": 37 | tag_class = 0b10000000 38 | else: 39 | assert cls == "private" 40 | tag_class = 0b11000000 41 | 42 | return int2byte(tag_class + tag) + encode_length(len(value)) + value 43 | 44 | 45 | def encode_integer(r): 46 | assert r >= 0 # can't support negative numbers yet 47 | h = ("%x" % r).encode() 48 | if len(h) % 2: 49 | h = b"0" + h 50 | s = binascii.unhexlify(h) 51 | num = str_idx_as_int(s, 0) 52 | if num <= 0x7F: 53 | return b"\x02" + encode_length(len(s)) + s 54 | else: 55 | # DER integers are two's complement, so if the first byte is 56 | # 0x80-0xff then we need an extra 0x00 byte to prevent it from 57 | # looking negative. 58 | return b"\x02" + encode_length(len(s) + 1) + b"\x00" + s 59 | 60 | 61 | # sentry object to check if an argument was specified (used to detect 62 | # deprecated calling convention) 63 | _sentry = object() 64 | 65 | 66 | def encode_bitstring(s, unused=_sentry): 67 | """ 68 | Encode a binary string as a BIT STRING using :term:`DER` encoding. 69 | 70 | Note, because there is no native Python object that can encode an actual 71 | bit string, this function only accepts byte strings as the `s` argument. 72 | The byte string is the actual bit string that will be encoded, padded 73 | on the right (least significant bits, looking from big endian perspective) 74 | to the first full byte. If the bit string has a bit length that is multiple 75 | of 8, then the padding should not be included. For correct DER encoding 76 | the padding bits MUST be set to 0. 77 | 78 | Number of bits of padding need to be provided as the `unused` parameter. 79 | In case they are specified as None, it means the number of unused bits 80 | is already encoded in the string as the first byte. 81 | 82 | The deprecated call convention specifies just the `s` parameters and 83 | encodes the number of unused bits as first parameter (same convention 84 | as with None). 85 | 86 | Empty string must be encoded with `unused` specified as 0. 87 | 88 | Future version of python-ecdsa will make specifying the `unused` argument 89 | mandatory. 90 | 91 | :param s: bytes to encode 92 | :type s: bytes like object 93 | :param unused: number of bits at the end of `s` that are unused, must be 94 | between 0 and 7 (inclusive) 95 | :type unused: int or None 96 | 97 | :raises ValueError: when `unused` is too large or too small 98 | 99 | :return: `s` encoded using DER 100 | :rtype: bytes 101 | """ 102 | encoded_unused = b"" 103 | len_extra = 0 104 | if unused is _sentry: 105 | warnings.warn( 106 | "Legacy call convention used, unused= needs to be specified", 107 | DeprecationWarning, 108 | ) 109 | elif unused is not None: 110 | if not 0 <= unused <= 7: 111 | raise ValueError("unused must be integer between 0 and 7") 112 | if unused: 113 | if not s: 114 | raise ValueError("unused is non-zero but s is empty") 115 | last = str_idx_as_int(s, -1) 116 | if last & (2**unused - 1): 117 | raise ValueError("unused bits must be zeros in DER") 118 | encoded_unused = int2byte(unused) 119 | len_extra = 1 120 | return b"\x03" + encode_length(len(s) + len_extra) + encoded_unused + s 121 | 122 | 123 | def encode_octet_string(s): 124 | return b"\x04" + encode_length(len(s)) + s 125 | 126 | 127 | def encode_oid(first, second, *pieces): 128 | assert 0 <= first < 2 and 0 <= second <= 39 or first == 2 and 0 <= second 129 | body = b"".join( 130 | chain( 131 | [encode_number(40 * first + second)], 132 | (encode_number(p) for p in pieces), 133 | ) 134 | ) 135 | return b"\x06" + encode_length(len(body)) + body 136 | 137 | 138 | def encode_sequence(*encoded_pieces): 139 | total_len = sum([len(p) for p in encoded_pieces]) 140 | return b"\x30" + encode_length(total_len) + b"".join(encoded_pieces) 141 | 142 | 143 | def encode_number(n): 144 | b128_digits = [] 145 | while n: 146 | b128_digits.insert(0, (n & 0x7F) | 0x80) 147 | n = n >> 7 148 | if not b128_digits: 149 | b128_digits.append(0) 150 | b128_digits[-1] &= 0x7F 151 | return b"".join([int2byte(d) for d in b128_digits]) 152 | 153 | 154 | def is_sequence(string): 155 | return string and string[:1] == b"\x30" 156 | 157 | 158 | def remove_constructed(string): 159 | s0 = str_idx_as_int(string, 0) 160 | if (s0 & 0xE0) != 0xA0: 161 | raise UnexpectedDER( 162 | "wanted type 'constructed tag' (0xa0-0xbf), got 0x%02x" % s0 163 | ) 164 | tag = s0 & 0x1F 165 | length, llen = read_length(string[1:]) 166 | body = string[1 + llen : 1 + llen + length] 167 | rest = string[1 + llen + length :] 168 | return tag, body, rest 169 | 170 | 171 | def remove_implicit(string, exp_class="context-specific"): 172 | """ 173 | Removes an IMPLICIT tagged value from ``string`` following :term:`DER`. 174 | 175 | :param bytes string: a byte string that can have one or more 176 | DER elements. 177 | :param str exp_class: the expected tag class of the implicitly 178 | encoded value. Possible values are: "context-specific", "application", 179 | and "private". 180 | :return: a tuple with first value being the tag without indicator bits, 181 | second being the raw bytes of the value and the third one being 182 | remaining bytes (or an empty string if there are none) 183 | :rtype: tuple(int,bytes,bytes) 184 | """ 185 | if exp_class not in ("context-specific", "application", "private"): 186 | raise ValueError("invalid `exp_class` value") 187 | if exp_class == "application": 188 | tag_class = 0b01000000 189 | elif exp_class == "context-specific": 190 | tag_class = 0b10000000 191 | else: 192 | assert exp_class == "private" 193 | tag_class = 0b11000000 194 | tag_mask = 0b11000000 195 | 196 | s0 = str_idx_as_int(string, 0) 197 | 198 | if (s0 & tag_mask) != tag_class: 199 | raise UnexpectedDER( 200 | "wanted class {0}, got 0x{1:02x} tag".format(exp_class, s0) 201 | ) 202 | if s0 & 0b00100000 != 0: 203 | raise UnexpectedDER( 204 | "wanted type primitive, got 0x{0:02x} tag".format(s0) 205 | ) 206 | 207 | tag = s0 & 0x1F 208 | length, llen = read_length(string[1:]) 209 | body = string[1 + llen : 1 + llen + length] 210 | rest = string[1 + llen + length :] 211 | return tag, body, rest 212 | 213 | 214 | def remove_sequence(string): 215 | if not string: 216 | raise UnexpectedDER("Empty string does not encode a sequence") 217 | if string[:1] != b"\x30": 218 | n = str_idx_as_int(string, 0) 219 | raise UnexpectedDER("wanted type 'sequence' (0x30), got 0x%02x" % n) 220 | length, lengthlength = read_length(string[1:]) 221 | if length > len(string) - 1 - lengthlength: 222 | raise UnexpectedDER("Length longer than the provided buffer") 223 | endseq = 1 + lengthlength + length 224 | return string[1 + lengthlength : endseq], string[endseq:] 225 | 226 | 227 | def remove_octet_string(string): 228 | if string[:1] != b"\x04": 229 | n = str_idx_as_int(string, 0) 230 | raise UnexpectedDER("wanted type 'octetstring' (0x04), got 0x%02x" % n) 231 | length, llen = read_length(string[1:]) 232 | body = string[1 + llen : 1 + llen + length] 233 | rest = string[1 + llen + length :] 234 | return body, rest 235 | 236 | 237 | def remove_object(string): 238 | if not string: 239 | raise UnexpectedDER( 240 | "Empty string does not encode an object identifier" 241 | ) 242 | if string[:1] != b"\x06": 243 | n = str_idx_as_int(string, 0) 244 | raise UnexpectedDER("wanted type 'object' (0x06), got 0x%02x" % n) 245 | length, lengthlength = read_length(string[1:]) 246 | body = string[1 + lengthlength : 1 + lengthlength + length] 247 | rest = string[1 + lengthlength + length :] 248 | if not body: 249 | raise UnexpectedDER("Empty object identifier") 250 | if len(body) != length: 251 | raise UnexpectedDER( 252 | "Length of object identifier longer than the provided buffer" 253 | ) 254 | numbers = [] 255 | while body: 256 | n, ll = read_number(body) 257 | numbers.append(n) 258 | body = body[ll:] 259 | n0 = numbers.pop(0) 260 | if n0 < 80: 261 | first = n0 // 40 262 | else: 263 | first = 2 264 | second = n0 - (40 * first) 265 | numbers.insert(0, first) 266 | numbers.insert(1, second) 267 | return tuple(numbers), rest 268 | 269 | 270 | def remove_integer(string): 271 | if not string: 272 | raise UnexpectedDER( 273 | "Empty string is an invalid encoding of an integer" 274 | ) 275 | if string[:1] != b"\x02": 276 | n = str_idx_as_int(string, 0) 277 | raise UnexpectedDER("wanted type 'integer' (0x02), got 0x%02x" % n) 278 | length, llen = read_length(string[1:]) 279 | if length > len(string) - 1 - llen: 280 | raise UnexpectedDER("Length longer than provided buffer") 281 | if length == 0: 282 | raise UnexpectedDER("0-byte long encoding of integer") 283 | numberbytes = string[1 + llen : 1 + llen + length] 284 | rest = string[1 + llen + length :] 285 | msb = str_idx_as_int(numberbytes, 0) 286 | if not msb < 0x80: 287 | raise UnexpectedDER("Negative integers are not supported") 288 | # check if the encoding is the minimal one (DER requirement) 289 | if length > 1 and not msb: 290 | # leading zero byte is allowed if the integer would have been 291 | # considered a negative number otherwise 292 | smsb = str_idx_as_int(numberbytes, 1) 293 | if smsb < 0x80: 294 | raise UnexpectedDER( 295 | "Invalid encoding of integer, unnecessary " 296 | "zero padding bytes" 297 | ) 298 | return int(binascii.hexlify(numberbytes), 16), rest 299 | 300 | 301 | def read_number(string): 302 | number = 0 303 | llen = 0 304 | if str_idx_as_int(string, 0) == 0x80: 305 | raise UnexpectedDER("Non minimal encoding of OID subidentifier") 306 | # base-128 big endian, with most significant bit set in all but the last 307 | # byte 308 | while True: 309 | if llen >= len(string): 310 | raise UnexpectedDER("ran out of length bytes") 311 | number = number << 7 312 | d = str_idx_as_int(string, llen) 313 | number += d & 0x7F 314 | llen += 1 315 | if not d & 0x80: 316 | break 317 | return number, llen 318 | 319 | 320 | def encode_length(l): 321 | assert l >= 0 322 | if l < 0x80: 323 | return int2byte(l) 324 | s = ("%x" % l).encode() 325 | if len(s) % 2: 326 | s = b"0" + s 327 | s = binascii.unhexlify(s) 328 | llen = len(s) 329 | return int2byte(0x80 | llen) + s 330 | 331 | 332 | def read_length(string): 333 | if not string: 334 | raise UnexpectedDER("Empty string can't encode valid length value") 335 | num = str_idx_as_int(string, 0) 336 | if not (num & 0x80): 337 | # short form 338 | return (num & 0x7F), 1 339 | # else long-form: b0&0x7f is number of additional base256 length bytes, 340 | # big-endian 341 | llen = num & 0x7F 342 | if not llen: 343 | raise UnexpectedDER("Invalid length encoding, length of length is 0") 344 | if llen > len(string) - 1: 345 | raise UnexpectedDER("Length of length longer than provided buffer") 346 | # verify that the encoding is minimal possible (DER requirement) 347 | msb = str_idx_as_int(string, 1) 348 | if not msb or llen == 1 and msb < 0x80: 349 | raise UnexpectedDER("Not minimal encoding of length") 350 | return int(binascii.hexlify(string[1 : 1 + llen]), 16), 1 + llen 351 | 352 | 353 | def remove_bitstring(string, expect_unused=_sentry): 354 | """ 355 | Remove a BIT STRING object from `string` following :term:`DER`. 356 | 357 | The `expect_unused` can be used to specify if the bit string should 358 | have the amount of unused bits decoded or not. If it's an integer, any 359 | read BIT STRING that has number of unused bits different from specified 360 | value will cause UnexpectedDER exception to be raised (this is especially 361 | useful when decoding BIT STRINGS that have DER encoded object in them; 362 | DER encoding is byte oriented, so the unused bits will always equal 0). 363 | 364 | If the `expect_unused` is specified as None, the first element returned 365 | will be a tuple, with the first value being the extracted bit string 366 | while the second value will be the decoded number of unused bits. 367 | 368 | If the `expect_unused` is unspecified, the decoding of byte with 369 | number of unused bits will not be attempted and the bit string will be 370 | returned as-is, the callee will be required to decode it and verify its 371 | correctness. 372 | 373 | Future version of python will require the `expected_unused` parameter 374 | to be specified. 375 | 376 | :param string: string of bytes to extract the BIT STRING from 377 | :type string: bytes like object 378 | :param expect_unused: number of bits that should be unused in the BIT 379 | STRING, or None, to return it to caller 380 | :type expect_unused: int or None 381 | 382 | :raises UnexpectedDER: when the encoding does not follow DER. 383 | 384 | :return: a tuple with first element being the extracted bit string and 385 | the second being the remaining bytes in the string (if any); if the 386 | `expect_unused` is specified as None, the first element of the returned 387 | tuple will be a tuple itself, with first element being the bit string 388 | as bytes and the second element being the number of unused bits at the 389 | end of the byte array as an integer 390 | :rtype: tuple 391 | """ 392 | if not string: 393 | raise UnexpectedDER("Empty string does not encode a bitstring") 394 | if expect_unused is _sentry: 395 | warnings.warn( 396 | "Legacy call convention used, expect_unused= needs to be" 397 | " specified", 398 | DeprecationWarning, 399 | ) 400 | num = str_idx_as_int(string, 0) 401 | if string[:1] != b"\x03": 402 | raise UnexpectedDER("wanted bitstring (0x03), got 0x%02x" % num) 403 | length, llen = read_length(string[1:]) 404 | if not length: 405 | raise UnexpectedDER("Invalid length of bit string, can't be 0") 406 | body = string[1 + llen : 1 + llen + length] 407 | rest = string[1 + llen + length :] 408 | if expect_unused is not _sentry: 409 | unused = str_idx_as_int(body, 0) 410 | if not 0 <= unused <= 7: 411 | raise UnexpectedDER("Invalid encoding of unused bits") 412 | if expect_unused is not None and expect_unused != unused: 413 | raise UnexpectedDER("Unexpected number of unused bits") 414 | body = body[1:] 415 | if unused: 416 | if not body: 417 | raise UnexpectedDER("Invalid encoding of empty bit string") 418 | last = str_idx_as_int(body, -1) 419 | # verify that all the unused bits are set to zero (DER requirement) 420 | if last & (2**unused - 1): 421 | raise UnexpectedDER("Non zero padding bits in bit string") 422 | if expect_unused is None: 423 | body = (body, unused) 424 | return body, rest 425 | 426 | 427 | # SEQUENCE([1, STRING(secexp), cont[0], OBJECT(curvename), cont[1], BINTSTRING) 428 | 429 | 430 | # signatures: (from RFC3279) 431 | # ansi-X9-62 OBJECT IDENTIFIER ::= { 432 | # iso(1) member-body(2) us(840) 10045 } 433 | # 434 | # id-ecSigType OBJECT IDENTIFIER ::= { 435 | # ansi-X9-62 signatures(4) } 436 | # ecdsa-with-SHA1 OBJECT IDENTIFIER ::= { 437 | # id-ecSigType 1 } 438 | # so 1,2,840,10045,4,1 439 | # so 0x42, .. .. 440 | 441 | # Ecdsa-Sig-Value ::= SEQUENCE { 442 | # r INTEGER, 443 | # s INTEGER } 444 | 445 | # id-public-key-type OBJECT IDENTIFIER ::= { ansi-X9.62 2 } 446 | # 447 | # id-ecPublicKey OBJECT IDENTIFIER ::= { id-publicKeyType 1 } 448 | 449 | # I think the secp224r1 identifier is (t=06,l=05,v=2b81040021) 450 | # secp224r1 OBJECT IDENTIFIER ::= { 451 | # iso(1) identified-organization(3) certicom(132) curve(0) 33 } 452 | # and the secp384r1 is (t=06,l=05,v=2b81040022) 453 | # secp384r1 OBJECT IDENTIFIER ::= { 454 | # iso(1) identified-organization(3) certicom(132) curve(0) 34 } 455 | 456 | 457 | def unpem(pem): 458 | if isinstance(pem, text_type): # pragma: no branch 459 | pem = pem.encode() 460 | 461 | d = b"".join( 462 | [ 463 | l.strip() 464 | for l in pem.split(b"\n") 465 | if l and not l.startswith(b"-----") 466 | ] 467 | ) 468 | return base64.b64decode(d) 469 | 470 | 471 | def topem(der, name): 472 | b64 = base64.b64encode(compat26_str(der)) 473 | lines = [("-----BEGIN %s-----\n" % name).encode()] 474 | lines.extend( 475 | [b64[start : start + 76] + b"\n" for start in range(0, len(b64), 76)] 476 | ) 477 | lines.append(("-----END %s-----\n" % name).encode()) 478 | return b"".join(lines) 479 | -------------------------------------------------------------------------------- /src/ecdsa/ecdh.py: -------------------------------------------------------------------------------- 1 | """ 2 | Class for performing Elliptic-curve Diffie-Hellman (ECDH) operations. 3 | """ 4 | 5 | from .util import number_to_string 6 | from .ellipticcurve import INFINITY 7 | from .keys import SigningKey, VerifyingKey 8 | 9 | 10 | __all__ = [ 11 | "ECDH", 12 | "NoKeyError", 13 | "NoCurveError", 14 | "InvalidCurveError", 15 | "InvalidSharedSecretError", 16 | ] 17 | 18 | 19 | class NoKeyError(Exception): 20 | """ECDH. Key not found but it is needed for operation.""" 21 | 22 | pass 23 | 24 | 25 | class NoCurveError(Exception): 26 | """ECDH. Curve not set but it is needed for operation.""" 27 | 28 | pass 29 | 30 | 31 | class InvalidCurveError(Exception): 32 | """ 33 | ECDH. Raised in case the public and private keys use different curves. 34 | """ 35 | 36 | pass 37 | 38 | 39 | class InvalidSharedSecretError(Exception): 40 | """ECDH. Raised in case the shared secret we obtained is an INFINITY.""" 41 | 42 | pass 43 | 44 | 45 | class ECDH(object): 46 | """ 47 | Elliptic-curve Diffie-Hellman (ECDH). A key agreement protocol. 48 | 49 | Allows two parties, each having an elliptic-curve public-private key 50 | pair, to establish a shared secret over an insecure channel 51 | """ 52 | 53 | def __init__(self, curve=None, private_key=None, public_key=None): 54 | """ 55 | ECDH init. 56 | 57 | Call can be initialised without parameters, then the first operation 58 | (loading either key) will set the used curve. 59 | All parameters must be ultimately set before shared secret 60 | calculation will be allowed. 61 | 62 | :param curve: curve for operations 63 | :type curve: Curve 64 | :param private_key: `my` private key for ECDH 65 | :type private_key: SigningKey 66 | :param public_key: `their` public key for ECDH 67 | :type public_key: VerifyingKey 68 | """ 69 | self.curve = curve 70 | self.private_key = None 71 | self.public_key = None 72 | if private_key: 73 | self.load_private_key(private_key) 74 | if public_key: 75 | self.load_received_public_key(public_key) 76 | 77 | def _get_shared_secret(self, remote_public_key): 78 | if not self.private_key: 79 | raise NoKeyError( 80 | "Private key needs to be set to create shared secret" 81 | ) 82 | if not self.public_key: 83 | raise NoKeyError( 84 | "Public key needs to be set to create shared secret" 85 | ) 86 | if not ( 87 | self.private_key.curve == self.curve == remote_public_key.curve 88 | ): 89 | raise InvalidCurveError( 90 | "Curves for public key and private key is not equal." 91 | ) 92 | 93 | # shared secret = PUBKEYtheirs * PRIVATEKEYours 94 | result = ( 95 | remote_public_key.pubkey.point 96 | * self.private_key.privkey.secret_multiplier 97 | ) 98 | if result == INFINITY: 99 | raise InvalidSharedSecretError("Invalid shared secret (INFINITY).") 100 | 101 | return result.x() 102 | 103 | def set_curve(self, key_curve): 104 | """ 105 | Set the working curve for ecdh operations. 106 | 107 | :param key_curve: curve from `curves` module 108 | :type key_curve: Curve 109 | """ 110 | self.curve = key_curve 111 | 112 | def generate_private_key(self): 113 | """ 114 | Generate local private key for ecdh operation with curve that was set. 115 | 116 | :raises NoCurveError: Curve must be set before key generation. 117 | 118 | :return: public (verifying) key from this private key. 119 | :rtype: VerifyingKey 120 | """ 121 | if not self.curve: 122 | raise NoCurveError("Curve must be set prior to key generation.") 123 | return self.load_private_key(SigningKey.generate(curve=self.curve)) 124 | 125 | def load_private_key(self, private_key): 126 | """ 127 | Load private key from SigningKey (keys.py) object. 128 | 129 | Needs to have the same curve as was set with set_curve method. 130 | If curve is not set - it sets from this SigningKey 131 | 132 | :param private_key: Initialised SigningKey class 133 | :type private_key: SigningKey 134 | 135 | :raises InvalidCurveError: private_key curve not the same as self.curve 136 | 137 | :return: public (verifying) key from this private key. 138 | :rtype: VerifyingKey 139 | """ 140 | if not self.curve: 141 | self.curve = private_key.curve 142 | if self.curve != private_key.curve: 143 | raise InvalidCurveError("Curve mismatch.") 144 | self.private_key = private_key 145 | return self.private_key.get_verifying_key() 146 | 147 | def load_private_key_bytes(self, private_key): 148 | """ 149 | Load private key from byte string. 150 | 151 | Uses current curve and checks if the provided key matches 152 | the curve of ECDH key agreement. 153 | Key loads via from_string method of SigningKey class 154 | 155 | :param private_key: private key in bytes string format 156 | :type private_key: :term:`bytes-like object` 157 | 158 | :raises NoCurveError: Curve must be set before loading. 159 | 160 | :return: public (verifying) key from this private key. 161 | :rtype: VerifyingKey 162 | """ 163 | if not self.curve: 164 | raise NoCurveError("Curve must be set prior to key load.") 165 | return self.load_private_key( 166 | SigningKey.from_string(private_key, curve=self.curve) 167 | ) 168 | 169 | def load_private_key_der(self, private_key_der): 170 | """ 171 | Load private key from DER byte string. 172 | 173 | Compares the curve of the DER-encoded key with the ECDH set curve, 174 | uses the former if unset. 175 | 176 | Note, the only DER format supported is the RFC5915 177 | Look at keys.py:SigningKey.from_der() 178 | 179 | :param private_key_der: string with the DER encoding of private ECDSA 180 | key 181 | :type private_key_der: string 182 | 183 | :raises InvalidCurveError: private_key curve not the same as self.curve 184 | 185 | :return: public (verifying) key from this private key. 186 | :rtype: VerifyingKey 187 | """ 188 | return self.load_private_key(SigningKey.from_der(private_key_der)) 189 | 190 | def load_private_key_pem(self, private_key_pem): 191 | """ 192 | Load private key from PEM string. 193 | 194 | Compares the curve of the DER-encoded key with the ECDH set curve, 195 | uses the former if unset. 196 | 197 | Note, the only PEM format supported is the RFC5915 198 | Look at keys.py:SigningKey.from_pem() 199 | it needs to have `EC PRIVATE KEY` section 200 | 201 | :param private_key_pem: string with PEM-encoded private ECDSA key 202 | :type private_key_pem: string 203 | 204 | :raises InvalidCurveError: private_key curve not the same as self.curve 205 | 206 | :return: public (verifying) key from this private key. 207 | :rtype: VerifyingKey 208 | """ 209 | return self.load_private_key(SigningKey.from_pem(private_key_pem)) 210 | 211 | def get_public_key(self): 212 | """ 213 | Provides a public key that matches the local private key. 214 | 215 | Needs to be sent to the remote party. 216 | 217 | :return: public (verifying) key from local private key. 218 | :rtype: VerifyingKey 219 | """ 220 | return self.private_key.get_verifying_key() 221 | 222 | def load_received_public_key(self, public_key): 223 | """ 224 | Load public key from VerifyingKey (keys.py) object. 225 | 226 | Needs to have the same curve as set as current for ecdh operation. 227 | If curve is not set - it sets it from VerifyingKey. 228 | 229 | :param public_key: Initialised VerifyingKey class 230 | :type public_key: VerifyingKey 231 | 232 | :raises InvalidCurveError: public_key curve not the same as self.curve 233 | """ 234 | if not self.curve: 235 | self.curve = public_key.curve 236 | if self.curve != public_key.curve: 237 | raise InvalidCurveError("Curve mismatch.") 238 | self.public_key = public_key 239 | 240 | def load_received_public_key_bytes( 241 | self, public_key_str, valid_encodings=None 242 | ): 243 | """ 244 | Load public key from byte string. 245 | 246 | Uses current curve and checks if key length corresponds to 247 | the current curve. 248 | Key loads via from_string method of VerifyingKey class 249 | 250 | :param public_key_str: public key in bytes string format 251 | :type public_key_str: :term:`bytes-like object` 252 | :param valid_encodings: list of acceptable point encoding formats, 253 | supported ones are: :term:`uncompressed`, :term:`compressed`, 254 | :term:`hybrid`, and :term:`raw encoding` (specified with ``raw`` 255 | name). All formats by default (specified with ``None``). 256 | :type valid_encodings: :term:`set-like object` 257 | """ 258 | return self.load_received_public_key( 259 | VerifyingKey.from_string( 260 | public_key_str, self.curve, valid_encodings 261 | ) 262 | ) 263 | 264 | def load_received_public_key_der(self, public_key_der): 265 | """ 266 | Load public key from DER byte string. 267 | 268 | Compares the curve of the DER-encoded key with the ECDH set curve, 269 | uses the former if unset. 270 | 271 | Note, the only DER format supported is the RFC5912 272 | Look at keys.py:VerifyingKey.from_der() 273 | 274 | :param public_key_der: string with the DER encoding of public ECDSA key 275 | :type public_key_der: string 276 | 277 | :raises InvalidCurveError: public_key curve not the same as self.curve 278 | """ 279 | return self.load_received_public_key( 280 | VerifyingKey.from_der(public_key_der) 281 | ) 282 | 283 | def load_received_public_key_pem(self, public_key_pem): 284 | """ 285 | Load public key from PEM string. 286 | 287 | Compares the curve of the PEM-encoded key with the ECDH set curve, 288 | uses the former if unset. 289 | 290 | Note, the only PEM format supported is the RFC5912 291 | Look at keys.py:VerifyingKey.from_pem() 292 | 293 | :param public_key_pem: string with PEM-encoded public ECDSA key 294 | :type public_key_pem: string 295 | 296 | :raises InvalidCurveError: public_key curve not the same as self.curve 297 | """ 298 | return self.load_received_public_key( 299 | VerifyingKey.from_pem(public_key_pem) 300 | ) 301 | 302 | def generate_sharedsecret_bytes(self): 303 | """ 304 | Generate shared secret from local private key and remote public key. 305 | 306 | The objects needs to have both private key and received public key 307 | before generation is allowed. 308 | 309 | :raises InvalidCurveError: public_key curve not the same as self.curve 310 | :raises NoKeyError: public_key or private_key is not set 311 | 312 | :return: shared secret 313 | :rtype: bytes 314 | """ 315 | return number_to_string( 316 | self.generate_sharedsecret(), self.private_key.curve.curve.p() 317 | ) 318 | 319 | def generate_sharedsecret(self): 320 | """ 321 | Generate shared secret from local private key and remote public key. 322 | 323 | The objects needs to have both private key and received public key 324 | before generation is allowed. 325 | 326 | It's the same for local and remote party, 327 | shared secret(local private key, remote public key) == 328 | shared secret(local public key, remote private key) 329 | 330 | :raises InvalidCurveError: public_key curve not the same as self.curve 331 | :raises NoKeyError: public_key or private_key is not set 332 | 333 | :return: shared secret 334 | :rtype: int 335 | """ 336 | return self._get_shared_secret(self.public_key) 337 | -------------------------------------------------------------------------------- /src/ecdsa/eddsa.py: -------------------------------------------------------------------------------- 1 | """Implementation of Edwards Digital Signature Algorithm.""" 2 | 3 | import hashlib 4 | from ._sha3 import shake_256 5 | from . import ellipticcurve 6 | from ._compat import ( 7 | remove_whitespace, 8 | bit_length, 9 | bytes_to_int, 10 | int_to_bytes, 11 | compat26_str, 12 | ) 13 | 14 | # edwards25519, defined in RFC7748 15 | _p = 2**255 - 19 16 | _a = -1 17 | _d = int( 18 | remove_whitespace( 19 | "370957059346694393431380835087545651895421138798432190163887855330" 20 | "85940283555" 21 | ) 22 | ) 23 | _h = 8 24 | 25 | _Gx = int( 26 | remove_whitespace( 27 | "151122213495354007725011514095885315114540126930418572060461132" 28 | "83949847762202" 29 | ) 30 | ) 31 | _Gy = int( 32 | remove_whitespace( 33 | "463168356949264781694283940034751631413079938662562256157830336" 34 | "03165251855960" 35 | ) 36 | ) 37 | _r = 2**252 + 0x14DEF9DEA2F79CD65812631A5CF5D3ED 38 | 39 | 40 | def _sha512(data): 41 | return hashlib.new("sha512", compat26_str(data)).digest() 42 | 43 | 44 | curve_ed25519 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _sha512) 45 | generator_ed25519 = ellipticcurve.PointEdwards( 46 | curve_ed25519, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True 47 | ) 48 | 49 | 50 | # edwards448, defined in RFC7748 51 | _p = 2**448 - 2**224 - 1 52 | _a = 1 53 | _d = -39081 % _p 54 | _h = 4 55 | 56 | _Gx = int( 57 | remove_whitespace( 58 | "224580040295924300187604334099896036246789641632564134246125461" 59 | "686950415467406032909029192869357953282578032075146446173674602635" 60 | "247710" 61 | ) 62 | ) 63 | _Gy = int( 64 | remove_whitespace( 65 | "298819210078481492676017930443930673437544040154080242095928241" 66 | "372331506189835876003536878655418784733982303233503462500531545062" 67 | "832660" 68 | ) 69 | ) 70 | _r = 2**446 - 0x8335DC163BB124B65129C96FDE933D8D723A70AADC873D6D54A7BB0D 71 | 72 | 73 | def _shake256(data): 74 | return shake_256(data, 114) 75 | 76 | 77 | curve_ed448 = ellipticcurve.CurveEdTw(_p, _a, _d, _h, _shake256) 78 | generator_ed448 = ellipticcurve.PointEdwards( 79 | curve_ed448, _Gx, _Gy, 1, _Gx * _Gy % _p, _r, generator=True 80 | ) 81 | 82 | 83 | class PublicKey(object): 84 | """Public key for the Edwards Digital Signature Algorithm.""" 85 | 86 | def __init__(self, generator, public_key, public_point=None): 87 | self.generator = generator 88 | self.curve = generator.curve() 89 | self.__encoded = public_key 90 | # plus one for the sign bit and round up 91 | self.baselen = (bit_length(self.curve.p()) + 1 + 7) // 8 92 | if len(public_key) != self.baselen: 93 | raise ValueError( 94 | "Incorrect size of the public key, expected: {0} bytes".format( 95 | self.baselen 96 | ) 97 | ) 98 | if public_point: 99 | self.__point = public_point 100 | else: 101 | self.__point = ellipticcurve.PointEdwards.from_bytes( 102 | self.curve, public_key 103 | ) 104 | 105 | def __eq__(self, other): 106 | if isinstance(other, PublicKey): 107 | return ( 108 | self.curve == other.curve and self.__encoded == other.__encoded 109 | ) 110 | return NotImplemented 111 | 112 | def __ne__(self, other): 113 | return not self == other 114 | 115 | @property 116 | def point(self): 117 | return self.__point 118 | 119 | @point.setter 120 | def point(self, other): 121 | if self.__point != other: 122 | raise ValueError("Can't change the coordinates of the point") 123 | self.__point = other 124 | 125 | def public_point(self): 126 | return self.__point 127 | 128 | def public_key(self): 129 | return self.__encoded 130 | 131 | def verify(self, data, signature): 132 | """Verify a Pure EdDSA signature over data.""" 133 | data = compat26_str(data) 134 | if len(signature) != 2 * self.baselen: 135 | raise ValueError( 136 | "Invalid signature length, expected: {0} bytes".format( 137 | 2 * self.baselen 138 | ) 139 | ) 140 | R = ellipticcurve.PointEdwards.from_bytes( 141 | self.curve, signature[: self.baselen] 142 | ) 143 | S = bytes_to_int(signature[self.baselen :], "little") 144 | if S >= self.generator.order(): 145 | raise ValueError("Invalid signature") 146 | 147 | dom = bytearray() 148 | if self.curve == curve_ed448: 149 | dom = bytearray(b"SigEd448" + b"\x00\x00") 150 | 151 | k = bytes_to_int( 152 | self.curve.hash_func(dom + R.to_bytes() + self.__encoded + data), 153 | "little", 154 | ) 155 | 156 | if self.generator * S != self.__point * k + R: 157 | raise ValueError("Invalid signature") 158 | 159 | return True 160 | 161 | 162 | class PrivateKey(object): 163 | """Private key for the Edwards Digital Signature Algorithm.""" 164 | 165 | def __init__(self, generator, private_key): 166 | self.generator = generator 167 | self.curve = generator.curve() 168 | # plus one for the sign bit and round up 169 | self.baselen = (bit_length(self.curve.p()) + 1 + 7) // 8 170 | if len(private_key) != self.baselen: 171 | raise ValueError( 172 | "Incorrect size of private key, expected: {0} bytes".format( 173 | self.baselen 174 | ) 175 | ) 176 | self.__private_key = bytes(private_key) 177 | self.__h = bytearray(self.curve.hash_func(private_key)) 178 | self.__public_key = None 179 | 180 | a = self.__h[: self.baselen] 181 | a = self._key_prune(a) 182 | scalar = bytes_to_int(a, "little") 183 | self.__s = scalar 184 | 185 | @property 186 | def private_key(self): 187 | return self.__private_key 188 | 189 | def __eq__(self, other): 190 | if isinstance(other, PrivateKey): 191 | return ( 192 | self.curve == other.curve 193 | and self.__private_key == other.__private_key 194 | ) 195 | return NotImplemented 196 | 197 | def __ne__(self, other): 198 | return not self == other 199 | 200 | def _key_prune(self, key): 201 | # make sure the key is not in a small subgroup 202 | h = self.curve.cofactor() 203 | if h == 4: 204 | h_log = 2 205 | elif h == 8: 206 | h_log = 3 207 | else: 208 | raise ValueError("Only cofactor 4 and 8 curves supported") 209 | key[0] &= ~((1 << h_log) - 1) 210 | 211 | # ensure the highest bit is set but no higher 212 | l = bit_length(self.curve.p()) 213 | if l % 8 == 0: 214 | key[-1] = 0 215 | key[-2] |= 0x80 216 | else: 217 | key[-1] = key[-1] & (1 << (l % 8)) - 1 | 1 << (l % 8) - 1 218 | return key 219 | 220 | def public_key(self): 221 | """Generate the public key based on the included private key""" 222 | if self.__public_key: 223 | return self.__public_key 224 | 225 | public_point = self.generator * self.__s 226 | 227 | self.__public_key = PublicKey( 228 | self.generator, public_point.to_bytes(), public_point 229 | ) 230 | 231 | return self.__public_key 232 | 233 | def sign(self, data): 234 | """Perform a Pure EdDSA signature over data.""" 235 | data = compat26_str(data) 236 | A = self.public_key().public_key() 237 | 238 | prefix = self.__h[self.baselen :] 239 | 240 | dom = bytearray() 241 | if self.curve == curve_ed448: 242 | dom = bytearray(b"SigEd448" + b"\x00\x00") 243 | 244 | r = bytes_to_int(self.curve.hash_func(dom + prefix + data), "little") 245 | R = (self.generator * r).to_bytes() 246 | 247 | k = bytes_to_int(self.curve.hash_func(dom + R + A + data), "little") 248 | k %= self.generator.order() 249 | 250 | S = (r + k * self.__s) % self.generator.order() 251 | 252 | return R + int_to_bytes(S, self.baselen, "little") 253 | -------------------------------------------------------------------------------- /src/ecdsa/errors.py: -------------------------------------------------------------------------------- 1 | class MalformedPointError(AssertionError): 2 | """Raised in case the encoding of private or public key is malformed.""" 3 | 4 | pass 5 | -------------------------------------------------------------------------------- /src/ecdsa/rfc6979.py: -------------------------------------------------------------------------------- 1 | """ 2 | RFC 6979: 3 | Deterministic Usage of the Digital Signature Algorithm (DSA) and 4 | Elliptic Curve Digital Signature Algorithm (ECDSA) 5 | 6 | http://tools.ietf.org/html/rfc6979 7 | 8 | Many thanks to Coda Hale for his implementation in Go language: 9 | https://github.com/codahale/rfc6979 10 | """ 11 | 12 | import hmac 13 | from binascii import hexlify 14 | from .util import number_to_string, number_to_string_crop, bit_length 15 | from ._compat import hmac_compat 16 | 17 | 18 | # bit_length was defined in this module previously so keep it for backwards 19 | # compatibility, will need to deprecate and remove it later 20 | __all__ = ["bit_length", "bits2int", "bits2octets", "generate_k"] 21 | 22 | 23 | def bits2int(data, qlen): 24 | x = int(hexlify(data), 16) 25 | l = len(data) * 8 26 | 27 | if l > qlen: 28 | return x >> (l - qlen) 29 | return x 30 | 31 | 32 | def bits2octets(data, order): 33 | z1 = bits2int(data, bit_length(order)) 34 | z2 = z1 - order 35 | 36 | if z2 < 0: 37 | z2 = z1 38 | 39 | return number_to_string_crop(z2, order) 40 | 41 | 42 | # https://tools.ietf.org/html/rfc6979#section-3.2 43 | def generate_k(order, secexp, hash_func, data, retry_gen=0, extra_entropy=b""): 44 | """ 45 | Generate the ``k`` value - the nonce for DSA. 46 | 47 | :param int order: order of the DSA generator used in the signature 48 | :param int secexp: secure exponent (private key) in numeric form 49 | :param hash_func: reference to the same hash function used for generating 50 | hash, like :py:class:`hashlib.sha1` 51 | :param bytes data: hash in binary form of the signing data 52 | :param int retry_gen: how many good 'k' values to skip before returning 53 | :param bytes extra_entropy: additional added data in binary form as per 54 | section-3.6 of rfc6979 55 | :rtype: int 56 | """ 57 | 58 | qlen = bit_length(order) 59 | holen = hash_func().digest_size 60 | rolen = (qlen + 7) // 8 61 | bx = ( 62 | hmac_compat(number_to_string(secexp, order)), 63 | hmac_compat(bits2octets(data, order)), 64 | hmac_compat(extra_entropy), 65 | ) 66 | 67 | # Step B 68 | v = b"\x01" * holen 69 | 70 | # Step C 71 | k = b"\x00" * holen 72 | 73 | # Step D 74 | 75 | k = hmac.new(k, digestmod=hash_func) 76 | k.update(v + b"\x00") 77 | for i in bx: 78 | k.update(i) 79 | k = k.digest() 80 | 81 | # Step E 82 | v = hmac.new(k, v, hash_func).digest() 83 | 84 | # Step F 85 | k = hmac.new(k, digestmod=hash_func) 86 | k.update(v + b"\x01") 87 | for i in bx: 88 | k.update(i) 89 | k = k.digest() 90 | 91 | # Step G 92 | v = hmac.new(k, v, hash_func).digest() 93 | 94 | # Step H 95 | while True: 96 | # Step H1 97 | t = b"" 98 | 99 | # Step H2 100 | while len(t) < rolen: 101 | v = hmac.new(k, v, hash_func).digest() 102 | t += v 103 | 104 | # Step H3 105 | secret = bits2int(t, qlen) 106 | 107 | if 1 <= secret < order: 108 | if retry_gen <= 0: 109 | return secret 110 | retry_gen -= 1 111 | 112 | k = hmac.new(k, v + b"\x00", hash_func).digest() 113 | v = hmac.new(k, v, hash_func).digest() 114 | -------------------------------------------------------------------------------- /src/ecdsa/ssh.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | from . import der 3 | from ._compat import compat26_str, int_to_bytes 4 | 5 | _SSH_ED25519 = b"ssh-ed25519" 6 | _SK_MAGIC = b"openssh-key-v1\0" 7 | _NONE = b"none" 8 | 9 | 10 | def _get_key_type(name): 11 | if name == "Ed25519": 12 | return _SSH_ED25519 13 | else: 14 | raise ValueError("Unsupported key type") 15 | 16 | 17 | class _Serializer: 18 | def __init__(self): 19 | self.bytes = b"" 20 | 21 | def put_raw(self, val): 22 | self.bytes += val 23 | 24 | def put_u32(self, val): 25 | self.bytes += int_to_bytes(val, length=4, byteorder="big") 26 | 27 | def put_str(self, val): 28 | self.put_u32(len(val)) 29 | self.bytes += val 30 | 31 | def put_pad(self, blklen=8): 32 | padlen = blklen - (len(self.bytes) % blklen) 33 | self.put_raw(bytearray(range(1, 1 + padlen))) 34 | 35 | def encode(self): 36 | return binascii.b2a_base64(compat26_str(self.bytes)) 37 | 38 | def tobytes(self): 39 | return self.bytes 40 | 41 | def topem(self): 42 | return der.topem(self.bytes, "OPENSSH PRIVATE KEY") 43 | 44 | 45 | def serialize_public(name, pub): 46 | serial = _Serializer() 47 | ktype = _get_key_type(name) 48 | serial.put_str(ktype) 49 | serial.put_str(pub) 50 | return b" ".join([ktype, serial.encode()]) 51 | 52 | 53 | def serialize_private(name, pub, priv): 54 | # encode public part 55 | spub = _Serializer() 56 | ktype = _get_key_type(name) 57 | spub.put_str(ktype) 58 | spub.put_str(pub) 59 | 60 | # encode private part 61 | spriv = _Serializer() 62 | checksum = 0 63 | spriv.put_u32(checksum) 64 | spriv.put_u32(checksum) 65 | spriv.put_raw(spub.tobytes()) 66 | spriv.put_str(priv + pub) 67 | comment = b"" 68 | spriv.put_str(comment) 69 | spriv.put_pad() 70 | 71 | # top-level structure 72 | main = _Serializer() 73 | main.put_raw(_SK_MAGIC) 74 | ciphername = kdfname = _NONE 75 | main.put_str(ciphername) 76 | main.put_str(kdfname) 77 | nokdf = 0 78 | main.put_u32(nokdf) 79 | nkeys = 1 80 | main.put_u32(nkeys) 81 | main.put_str(spub.tobytes()) 82 | main.put_str(spriv.tobytes()) 83 | return main.topem() 84 | -------------------------------------------------------------------------------- /src/ecdsa/test_curves.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | 6 | import base64 7 | import pytest 8 | from .curves import ( 9 | Curve, 10 | NIST256p, 11 | curves, 12 | UnknownCurveError, 13 | PRIME_FIELD_OID, 14 | curve_by_name, 15 | ) 16 | from .ellipticcurve import CurveFp, PointJacobi, CurveEdTw 17 | from . import der 18 | from .util import number_to_string 19 | 20 | 21 | class TestParameterEncoding(unittest.TestCase): 22 | @classmethod 23 | def setUpClass(cls): 24 | # minimal, but with cofactor (excludes seed when compared to 25 | # OpenSSL output) 26 | cls.base64_params = ( 27 | "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////" 28 | "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K" 29 | "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd" 30 | "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1" 31 | "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=" 32 | ) 33 | 34 | def test_from_pem(self): 35 | pem_params = ( 36 | "-----BEGIN EC PARAMETERS-----\n" 37 | "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" 38 | "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" 39 | "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" 40 | "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" 41 | "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" 42 | "-----END EC PARAMETERS-----\n" 43 | ) 44 | curve = Curve.from_pem(pem_params) 45 | 46 | self.assertIs(curve, NIST256p) 47 | 48 | def test_from_pem_with_explicit_when_explicit_disabled(self): 49 | pem_params = ( 50 | "-----BEGIN EC PARAMETERS-----\n" 51 | "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" 52 | "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" 53 | "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" 54 | "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" 55 | "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" 56 | "-----END EC PARAMETERS-----\n" 57 | ) 58 | with self.assertRaises(der.UnexpectedDER) as e: 59 | Curve.from_pem(pem_params, ["named_curve"]) 60 | 61 | self.assertIn("explicit curve parameters not", str(e.exception)) 62 | 63 | def test_from_pem_with_named_curve_with_named_curve_disabled(self): 64 | pem_params = ( 65 | "-----BEGIN EC PARAMETERS-----\n" 66 | "BggqhkjOPQMBBw==\n" 67 | "-----END EC PARAMETERS-----\n" 68 | ) 69 | with self.assertRaises(der.UnexpectedDER) as e: 70 | Curve.from_pem(pem_params, ["explicit"]) 71 | 72 | self.assertIn("named_curve curve parameters not", str(e.exception)) 73 | 74 | def test_from_pem_with_wrong_header(self): 75 | pem_params = ( 76 | "-----BEGIN PARAMETERS-----\n" 77 | "MIHgAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP/////////\n" 78 | "//////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12K\n" 79 | "o6k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEQQRrF9Hy4SxCR/i85uVjpEDyd\n" 80 | "wN9gS3rM6D0oTlF2JjClk/jQuL+Gn+bjufrSnwPnhYrzjNXazFezsu2QGg3v1H1\n" 81 | "AiEA/////wAAAAD//////////7zm+q2nF56E87nKwvxjJVECAQE=\n" 82 | "-----END PARAMETERS-----\n" 83 | ) 84 | with self.assertRaises(der.UnexpectedDER) as e: 85 | Curve.from_pem(pem_params) 86 | 87 | self.assertIn("PARAMETERS PEM header", str(e.exception)) 88 | 89 | def test_to_pem(self): 90 | pem_params = ( 91 | b"-----BEGIN EC PARAMETERS-----\n" 92 | b"BggqhkjOPQMBBw==\n" 93 | b"-----END EC PARAMETERS-----\n" 94 | ) 95 | encoding = NIST256p.to_pem() 96 | 97 | self.assertEqual(pem_params, encoding) 98 | 99 | def test_compare_with_different_object(self): 100 | self.assertNotEqual(NIST256p, 256) 101 | 102 | def test_named_curve_params_der(self): 103 | encoded = NIST256p.to_der() 104 | 105 | # just the encoding of the NIST256p OID (prime256v1) 106 | self.assertEqual(b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07", encoded) 107 | 108 | def test_verify_that_default_is_named_curve_der(self): 109 | encoded_default = NIST256p.to_der() 110 | encoded_named = NIST256p.to_der("named_curve") 111 | 112 | self.assertEqual(encoded_default, encoded_named) 113 | 114 | def test_encoding_to_explicit_params(self): 115 | encoded = NIST256p.to_der("explicit") 116 | 117 | self.assertEqual(encoded, bytes(base64.b64decode(self.base64_params))) 118 | 119 | def test_encoding_to_unsupported_type(self): 120 | with self.assertRaises(ValueError) as e: 121 | NIST256p.to_der("unsupported") 122 | 123 | self.assertIn("Only 'named_curve'", str(e.exception)) 124 | 125 | def test_encoding_to_explicit_compressed_params(self): 126 | encoded = NIST256p.to_der("explicit", "compressed") 127 | 128 | compressed_base_point = ( 129 | "MIHAAgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////" 130 | "/////zBEBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6" 131 | "k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsEIQNrF9Hy4SxCR/i85uVjpEDydwN9" 132 | "gS3rM6D0oTlF2JjClgIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8YyVR" 133 | "AgEB" 134 | ) 135 | 136 | self.assertEqual( 137 | encoded, bytes(base64.b64decode(compressed_base_point)) 138 | ) 139 | 140 | def test_decoding_explicit_from_openssl(self): 141 | # generated with openssl 1.1.1k using 142 | # openssl ecparam -name P-256 -param_enc explicit -out /tmp/file.pem 143 | p256_explicit = ( 144 | "MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////" 145 | "/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6" 146 | "k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+" 147 | "kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK" 148 | "fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz" 149 | "ucrC/GMlUQIBAQ==" 150 | ) 151 | 152 | decoded = Curve.from_der(bytes(base64.b64decode(p256_explicit))) 153 | 154 | self.assertEqual(NIST256p, decoded) 155 | 156 | def test_decoding_well_known_from_explicit_params(self): 157 | curve = Curve.from_der(bytes(base64.b64decode(self.base64_params))) 158 | 159 | self.assertIs(curve, NIST256p) 160 | 161 | def test_decoding_with_incorrect_valid_encodings(self): 162 | with self.assertRaises(ValueError) as e: 163 | Curve.from_der(b"", ["explicitCA"]) 164 | 165 | self.assertIn("Only named_curve", str(e.exception)) 166 | 167 | def test_compare_curves_with_different_generators(self): 168 | curve_fp = CurveFp(23, 1, 7) 169 | base_a = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) 170 | base_b = PointJacobi(curve_fp, 1, 20, 1, 9, generator=True) 171 | 172 | curve_a = Curve("unknown", curve_fp, base_a, None) 173 | curve_b = Curve("unknown", curve_fp, base_b, None) 174 | 175 | self.assertNotEqual(curve_a, curve_b) 176 | 177 | def test_default_encode_for_custom_curve(self): 178 | curve_fp = CurveFp(23, 1, 7) 179 | base_point = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) 180 | 181 | curve = Curve("unknown", curve_fp, base_point, None) 182 | 183 | encoded = curve.to_der() 184 | 185 | decoded = Curve.from_der(encoded) 186 | 187 | self.assertEqual(curve, decoded) 188 | 189 | expected = "MCECAQEwDAYHKoZIzj0BAQIBFzAGBAEBBAEHBAMEDQMCAQk=" 190 | 191 | self.assertEqual(encoded, bytes(base64.b64decode(expected))) 192 | 193 | def test_named_curve_encode_for_custom_curve(self): 194 | curve_fp = CurveFp(23, 1, 7) 195 | base_point = PointJacobi(curve_fp, 13, 3, 1, 9, generator=True) 196 | 197 | curve = Curve("unknown", curve_fp, base_point, None) 198 | 199 | with self.assertRaises(UnknownCurveError) as e: 200 | curve.to_der("named_curve") 201 | 202 | self.assertIn("Can't encode curve", str(e.exception)) 203 | 204 | def test_try_decoding_binary_explicit(self): 205 | sect113r1_explicit = ( 206 | "MIGRAgEBMBwGByqGSM49AQIwEQIBcQYJKoZIzj0BAgMCAgEJMDkEDwAwiCUMpufH" 207 | "/mSc6Fgg9wQPAOi+5NPiJgdEGIvg6ccjAxUAEOcjqxTWluZ2h1YVF1b+v4/LSakE" 208 | "HwQAnXNhbzX0qxQH1zViwQ8ApSgwJ3lY7oTRMV7TGIYCDwEAAAAAAAAA2czsijnl" 209 | "bwIBAg==" 210 | ) 211 | 212 | with self.assertRaises(UnknownCurveError) as e: 213 | Curve.from_der(base64.b64decode(sect113r1_explicit)) 214 | 215 | self.assertIn("Characteristic 2 curves unsupported", str(e.exception)) 216 | 217 | def test_decode_malformed_named_curve(self): 218 | bad_der = der.encode_oid(*NIST256p.oid) + der.encode_integer(1) 219 | 220 | with self.assertRaises(der.UnexpectedDER) as e: 221 | Curve.from_der(bad_der) 222 | 223 | self.assertIn("Unexpected data after OID", str(e.exception)) 224 | 225 | def test_decode_malformed_explicit_garbage_after_ECParam(self): 226 | bad_der = bytes( 227 | base64.b64decode(self.base64_params) 228 | ) + der.encode_integer(1) 229 | 230 | with self.assertRaises(der.UnexpectedDER) as e: 231 | Curve.from_der(bad_der) 232 | 233 | self.assertIn("Unexpected data after ECParameters", str(e.exception)) 234 | 235 | def test_decode_malformed_unknown_version_number(self): 236 | bad_der = der.encode_sequence(der.encode_integer(2)) 237 | 238 | with self.assertRaises(der.UnexpectedDER) as e: 239 | Curve.from_der(bad_der) 240 | 241 | self.assertIn("Unknown parameter encoding format", str(e.exception)) 242 | 243 | def test_decode_malformed_unknown_field_type(self): 244 | curve_p = NIST256p.curve.p() 245 | bad_der = der.encode_sequence( 246 | der.encode_integer(1), 247 | der.encode_sequence( 248 | der.encode_oid(1, 2, 3), der.encode_integer(curve_p) 249 | ), 250 | der.encode_sequence( 251 | der.encode_octet_string( 252 | number_to_string(NIST256p.curve.a() % curve_p, curve_p) 253 | ), 254 | der.encode_octet_string( 255 | number_to_string(NIST256p.curve.b(), curve_p) 256 | ), 257 | ), 258 | der.encode_octet_string( 259 | NIST256p.generator.to_bytes("uncompressed") 260 | ), 261 | der.encode_integer(NIST256p.generator.order()), 262 | ) 263 | 264 | with self.assertRaises(UnknownCurveError) as e: 265 | Curve.from_der(bad_der) 266 | 267 | self.assertIn("Unknown field type: (1, 2, 3)", str(e.exception)) 268 | 269 | def test_decode_malformed_garbage_after_prime(self): 270 | curve_p = NIST256p.curve.p() 271 | bad_der = der.encode_sequence( 272 | der.encode_integer(1), 273 | der.encode_sequence( 274 | der.encode_oid(*PRIME_FIELD_OID), 275 | der.encode_integer(curve_p), 276 | der.encode_integer(1), 277 | ), 278 | der.encode_sequence( 279 | der.encode_octet_string( 280 | number_to_string(NIST256p.curve.a() % curve_p, curve_p) 281 | ), 282 | der.encode_octet_string( 283 | number_to_string(NIST256p.curve.b(), curve_p) 284 | ), 285 | ), 286 | der.encode_octet_string( 287 | NIST256p.generator.to_bytes("uncompressed") 288 | ), 289 | der.encode_integer(NIST256p.generator.order()), 290 | ) 291 | 292 | with self.assertRaises(der.UnexpectedDER) as e: 293 | Curve.from_der(bad_der) 294 | 295 | self.assertIn("Prime-p element", str(e.exception)) 296 | 297 | 298 | class TestCurveSearching(unittest.TestCase): 299 | def test_correct_name(self): 300 | c = curve_by_name("NIST256p") 301 | self.assertIs(c, NIST256p) 302 | 303 | def test_openssl_name(self): 304 | c = curve_by_name("prime256v1") 305 | self.assertIs(c, NIST256p) 306 | 307 | def test_unknown_curve(self): 308 | with self.assertRaises(UnknownCurveError) as e: 309 | curve_by_name("foo bar") 310 | 311 | self.assertIn( 312 | "name 'foo bar' unknown, only curves supported: " 313 | "['NIST192p', 'NIST224p'", 314 | str(e.exception), 315 | ) 316 | 317 | def test_with_None_as_parameter(self): 318 | with self.assertRaises(UnknownCurveError) as e: 319 | curve_by_name(None) 320 | 321 | self.assertIn( 322 | "name None unknown, only curves supported: " 323 | "['NIST192p', 'NIST224p'", 324 | str(e.exception), 325 | ) 326 | 327 | 328 | @pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) 329 | def test_curve_params_encode_decode_named(curve): 330 | ret = Curve.from_der(curve.to_der("named_curve")) 331 | 332 | assert curve == ret 333 | 334 | 335 | @pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) 336 | def test_curve_params_encode_decode_explicit(curve): 337 | if isinstance(curve.curve, CurveEdTw): 338 | with pytest.raises(UnknownCurveError): 339 | curve.to_der("explicit") 340 | else: 341 | ret = Curve.from_der(curve.to_der("explicit")) 342 | 343 | assert curve == ret 344 | 345 | 346 | @pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) 347 | def test_curve_params_encode_decode_default(curve): 348 | ret = Curve.from_der(curve.to_der()) 349 | 350 | assert curve == ret 351 | 352 | 353 | @pytest.mark.parametrize("curve", curves, ids=[i.name for i in curves]) 354 | def test_curve_params_encode_decode_explicit_compressed(curve): 355 | if isinstance(curve.curve, CurveEdTw): 356 | with pytest.raises(UnknownCurveError): 357 | curve.to_der("explicit", "compressed") 358 | else: 359 | ret = Curve.from_der(curve.to_der("explicit", "compressed")) 360 | 361 | assert curve == ret 362 | -------------------------------------------------------------------------------- /src/ecdsa/test_ecdh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import shutil 4 | import subprocess 5 | import pytest 6 | from binascii import unhexlify 7 | 8 | try: 9 | import unittest2 as unittest 10 | except ImportError: 11 | import unittest 12 | 13 | from .curves import ( 14 | NIST192p, 15 | NIST224p, 16 | NIST256p, 17 | NIST384p, 18 | NIST521p, 19 | BRAINPOOLP160r1, 20 | SECP112r2, 21 | SECP128r1, 22 | ) 23 | from .curves import curves 24 | from .ecdh import ( 25 | ECDH, 26 | InvalidCurveError, 27 | InvalidSharedSecretError, 28 | NoKeyError, 29 | NoCurveError, 30 | ) 31 | from .keys import SigningKey, VerifyingKey 32 | from .ellipticcurve import CurveEdTw 33 | 34 | 35 | if "--fast" in sys.argv: # pragma: no cover 36 | curves = [SECP112r2, SECP128r1] 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "vcurve", 41 | curves, 42 | ids=[curve.name for curve in curves], 43 | ) 44 | def test_ecdh_each(vcurve): 45 | if isinstance(vcurve.curve, CurveEdTw): 46 | pytest.skip("ECDH is not supported for Edwards curves") 47 | ecdh1 = ECDH(curve=vcurve) 48 | ecdh2 = ECDH(curve=vcurve) 49 | 50 | ecdh2.generate_private_key() 51 | ecdh1.load_received_public_key(ecdh2.get_public_key()) 52 | ecdh2.load_received_public_key(ecdh1.generate_private_key()) 53 | 54 | secret1 = ecdh1.generate_sharedsecret_bytes() 55 | secret2 = ecdh2.generate_sharedsecret_bytes() 56 | assert secret1 == secret2 57 | 58 | 59 | def test_ecdh_both_keys_present(): 60 | key1 = SigningKey.generate(BRAINPOOLP160r1) 61 | key2 = SigningKey.generate(BRAINPOOLP160r1) 62 | 63 | ecdh1 = ECDH(BRAINPOOLP160r1, key1, key2.verifying_key) 64 | ecdh2 = ECDH(private_key=key2, public_key=key1.verifying_key) 65 | 66 | secret1 = ecdh1.generate_sharedsecret_bytes() 67 | secret2 = ecdh2.generate_sharedsecret_bytes() 68 | 69 | assert secret1 == secret2 70 | 71 | 72 | def test_ecdh_no_public_key(): 73 | ecdh1 = ECDH(curve=NIST192p) 74 | 75 | with pytest.raises(NoKeyError): 76 | ecdh1.generate_sharedsecret_bytes() 77 | 78 | ecdh1.generate_private_key() 79 | 80 | with pytest.raises(NoKeyError): 81 | ecdh1.generate_sharedsecret_bytes() 82 | 83 | 84 | class TestECDH(unittest.TestCase): 85 | def test_load_key_from_wrong_curve(self): 86 | ecdh1 = ECDH() 87 | ecdh1.set_curve(NIST192p) 88 | 89 | key1 = SigningKey.generate(BRAINPOOLP160r1) 90 | 91 | with self.assertRaises(InvalidCurveError) as e: 92 | ecdh1.load_private_key(key1) 93 | 94 | self.assertIn("Curve mismatch", str(e.exception)) 95 | 96 | def test_generate_without_curve(self): 97 | ecdh1 = ECDH() 98 | 99 | with self.assertRaises(NoCurveError) as e: 100 | ecdh1.generate_private_key() 101 | 102 | self.assertIn("Curve must be set", str(e.exception)) 103 | 104 | def test_load_bytes_without_curve_set(self): 105 | ecdh1 = ECDH() 106 | 107 | with self.assertRaises(NoCurveError) as e: 108 | ecdh1.load_private_key_bytes(b"\x01" * 32) 109 | 110 | self.assertIn("Curve must be set", str(e.exception)) 111 | 112 | def test_set_curve_from_received_public_key(self): 113 | ecdh1 = ECDH() 114 | 115 | key1 = SigningKey.generate(BRAINPOOLP160r1) 116 | 117 | ecdh1.load_received_public_key(key1.verifying_key) 118 | 119 | self.assertEqual(ecdh1.curve, BRAINPOOLP160r1) 120 | 121 | 122 | def test_ecdh_wrong_public_key_curve(): 123 | ecdh1 = ECDH(curve=NIST192p) 124 | ecdh1.generate_private_key() 125 | ecdh2 = ECDH(curve=NIST256p) 126 | ecdh2.generate_private_key() 127 | 128 | with pytest.raises(InvalidCurveError): 129 | ecdh1.load_received_public_key(ecdh2.get_public_key()) 130 | 131 | with pytest.raises(InvalidCurveError): 132 | ecdh2.load_received_public_key(ecdh1.get_public_key()) 133 | 134 | ecdh1.public_key = ecdh2.get_public_key() 135 | ecdh2.public_key = ecdh1.get_public_key() 136 | 137 | with pytest.raises(InvalidCurveError): 138 | ecdh1.generate_sharedsecret_bytes() 139 | 140 | with pytest.raises(InvalidCurveError): 141 | ecdh2.generate_sharedsecret_bytes() 142 | 143 | 144 | def test_ecdh_invalid_shared_secret_curve(): 145 | ecdh1 = ECDH(curve=NIST256p) 146 | ecdh1.generate_private_key() 147 | 148 | ecdh1.load_received_public_key( 149 | SigningKey.generate(NIST256p).get_verifying_key() 150 | ) 151 | 152 | ecdh1.private_key.privkey.secret_multiplier = ecdh1.private_key.curve.order 153 | 154 | with pytest.raises(InvalidSharedSecretError): 155 | ecdh1.generate_sharedsecret_bytes() 156 | 157 | 158 | # https://github.com/scogliani/ecc-test-vectors/blob/master/ecdh_kat/secp192r1.txt 159 | # https://github.com/scogliani/ecc-test-vectors/blob/master/ecdh_kat/secp256r1.txt 160 | # https://github.com/coruus/nist-testvectors/blob/master/csrc.nist.gov/groups/STM/cavp/documents/components/ecccdhtestvectors/KAS_ECC_CDH_PrimitiveTest.txt 161 | @pytest.mark.parametrize( 162 | "curve,privatekey,pubkey,secret", 163 | [ 164 | pytest.param( 165 | NIST192p, 166 | "f17d3fea367b74d340851ca4270dcb24c271f445bed9d527", 167 | "42ea6dd9969dd2a61fea1aac7f8e98edcc896c6e55857cc0" 168 | "dfbe5d7c61fac88b11811bde328e8a0d12bf01a9d204b523", 169 | "803d8ab2e5b6e6fca715737c3a82f7ce3c783124f6d51cd0", 170 | id="NIST192p-1", 171 | ), 172 | pytest.param( 173 | NIST192p, 174 | "56e853349d96fe4c442448dacb7cf92bb7a95dcf574a9bd5", 175 | "deb5712fa027ac8d2f22c455ccb73a91e17b6512b5e030e7" 176 | "7e2690a02cc9b28708431a29fb54b87b1f0c14e011ac2125", 177 | "c208847568b98835d7312cef1f97f7aa298283152313c29d", 178 | id="NIST192p-2", 179 | ), 180 | pytest.param( 181 | NIST192p, 182 | "c6ef61fe12e80bf56f2d3f7d0bb757394519906d55500949", 183 | "4edaa8efc5a0f40f843663ec5815e7762dddc008e663c20f" 184 | "0a9f8dc67a3e60ef6d64b522185d03df1fc0adfd42478279", 185 | "87229107047a3b611920d6e3b2c0c89bea4f49412260b8dd", 186 | id="NIST192p-3", 187 | ), 188 | pytest.param( 189 | NIST192p, 190 | "e6747b9c23ba7044f38ff7e62c35e4038920f5a0163d3cda", 191 | "8887c276edeed3e9e866b46d58d895c73fbd80b63e382e88" 192 | "04c5097ba6645e16206cfb70f7052655947dd44a17f1f9d5", 193 | "eec0bed8fc55e1feddc82158fd6dc0d48a4d796aaf47d46c", 194 | id="NIST192p-4", 195 | ), 196 | pytest.param( 197 | NIST192p, 198 | "beabedd0154a1afcfc85d52181c10f5eb47adc51f655047d", 199 | "0d045f30254adc1fcefa8a5b1f31bf4e739dd327cd18d594" 200 | "542c314e41427c08278a08ce8d7305f3b5b849c72d8aff73", 201 | "716e743b1b37a2cd8479f0a3d5a74c10ba2599be18d7e2f4", 202 | id="NIST192p-5", 203 | ), 204 | pytest.param( 205 | NIST192p, 206 | "cf70354226667321d6e2baf40999e2fd74c7a0f793fa8699", 207 | "fb35ca20d2e96665c51b98e8f6eb3d79113508d8bccd4516" 208 | "368eec0d5bfb847721df6aaff0e5d48c444f74bf9cd8a5a7", 209 | "f67053b934459985a315cb017bf0302891798d45d0e19508", 210 | id="NIST192p-6", 211 | ), 212 | pytest.param( 213 | NIST224p, 214 | "8346a60fc6f293ca5a0d2af68ba71d1dd389e5e40837942df3e43cbd", 215 | "af33cd0629bc7e996320a3f40368f74de8704fa37b8fab69abaae280" 216 | "882092ccbba7930f419a8a4f9bb16978bbc3838729992559a6f2e2d7", 217 | "7d96f9a3bd3c05cf5cc37feb8b9d5209d5c2597464dec3e9983743e8", 218 | id="NIST224p", 219 | ), 220 | pytest.param( 221 | NIST256p, 222 | "7d7dc5f71eb29ddaf80d6214632eeae03d9058af1fb6d22ed80badb62bc1a534", 223 | "700c48f77f56584c5cc632ca65640db91b6bacce3a4df6b42ce7cc838833d287" 224 | "db71e509e3fd9b060ddb20ba5c51dcc5948d46fbf640dfe0441782cab85fa4ac", 225 | "46fc62106420ff012e54a434fbdd2d25ccc5852060561e68040dd7778997bd7b", 226 | id="NIST256p-1", 227 | ), 228 | pytest.param( 229 | NIST256p, 230 | "38f65d6dce47676044d58ce5139582d568f64bb16098d179dbab07741dd5caf5", 231 | "809f04289c64348c01515eb03d5ce7ac1a8cb9498f5caa50197e58d43a86a7ae" 232 | "b29d84e811197f25eba8f5194092cb6ff440e26d4421011372461f579271cda3", 233 | "057d636096cb80b67a8c038c890e887d1adfa4195e9b3ce241c8a778c59cda67", 234 | id="NIST256p-2", 235 | ), 236 | pytest.param( 237 | NIST256p, 238 | "1accfaf1b97712b85a6f54b148985a1bdc4c9bec0bd258cad4b3d603f49f32c8", 239 | "a2339c12d4a03c33546de533268b4ad667debf458b464d77443636440ee7fec3" 240 | "ef48a3ab26e20220bcda2c1851076839dae88eae962869a497bf73cb66faf536", 241 | "2d457b78b4614132477618a5b077965ec90730a8c81a1c75d6d4ec68005d67ec", 242 | id="NIST256p-3", 243 | ), 244 | pytest.param( 245 | NIST256p, 246 | "207c43a79bfee03db6f4b944f53d2fb76cc49ef1c9c4d34d51b6c65c4db6932d", 247 | "df3989b9fa55495719b3cf46dccd28b5153f7808191dd518eff0c3cff2b705ed" 248 | "422294ff46003429d739a33206c8752552c8ba54a270defc06e221e0feaf6ac4", 249 | "96441259534b80f6aee3d287a6bb17b5094dd4277d9e294f8fe73e48bf2a0024", 250 | id="NIST256p-4", 251 | ), 252 | pytest.param( 253 | NIST256p, 254 | "59137e38152350b195c9718d39673d519838055ad908dd4757152fd8255c09bf", 255 | "41192d2813e79561e6a1d6f53c8bc1a433a199c835e141b05a74a97b0faeb922" 256 | "1af98cc45e98a7e041b01cf35f462b7562281351c8ebf3ffa02e33a0722a1328", 257 | "19d44c8d63e8e8dd12c22a87b8cd4ece27acdde04dbf47f7f27537a6999a8e62", 258 | id="NIST256p-5", 259 | ), 260 | pytest.param( 261 | NIST256p, 262 | "f5f8e0174610a661277979b58ce5c90fee6c9b3bb346a90a7196255e40b132ef", 263 | "33e82092a0f1fb38f5649d5867fba28b503172b7035574bf8e5b7100a3052792" 264 | "f2cf6b601e0a05945e335550bf648d782f46186c772c0f20d3cd0d6b8ca14b2f", 265 | "664e45d5bba4ac931cd65d52017e4be9b19a515f669bea4703542a2c525cd3d3", 266 | id="NIST256p-6", 267 | ), 268 | pytest.param( 269 | NIST384p, 270 | "3cc3122a68f0d95027ad38c067916ba0eb8c38894d22e1b1" 271 | "5618b6818a661774ad463b205da88cf699ab4d43c9cf98a1", 272 | "a7c76b970c3b5fe8b05d2838ae04ab47697b9eaf52e76459" 273 | "2efda27fe7513272734466b400091adbf2d68c58e0c50066" 274 | "ac68f19f2e1cb879aed43a9969b91a0839c4c38a49749b66" 275 | "1efedf243451915ed0905a32b060992b468c64766fc8437a", 276 | "5f9d29dc5e31a163060356213669c8ce132e22f57c9a04f4" 277 | "0ba7fcead493b457e5621e766c40a2e3d4d6a04b25e533f1", 278 | id="NIST384p", 279 | ), 280 | pytest.param( 281 | NIST521p, 282 | "017eecc07ab4b329068fba65e56a1f8890aa935e57134ae0ffcce802735151f4ea" 283 | "c6564f6ee9974c5e6887a1fefee5743ae2241bfeb95d5ce31ddcb6f9edb4d6fc47", 284 | "00685a48e86c79f0f0875f7bc18d25eb5fc8c0b07e5da4f4370f3a949034085433" 285 | "4b1e1b87fa395464c60626124a4e70d0f785601d37c09870ebf176666877a2046d" 286 | "01ba52c56fc8776d9e8f5db4f0cc27636d0b741bbe05400697942e80b739884a83" 287 | "bde99e0f6716939e632bc8986fa18dccd443a348b6c3e522497955a4f3c302f676", 288 | "005fc70477c3e63bc3954bd0df3ea0d1f41ee21746ed95fc5e1fdf90930d5e1366" 289 | "72d72cc770742d1711c3c3a4c334a0ad9759436a4d3c5bf6e74b9578fac148c831", 290 | id="NIST521p", 291 | ), 292 | ], 293 | ) 294 | def test_ecdh_NIST(curve, privatekey, pubkey, secret): 295 | ecdh = ECDH(curve=curve) 296 | ecdh.load_private_key_bytes(unhexlify(privatekey)) 297 | ecdh.load_received_public_key_bytes(unhexlify(pubkey)) 298 | 299 | sharedsecret = ecdh.generate_sharedsecret_bytes() 300 | 301 | assert sharedsecret == unhexlify(secret) 302 | 303 | 304 | pem_local_private_key = ( 305 | "-----BEGIN EC PRIVATE KEY-----\n" 306 | "MF8CAQEEGF7IQgvW75JSqULpiQQ8op9WH6Uldw6xxaAKBggqhkjOPQMBAaE0AzIA\n" 307 | "BLiBd9CE7xf15FY5QIAoNg+fWbSk1yZOYtoGUdzkejWkxbRc9RWTQjqLVXucIJnz\n" 308 | "bA==\n" 309 | "-----END EC PRIVATE KEY-----\n" 310 | ) 311 | der_local_private_key = ( 312 | "305f02010104185ec8420bd6ef9252a942e989043ca29f561fa525770eb1c5a00a06082a864" 313 | "8ce3d030101a13403320004b88177d084ef17f5e45639408028360f9f59b4a4d7264e62da06" 314 | "51dce47a35a4c5b45cf51593423a8b557b9c2099f36c" 315 | ) 316 | pem_remote_public_key = ( 317 | "-----BEGIN PUBLIC KEY-----\n" 318 | "MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEuIF30ITvF/XkVjlAgCg2D59ZtKTX\n" 319 | "Jk5i2gZR3OR6NaTFtFz1FZNCOotVe5wgmfNs\n" 320 | "-----END PUBLIC KEY-----\n" 321 | ) 322 | der_remote_public_key = ( 323 | "3049301306072a8648ce3d020106082a8648ce3d03010103320004b88177d084ef17f5e4563" 324 | "9408028360f9f59b4a4d7264e62da0651dce47a35a4c5b45cf51593423a8b557b9c2099f36c" 325 | ) 326 | gshared_secret = "8f457e34982478d1c34b9cd2d0c15911b72dd60d869e2cea" 327 | 328 | 329 | def test_ecdh_pem(): 330 | ecdh = ECDH() 331 | ecdh.load_private_key_pem(pem_local_private_key) 332 | ecdh.load_received_public_key_pem(pem_remote_public_key) 333 | 334 | sharedsecret = ecdh.generate_sharedsecret_bytes() 335 | 336 | assert sharedsecret == unhexlify(gshared_secret) 337 | 338 | 339 | def test_ecdh_der(): 340 | ecdh = ECDH() 341 | ecdh.load_private_key_der(unhexlify(der_local_private_key)) 342 | ecdh.load_received_public_key_der(unhexlify(der_remote_public_key)) 343 | 344 | sharedsecret = ecdh.generate_sharedsecret_bytes() 345 | 346 | assert sharedsecret == unhexlify(gshared_secret) 347 | 348 | 349 | # Exception classes used by run_openssl. 350 | class RunOpenSslError(Exception): 351 | pass 352 | 353 | 354 | def run_openssl(cmd): 355 | OPENSSL = "openssl" 356 | p = subprocess.Popen( 357 | [OPENSSL] + cmd.split(), 358 | stdout=subprocess.PIPE, 359 | stderr=subprocess.STDOUT, 360 | ) 361 | stdout, ignored = p.communicate() 362 | if p.returncode != 0: 363 | raise RunOpenSslError( 364 | "cmd '%s %s' failed: rc=%s, stdout/err was %s" 365 | % (OPENSSL, cmd, p.returncode, stdout) 366 | ) 367 | return stdout.decode() 368 | 369 | 370 | OPENSSL_SUPPORTED_CURVES = set( 371 | c.split(":")[0].strip() 372 | for c in run_openssl("ecparam -list_curves").split("\n") 373 | ) 374 | 375 | 376 | @pytest.mark.slow 377 | @pytest.mark.parametrize( 378 | "vcurve", 379 | curves, 380 | ids=[curve.name for curve in curves], 381 | ) 382 | def test_ecdh_with_openssl(vcurve): 383 | if isinstance(vcurve.curve, CurveEdTw): 384 | pytest.skip("Edwards curves are not supported for ECDH") 385 | 386 | assert vcurve.openssl_name 387 | 388 | if vcurve.openssl_name not in OPENSSL_SUPPORTED_CURVES: 389 | pytest.skip("system openssl does not support " + vcurve.openssl_name) 390 | 391 | try: 392 | hlp = run_openssl("pkeyutl -help") 393 | if hlp.find("-derive") == 0: # pragma: no cover 394 | pytest.skip("system openssl does not support `pkeyutl -derive`") 395 | except RunOpenSslError: # pragma: no cover 396 | pytest.skip("system openssl could not be executed") 397 | 398 | if os.path.isdir("t"): # pragma: no branch 399 | shutil.rmtree("t") 400 | os.mkdir("t") 401 | run_openssl( 402 | "ecparam -name %s -genkey -out t/privkey1.pem" % vcurve.openssl_name 403 | ) 404 | run_openssl( 405 | "ecparam -name %s -genkey -out t/privkey2.pem" % vcurve.openssl_name 406 | ) 407 | run_openssl("ec -in t/privkey1.pem -pubout -out t/pubkey1.pem") 408 | 409 | ecdh1 = ECDH(curve=vcurve) 410 | ecdh2 = ECDH(curve=vcurve) 411 | with open("t/privkey1.pem") as e: 412 | key = e.read() 413 | ecdh1.load_private_key_pem(key) 414 | with open("t/privkey2.pem") as e: 415 | key = e.read() 416 | ecdh2.load_private_key_pem(key) 417 | 418 | with open("t/pubkey1.pem") as e: 419 | key = e.read() 420 | vk1 = VerifyingKey.from_pem(key) 421 | assert vk1.to_string() == ecdh1.get_public_key().to_string() 422 | vk2 = ecdh2.get_public_key() 423 | with open("t/pubkey2.pem", "wb") as e: 424 | e.write(vk2.to_pem()) 425 | 426 | ecdh1.load_received_public_key(vk2) 427 | ecdh2.load_received_public_key(vk1) 428 | secret1 = ecdh1.generate_sharedsecret_bytes() 429 | secret2 = ecdh2.generate_sharedsecret_bytes() 430 | 431 | assert secret1 == secret2 432 | 433 | run_openssl( 434 | "pkeyutl -derive -inkey t/privkey1.pem -peerkey t/pubkey2.pem -out t/secret1" 435 | ) 436 | run_openssl( 437 | "pkeyutl -derive -inkey t/privkey2.pem -peerkey t/pubkey1.pem -out t/secret2" 438 | ) 439 | 440 | with open("t/secret1", "rb") as e: 441 | ssl_secret1 = e.read() 442 | with open("t/secret1", "rb") as e: 443 | ssl_secret2 = e.read() 444 | 445 | assert len(ssl_secret1) == vk1.curve.verifying_key_length // 2 446 | assert len(secret1) == vk1.curve.verifying_key_length // 2 447 | 448 | assert ssl_secret1 == ssl_secret2 449 | assert secret1 == ssl_secret1 450 | -------------------------------------------------------------------------------- /src/ecdsa/test_ellipticcurve.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | try: 4 | import unittest2 as unittest 5 | except ImportError: 6 | import unittest 7 | from hypothesis import given, settings 8 | import hypothesis.strategies as st 9 | 10 | try: 11 | from hypothesis import HealthCheck 12 | 13 | HC_PRESENT = True 14 | except ImportError: # pragma: no cover 15 | HC_PRESENT = False 16 | from .numbertheory import inverse_mod 17 | from .ellipticcurve import CurveFp, INFINITY, Point, CurveEdTw 18 | 19 | 20 | HYP_SETTINGS = {} 21 | if HC_PRESENT: # pragma: no branch 22 | HYP_SETTINGS["suppress_health_check"] = [HealthCheck.too_slow] 23 | HYP_SETTINGS["deadline"] = 5000 24 | 25 | 26 | # NIST Curve P-192: 27 | p = 6277101735386680763835789423207666416083908700390324961279 28 | r = 6277101735386680763835789423176059013767194773182842284081 29 | # s = 0x3045ae6fc8422f64ed579528d38120eae12196d5 30 | # c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65 31 | b = 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1 32 | Gx = 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012 33 | Gy = 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 34 | 35 | c192 = CurveFp(p, -3, b) 36 | p192 = Point(c192, Gx, Gy, r) 37 | 38 | c_23 = CurveFp(23, 1, 1) 39 | g_23 = Point(c_23, 13, 7, 7) 40 | 41 | 42 | HYP_SLOW_SETTINGS = dict(HYP_SETTINGS) 43 | HYP_SLOW_SETTINGS["max_examples"] = 2 44 | 45 | 46 | @settings(**HYP_SLOW_SETTINGS) 47 | @given(st.integers(min_value=1, max_value=r - 1)) 48 | def test_p192_mult_tests(multiple): 49 | inv_m = inverse_mod(multiple, r) 50 | 51 | p1 = p192 * multiple 52 | assert p1 * inv_m == p192 53 | 54 | 55 | def add_n_times(point, n): 56 | ret = INFINITY 57 | i = 0 58 | while i <= n: 59 | yield ret 60 | ret = ret + point 61 | i += 1 62 | 63 | 64 | # From X9.62 I.1 (p. 96): 65 | @pytest.mark.parametrize( 66 | "p, m, check", 67 | [(g_23, n, exp) for n, exp in enumerate(add_n_times(g_23, 8))], 68 | ids=["g_23 test with mult {0}".format(i) for i in range(9)], 69 | ) 70 | def test_add_and_mult_equivalence(p, m, check): 71 | assert p * m == check 72 | 73 | 74 | class TestCurve(unittest.TestCase): 75 | @classmethod 76 | def setUpClass(cls): 77 | cls.c_23 = CurveFp(23, 1, 1) 78 | 79 | def test_equality_curves(self): 80 | self.assertEqual(self.c_23, CurveFp(23, 1, 1)) 81 | 82 | def test_inequality_curves(self): 83 | c192 = CurveFp(p, -3, b) 84 | self.assertNotEqual(self.c_23, c192) 85 | 86 | def test_inequality_curves_by_b_only(self): 87 | a = CurveFp(23, 1, 0) 88 | b = CurveFp(23, 1, 1) 89 | self.assertNotEqual(a, b) 90 | 91 | def test_usability_in_a_hashed_collection_curves(self): 92 | {self.c_23: None} 93 | 94 | def test_hashability_curves(self): 95 | hash(self.c_23) 96 | 97 | def test_conflation_curves(self): 98 | ne1, ne2, ne3 = CurveFp(24, 1, 1), CurveFp(23, 2, 1), CurveFp(23, 1, 2) 99 | eq1, eq2, eq3 = CurveFp(23, 1, 1), CurveFp(23, 1, 1), self.c_23 100 | self.assertEqual(len(set((c_23, eq1, eq2, eq3))), 1) 101 | self.assertEqual(len(set((c_23, ne1, ne2, ne3))), 4) 102 | self.assertDictEqual({c_23: None}, {eq1: None}) 103 | self.assertIn(eq2, {eq3: None}) 104 | 105 | def test___str__(self): 106 | self.assertEqual(str(self.c_23), "CurveFp(p=23, a=1, b=1)") 107 | 108 | def test___str___with_cofactor(self): 109 | c = CurveFp(23, 1, 1, 4) 110 | self.assertEqual(str(c), "CurveFp(p=23, a=1, b=1, h=4)") 111 | 112 | 113 | class TestCurveEdTw(unittest.TestCase): 114 | @classmethod 115 | def setUpClass(cls): 116 | cls.c_23 = CurveEdTw(23, 1, 1) 117 | 118 | def test___str__(self): 119 | self.assertEqual(str(self.c_23), "CurveEdTw(p=23, a=1, d=1)") 120 | 121 | def test___str___with_cofactor(self): 122 | c = CurveEdTw(23, 1, 1, 4) 123 | self.assertEqual(str(c), "CurveEdTw(p=23, a=1, d=1, h=4)") 124 | 125 | def test_usability_in_a_hashed_collection_curves(self): 126 | {self.c_23: None} 127 | 128 | def test_hashability_curves(self): 129 | hash(self.c_23) 130 | 131 | 132 | class TestPoint(unittest.TestCase): 133 | @classmethod 134 | def setUpClass(cls): 135 | cls.c_23 = CurveFp(23, 1, 1) 136 | cls.g_23 = Point(cls.c_23, 13, 7, 7) 137 | 138 | p = 6277101735386680763835789423207666416083908700390324961279 139 | r = 6277101735386680763835789423176059013767194773182842284081 140 | # s = 0x3045ae6fc8422f64ed579528d38120eae12196d5 141 | # c = 0x3099d2bbbfcb2538542dcd5fb078b6ef5f3d6fe2c745de65 142 | b = 0x64210519E59C80E70FA7E9AB72243049FEB8DEECC146B9B1 143 | Gx = 0x188DA80EB03090F67CBF20EB43A18800F4FF0AFD82FF1012 144 | Gy = 0x07192B95FFC8DA78631011ED6B24CDD573F977A11E794811 145 | 146 | cls.c192 = CurveFp(p, -3, b) 147 | cls.p192 = Point(cls.c192, Gx, Gy, r) 148 | 149 | def test_p192(self): 150 | # Checking against some sample computations presented 151 | # in X9.62: 152 | d = 651056770906015076056810763456358567190100156695615665659 153 | Q = d * self.p192 154 | self.assertEqual( 155 | Q.x(), 0x62B12D60690CDCF330BABAB6E69763B471F994DD702D16A5 156 | ) 157 | 158 | k = 6140507067065001063065065565667405560006161556565665656654 159 | R = k * self.p192 160 | self.assertEqual( 161 | R.x(), 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD 162 | ) 163 | self.assertEqual( 164 | R.y(), 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835 165 | ) 166 | 167 | u1 = 2563697409189434185194736134579731015366492496392189760599 168 | u2 = 6266643813348617967186477710235785849136406323338782220568 169 | temp = u1 * self.p192 + u2 * Q 170 | self.assertEqual( 171 | temp.x(), 0x885052380FF147B734C330C43D39B2C4A89F29B0F749FEAD 172 | ) 173 | self.assertEqual( 174 | temp.y(), 0x9CF9FA1CBEFEFB917747A3BB29C072B9289C2547884FD835 175 | ) 176 | 177 | def test_double_infinity(self): 178 | p1 = INFINITY 179 | p3 = p1.double() 180 | self.assertEqual(p1, p3) 181 | self.assertEqual(p3.x(), p1.x()) 182 | self.assertEqual(p3.y(), p3.y()) 183 | 184 | def test_double(self): 185 | x1, y1, x3, y3 = (3, 10, 7, 12) 186 | 187 | p1 = Point(self.c_23, x1, y1) 188 | p3 = p1.double() 189 | self.assertEqual(p3.x(), x3) 190 | self.assertEqual(p3.y(), y3) 191 | 192 | def test_double_to_infinity(self): 193 | p1 = Point(self.c_23, 11, 20) 194 | p2 = p1.double() 195 | self.assertEqual((p2.x(), p2.y()), (4, 0)) 196 | self.assertNotEqual(p2, INFINITY) 197 | p3 = p2.double() 198 | self.assertEqual(p3, INFINITY) 199 | self.assertIs(p3, INFINITY) 200 | 201 | def test_add_self_to_infinity(self): 202 | p1 = Point(self.c_23, 11, 20) 203 | p2 = p1 + p1 204 | self.assertEqual((p2.x(), p2.y()), (4, 0)) 205 | self.assertNotEqual(p2, INFINITY) 206 | p3 = p2 + p2 207 | self.assertEqual(p3, INFINITY) 208 | self.assertIs(p3, INFINITY) 209 | 210 | def test_mul_to_infinity(self): 211 | p1 = Point(self.c_23, 11, 20) 212 | p2 = p1 * 2 213 | self.assertEqual((p2.x(), p2.y()), (4, 0)) 214 | self.assertNotEqual(p2, INFINITY) 215 | p3 = p2 * 2 216 | self.assertEqual(p3, INFINITY) 217 | self.assertIs(p3, INFINITY) 218 | 219 | def test_multiply(self): 220 | x1, y1, m, x3, y3 = (3, 10, 2, 7, 12) 221 | p1 = Point(self.c_23, x1, y1) 222 | p3 = p1 * m 223 | self.assertEqual(p3.x(), x3) 224 | self.assertEqual(p3.y(), y3) 225 | 226 | # Trivial tests from X9.62 B.3: 227 | def test_add(self): 228 | """We expect that on curve c, (x1,y1) + (x2, y2 ) = (x3, y3).""" 229 | 230 | x1, y1, x2, y2, x3, y3 = (3, 10, 9, 7, 17, 20) 231 | p1 = Point(self.c_23, x1, y1) 232 | p2 = Point(self.c_23, x2, y2) 233 | p3 = p1 + p2 234 | self.assertEqual(p3.x(), x3) 235 | self.assertEqual(p3.y(), y3) 236 | 237 | def test_add_as_double(self): 238 | """We expect that on curve c, (x1,y1) + (x2, y2 ) = (x3, y3).""" 239 | 240 | x1, y1, x2, y2, x3, y3 = (3, 10, 3, 10, 7, 12) 241 | p1 = Point(self.c_23, x1, y1) 242 | p2 = Point(self.c_23, x2, y2) 243 | p3 = p1 + p2 244 | self.assertEqual(p3.x(), x3) 245 | self.assertEqual(p3.y(), y3) 246 | 247 | def test_equality_points(self): 248 | self.assertEqual(self.g_23, Point(self.c_23, 13, 7, 7)) 249 | 250 | def test_inequality_points(self): 251 | c = CurveFp(100, -3, 100) 252 | p = Point(c, 100, 100, 100) 253 | self.assertNotEqual(self.g_23, p) 254 | 255 | def test_inequality_points_diff_types(self): 256 | c = CurveFp(100, -3, 100) 257 | self.assertNotEqual(self.g_23, c) 258 | 259 | def test_inequality_diff_y(self): 260 | p1 = Point(self.c_23, 6, 4) 261 | p2 = Point(self.c_23, 6, 19) 262 | 263 | self.assertNotEqual(p1, p2) 264 | 265 | def test_to_bytes_from_bytes(self): 266 | p = Point(self.c_23, 3, 10) 267 | 268 | self.assertEqual(p, Point.from_bytes(self.c_23, p.to_bytes())) 269 | 270 | def test_add_to_neg_self(self): 271 | p = Point(self.c_23, 3, 10) 272 | 273 | self.assertEqual(INFINITY, p + (-p)) 274 | 275 | def test_add_to_infinity(self): 276 | p = Point(self.c_23, 3, 10) 277 | 278 | self.assertIs(p, p + INFINITY) 279 | 280 | def test_mul_infinity_by_scalar(self): 281 | self.assertIs(INFINITY, INFINITY * 10) 282 | 283 | def test_mul_by_negative(self): 284 | p = Point(self.c_23, 3, 10) 285 | 286 | self.assertEqual(p * -5, (-p) * 5) 287 | 288 | def test_str_infinity(self): 289 | self.assertEqual(str(INFINITY), "infinity") 290 | 291 | def test_str_point(self): 292 | p = Point(self.c_23, 3, 10) 293 | 294 | self.assertEqual(str(p), "(3,10)") 295 | -------------------------------------------------------------------------------- /src/ecdsa/test_malformed_sigs.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement, division 2 | 3 | import hashlib 4 | 5 | try: 6 | from hashlib import algorithms_available 7 | except ImportError: # pragma: no cover 8 | algorithms_available = [ 9 | "md5", 10 | "sha1", 11 | "sha224", 12 | "sha256", 13 | "sha384", 14 | "sha512", 15 | ] 16 | # skip algorithms broken by change to OpenSSL 3.0 and early versions 17 | # of hashlib that list algorithms that require the legacy provider to work 18 | # https://bugs.python.org/issue38820 19 | algorithms_available = [ 20 | i 21 | for i in algorithms_available 22 | if i not in ("mdc2", "md2", "md4", "whirlpool", "ripemd160") 23 | ] 24 | from functools import partial 25 | import pytest 26 | import sys 27 | import hypothesis.strategies as st 28 | from hypothesis import note, assume, given, settings, example 29 | 30 | from .keys import SigningKey 31 | from .keys import BadSignatureError 32 | from .util import sigencode_der, sigencode_string 33 | from .util import sigdecode_der, sigdecode_string 34 | from .curves import curves, SECP112r2, SECP128r1 35 | from .der import ( 36 | encode_integer, 37 | encode_bitstring, 38 | encode_octet_string, 39 | encode_oid, 40 | encode_sequence, 41 | encode_constructed, 42 | ) 43 | from .ellipticcurve import CurveEdTw 44 | 45 | 46 | example_data = b"some data to sign" 47 | """Since the data is hashed for processing, really any string will do.""" 48 | 49 | 50 | hash_and_size = [ 51 | (name, hashlib.new(name).digest_size) for name in algorithms_available 52 | ] 53 | """Pairs of hash names and their output sizes. 54 | Needed for pairing with curves as we don't support hashes 55 | bigger than order sizes of curves.""" 56 | 57 | 58 | if "--fast" in sys.argv: # pragma: no cover 59 | curves = [SECP112r2, SECP128r1] 60 | 61 | 62 | keys_and_sigs = [] 63 | """Name of the curve+hash combination, VerifyingKey and DER signature.""" 64 | 65 | 66 | # for hypothesis strategy shrinking we want smallest curves and hashes first 67 | for curve in sorted(curves, key=lambda x: x.baselen): 68 | for hash_alg in [ 69 | name 70 | for name, size in sorted(hash_and_size, key=lambda x: x[1]) 71 | if 0 < size <= curve.baselen 72 | ]: 73 | sk = SigningKey.generate( 74 | curve, hashfunc=partial(hashlib.new, hash_alg) 75 | ) 76 | 77 | keys_and_sigs.append( 78 | ( 79 | "{0} {1}".format(curve, hash_alg), 80 | sk.verifying_key, 81 | sk.sign(example_data, sigencode=sigencode_der), 82 | ) 83 | ) 84 | 85 | 86 | # first make sure that the signatures can be verified 87 | @pytest.mark.parametrize( 88 | "verifying_key,signature", 89 | [pytest.param(vk, sig, id=name) for name, vk, sig in keys_and_sigs], 90 | ) 91 | def test_signatures(verifying_key, signature): 92 | assert verifying_key.verify( 93 | signature, example_data, sigdecode=sigdecode_der 94 | ) 95 | 96 | 97 | @st.composite 98 | def st_fuzzed_sig(draw, keys_and_sigs): # pragma: no cover 99 | """ 100 | Hypothesis strategy that generates pairs of VerifyingKey and malformed 101 | signatures created by fuzzing of a valid signature. 102 | """ 103 | name, verifying_key, old_sig = draw(st.sampled_from(keys_and_sigs)) 104 | note("Configuration: {0}".format(name)) 105 | 106 | sig = bytearray(old_sig) 107 | 108 | # decide which bytes should be removed 109 | to_remove = draw( 110 | st.lists(st.integers(min_value=0, max_value=len(sig) - 1), unique=True) 111 | ) 112 | to_remove.sort() 113 | for i in reversed(to_remove): 114 | del sig[i] 115 | note("Remove bytes: {0}".format(to_remove)) 116 | 117 | # decide which bytes of the original signature should be changed 118 | xors = None 119 | if sig: # pragma: no branch 120 | xors = draw( 121 | st.dictionaries( 122 | st.integers(min_value=0, max_value=len(sig) - 1), 123 | st.integers(min_value=1, max_value=255), 124 | ) 125 | ) 126 | for i, val in xors.items(): 127 | sig[i] ^= val 128 | note("xors: {0}".format(xors)) 129 | 130 | # decide where new data should be inserted 131 | insert_pos = draw(st.integers(min_value=0, max_value=len(sig))) 132 | # NIST521p signature is about 140 bytes long, test slightly longer 133 | insert_data = draw(st.binary(max_size=256)) 134 | 135 | sig = sig[:insert_pos] + insert_data + sig[insert_pos:] 136 | note( 137 | "Inserted at position {0} bytes: {1!r}".format(insert_pos, insert_data) 138 | ) 139 | 140 | sig = bytes(sig) 141 | # make sure that there was performed at least one mutation on the data 142 | assume(to_remove or xors or insert_data) 143 | # and that the mutations didn't cancel each-other out 144 | assume(sig != old_sig) 145 | 146 | return verifying_key, sig 147 | 148 | 149 | params = {} 150 | # not supported in hypothesis 2.0.0 151 | if sys.version_info >= (2, 7): # pragma: no branch 152 | from hypothesis import HealthCheck 153 | 154 | # deadline=5s because NIST521p are slow to verify 155 | params["deadline"] = 5000 156 | params["suppress_health_check"] = [ 157 | HealthCheck.data_too_large, 158 | HealthCheck.filter_too_much, 159 | HealthCheck.too_slow, 160 | ] 161 | if "--fast" in sys.argv: # pragma: no cover 162 | params["max_examples"] = 20 163 | 164 | slow_params = dict(params) 165 | if "--fast" in sys.argv: # pragma: no cover 166 | slow_params["max_examples"] = 1 167 | else: 168 | slow_params["max_examples"] = 10 169 | 170 | 171 | @settings(**slow_params) 172 | @given(st_fuzzed_sig(keys_and_sigs)) 173 | def test_fuzzed_der_signatures(args): 174 | verifying_key, sig = args 175 | 176 | with pytest.raises(BadSignatureError): 177 | verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) 178 | 179 | 180 | @st.composite 181 | def st_random_der_ecdsa_sig_value(draw): # pragma: no cover 182 | """ 183 | Hypothesis strategy for selecting random values and encoding them 184 | to ECDSA-Sig-Value object:: 185 | 186 | ECDSA-Sig-Value ::= SEQUENCE { 187 | r INTEGER, 188 | s INTEGER 189 | } 190 | """ 191 | name, verifying_key, _ = draw(st.sampled_from(keys_and_sigs)) 192 | note("Configuration: {0}".format(name)) 193 | order = int(verifying_key.curve.order) 194 | 195 | # the encode_integer doesn't support negative numbers, would be nice 196 | # to generate them too, but we have coverage for remove_integer() 197 | # verifying that it doesn't accept them, so meh. 198 | # Test all numbers around the ones that can show up (around order) 199 | # way smaller and slightly bigger 200 | r = draw( 201 | st.integers(min_value=0, max_value=order << 4) 202 | | st.integers(min_value=order >> 2, max_value=order + 1) 203 | ) 204 | s = draw( 205 | st.integers(min_value=0, max_value=order << 4) 206 | | st.integers(min_value=order >> 2, max_value=order + 1) 207 | ) 208 | 209 | sig = encode_sequence(encode_integer(r), encode_integer(s)) 210 | 211 | return verifying_key, sig 212 | 213 | 214 | @settings(**slow_params) 215 | @given(st_random_der_ecdsa_sig_value()) 216 | def test_random_der_ecdsa_sig_value(params): 217 | """ 218 | Check if random values encoded in ECDSA-Sig-Value structure are rejected 219 | as signature. 220 | """ 221 | verifying_key, sig = params 222 | 223 | with pytest.raises(BadSignatureError): 224 | verifying_key.verify(sig, example_data, sigdecode=sigdecode_der) 225 | 226 | 227 | def st_der_integer(*args, **kwargs): # pragma: no cover 228 | """ 229 | Hypothesis strategy that returns a random positive integer as DER 230 | INTEGER. 231 | Parameters are passed to hypothesis.strategy.integer. 232 | """ 233 | if "min_value" not in kwargs: # pragma: no branch 234 | kwargs["min_value"] = 0 235 | return st.builds(encode_integer, st.integers(*args, **kwargs)) 236 | 237 | 238 | @st.composite 239 | def st_der_bit_string(draw, *args, **kwargs): # pragma: no cover 240 | """ 241 | Hypothesis strategy that returns a random DER BIT STRING. 242 | Parameters are passed to hypothesis.strategy.binary. 243 | """ 244 | data = draw(st.binary(*args, **kwargs)) 245 | if data: 246 | unused = draw(st.integers(min_value=0, max_value=7)) 247 | data = bytearray(data) 248 | data[-1] &= -(2**unused) 249 | data = bytes(data) 250 | else: 251 | unused = 0 252 | return encode_bitstring(data, unused) 253 | 254 | 255 | def st_der_octet_string(*args, **kwargs): # pragma: no cover 256 | """ 257 | Hypothesis strategy that returns a random DER OCTET STRING object. 258 | Parameters are passed to hypothesis.strategy.binary 259 | """ 260 | return st.builds(encode_octet_string, st.binary(*args, **kwargs)) 261 | 262 | 263 | def st_der_null(): # pragma: no cover 264 | """ 265 | Hypothesis strategy that returns DER NULL object. 266 | """ 267 | return st.just(b"\x05\x00") 268 | 269 | 270 | @st.composite 271 | def st_der_oid(draw): # pragma: no cover 272 | """ 273 | Hypothesis strategy that returns DER OBJECT IDENTIFIER objects. 274 | """ 275 | first = draw(st.integers(min_value=0, max_value=2)) 276 | if first < 2: 277 | second = draw(st.integers(min_value=0, max_value=39)) 278 | else: 279 | second = draw(st.integers(min_value=0, max_value=2**512)) 280 | rest = draw( 281 | st.lists(st.integers(min_value=0, max_value=2**512), max_size=50) 282 | ) 283 | return encode_oid(first, second, *rest) 284 | 285 | 286 | def st_der(): # pragma: no cover 287 | """ 288 | Hypothesis strategy that returns random DER structures. 289 | 290 | A valid DER structure is any primitive object, an octet encoding 291 | of a valid DER structure, sequence of valid DER objects or a constructed 292 | encoding of any of the above. 293 | """ 294 | return st.recursive( # pragma: no branch 295 | st.just(b"") 296 | | st_der_integer(max_value=2**4096) 297 | | st_der_bit_string(max_size=1024**2) 298 | | st_der_octet_string(max_size=1024**2) 299 | | st_der_null() 300 | | st_der_oid(), 301 | lambda children: st.builds(encode_octet_string, st.one_of(children)) 302 | | st.builds(lambda x: encode_bitstring(x, 0), st.one_of(children)) 303 | | st.builds( 304 | lambda x: encode_sequence(*x), st.lists(children, max_size=200) 305 | ) 306 | | st.builds( 307 | encode_constructed, 308 | st.integers(min_value=0, max_value=0x3F), 309 | st.one_of(children), 310 | ), 311 | max_leaves=40, 312 | ) 313 | 314 | 315 | @settings(**slow_params) 316 | @given(st.sampled_from(keys_and_sigs), st_der()) 317 | def test_random_der_as_signature(params, der): 318 | """Check if random DER structures are rejected as signature""" 319 | name, verifying_key, _ = params 320 | 321 | with pytest.raises(BadSignatureError): 322 | verifying_key.verify(der, example_data, sigdecode=sigdecode_der) 323 | 324 | 325 | @settings(**slow_params) 326 | @given(st.sampled_from(keys_and_sigs), st.binary(max_size=1024**2)) 327 | @example( 328 | keys_and_sigs[0], encode_sequence(encode_integer(0), encode_integer(0)) 329 | ) 330 | @example( 331 | keys_and_sigs[0], 332 | encode_sequence(encode_integer(1), encode_integer(1)) + b"\x00", 333 | ) 334 | @example(keys_and_sigs[0], encode_sequence(*[encode_integer(1)] * 3)) 335 | def test_random_bytes_as_signature(params, der): 336 | """Check if random bytes are rejected as signature""" 337 | name, verifying_key, _ = params 338 | 339 | with pytest.raises(BadSignatureError): 340 | verifying_key.verify(der, example_data, sigdecode=sigdecode_der) 341 | 342 | 343 | keys_and_string_sigs = [ 344 | ( 345 | name, 346 | verifying_key, 347 | sigencode_string( 348 | *sigdecode_der(sig, verifying_key.curve.order), 349 | order=verifying_key.curve.order 350 | ), 351 | ) 352 | for name, verifying_key, sig in keys_and_sigs 353 | if not isinstance(verifying_key.curve.curve, CurveEdTw) 354 | ] 355 | """ 356 | Name of the curve+hash combination, VerifyingKey and signature as a 357 | byte string. 358 | """ 359 | 360 | 361 | keys_and_string_sigs += [ 362 | ( 363 | name, 364 | verifying_key, 365 | sig, 366 | ) 367 | for name, verifying_key, sig in keys_and_sigs 368 | if isinstance(verifying_key.curve.curve, CurveEdTw) 369 | ] 370 | 371 | 372 | @settings(**slow_params) 373 | @given(st_fuzzed_sig(keys_and_string_sigs)) 374 | def test_fuzzed_string_signatures(params): 375 | verifying_key, sig = params 376 | 377 | with pytest.raises(BadSignatureError): 378 | verifying_key.verify(sig, example_data, sigdecode=sigdecode_string) 379 | -------------------------------------------------------------------------------- /src/ecdsa/test_numbertheory.py: -------------------------------------------------------------------------------- 1 | import operator 2 | from functools import reduce 3 | import sys 4 | 5 | try: 6 | import unittest2 as unittest 7 | except ImportError: 8 | import unittest 9 | import hypothesis.strategies as st 10 | import pytest 11 | from hypothesis import given, settings, example 12 | 13 | try: 14 | from hypothesis import HealthCheck 15 | 16 | HC_PRESENT = True 17 | except ImportError: # pragma: no cover 18 | HC_PRESENT = False 19 | from .numbertheory import ( 20 | SquareRootError, 21 | JacobiError, 22 | factorization, 23 | gcd, 24 | lcm, 25 | jacobi, 26 | inverse_mod, 27 | is_prime, 28 | next_prime, 29 | smallprimes, 30 | square_root_mod_prime, 31 | ) 32 | 33 | try: 34 | from gmpy2 import mpz 35 | except ImportError: 36 | try: 37 | from gmpy import mpz 38 | except ImportError: 39 | 40 | def mpz(x): 41 | return x 42 | 43 | 44 | BIGPRIMES = ( 45 | 999671, 46 | 999683, 47 | 999721, 48 | 999727, 49 | 999749, 50 | 999763, 51 | 999769, 52 | 999773, 53 | 999809, 54 | 999853, 55 | 999863, 56 | 999883, 57 | 999907, 58 | 999917, 59 | 999931, 60 | 999953, 61 | 999959, 62 | 999961, 63 | 999979, 64 | 999983, 65 | ) 66 | 67 | 68 | @pytest.mark.parametrize( 69 | "prime, next_p", [(p, q) for p, q in zip(BIGPRIMES[:-1], BIGPRIMES[1:])] 70 | ) 71 | def test_next_prime(prime, next_p): 72 | assert next_prime(prime) == next_p 73 | 74 | 75 | @pytest.mark.parametrize("val", [-1, 0, 1]) 76 | def test_next_prime_with_nums_less_2(val): 77 | assert next_prime(val) == 2 78 | 79 | 80 | @pytest.mark.slow 81 | @pytest.mark.parametrize("prime", smallprimes) 82 | def test_square_root_mod_prime_for_small_primes(prime): 83 | squares = set() 84 | for num in range(0, 1 + prime // 2): 85 | sq = num * num % prime 86 | squares.add(sq) 87 | root = square_root_mod_prime(sq, prime) 88 | # tested for real with TestNumbertheory.test_square_root_mod_prime 89 | assert root * root % prime == sq 90 | 91 | for nonsquare in range(0, prime): 92 | if nonsquare in squares: 93 | continue 94 | with pytest.raises(SquareRootError): 95 | square_root_mod_prime(nonsquare, prime) 96 | 97 | 98 | def test_square_root_mod_prime_for_2(): 99 | a = square_root_mod_prime(1, 2) 100 | assert a == 1 101 | 102 | 103 | def test_square_root_mod_prime_for_small_prime(): 104 | root = square_root_mod_prime(98**2 % 101, 101) 105 | assert root * root % 101 == 9 106 | 107 | 108 | def test_square_root_mod_prime_for_p_congruent_5(): 109 | p = 13 110 | assert p % 8 == 5 111 | 112 | root = square_root_mod_prime(3, p) 113 | assert root * root % p == 3 114 | 115 | 116 | def test_square_root_mod_prime_for_p_congruent_5_large_d(): 117 | p = 29 118 | assert p % 8 == 5 119 | 120 | root = square_root_mod_prime(4, p) 121 | assert root * root % p == 4 122 | 123 | 124 | class TestSquareRootModPrime(unittest.TestCase): 125 | def test_power_of_2_p(self): 126 | with self.assertRaises(JacobiError): 127 | square_root_mod_prime(12, 32) 128 | 129 | def test_no_square(self): 130 | with self.assertRaises(SquareRootError) as e: 131 | square_root_mod_prime(12, 31) 132 | 133 | self.assertIn("no square root", str(e.exception)) 134 | 135 | def test_non_prime(self): 136 | with self.assertRaises(SquareRootError) as e: 137 | square_root_mod_prime(12, 33) 138 | 139 | self.assertIn("p is not prime", str(e.exception)) 140 | 141 | def test_non_prime_with_negative(self): 142 | with self.assertRaises(SquareRootError) as e: 143 | square_root_mod_prime(697 - 1, 697) 144 | 145 | self.assertIn("p is not prime", str(e.exception)) 146 | 147 | 148 | @st.composite 149 | def st_two_nums_rel_prime(draw): 150 | # 521-bit is the biggest curve we operate on, use 1024 for a bit 151 | # of breathing space 152 | mod = draw(st.integers(min_value=2, max_value=2**1024)) 153 | num = draw( 154 | st.integers(min_value=1, max_value=mod - 1).filter( 155 | lambda x: gcd(x, mod) == 1 156 | ) 157 | ) 158 | return num, mod 159 | 160 | 161 | @st.composite 162 | def st_primes(draw, *args, **kwargs): 163 | if "min_value" not in kwargs: # pragma: no branch 164 | kwargs["min_value"] = 1 165 | prime = draw( 166 | st.sampled_from(smallprimes) 167 | | st.integers(*args, **kwargs).filter(is_prime) 168 | ) 169 | return prime 170 | 171 | 172 | @st.composite 173 | def st_num_square_prime(draw): 174 | prime = draw(st_primes(max_value=2**1024)) 175 | num = draw(st.integers(min_value=0, max_value=1 + prime // 2)) 176 | sq = num * num % prime 177 | return sq, prime 178 | 179 | 180 | @st.composite 181 | def st_comp_with_com_fac(draw): 182 | """ 183 | Strategy that returns lists of numbers, all having a common factor. 184 | """ 185 | primes = draw( 186 | st.lists(st_primes(max_value=2**512), min_size=1, max_size=10) 187 | ) 188 | # select random prime(s) that will make the common factor of composites 189 | com_fac_primes = draw( 190 | st.lists(st.sampled_from(primes), min_size=1, max_size=20) 191 | ) 192 | com_fac = reduce(operator.mul, com_fac_primes, 1) 193 | 194 | # select at most 20 lists (returned numbers), 195 | # each having at most 30 primes (factors) including none (then the number 196 | # will be 1) 197 | comp_primes = draw( # pragma: no branch 198 | st.integers(min_value=1, max_value=20).flatmap( 199 | lambda n: st.lists( 200 | st.lists(st.sampled_from(primes), max_size=30), 201 | min_size=1, 202 | max_size=n, 203 | ) 204 | ) 205 | ) 206 | 207 | return [reduce(operator.mul, nums, 1) * com_fac for nums in comp_primes] 208 | 209 | 210 | @st.composite 211 | def st_comp_no_com_fac(draw): 212 | """ 213 | Strategy that returns lists of numbers that don't have a common factor. 214 | """ 215 | primes = draw( 216 | st.lists( 217 | st_primes(max_value=2**512), min_size=2, max_size=10, unique=True 218 | ) 219 | ) 220 | # first select the primes that will create the uncommon factor 221 | # between returned numbers 222 | uncom_fac_primes = draw( 223 | st.lists( 224 | st.sampled_from(primes), 225 | min_size=1, 226 | max_size=len(primes) - 1, 227 | unique=True, 228 | ) 229 | ) 230 | uncom_fac = reduce(operator.mul, uncom_fac_primes, 1) 231 | 232 | # then build composites from leftover primes 233 | leftover_primes = [i for i in primes if i not in uncom_fac_primes] 234 | 235 | assert leftover_primes 236 | assert uncom_fac_primes 237 | 238 | # select at most 20 lists, each having at most 30 primes 239 | # selected from the leftover_primes list 240 | number_primes = draw( # pragma: no branch 241 | st.integers(min_value=1, max_value=20).flatmap( 242 | lambda n: st.lists( 243 | st.lists(st.sampled_from(leftover_primes), max_size=30), 244 | min_size=1, 245 | max_size=n, 246 | ) 247 | ) 248 | ) 249 | 250 | numbers = [reduce(operator.mul, nums, 1) for nums in number_primes] 251 | 252 | insert_at = draw(st.integers(min_value=0, max_value=len(numbers))) 253 | numbers.insert(insert_at, uncom_fac) 254 | return numbers 255 | 256 | 257 | HYP_SETTINGS = {} 258 | if HC_PRESENT: # pragma: no branch 259 | HYP_SETTINGS["suppress_health_check"] = [ 260 | HealthCheck.filter_too_much, 261 | HealthCheck.too_slow, 262 | ] 263 | # the factorization() sometimes takes a long time to finish 264 | HYP_SETTINGS["deadline"] = 5000 265 | 266 | if "--fast" in sys.argv: # pragma: no cover 267 | HYP_SETTINGS["max_examples"] = 20 268 | 269 | 270 | HYP_SLOW_SETTINGS = dict(HYP_SETTINGS) 271 | if "--fast" in sys.argv: # pragma: no cover 272 | HYP_SLOW_SETTINGS["max_examples"] = 1 273 | else: 274 | HYP_SLOW_SETTINGS["max_examples"] = 20 275 | 276 | 277 | class TestIsPrime(unittest.TestCase): 278 | def test_very_small_prime(self): 279 | assert is_prime(23) 280 | 281 | def test_very_small_composite(self): 282 | assert not is_prime(22) 283 | 284 | def test_small_prime(self): 285 | assert is_prime(123456791) 286 | 287 | def test_special_composite(self): 288 | assert not is_prime(10261) 289 | 290 | def test_medium_prime_1(self): 291 | # nextPrime[2^256] 292 | assert is_prime(2**256 + 0x129) 293 | 294 | def test_medium_prime_2(self): 295 | # nextPrime(2^256+0x129) 296 | assert is_prime(2**256 + 0x12D) 297 | 298 | def test_medium_trivial_composite(self): 299 | assert not is_prime(2**256 + 0x130) 300 | 301 | def test_medium_non_trivial_composite(self): 302 | assert not is_prime(2**256 + 0x12F) 303 | 304 | def test_large_prime(self): 305 | # nextPrime[2^2048] 306 | assert is_prime(mpz(2) ** 2048 + 0x3D5) 307 | 308 | def test_pseudoprime_base_19(self): 309 | assert not is_prime(1543267864443420616877677640751301) 310 | 311 | def test_pseudoprime_base_300(self): 312 | # F. Arnault "Constructing Carmichael Numbers Which Are Strong 313 | # Pseudoprimes to Several Bases". Journal of Symbolic 314 | # Computation. 20 (2): 151-161. doi:10.1006/jsco.1995.1042. 315 | # Section 4.4 Large Example (a pseudoprime to all bases up to 316 | # 300) 317 | p = int( 318 | "29 674 495 668 685 510 550 154 174 642 905 332 730 " 319 | "771 991 799 853 043 350 995 075 531 276 838 753 171 " 320 | "770 199 594 238 596 428 121 188 033 664 754 218 345 " 321 | "562 493 168 782 883".replace(" ", "") 322 | ) 323 | 324 | assert is_prime(p) 325 | for _ in range(10): 326 | if not is_prime(p * (313 * (p - 1) + 1) * (353 * (p - 1) + 1)): 327 | break 328 | else: 329 | assert False, "composite not detected" 330 | 331 | 332 | class TestNumbertheory(unittest.TestCase): 333 | def test_gcd(self): 334 | assert gcd(3 * 5 * 7, 3 * 5 * 11, 3 * 5 * 13) == 3 * 5 335 | assert gcd([3 * 5 * 7, 3 * 5 * 11, 3 * 5 * 13]) == 3 * 5 336 | assert gcd(3) == 3 337 | 338 | @unittest.skipUnless( 339 | HC_PRESENT, 340 | "Hypothesis 2.0.0 can't be made tolerant of hard to " 341 | "meet requirements (like `is_prime()`), the test " 342 | "case times-out on it", 343 | ) 344 | @settings(**HYP_SLOW_SETTINGS) 345 | @example([877 * 1151, 877 * 1009]) 346 | @given(st_comp_with_com_fac()) 347 | def test_gcd_with_com_factor(self, numbers): 348 | n = gcd(numbers) 349 | assert 1 in numbers or n != 1 350 | for i in numbers: 351 | assert i % n == 0 352 | 353 | @unittest.skipUnless( 354 | HC_PRESENT, 355 | "Hypothesis 2.0.0 can't be made tolerant of hard to " 356 | "meet requirements (like `is_prime()`), the test " 357 | "case times-out on it", 358 | ) 359 | @settings(**HYP_SLOW_SETTINGS) 360 | @example([1151, 1069, 1009]) 361 | @given(st_comp_no_com_fac()) 362 | def test_gcd_with_uncom_factor(self, numbers): 363 | n = gcd(numbers) 364 | assert n == 1 365 | 366 | @settings(**HYP_SLOW_SETTINGS) 367 | @given( 368 | st.lists( 369 | st.integers(min_value=1, max_value=2**8192), 370 | min_size=1, 371 | max_size=20, 372 | ) 373 | ) 374 | def test_gcd_with_random_numbers(self, numbers): 375 | n = gcd(numbers) 376 | for i in numbers: 377 | # check that at least it's a divider 378 | assert i % n == 0 379 | 380 | def test_lcm(self): 381 | assert lcm(3, 5 * 3, 7 * 3) == 3 * 5 * 7 382 | assert lcm([3, 5 * 3, 7 * 3]) == 3 * 5 * 7 383 | assert lcm(3) == 3 384 | 385 | @settings(**HYP_SLOW_SETTINGS) 386 | @given( 387 | st.lists( 388 | st.integers(min_value=1, max_value=2**8192), 389 | min_size=1, 390 | max_size=20, 391 | ) 392 | ) 393 | def test_lcm_with_random_numbers(self, numbers): 394 | n = lcm(numbers) 395 | for i in numbers: 396 | assert n % i == 0 397 | 398 | @unittest.skipUnless( 399 | HC_PRESENT, 400 | "Hypothesis 2.0.0 can't be made tolerant of hard to " 401 | "meet requirements (like `is_prime()`), the test " 402 | "case times-out on it", 403 | ) 404 | @settings(**HYP_SLOW_SETTINGS) 405 | @given(st_num_square_prime()) 406 | def test_square_root_mod_prime(self, vals): 407 | square, prime = vals 408 | 409 | calc = square_root_mod_prime(square, prime) 410 | assert calc * calc % prime == square 411 | 412 | @pytest.mark.slow 413 | @settings(**HYP_SLOW_SETTINGS) 414 | @given(st.integers(min_value=1, max_value=10**12)) 415 | @example(265399 * 1526929) 416 | @example(373297**2 * 553991) 417 | def test_factorization(self, num): 418 | factors = factorization(num) 419 | mult = 1 420 | for i in factors: 421 | mult *= i[0] ** i[1] 422 | assert mult == num 423 | 424 | def test_factorisation_smallprimes(self): 425 | exp = 101 * 103 426 | assert 101 in smallprimes 427 | assert 103 in smallprimes 428 | factors = factorization(exp) 429 | mult = 1 430 | for i in factors: 431 | mult *= i[0] ** i[1] 432 | assert mult == exp 433 | 434 | def test_factorisation_not_smallprimes(self): 435 | exp = 1231 * 1237 436 | assert 1231 not in smallprimes 437 | assert 1237 not in smallprimes 438 | factors = factorization(exp) 439 | mult = 1 440 | for i in factors: 441 | mult *= i[0] ** i[1] 442 | assert mult == exp 443 | 444 | def test_jacobi_with_zero(self): 445 | assert jacobi(0, 3) == 0 446 | 447 | def test_jacobi_with_one(self): 448 | assert jacobi(1, 3) == 1 449 | 450 | @settings(**HYP_SLOW_SETTINGS) 451 | @given(st.integers(min_value=3, max_value=1000).filter(lambda x: x % 2)) 452 | def test_jacobi(self, mod): 453 | mod = mpz(mod) 454 | if is_prime(mod): 455 | squares = set() 456 | for root in range(1, mod): 457 | root = mpz(root) 458 | assert jacobi(root * root, mod) == 1 459 | squares.add(root * root % mod) 460 | for i in range(1, mod): 461 | if i not in squares: 462 | i = mpz(i) 463 | assert jacobi(i, mod) == -1 464 | else: 465 | factors = factorization(mod) 466 | for a in range(1, mod): 467 | c = 1 468 | for i in factors: 469 | c *= jacobi(a, i[0]) ** i[1] 470 | assert c == jacobi(a, mod) 471 | 472 | @settings(**HYP_SLOW_SETTINGS) 473 | @given(st_two_nums_rel_prime()) 474 | def test_inverse_mod(self, nums): 475 | num, mod = nums 476 | 477 | inv = inverse_mod(num, mod) 478 | 479 | assert 0 < inv < mod 480 | assert num * inv % mod == 1 481 | 482 | def test_inverse_mod_with_zero(self): 483 | assert 0 == inverse_mod(0, 11) 484 | -------------------------------------------------------------------------------- /src/ecdsa/test_sha3.py: -------------------------------------------------------------------------------- 1 | try: 2 | import unittest2 as unittest 3 | except ImportError: 4 | import unittest 5 | import pytest 6 | 7 | try: 8 | from gmpy2 import mpz 9 | 10 | GMPY = True 11 | except ImportError: # pragma: no cover 12 | try: 13 | from gmpy import mpz 14 | 15 | GMPY = True 16 | except ImportError: 17 | GMPY = False 18 | 19 | from ._sha3 import shake_256 20 | from ._compat import bytes_to_int, int_to_bytes 21 | 22 | B2I_VECTORS = [ 23 | (b"\x00\x01", "big", 1), 24 | (b"\x00\x01", "little", 0x0100), 25 | (b"", "big", 0), 26 | (b"\x00", "little", 0), 27 | ] 28 | 29 | 30 | @pytest.mark.parametrize("bytes_in,endian,int_out", B2I_VECTORS) 31 | def test_bytes_to_int(bytes_in, endian, int_out): 32 | out = bytes_to_int(bytes_in, endian) 33 | assert out == int_out 34 | 35 | 36 | class TestBytesToInt(unittest.TestCase): 37 | def test_bytes_to_int_wrong_endian(self): 38 | with self.assertRaises(ValueError): 39 | bytes_to_int(b"\x00", "middle") 40 | 41 | def test_int_to_bytes_wrong_endian(self): 42 | with self.assertRaises(ValueError): 43 | int_to_bytes(0, byteorder="middle") 44 | 45 | 46 | @pytest.mark.skipif(GMPY == False, reason="requires gmpy or gmpy2") 47 | def test_int_to_bytes_with_gmpy(): 48 | assert int_to_bytes(mpz(1)) == b"\x01" 49 | 50 | 51 | I2B_VECTORS = [ 52 | (0, None, "big", b""), 53 | (0, 1, "big", b"\x00"), 54 | (1, None, "big", b"\x01"), 55 | (0x0100, None, "little", b"\x00\x01"), 56 | (0x0100, 4, "little", b"\x00\x01\x00\x00"), 57 | (1, 4, "big", b"\x00\x00\x00\x01"), 58 | ] 59 | 60 | 61 | @pytest.mark.parametrize("int_in,length,endian,bytes_out", I2B_VECTORS) 62 | def test_int_to_bytes(int_in, length, endian, bytes_out): 63 | out = int_to_bytes(int_in, length, endian) 64 | assert out == bytes_out 65 | 66 | 67 | SHAKE_256_VECTORS = [ 68 | ( 69 | b"Message.", 70 | 32, 71 | b"\x78\xa1\x37\xbb\x33\xae\xe2\x72\xb1\x02\x4f\x39\x43\xe5\xcf\x0c" 72 | b"\x4e\x9c\x72\x76\x2e\x34\x4c\xf8\xf9\xc3\x25\x9d\x4f\x91\x2c\x3a", 73 | ), 74 | ( 75 | b"", 76 | 32, 77 | b"\x46\xb9\xdd\x2b\x0b\xa8\x8d\x13\x23\x3b\x3f\xeb\x74\x3e\xeb\x24" 78 | b"\x3f\xcd\x52\xea\x62\xb8\x1b\x82\xb5\x0c\x27\x64\x6e\xd5\x76\x2f", 79 | ), 80 | ( 81 | b"message", 82 | 32, 83 | b"\x86\x16\xe1\xe4\xcf\xd8\xb5\xf7\xd9\x2d\x43\xd8\x6e\x1b\x14\x51" 84 | b"\xa2\xa6\x5a\xf8\x64\xfc\xb1\x26\xc2\x66\x0a\xb3\x46\x51\xb1\x75", 85 | ), 86 | ( 87 | b"message", 88 | 16, 89 | b"\x86\x16\xe1\xe4\xcf\xd8\xb5\xf7\xd9\x2d\x43\xd8\x6e\x1b\x14\x51", 90 | ), 91 | ( 92 | b"message", 93 | 64, 94 | b"\x86\x16\xe1\xe4\xcf\xd8\xb5\xf7\xd9\x2d\x43\xd8\x6e\x1b\x14\x51" 95 | b"\xa2\xa6\x5a\xf8\x64\xfc\xb1\x26\xc2\x66\x0a\xb3\x46\x51\xb1\x75" 96 | b"\x30\xd6\xba\x2a\x46\x65\xf1\x9d\xf0\x62\x25\xb1\x26\xd1\x3e\xed" 97 | b"\x91\xd5\x0d\xe7\xb9\xcb\x65\xf3\x3a\x46\xae\xd3\x6c\x7d\xc5\xe8", 98 | ), 99 | ( 100 | b"A" * 1024, 101 | 32, 102 | b"\xa5\xef\x7e\x30\x8b\xe8\x33\x64\xe5\x9c\xf3\xb5\xf3\xba\x20\xa3" 103 | b"\x5a\xe7\x30\xfd\xbc\x33\x11\xbf\x83\x89\x50\x82\xb4\x41\xe9\xb3", 104 | ), 105 | ] 106 | 107 | 108 | @pytest.mark.parametrize("msg,olen,ohash", SHAKE_256_VECTORS) 109 | def test_shake_256(msg, olen, ohash): 110 | out = shake_256(msg, olen) 111 | assert out == bytearray(ohash) 112 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | 2 | [tox] 3 | envlist = py26, py27, py35, py36, py37, py38, py39, py310, py311, py312, py313, py, pypy, pypy3, gmpy2py27, gmpy2py39, gmpy2py310, gmpypy27, gmpypy39, gmpypy310, codechecks 4 | 5 | [testenv] 6 | deps = 7 | py{26}: unittest2 8 | py{26}: hypothesis<3 9 | py{26,27,35,36,37,38,39,310,311,312,313,py,py3}: pytest 10 | py{27,35,36,37,38,39,310,311,312,313,py,py3}: hypothesis 11 | gmpy2py{27,39,310,311,312,313}: gmpy2 12 | gmpypy{27,39,310,311,312,313}: gmpy 13 | gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: pytest 14 | gmpy{2py27,2py39,2py310,2py311,2py312,2py313,py27,py39,py310,py311,py312,py313}: hypothesis 15 | # six==1.9.0 comes from setup.py install_requires 16 | py27_old_six: six==1.9.0 17 | py27_old_six: pytest 18 | py27_old_six: hypothesis 19 | # those are the oldest versions of gmpy and gmpy2 on PyPI (i.e. oldest we can 20 | # actually test), older versions may work, but are not easy to test 21 | py27_old_gmpy: gmpy==1.15 22 | py27_old_gmpy: pytest 23 | py27_old_gmpy: hypothesis 24 | py27_old_gmpy2: gmpy2==2.0.1 25 | py27_old_gmpy2: pytest 26 | py27_old_gmpy2: hypothesis 27 | py: pytest 28 | py: hypothesis 29 | coverage 30 | commands = coverage run --branch -m pytest {posargs:src/ecdsa} 31 | 32 | [testenv:py27_old_gmpy] 33 | basepython = python2.7 34 | 35 | [testenv:py27_old_gmpy2] 36 | basepython = python2.7 37 | 38 | [testenv:py27_old_six] 39 | basepython = python2.7 40 | 41 | [testenv:gmpypy27] 42 | basepython=python2.7 43 | 44 | [testenv:gmpypy39] 45 | basepython=python3.9 46 | 47 | [testenv:gmpypy310] 48 | basepython=python3.10 49 | 50 | [testenv:gmpypy311] 51 | basepython=python3.11 52 | 53 | [testenv:gmpypy312] 54 | basepython=python3.12 55 | 56 | [testenv:gmpypy313] 57 | basepython=python3.13 58 | 59 | [testenv:gmpy2py27] 60 | basepython=python2.7 61 | 62 | [testenv:gmpy2py39] 63 | basepython=python3.9 64 | 65 | [testenv:gmpy2py310] 66 | basepython=python3.10 67 | 68 | [testenv:gmpy2py311] 69 | basepython=python3.11 70 | 71 | [testenv:gmpy2py312] 72 | basepython=python3.12 73 | 74 | [testenv:gmpy2py313] 75 | basepython=python3.13 76 | 77 | [testenv:instrumental] 78 | basepython = python2.7 79 | deps = 80 | gmpy2 81 | instrumental 82 | hypothesis 83 | pytest>=4.6.0 84 | coverage 85 | six 86 | commands = 87 | instrumental -t ecdsa -i '.*test_.*|.*_version|.*_compat|.*_sha3' {envbindir}/pytest {posargs:src/ecdsa} 88 | instrumental -f .instrumental.cov -sr 89 | 90 | [testenv:coverage] 91 | sitepackages=True 92 | whitelist_externals=coverage 93 | commands = 94 | coverage run --branch -m pytest --hypothesis-show-statistics {posargs:src/ecdsa} 95 | coverage html 96 | coverage report -m 97 | 98 | [testenv:speed] 99 | commands = {envpython} speed.py 100 | 101 | [testenv:speedgmpy] 102 | deps = gmpy 103 | commands = {envpython} speed.py 104 | 105 | [testenv:speedgmpy2] 106 | deps = gmpy2 107 | commands = {envpython} speed.py 108 | 109 | [testenv:codechecks] 110 | basepython = python3 111 | deps = 112 | black==22.3.0 113 | flake8==6.1.0 114 | commands = 115 | flake8 setup.py speed.py src 116 | black --check --line-length 79 . 117 | 118 | [testenv:codeformat] 119 | basepython = python3 120 | deps = 121 | black==22.3.0 122 | commands = 123 | black --line-length 79 . 124 | 125 | [flake8] 126 | exclude = src/ecdsa/test*.py 127 | # We're just getting started. For now, ignore the following problems: 128 | # E203: whitespace before ':' (this needs to be ignored for black compatibility) 129 | # E741: ambiguous variable name 130 | extend-ignore = E203,E741 131 | --------------------------------------------------------------------------------