├── .coveragerc ├── .deepsource.toml ├── .github └── workflows │ ├── docs.yml │ ├── lint.yml │ ├── perf.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── codecov.yml ├── docs ├── .gitignore ├── Makefile ├── _static │ ├── .gitignore │ ├── custom.css │ ├── logo.svg │ ├── logo_black.png │ ├── logo_black.svg │ ├── logo_black_full.png │ ├── logo_black_full.svg │ ├── logo_black_small.png │ ├── logo_white.png │ ├── logo_white_small.png │ └── pyecsca_ches24.pdf ├── _templates │ └── nav.html ├── api.rst ├── conf.py ├── index.rst ├── installation.rst ├── libraries.rst ├── libraries │ ├── bearssl.rst │ ├── boringssl.rst │ ├── botan.rst │ ├── bouncy_castle.rst │ ├── fastecdsa.rst │ ├── go_crypto.rst │ ├── ipp_crypto.rst │ ├── libgcrypt.rst │ ├── libressl.rst │ ├── libsecp256k1.rst │ ├── libtomcrypt.rst │ ├── mbedtls.rst │ ├── micro_ecc.rst │ ├── microsoft_symcrypt.rst │ ├── nettle.rst │ ├── nss.rst │ ├── openssl.rst │ ├── sunec.rst │ └── wolfssl.rst ├── notebook ├── notebooks.rst ├── papers.rst └── references.rst ├── pyecsca ├── ec │ ├── __init__.py │ ├── configuration.py │ ├── context.py │ ├── coordinates.py │ ├── countermeasures.py │ ├── curve.py │ ├── data │ │ └── formulas │ │ │ ├── add-bc-r1rv76-jac │ │ │ ├── add-bc-r1rv76-jac.op3 │ │ │ ├── add-bc-r1rv76-mod │ │ │ ├── add-bc-r1rv76-mod.op3 │ │ │ ├── add-bearssl-v06 │ │ │ ├── add-bearssl-v06.op3 │ │ │ ├── add-boringssl-p224 │ │ │ ├── add-boringssl-p224.op3 │ │ │ ├── add-gecc-322 │ │ │ ├── add-gecc-322.op3 │ │ │ ├── add-libgcrypt-v1102 │ │ │ ├── add-libgcrypt-v1102.op3 │ │ │ ├── add-libressl-v382 │ │ │ ├── add-libressl-v382.op3 │ │ │ ├── add-openssl-z256 │ │ │ ├── add-openssl-z256.op3 │ │ │ ├── add-openssl-z256a │ │ │ ├── add-openssl-z256a.op3 │ │ │ ├── add-sunec-v21 │ │ │ ├── add-sunec-v21-ed25519 │ │ │ ├── add-sunec-v21-ed25519.op3 │ │ │ ├── add-sunec-v21.op3 │ │ │ ├── dbl-bc-r1rv76-jac │ │ │ ├── dbl-bc-r1rv76-jac.op3 │ │ │ ├── dbl-bc-r1rv76-mod │ │ │ ├── dbl-bc-r1rv76-mod.op3 │ │ │ ├── dbl-bc-r1rv76-x25519 │ │ │ ├── dbl-bc-r1rv76-x25519.op3 │ │ │ ├── dbl-bearssl-v06 │ │ │ ├── dbl-bearssl-v06.op3 │ │ │ ├── dbl-boringssl-p224 │ │ │ ├── dbl-boringssl-p224.op3 │ │ │ ├── dbl-gecc-321 │ │ │ ├── dbl-gecc-321.op3 │ │ │ ├── dbl-hacl-x25519 │ │ │ ├── dbl-hacl-x25519.op3 │ │ │ ├── dbl-ipp-x25519 │ │ │ ├── dbl-ipp-x25519.op3 │ │ │ ├── dbl-libgcrypt-v1102 │ │ │ ├── dbl-libgcrypt-v1102.op3 │ │ │ ├── dbl-libressl-v382 │ │ │ ├── dbl-libressl-v382.op3 │ │ │ ├── dbl-secp256k1-v040 │ │ │ ├── dbl-secp256k1-v040.op3 │ │ │ ├── dbl-sunec-v21 │ │ │ ├── dbl-sunec-v21-ed25519 │ │ │ ├── dbl-sunec-v21-ed25519.op3 │ │ │ ├── dbl-sunec-v21.op3 │ │ │ ├── ladd-bc-r1rv76-x25519 │ │ │ ├── ladd-bc-r1rv76-x25519.op3 │ │ │ ├── ladd-boringssl-x25519 │ │ │ ├── ladd-boringssl-x25519.op3 │ │ │ ├── ladd-botan-x25519 │ │ │ ├── ladd-botan-x25519.op3 │ │ │ ├── ladd-go-1214 │ │ │ ├── ladd-go-1214.op3 │ │ │ ├── ladd-hacl-x25519 │ │ │ ├── ladd-hacl-x25519.op3 │ │ │ ├── ladd-openssl-x25519 │ │ │ ├── ladd-openssl-x25519.op3 │ │ │ ├── ladd-rfc7748 │ │ │ ├── ladd-rfc7748.op3 │ │ │ ├── madd-secp256k1-v040 │ │ │ └── madd-secp256k1-v040.op3 │ ├── divpoly.py │ ├── error.py │ ├── formula │ │ ├── __init__.py │ │ ├── base.py │ │ ├── code.py │ │ ├── efd.py │ │ ├── expand.py │ │ ├── fake.py │ │ ├── fliparoo.py │ │ ├── graph.py │ │ ├── metrics.py │ │ ├── partitions.py │ │ ├── switch_sign.py │ │ └── unroll.py │ ├── key_agreement.py │ ├── key_generation.py │ ├── mod │ │ ├── __init__.py │ │ ├── base.py │ │ ├── flint.py │ │ ├── gmp.py │ │ ├── raw.py │ │ └── symbolic.py │ ├── model.py │ ├── mult │ │ ├── __init__.py │ │ ├── base.py │ │ ├── binary.py │ │ ├── comb.py │ │ ├── fake.py │ │ ├── fixed.py │ │ ├── ladder.py │ │ ├── naf.py │ │ └── window.py │ ├── op.py │ ├── params.py │ ├── point.py │ ├── scalar.py │ ├── signature.py │ └── transformations.py ├── misc │ ├── __init__.py │ ├── cache.py │ ├── cfg.py │ └── utils.py └── sca │ ├── __init__.py │ ├── attack │ ├── CPA.py │ ├── DPA.py │ ├── __init__.py │ └── leakage_model.py │ ├── re │ ├── __init__.py │ ├── base.py │ ├── rpa.py │ ├── structural.py │ ├── tree.py │ └── zvp.py │ ├── scope │ ├── __init__.py │ ├── base.py │ ├── chipwhisperer.py │ ├── picoscope_alt.py │ └── picoscope_sdk.py │ ├── stacked_traces │ ├── __init__.py │ ├── combine.py │ └── stacked_traces.py │ ├── target │ ├── ISO7816.py │ ├── PCSC.py │ ├── __init__.py │ ├── base.py │ ├── binary.py │ ├── chipwhisperer.py │ ├── ectester.py │ ├── flash.py │ ├── leakage.py │ ├── leia.py │ ├── serial.py │ └── simpleserial.py │ ├── trace │ ├── __init__.py │ ├── align.py │ ├── combine.py │ ├── edit.py │ ├── filter.py │ ├── match.py │ ├── plot.py │ ├── process.py │ ├── sampling.py │ ├── test.py │ └── trace.py │ └── trace_set │ ├── __init__.py │ ├── base.py │ ├── chipwhisperer.py │ ├── hdf5.py │ ├── inspector.py │ └── pickle.py ├── pyproject.toml ├── setup.py └── test ├── __init__.py ├── conftest.py ├── data ├── __init__.py ├── divpoly │ ├── __init__.py │ └── mult_21.json ├── ec │ ├── __init__.py │ ├── curve.json │ ├── curves.json │ ├── ecdh_tv.json │ ├── ecdsa_tv.json │ ├── ecgen_secp128r1.json │ └── ectester_secp128r1.csv └── sca │ ├── __init__.py │ ├── chipwhisperer_keylist.npy │ ├── chipwhisperer_knownkey.npy │ ├── chipwhisperer_settings.cwset │ ├── chipwhisperer_textin.npy │ ├── chipwhisperer_textout.npy │ ├── chipwhisperer_traces.npy │ ├── config_chipwhisperer_.cfg │ ├── example.trs │ ├── target.py │ ├── test.h5 │ └── test.pickle ├── ec ├── __init__.py ├── bench_divpoly.py ├── conftest.py ├── perf_formula.py ├── perf_mod.py ├── perf_mult.py ├── test_configuration.py ├── test_context.py ├── test_countermeasures.py ├── test_curve.py ├── test_divpoly.py ├── test_formula.py ├── test_key_agreement.py ├── test_key_generation.py ├── test_mod.py ├── test_model.py ├── test_mult.py ├── test_op.py ├── test_params.py ├── test_pickle.py ├── test_point.py ├── test_regress.py ├── test_scalar.py ├── test_signature.py ├── test_transformations.py └── utils.py ├── misc ├── __init__.py └── test_utils.py ├── plots ├── .gitignore └── plot_perf.py ├── sca ├── __init__.py ├── conftest.py ├── perf_combine.py ├── perf_stacked_combine.py ├── perf_zvp.py ├── test_align.py ├── test_attacks.py ├── test_combine.py ├── test_edit.py ├── test_filter.py ├── test_leakage_models.py ├── test_match.py ├── test_plot.py ├── test_process.py ├── test_rpa.py ├── test_rpa_context.py ├── test_sampling.py ├── test_scope.py ├── test_stacked_combine.py ├── test_stacked_traces.py ├── test_target.py ├── test_test.py ├── test_trace.py ├── test_traceset.py ├── test_tree.py └── test_zvp.py └── utils.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | test/* 5 | virt/* 6 | setup.py 7 | fakesource 8 | 9 | [report] 10 | exclude_lines = 11 | __repr__ 12 | __str__ 13 | pragma: no cover 14 | raise AssertionError 15 | raise NotImplementedError 16 | if 0: 17 | if __name__ == .__main__.: 18 | \.\.\.$ 19 | -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | test_patterns = ["test/**"] 4 | 5 | [[analyzers]] 6 | name = "python" 7 | enabled = true 8 | 9 | [analyzers.meta] 10 | runtime_version = "3.x.x" 11 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | LLVM_CONFIG: /usr/bin/llvm-config-10 10 | PS_PACKAGES: libps4000 libps5000 libps6000 11 | GMP_PACKAGES: libgmp-dev libmpfr-dev libmpc-dev 12 | FLINT_PACKAGES: libflint-dev 13 | OTHER_PACKAGES: pandoc swig gcc libpcsclite-dev llvm-14 libllvm14 llvm-14-dev libpari-dev pari-gp pari-seadata gcc-arm-none-eabi binutils-arm-none-eabi libnewlib-arm-none-eabi 14 | 15 | jobs: 16 | docs: 17 | runs-on: ubuntu-latest 18 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 19 | permissions: 20 | pages: write # to deploy to Pages 21 | id-token: write # to verify the deployment originates from an appropriate source 22 | # Deploy to the github-pages environment 23 | environment: 24 | name: github-pages 25 | url: ${{ steps.deployment.outputs.page_url }} 26 | # Do the build and deploy 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | submodules: true 31 | path: pyecsca 32 | - uses: actions/checkout@v4 33 | with: 34 | repository: J08nY/pyecsca-codegen 35 | submodules: true 36 | path: pyecsca-codegen 37 | - uses: actions/cache@v4 38 | with: 39 | path: ~/.cache/pip 40 | key: pip-${{ runner.os }}-3.11-${{ hashFiles('pyproject.toml') }} 41 | restore-keys: | 42 | pip-${{ runner.os }}- 43 | - name: Setup Python 3.11 44 | uses: actions/setup-python@v5 45 | with: 46 | python-version: "3.11" 47 | - name: Add picoscope repository 48 | run: | 49 | curl "https://labs.picotech.com/debian/dists/picoscope/Release.gpg.key" | sudo apt-key add 50 | sudo echo "deb https://labs.picotech.com/debian/ picoscope main" | sudo tee /etc/apt/sources.list.d/picoscope.list 51 | sudo apt-get update 52 | - name: Install system dependencies 53 | run: | 54 | sudo apt-get install -y $PS_PACKAGES $OTHER_PACKAGES $GMP_PACKAGES $FLINT_PACKAGES 55 | - name: Install picoscope bindings 56 | run: | 57 | python -m pip install -U pip setuptools wheel 58 | git clone https://github.com/colinoflynn/pico-python && cd pico-python && pip install . && cd .. 59 | git clone https://github.com/picotech/picosdk-python-wrappers && cd picosdk-python-wrappers && pip install . && cd .. 60 | - name: Install pyecsca 61 | working-directory: pyecsca 62 | run: | 63 | pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, gmp, flint, leia, test, dev, doc]" 64 | - name: Install pyecsca-codegen 65 | working-directory: pyecsca-codegen 66 | run: | 67 | pip install -e "." 68 | - name: Build docs 69 | working-directory: pyecsca/docs 70 | run: | 71 | make apidoc 72 | make html 73 | - uses: actions/upload-pages-artifact@v3.0.1 74 | with: 75 | path: pyecsca/docs/_build/html/ 76 | - id: deployment 77 | uses: actions/deploy-pages@v4 78 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | LLVM_CONFIG: /usr/bin/llvm-config-14 7 | PS_PACKAGES: libps4000 libps5000 libps6000 8 | GMP_PACKAGES: libgmp-dev libmpfr-dev libmpc-dev 9 | FLINT_PACKAGES: libflint-dev 10 | OTHER_PACKAGES: swig gcc libpcsclite-dev llvm-14 libllvm14 llvm-14-dev libpari-dev pari-gp pari-seadata 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | - uses: actions/cache@v4 20 | with: 21 | path: ~/.cache/pip 22 | key: pip-${{ runner.os }}-3.11-${{ hashFiles('pyproject.toml') }} 23 | restore-keys: | 24 | pip-${{ runner.os }}- 25 | - name: Setup Python 3.11 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.11" 29 | - name: Add picoscope repository 30 | run: | 31 | curl "https://labs.picotech.com/debian/dists/picoscope/Release.gpg.key" | sudo apt-key add 32 | sudo echo "deb https://labs.picotech.com/debian/ picoscope main" | sudo tee /etc/apt/sources.list.d/picoscope.list 33 | sudo apt-get update 34 | - name: Install system dependencies 35 | run: | 36 | sudo apt-get install -y $PS_PACKAGES $OTHER_PACKAGES $GMP_PACKAGES $FLINT_PACKAGES 37 | - name: Install picoscope bindings 38 | run: | 39 | python -m pip install -U pip setuptools wheel 40 | git clone https://github.com/colinoflynn/pico-python && cd pico-python && pip install . && cd .. 41 | git clone https://github.com/picotech/picosdk-python-wrappers && cd picosdk-python-wrappers && pip install . && cd .. 42 | - name: Install dependencies 43 | run: | 44 | pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, gmp, flint, leia, test, dev]" 45 | - name: Typecheck 46 | run: | 47 | make typecheck 48 | - name: Codestyle 49 | run: | 50 | make codestyle-all 51 | - name: Documentation coverage 52 | run: | 53 | make doc-coverage 54 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | LLVM_CONFIG: /usr/bin/llvm-config-14 7 | PS_PACKAGES: libps4000 libps5000 libps6000 8 | GMP_PACKAGES: libgmp-dev libmpfr-dev libmpc-dev 9 | FLINT_PACKAGES: libflint-dev 10 | OTHER_PACKAGES: swig gcc libpcsclite-dev llvm-14 libllvm14 llvm-14-dev libpari-dev pari-gp pari-seadata 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: ["3.9", "3.10", "3.11", "3.12"] 18 | mod: ["python", "gmp", "flint"] 19 | env: 20 | PYTHON: ${{ matrix.python-version }} 21 | MOD_IMPL: ${{ matrix.mod }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: true 26 | - uses: actions/cache@v4 27 | with: 28 | path: ~/.cache/pip 29 | key: pip-${{ runner.os }}-${{ matrix.mod }}-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} 30 | restore-keys: | 31 | pip-${{ runner.os }}-${{ matrix.mod }}-${{ matrix.python-version }}- 32 | pip-${{ runner.os }}-${{ matrix.mod }}- 33 | pip-${{ runner.os }}- 34 | - name: Setup Python ${{ matrix.python-version }} 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | - name: Add picoscope repository 39 | run: | 40 | curl "https://labs.picotech.com/debian/dists/picoscope/Release.gpg.key" | sudo apt-key add 41 | sudo echo "deb https://labs.picotech.com/debian/ picoscope main" | sudo tee /etc/apt/sources.list.d/picoscope.list 42 | sudo apt-get update 43 | - name: Install system dependencies 44 | run: | 45 | sudo apt-get install -y $PS_PACKAGES $OTHER_PACKAGES 46 | if [ $MOD_IMPL == "gmp" ]; then sudo apt-get install -y $GMP_PACKAGES; fi 47 | if [ $MOD_IMPL == "flint" ]; then sudo apt-get install -y $FLINT_PACKAGES; fi 48 | - name: Install picoscope bindings 49 | run: | 50 | python -m pip install -U pip setuptools wheel 51 | git clone https://github.com/colinoflynn/pico-python && cd pico-python && pip install . && cd .. 52 | git clone https://github.com/picotech/picosdk-python-wrappers && cd picosdk-python-wrappers && pip install . && cd .. 53 | - name: Install 54 | run: | 55 | if [ $MOD_IMPL == "gmp" ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, pari, leia, gmp, test, dev]"; fi 56 | if [ $MOD_IMPL == "flint" ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, pari, leia, flint, test, dev]"; fi 57 | if [ $MOD_IMPL == "python" ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, pari, leia, test, dev]"; fi 58 | - name: Test 59 | run: | 60 | make test 61 | - name: Test notebooks 62 | run: | 63 | make test-notebooks 64 | - name: Code coverage 65 | uses: codecov/codecov-action@v4 66 | if: ${{ matrix.python-version == 3.9 }} 67 | with: 68 | env_vars: PYTHON,USE_GMP 69 | token: ${{ secrets.CODECOV_TOKEN }} 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.trs 2 | 3 | pyecsca.egg-info 4 | .perf/ 5 | .coverage 6 | .mypy_cache/ 7 | /.pytest_cache/ 8 | /htmlcov/ 9 | /build/ 10 | /dist/ 11 | /Pipfile.lock 12 | __pycache__/ 13 | /venv 14 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pyecsca/ec/std"] 2 | path = pyecsca/ec/std 3 | url = https://github.com/J08nY/std-curves 4 | branch = data 5 | [submodule "notebook"] 6 | path = notebook 7 | url = https://github.com/J08nY/pyecsca-notebook 8 | [submodule "pyecsca/ec/efd"] 9 | path = pyecsca/ec/efd 10 | url = https://github.com/J08nY/efd 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: mixed-line-ending 10 | - id: check-ast 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | - repo: https://github.com/pre-commit/mirrors-mypy 14 | rev: v1.15.0 15 | hooks: 16 | - id: mypy 17 | additional_dependencies: 18 | - "types-setuptools" 19 | - "numpy" 20 | - "gmpy2" 21 | - "python-flint" 22 | args: [--ignore-missing-imports, --show-error-codes, --namespace-packages, --explicit-package-bases, --check-untyped-defs] 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: 7.1.2 25 | hooks: 26 | - id: flake8 27 | args: ["--extend-ignore=E501,F405,F403,F401,E126,E203"] 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 Jan Jancar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | graft pyecsca/ec/efd/ 3 | graft pyecsca/ec/std/ 4 | graft pyecsca/ec/data/ 5 | prune .github/ 6 | prune docs/ 7 | prune test/ 8 | exclude .coveragerc .deepsource.toml .pre-commit-config.yml codecov.yml 9 | global-exclude .git* 10 | exclude pyecsca/ec/efd/README.md pyecsca/ec/std/README.md 11 | prune pyecsca/ec/std/.github/ 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PERF_SCRIPTS = test.ec.perf_mod test.ec.perf_formula test.ec.perf_mult test.sca.perf_combine test.sca.perf_zvp 2 | 3 | all: help 4 | 5 | test: 6 | pytest -m "not slow" --cov=pyecsca 7 | 8 | test-plots: 9 | env PYECSCA_TEST_PLOTS=1 pytest -m "not slow" 10 | 11 | test-all: 12 | pytest --cov=pyecsca 13 | 14 | test-notebooks: 15 | pytest -m "not slow" --nbmake --cov=pyecsca --cov-append notebook/simulation.ipynb notebook/visualization.ipynb 16 | 17 | typecheck: 18 | mypy --namespace-packages -p pyecsca --ignore-missing-imports --show-error-codes --check-untyped-defs 19 | 20 | typecheck-all: 21 | mypy --namespace-packages -p pyecsca -p test --ignore-missing-imports --show-error-codes --check-untyped-defs 22 | 23 | codestyle: 24 | flake8 --extend-ignore=E501,F405,F403,F401,E126,E203 pyecsca 25 | 26 | codestyle-all: 27 | flake8 --extend-ignore=E501,F405,F403,F401,E126,E203 pyecsca test 28 | 29 | docstyle: 30 | pydocstyle pyecsca --ignore=D1,D203,D212 -e --count 31 | 32 | black: 33 | black --exclude pyecsca/ec/{std,efd} pyecsca 34 | 35 | black-all: 36 | black --exclude pyecsca/ec/{std,efd} pyecsca test 37 | 38 | perf: 39 | mkdir -p .perf 40 | echo ${PERF_SCRIPTS}| env DIR=".perf" xargs -n 1 python -m 41 | 42 | perf-plots: 43 | python test/plots/plot_perf.py -d .perf 44 | 45 | doc-coverage: 46 | interrogate -c pyproject.toml -vv -nmps -f 55 pyecsca 47 | 48 | docs: 49 | $(MAKE) -C docs apidoc 50 | $(MAKE) -C docs html 51 | 52 | help: 53 | @echo "pyecsca, Python Elliptic Curve cryptography Side-Channel Analysis toolkit." 54 | @echo 55 | @echo "Available targets:" 56 | @echo " - test: Test pyecsca." 57 | @echo " - test-plots: Test pyecsca and produce debugging plots." 58 | @echo " - test-all: Test pyecsca but also run slow (and disabled) tests." 59 | @echo " - typecheck: Use mypy to verify the use of types in pyecsca." 60 | @echo " - typecheck-all: Use mypy to verify the use of types in pyecsca and in tests." 61 | @echo " - codestyle: Use flake8 to check codestyle in pyecsca." 62 | @echo " - codestyle-all: Use flake8 to check codestyle in pyecsca and in tests." 63 | @echo " - docstyle: Use pydocstyle to check format of docstrings." 64 | @echo " - black: Run black on pyecsca sources (will transform them inplace)." 65 | @echo " - black-all: Run black on pyecsca sources and tests (will transform them inplace)." 66 | @echo " - perf: Run performance measurements (prints results and stores them in .perf/)." 67 | @echo " - perf-plots: Plot performance measurements (stores the plots in .perf/)." 68 | @echo " - doc-coverage: Use interrogate to check documentation coverage of public API." 69 | @echo " - docs: Build docs using sphinx." 70 | @echo " - help: Show this help." 71 | 72 | .PHONY: all test test-plots test-all typecheck typecheck-all codestyle codestyle-all docstyle black black-all perf doc-coverage docs 73 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 50..100 3 | round: up 4 | precision: 2 5 | 6 | ignore: 7 | - "test/**/*.py" 8 | - "setup.py" 9 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build/ 2 | api/* 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | @echo " apidoc to build api docs" 14 | 15 | apidoc: 16 | mkdir -p api/ 17 | sphinx-apidoc ../pyecsca/ --implicit-namespaces --ext-autodoc --no-toc -M -f -e -o api/ 18 | sphinx-apidoc ../../pyecsca-codegen/pyecsca/ --implicit-namespaces --ext-autodoc --no-toc -M -f -e -o api/ 19 | rm api/pyecsca.rst #It is jumbled anyway. 20 | 21 | .PHONY: help apidoc Makefile 22 | 23 | # Catch-all target: route all unknown targets to Sphinx using the new 24 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 25 | %: Makefile 26 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 27 | -------------------------------------------------------------------------------- /docs/_static/.gitignore: -------------------------------------------------------------------------------- 1 | font/ 2 | graphik.css 3 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { 2 | font-weight: bold !important; 3 | } 4 | 5 | img.logo { 6 | max-width: 40%; 7 | } 8 | 9 | dl.class, dl.function { 10 | margin-bottom: 2em; 11 | } 12 | 13 | a { 14 | color: #004B6B; 15 | } 16 | 17 | a:visited { 18 | color: #001c63; 19 | } 20 | 21 | dt:target, .highlight { 22 | background: #ffffff; 23 | } 24 | 25 | .bibtex-dropdown summary { 26 | width: 107px; 27 | font-weight: normal !important; 28 | } 29 | 30 | .frame mjx-math { 31 | border: 1px solid #e0e0e0; 32 | padding: 5px; 33 | } 34 | -------------------------------------------------------------------------------- /docs/_static/logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/logo_black.png -------------------------------------------------------------------------------- /docs/_static/logo_black_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/logo_black_full.png -------------------------------------------------------------------------------- /docs/_static/logo_black_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/logo_black_small.png -------------------------------------------------------------------------------- /docs/_static/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/logo_white.png -------------------------------------------------------------------------------- /docs/_static/logo_white_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/logo_white_small.png -------------------------------------------------------------------------------- /docs/_static/pyecsca_ches24.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/docs/_static/pyecsca_ches24.pdf -------------------------------------------------------------------------------- /docs/_templates/nav.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | =============================== 2 | :fas:`code;fa-fw` API reference 3 | =============================== 4 | 5 | .. toctree:: 6 | :titlesonly: 7 | :maxdepth: 3 8 | 9 | api/pyecsca.ec 10 | api/pyecsca.misc 11 | api/pyecsca.sca 12 | api/pyecsca.codegen 13 | -------------------------------------------------------------------------------- /docs/libraries.rst: -------------------------------------------------------------------------------- 1 | ==================================== 2 | :fas:`server;fa-fw` ECC in Libraries 3 | ==================================== 4 | 5 | This page collects information about ECC implementations in open-source software. It was extracted 6 | by source code analysis (i.e. we looked at the code and tried really hard to name what we see), so it 7 | may or may not match the reality (whether due to a mistake or due to different naming choices). 8 | 9 | We restricted ourselves to ECDH/ECDSA (on prime field curves) and X25519/Ed25519. In case libraries contain multiple 10 | implementations we tried to document them clearly, along with the rules on how they pick one 11 | (e.g. curve-based/architecture-based). Naming of scalar multipliers is tricky. Naming of coordinate systems 12 | and formulas is taken from the `EFD `__. 13 | 14 | 15 | .. toctree:: 16 | :titlesonly: 17 | :glob: 18 | :maxdepth: 2 19 | 20 | libraries/* 21 | -------------------------------------------------------------------------------- /docs/libraries/bearssl.rst: -------------------------------------------------------------------------------- 1 | BearSSL 2 | ======= 3 | 4 | | Version: ``v0.6`` 5 | | Repository: https://bearssl.org/gitweb/?p=BearSSL;a=summary 6 | | Docs: https://bearssl.org/index.html 7 | 8 | Primitives 9 | ---------- 10 | 11 | Supports SECG prime field curves, as well as Brainpool and Curve25519, Curve448. 12 | Has API functions for ECDSA, but does ECDH only implicitly in its TLS implementation (no public API exposed). 13 | Unclear whether Ed25519 is supported. 14 | 15 | ECDH 16 | ^^^^ 17 | 18 | KeyGen: 19 | - Short-Weierstrass 20 | - (width=2) Fixed Window via ``br_ec_compute_pub -> impl.mulgen -> impl.mul``, but (width=4) Fixed Window via ``br_ec_compute_pub -> impl.mulgen`` for special (P-256) curves. 21 | - Jacobian coordinates 22 | - Unknown formulas: `add-bearssl-v06 `__, 23 | `dbl-bearssl-v06 `__, 24 | 25 | Derive: 26 | - Short-Weierstrass 27 | - (width=2) Fixed Window via ``impl.mul``. 28 | - Coordinates and formulas same as in KeyGen. 29 | 30 | ECDSA 31 | ^^^^^ 32 | 33 | KeyGen: 34 | - Same as ECDH. 35 | 36 | Sign: 37 | - Short-Weierstrass 38 | - (width=2) Fixed Window via ``br_ecdsa_*_sign_raw -> impl.mulgen -> impl.mul``, but (width=4) Fixed Window via ``br_ecdsa_*_sign_raw -> impl.mulgen`` for special (P-256) curves. 39 | - Coordinates and formulas same as in KeyGen. 40 | 41 | Verify: 42 | - Short-Weierstrass 43 | - Simple scalarmult then add via ``br_ecdsa_*_verify_raw -> impl.muladd -> impl.mul + add`` 44 | - Coordinates and formulas same as in KeyGen. 45 | 46 | x25519 47 | ^^^^^^ 48 | 49 | KeyGen: 50 | - Montgomery 51 | - Montgomery ladder via ``br_ec_compute_pub -> impl.mulgen -> impl.mul``. 52 | - xz coordinates 53 | - ladd-rfc7748 54 | 55 | Derive: 56 | - Same as KeyGen. 57 | -------------------------------------------------------------------------------- /docs/libraries/botan.rst: -------------------------------------------------------------------------------- 1 | Botan 2 | ===== 3 | 4 | | Version: ``3.2.0`` (tag 3.2.0) 5 | | Repository: https://github.com/randombit/botan/ 6 | | Docs: https://botan.randombit.net/handbook/ 7 | 8 | Primitives 9 | ---------- 10 | 11 | Has coordinate and scalar blinding, 12 | 13 | ECDH 14 | ^^^^ 15 | 16 | KeyGen: 17 | - Short-Weierstrass 18 | - `Fixed Window with FullPrecomputation (no doublings) (w=3) `__, via ``blinded_base_point_multiply -> EC_Point_Base_Point_Precompute::mul``. 19 | - `Jacobian `__ 20 | - `add-1998-cmo-2 `__ 21 | 22 | Derive: 23 | - Short-Weierstrass 24 | - Fixed Window (w=4) via ``blinded_var_point_multiply -> EC_Point_Var_Point_Precompute::mul``. 25 | - `Jacobian `__ 26 | - `add-1998-cmo-2 `__, 27 | `dbl-1986-cc `__ 28 | 29 | ECDSA 30 | ^^^^^ 31 | 32 | KeyGen: 33 | - Short-Weierstrass 34 | - `Fixed Window with FullPrecomputation (no doublings) (w=3) `__, via ``blinded_base_point_multiply -> EC_Point_Base_Point_Precompute::mul``. 35 | - `Jacobian `__ 36 | - `add-1998-cmo-2 `__ 37 | 38 | Sign: 39 | - Short-Weierstrass 40 | - `Fixed Window with FullPrecomputation (no doublings) (w=3) `__, via ``blinded_base_point_multiply -> EC_Point_Base_Point_Precompute::mul``. 41 | - `Jacobian `__ 42 | - `add-1998-cmo-2 `__ 43 | 44 | Verify: 45 | - Short-Weierstrass 46 | - Multi-scalar (interleaved) fixed-window via ``ECDSA::verify -> EC_Point_Multi_Point_Precompute::multi_exp``. 47 | - `Jacobian `__ 48 | - `add-1998-cmo-2 `__, 49 | `dbl-1986-cc `__ 50 | 51 | X25519 52 | ^^^^^^ 53 | Based on curve25519_donna. 54 | 55 | - Montgomery 56 | - Montgomery ladder (unrolled several iterations) 57 | - xz 58 | - Unknown formula: `ladd-botan-x25519 `__ 59 | 60 | Ed25519 61 | ^^^^^^^ 62 | Based on ref10 of Ed25519. 63 | See :doc:`boringssl`. 64 | -------------------------------------------------------------------------------- /docs/libraries/fastecdsa.rst: -------------------------------------------------------------------------------- 1 | fastecdsa 2 | ========= 3 | 4 | | Version: ``v2.3.1`` 5 | | Repository: https://github.com/AntonKueltz/fastecdsa/ 6 | | Docs: https://fastecdsa.readthedocs.io/en/latest/index.html 7 | 8 | Primitives 9 | ---------- 10 | 11 | Offers only ECDSA. 12 | Supported `curves `__: all SECP curves (8) for 192-256 bits, all (7) Brainpool curves as well as custom curves. 13 | 14 | 15 | ECDSA 16 | ^^^^^ 17 | 18 | KeyGen: 19 | - Short-Weierstrass 20 | - `Ladder `__ via ``get_public_key -> pointZZ_pMul``. 21 | - Affine and schoolbook `add `__ and `double `__. 22 | 23 | Sign: 24 | - Short-Weierstrass 25 | - Same ladder as Keygen via ``sign``. 26 | 27 | Verify: 28 | - Short-Weierstrass 29 | - `Shamir's trick `__ via ``verify -> pointZZ_pShamirsTrick``. 30 | -------------------------------------------------------------------------------- /docs/libraries/go_crypto.rst: -------------------------------------------------------------------------------- 1 | Go 2 | == 3 | 4 | | Version: ``go1.21.4`` 5 | | Repository: https://github.com/golang/go 6 | | Docs: 7 | 8 | Primitives 9 | ---------- 10 | 11 | ECDH, ECDSA over P-224, P-256, P-384 and P-521. 12 | Ed25519, X25519 13 | 14 | ECDH 15 | ^^^^ 16 | 17 | KeyGen: 18 | - Short-Weierstrass 19 | - `Fixed window (w=4) `__ (link points to P-224, but others are the same) via ``privateKeyToPublicKey -> ScalarBaseMult`` 20 | - Projective 21 | - `add-2015-rcb `__, `dbl-2015-rcb `__ 22 | 23 | Derive: 24 | - Short-Weierstrass 25 | - `Fixed window (w=4) `__ via ``ecdh -> ScalarMult``. 26 | - Same formulas as in Keygen. 27 | 28 | Also supports constant-time, 64-bit assembly implementation of P256 described in https://eprint.iacr.org/2013/816.pdf 29 | 30 | ECDSA 31 | ^^^^^ 32 | 33 | KeyGen: 34 | - Same as ECDH KeyGen via ``ecdsa.go:GenerateKey -> generateNISTEC -> randomPoint -> ScalarBaseMult``. 35 | 36 | Sign: 37 | - Same as KeyGen via ``ecdsa.go:SignASN1 -> signNISTEC -> randomPoint -> ScalarBaseMult``. 38 | 39 | Verify: 40 | - Two separate scalar multiplications ``ScalarBaseMult`` (same as KeyGen) and ``ScalarMult`` (same as ECDH Derive) via ``ecdsa.go:VerifyASN1 -> verifyNISTEC``. 41 | 42 | X25519 43 | ^^^^^^ 44 | 45 | KeyGen: 46 | - Montgomery 47 | - `Ladder `__ via ``privateKeyToPublicKey -> x25519ScalarMult``. 48 | - xz 49 | - Unknown formula: `ladd-go-1214 `__ 50 | 51 | Derive: 52 | - Same as KeyGen via ``x25519.go:ecdh -> x25519ScalarMult``. 53 | 54 | Ed25519 55 | ^^^^^^^ 56 | 57 | KeyGen: 58 | - Twisted-Edwards 59 | - Pippenger's signed 4-bit method with precomputation via ``ed25519.go:GenerateKey -> NewKeyFromSeed -> newKeyFromSeed -> ScalarBaseMult``. 60 | - `Extended coordinates `__ mixed with `y-x,y+x,2dxy `__ coordinates 61 | - `AddAffine `__ (and similar SubAffine):: 62 | 63 | YplusX.Add(&p.y, &p.x) 64 | YminusX.Subtract(&p.y, &p.x) 65 | 66 | PP.Multiply(&YplusX, &q.YplusX) 67 | MM.Multiply(&YminusX, &q.YminusX) 68 | TT2d.Multiply(&p.t, &q.T2d) 69 | 70 | Z2.Add(&p.z, &p.z) 71 | 72 | v.X.Subtract(&PP, &MM) 73 | v.Y.Add(&PP, &MM) 74 | v.Z.Add(&Z2, &TT2d) 75 | v.T.Subtract(&Z2, &TT2d) 76 | 77 | Sign: 78 | - Same as Keygen via ``ed25519.go: Sign -> sign -> ScalarBaseMult``. 79 | 80 | Verify: 81 | - Bos-Coster method via ``ed25519.go: Verify -> verify -> VarTimeDoubleScalarBaseMult``. 82 | - Same coordinates and formulas as in Keygen. 83 | -------------------------------------------------------------------------------- /docs/libraries/libgcrypt.rst: -------------------------------------------------------------------------------- 1 | libgcrypt 2 | ========= 3 | 4 | | Version: ``1.10.2`` 5 | | Repository: https://git.gnupg.org/ 6 | | Docs: https://gnupg.org/documentation/manuals/gcrypt/ 7 | 8 | Primitives 9 | ---------- 10 | 11 | Supports ECDH, X25519 and EdDSA `on `__ C25519, X448, Ed25519, Ed448, NIST curves, Brainpool curves and secp256k1. 12 | Also supports GOST and SM2 signatures. 13 | 14 | ECDH 15 | ^^^^ 16 | 17 | KeyGen: 18 | - Short-Weierstrass 19 | - `Left to right double-and-add-always `__ via ``gcry_pk_genkey -> _gcry_pk_genkey -> generate -> ecc_generate -> nist_generate_key -> _gcry_mpi_ec_mul_point``. 20 | - Jacobian coords 21 | - Unknown formulas: `add-libgcrypt-v1102 `__, 22 | `dbl-libgcrypt-v1102 `__, 23 | 24 | Derive: 25 | - Same as Keygen via ``gcry_pk_encrypt -> _gcry_pk_encrypt -> generate -> ecc_encrypt_raw -> _gcry_mpi_ec_mul_point``. 26 | 27 | 28 | ECDSA 29 | ^^^^^ 30 | 31 | Keygen: 32 | - Same as ECDH. 33 | 34 | Sign: 35 | - Same as Keygen via ``gcry_ecc_ecdsa_sign -> _gcry_ecc_ecdsa_sign -> _gcry_mpi_ec_mul_point``. 36 | 37 | Verify: 38 | - Two separate scalar multiplications via ``gcry_ecc_ecdsa_verify -> _gcry_ecc_ecdsa_verify``. 39 | 40 | EdDSA 41 | ^^^^^ 42 | 43 | Keygen: 44 | - Twisted-Edwards 45 | - `Left to right double-and-add-always `__ via ``gcry_pk_genkey -> _gcry_pk_genkey -> generate -> ecc_generate -> _gcry_ecc_eddsa_genkey -> _gcry_mpi_ec_mul_point``. 46 | - Projective, `dbl-2008-bbjlp `__ and `add-2008-bbjlp `__ 47 | 48 | Sign: 49 | - Same as Keygen via ``gcry_ecc_eddsa_sign -> _gcry_ecc_eddsa_sign -> _gcry_mpi_ec_mul_point``. 50 | 51 | Verify: 52 | - Two separate scalar multiplications via ``gcry_ecc_eddsa_verify -> _gcry_ecc_eddsa_verify``. 53 | 54 | 55 | X25519 56 | ^^^^^^ 57 | 58 | KeyGen: 59 | - Montgomery 60 | - `Montgomery ladder `__ via ``gcry_pk_genkey -> _gcry_pk_genkey -> generate -> ecc_generate -> nist_generate_key -> _gcry_mpi_ec_mul_point``. 61 | - xz coordinates with a shuffled version of `ladd-1987-m-3 `__ 62 | 63 | 64 | Derive: 65 | - Same as Keygen via ``gcry_pk_encrypt -> _gcry_pk_encrypt -> generate -> ecc_encrypt_raw -> _gcry_mpi_ec_mul_point``. 66 | -------------------------------------------------------------------------------- /docs/libraries/libressl.rst: -------------------------------------------------------------------------------- 1 | LibreSSL 2 | ======== 3 | 4 | | Version: ``v3.8.2`` 5 | | Repository: https://github.com/libressl/portable 6 | | Docs: 7 | 8 | Primitives 9 | ---------- 10 | 11 | Supports ECDH, ECDSA as well as x25519 and Ed25519. 12 | 13 | ECDH 14 | ^^^^ 15 | 16 | KeyGen: 17 | - Short-Weierstrass 18 | - `Simple Ladder `__ via ``kmethod.keygen -> ec_key_gen -> EC_POINT_mul -> method.mul_generator_ct -> ec_GFp_simple_mul_generator_ct -> ec_GFp_simple_mul_ct``. 19 | Also does coordinate blinding and fixes scalar bit-length. 20 | - Jacobian coordinates. 21 | - Unknown formulas: `add-libressl-v382 `__, 22 | `dbl-libressl-v382 `__ 23 | 24 | Derive: 25 | - Short-Weierstrass 26 | - `Simple Ladder `__ via ``kmethod.compute_key -> ecdh_compute_key -> EC_POINT_mul -> method.mul_single_ct -> ec_GFp_simple_mul_single_ct -> ec_GFp_simple_mul_ct``. 27 | Also does coordinate blinding and fixes scalar bit-length. 28 | - Same as KeyGen. 29 | 30 | 31 | ECDSA 32 | ^^^^^ 33 | 34 | KeyGen: 35 | - Same as ECDH. 36 | 37 | Sign: 38 | - Short-Weierstrass 39 | - `Simple Ladder `__ via ``ECDSA_sign -> kmethod.sign -> ecdsa_sign -> ECDSA_do_sign -> kmethod.sign_sig -> ecdsa_sign_sig -> ECDSA_sign_setup -> kmethod.sign_setup -> ecdsa_sign_setup -> EC_POINT_mul -> method.mul_generator_ct -> ec_GFp_simple_mul_generator_ct -> ec_GFp_simple_mul_ct``. 40 | - Same as ECDH. 41 | 42 | Verify: 43 | - Short-Weierstrass 44 | - Window NAF interleaving multi-exponentiation method ``ECDSA_verify -> kmethod.verify -> ecdsa_verify -> ECDSA_do_verify -> kmethod.verify_sig -> ecdsa_verify_sig -> EC_POINT_mul -> method.mul_double_nonct -> ec_GFp_simple_mul_double_nonct -> ec_wNAF_mul``. 45 | Refers to http://www.informatik.tu-darmstadt.de/TI/Mitarbeiter/moeller.html#multiexp and https://www.informatik.tu-darmstadt.de/TI/Mitarbeiter/moeller.html#fastexp 46 | - Same coordinates and formulas as ECDH. 47 | 48 | 49 | X25519 50 | ^^^^^^ 51 | Based on ref10 of Ed25519. 52 | See :doc:`boringssl`. Not exactly the same. 53 | 54 | Ed25519 55 | ^^^^^^^ 56 | Based on ref10 of Ed25519. 57 | See :doc:`boringssl`. Not exactly the same. 58 | -------------------------------------------------------------------------------- /docs/libraries/libsecp256k1.rst: -------------------------------------------------------------------------------- 1 | libsecp256k1 2 | ============ 3 | 4 | | Version: ``v0.4.0`` 5 | | Repository: https://github.com/bitcoin-core/secp256k1 6 | | Docs: 7 | 8 | Primitives 9 | ---------- 10 | 11 | Supports ECDSA, ECDH and Schnorr signatures over secp256k1. 12 | 13 | ECDH 14 | ^^^^ 15 | 16 | KeyGen: 17 | - Short-Weierstrass 18 | - `Fixed window with full precomputation `__ via ``secp256k1_ec_pubkey_create -> secp256k1_ec_pubkey_create_helper -> secp256k1_ecmult_gen``. Window of size 4. 19 | - Uses scalar blinding. 20 | - `Jacobian version of add-2002-bj `__ (via ``secp256k1_gej_add_ge``). 21 | - No doubling. 22 | 23 | 24 | Derive: 25 | - Uses GLV decomposition and `interleaving with width-5 NAFs `__ via ``secp256k1_ecdh -> secp256k1_ecmult_const``. 26 | - Addition same as in Keygen. 27 | - Unknown doubling: `dbl-secp256k1-v040 `__ (via `secp256k1_gej_double `__) 28 | 29 | ECDSA 30 | ^^^^^ 31 | 32 | Keygen: 33 | - Same as ECDH. 34 | 35 | Sign: 36 | - Same as Keygen via ``secp256k1_ecdsa_sign -> secp256k1_ecdsa_sign_inner -> secp256k1_ecdsa_sig_sign -> secp256k1_ecmult_gen``. 37 | 38 | Verify: 39 | - Split both scalars using GLV and then interleaving with width-5 NAFS on 4 scalars via ``secp256k1_ecdsa_verify -> secp256k1_ecdsa_sig_verify -> secp256k1_ecmult -> secp256k1_ecmult_strauss_wnaf``. 40 | - DBL same as in ECDH DERIVE. Two formulas for addition are implemented. For the generator part, same addition as in Keygen is used. For public key, the following:: 41 | 42 | assume iZ2 = 1/Z2 43 | az = Z_1*iZ2 44 | Z12 = az^2 45 | u1 = X1 46 | u2 = X2*Z12 47 | s1 = Y1 48 | s2 = Y2*Z12 49 | s2 = s2*az 50 | h = -u1 51 | h = h+u2 52 | i = -s2 53 | i = i+s1 54 | Z3 = Z1*h 55 | h2 = h^2 56 | h2 = -h2 57 | h3 = h2*h 58 | t = u1*h2 59 | X3 = i^2 60 | X3 = X3+h3 61 | X3 = X3+t 62 | X3 = X3+t 63 | t = t+X3 64 | Y3 = t*i 65 | h3 = h3*s1 66 | Y3 = Y3+h3 67 | 68 | - Before the addition the Jacobian coordinates are mapped to an isomorphic curve. 69 | -------------------------------------------------------------------------------- /docs/libraries/libtomcrypt.rst: -------------------------------------------------------------------------------- 1 | libtomcrypt 2 | =========== 3 | 4 | | Version: ``v1.18.2`` 5 | | Repository: https://github.com/libtom/libtomcrypt/ 6 | | Docs: 7 | 8 | Primitives 9 | ---------- 10 | 11 | Offers ECDH and ECDSA on the `curves `__: SECP112r1, SECP128r1, SECP160r1, P-192, P-224, P-256, P-384, P-521. 12 | 13 | ECDH 14 | ^^^^ 15 | 16 | KeyGen: 17 | - Short-Weierstrass 18 | - `Simple ladder `__ via ``ecc_make_key -> ecc_make_key_ex -> ecc_ptmul -> ltc_ecc_mulmod_timing``. 19 | - jacobian, `dbl-1998-hnm `__ via ltc_ecc_projective_dbl_point 20 | - jacobian, `add-1998-hnm `__ via ltc_ecc_projective_add_point 21 | 22 | Derive: 23 | - Same as Keygen via ``ecc_shared_secret -> ecc_ptmul -> ltc_ecc_mulmod_timing``. 24 | 25 | ECDSA 26 | ^^^^^ 27 | 28 | Keygen: 29 | - Same as ECDH. 30 | 31 | Sign: 32 | - Same as Keygen via ``ecc_sign_hash -> _ecc_sign_hash -> ecc_make_key_ex``. 33 | 34 | Verify: 35 | - `Shamir's trick `__ via ``ecc_verify_hash -> _ecc_verify_hash -> ecc_mul2add`` or two separate sliding windows. 36 | - Same coords and formulas as KeyGen. 37 | -------------------------------------------------------------------------------- /docs/libraries/mbedtls.rst: -------------------------------------------------------------------------------- 1 | mbedTLS 2 | ======= 3 | 4 | | Version: ``3.5.1`` 5 | | Repository: https://github.com/Mbed-TLS/mbedtls 6 | | Docs: https://mbed-tls.readthedocs.io/en/latest/index.html 7 | 8 | Primitives 9 | ---------- 10 | 11 | ECDH and ECDSA on P192, P224, P256, P384, P521 (their R and K variants) as well 12 | as x25519 and x448. 13 | 14 | x25519 has two implementations, and mbedTLS one (described below) and `one `__ from 15 | `Project Everest `__. 16 | 17 | ECDH 18 | ^^^^ 19 | 20 | KeyGen: 21 | - Short-Weierstrass 22 | - `Comb `__ via ``mbedtls_ecdh_gen_public -> ecdh_gen_public_restartable -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_comb``. 23 | w = 5 for curves < 384 bits, then w = 6. 24 | - `Jacobian `__ coords with coordinate randomization. 25 | - `add-gecc-322 [GECC]_ algorithm 3.22 `__, `dbl-1998-cmo-2 `__. Also has alternative impl (``_ALT``). 26 | 27 | Derive: 28 | - Short-Weierstrass 29 | - `Comb `__ via ``mbedtls_ecdh_compute_shared -> ecdh_compute_shared_restartable -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_comb``. 30 | w = 4 for curves < 384 bits, then w = 5. The width is smaller by 1 than the case when the generator point is used (in KeyGen). 31 | - Same coords and formulas as KeyGen. 32 | 33 | ECDSA 34 | ^^^^^ 35 | 36 | KeyGen: 37 | - Short-Weierstrass 38 | - `Comb `__ via ``mbedtls_ecdsa_genkey -> mbedtls_ecp_gen_keypair -> mbedtls_ecp_gen_keypair_base -> mbedtls_ecp_mul -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_comb``. 39 | - Same as ECDH (KeyGen). 40 | 41 | Sign: 42 | - Short-Weierstrass 43 | - `Comb `__ via ``mbedtls_ecdsa_sign -> mbedtls_ecdsa_sign_restartable -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_comb``. 44 | - Same as ECDH (KeyGen). 45 | 46 | Verify: 47 | - Short-Weierstrass 48 | - `Comb `__ + `Comb `__ via ``mbedtls_ecdsa_verify -> mbedtls_ecdsa_verify_restartable -> mbedtls_ecp_muladd_restartable -> mbedtls_ecp_mul_shortcuts + mbedtls_ecp_mul_shortcuts -> ecp_mul_restartable_internal -> ecp_mul_comb``. 49 | - Same as ECDH (KeyGen, Derive). 50 | 51 | x25519 52 | ^^^^^^ 53 | 54 | KeyGen: 55 | - Montgomery 56 | - `Montgomery Ladder `__ via ``mbedtls_ecdh_gen_public -> ecdh_gen_public_restartable -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_mxz``. 57 | - `xz `__ coords. 58 | - `mladd-1987-m `__. 59 | 60 | Derive: 61 | - Montgomery 62 | - `Montgomery Ladder `__ via ``mbedtls_ecdh_compute_shared -> ecdh_compute_shared_restartable -> mbedtls_ecp_mul_restartable -> ecp_mul_restartable_internal -> ecp_mul_mxz``. 63 | - Same as KeyGen. 64 | -------------------------------------------------------------------------------- /docs/libraries/micro_ecc.rst: -------------------------------------------------------------------------------- 1 | micro-ecc 2 | ========= 3 | 4 | | Version: ``v1.1`` 5 | | Repository: https://github.com/kmackay/micro-ecc/ 6 | | Docs: 7 | 8 | Primitives 9 | ---------- 10 | 11 | Offers ECDH and ECDSA on secp160r1, secp192r1, secp224r1, secp256r1, and secp256k1. 12 | 13 | ECDH 14 | ^^^^ 15 | 16 | KeyGen: 17 | - Short-Weierstrass 18 | - `Ladder (coZ, with subtraction) `__ via ``uECC_make_key -> EccPoint_compute_public_key -> EccPoint_mult`` (also has coordinate randomization). 19 | - `Jacobian coZ coordinates (Z1 == Z2) `__ from https://eprint.iacr.org/2011/338.pdf. 20 | - `coZ formulas `__ from https://eprint.iacr.org/2011/338.pdf. 21 | 22 | Derive: 23 | - Short-Weierstrass 24 | - `Ladder (coZ, with subtraction) `__ via ``uECC_shared_secret -> EccPoint_compute_public_key -> EccPoint_mult`` (also has coordinate randomization). 25 | - Same coords and formulas as KeyGen. 26 | 27 | ECDSA 28 | ^^^^^ 29 | 30 | Keygen: 31 | - Same as ECDH. 32 | 33 | Sign: 34 | - Short-Weierstrass 35 | - `Ladder (coZ, with subtraction) `__ via ``uECC_sign -> uECC_sign_with_k_internal -> EccPoint_mult`` (also has coordinate randomization). 36 | - Same coords and formulas as KeyGen. 37 | 38 | Verify: 39 | - Short-Weierstrass 40 | - `Shamir's trick `__ via ``uECC_verify``. 41 | - Same coords and formulas as KeyGen. 42 | -------------------------------------------------------------------------------- /docs/libraries/nettle.rst: -------------------------------------------------------------------------------- 1 | Nettle 2 | ====== 3 | 4 | | Version: ``3.9.1`` 5 | | Repository: https://git.lysator.liu.se/nettle/nettle 6 | | Docs: https://www.lysator.liu.se/~nisse/nettle/nettle.html 7 | 8 | Primitives 9 | ---------- 10 | 11 | ECDSA on P192, P224, P256, P384 and P521, also EdDSA on Curve25519, Curve448. 12 | 13 | .. csv-table:: Pippenger parameters 14 | :header: "Curve", "K", "C" 15 | 16 | P192, 8, 6 17 | P224, 16, 7 18 | P256, 11, 6 19 | P384, 32, 6 20 | P521, 44, 6 21 | Curve25519, 11, 6 22 | 23 | ECDSA 24 | ^^^^^ 25 | 26 | KeyGen: 27 | - Short-Weierstrass 28 | - `Pippenger `__ via ``ecdsa_generate_keypair -> ecc_curve.mul_g -> ecc_mul_g``. 29 | - Jacobian 30 | - `madd-2007-bl `__, `dbl-2001-b `__ 31 | 32 | Sign: 33 | - Short-Weierstrass 34 | - `Pippenger `__ via ``ecc_ecdsa_sign -> ecc_mul_g``. 35 | - Same as KeyGen. 36 | 37 | 38 | Verify: 39 | - Short-Weierstrass 40 | - `Pippenger `__ and `4-bit Fixed Window `__ via ``ecc_ecdsa_verify -> ecc_mul_a + ecc_mul_g``. 41 | - Jacobian 42 | - `madd-2007-bl `__, `dbl-2001-b `__, 43 | also `add-2007-bl `__. 44 | 45 | Ed25519 46 | ^^^^^^^ 47 | 48 | KeyGen: 49 | - Twisted Edwards 50 | - `Pippenger `__ via ``ed25519_sha512_public_key -> _eddsa_public_key -> ecc_curve.mul_g -> ecc_mul_g_eh``. 51 | - Projective 52 | - `madd-2008-bbjlp `__, `add-2008-bbjlp `__ and `dup-2008-bbjlp `__. 53 | 54 | Sign: 55 | - Twisted Edwards 56 | - `Pippenger `__ via ``ed25519_sha512_sign -> _eddsa_sign -> ecc_curve.mul_g -> ecc_mul_g_eh``. 57 | - Same as KeyGen. 58 | 59 | Verify: 60 | - Twisted Edwards 61 | - `Pippenger `__ and `4-bit Fixed Window `__ via ``ed25519_sha512_verify -> _eddsa_verify -> ecc_curve.mul + ecc_curve.mul_g``. 62 | - Same as KeyGen. 63 | -------------------------------------------------------------------------------- /docs/libraries/nss.rst: -------------------------------------------------------------------------------- 1 | NSS 2 | === 3 | 4 | | Version: ``3.94`` 5 | | Repository: https://hg.mozilla.org/projects/nss 6 | | Docs: 7 | 8 | 9 | Primitives 10 | ---------- 11 | 12 | ECDH, ECDSA (only standard curves P-256, P-384, P-521), also x25519. 13 | 14 | Two ECMethods: 15 | - Curve25519 16 | - 32-bit -> own impl 17 | - 64-bit -> HACL* 18 | - P-256 from HACL* 19 | 20 | Several ECGroups: 21 | - generic ``ECGroup_consGFp`` 22 | - Montgomery arithmetic ``ECGroup_consGFp_mont`` 23 | - P-256 24 | - P-384 from ECCkiila 25 | - P-521 from ECCkiila 26 | 27 | The ECMethods override the scalarmult of the ECGroups in: 28 | - ``ec_NewKey`` via ``ec_get_method_from_name`` and then calling the ``method.mul``. 29 | - ``EC_ValidatePublicKey`` via ``ec_get_method_from_name`` and then calling the ``method.validate``. 30 | - ``ECDH_Derive`` via ``ec_get_method_from_name`` and then calling the ``method.mul``. 31 | - ``ECDSA_SignDigest`` and ``ECDSA_SignDigestWithSeed`` via ``ec_SignDigestWithSeed``, then ``ec_get_method_from_name`` and then calling the ``method.mul``. 32 | 33 | 34 | P-256 from HACL* 35 | ^^^^^^^^^^^^^^^^ 36 | 37 | KeyGen: 38 | - Short-Weierstrass 39 | - Fixed Window (width = 4)? points to https://eprint.iacr.org/2013/816.pdf? via ``ec_secp256r1_pt_mul -> (Hacl*) Hacl_P256_dh_initiator -> point_mul_g`` 40 | - projective-3 coords. 41 | - `add-2015-rcb`, `dbl-2015-rcb-3` 42 | 43 | Derive: 44 | - Same as KeyGen. 45 | 46 | Sign: 47 | - Same as Keygen. 48 | 49 | Verify: 50 | - Short-Weierstrass 51 | - Multi-scalar simultaneous Fixed Window 52 | - Same coords and formulas as KeyGen. 53 | 54 | P-384 55 | ^^^^^ 56 | 57 | KeyGen: 58 | - Short-Weierstrass 59 | - Comb from ecckiila: ``EC_NewKeyFromSeed -> ec_NewKey -> ec_points_mul -> ECPoints_mul -> ecgroup.points_mul -> point_mul_two_secp384r1_wrap -> point_mul_g_secp384r1_wrap -> point_mul_g_secp384r1 -> fixed_smul_cmb``. 60 | - projective-3 coords. 61 | - `dbl-2015-rcb-3`, `madd-2015-rcb-3` also `add-2015-rcb` in point_add_proj. 62 | 63 | Derive: 64 | - Short-Weierstrass 65 | - Regular Window NAF (width = 5) from ecckiila: ``ECDH_Derive -> ec_points_mul -> ECPoints_mul -> ecgroup.points_mul -> point_mul_secp384r1_wrap -> point_mul_secp384r1 -> var_smul_rwnaf``. 66 | - projective-3 coords. 67 | - `dbl-2015-rcb-3`, `add-2015-rcb`. 68 | 69 | Sign: 70 | - Same as KeyGen. 71 | 72 | Verify: 73 | - Short-Weierstrass 74 | - Interleaved multi-scalar window NAF (width = 5) with Shamir's trick from ecckiila: ``ECDSA_SignDigest -> ECDSA_SignDigestWithSeed -> ec_SignDigestWithSeed -> ec_points_mul -> ECPoints_mul -> ecgroup.points_mul -> point_mul_two_secp384r1_wrap -> point_mul_two_secp384r1 -> var_smul_wnaf_two`` 75 | - projective-3 coords. 76 | - `dbl-2015-rcb-3`, `madd-2015-rcb-3` also `add-2015-rcb` in point_add_proj. 77 | 78 | P-521 79 | ^^^^^ 80 | 81 | Same as P-384. 82 | 83 | x25519 84 | ^^^^^^ 85 | 86 | KeyGen: 87 | - Montgomery 88 | - Montgomery ladder via ``-> ec_Curve25519_pt_mul -> ec_Curve25519_mul``. 89 | - xz coords 90 | - Unknown formulas: `ladd-hacl-x25519 `__, 91 | `dbl-hacl-x25519 `__ 92 | 93 | Derive: 94 | - Same as KeyGen. 95 | -------------------------------------------------------------------------------- /docs/libraries/sunec.rst: -------------------------------------------------------------------------------- 1 | SunEC 2 | ===== 3 | 4 | | Version: ``jdk-21-ga`` (JDK 21) 5 | | Repository: https://github.com/openjdk/jdk/ 6 | | Docs: 7 | 8 | 9 | Primitives 10 | ---------- 11 | 12 | ECDH, ECDSA, x25519, Ed25519 13 | 14 | P-256 15 | ^^^^^ 16 | 17 | The only special thing is the generator scalarmult, ``Secp256R1GeneratorMultiplier`` which is a Comb. 18 | 19 | ECDH 20 | ^^^^ 21 | 22 | KeyGen: 23 | - Short-Weierstrass 24 | - Fixed Window (width = 4) via ``ECKeyPairGenerator.generateKeyPair -> ECKeyPairGenerator.generateKeyPairImpl -> ECPrivateKeyImpl.calculatePublicKey -> ECOperations.multiply -> Default(PointMultiplier).pointMultiply`` 25 | - projective-3 coords 26 | - RCB-based formulas: `add-sunec-v21 `__, 27 | `dbl-sunec-v21 `__, 28 | 29 | 30 | Derive: 31 | - Same as KeyGen. 32 | 33 | ECDSA 34 | ^^^^^ 35 | 36 | Same as ECDH. 37 | 38 | x25519 39 | ^^^^^^ 40 | 41 | KeyGen: 42 | - Montgomery 43 | - Montgomery ladder 44 | - xz 45 | - Ladder formula from RFC 7748 46 | 47 | Derive: 48 | - Same as KeyGen. 49 | 50 | Ed25519 51 | ^^^^^^^ 52 | 53 | KeyGen: 54 | - Twisted-Edwards 55 | - Double and add always 56 | - Extended coords 57 | - Unknown formulas: `add-sunec-v21-ed25519 `__, `dbl-sunec-v21-ed25519 `__ 58 | 59 | Sign: 60 | - Same as KeyGen. 61 | 62 | Verify: 63 | - Same as KeyGen. 64 | -------------------------------------------------------------------------------- /docs/libraries/wolfssl.rst: -------------------------------------------------------------------------------- 1 | wolfSSL 2 | ======= 3 | -------------------------------------------------------------------------------- /docs/notebook: -------------------------------------------------------------------------------- 1 | ../notebook/ -------------------------------------------------------------------------------- /docs/notebooks.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | :fas:`hand-pointer;fa-fw` Notebooks 3 | =================================== 4 | The notebooks below contain a showcase of what is possible using **pyecsca** and 5 | are the best source of documentation on how to use **pyecsca**. 6 | 7 | .. toctree:: 8 | :titlesonly: 9 | :maxdepth: 1 10 | 11 | notebook/configuration_space 12 | notebook/simulation 13 | notebook/codegen 14 | notebook/emulator 15 | notebook/measurement 16 | notebook/visualization 17 | notebook/smartcards 18 | notebook/re/formulas 19 | notebook/re/rpa 20 | notebook/re/zvp 21 | notebook/re/epa 22 | -------------------------------------------------------------------------------- /docs/papers.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | :fas:`file-alt;fa-fw` Papers 3 | ============================ 4 | 5 | pyecsca: Reverse engineering black-box elliptic curve cryptography via side-channel analysis 6 | ============================================================================================ 7 | 8 | Jan Jancar, Vojtech Suchanek, Petr Svenda, Vladimir Sedlacek, Lukasz Chmielewski 9 | 10 | `CHES 2024, Halifax, Canada `_ 11 | 12 | .. grid:: 13 | :margin: 2 0 0 2 14 | :padding: 2 0 0 2 15 | 16 | .. grid-item:: 17 | :columns: auto 18 | 19 | .. button-link:: _static/pyecsca_ches24.pdf 20 | :color: secondary 21 | 22 | :fas:`file-alt;fa-fw` Preprint 23 | 24 | .. grid-item:: 25 | :columns: auto 26 | 27 | .. button-link:: https://github.com/J08nY/pyecsca-artifact 28 | :color: secondary 29 | 30 | :fas:`file-zipper;fa-fw` Artifact 31 | 32 | .. dropdown:: BibTeX 33 | :color: secondary 34 | :name: pyecsca-bibtex 35 | :class-container: bibtex-dropdown 36 | 37 | .. code-block:: Bibtex 38 | 39 | @InProceedings{2024-ches-jancar, 40 | title = {pyecsca: Reverse engineering black-box elliptic curve cryptography via side-channel analysis}, 41 | author = {Jan Jancar and Vojtech Suchanek and Petr Svenda and Vladimir Sedlacek and Lukasz Chmielewski}, 42 | booktitle = {IACR Transactions on Cryptographic Hardware and Embedded Systems}, 43 | publisher = {Ruhr-University of Bochum}, 44 | year = {2024}, 45 | doi = {10.46586/tches.v2024.i4.355-381}, 46 | url = {https://tches.iacr.org/index.php/TCHES/article/view/11796}, 47 | pages = {355–381}, 48 | } 49 | 50 | Abstract 51 | -------- 52 | 53 | Side-channel attacks on elliptic curve cryptography (ECC) often assume a 54 | white-box attacker who has detailed knowledge of the implementation choices taken 55 | by the target implementation. Due to the complex and layered nature of ECC, there 56 | are many choices that a developer makes to obtain a functional and interoperable 57 | implementation. These include the curve model, coordinate system, addition formulas, 58 | and the scalar multiplier, or lower-level details such as the finite-field multiplication 59 | algorithm. This creates a gap between the attack requirements and a real-world 60 | attacker that often only has black-box access to the target – i.e., has no access to 61 | the source code nor knowledge of specific implementation choices made. Yet, when 62 | the gap is closed, even real-world implementations of ECC succumb to side-channel 63 | attacks, as evidenced by attacks such as TPM-Fail, Minerva, the Side Journey to 64 | Titan, or TPMScan. 65 | 66 | We study this gap by first analyzing open-source ECC libraries for insight into real- 67 | world implementation choices. We then examine the space of all ECC implementations 68 | combinatorially. Finally, we present a set of novel methods for automated reverse 69 | engineering of black-box ECC implementations and release a documented and usable 70 | open-source toolkit for side-channel analysis of ECC called **pyecsca**. 71 | 72 | Our methods turn attacks around: instead of attempting to recover the private key, 73 | they attempt to recover the implementation configuration given control over the 74 | private and public inputs. We evaluate them on two simulation levels and study the 75 | effect of noise on their performance. Our methods are able to 1) reverse-engineer 76 | the scalar multiplication algorithm completely and 2) infer significant information 77 | about the coordinate system and addition formulas used in a target implementation 78 | -------------------------------------------------------------------------------- /docs/references.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | :fas:`asterisk;fa-fw` References 3 | ================================ 4 | 5 | .. [STD] Standard Curve Database, https://neuromancer.sk/std/ 6 | .. [EFD] Explicit Formulas Database, https://hyperelliptic.org/EFD/ 7 | .. [GECC] Guide to Elliptic Curve Cryptography, https://link.springer.com/book/10.1007/b97644 8 | .. [HEHCC] Handbook of Elliptic and Hyper-Elliptic Curve Cryptography, https://www.hyperelliptic.org/HEHCC/ 9 | .. [HAC] Handbook of Applied Cryptography, https://cacr.uwaterloo.ca/hac/ 10 | .. [BBG+17] Sliding right into disaster: Left-to-right sliding windows leak, https://eprint.iacr.org/2017/627.pdf 11 | .. [RPA] A Refined Power-Analysis Attack on Elliptic Curve Cryptosystems, http://www.goubin.fr/papers/ecc-dpa.pdf 12 | .. [ZVP] Zero-value point attacks on elliptic curve cryptosystem, https://doi.org/10.1007/10958513_17 13 | .. [EPA] Exceptional procedure attack on elliptic curve cryptosystems, https://doi.org/10.1007/3-540-36288-6_17 14 | .. [FFD] A formula for disaster: a unified approach to elliptic curve special-point-based attacks, https://eprint.iacr.org/2021/1595.pdf 15 | .. [BT11] Remote Timing Attacks Are Still Practical, https://eprint.iacr.org/2011/232.pdf 16 | .. [MT1991] Mazur, B., & Tate, J. (1991). The `p`-adic sigma function. Duke Mathematical Journal, 62 (3), 663-688. 17 | .. [CO2002] Jean-Sébastien Coron. Resistance against Differential Power Analysis for Elliptic Curve Cryptosystems, https://link.springer.com/chapter/10.1007/3-540-48059-5_25 18 | .. [DJB02] D.J. Bernstein: Pippenger's Exponentiation Algorithm, https://cr.yp.to/papers/pippenger.pdf 19 | .. [SG14] Shay Gueron & Vlad Krasnov. Fast prime field elliptic-curve cryptography with 256-bit primes, https://link.springer.com/article/10.1007/s13389-014-0090-x 20 | .. [B51] Andrew D. Booth. A signed binary multiplication technique. 21 | .. [M61] O.L. Macsorley. High-speed arithmetic in binary computers. 22 | .. [RFC7748] A. Langley, M. Hamburg, S. Turner, https://datatracker.ietf.org/doc/html/rfc7748 23 | -------------------------------------------------------------------------------- /pyecsca/ec/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for handling Elliptic Curves.""" 2 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bc-r1rv76-jac: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/ECPoint.java#L749 2 | coords jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bc-r1rv76-jac.op3: -------------------------------------------------------------------------------- 1 | Z1Squared = Z1^2 2 | U2 = Z1Squared * X2 3 | Z1Cubed = Z1Squared * Z1 4 | S2 = Z1Cubed * Y2 5 | Z2Squared = Z2^2 6 | U1 = Z2Squared * X1 7 | Z2Cubed = Z2Squared * Z2 8 | S1 = Z2Cubed * Y1 9 | H = U1 - U2 10 | R = S1 - S2 11 | HSquared = H^2 12 | G = HSquared * H 13 | V = HSquared * U1 14 | t0 = 2 * V 15 | t1 = R^2 16 | t2 = t1 + G 17 | X3 = t2 - t0 18 | t3 = V - X3 19 | t4 = G * S1 20 | t5 = t3 * R 21 | Y3 = t5 - t4 22 | Z3 = H * Z1 23 | Z3 = Z3 * Z2 24 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bc-r1rv76-mod: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/ECPoint.java#L749 2 | coords modified 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bc-r1rv76-mod.op3: -------------------------------------------------------------------------------- 1 | Z1Squared = Z1^2 2 | U2 = Z1Squared * X2 3 | Z1Cubed = Z1Squared * Z1 4 | S2 = Z1Cubed * Y2 5 | Z2Squared = Z2^2 6 | U1 = Z2Squared * X1 7 | Z2Cubed = Z2Squared * Z2 8 | S1 = Z2Cubed * Y1 9 | H = U1 - U2 10 | R = S1 - S2 11 | HSquared = H^2 12 | G = HSquared * H 13 | V = HSquared * U1 14 | t0 = 2 * V 15 | t1 = R^2 16 | t2 = t1 + G 17 | X3 = t2 - t0 18 | t3 = V - X3 19 | t4 = G * S1 20 | t5 = t3 * R 21 | Y3 = t5 - t4 22 | Z3 = H * Z1 23 | Z3 = Z3 * Z2 24 | Z3Squared = Z3^2 25 | W = Z3Squared^2 26 | T3 = W * a 27 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bearssl-v06: -------------------------------------------------------------------------------- 1 | source BearSSL v0.6 https://bearssl.org/gitweb/?p=BearSSL;a=blob;f=src/ec/ec_prime_i15.c;h=f86dbe6ff0dbc036af470e369048c4ae02d33337;hb=HEAD#l320 2 | coords jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-bearssl-v06.op3: -------------------------------------------------------------------------------- 1 | t3 = Z2^2 2 | t1 = X1 * t3 3 | t4 = Z2 * t3 4 | t3 = Y1 * t4 5 | t4 = Z1^2 6 | t2 = X2 * t4 7 | t5 = Z1 * t4 8 | t4 = Y2 * t5 9 | t2 = t2 - t1 10 | t4 = t4 - t3 11 | t7 = t2^2 12 | t6 = t1 * t7 13 | t5 = t7 * t2 14 | X = t4^2 15 | X = X - t5 16 | X = X - t6 17 | X3 = X - t6 18 | t6 = t6 - X3 19 | Y = t4 * t6 20 | t1 = t5 * t3 21 | Y3 = Y - t1 22 | t1 = Z1 * Z2 23 | Z3 = t1 * t2 24 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-boringssl-p224: -------------------------------------------------------------------------------- 1 | source BoringSSL bfa8369 https://github.com/google/boringssl/blob/bfa8369795b7533a222a72b7a1bc928941cd66bf/crypto/fipsmodule/ec/p224-64.c#L676 2 | coords jacobian-3 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-boringssl-p224.op3: -------------------------------------------------------------------------------- 1 | ftmp2 = Z2^2 2 | ftmp4 = Z2 * ftmp2 3 | ftmp4 = ftmp4 * Y1 4 | ftmp2 = ftmp2 * X1 5 | ftmp = Z1^2 6 | ftmp3 = Z1 * ftmp 7 | tmp = ftmp3 * Y2 8 | ftmp3 = tmp - ftmp4 9 | tmp = ftmp * X2 10 | ftmp = tmp - ftmp2 11 | ftmp5 = Z1 * Z2 12 | Z3 = ftmp * ftmp5 13 | _ftmp = ftmp^2 14 | ftmp5 = ftmp * _ftmp 15 | ftmp2 = ftmp2 * _ftmp 16 | tmp = ftmp4 * ftmp5 17 | tmp2 = ftmp3^2 18 | tmp2 = tmp2 - ftmp5 19 | ftmp5 = 2 * ftmp2 20 | X3 = tmp2 - ftmp5 21 | ftmp2 = ftmp2 - X3 22 | tmp2 = ftmp3 * ftmp2 23 | Y3 = tmp2 - tmp 24 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-gecc-322: -------------------------------------------------------------------------------- 1 | source GECC Algorithm 3.22 2 | coords jacobian-3 3 | assume Z2 = 1 4 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-gecc-322.op3: -------------------------------------------------------------------------------- 1 | T1 = Z1^2 2 | T2 = T1 * Z1 3 | T1 = T1 * X2 4 | T2 = T2 * Y2 5 | T1 = T1 - X1 6 | T2 = T2 - Y1 7 | Z3 = Z1 * T1 8 | T3 = T1^2 9 | T4 = T3 * T1 10 | T3 = T3 * X1 11 | T1 = 2 * T3 12 | X3 = T2^2 13 | X3 = X3 - T1 14 | X3 = X3 - T4 15 | T3 = T3 - X3 16 | T3 = T3 * T2 17 | T4 = T4 * Y1 18 | Y3 = T3 - T4 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-libgcrypt-v1102: -------------------------------------------------------------------------------- 1 | source libgcrypt v1.10.2 https://git.gnupg.org/cgi-bin/gitweb.cgi?p=libgcrypt.git;a=blob;f=mpi/ec.c;h=c24921eea8bea8363a503d6d6071b116c176d8e5;hb=1c5cbacf3d88dded5063e959ee68678ff7d0fa56#l1406 2 | parameter half 3 | assume half = 1/2 4 | coords jacobian 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-libgcrypt-v1102.op3: -------------------------------------------------------------------------------- 1 | l1 = Z2^2 2 | l1 = l1 * X1 3 | l2 = Z1^2 4 | l2 = l2 * X2 5 | l3 = l1 - l2 6 | l4 = Z2^3 7 | l4 = l4 * Y1 8 | l5 = Z1^3 9 | l5 = l5 * Y2 10 | l6 = l4 - l5 11 | l7 = l1 + l2 12 | l8 = l4 + l5 13 | Z3 = Z1 * Z2 14 | Z3 = Z3 * l3 15 | t1 = l6^2 16 | t2 = l3^2 17 | t2 = t2 * l7 18 | X3 = t1 - t2 19 | t1 = X3 * 2 20 | l9 = t2 - t1 21 | l9 = l9 * l6 22 | t1 = l3^3 23 | t1 = t1 * l8 24 | Y3 = l9 - t1 25 | Y3 = Y3 * half 26 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-libressl-v382: -------------------------------------------------------------------------------- 1 | source LibreSSL v3.8.2 https://github.com/libressl/openbsd/blob/libressl-v3.8.2/src/lib/libcrypto/ec/ecp_smpl.c#L472 2 | coords Jacobian 3 | parameter half 4 | assume half = 1 / 2 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-libressl-v382.op3: -------------------------------------------------------------------------------- 1 | n0 = Z2^2 2 | n1 = X1 * n0 3 | n0 = n0 * Z2 4 | n2 = Y1 * n0 5 | n0 = Z1^2 6 | n3 = X2 * n0 7 | n0 = n0 * Z1 8 | n4 = Y2 * n0 9 | n5 = n1 - n3 10 | n6 = n2 - n4 11 | n7 = n1 + n3 12 | n8 = n2 + n4 13 | n0 = Z1 * Z2 14 | Z3 = n0 * n5 15 | n0 = n6^2 16 | n4 = n5^2 17 | n3 = n4 * n7 18 | X3 = n0 - n3 19 | n0 = 2 * X3 20 | n9 = n3 - n0 21 | t0 = n6 * n9 22 | t1 = n4 * n5 23 | t2 = n8 * t1 24 | Y3 = t0 - t2 25 | Y3 = Y3 * half 26 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-openssl-z256: -------------------------------------------------------------------------------- 1 | source OpenSSL 3.1.4 https://github.com/openssl/openssl/blob/openssl-3.1.4/crypto/ec/ecp_nistz256.c#L312 2 | coords jacobian-3 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-openssl-z256.op3: -------------------------------------------------------------------------------- 1 | Z2sqr = Z2^2 2 | Z1sqr = Z1^2 3 | S1 = Z2sqr * Z2 4 | S2 = Z1sqr * Z1 5 | S1 = Y1 * S1 6 | S2 = Y2 * S2 7 | R = S2 - S1 8 | U1 = X1 * Z2sqr 9 | U2 = X2 * Z1sqr 10 | H = U2 - U1 11 | Rsqr = R^2 12 | Z3 = H * Z1 13 | Hsqr = H^2 14 | Z3 = Z3 * Z2 15 | Hcub = Hsqr * H 16 | U2 = U1 * Hsqr 17 | Hsqr = 2 * U2 18 | X3 = Rsqr - Hsqr 19 | X3 = X3 - Hcub 20 | Y3 = U2 - X3 21 | S2 = S1 * Hcub 22 | Y3 = R * Y3 23 | Y3 = Y3 - S2 24 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-openssl-z256a: -------------------------------------------------------------------------------- 1 | source OpenSSL 3.1.4 https://github.com/openssl/openssl/blob/openssl-3.1.4/crypto/ec/ecp_nistz256.c#L442 2 | coords jacobian-3 3 | assume Z2 = 1 4 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-openssl-z256a.op3: -------------------------------------------------------------------------------- 1 | Z1sqr = Z1^2 2 | U2 = X2 * Z1sqr 3 | H = U2 - X1 4 | S2 = Z1sqr * Z1 5 | Z3 = H * Z1 6 | S2 = S2 * Y2 7 | R = S2 - Y1 8 | Hsqr = H^2 9 | Rsqr = R^2 10 | Hcub = Hsqr * H 11 | U2 = X1 * Hsqr 12 | Hsqr = 2 * U2 13 | X3 = Rsqr - Hsqr 14 | X3 = X3 - Hcub 15 | H = U2 - X3 16 | S2 = Y1 * Hcub 17 | H = H * R 18 | Y3 = H - S2 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-sunec-v21: -------------------------------------------------------------------------------- 1 | source Java JDK 21 https://github.com/openjdk/jdk/blob/jdk-21-ga/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java#L287 2 | coords projective-3 3 | assume Z2 = 1 4 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-sunec-v21-ed25519: -------------------------------------------------------------------------------- 1 | source Java JDK 21 https://github.com/openjdk/jdk/blob/jdk-21-ga/src/jdk.crypto.ec/share/classes/sun/security/ec/ed/Ed25519Operations.java#L147 2 | coords extended-1 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-sunec-v21-ed25519.op3: -------------------------------------------------------------------------------- 1 | t1 = Y2 - X2 2 | t2 = Y1 - X1 3 | t2 = t2 * t1 4 | t1 = Y2 + X2 5 | t3 = Y1 + X1 6 | t3 = t3 * t1 7 | X = t3 - t2 8 | t3 = t3 + t2 9 | t2 = d + d 10 | t2 = t2 * T1 11 | t2 = t2 * T2 12 | t1 = Z1 * Z2 13 | t1 = t1 * 2 14 | Y = t1 + t2 15 | Z = t1 - t2 16 | T3 = X1 * t3 17 | X3 = X * Z 18 | Z3 = Z * Y 19 | Y3 = Y * t3 20 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/add-sunec-v21.op3: -------------------------------------------------------------------------------- 1 | t0 = X1 * X2 2 | t1 = Y1 * Y2 3 | t3 = X2 + Y2 4 | t4 = X1 + Y1 5 | t3 = t3 * t4 6 | t4 = t0 + t1 7 | t3 = t3 - t4 8 | t4 = Y2 * Z1 9 | t4 = t4 + Y1 10 | Y = X2 * Z1 11 | Y = Y + X1 12 | Z = Z1 * b 13 | X = Y - Z 14 | X = X * 3 15 | Z = t1 - X 16 | X = X + t1 17 | Y = Y * b 18 | t2 = Z1 * 3 19 | Y = Y - t2 20 | Y = Y - t0 21 | Y = Y * 3 22 | t0 = t0 * 3 23 | t0 = t0 - t2 24 | t1 = t4 * Y 25 | t2 = t0 * Y 26 | Y = X * Z 27 | Y3 = Y + t2 28 | X = X * t3 29 | X3 = X - t1 30 | Z = Z * t4 31 | t3 = t3 * t0 32 | Z3 = Z + t3 33 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-jac: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/ECPoint.java#L877 2 | coords jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-jac.op3: -------------------------------------------------------------------------------- 1 | Y1Squared = Y1^2 2 | T = Y1Squared^2 3 | X1Squared = X1^2 4 | M = 3 * X1Squared 5 | Z1Squared = Z1^2 6 | Z1Pow4 = Z1Squared^2 7 | t0 = Z1Pow4 * a 8 | M = M + t0 9 | t1 = X1 * Y1Squared 10 | S = 4 * t1 11 | t2 = 2 * S 12 | t3 = M^2 13 | X3 = t3 - t2 14 | t4 = 8 * T 15 | t5 = S - X3 16 | t6 = t5 * M 17 | Y3 = t6 - t4 18 | Z3 = 2 * Y1 19 | Z3 = Z3 * Z1 20 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-mod: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/ECPoint.java#L1321 2 | coords modified 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-mod.op3: -------------------------------------------------------------------------------- 1 | X1Squared = X1^2 2 | t0 = 3 * X1Squared 3 | M = t0 + T1 4 | _2Y1 = 2 * Y1 5 | _2Y1Squared = _2Y1 * Y1 6 | t1 = X1 * _2Y1Squared 7 | S = 2 * t1 8 | t2 = M^2 9 | t3 = 2 * S 10 | X3 = t2 - t3 11 | _4T = _2Y1Squared^2 12 | _8T = 2 * _4T 13 | t4 = S - X3 14 | t5 = M * t4 15 | Y3 = t5 - _8T 16 | t6 = _8T * T1 17 | T3 = 2 * t6 18 | Z3 = _2Y1 * Z1 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-x25519: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/rfc7748/X25519.java#L73 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | coords xz 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bc-r1rv76-x25519.op3: -------------------------------------------------------------------------------- 1 | pa = X1 + Z1 2 | pb = X1 - Z1 3 | pa = pa^2 4 | pb = pb^2 5 | X3 = pa * pb 6 | pa = pa - pb 7 | Z3 = pa * a24 8 | Z3 = Z3 + pb 9 | Z3 = Z3 * pa 10 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bearssl-v06: -------------------------------------------------------------------------------- 1 | source BearSSL v0.6 https://bearssl.org/gitweb/?p=BearSSL;a=blob;f=src/ec/ec_prime_i15.c;h=f86dbe6ff0dbc036af470e369048c4ae02d33337;hb=HEAD#l214 2 | coords jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-bearssl-v06.op3: -------------------------------------------------------------------------------- 1 | t1 = Z1^2 2 | t2 = X1 - t1 3 | t1 = t1 + X1 4 | t3 = t1 * t2 5 | t1 = t3 + t3 6 | t1 = t1 + t3 7 | t3 = Y1^2 8 | t3 = t3 + t3 9 | t2 = X1 * t3 10 | t2 = t2 + t2 11 | X = t1^2 12 | X = X - t2 13 | X3 = X - t2 14 | t4 = Y1 * Z1 15 | Z3 = t4 + t4 16 | t2 = t2 - X3 17 | Y = t1 * t2 18 | t4 = t3^2 19 | Y = Y - t4 20 | Y3 = Y - t4 21 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-boringssl-p224: -------------------------------------------------------------------------------- 1 | source BoringSSL bfa8369 https://github.com/google/boringssl/blob/bfa8369795b7533a222a72b7a1bc928941cd66bf/crypto/fipsmodule/ec/p224-64.c#L591 2 | coords jacobian-3 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-boringssl-p224.op3: -------------------------------------------------------------------------------- 1 | delta = Z1^2 2 | gamma = Y1^2 3 | beta = X1 * gamma 4 | ftmp = X1 - delta 5 | ftmp2 = X1 + delta 6 | ftmp2 = 3 * ftmp2 7 | alpha = ftmp * ftmp2 8 | tmp = alpha^2 9 | ftmp = 8 * beta 10 | X3 = tmp - ftmp 11 | delta = delta + gamma 12 | ftmp = Y1 + Z1 13 | tmp = ftmp^2 14 | Z3 = tmp - delta 15 | beta = 4 * beta 16 | beta = beta - X3 17 | tmp = alpha * beta 18 | tmp2 = gamma^2 19 | tmp2 = 8 * tmp2 20 | Y3 = tmp - tmp2 21 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-gecc-321: -------------------------------------------------------------------------------- 1 | source GECC Algorithm 3.21 2 | parameter half 3 | assume half = 1/2 4 | coords jacobian-3 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-gecc-321.op3: -------------------------------------------------------------------------------- 1 | T1 = Z1^2 2 | T2 = X1 - T1 3 | T1 = X1 + T1 4 | T2 = T2 * T1 5 | T2 = 3 * T2 6 | Y3 = 2 * Y1 7 | Z3 = Y3 * Z1 8 | Y3 = Y3^2 9 | T3 = Y3 * X1 10 | Y3 = Y3^2 11 | Y3 = Y3 * half 12 | X3 = T2^2 13 | T1 = 2 * T3 14 | X3 = X3 - T1 15 | T1 = T3 - X3 16 | T1 = T1 * T2 17 | Y3 = T1 - Y3 18 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-hacl-x25519: -------------------------------------------------------------------------------- 1 | source HACL* https://github.com/hacl-star/hacl-star/blob/v0.3.0/specs/Spec.Curve25519.fst#L80C9-L80C9 2 | parameter am24 3 | assume am24 = (a-2)/4 4 | coords xz 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-hacl-x25519.op3: -------------------------------------------------------------------------------- 1 | a = X1 + Z1 2 | b = X1 - Z1 3 | aa = a^2 4 | bb = b^2 5 | e = aa - bb 6 | e121665 = e * am24 7 | aa_e121665 = e121665 + aa 8 | X3 = aa * bb 9 | Z3 = e * aa_e121665 10 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-ipp-x25519: -------------------------------------------------------------------------------- 1 | source Intel IPP crypto https://github.com/intel/ipp-crypto/blob/ippcp_2021.9.0/sources/ippcp/crypto_mb/src/x25519/ifma_x25519.c#L1689 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | coords xz 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-ipp-x25519.op3: -------------------------------------------------------------------------------- 1 | A = X1 + Z1 2 | B = X1 - Z1 3 | A = A^2 4 | B = B^2 5 | C = A - B 6 | D = a24 * C 7 | D = D + B 8 | X3 = A * B 9 | Z3 = C * D 10 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-libgcrypt-v1102: -------------------------------------------------------------------------------- 1 | source libgcrypt v1.10.2 https://git.gnupg.org/cgi-bin/gitweb.cgi?p=libgcrypt.git;a=blob;f=mpi/ec.c;h=c24921eea8bea8363a503d6d6071b116c176d8e5;hb=1c5cbacf3d88dded5063e959ee68678ff7d0fa56#l1219 2 | coords jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-libgcrypt-v1102.op3: -------------------------------------------------------------------------------- 1 | l1 = X1^2 2 | l1 = l1 * 3 3 | t1 = Z1^4 4 | t1 = t1 * a 5 | l1 = l1 + t1 6 | Z3 = Y1 * Z1 7 | Z3 = Z3 * 2 8 | t2 = Y1^2 9 | l2 = t2 * X1 10 | l2 = l2 * 4 11 | X3 = l1^2 12 | t1 = l2 * 2 13 | X3 = X3 - t1 14 | t2 = t2^2 15 | l3 = t2 * 8 16 | Y3 = l2 - X3 17 | Y3 = Y3 * l1 18 | Y3 = Y3 - l3 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-libressl-v382: -------------------------------------------------------------------------------- 1 | source LibreSSL v3.8.2 https://github.com/libressl/openbsd/blob/libressl-v3.8.2/src/lib/libcrypto/ec/ecp_smpl.c#L654 2 | coords Jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-libressl-v382.op3: -------------------------------------------------------------------------------- 1 | n0 = X1^2 2 | n1 = n0 * 2 3 | n0 = n0 + n1 4 | n1 = Z1^2 5 | n1 = n1^2 6 | n1 = a * n1 7 | n1 = n0 + n1 8 | n0 = Y1 * Z1 9 | Z3 = 2 * n0 10 | n3 = Y1^2 11 | n2 = X1 * n3 12 | n2 = 4 * n2 13 | n0 = 2 * n2 14 | X3 = n1^2 15 | X3 = X3 - n0 16 | n0 = n3^2 17 | n3 = 8 * n0 18 | n0 = n2 - X3 19 | n0 = n1 * n0 20 | Y3 = n0 - n3 21 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-secp256k1-v040: -------------------------------------------------------------------------------- 1 | source libsecp256k1 v0.4.0 https://github.com/bitcoin-core/secp256k1/blob/v0.4.0/src/group_impl.h#L406 2 | coords Jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-secp256k1-v040.op3: -------------------------------------------------------------------------------- 1 | Z3 = Y1*Z1 2 | S = Y1^2 3 | L = X1^2 4 | L = 3*L 5 | L = L/2 6 | T = -S 7 | T = T*X1 8 | X3 = L^2 9 | X3 = X3+T 10 | X3 = X3+T 11 | S = S^2 12 | T = T+X3 13 | Y3 = T*L 14 | Y3 = Y3+S 15 | Y3 = -Y3 16 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-sunec-v21: -------------------------------------------------------------------------------- 1 | source Java JDK 21 https://github.com/openjdk/jdk/blob/jdk-21-ga/src/jdk.crypto.ec/share/classes/sun/security/ec/ECOperations.java#L220 2 | coords projective-3 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-sunec-v21-ed25519: -------------------------------------------------------------------------------- 1 | source Java JDK 21 https://github.com/openjdk/jdk/blob/jdk-21-ga/src/jdk.crypto.ec/share/classes/sun/security/ec/ed/Ed25519Operations.java#L184 2 | coords extended-1 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-sunec-v21-ed25519.op3: -------------------------------------------------------------------------------- 1 | t1 = X1 + Y1 2 | t1 = t1^2 3 | X = X1^2 4 | Y = Y1^2 5 | t2 = X + Y 6 | Z = Z1^2 7 | Z = Z * 2 8 | T = t2 - t1 9 | t1 = X - Y 10 | Z = Z + t1 11 | X3 = T * Z 12 | Y3 = t1 * t2 13 | T3 = T * t2 14 | Z3 = Z * t1 15 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/dbl-sunec-v21.op3: -------------------------------------------------------------------------------- 1 | t0 = X1^2 2 | t1 = Y1^2 3 | t2 = Z1^2 4 | t3 = X1 * Y1 5 | t4 = Y1 * Z1 6 | t3 = t3 + t3 7 | Z = Z1 * X1 8 | Z = Z * 2 9 | Y = t2 * b 10 | Y = Y - Z 11 | Y = 3 * Y 12 | X = t1 - Y 13 | Y = Y + t1 14 | Y = Y * X 15 | X = X * t3 16 | t2 = t2 * 3 17 | Z = Z * b 18 | Z = Z - t2 19 | Z = Z - t0 20 | Z = Z * 3 21 | t0 = t0 * 3 22 | t0 = t0 - t2 23 | t0 = t0 * Z 24 | Y3 = Y + t0 25 | t4 = t4 + t4 26 | Z = Z * t4 27 | X3 = X - Z 28 | Z = t4 * t1 29 | Z3 = Z * 4 30 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-bc-r1rv76-x25519: -------------------------------------------------------------------------------- 1 | source BouncyCastle r1rv76 https://github.com/bcgit/bc-java/blob/r1rv76/core/src/main/java/org/bouncycastle/math/ec/rfc7748/X25519.java#L111 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-bc-r1rv76-x25519.op3: -------------------------------------------------------------------------------- 1 | t1 = X3 + Z3 2 | X3 = X3 - Z3 3 | Z3 = X2 + Z2 4 | X2 = X2 - Z2 5 | t1 = t1 * X2 6 | X3 = X3 * Z3 7 | Z3 = Z3^2 8 | X2 = X2^2 9 | t2 = Z3 - X2 10 | Z2 = t2 * a24 11 | Z2 = Z2 + X2 12 | Z4 = Z2 * t2 13 | X4 = X2 * Z3 14 | _X3 = t1 + X3 15 | Z3 = t1 - X3 16 | X5 = _X3^2 17 | Z3 = Z3^2 18 | Z5 = X1 * Z3 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-boringssl-x25519: -------------------------------------------------------------------------------- 1 | source BoringSSL bfa8369 https://github.com/google/boringssl/blob/bfa8369795b7533a222a72b7a1bc928941cd66bf/crypto/curve25519/curve25519.c#L624 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-boringssl-x25519.op3: -------------------------------------------------------------------------------- 1 | tmp0 = X3 - Z3 2 | tmp1 = X2 - Z2 3 | X2 = X2 + Z2 4 | Z2 = X3 + Z3 5 | Z3 = tmp0 * X2 6 | Z2 = Z2 * tmp1 7 | tmp0 = tmp1^2 8 | tmp1 = X2^2 9 | X3 = Z3 + Z2 10 | Z2 = Z3 - Z2 11 | X4 = tmp1 * tmp0 12 | tmp1 = tmp1 - tmp0 13 | Z2 = Z2^2 14 | Z3 = tmp1 * a24 15 | X5 = X3^2 16 | tmp0 = tmp0 + Z3 17 | Z5 = X1 * Z2 18 | Z4 = tmp1 * tmp0 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-botan-x25519: -------------------------------------------------------------------------------- 1 | source Botan 3.2.0 https://github.com/randombit/botan/blob/3.2.0/src/lib/pubkey/curve25519/donna.cpp#L299 2 | coords xz 3 | parameter am24 4 | assume am24 = (a-2)/4 5 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-botan-x25519.op3: -------------------------------------------------------------------------------- 1 | Z1new = X2 - Z2 2 | X1new = X2 + Z2 3 | Z2new = X3 - Z3 4 | X2new = X3 + Z3 5 | xxprime = X2new * Z1new 6 | zzprime = Z2new * X1new 7 | zzprime_new = xxprime - zzprime 8 | xxprime_new = xxprime + zzprime 9 | X5 = xxprime_new^2 10 | zzzprime = zzprime_new^2 11 | Z5 = zzzprime * X1 12 | xx = X1new^2 13 | zz = Z1new^2 14 | X4 = xx * zz 15 | zz = xx - zz 16 | zzz = zz * am24 17 | zzz = zzz + xx 18 | Z4 = zz * zzz 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-go-1214: -------------------------------------------------------------------------------- 1 | source go crypto/ecdh/x25519 https://github.com/golang/go/blob/go1.21.4/src/crypto/ecdh/x25519.go#L86 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-go-1214.op3: -------------------------------------------------------------------------------- 1 | t0 = X3-Z3 2 | t1 = X2-Z2 3 | X2 = X2+Z2 4 | Z2 = X3+Z3 5 | Z3 = t0*X2 6 | Z2 = Z2*t1 7 | t0 = t1^2 8 | t1 = X2^2 9 | X3 = Z3+Z2 10 | Z2 = Z3-Z2 11 | X4 = t1*t0 12 | t1 = t1-t0 13 | Z2 = Z2^2 14 | Z3 = t1 * a24 15 | X5 = X3^2 16 | t0 = t0+Z3 17 | Z5 = X1*Z2 18 | Z4 = t1*t0 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-hacl-x25519: -------------------------------------------------------------------------------- 1 | source HACL* https://github.com/hacl-star/hacl-star/blob/v0.3.0/specs/Spec.Curve25519.fst#L56 2 | parameter am24 3 | assume am24 = (a-2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-hacl-x25519.op3: -------------------------------------------------------------------------------- 1 | a = X2 + Z2 2 | b = X2 - Z2 3 | c = X3 + Z3 4 | d = X3 - Z3 5 | da = d * a 6 | cb = c * b 7 | X3 = da + cb 8 | Z3 = da - cb 9 | aa = a^2 10 | bb = b^2 11 | X5 = X3^2 12 | Z3 = Z3^2 13 | e = aa - bb 14 | e121665 = e * am24 15 | aa_e121665 = aa + e121665 16 | X4 = aa * bb 17 | Z4 = e * aa_e121665 18 | Z5 = Z3 * X1 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-openssl-x25519: -------------------------------------------------------------------------------- 1 | source OpenSSL 3.1.4 https://github.com/openssl/openssl/blob/openssl-3.1.4/crypto/ec/curve25519.c#L211 2 | parameter a24 3 | assume a24 = (a+2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-openssl-x25519.op3: -------------------------------------------------------------------------------- 1 | tmp0 = X3 - Z3 2 | tmp1 = X2 - Z2 3 | X2 = X2 + Z2 4 | Z2 = X3 + Z3 5 | Z3 = X2 * tmp0 6 | Z2 = Z2 * tmp1 7 | tmp0 = tmp1^2 8 | tmp1 = X2^2 9 | X3 = Z3 + Z2 10 | Z2 = Z3 - Z2 11 | X4 = tmp1 * tmp0 12 | tmp1 = tmp1 - tmp0 13 | Z2 = Z2^2 14 | Z3 = tmp1 * a24 15 | X5 = X3^2 16 | tmp0 = tmp0 + Z3 17 | Z5 = X1 * Z2 18 | Z4 = tmp1 * tmp0 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-rfc7748: -------------------------------------------------------------------------------- 1 | source RFC 7748 2 | parameter am24 3 | assume am24 = (a-2)/4 4 | assume Z1 = 1 5 | coords xz 6 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/ladd-rfc7748.op3: -------------------------------------------------------------------------------- 1 | A = X2 + Z2 2 | AA = A^2 3 | B = X2 - Z2 4 | BB = B^2 5 | E = AA - BB 6 | C = X3 + Z3 7 | D = X3 - Z3 8 | DA = D * A 9 | CB = C * B 10 | DApCB = DA + CB 11 | X5 = DApCB^2 12 | DAmCB = DA - CB 13 | DAmCB2 = DAmCB^2 14 | Z5 = X1 * DAmCB2 15 | X4 = AA * BB 16 | E24 = E * am24 17 | AAE = AA + E24 18 | Z4 = E * AAE 19 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/madd-secp256k1-v040: -------------------------------------------------------------------------------- 1 | source libsecp256k1 v0.4.0 https://github.com/bitcoin-core/secp256k1/blob/v0.4.0/src/group_impl.h#L670 2 | coords Jacobian 3 | -------------------------------------------------------------------------------- /pyecsca/ec/data/formulas/madd-secp256k1-v040.op3: -------------------------------------------------------------------------------- 1 | zz = Z1^2 2 | u1 = X1 3 | u2 = X2*zz 4 | s1 = Y1 5 | s2 = Y2*zz 6 | s2 = s2*Z1 7 | t = u1+u2 8 | m = s1+s2 9 | rr = t^2 10 | malt = -u2 11 | tt = u1*malt 12 | rr = rr+tt 13 | rralt = s1*2 14 | malt = malt+u1 15 | rralt = rr 16 | malt = m 17 | n = malt^2 18 | q = -t 19 | q = q*n 20 | n = n^2 21 | t = rralt^2 22 | Z3 = Z1*malt 23 | t = t+q 24 | X3 = t 25 | t = t*2 26 | t = t+q 27 | t = t*rralt 28 | t = t+n 29 | Y3 = -t 30 | Y3 = Y3/2 31 | -------------------------------------------------------------------------------- /pyecsca/ec/error.py: -------------------------------------------------------------------------------- 1 | """Contains exceptions and warnings used in the library.""" 2 | 3 | import warnings 4 | from public import public 5 | from pyecsca.misc.cfg import getconfig 6 | 7 | 8 | @public 9 | class NonInvertibleError(ArithmeticError): 10 | """Non-invertible element was inverted.""" 11 | 12 | pass 13 | 14 | 15 | @public 16 | class NonInvertibleWarning(UserWarning): 17 | """Non-invertible element was inverted.""" 18 | 19 | pass 20 | 21 | 22 | def raise_non_invertible(): 23 | """ 24 | Raise either :py:class:`NonInvertibleError` or :py:class:`NonInvertiblerWarning` or ignore. 25 | 26 | Depends on the current config value of :py:attr:`Config.ec.no_inverse_action`. 27 | """ 28 | cfg = getconfig() 29 | if cfg.ec.no_inverse_action == "error": 30 | raise NonInvertibleError("Element not invertible.") 31 | elif cfg.ec.no_inverse_action == "warning": 32 | warnings.warn(NonInvertibleWarning("Element not invertible.")) 33 | 34 | 35 | @public 36 | class NonResidueError(ArithmeticError): 37 | """Non-residue element was square-rooted.""" 38 | 39 | pass 40 | 41 | 42 | @public 43 | class NonResidueWarning(UserWarning): 44 | """Non-residue element was square-rooted.""" 45 | 46 | pass 47 | 48 | 49 | def raise_non_residue(): 50 | """ 51 | Raise either :py:class:`NonResidueError` or :py:class:`NonResidueWarning` or ignore. 52 | 53 | Depends on the current config value of :py:attr:`Config.ec.non_residue_action`. 54 | """ 55 | cfg = getconfig() 56 | if cfg.ec.non_residue_action == "error": 57 | raise NonResidueError("No square root exists.") 58 | elif cfg.ec.non_residue_action == "warning": 59 | warnings.warn(NonResidueWarning("No square root exists.")) 60 | 61 | 62 | @public 63 | class UnsatisfiedAssumptionError(ValueError): 64 | """Unsatisfied assumption was hit.""" 65 | 66 | pass 67 | 68 | 69 | @public 70 | class UnsatisfiedAssumptionWarning(UserWarning): 71 | """Unsatisfied assumption was hit.""" 72 | 73 | pass 74 | 75 | 76 | def raise_unsatisified_assumption(action: str, msg: str): 77 | """ 78 | Raise either :py:class:`UnsatisfiedAssumptionError` or :py:class:`UnsatisfiedAssumptionWarning` or ignore. 79 | 80 | Depends on the value of :paramref:`~.raise_unsatisified_assumption.action`. 81 | """ 82 | if action == "error": 83 | raise UnsatisfiedAssumptionError(msg) 84 | elif action == "warning": 85 | warnings.warn(UnsatisfiedAssumptionWarning(msg)) 86 | -------------------------------------------------------------------------------- /pyecsca/ec/formula/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides functionality for working with addition formulas.""" 2 | 3 | from .base import * 4 | from .code import * 5 | from .efd import * 6 | from .unroll import * 7 | -------------------------------------------------------------------------------- /pyecsca/ec/formula/code.py: -------------------------------------------------------------------------------- 1 | """Provides a concrete class of a formula that has a constructor and some code.""" 2 | 3 | from typing import List, Any 4 | from ast import Expression 5 | from astunparse import unparse 6 | from public import public 7 | 8 | from pyecsca.ec.formula.base import ( 9 | Formula, 10 | AdditionFormula, 11 | DoublingFormula, 12 | LadderFormula, 13 | TriplingFormula, 14 | NegationFormula, 15 | ScalingFormula, 16 | DifferentialAdditionFormula, 17 | ) 18 | from pyecsca.ec.op import CodeOp 19 | from pyecsca.misc.utils import peval 20 | 21 | 22 | @public 23 | class CodeFormula(Formula): 24 | """A basic formula class that can be directly initialized with the code and other attributes.""" 25 | 26 | def __init__( 27 | self, 28 | name: str, 29 | code: List[CodeOp], 30 | coordinate_model: Any, 31 | parameters: List[str], 32 | assumptions: List[Expression], 33 | unified: bool = False, 34 | ): 35 | self.name = name 36 | self.code = code 37 | self.coordinate_model = coordinate_model 38 | self.meta = {} 39 | self.parameters = parameters 40 | self.assumptions = assumptions 41 | self.unified = unified 42 | 43 | def __hash__(self): 44 | return hash( 45 | ( 46 | self.name, 47 | self.coordinate_model, 48 | tuple(self.code), 49 | tuple(self.parameters), 50 | tuple(self.assumptions_str), 51 | self.unified, 52 | ) 53 | ) 54 | 55 | def __eq__(self, other): 56 | if not isinstance(other, CodeFormula): 57 | return False 58 | return ( 59 | self.name == other.name 60 | and self.coordinate_model == other.coordinate_model 61 | and self.code == other.code 62 | and self.parameters == other.parameters 63 | and self.assumptions_str == other.assumptions_str 64 | and self.unified == other.unified 65 | ) 66 | 67 | def __getstate__(self): 68 | state = self.__dict__.copy() 69 | state["assumptions"] = list(map(unparse, state["assumptions"])) 70 | return state 71 | 72 | def __setstate__(self, state): 73 | state["assumptions"] = list(map(peval, state["assumptions"])) 74 | self.__dict__.update(state) 75 | 76 | 77 | @public 78 | class CodeAdditionFormula(AdditionFormula, CodeFormula): 79 | pass 80 | 81 | 82 | @public 83 | class CodeDoublingFormula(DoublingFormula, CodeFormula): 84 | pass 85 | 86 | 87 | @public 88 | class CodeLadderFormula(LadderFormula, CodeFormula): 89 | pass 90 | 91 | 92 | @public 93 | class CodeTriplingFormula(TriplingFormula, CodeFormula): 94 | pass 95 | 96 | 97 | @public 98 | class CodeNegationFormula(NegationFormula, CodeFormula): 99 | pass 100 | 101 | 102 | @public 103 | class CodeScalingFormula(ScalingFormula, CodeFormula): 104 | pass 105 | 106 | 107 | @public 108 | class CodeDifferentialAdditionFormula(DifferentialAdditionFormula, CodeFormula): 109 | pass 110 | -------------------------------------------------------------------------------- /pyecsca/ec/formula/fake.py: -------------------------------------------------------------------------------- 1 | """Provides "fake" formulas.""" 2 | 3 | from abc import ABC 4 | from typing import Any, Tuple 5 | 6 | from public import public 7 | 8 | from pyecsca.ec.formula.base import ( 9 | AdditionFormula, 10 | FormulaAction, 11 | Formula, 12 | DoublingFormula, 13 | TriplingFormula, 14 | LadderFormula, 15 | NegationFormula, 16 | ScalingFormula, 17 | DifferentialAdditionFormula, 18 | ) 19 | from pyecsca.ec.mod import Mod, Undefined 20 | from pyecsca.ec.point import Point 21 | 22 | 23 | @public 24 | class FakeFormula(Formula, ABC): 25 | """ 26 | No matter what the input point is, it just returns the right amount of FakePoints. 27 | 28 | Useful for computing with the scalar multipliers without having concrete formulas 29 | and points (for example to get the addition chain via the :py:class:`~.MultipleContext`). 30 | """ 31 | 32 | def __init__(self, coordinate_model): 33 | # TODO: This is missing all of the other attributes 34 | self.coordinate_model = coordinate_model 35 | self.code = [] 36 | 37 | def __call__(self, field: int, *points: Any, **params: Mod) -> Tuple[Any, ...]: 38 | with FormulaAction(self, *points, **params) as action: 39 | result = [] 40 | for i in range(self.num_outputs): 41 | res = FakePoint(self.coordinate_model) 42 | action.output_points.append(res) 43 | result.append(res) 44 | return action.exit(tuple(result)) 45 | 46 | 47 | @public 48 | class FakeAdditionFormula(FakeFormula, AdditionFormula): 49 | name = "fake" 50 | 51 | 52 | @public 53 | class FakeDoublingFormula(FakeFormula, DoublingFormula): 54 | name = "fake" 55 | 56 | 57 | @public 58 | class FakeTriplingFormula(FakeFormula, TriplingFormula): 59 | name = "fake" 60 | 61 | 62 | @public 63 | class FakeNegationFormula(FakeFormula, NegationFormula): 64 | name = "fake" 65 | 66 | 67 | @public 68 | class FakeScalingFormula(FakeFormula, ScalingFormula): 69 | name = "fake" 70 | 71 | 72 | @public 73 | class FakeDifferentialAdditionFormula(FakeFormula, DifferentialAdditionFormula): 74 | name = "fake" 75 | 76 | 77 | @public 78 | class FakeLadderFormula(FakeFormula, LadderFormula): 79 | name = "fake" 80 | 81 | 82 | @public 83 | class FakePoint(Point): 84 | """Just a fake point.""" 85 | 86 | def __init__(self, model): 87 | coords = {key: Undefined() for key in model.variables} 88 | super().__init__(model, **coords) 89 | 90 | def __str__(self): 91 | return f"FakePoint{id(self)}" 92 | 93 | def __repr__(self): 94 | return str(self) 95 | -------------------------------------------------------------------------------- /pyecsca/ec/formula/unroll.py: -------------------------------------------------------------------------------- 1 | """Provides functions for unrolling formula intermediate values symvolically.""" 2 | 3 | from typing import List, Tuple 4 | 5 | from astunparse import unparse 6 | from public import public 7 | from sympy import Expr, symbols, Poly 8 | 9 | from pyecsca.misc.cache import sympify 10 | from pyecsca.ec.formula.base import Formula 11 | 12 | 13 | @public 14 | def unroll_formula_expr(formula: Formula) -> List[Tuple[str, Expr]]: 15 | """ 16 | Unroll a given formula symbolically to obtain symbolic expressions for its intermediate values. 17 | 18 | :param formula: Formula to unroll. 19 | :return: List of symbolic intermediate values, with associated variable names. 20 | """ 21 | params = { 22 | var: symbols(var) 23 | for var in formula.coordinate_model.curve_model.parameter_names 24 | } 25 | inputs = { 26 | f"{var}{i}": symbols(f"{var}{i}") 27 | for var in formula.coordinate_model.variables 28 | for i in range(1, formula.num_inputs + 1) 29 | } 30 | for coord_assumption in formula.coordinate_model.assumptions: 31 | assumption_string = unparse(coord_assumption).strip() 32 | lhs, rhs = assumption_string.split(" = ") 33 | if lhs in params: 34 | expr = sympify(rhs, evaluate=False) 35 | params[lhs] = expr 36 | for assumption_string in formula.assumptions_str: 37 | lhs, rhs = assumption_string.split(" == ") 38 | if lhs in formula.parameters: 39 | # Handle a symbolic assignment to a new parameter. 40 | expr = sympify(rhs, evaluate=False) 41 | for curve_param, value in params.items(): 42 | expr = expr.xreplace({curve_param: value}) 43 | params[lhs] = expr 44 | 45 | locls = {**params, **inputs} 46 | values: List[Tuple[str, Expr]] = [] 47 | for op in formula.code: 48 | result: Expr = op(**locls) # type: ignore 49 | locls[op.result] = result 50 | values.append((op.result, result)) 51 | return values 52 | 53 | 54 | @public 55 | def unroll_formula(formula: Formula) -> List[Tuple[str, Poly]]: 56 | """ 57 | Unroll a given formula symbolically to obtain symbolic expressions (as Polynomials) for its intermediate values. 58 | 59 | :param formula: Formula to unroll. 60 | :return: List of symbolic intermediate values, with associated variable names. 61 | """ 62 | values = unroll_formula_expr(formula) 63 | polys = [] 64 | for name, result in values: 65 | if result.free_symbols: 66 | gens = list(result.free_symbols) 67 | gens.sort(key=str) 68 | poly = Poly(result, *gens) 69 | polys.append((name, poly)) 70 | else: 71 | # TODO: We cannot create a Poly here, because the result does not have free symbols (i.e. it is a constant) 72 | pass 73 | return polys 74 | -------------------------------------------------------------------------------- /pyecsca/ec/key_generation.py: -------------------------------------------------------------------------------- 1 | """Provides a key generator for elliptic curve keypairs.""" 2 | 3 | from typing import Tuple 4 | 5 | from public import public 6 | 7 | from pyecsca.ec.context import ResultAction 8 | from pyecsca.ec.mod import Mod 9 | from pyecsca.ec.mult import ScalarMultiplier 10 | from pyecsca.ec.params import DomainParameters 11 | from pyecsca.ec.point import Point 12 | 13 | 14 | @public 15 | class KeygenAction(ResultAction): 16 | """A key generation.""" 17 | 18 | params: DomainParameters 19 | 20 | def __init__(self, params: DomainParameters): 21 | super().__init__() 22 | self.params = params 23 | 24 | def __repr__(self): 25 | return f"{self.__class__.__name__}({self.params})" 26 | 27 | 28 | @public 29 | class KeyGeneration: 30 | """ 31 | Key generator. 32 | 33 | :param mult: The scalar multiplier to use during key generation. 34 | :param params: The domain parameters over which to generate the keypair. 35 | :param affine: Whether to transform the public key point to the affine form during key generation. 36 | """ 37 | 38 | mult: ScalarMultiplier 39 | params: DomainParameters 40 | affine: bool 41 | 42 | def __init__( 43 | self, mult: ScalarMultiplier, params: DomainParameters, affine: bool = False 44 | ): 45 | self.mult = mult 46 | self.params = params 47 | self.mult.init(self.params, self.params.generator) 48 | self.affine = affine 49 | 50 | def generate(self) -> Tuple[Mod, Point]: 51 | """ 52 | Generate a keypair. 53 | 54 | :return: The generated keypair, a `tuple` of the private key (scalar) and the public key (point). 55 | """ 56 | with KeygenAction(self.params) as action: 57 | privkey = Mod.random(self.params.order) 58 | pubkey = self.mult.multiply(int(privkey.x)) 59 | if self.affine: 60 | pubkey = pubkey.to_affine() 61 | return action.exit((privkey, pubkey)) 62 | -------------------------------------------------------------------------------- /pyecsca/ec/mod/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides several implementations of an element of ℤₙ. 3 | 4 | The base class :py:class:`Mod` dynamically 5 | dispatches to the implementation chosen by the runtime configuration of the library 6 | (see :py:class:`pyecsca.misc.cfg.Config`). A Python integer based implementation is available under 7 | :py:class:`RawMod`. A symbolic implementation based on sympy is available under :py:class:`SymbolicMod`. If 8 | `gmpy2` is installed, a GMP based implementation is available under :py:class:`GMPMod`. If `python-flint` is 9 | installed, a flint based implementation is available under :py:class:`FlintMod`. 10 | """ 11 | 12 | from .base import * 13 | from .raw import * 14 | from .symbolic import * 15 | from .gmp import * 16 | from .flint import * 17 | -------------------------------------------------------------------------------- /pyecsca/ec/mod/raw.py: -------------------------------------------------------------------------------- 1 | from public import public 2 | from pyecsca.ec.error import ( 3 | raise_non_invertible, 4 | raise_non_residue, 5 | ) 6 | 7 | from pyecsca.ec.mod.base import Mod, extgcd, miller_rabin, jacobi, cube_root_inner, square_root_inner 8 | 9 | 10 | @public 11 | class RawMod(Mod["RawMod"]): 12 | """An element x of ℤₙ (implemented using Python integers).""" 13 | 14 | x: int 15 | n: int 16 | __slots__ = ("x", "n") 17 | 18 | def __init__(self, x: int, n: int): 19 | self.x = x % n 20 | self.n = n 21 | 22 | def bit_length(self): 23 | return self.x.bit_length() 24 | 25 | def inverse(self) -> "RawMod": 26 | if self.x == 0: 27 | raise_non_invertible() 28 | x, _, d = extgcd(self.x, self.n) 29 | if d != 1: 30 | raise_non_invertible() 31 | return RawMod(x, self.n) 32 | 33 | def is_residue(self): 34 | if not miller_rabin(self.n): 35 | raise NotImplementedError 36 | if self.x == 0: 37 | return True 38 | if self.n == 2: 39 | return self.x in (0, 1) 40 | legendre_symbol = jacobi(self.x, self.n) 41 | return legendre_symbol == 1 42 | 43 | def sqrt(self) -> "RawMod": 44 | if not miller_rabin(self.n): 45 | raise NotImplementedError 46 | if self.x == 0: 47 | return RawMod(0, self.n) 48 | if not self.is_residue(): 49 | raise_non_residue() 50 | return square_root_inner(self, int, lambda x: RawMod(x, self.n)) 51 | 52 | def is_cubic_residue(self): 53 | if not miller_rabin(self.n): 54 | raise NotImplementedError 55 | if self.x in (0, 1): 56 | return True 57 | if self.n % 3 == 2: 58 | return True 59 | pm1 = self.n - 1 60 | r = self ** (pm1 // 3) 61 | return r == 1 62 | 63 | def cube_root(self) -> "RawMod": 64 | if not miller_rabin(self.n): 65 | raise NotImplementedError 66 | if self.x == 0: 67 | return RawMod(0, self.n) 68 | if self.x == 1: 69 | return RawMod(1, self.n) 70 | if not self.is_cubic_residue(): 71 | raise_non_residue() 72 | return cube_root_inner(self, int, lambda x: RawMod(x, self.n)) 73 | 74 | def __bytes__(self): 75 | return self.x.to_bytes((self.n.bit_length() + 7) // 8, byteorder="big") 76 | 77 | def __int__(self): 78 | return self.x 79 | 80 | def __eq__(self, other): 81 | if type(other) is int: 82 | return self.x == (other % self.n) 83 | if type(other) is not RawMod: 84 | return False 85 | return self.x == other.x and self.n == other.n 86 | 87 | def __ne__(self, other): 88 | return not self == other 89 | 90 | def __repr__(self): 91 | return str(self.x) 92 | 93 | def __hash__(self): 94 | return hash(("RawMod", self.x, self.n)) 95 | 96 | def __pow__(self, n, _=None) -> "RawMod": 97 | if type(n) is not int: 98 | raise TypeError 99 | if n == 0: 100 | return RawMod(1, self.n) 101 | if n < 0: 102 | return self.inverse() ** (-n) 103 | if n == 1: 104 | return RawMod(self.x, self.n) 105 | 106 | return RawMod(pow(self.x, n, self.n), self.n) 107 | 108 | 109 | from pyecsca.ec.mod.base import _mod_classes # noqa 110 | 111 | _mod_classes["python"] = RawMod 112 | -------------------------------------------------------------------------------- /pyecsca/ec/mod/symbolic.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from public import public 4 | from sympy import Expr 5 | 6 | from pyecsca.ec.mod.base import Mod 7 | 8 | 9 | def _check(func): 10 | @wraps(func) 11 | def method(self, other): 12 | if self.__class__ is not type(other): 13 | other = self.__class__(other, self.n) 14 | elif self.n != other.n: 15 | raise ValueError 16 | return func(self, other) 17 | 18 | return method 19 | 20 | 21 | @public 22 | class SymbolicMod(Mod): 23 | """A symbolic element x of ℤₙ (implemented using sympy).""" 24 | 25 | x: Expr 26 | n: int 27 | __slots__ = ("x", "n") 28 | 29 | def __init__(self, x: Expr, n: int): 30 | self.x = x 31 | self.n = n 32 | 33 | @_check 34 | def __add__(self, other) -> "SymbolicMod": 35 | return self.__class__((self.x + other.x), self.n) 36 | 37 | @_check 38 | def __radd__(self, other) -> "SymbolicMod": 39 | return self + other 40 | 41 | @_check 42 | def __sub__(self, other) -> "SymbolicMod": 43 | return self.__class__((self.x - other.x), self.n) 44 | 45 | @_check 46 | def __rsub__(self, other) -> "SymbolicMod": 47 | return -self + other 48 | 49 | def __neg__(self) -> "SymbolicMod": 50 | return self.__class__(-self.x, self.n) 51 | 52 | def bit_length(self) -> int: 53 | raise NotImplementedError 54 | 55 | def inverse(self) -> "SymbolicMod": 56 | return self.__class__(self.x ** (-1), self.n) 57 | 58 | def is_residue(self) -> bool: 59 | raise NotImplementedError 60 | 61 | def sqrt(self) -> "SymbolicMod": 62 | raise NotImplementedError 63 | 64 | def is_cubic_residue(self) -> bool: 65 | raise NotImplementedError 66 | 67 | def cube_root(self) -> "SymbolicMod": 68 | raise NotImplementedError 69 | 70 | def __invert__(self) -> "SymbolicMod": 71 | return self.inverse() 72 | 73 | @_check 74 | def __mul__(self, other) -> "SymbolicMod": 75 | return self.__class__(self.x * other.x, self.n) 76 | 77 | @_check 78 | def __rmul__(self, other) -> "SymbolicMod": 79 | return self * other 80 | 81 | @_check 82 | def __truediv__(self, other) -> "SymbolicMod": 83 | return self * ~other 84 | 85 | @_check 86 | def __rtruediv__(self, other) -> "SymbolicMod": 87 | return ~self * other 88 | 89 | @_check 90 | def __floordiv__(self, other) -> "SymbolicMod": 91 | return self * ~other 92 | 93 | @_check 94 | def __rfloordiv__(self, other) -> "SymbolicMod": 95 | return ~self * other 96 | 97 | def __bytes__(self): 98 | return int(self.x).to_bytes((self.n.bit_length() + 7) // 8, byteorder="big") 99 | 100 | def __int__(self): 101 | return int(self.x) 102 | 103 | def __eq__(self, other): 104 | if type(other) is int: 105 | return self.x == other % self.n 106 | if type(other) is not SymbolicMod: 107 | return False 108 | return self.x == other.x and self.n == other.n 109 | 110 | def __ne__(self, other): 111 | return not self == other 112 | 113 | def __repr__(self): 114 | return str(self.x) 115 | 116 | def __hash__(self): 117 | return hash(("SymbolicMod", self.x, self.n)) 118 | 119 | def __pow__(self, n, _=None) -> "SymbolicMod": 120 | return self.__class__(pow(self.x, n), self.n) 121 | 122 | 123 | from pyecsca.ec.mod.base import _mod_classes # noqa 124 | 125 | _mod_classes["symbolic"] = SymbolicMod 126 | -------------------------------------------------------------------------------- /pyecsca/ec/mult/__init__.py: -------------------------------------------------------------------------------- 1 | """Provides several classes implementing different scalar multiplication algorithms.""" 2 | 3 | from .base import * 4 | from .binary import * 5 | from .comb import * 6 | from .fake import * 7 | from .fixed import * 8 | from .ladder import * 9 | from .naf import * 10 | from .window import * 11 | -------------------------------------------------------------------------------- /pyecsca/ec/mult/fake.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Callable 2 | from copy import deepcopy 3 | 4 | from pyecsca.ec.formula import ( 5 | AdditionFormula, 6 | DifferentialAdditionFormula, 7 | DoublingFormula, 8 | LadderFormula, 9 | NegationFormula, 10 | ScalingFormula, 11 | ) 12 | from pyecsca.ec.formula.fake import FakeAdditionFormula, FakeDifferentialAdditionFormula, \ 13 | FakeDoublingFormula, FakeLadderFormula, FakeNegationFormula, FakeScalingFormula 14 | from pyecsca.ec.mult import ScalarMultiplier 15 | from pyecsca.ec.params import DomainParameters 16 | 17 | 18 | fake_map = { 19 | AdditionFormula: FakeAdditionFormula, 20 | DifferentialAdditionFormula: FakeDifferentialAdditionFormula, 21 | DoublingFormula: FakeDoublingFormula, 22 | LadderFormula: FakeLadderFormula, 23 | NegationFormula: FakeNegationFormula, 24 | ScalingFormula: FakeScalingFormula, 25 | } 26 | 27 | 28 | def fake_mult( 29 | mult_class: Type[ScalarMultiplier], mult_factory: Callable, params: DomainParameters 30 | ) -> ScalarMultiplier: 31 | """ 32 | Get a multiplier with `FakeFormula`s. 33 | 34 | :param mult_class: The class of the scalar multiplier to use. 35 | :param mult_factory: A callable that takes the formulas and instantiates the multiplier. 36 | :param params: The domain parameters to use. 37 | :return: The multiplier. 38 | """ 39 | formulas = [] 40 | for formula, fake_formula in fake_map.items(): 41 | if formula in mult_class.requires: 42 | formulas.append(fake_formula(params.curve.coordinate_model)) 43 | mult = mult_factory(*formulas, short_circuit=False) 44 | return mult 45 | 46 | 47 | def turn_fake(mult: ScalarMultiplier) -> ScalarMultiplier: 48 | """ 49 | Turn a multiplier into a fake multiplier. 50 | 51 | :param mult: The multiplier to turn into a fake multiplier. 52 | :return: The multiplier with fake formulas. 53 | """ 54 | copy = deepcopy(mult) 55 | copy.short_circuit = False 56 | formulas = {} 57 | for key, formula in copy.formulas.items(): 58 | for real, fake in fake_map.items(): 59 | if isinstance(formula, real): 60 | formulas[key] = fake(formula.coordinate_model) 61 | copy.formulas = formulas 62 | return copy 63 | -------------------------------------------------------------------------------- /pyecsca/misc/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for miscellaneous things.""" 2 | -------------------------------------------------------------------------------- /pyecsca/misc/cache.py: -------------------------------------------------------------------------------- 1 | """Cache some things.""" 2 | 3 | from functools import lru_cache 4 | from sympy import sympify as _orig_sympify, simplify as _orig_simplify, count_ops 5 | from public import public 6 | 7 | 8 | @public 9 | @lru_cache(maxsize=256, typed=True) 10 | def sympify( 11 | a, locals=None, convert_xor=True, strict=False, rational=False, evaluate=None 12 | ): 13 | return _orig_sympify(a, locals, convert_xor, strict, rational, evaluate) 14 | 15 | 16 | @public 17 | @lru_cache(maxsize=256, typed=True) 18 | def simplify( 19 | expr, 20 | ratio=1.7, 21 | measure=count_ops, 22 | rational=False, 23 | inverse=False, 24 | doit=True, 25 | **kwargs, 26 | ): 27 | return _orig_simplify( 28 | expr, 29 | ratio=ratio, 30 | measure=measure, 31 | rational=rational, 32 | inverse=inverse, 33 | doit=doit, 34 | **kwargs, 35 | ) 36 | -------------------------------------------------------------------------------- /pyecsca/misc/utils.py: -------------------------------------------------------------------------------- 1 | """Just some utilities I promise.""" 2 | 3 | import sys 4 | from ast import parse 5 | from contextlib import contextmanager 6 | from typing import List, Any, Generator 7 | 8 | from pyecsca.misc.cfg import getconfig, TemporaryConfig 9 | 10 | from loky import ProcessPoolExecutor, as_completed, Future 11 | 12 | 13 | def pexec(s): 14 | """Parse with exec.""" 15 | return parse(s, mode="exec") 16 | 17 | 18 | def peval(s): 19 | """Parse with eval.""" 20 | return parse(s, mode="eval") 21 | 22 | 23 | def in_notebook() -> bool: 24 | """Test whether we are executing in Jupyter notebook.""" 25 | try: 26 | from IPython import get_ipython 27 | 28 | if "IPKernelApp" not in get_ipython().config: # pragma: no cover 29 | return False 30 | except ImportError: 31 | return False 32 | except AttributeError: 33 | return False 34 | return True 35 | 36 | 37 | def log(*args, **kwargs): 38 | """Log a message.""" 39 | if in_notebook() and getconfig().log.enabled: 40 | print(*args, **kwargs) 41 | 42 | 43 | def warn(*args, **kwargs): 44 | """Log a message.""" 45 | if in_notebook() and getconfig().log.enabled: 46 | print(*args, **kwargs, file=sys.stderr) 47 | 48 | 49 | @contextmanager 50 | def silent(): 51 | """Temporarily disable output.""" 52 | with TemporaryConfig() as cfg: 53 | cfg.log.enabled = False 54 | yield 55 | 56 | 57 | class TaskExecutor(ProcessPoolExecutor): 58 | """A simple ProcessPoolExecutor that keeps tracks of tasks that were submitted to it.""" 59 | 60 | keys: List[Any] 61 | """A list of keys that identify the futures.""" 62 | futures: List[Future] 63 | """A list of futures submitted to the executor.""" 64 | 65 | def __init__(self, *args, **kwargs): 66 | super().__init__(*args, **kwargs) 67 | self.keys = [] 68 | self.futures = [] 69 | 70 | def submit_task(self, key: Any, fn, /, *args, **kwargs): 71 | """Submit a task (function `fn`), identified by `key` and with `args` and `kwargs`.""" 72 | future = self.submit(fn, *args, **kwargs) 73 | self.futures.append(future) 74 | self.keys.append(key) 75 | return future 76 | 77 | @property 78 | def tasks(self): 79 | """A list of tasks that were submitted to this executor.""" 80 | return list(zip(self.keys, self.futures)) 81 | 82 | def as_completed(self) -> Generator[tuple[Any, Future], Any, None]: 83 | """Like `concurrent.futures.as_completed`, but yields a pair of key and future.""" 84 | for future in as_completed(self.futures): 85 | i = self.futures.index(future) 86 | yield self.keys[i], future 87 | self.futures = [] 88 | self.keys = [] 89 | -------------------------------------------------------------------------------- /pyecsca/sca/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for Side-Channel Analysis.""" 2 | 3 | from .re import * 4 | from .scope import * 5 | from .target import * 6 | from .trace import * 7 | from .trace_set import * 8 | from .stacked_traces import * 9 | from .attack import * 10 | -------------------------------------------------------------------------------- /pyecsca/sca/attack/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for attacks.""" 2 | 3 | from .leakage_model import * 4 | -------------------------------------------------------------------------------- /pyecsca/sca/re/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for reverse-engineering.""" 2 | 3 | from .rpa import * 4 | from .zvp import * 5 | from .structural import * 6 | -------------------------------------------------------------------------------- /pyecsca/sca/re/base.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod, ABC 2 | from typing import Optional, Any, Set 3 | 4 | from public import public 5 | 6 | from pyecsca.sca.re.tree import Tree 7 | 8 | 9 | @public 10 | class RE(ABC): 11 | """A base class for Reverse-Engineering methods.""" 12 | 13 | tree: Optional[Tree] = None 14 | """The RE tree (if any).""" 15 | configs: Set[Any] 16 | """The set of configurations to reverse-engineer.""" 17 | 18 | def __init__(self, configs: Set[Any]): 19 | self.configs = configs 20 | 21 | @abstractmethod 22 | def build_tree(self, *args, **kwargs): 23 | """Build the RE tree.""" 24 | pass 25 | 26 | @abstractmethod 27 | def run(self, *args, **kwargs): 28 | """Run the reverse-engineering (and obtain a result set of possible configurations).""" 29 | pass 30 | -------------------------------------------------------------------------------- /pyecsca/sca/re/structural.py: -------------------------------------------------------------------------------- 1 | """Provides functionality for structural reverse-engineering.""" 2 | -------------------------------------------------------------------------------- /pyecsca/sca/scope/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for handling oscilloscopes for measurement of power/EM traces.""" 2 | 3 | from typing import Type 4 | 5 | from .base import * 6 | 7 | has_picoscope = False 8 | has_picosdk = False 9 | has_chipwhisperer = False 10 | 11 | try: 12 | import picoscope 13 | 14 | has_picoscope = True 15 | except ImportError: # pragma: no cover 16 | pass 17 | 18 | try: 19 | import picosdk 20 | 21 | has_picosdk = True 22 | except ImportError: # pragma: no cover 23 | pass 24 | 25 | try: 26 | import chipwhisperer 27 | 28 | has_chipwhisperer = True 29 | except ImportError: # pragma: no cover 30 | pass 31 | 32 | PicoScope: Type[Scope] 33 | if has_picoscope: 34 | from .picoscope_alt import * 35 | 36 | PicoScope = PicoScopeAlt 37 | elif has_picosdk: 38 | from .picoscope_sdk import * 39 | 40 | PicoScope = PicoScopeSdk 41 | 42 | if has_chipwhisperer: 43 | from .chipwhisperer import * 44 | -------------------------------------------------------------------------------- /pyecsca/sca/scope/chipwhisperer.py: -------------------------------------------------------------------------------- 1 | """Provides an oscilloscope class using the ChipWhisperer-Lite scope.""" 2 | 3 | from typing import Optional, Tuple, Sequence, Set 4 | 5 | import numpy as np 6 | from chipwhisperer.capture.scopes.OpenADC import OpenADC 7 | from public import public 8 | 9 | from pyecsca.sca.scope.base import Scope, SampleType 10 | from pyecsca.sca.trace import Trace 11 | 12 | 13 | @public 14 | class ChipWhispererScope(Scope): # pragma: no cover 15 | """A ChipWhisperer based scope.""" 16 | 17 | def __init__(self, scope: OpenADC): 18 | super().__init__() 19 | self.scope = scope 20 | self.triggers: Set[str] = set() 21 | 22 | def open(self) -> None: 23 | self.scope.con() 24 | 25 | @property 26 | def channels(self) -> Sequence[str]: 27 | return [] 28 | 29 | def setup_frequency( 30 | self, frequency: int, pretrig: int, posttrig: int 31 | ) -> Tuple[int, int]: 32 | if pretrig != 0: 33 | raise ValueError("ChipWhisperer does not support pretrig samples.") 34 | self.scope.clock.clkgen_freq = frequency 35 | self.scope.adc.samples = posttrig 36 | return self.scope.clock.clkgen_freq, self.scope.adc.samples 37 | 38 | def setup_channel( 39 | self, channel: str, coupling: str, range: float, offset: float, enable: bool 40 | ) -> None: 41 | pass # Nothing to setup 42 | 43 | def setup_trigger( 44 | self, 45 | channel: str, 46 | threshold: float, 47 | direction: str, 48 | delay: int, 49 | timeout: int, 50 | enable: bool, 51 | ) -> None: 52 | if enable: 53 | self.triggers.add(channel) 54 | elif channel in self.triggers: 55 | self.triggers.remove(channel) 56 | self.scope.adc.basic_mode = direction 57 | self.scope.trigger.triggers = " OR ".join(self.triggers) 58 | 59 | def setup_capture(self, channel: str, enable: bool) -> None: 60 | pass # Nothing to setup 61 | 62 | def arm(self) -> None: 63 | self.scope.arm() 64 | 65 | def capture(self, timeout: Optional[int] = None) -> bool: 66 | return not self.scope.capture() 67 | 68 | def retrieve( 69 | self, channel: str, type: SampleType, dtype=np.float16 70 | ) -> Optional[Trace]: 71 | data = self.scope.get_last_trace() 72 | if data is None: 73 | return None 74 | return Trace( 75 | np.array(data, dtype=dtype), 76 | {"sampling_frequency": self.scope.clock.clkgen_freq, "channel": channel}, 77 | ) 78 | 79 | def stop(self) -> None: 80 | pass # Nothing to do 81 | 82 | def close(self) -> None: 83 | self.scope.dis() 84 | -------------------------------------------------------------------------------- /pyecsca/sca/stacked_traces/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for efficient handling of stacked traces and GPU acceleration.""" 2 | 3 | from .stacked_traces import * 4 | from .combine import * 5 | -------------------------------------------------------------------------------- /pyecsca/sca/stacked_traces/stacked_traces.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from public import public 3 | from typing import Any, Mapping, Sequence, Optional 4 | 5 | from pyecsca.sca.trace_set.base import TraceSet 6 | 7 | 8 | @public 9 | class StackedTraces: 10 | """Samples of multiple traces and metadata""" 11 | 12 | meta: Mapping[str, Any] 13 | samples: np.ndarray 14 | 15 | # TODO: Split metadata into common and per-trace 16 | def __init__( 17 | self, samples: np.ndarray, meta: Optional[Mapping[str, Any]] = None 18 | ) -> None: 19 | if meta is None: 20 | meta = {} 21 | self.meta = meta 22 | self.samples = samples 23 | 24 | @classmethod 25 | def fromarray( 26 | cls, traces: Sequence[np.ndarray], meta: Optional[Mapping[str, Any]] = None 27 | ) -> "StackedTraces": 28 | if meta is None: 29 | meta = {} 30 | ts = list(traces) 31 | min_samples = min(map(len, ts)) 32 | for i, t in enumerate(ts): 33 | ts[i] = t[:min_samples] 34 | stacked = np.stack(ts) 35 | return cls(stacked, meta) 36 | 37 | @classmethod 38 | def fromtraceset(cls, traceset: TraceSet) -> "StackedTraces": 39 | traces = [t.samples for t in traceset] 40 | return cls.fromarray(traces) 41 | 42 | def __len__(self): 43 | return self.samples.shape[0] 44 | 45 | def __getitem__(self, index): 46 | return self.samples[index] 47 | 48 | def __iter__(self): 49 | yield from self.samples 50 | -------------------------------------------------------------------------------- /pyecsca/sca/target/PCSC.py: -------------------------------------------------------------------------------- 1 | """Provides a smartcard target communicating via PC/SC (Personal Computer/Smart Card).""" 2 | 3 | from typing import Union, Optional 4 | 5 | from public import public 6 | from smartcard.CardConnection import CardConnection 7 | from smartcard.System import readers 8 | from smartcard.pcsc.PCSCCardConnection import PCSCCardConnection 9 | from smartcard.pcsc.PCSCReader import PCSCReader 10 | 11 | from pyecsca.sca.target.ISO7816 import ( 12 | ISO7816Target, 13 | CommandAPDU, 14 | ResponseAPDU, 15 | ISO7816, 16 | CardProtocol, 17 | CardConnectionException, 18 | ) 19 | 20 | 21 | @public 22 | class PCSCTarget(ISO7816Target): # pragma: no cover 23 | """Smartcard target communicating via PCSC.""" 24 | 25 | def __init__(self, reader: Union[str, PCSCReader]): 26 | if isinstance(reader, str): 27 | rs = readers() 28 | for r in rs: 29 | if r.name == reader: 30 | self.reader = r 31 | break 32 | else: 33 | raise ValueError(f"Reader '{reader}' not found.") 34 | else: 35 | self.reader = reader 36 | self.connection: PCSCCardConnection = self.reader.createConnection() 37 | 38 | def connect(self, protocol: Optional[CardProtocol] = None): 39 | proto = CardConnection.T0_protocol | CardConnection.T1_protocol 40 | if protocol == CardProtocol.T0: 41 | proto = CardConnection.T0_protocol 42 | elif protocol == CardProtocol.T1: 43 | proto = CardConnection.T1_protocol 44 | try: 45 | self.connection.connect(proto) 46 | except: # noqa 47 | raise CardConnectionException() 48 | 49 | @property 50 | def atr(self) -> bytes: 51 | return bytes(self.connection.getATR()) 52 | 53 | def select(self, aid: bytes) -> bool: 54 | apdu = CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid) 55 | resp = self.send_apdu(apdu) 56 | return resp.sw == ISO7816.SW_NO_ERROR 57 | 58 | def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU: 59 | resp, sw1, sw2 = self.connection.transmit(list(bytes(apdu))) 60 | return ResponseAPDU(bytes(resp), sw1 << 8 | sw2) 61 | 62 | def disconnect(self): 63 | self.connection.disconnect() 64 | -------------------------------------------------------------------------------- /pyecsca/sca/target/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for communicating with targets of measurement.""" 2 | 3 | from .ISO7816 import * 4 | from .base import * 5 | from .serial import * 6 | from .simpleserial import * 7 | from .binary import * 8 | from .flash import * 9 | from .leakage import * 10 | 11 | has_chipwhisperer: bool = False 12 | has_pyscard: bool = False 13 | has_leia: bool = False 14 | 15 | try: 16 | import chipwhisperer 17 | 18 | has_chipwhisperer = True 19 | except ImportError: # pragma: no cover 20 | pass 21 | 22 | try: 23 | import smartcard 24 | 25 | has_pyscard = True 26 | except ImportError: # pragma: no cover 27 | pass 28 | 29 | try: 30 | import smartleia 31 | 32 | has_leia = True 33 | except ImportError: # pragma: no cover 34 | pass 35 | 36 | from .ectester import ECTesterTarget # noqa 37 | 38 | if has_pyscard: 39 | from .PCSC import * 40 | 41 | if has_leia: 42 | from .leia import * 43 | 44 | if has_chipwhisperer: 45 | from .chipwhisperer import * 46 | -------------------------------------------------------------------------------- /pyecsca/sca/target/base.py: -------------------------------------------------------------------------------- 1 | """Provides an abstract base class for targets.""" 2 | 3 | from abc import ABC, abstractmethod 4 | 5 | from public import public 6 | 7 | 8 | @public 9 | class Target(ABC): 10 | """A target.""" 11 | 12 | @abstractmethod 13 | def connect(self): 14 | """Connect to the target device.""" 15 | raise NotImplementedError 16 | 17 | @abstractmethod 18 | def disconnect(self): 19 | """Disconnect from the target device.""" 20 | raise NotImplementedError 21 | -------------------------------------------------------------------------------- /pyecsca/sca/target/binary.py: -------------------------------------------------------------------------------- 1 | """Provides a binary target class which represents a target that is a runnable binary on the host.""" 2 | 3 | import subprocess 4 | from subprocess import Popen 5 | from typing import Optional, Union, List 6 | 7 | from public import public 8 | 9 | from pyecsca.sca.target.serial import SerialTarget 10 | 11 | 12 | @public 13 | class BinaryTarget(SerialTarget): 14 | """Binary target that is runnable on the host and communicates using the stdin/stdout streams.""" 15 | 16 | binary: List[str] 17 | process: Optional[Popen] = None 18 | debug_output: bool 19 | 20 | def __init__( 21 | self, binary: Union[str, List[str]], debug_output: bool = False, **kwargs 22 | ): 23 | super().__init__() 24 | if not isinstance(binary, (str, list)): 25 | raise TypeError 26 | if isinstance(binary, str): 27 | binary = [binary] 28 | self.binary = binary 29 | self.debug_output = debug_output 30 | 31 | def connect(self): 32 | self.process = Popen( 33 | self.binary, 34 | stdin=subprocess.PIPE, 35 | stdout=subprocess.PIPE, 36 | text=True, 37 | bufsize=1, 38 | ) 39 | 40 | def write(self, data: bytes) -> None: 41 | if self.process is None: 42 | raise ValueError 43 | if self.debug_output: 44 | print(">>", data.decode()) 45 | if self.process.stdin: 46 | self.process.stdin.write(data.decode()) 47 | self.process.stdin.flush() 48 | 49 | def read(self, num: int = 0, timeout: int = 0) -> bytes: 50 | if self.process is None: 51 | raise ValueError 52 | if self.process.stdout: 53 | if num != 0: 54 | read = self.process.stdout.readline(num) 55 | else: 56 | read = self.process.stdout.readline() 57 | else: 58 | read = bytes() # pragma: no cover 59 | if self.debug_output: 60 | print("<<", read, end="") 61 | return read.encode() 62 | 63 | def disconnect(self): 64 | if self.process is None: 65 | return 66 | if self.process.stdin is not None: 67 | self.process.stdin.close() 68 | if self.process.stdout is not None: 69 | self.process.stdout.close() 70 | self.process.terminate() 71 | self.process.wait() 72 | -------------------------------------------------------------------------------- /pyecsca/sca/target/chipwhisperer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a `ChipWhisperer `_ target class. 3 | 4 | ChipWhisperer is a side-channel analysis tool and framework. A ChipWhisperer target is one 5 | that uses the ChipWhisperer's SimpleSerial communication protocol and is communicated with 6 | using ChipWhisperer-Lite or Pro. 7 | """ 8 | 9 | from time import sleep 10 | 11 | import chipwhisperer as cw 12 | from chipwhisperer.capture.scopes import ScopeTypes 13 | from chipwhisperer.capture.targets.SimpleSerial import SimpleSerial 14 | from public import public 15 | 16 | from pyecsca.sca.target.flash import Flashable 17 | from pyecsca.sca.target.simpleserial import SimpleSerialTarget 18 | 19 | 20 | @public 21 | class ChipWhispererTarget(Flashable, SimpleSerialTarget): # pragma: no cover 22 | """ChipWhisperer-based target, using the SimpleSerial-ish protocol and communicating using ChipWhisperer-Lite/Pro.""" 23 | 24 | def __init__(self, target: SimpleSerial, scope: ScopeTypes, programmer, **kwargs): 25 | super().__init__() 26 | self.target = target 27 | self.scope = scope 28 | self.programmer = programmer 29 | 30 | def connect(self): 31 | self.target.con(self.scope, noflush=True) 32 | self.target.baud = 115200 33 | sleep(0.5) 34 | 35 | def flash(self, fw_path: str) -> bool: 36 | try: 37 | cw.program_target(self.scope, self.programmer, fw_path) 38 | except Exception as e: 39 | print(e) 40 | return False 41 | return True 42 | 43 | def write(self, data: bytes) -> None: 44 | self.target.flush() 45 | self.target.write(data.decode()) 46 | 47 | def read(self, num: int = 0, timeout: int = 0) -> bytes: 48 | return self.target.read(num, timeout).encode() 49 | 50 | def reset(self): 51 | self.scope.io.nrst = "low" 52 | sleep(0.05) 53 | self.scope.io.nrst = "high" 54 | sleep(0.05) 55 | 56 | def disconnect(self): 57 | self.target.dis() 58 | -------------------------------------------------------------------------------- /pyecsca/sca/target/flash.py: -------------------------------------------------------------------------------- 1 | """Provides a mix-in class of a flashable target (e.g. one where the code gets flashed to it before running).""" 2 | 3 | from public import public 4 | from abc import ABC, abstractmethod 5 | 6 | 7 | @public 8 | class Flashable(ABC): 9 | """A flashable target.""" 10 | 11 | @abstractmethod 12 | def flash(self, fw_path: str) -> bool: 13 | """ 14 | Flash the firmware at `fw_path` to the target. 15 | 16 | :param fw_path: The path to the firmware blob. 17 | :return: Whether the flashing was successful. 18 | """ 19 | raise NotImplementedError 20 | -------------------------------------------------------------------------------- /pyecsca/sca/target/leia.py: -------------------------------------------------------------------------------- 1 | """Provides a smartcard target communicating via the LEIA board in solo mode.""" 2 | 3 | from typing import Optional 4 | 5 | from smartleia import LEIA, create_APDU_from_bytes, T 6 | 7 | from pyecsca.sca.target.ISO7816 import ( 8 | ISO7816Target, 9 | CommandAPDU, 10 | ResponseAPDU, 11 | ISO7816, 12 | CardProtocol, 13 | CardConnectionException, 14 | ) 15 | 16 | 17 | class LEIATarget(ISO7816Target): # pragma: no cover 18 | """Smartcard target communicating via LEIA in solo mode.""" 19 | 20 | def __init__(self, leia: LEIA): 21 | self.leia = leia 22 | 23 | @property 24 | def atr(self) -> bytes: 25 | return self.leia.get_ATR().normalized() 26 | 27 | @property 28 | def card_present(self) -> bool: 29 | return self.leia.is_card_inserted() 30 | 31 | def select(self, aid: bytes) -> bool: 32 | apdu = CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid) 33 | resp = self.send_apdu(apdu) 34 | return resp.sw == ISO7816.SW_NO_ERROR 35 | 36 | def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU: 37 | leia_apdu = create_APDU_from_bytes(bytes(apdu)) 38 | resp = self.leia.send_APDU(leia_apdu) 39 | return ResponseAPDU(bytes(resp.data), resp.sw1 << 8 | resp.sw2) 40 | 41 | def connect(self, protocol: Optional[CardProtocol] = None): 42 | proto = T.AUTO 43 | if protocol == CardProtocol.T0: 44 | proto = T.T0 45 | elif protocol == CardProtocol.T1: 46 | proto = T.T1 47 | try: 48 | self.leia.configure_smartcard(protocol_to_use=proto) 49 | except: # noqa 50 | raise CardConnectionException() 51 | 52 | def disconnect(self): 53 | pass 54 | -------------------------------------------------------------------------------- /pyecsca/sca/target/serial.py: -------------------------------------------------------------------------------- 1 | """Provides an abstract serial target, that communicates by writing and reading a stream of bytes.""" 2 | 3 | from abc import abstractmethod 4 | 5 | from public import public 6 | 7 | from pyecsca.sca.target.base import Target 8 | 9 | 10 | @public 11 | class SerialTarget(Target): 12 | """Serial target.""" 13 | 14 | @abstractmethod 15 | def write(self, data: bytes) -> None: 16 | """ 17 | Write the :paramref:`~.write.data` bytes to the target's serial input. 18 | 19 | :param data: The data to write. 20 | """ 21 | raise NotImplementedError 22 | 23 | @abstractmethod 24 | def read(self, num: int = 0, timeout: int = 0) -> bytes: 25 | """ 26 | Read upto :paramref:`~.read.num` bytes or until :paramref:`~.read.timeout` milliseconds from the target's serial output. 27 | 28 | :param num: The number of bytes to read, ``0`` for all available. 29 | :param timeout: The timeout in milliseconds. 30 | :return: The bytes read. 31 | """ 32 | raise NotImplementedError 33 | -------------------------------------------------------------------------------- /pyecsca/sca/target/simpleserial.py: -------------------------------------------------------------------------------- 1 | """Provides an abstract target class communicating using the `ChipWhisperer's `_ SimpleSerial protocol.""" 2 | 3 | from abc import ABC 4 | from time import time_ns, sleep 5 | from typing import Mapping, Union 6 | 7 | from public import public 8 | 9 | from pyecsca.sca.target.serial import SerialTarget 10 | 11 | 12 | @public 13 | class SimpleSerialMessage: 14 | """A SimpleSerial message consisting of a starting character and a hexadecimal string.""" 15 | 16 | char: str 17 | data: str 18 | 19 | def __init__(self, char: str, data: str): 20 | self.char = char 21 | self.data = data 22 | 23 | @staticmethod 24 | def from_raw(raw: Union[str, bytes]) -> "SimpleSerialMessage": 25 | if isinstance(raw, bytes): 26 | raw = raw.decode() 27 | return SimpleSerialMessage(raw[0], raw[1:]) 28 | 29 | def __bytes__(self): 30 | return str(self).encode() 31 | 32 | def __str__(self): 33 | return self.char + self.data 34 | 35 | def __repr__(self): 36 | return str(self) 37 | 38 | 39 | @public 40 | class SimpleSerialTarget(SerialTarget, ABC): 41 | """A SimpleSerial-ish target, sends and receives SimpleSerial messages over a serial link.""" 42 | 43 | def recv_msgs(self, timeout: int) -> Mapping[str, SimpleSerialMessage]: 44 | """ 45 | Receive :py:class:`SimpleSerialMessage` messages, while waiting upto :paramref:`~.recv_msgs.timeout` seconds. 46 | 47 | :param timeout: How long to wait. 48 | :return: The received messages with their char. 49 | """ 50 | start = time_ns() // 1000000 51 | buffer = bytes() 52 | # Expect "z00" confirmation response, as in SimpleSerial 1. 53 | while not buffer.endswith(b"z00\n"): 54 | wait = timeout - ((time_ns() // 1000000) - start) 55 | if wait <= 0: 56 | break 57 | buffer += self.read(1 if not buffer else 0, wait) 58 | if not buffer: 59 | return {} 60 | msgs = buffer.split(b"\n") 61 | if buffer.endswith(b"\n"): 62 | msgs.pop() 63 | 64 | result = {} 65 | for raw in msgs: 66 | msg = SimpleSerialMessage.from_raw(raw) 67 | result[msg.char] = msg 68 | return result 69 | 70 | def send_cmd( 71 | self, cmd: SimpleSerialMessage, timeout: int 72 | ) -> Mapping[str, SimpleSerialMessage]: 73 | """ 74 | Send a :py:class:`SimpleSerialMessage` and receive the response messages that the command produces, within a :paramref:`~.send_cmd.timeout`. 75 | 76 | :param cmd: The command message to send. 77 | :param timeout: The timeout value to wait for the responses. 78 | :return: A mapping of the starting character of the message to the message. 79 | """ 80 | data = bytes(cmd) 81 | for i in range(0, len(data), 64): 82 | chunk = data[i : i + 64] 83 | sleep(0.010) 84 | self.write(chunk) 85 | self.write(b"\n") 86 | return self.recv_msgs(timeout) 87 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for manipulating traces.""" 2 | 3 | from .align import * 4 | from .combine import * 5 | from .edit import * 6 | from .filter import * 7 | from .match import * 8 | from .plot import * 9 | from .process import * 10 | from .sampling import * 11 | from .test import * 12 | from .trace import * 13 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/edit.py: -------------------------------------------------------------------------------- 1 | """Provides functions for editing traces as if they were tapes you can trim, reverse, etc.""" 2 | 3 | import numpy as np 4 | from public import public 5 | from typing import Union, Tuple, Any, Optional 6 | 7 | from pyecsca.sca.trace.trace import Trace 8 | 9 | 10 | @public 11 | def trim(trace: Trace, start: Optional[int] = None, end: Optional[int] = None) -> Trace: 12 | """ 13 | Trim the `trace` samples, output contains samples between the `start` and `end` indices. 14 | 15 | :param trace: The trace to trim. 16 | :param start: Starting index (inclusive). 17 | :param end: Ending index (exclusive). 18 | :return: 19 | """ 20 | if start is None: 21 | start = 0 22 | if end is None: 23 | end = len(trace.samples) 24 | if start > end: 25 | raise ValueError("Invalid trim arguments.") 26 | return trace.with_samples(trace.samples[start:end].copy()) 27 | 28 | 29 | @public 30 | def reverse(trace: Trace) -> Trace: 31 | """ 32 | Reverse the samples of the `trace`. 33 | 34 | :param trace: The trace to reverse. 35 | :return: 36 | """ 37 | return trace.with_samples(np.flipud(trace.samples).copy()) 38 | 39 | 40 | @public 41 | def pad( 42 | trace: Trace, 43 | lengths: Union[Tuple[int, int], int], 44 | values: Union[Tuple[Any, Any], Any] = (0, 0), 45 | ) -> Trace: 46 | """ 47 | Pad the samples of the `trace` by `values` at the beginning and end. 48 | 49 | :param trace: The trace to pad. 50 | :param lengths: How much to pad at the beginning and end, either symmetric (if integer) or asymmetric (if tuple). 51 | :param values: What value to pad with, either symmetric or asymmetric (if tuple). 52 | :return: 53 | """ 54 | if not isinstance(lengths, tuple): 55 | lengths = (lengths, lengths) 56 | if not isinstance(values, tuple): 57 | values = (values, values) 58 | return trace.with_samples( 59 | np.pad(trace.samples, lengths, "constant", constant_values=values) 60 | ) 61 | 62 | 63 | @public 64 | def stretch(trace: Trace, length: int) -> Trace: 65 | """ 66 | Stretch (or squeeze) a trace linearly to fit a given length. 67 | 68 | :param trace: The trace to stretch (or squeeze). 69 | :param length: The length it should be. 70 | :return: 71 | """ 72 | current_indices = np.arange(len(trace)) 73 | target_indices = np.linspace(0, len(trace) - 1, length) 74 | return trace.with_samples(np.interp(target_indices, current_indices, trace.samples)) 75 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/filter.py: -------------------------------------------------------------------------------- 1 | """Provides functions for filtering traces using digital (low/high/band)-pass and bandstop filters.""" 2 | 3 | from public import public 4 | from scipy.signal import butter, lfilter 5 | from typing import Union, Tuple 6 | 7 | from pyecsca.sca.trace.trace import Trace 8 | 9 | 10 | def _filter_any( 11 | trace: Trace, 12 | sampling_frequency: int, 13 | cutoff: Union[int, Tuple[int, int]], 14 | band_type: str, 15 | ) -> Trace: 16 | nyq = 0.5 * sampling_frequency 17 | if not isinstance(cutoff, int): 18 | b, a = butter( 19 | 6, 20 | tuple(map(lambda x: x / nyq, cutoff)), 21 | btype=band_type, 22 | analog=False, 23 | output="ba", 24 | ) 25 | else: 26 | b, a = butter(6, cutoff / nyq, btype=band_type, analog=False, output="ba") 27 | return trace.with_samples(lfilter(b, a, trace.samples)) 28 | 29 | 30 | @public 31 | def filter_lowpass(trace: Trace, sampling_frequency: int, cutoff: int) -> Trace: 32 | """ 33 | Apply a lowpass digital filter (Butterworth) to `trace`, given `sampling_frequency` and `cutoff` frequency. 34 | 35 | :param trace: 36 | :param sampling_frequency: 37 | :param cutoff: 38 | :return: 39 | """ 40 | return _filter_any(trace, sampling_frequency, cutoff, "lowpass") 41 | 42 | 43 | @public 44 | def filter_highpass(trace: Trace, sampling_frequency: int, cutoff: int) -> Trace: 45 | """ 46 | Apply a highpass digital filter (Butterworth) to `trace`, given `sampling_frequency` and `cutoff` frequency. 47 | 48 | :param trace: 49 | :param sampling_frequency: 50 | :param cutoff: 51 | :return: 52 | """ 53 | return _filter_any(trace, sampling_frequency, cutoff, "highpass") 54 | 55 | 56 | @public 57 | def filter_bandpass( 58 | trace: Trace, sampling_frequency: int, low: int, high: int 59 | ) -> Trace: 60 | """ 61 | Apply a bandpass digital filter (Butterworth) to `trace`, given `sampling_frequency`, with the passband from `low` to `high`. 62 | 63 | :param trace: 64 | :param sampling_frequency: 65 | :param low: 66 | :param high: 67 | :return: 68 | """ 69 | return _filter_any(trace, sampling_frequency, (low, high), "bandpass") 70 | 71 | 72 | @public 73 | def filter_bandstop( 74 | trace: Trace, sampling_frequency: int, low: int, high: int 75 | ) -> Trace: 76 | """ 77 | Apply a bandstop digital filter (Butterworth) to `trace`, given `sampling_frequency`, with the stopband from `low` to `high`. 78 | 79 | :param trace: 80 | :param sampling_frequency: 81 | :param low: 82 | :param high: 83 | :return: 84 | """ 85 | return _filter_any(trace, sampling_frequency, (low, high), "bandstop") 86 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/match.py: -------------------------------------------------------------------------------- 1 | """Provides functions for matching a pattern within a trace to it.""" 2 | 3 | import numpy as np 4 | from scipy.signal import find_peaks 5 | from public import public 6 | from typing import List 7 | 8 | from pyecsca.sca.trace.process import normalize 9 | from pyecsca.sca.trace.edit import trim 10 | from pyecsca.sca.trace.trace import Trace 11 | 12 | 13 | @public 14 | def match_pattern(trace: Trace, pattern: Trace, threshold: float = 0.8) -> List[int]: 15 | """ 16 | Match a :paramref:`~.match_pattern.pattern` to a :paramref:`~.match_pattern.trace`. 17 | 18 | Return the indices where the pattern matches, e.g. those where correlation 19 | of the two traces has peaks larger than :paramref:`~.match_pattern.threshold`. 20 | Uses the :py:func:`scipy.signal.find_peaks` function. 21 | 22 | :param trace: The trace to match into. 23 | :param pattern: The pattern to match. 24 | :param threshold: The threshold passed to :py:func:`scipy.signal.find_peaks` as a ``prominence`` value. 25 | :return: Indices where the pattern matches. 26 | """ 27 | normalized = normalize(trace) 28 | pattern_samples = normalize(pattern).samples 29 | correlation = np.correlate(normalized.samples, pattern_samples, "same") 30 | correlation = (correlation - np.mean(correlation)) / (np.max(correlation)) 31 | peaks, props = find_peaks(correlation, prominence=(threshold, None)) 32 | pairs = sorted(zip(peaks, props["prominences"]), key=lambda it: it[1], reverse=True) 33 | half = len(pattern_samples) // 2 34 | filtered_peaks: List[int] = [] 35 | for peak, _ in pairs: 36 | if not filtered_peaks: 37 | filtered_peaks.append(peak - half) 38 | else: 39 | for other_peak in filtered_peaks: 40 | if abs((peak - half) - other_peak) <= len(pattern_samples): 41 | break 42 | else: 43 | filtered_peaks.append(peak - half) 44 | return filtered_peaks 45 | 46 | 47 | @public 48 | def match_part( 49 | trace: Trace, offset: int, length: int, threshold: float = 0.8 50 | ) -> List[int]: 51 | """ 52 | Match a part of a :paramref:`~.match_part.trace` starting at :paramref:`~.match_part.offset` of :paramref:`~.match_part.length` to the :paramref:`~.match_part.trace`. 53 | 54 | Returns indices where the pattern matches, e.g. those where correlation of the two 55 | traces has peaks larger than :paramref:`~.match_part.threshold`. 56 | Uses the :py:func:`scipy.signal.find_peaks` function. 57 | 58 | :param trace: The trace to match into. 59 | :param offset: The start of the pattern in the trace to match. 60 | :param length: The length of the pattern in the trace to match. 61 | :param threshold: The threshold passed to :py:func:`scipy.signal.find_peaks` as a ``prominence`` value. 62 | :return: Indices where the part of the trace matches matches. 63 | """ 64 | return match_pattern(trace, trim(trace, offset, offset + length), threshold) 65 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/plot.py: -------------------------------------------------------------------------------- 1 | """Provides functions for plotting traces.""" 2 | 3 | from functools import reduce 4 | 5 | import holoviews as hv 6 | from holoviews.operation.datashader import datashade 7 | from public import public 8 | 9 | from pyecsca.sca.trace.trace import Trace 10 | 11 | 12 | @public 13 | def save_figure(figure, fname: str): # pragma: no cover 14 | hv.save(figure, fname + ".html", fmt="html") 15 | 16 | 17 | @public 18 | def save_figure_png(figure, fname: str): # pragma: no cover 19 | hv.save(figure, fname + ".png", fmt="png") 20 | 21 | 22 | @public 23 | def save_figure_svg(figure, fname: str): # pragma: no cover 24 | hv.save(figure, fname + ".svg", fmt="svg") 25 | 26 | 27 | @public 28 | def plot_trace(trace: Trace, **kwargs): # pragma: no cover 29 | line = hv.Curve((range(len(trace)), trace.samples), kdims="x", vdims="y", **kwargs) 30 | return datashade(line) 31 | 32 | 33 | @public 34 | def plot_traces(*traces: Trace, **kwargs): # pragma: no cover 35 | _cmaps = [ 36 | ["lightblue", "darkblue"], 37 | ["lightcoral", "red"], 38 | ["lime", "green"], 39 | ["orange", "darkorange"], 40 | ["plum", "deeppink"], 41 | ["peru", "chocolate"], 42 | ["cyan", "darkcyan"], 43 | ] 44 | dss = [] 45 | for i, trace in enumerate(traces): 46 | line = hv.Curve( 47 | (range(len(trace)), trace.samples), kdims="x", vdims="y", **kwargs 48 | ) 49 | dss.append(datashade(line, cmap=_cmaps[i % len(_cmaps)])) 50 | return reduce(lambda x, y: x * y, dss) 51 | -------------------------------------------------------------------------------- /pyecsca/sca/trace/sampling.py: -------------------------------------------------------------------------------- 1 | """Provides downsampling functions for traces.""" 2 | 3 | from typing import cast 4 | 5 | import numpy as np 6 | from public import public 7 | from scipy.signal import decimate 8 | 9 | from pyecsca.sca.trace.trace import Trace 10 | 11 | 12 | @public 13 | def downsample_average(trace: Trace, factor: int = 2) -> Trace: 14 | """ 15 | Downsample samples of `trace` by `factor` by averaging `factor` consecutive samples in non-intersecting windows. 16 | 17 | :param trace: 18 | :param factor: 19 | :return: 20 | """ 21 | resized = np.resize( 22 | trace.samples, len(trace.samples) - (len(trace.samples) % factor) 23 | ) 24 | result_samples = cast( 25 | np.ndarray, 26 | resized.reshape(-1, factor) 27 | .mean(axis=1) 28 | .astype(trace.samples.dtype, copy=False), 29 | ) 30 | return trace.with_samples(result_samples) 31 | 32 | 33 | @public 34 | def downsample_pick(trace: Trace, factor: int = 2, offset: int = 0) -> Trace: 35 | """ 36 | Downsample samples of `trace` by `factor` by picking each `factor`-th sample, starting at `offset`. 37 | 38 | :param trace: 39 | :param factor: 40 | :param offset: 41 | :return: 42 | """ 43 | result_samples = trace.samples[offset::factor].copy() 44 | return trace.with_samples(result_samples) 45 | 46 | 47 | @public 48 | def downsample_max(trace: Trace, factor: int = 2) -> Trace: 49 | """ 50 | Downsample samples of `trace` by `factor` by taking the maximum out of `factor` consecutive samples in non-intersecting windows. 51 | 52 | :param trace: 53 | :param factor: 54 | :return: 55 | """ 56 | resized = np.resize( 57 | trace.samples, len(trace.samples) - (len(trace.samples) % factor) 58 | ) 59 | result_samples = cast( 60 | np.ndarray, 61 | resized.reshape(-1, factor).max(axis=1).astype(trace.samples.dtype, copy=False), 62 | ) 63 | return trace.with_samples(result_samples) 64 | 65 | 66 | @public 67 | def downsample_min(trace: Trace, factor: int = 2) -> Trace: 68 | """ 69 | Downsample samples of `trace` by `factor` by taking the minimum out of `factor` consecutive samples in non-intersecting windows. 70 | 71 | :param trace: 72 | :param factor: 73 | :return: 74 | """ 75 | resized = np.resize( 76 | trace.samples, len(trace.samples) - (len(trace.samples) % factor) 77 | ) 78 | result_samples = cast( 79 | np.ndarray, 80 | resized.reshape(-1, factor).min(axis=1).astype(trace.samples.dtype, copy=False), 81 | ) 82 | return trace.with_samples(result_samples) 83 | 84 | 85 | @public 86 | def downsample_decimate(trace: Trace, factor: int = 2) -> Trace: 87 | """ 88 | Downsample samples of `trace` by `factor` by decimating. 89 | 90 | :param trace: 91 | :param factor: 92 | :return: 93 | """ 94 | result_samples = decimate(trace.samples, factor) 95 | return trace.with_samples(result_samples) 96 | -------------------------------------------------------------------------------- /pyecsca/sca/trace_set/__init__.py: -------------------------------------------------------------------------------- 1 | """Package for manipulating, reading and writing trace sets.""" 2 | 3 | from .base import * 4 | from .inspector import * 5 | from .chipwhisperer import * 6 | from .pickle import * 7 | from .hdf5 import * 8 | -------------------------------------------------------------------------------- /pyecsca/sca/trace_set/base.py: -------------------------------------------------------------------------------- 1 | """Provides a base traceset class.""" 2 | 3 | from pathlib import Path 4 | from typing import List, Union, BinaryIO 5 | 6 | from public import public 7 | 8 | from pyecsca.sca.trace import Trace 9 | 10 | 11 | @public 12 | class TraceSet: 13 | """Set of traces with some metadata.""" 14 | 15 | _traces: List[Trace] 16 | _keys: List 17 | 18 | def __init__(self, *traces: Trace, **kwargs): 19 | self._traces = list(traces) 20 | for trace in self._traces: 21 | trace.trace_set = self 22 | self.__dict__.update(kwargs) 23 | self._keys = list(kwargs.keys()) 24 | 25 | def __len__(self): 26 | """Return the number of traces.""" 27 | return len(self._traces) 28 | 29 | def __getitem__(self, index) -> Trace: 30 | """Get the trace at `index`.""" 31 | return self._traces[index] 32 | 33 | def __iter__(self): 34 | """Iterate over the traces.""" 35 | yield from self._traces 36 | 37 | @classmethod 38 | def read(cls, input: Union[str, Path, bytes, BinaryIO], **kwargs) -> "TraceSet": 39 | raise NotImplementedError 40 | 41 | @classmethod 42 | def inplace(cls, input: Union[str, Path, bytes, BinaryIO], **kwargs) -> "TraceSet": 43 | raise NotImplementedError 44 | 45 | def write(self, output: Union[str, Path, BinaryIO]): 46 | raise NotImplementedError 47 | 48 | def __repr__(self): 49 | args = ", ".join([f"{key}={getattr(self, key)!r}" for key in self._keys]) 50 | return f"{self.__class__.__name__}({args})" 51 | -------------------------------------------------------------------------------- /pyecsca/sca/trace_set/chipwhisperer.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | from itertools import zip_longest 3 | from os.path import exists, isfile, join, basename, dirname 4 | from pathlib import Path 5 | from typing import Union, BinaryIO 6 | 7 | import numpy as np 8 | from public import public 9 | 10 | from pyecsca.sca.trace_set.base import TraceSet 11 | from pyecsca.sca.trace import Trace 12 | 13 | 14 | @public 15 | class ChipWhispererTraceSet(TraceSet): 16 | """ChipWhisperer trace set (native) format.""" 17 | 18 | @classmethod 19 | def read( 20 | cls, input: Union[str, Path, bytes, BinaryIO], **kwargs 21 | ) -> "ChipWhispererTraceSet": 22 | if isinstance(input, (str, Path)): 23 | traces, kws = ChipWhispererTraceSet.__read(input) 24 | return ChipWhispererTraceSet(*traces, **kws) 25 | else: 26 | raise ValueError 27 | 28 | @classmethod 29 | def inplace( 30 | cls, input: Union[str, Path, bytes, BinaryIO], **kwargs 31 | ) -> "ChipWhispererTraceSet": 32 | raise NotImplementedError 33 | 34 | def write(self, output: Union[str, Path, BinaryIO]): 35 | raise NotImplementedError 36 | 37 | @classmethod 38 | def __read(cls, full_path): 39 | file_name = basename(full_path) 40 | if not file_name.startswith("config_") or not file_name.endswith(".cfg"): 41 | raise ValueError 42 | path = dirname(full_path) 43 | name = file_name[7:-4] 44 | data = ChipWhispererTraceSet.__read_data(path, name) 45 | traces = [] 46 | for samples, key, textin, textout in zip_longest( 47 | data["traces"], data["keylist"], data["textin"], data["textout"] 48 | ): 49 | traces.append( 50 | Trace(samples, {"key": key, "textin": textin, "textout": textout}) 51 | ) 52 | del data["traces"] 53 | del data["keylist"] 54 | del data["textin"] 55 | del data["textout"] 56 | config = ChipWhispererTraceSet.__read_config(path, name) 57 | return traces, {**data, **config} 58 | 59 | @classmethod 60 | def __read_data(cls, path, name): 61 | types = { 62 | "keylist": None, 63 | "knownkey": None, 64 | "textin": None, 65 | "textout": None, 66 | "traces": None, 67 | } 68 | for type in types: 69 | type_path = join(path, name + type + ".npy") 70 | if exists(type_path) and isfile(type_path): 71 | types[type] = np.load(type_path, allow_pickle=True) 72 | return types 73 | 74 | @classmethod 75 | def __read_config(cls, path, name): 76 | config_path = join(path, "config_" + name + ".cfg") 77 | if exists(config_path) and isfile(config_path): 78 | config = ConfigParser() 79 | config.read(config_path) 80 | return config["Trace Config"] 81 | else: 82 | return {} 83 | 84 | def __repr__(self): 85 | return "ChipWhispererTraceSet()" 86 | -------------------------------------------------------------------------------- /pyecsca/sca/trace_set/pickle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a traceset implementation based on Python's pickle format. 3 | 4 | This implementation of the traceset is thus very generic. 5 | """ 6 | 7 | import pickle 8 | from io import BufferedIOBase, RawIOBase 9 | from pathlib import Path 10 | from typing import Union, BinaryIO 11 | 12 | from public import public 13 | 14 | from pyecsca.sca.trace_set.base import TraceSet 15 | 16 | 17 | @public 18 | class PickleTraceSet(TraceSet): 19 | """Pickle-based traceset format.""" 20 | 21 | @classmethod 22 | def read( 23 | cls, input: Union[str, Path, bytes, BinaryIO], **kwargs 24 | ) -> "PickleTraceSet": 25 | if isinstance(input, bytes): 26 | return pickle.loads(input) # pickle is OK here, skipcq: BAN-B301 27 | elif isinstance(input, (str, Path)): 28 | with open(input, "rb") as f: 29 | return pickle.load(f) # pickle is OK here, skipcq: BAN-B301 30 | elif isinstance(input, (RawIOBase, BufferedIOBase, BinaryIO)): 31 | return pickle.load(input) # pickle is OK here, skipcq: BAN-B301 32 | raise TypeError 33 | 34 | @classmethod 35 | def inplace( 36 | cls, input: Union[str, Path, bytes, BinaryIO], **kwargs 37 | ) -> "PickleTraceSet": 38 | raise NotImplementedError 39 | 40 | def write(self, output: Union[str, Path, BinaryIO]): 41 | if isinstance(output, (str, Path)): 42 | with open(output, "wb") as f: 43 | pickle.dump(self, f) 44 | elif isinstance(output, (RawIOBase, BufferedIOBase, BinaryIO)): 45 | pickle.dump(self, output) 46 | else: 47 | raise TypeError 48 | 49 | def __repr__(self): 50 | args = ", ".join([f"{key}={getattr(self, key)!r}" for key in self._keys]) 51 | return f"PickleTraceSet(num_traces={len(self)}, {args})" 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import Command, setup 3 | from setuptools.command.sdist import sdist 4 | from setuptools.command.build import build 5 | 6 | 7 | class SubmoduleMissingException(Exception): 8 | pass 9 | 10 | 11 | class CustomSubmoduleCheck(Command): 12 | def initialize_options(self) -> None: 13 | pass 14 | 15 | def finalize_options(self) -> None: 16 | pass 17 | 18 | def run(self) -> None: 19 | efd_path = Path("pyecsca/ec/efd") 20 | if not list(efd_path.iterdir()): 21 | raise SubmoduleMissingException( 22 | "The EFD submodule of pyecsca is missing, did you initialize the git submodules?" 23 | ) 24 | std_path = Path("pyecsca/ec/std") 25 | if not list(std_path.iterdir()): 26 | raise SubmoduleMissingException( 27 | "The std-curves submodule of pyecsca is missing, did you initialize the git submodules?" 28 | ) 29 | 30 | 31 | class CustomSdist(sdist): 32 | sub_commands = [("check_submodules", None)] + sdist.sub_commands 33 | 34 | 35 | class CustomBuild(build): 36 | sub_commands = [("check_submodules", None)] + build.sub_commands 37 | 38 | 39 | setup( 40 | cmdclass={ 41 | "sdist": CustomSdist, 42 | "build": CustomBuild, 43 | "check_submodules": CustomSubmoduleCheck, 44 | } 45 | ) 46 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/__init__.py -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyecsca.ec.params import get_params, DomainParameters 4 | 5 | 6 | @pytest.fixture(scope="session") 7 | def secp128r1() -> DomainParameters: 8 | return get_params("secg", "secp128r1", "projective") 9 | 10 | 11 | @pytest.fixture(scope="session") 12 | def curve25519() -> DomainParameters: 13 | return get_params("other", "Curve25519", "xz", infty=False) 14 | 15 | 16 | @pytest.fixture(scope="session") 17 | def curve448() -> DomainParameters: 18 | return get_params("other", "Curve448", "xz", infty=False) 19 | 20 | 21 | @pytest.fixture(scope="session") 22 | def ed25519() -> DomainParameters: 23 | return get_params("other", "Ed25519", "projective") 24 | -------------------------------------------------------------------------------- /test/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/__init__.py -------------------------------------------------------------------------------- /test/data/divpoly/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/divpoly/__init__.py -------------------------------------------------------------------------------- /test/data/ec/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/ec/__init__.py -------------------------------------------------------------------------------- /test/data/ec/curve.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "small", 3 | "category": "test", 4 | "desc": "A small prime order test curve.", 5 | "field": { 6 | "type": "Prime", 7 | "p": "0xc5f5cd5e4a0beb9a445bd3b824e46d8b", 8 | "bits": 128 9 | }, 10 | "form": "Weierstrass", 11 | "params": { 12 | "a": { 13 | "raw": "0x307c28b1209bb670e6b44f5ff6ee8118" 14 | }, 15 | "b": { 16 | "raw": "0xa3206b2b16fa6dd383acc084e649782a" 17 | } 18 | }, 19 | "generator": { 20 | "x": { 21 | "raw": "0xb805e6ee5f3cd8a9c103c3978b688391" 22 | }, 23 | "y": { 24 | "raw": "0x74b0a63b1cc5aa501a9e2101c16d9939" 25 | } 26 | }, 27 | "order": "0xc5f5cd5e4a0beb9b5515cd2847c4051d", 28 | "cofactor": "0x01" 29 | } 30 | -------------------------------------------------------------------------------- /test/data/ec/curves.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "desc": "Test description", 4 | "curves": [ 5 | { 6 | "form": "Edwards", 7 | "name": "small-8bits-edwards-curve-c1-d-has-sqrt", 8 | "category": "test", 9 | "desc": "A small 8-bit Edwards curve with with c = 1 and d has sqrt.", 10 | "field": { 11 | "type": "Prime", 12 | "p": "0xDF", 13 | "bits": 8 14 | }, 15 | "params": { 16 | "c": { 17 | "raw": "0x1" 18 | }, 19 | "d": { 20 | "raw": "0xF" 21 | } 22 | }, 23 | "generator": { 24 | "x": { 25 | "raw": "0xC3" 26 | }, 27 | "y": { 28 | "raw": "0xB4" 29 | } 30 | }, 31 | "r": "0x62", 32 | "s": "0xB0", 33 | "order": "0x1F0", 34 | "cofactor": "0x1" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/data/ec/ecdh_tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "raw": "ca7c0f8c3ffa87a96e1b74ac8e6af594347bb40a", 3 | "sha1": "d248313e865a1ae677782b54b24d8abaf11a53c2", 4 | "keyA": { 5 | "priv": "0xaa374ffc3ce144e6b073307972cb6d57b2a4e982", 6 | "pub": { 7 | "x": "0x51b4496fecc406ed0e75a24a3c03206251419dc0", 8 | "y": "0xc28dcb4b73a514b468d793894f381ccc1756aa6c" 9 | } 10 | }, 11 | "keyB": { 12 | "priv": "0x45fb58a92a17ad4b15101c66e74f277e2b460866", 13 | "pub": { 14 | "x": "0x49b41e0e9c0369c2328739d90f63d56707c6e5bc", 15 | "y": "0x26e008b567015ed96d232a03111c3edc0e9c8f83" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/data/ec/ecdsa_tv.json: -------------------------------------------------------------------------------- 1 | { 2 | "msg": "608079423f12421de616b7493ebe551cf4d65b92", 3 | "priv": "e14f37b3d1374ff8b03f41b9b3fdd2f0ebccf275d660d7f3", 4 | "pub": { 5 | "x": "07008ea40b08dbe76432096e80a2494c94982d2d5bcf98e6", 6 | "y": "76fab681d00b414ea636ba215de26d98c41bd7f2e4d65477" 7 | }, 8 | "k": "cb0abc7043a10783684556fb12c4154d57bc31a289685f25", 9 | "signature": { 10 | "r": "6994d962bdd0d793ffddf855ec5bf2f91a9698b46258a63e", 11 | "s": "02ba6465a234903744ab02bc8521405b73cf5fc00e1a9f41" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/data/ec/ecgen_secp128r1.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "field": { 3 | "p": "0xfffffffdffffffffffffffffffffffff" 4 | }, 5 | "seed": "0x000e0d4d696e6768756151750cc03a4473d03679", 6 | "a": "0xfffffffdfffffffffffffffffffffffc", 7 | "b": "0xe87579c11079f43dd824993c2cee5ed3", 8 | "order": "0xfffffffe0000000075a30d1b9038a115", 9 | "subgroups": [ 10 | { 11 | "x": "0x161ff7528b899b2d0c28607ca52c5b86", 12 | "y": "0xcf5ac8395bafeb13c02da292dded7a83", 13 | "order": "0xfffffffe0000000075a30d1b9038a115", 14 | "cofactor": "0x1", 15 | "points": [ 16 | { 17 | "x": "0x42d2d24d7c0faa3c89b4568396d603c4", 18 | "y": "0x992617de3aa2dfdf8da948913a69f185", 19 | "order": "0xfffffffe0000000075a30d1b9038a115" 20 | } 21 | ] 22 | } 23 | ] 24 | }] 25 | -------------------------------------------------------------------------------- /test/data/ec/ectester_secp128r1.csv: -------------------------------------------------------------------------------- 1 | 0xfffffffdffffffffffffffffffffffff,0xfffffffdfffffffffffffffffffffffc,0xe87579c11079f43dd824993c2cee5ed3,0x161ff7528b899b2d0c28607ca52c5b86,0xcf5ac8395bafeb13c02da292dded7a83,0xfffffffe0000000075a30d1b9038a115,0x1 2 | -------------------------------------------------------------------------------- /test/data/sca/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/__init__.py -------------------------------------------------------------------------------- /test/data/sca/chipwhisperer_keylist.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/chipwhisperer_keylist.npy -------------------------------------------------------------------------------- /test/data/sca/chipwhisperer_knownkey.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/chipwhisperer_knownkey.npy -------------------------------------------------------------------------------- /test/data/sca/chipwhisperer_textin.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/chipwhisperer_textin.npy -------------------------------------------------------------------------------- /test/data/sca/chipwhisperer_textout.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/chipwhisperer_textout.npy -------------------------------------------------------------------------------- /test/data/sca/chipwhisperer_traces.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/chipwhisperer_traces.npy -------------------------------------------------------------------------------- /test/data/sca/config_chipwhisperer_.cfg: -------------------------------------------------------------------------------- 1 | [Trace Config] 2 | numTraces = 2 3 | format = native 4 | numPoints = 24000 5 | prefix = chipwhisperer_ 6 | date = 2017-07-15 21:56:26 7 | scopeSampleRate = 7384609 8 | notes = "AckPattern: Basic (Key=Random, Plaintext=Fixed:00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 00); Aux: " 9 | scopeName = ChipWhisperer/OpenADC 10 | scopeXUnits = 0 11 | targetSW = unknown 12 | targetHW = Simple Serial 13 | scopeYUnits = 0 14 | -------------------------------------------------------------------------------- /test/data/sca/example.trs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/example.trs -------------------------------------------------------------------------------- /test/data/sca/target.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from sys import stdout 3 | 4 | if __name__ == "__main__": 5 | 6 | while True: 7 | try: 8 | line = input() 9 | except EOFError: 10 | break 11 | char = line[0] 12 | content = line[1:] 13 | if char == "d": 14 | for c in "r01020304": 15 | stdout.write(c) 16 | stdout.write("\n") 17 | for c in "z00": 18 | stdout.write(c) 19 | stdout.write("\n") 20 | elif char == "x": 21 | break 22 | -------------------------------------------------------------------------------- /test/data/sca/test.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/test.h5 -------------------------------------------------------------------------------- /test/data/sca/test.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/data/sca/test.pickle -------------------------------------------------------------------------------- /test/ec/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/ec/__init__.py -------------------------------------------------------------------------------- /test/ec/bench_divpoly.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | from datetime import datetime 4 | 5 | import click 6 | 7 | from pyecsca.ec.divpoly import mult_by_n 8 | from pyecsca.ec.params import get_params 9 | 10 | 11 | @click.command() 12 | @click.option("-n", type=click.INT, default=21) 13 | def main(n): 14 | p256 = get_params("secg", "secp256r1", "projective") 15 | 16 | print("Benchmarking divpoly computation on P-256...", file=sys.stderr) 17 | 18 | ns = [] 19 | durs = [] 20 | mems = [] 21 | for i in range(2, n): 22 | start = datetime.now() 23 | mx, my = mult_by_n(p256.curve, i) 24 | end = datetime.now() 25 | duration = (end - start).total_seconds() 26 | memory = (mx[0].degree() + mx[1].degree() + my[0].degree() + my[1].degree()) * 32 27 | ns.append(i) 28 | durs.append((end - start).total_seconds()) 29 | mems.append(memory) 30 | print(i, duration, memory, sep=",") 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /test/ec/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/ec/conftest.py -------------------------------------------------------------------------------- /test/ec/perf_formula.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import click 3 | 4 | from pyecsca.ec.mod.flint import has_flint 5 | from pyecsca.ec.mod.gmp import has_gmp 6 | from pyecsca.ec.params import get_params 7 | from pyecsca.misc.cfg import TemporaryConfig 8 | from test.utils import Profiler 9 | 10 | 11 | @click.command() 12 | @click.option( 13 | "-p", 14 | "--profiler", 15 | type=click.Choice(("py", "c", "raw")), 16 | default="py", 17 | envvar="PROF", 18 | ) 19 | @click.option( 20 | "-m", 21 | "--mod", 22 | type=click.Choice(("python", "gmp", "flint")), 23 | default="flint" if has_flint else "gmp" if has_gmp else "python", 24 | envvar="MOD", 25 | ) 26 | @click.option("-o", "--operations", type=click.INT, default=5000) 27 | @click.option( 28 | "-d", 29 | "--directory", 30 | type=click.Path(file_okay=False, dir_okay=True), 31 | default=None, 32 | envvar="DIR", 33 | ) 34 | def main(profiler, mod, operations, directory): 35 | with TemporaryConfig() as cfg: 36 | cfg.ec.mod_implementation = mod 37 | p256 = get_params("secg", "secp256r1", "projective") 38 | coords = p256.curve.coordinate_model 39 | add = coords.formulas["add-2015-rcb"] 40 | dbl = coords.formulas["dbl-2015-rcb"] 41 | click.echo( 42 | f"Profiling {operations} {p256.curve.prime.bit_length()}-bit doubling formula (dbl2015rcb) executions..." 43 | ) 44 | one_point = p256.generator 45 | with Profiler( 46 | profiler, 47 | directory, 48 | f"formula_dbl2016rcb_p256_{operations}_{mod}", 49 | operations, 50 | ): 51 | for _ in range(operations): 52 | one_point = dbl(p256.curve.prime, one_point, **p256.curve.parameters)[0] 53 | click.echo( 54 | f"Profiling {operations} {p256.curve.prime.bit_length()}-bit addition formula (add2015rcb) executions..." 55 | ) 56 | other_point = p256.generator 57 | with Profiler( 58 | profiler, 59 | directory, 60 | f"formula_add2016rcb_p256_{operations}_{mod}", 61 | operations, 62 | ): 63 | for _ in range(operations): 64 | one_point = add( 65 | p256.curve.prime, one_point, other_point, **p256.curve.parameters 66 | )[0] 67 | ed25519 = get_params("other", "Ed25519", "extended") 68 | ecoords = ed25519.curve.coordinate_model 69 | dblg = ecoords.formulas["mdbl-2008-hwcd"] 70 | click.echo( 71 | f"Profiling {operations} {ed25519.curve.prime.bit_length()}-bit doubling formula (mdbl2008hwcd) executions (with assumption)..." 72 | ) 73 | eone_point = ed25519.generator 74 | with Profiler( 75 | profiler, 76 | directory, 77 | f"formula_mdbl2008hwcd_ed25519_{operations}_{mod}", 78 | operations, 79 | ): 80 | for _ in range(operations): 81 | dblg(ed25519.curve.prime, eone_point, **ed25519.curve.parameters) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /test/ec/perf_mult.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from typing import cast 3 | 4 | import click 5 | 6 | from pyecsca.ec.context import local, DefaultContext 7 | from pyecsca.ec.formula import AdditionFormula, DoublingFormula 8 | from pyecsca.ec.mod.flint import has_flint 9 | from pyecsca.ec.mod.gmp import has_gmp 10 | from pyecsca.ec.mult import LTRMultiplier 11 | from pyecsca.ec.params import get_params 12 | from pyecsca.misc.cfg import TemporaryConfig 13 | from test.utils import Profiler 14 | 15 | 16 | @click.command() 17 | @click.option("-p", "--profiler", type=click.Choice(("py", "c", "raw")), default="py") 18 | @click.option( 19 | "-m", 20 | "--mod", 21 | type=click.Choice(("python", "gmp", "flint")), 22 | default="flint" if has_flint else "gmp" if has_gmp else "python", 23 | ) 24 | @click.option("-o", "--operations", type=click.INT, default=50) 25 | @click.option( 26 | "-d", 27 | "--directory", 28 | type=click.Path(file_okay=False, dir_okay=True), 29 | default=None, 30 | envvar="DIR", 31 | ) 32 | def main(profiler, mod, operations, directory): 33 | with TemporaryConfig() as cfg: 34 | cfg.ec.mod_implementation = mod 35 | p256 = get_params("secg", "secp256r1", "projective") 36 | coords = p256.curve.coordinate_model 37 | add = cast(AdditionFormula, coords.formulas["add-2015-rcb"]) 38 | dbl = cast(DoublingFormula, coords.formulas["dbl-2015-rcb"]) 39 | mult = LTRMultiplier(add, dbl) 40 | click.echo( 41 | f"Profiling {operations} {p256.curve.prime.bit_length()}-bit scalar multiplication executions..." 42 | ) 43 | one_point = p256.generator 44 | with Profiler(profiler, directory, f"mult_ltr_rcb_p256_{operations}_{mod}", operations): 45 | for _ in range(operations): 46 | mult.init(p256, one_point) 47 | one_point = mult.multiply( 48 | 0x71A55E0C1ABB3A0E069419E0F837BC195F1B9545E69FC51E53C4D48D7FEA3B1A 49 | ) 50 | click.echo( 51 | f"Profiling {operations} {p256.curve.prime.bit_length()}-bit scalar multiplication executions (with tracing)..." 52 | ) 53 | with local(DefaultContext()): 54 | one_point = p256.generator 55 | with Profiler(profiler, directory, f"mult_ltr_rcb_p256_wtrace_{operations}_{mod}", operations): 56 | for _ in range(operations): 57 | mult.init(p256, one_point) 58 | one_point = mult.multiply( 59 | 0x71A55E0C1ABB3A0E069419E0F837BC195F1B9545E69FC51E53C4D48D7FEA3B1A 60 | ) 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /test/ec/test_configuration.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | import pytest 4 | 5 | from pyecsca.ec.configuration import ( 6 | all_configurations, 7 | HashType, 8 | RandomMod, 9 | Multiplication, 10 | Squaring, 11 | Reduction, 12 | Inversion, 13 | ) 14 | from pyecsca.ec.model import ShortWeierstrassModel 15 | from pyecsca.ec.mult import LTRMultiplier, AccumulationOrder 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def base_independents(): 20 | return {"hash_type": HashType.SHA1, "mod_rand": RandomMod.SAMPLE, "mult": Multiplication.BASE, "sqr": Squaring.BASE, 21 | "red": Reduction.BASE, "inv": Inversion.GCD, } 22 | 23 | 24 | @pytest.mark.slow 25 | def test_all(): 26 | j = 0 27 | for _ in all_configurations(model=ShortWeierstrassModel()): 28 | j += 1 29 | 30 | 31 | def test_weierstrass_projective(base_independents): 32 | model = ShortWeierstrassModel() 33 | coords = model.coordinates["projective"] 34 | configs = list(all_configurations(model=model, coords=coords, **base_independents)) 35 | assert len(set(map(lambda cfg: cfg.scalarmult, configs))) == len(configs) 36 | assert len(configs) == 20960 37 | 38 | 39 | def test_mult_class(base_independents): 40 | model = ShortWeierstrassModel() 41 | coords = model.coordinates["projective"] 42 | scalarmult = LTRMultiplier 43 | configs = list(all_configurations(model=model, coords=coords, scalarmult=scalarmult, **base_independents)) 44 | assert len(set(map(lambda cfg: cfg.scalarmult, configs))) == len(configs) 45 | assert len(configs) == 1280 46 | 47 | 48 | def test_one(base_independents): 49 | model = ShortWeierstrassModel() 50 | coords = model.coordinates["projective"] 51 | scalarmult = {"cls": LTRMultiplier, "add": coords.formulas["add-1998-cmo"], "dbl": coords.formulas["dbl-1998-cmo"], 52 | "scl": None, "always": True, "complete": False, "short_circuit": True, 53 | "accumulation_order": AccumulationOrder.PeqRP} 54 | configs = list(all_configurations(model=model, coords=coords, scalarmult=scalarmult, **base_independents)) 55 | assert len(configs) == 1 56 | scalarmult = LTRMultiplier(coords.formulas["add-1998-cmo"], coords.formulas["dbl-1998-cmo"], None, True, 57 | accumulation_order=AccumulationOrder.PeqRP, complete=False, short_circuit=True) 58 | configs = list(all_configurations(model=model, coords=coords, scalarmult=scalarmult, **base_independents)) 59 | assert len(configs) == 1 60 | configs = list(all_configurations(model=model, scalarmult=scalarmult, **base_independents)) 61 | assert len(configs) == 1 62 | 63 | 64 | def test_pickle(base_independents): 65 | model = ShortWeierstrassModel() 66 | coords = model.coordinates["projective"] 67 | scalarmult = {"cls": LTRMultiplier, "add": coords.formulas["add-1998-cmo"], "dbl": coords.formulas["dbl-1998-cmo"], 68 | "scl": None, "always": True, "complete": False, "short_circuit": True, 69 | "accumulation_order": AccumulationOrder.PeqRP} 70 | configs = list(all_configurations(model=model, coords=coords, scalarmult=scalarmult, **base_independents)) 71 | config = configs[0] 72 | assert config == pickle.loads(pickle.dumps(config)) 73 | -------------------------------------------------------------------------------- /test/ec/test_key_generation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyecsca.ec.key_generation import KeyGeneration 4 | from pyecsca.ec.mult import LTRMultiplier 5 | 6 | 7 | @pytest.fixture() 8 | def mult(secp128r1): 9 | add = secp128r1.curve.coordinate_model.formulas["add-2007-bl"] 10 | dbl = secp128r1.curve.coordinate_model.formulas["dbl-2007-bl"] 11 | return LTRMultiplier(add, dbl) 12 | 13 | 14 | def test_basic(secp128r1, mult): 15 | generator = KeyGeneration(mult, secp128r1) 16 | priv, pub = generator.generate() 17 | assert priv is not None 18 | assert pub is not None 19 | assert secp128r1.curve.is_on_curve(pub) 20 | generator = KeyGeneration(mult, secp128r1, True) 21 | priv, pub = generator.generate() 22 | assert priv is not None 23 | assert pub is not None 24 | assert secp128r1.curve.is_on_curve(pub) 25 | -------------------------------------------------------------------------------- /test/ec/test_model.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from pyecsca.ec.model import ( 4 | ShortWeierstrassModel, 5 | MontgomeryModel, 6 | EdwardsModel, 7 | TwistedEdwardsModel, 8 | ) 9 | 10 | 11 | def test_load(): 12 | assert len(ShortWeierstrassModel().coordinates) > 0 13 | assert len(MontgomeryModel().coordinates) > 0 14 | assert len(EdwardsModel().coordinates) > 0 15 | assert len(TwistedEdwardsModel().coordinates) > 0 16 | 17 | 18 | def test_pickle(): 19 | sw = ShortWeierstrassModel() 20 | m = MontgomeryModel() 21 | e = EdwardsModel() 22 | te = TwistedEdwardsModel() 23 | assert sw == pickle.loads(pickle.dumps(sw)) 24 | assert m == pickle.loads(pickle.dumps(MontgomeryModel())) 25 | assert e == pickle.loads(pickle.dumps(EdwardsModel())) 26 | assert te == pickle.loads(pickle.dumps(TwistedEdwardsModel())) 27 | -------------------------------------------------------------------------------- /test/ec/test_op.py: -------------------------------------------------------------------------------- 1 | from ast import parse 2 | 3 | import pytest 4 | 5 | from pyecsca.ec.formula import OpResult 6 | from pyecsca.ec.mod import mod 7 | from pyecsca.ec.op import CodeOp, OpType 8 | 9 | 10 | @pytest.mark.parametrize("name,module,result,op_type", 11 | [("add", "x = a+b", "x = a+b", OpType.Add), ("sub", "x = a-b", "x = a-b", OpType.Sub), 12 | ("mul", "y = a*b", "y = a*b", OpType.Mult), ("div", "z = a/b", "z = a/b", OpType.Div), 13 | ("inv", "z = 1/b", "z = 1/b", OpType.Inv), ("pow", "b = a**d", "b = a^d", OpType.Pow), 14 | ("sqr", "b = a**2", "b = a^2", OpType.Sqr), ("id1", "b = 7", "b = 7", OpType.Id), 15 | ("id2", "b = a", "b = a", OpType.Id), ]) 16 | def test_str(name, module, result, op_type): 17 | code = parse(module, mode="exec") 18 | op = CodeOp(code) 19 | assert str(op) == result 20 | assert op.operator == op_type 21 | 22 | 23 | @pytest.mark.parametrize("name,module,locals,result", 24 | [("add", "x = a+b", {"a": mod(5, 21), "b": mod(7, 21)}, mod(12, 21)), 25 | ("sub", "x = a-b", {"a": mod(7, 21), "b": mod(5, 21)}, mod(2, 21)), ]) 26 | def test_call(name, module, locals, result): 27 | code = parse(module, mode="exec") 28 | op = CodeOp(code) 29 | res = op(**locals) 30 | assert res == result 31 | 32 | 33 | def test_opresult_repr(): 34 | res = OpResult("a", mod(7, 11), OpType.Neg, "b") 35 | assert repr(res) == "a = -b" 36 | res = OpResult("a", mod(5, 7), OpType.Add, "c", 3) 37 | assert repr(res) == "a = c+3" 38 | res = OpResult("a", mod(3, 11), OpType.Inv, "d") 39 | assert repr(res) == "a = 1/d" 40 | -------------------------------------------------------------------------------- /test/ec/test_pickle.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from multiprocessing import get_context 3 | from multiprocessing.context import BaseContext 4 | 5 | import pytest 6 | 7 | from pyecsca.ec.formula.efd import EFDFormula 8 | from pyecsca.ec.formula.graph import FormulaGraph 9 | from pyecsca.ec.model import ShortWeierstrassModel 10 | from pyecsca.ec.params import get_params 11 | 12 | 13 | @pytest.fixture() 14 | def ctx() -> BaseContext: 15 | return get_context("spawn") 16 | 17 | 18 | def model_target(obj): 19 | return obj._loaded 20 | 21 | 22 | def test_model_loads(ctx): 23 | # Test that the unpickling of EFDCurveModel triggers a reload that loads the EFD data for that model. 24 | sw = ShortWeierstrassModel() 25 | with ctx.Pool(processes=1) as pool: 26 | res = pool.apply(model_target, args=(sw,)) 27 | assert res 28 | 29 | 30 | def test_model(): 31 | sw = ShortWeierstrassModel() 32 | data = pickle.dumps(sw) 33 | back = pickle.loads(data) 34 | assert sw == back 35 | 36 | 37 | def test_coords(): 38 | sw = ShortWeierstrassModel() 39 | coords = sw.coordinates["projective"] 40 | data = pickle.dumps(coords) 41 | back = pickle.loads(data) 42 | assert coords == back 43 | 44 | 45 | def test_formula(): 46 | sw = ShortWeierstrassModel() 47 | coords = sw.coordinates["projective"] 48 | formula: EFDFormula = coords.formulas["add-2007-bl"] # type: ignore 49 | data = pickle.dumps(formula) 50 | back = pickle.loads(data) 51 | assert formula == back 52 | formulas = tuple(coords.formulas.values()) 53 | data = pickle.dumps(formulas) 54 | back = pickle.loads(data) 55 | assert formulas == back 56 | code_formula = formula.to_code() 57 | assert formula != code_formula 58 | back_code = pickle.loads(pickle.dumps(code_formula)) 59 | assert code_formula == back_code 60 | 61 | 62 | def formula_target(formula): 63 | return hasattr(formula, "coordinate_model") 64 | 65 | 66 | def test_formula_loads(ctx): 67 | sw = ShortWeierstrassModel() 68 | coords = sw.coordinates["projective"] 69 | formula: EFDFormula = coords.formulas["add-2007-bl"] # type: ignore 70 | with ctx.Pool(processes=1) as pool: 71 | res = pool.apply(formula_target, args=(formula,)) 72 | assert res 73 | formula = formula.to_code() # type: ignore 74 | with ctx.Pool(processes=1) as pool: 75 | res = pool.apply(formula_target, args=(formula,)) 76 | assert res 77 | 78 | 79 | def test_code_formula(): 80 | sw = ShortWeierstrassModel() 81 | coords = sw.coordinates["projective"] 82 | formula = coords.formulas["add-2007-bl"] 83 | graph = FormulaGraph(formula) 84 | code = graph.to_formula() 85 | data = pickle.dumps(code) 86 | back = pickle.loads(data) 87 | assert code == back 88 | 89 | 90 | def test_params(secp128r1): 91 | secp256r1 = get_params("secg", "secp256r1", "projective") 92 | params = (secp128r1, secp256r1) 93 | data = pickle.dumps(params) 94 | back = pickle.loads(data) 95 | assert params == back 96 | 97 | 98 | def test_op(secp128r1): 99 | formula = secp128r1.curve.coordinate_model.formulas["add-2007-bl"] 100 | op = formula.code[0] 101 | data = pickle.dumps(op) 102 | back = pickle.loads(data) 103 | assert op == back 104 | -------------------------------------------------------------------------------- /test/ec/test_scalar.py: -------------------------------------------------------------------------------- 1 | from pyecsca.ec.scalar import ( 2 | convert_base, 3 | sliding_window_ltr, 4 | sliding_window_rtl, 5 | wnaf, 6 | naf, 7 | booth, 8 | booth_word, 9 | booth_window, 10 | ) 11 | 12 | 13 | def test_convert(): 14 | assert convert_base(5, 2) == [1, 0, 1] 15 | assert convert_base(10, 5) == [0, 2] 16 | 17 | 18 | def test_sliding_window(): 19 | assert sliding_window_ltr(181, 3) == [5, 0, 0, 5, 0, 1] 20 | assert sliding_window_rtl(181, 3) == [1, 0, 0, 3, 0, 0, 0, 5] 21 | 22 | 23 | def test_nafs(): 24 | i = 0b1100110101001101011011 25 | assert naf(i) == wnaf(i, 2) 26 | 27 | 28 | def test_booth(): 29 | assert booth(0b101) == [-1, 1, -1, 1] 30 | for i in range(2**6): 31 | bw = booth_word(i, 5) 32 | # verified with BoringSSL recoding impl. (ec_GFp_nistp_recode_scalar_bits) 33 | if i <= 31: 34 | assert bw == (i + 1) // 2 35 | else: 36 | assert bw == -((64 - i) // 2) 37 | s = 0x12345678123456781234567812345678123456781234567812345678 38 | bw = booth_window(s, 5, 224) 39 | # verified with BoringSSL ec_GFp_nistp224_point_mul 40 | assert bw == [1, 4, 13, 3, -10, 15, 0, 9, 3, 9, -10, -12, -8, 2, 9, -6, 5, 13, -2, 1, -14, 7, -15, 11, 8, -16, 5, -14, -12, 11, -6, -4, 1, 4, 13, 3, -10, 15, 0, 9, 3, 9, -10, -12, -8] 41 | -------------------------------------------------------------------------------- /test/ec/test_transformations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyecsca.ec.params import get_params 4 | from pyecsca.ec.transformations import M2SW, M2TE, M2E, TE2M, TE2E, TE2SW, SW2M, SW2TE, SW2E 5 | 6 | 7 | def test_montgomery(): 8 | curve25519 = get_params("other", "Curve25519", "affine") 9 | sw = M2SW(curve25519) 10 | assert sw is not None 11 | assert sw.curve.is_on_curve(sw.generator) 12 | assert sw.curve.is_neutral(sw.curve.neutral) 13 | te = M2TE(curve25519) 14 | assert te is not None 15 | assert te.curve.is_on_curve(te.generator) 16 | assert te.curve.is_neutral(te.curve.neutral) 17 | e = M2E(curve25519) 18 | assert e is not None 19 | assert e.curve.is_on_curve(e.generator) 20 | assert e.curve.is_neutral(e.curve.neutral) 21 | 22 | 23 | def test_twistededwards(): 24 | ed25519 = get_params("other", "Ed25519", "affine") 25 | m = TE2M(ed25519) 26 | assert m is not None 27 | assert m.curve.is_on_curve(m.generator) 28 | assert m.curve.is_neutral(m.curve.neutral) 29 | e = TE2E(ed25519) 30 | assert e is not None 31 | assert e.curve.is_on_curve(e.generator) 32 | assert e.curve.is_neutral(e.curve.neutral) 33 | sw = TE2SW(ed25519) 34 | assert sw is not None 35 | assert sw.curve.is_on_curve(sw.generator) 36 | assert sw.curve.is_neutral(sw.curve.neutral) 37 | 38 | 39 | def test_shortweierstrass(): 40 | secp128r2 = get_params("secg", "secp128r2", "affine") 41 | m = SW2M(secp128r2) 42 | assert m is not None 43 | assert m.curve.is_on_curve(m.generator) 44 | assert m.curve.is_neutral(m.curve.neutral) 45 | te = SW2TE(secp128r2) 46 | assert te is not None 47 | assert te.curve.is_on_curve(te.generator) 48 | assert te.curve.is_neutral(te.curve.neutral) 49 | with pytest.raises(ValueError): 50 | SW2E(secp128r2) 51 | -------------------------------------------------------------------------------- /test/ec/utils.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from itertools import product 3 | 4 | 5 | def cartesian(*items): 6 | for cart in product(*items): 7 | yield reduce(lambda x, y: x + y, cart) 8 | -------------------------------------------------------------------------------- /test/misc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/misc/__init__.py -------------------------------------------------------------------------------- /test/misc/test_utils.py: -------------------------------------------------------------------------------- 1 | 2 | from pyecsca.misc.utils import TaskExecutor 3 | 4 | 5 | def run(a, b): 6 | return a + b 7 | 8 | 9 | def test_executor(): 10 | with TaskExecutor(max_workers=2) as pool: 11 | for i in range(10): 12 | pool.submit_task(i, 13 | run, 14 | i, 5) 15 | for i, future in pool.as_completed(): 16 | res = future.result() 17 | assert res == i + 5 18 | -------------------------------------------------------------------------------- /test/plots/.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.html 3 | *.svg 4 | -------------------------------------------------------------------------------- /test/plots/plot_perf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import click 3 | 4 | from pathlib import Path 5 | import holoviews as hv 6 | import numpy as np 7 | 8 | 9 | @click.command() 10 | @click.option( 11 | "-d", 12 | "--directory", 13 | type=click.Path(file_okay=False, dir_okay=True), 14 | default=None, 15 | envvar="DIR", 16 | required=True, 17 | ) 18 | def main(directory): 19 | directory = Path(directory) 20 | for f in directory.glob("*.csv"): 21 | pname = str(f).removesuffix(".csv") 22 | d = np.genfromtxt( 23 | f, 24 | delimiter=",", 25 | dtype=None, 26 | encoding="ascii", 27 | names=("commit", "pyversion", "time"), 28 | ) 29 | if len(d.shape) == 0: 30 | d.shape = (1,) 31 | line = hv.Curve(zip(d["commit"], d["time"]), "commit", "duration") 32 | hv.save(line, pname + ".svg", fmt="svg") 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /test/sca/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J08nY/pyecsca/1578542520608ba1af342abb457a10b5a6096f57/test/sca/__init__.py -------------------------------------------------------------------------------- /test/sca/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | import matplotlib.pyplot as plt 4 | import pytest 5 | from importlib_resources import files, as_file 6 | 7 | from pyecsca.sca import Trace 8 | 9 | cases: Dict[str, int] = {} 10 | 11 | 12 | @pytest.fixture() 13 | def plot_name(request): 14 | def namer(): 15 | test_name = f"{request.module.__name__}.{request.node.name}" 16 | case_id = cases.setdefault(test_name, 0) + 1 17 | cases[test_name] = case_id 18 | return test_name + str(case_id) 19 | return namer 20 | 21 | 22 | @pytest.fixture() 23 | def plot_path(plot_name): 24 | def namer(): 25 | with as_file(files("test").joinpath("plots", plot_name())) as fname: 26 | return fname 27 | return namer 28 | 29 | 30 | @pytest.fixture() 31 | def plot(plot_path): 32 | def plotter(*traces: Trace, **kwtraces: Trace): 33 | fig = plt.figure() 34 | ax = fig.add_subplot(111) 35 | for i, trace in enumerate(traces): 36 | ax.plot(trace.samples, label=str(i)) 37 | for name, trace in kwtraces.items(): 38 | ax.plot(trace.samples, label=name) 39 | ax.legend(loc="best") 40 | fname = plot_path() 41 | plt.savefig(fname.parent / (fname.name + ".png")) 42 | return plotter 43 | -------------------------------------------------------------------------------- /test/sca/perf_combine.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import click 3 | from importlib_resources import files, as_file 4 | 5 | import test.data.sca 6 | from pyecsca.sca import ( 7 | InspectorTraceSet, 8 | average, 9 | variance, 10 | standard_deviation, 11 | add, 12 | subtract, 13 | conditional_average, 14 | ) 15 | from test.utils import Profiler 16 | 17 | 18 | @click.command() 19 | @click.option( 20 | "-p", 21 | "--profiler", 22 | type=click.Choice(("py", "c", "raw")), 23 | default="py", 24 | envvar="PROF", 25 | ) 26 | @click.option("-o", "--operations", type=click.INT, default=100) 27 | @click.option( 28 | "-d", 29 | "--directory", 30 | type=click.Path(file_okay=False, dir_okay=True), 31 | default=None, 32 | envvar="DIR", 33 | ) 34 | def main(profiler, operations, directory): 35 | with as_file(files(test.data.sca).joinpath("example.trs")) as path: 36 | traces = InspectorTraceSet.read(path) 37 | with Profiler(profiler, directory, f"combine_average_example_{operations}"): 38 | for _ in range(operations): 39 | average(*traces) 40 | with Profiler(profiler, directory, f"combine_condavg_example_{operations}"): 41 | for _ in range(operations): 42 | conditional_average(*traces, condition=lambda trace: trace[0] > 0) 43 | with Profiler(profiler, directory, f"combine_variance_example_{operations}"): 44 | for _ in range(operations): 45 | variance(*traces) 46 | with Profiler(profiler, directory, f"combine_stddev_example_{operations}"): 47 | for _ in range(operations): 48 | standard_deviation(*traces) 49 | with Profiler(profiler, directory, f"combine_add_example_{operations}"): 50 | for _ in range(operations): 51 | add(*traces) 52 | with Profiler(profiler, directory, f"combine_subtract_example_{operations}"): 53 | for _ in range(operations): 54 | subtract(traces[0], traces[1]) 55 | 56 | 57 | if __name__ == "__main__": 58 | main() 59 | -------------------------------------------------------------------------------- /test/sca/perf_zvp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import click 3 | 4 | from pyecsca.ec.formula.unroll import unroll_formula 5 | from pyecsca.ec.mod.flint import has_flint 6 | from pyecsca.ec.mod.gmp import has_gmp 7 | from pyecsca.ec.params import get_params 8 | from pyecsca.misc.cfg import TemporaryConfig 9 | from pyecsca.sca.re.zvp import zvp_points, map_to_affine 10 | from test.utils import Profiler 11 | 12 | 13 | @click.command() 14 | @click.option( 15 | "-p", 16 | "--profiler", 17 | type=click.Choice(("py", "c", "raw")), 18 | default="py", 19 | envvar="PROF", 20 | ) 21 | @click.option( 22 | "-m", 23 | "--mod", 24 | type=click.Choice(("python", "gmp", "flint")), 25 | default="flint" if has_flint else "gmp" if has_gmp else "python", 26 | envvar="MOD", 27 | ) 28 | @click.option("-o", "--operations", type=click.INT, default=1) 29 | @click.option( 30 | "-d", 31 | "--directory", 32 | type=click.Path(file_okay=False, dir_okay=True), 33 | default=None, 34 | envvar="DIR", 35 | ) 36 | def main(profiler, mod, operations, directory): 37 | with TemporaryConfig() as cfg: 38 | cfg.ec.mod_implementation = mod 39 | p128 = get_params("secg", "secp128r1", "projective") 40 | formula = p128.curve.coordinate_model.formulas["add-2015-rcb"] 41 | unrolled = unroll_formula(formula) 42 | unrolled = map_to_affine(formula, unrolled) 43 | poly = unrolled[6][1] 44 | k = 5 45 | 46 | click.echo( 47 | f"Profiling {operations} {p128.curve.prime.bit_length()}-bit (k = {k}) ZVP computations..." 48 | ) 49 | with Profiler(profiler, directory, f"zvp_p128_{operations}_{mod}"): 50 | for _ in range(operations): 51 | zvp_points(poly, p128.curve, k, p128.order) 52 | 53 | 54 | if __name__ == "__main__": 55 | main() 56 | -------------------------------------------------------------------------------- /test/sca/test_attacks.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | 5 | from pyecsca.ec.mult import LTRMultiplier 6 | from pyecsca.sca.attack.CPA import CPA 7 | from pyecsca.sca.attack.DPA import DPA 8 | from pyecsca.sca.attack.leakage_model import HammingWeight, NormalNoice 9 | from pyecsca.sca.target import LeakageTarget 10 | 11 | 12 | @pytest.fixture() 13 | def mult(secp128r1): 14 | return LTRMultiplier( 15 | secp128r1.curve.coordinate_model.formulas["add-2015-rcb"], 16 | secp128r1.curve.coordinate_model.formulas["dbl-2015-rcb"], 17 | ) 18 | 19 | 20 | @pytest.fixture() 21 | def target(secp128r1, mult): 22 | target = LeakageTarget( 23 | secp128r1.curve.model, secp128r1.curve.coordinate_model, mult, HammingWeight() 24 | ) 25 | target.set_params(secp128r1) 26 | return target 27 | 28 | 29 | def test_dpa(secp128r1, mult, target, mocker): 30 | random.seed(1337) 31 | 32 | def randbelow(n): 33 | return random.randrange(n) 34 | 35 | mocker.patch("secrets.randbelow", randbelow) 36 | scalar = 5 37 | pub = secp128r1.curve.affine_multiply( 38 | secp128r1.generator.to_affine(), scalar 39 | ).to_model(secp128r1.curve.coordinate_model, secp128r1.curve) 40 | points, traces = target.simulate_scalar_mult_traces(200, scalar) 41 | dpa = DPA(points, traces, mult, secp128r1) 42 | res = dpa.perform(3, pub) 43 | assert res == 5 44 | 45 | 46 | def test_cpa(secp128r1, mult, target, mocker): 47 | random.seed(1337) 48 | 49 | def randbelow(n): 50 | return random.randrange(n) 51 | 52 | mocker.patch("secrets.randbelow", randbelow) 53 | scalar = 5 54 | pub = secp128r1.curve.affine_multiply( 55 | secp128r1.generator.to_affine(), scalar 56 | ).to_model(secp128r1.curve.coordinate_model, secp128r1.curve) 57 | points, original_traces = target.simulate_scalar_mult_traces(100, scalar) 58 | 59 | for _ in range(5): 60 | noise = NormalNoice(0, 2) 61 | traces = [noise(trace) for trace in original_traces] 62 | cpa = CPA(points, traces, HammingWeight(), mult, secp128r1) 63 | res = cpa.perform(3, pub) 64 | assert res == 5 65 | -------------------------------------------------------------------------------- /test/sca/test_combine.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pyecsca.sca import ( 7 | Trace, 8 | CombinedTrace, 9 | average, 10 | conditional_average, 11 | standard_deviation, 12 | variance, 13 | average_and_variance, 14 | add, 15 | subtract, 16 | ) 17 | 18 | 19 | @pytest.fixture() 20 | def data(): 21 | Data = namedtuple("Data", ["a", "b", "c"]) 22 | return Data(a=Trace(np.array([20, 80], dtype=np.dtype("i1")), {"data": b"\xff"}), 23 | b=Trace(np.array([30, 42], dtype=np.dtype("i1")), {"data": b"\xff"}), 24 | c=Trace(np.array([78, 56], dtype=np.dtype("i1")), {"data": b"\x00"})) 25 | 26 | 27 | def test_average(data): 28 | with pytest.raises(ValueError): 29 | average() 30 | result = average(data.a, data.b) 31 | assert result is not None 32 | assert isinstance(result, CombinedTrace) 33 | assert len(result.samples) == 2 34 | assert result.samples[0] == 25 35 | assert result.samples[1] == 61 36 | 37 | 38 | def test_conditional_average(data): 39 | result = conditional_average(data.a, data.b, data.c, condition=lambda trace: trace.meta["data"] == b"\xff", ) 40 | assert isinstance(result, CombinedTrace) 41 | assert len(result.samples) == 2 42 | assert result.samples[0] == 25 43 | assert result.samples[1] == 61 44 | 45 | 46 | def test_standard_deviation(data): 47 | with pytest.raises(ValueError): 48 | standard_deviation() 49 | result = standard_deviation(data.a, data.b) 50 | assert isinstance(result, CombinedTrace) 51 | assert len(result.samples) == 2 52 | 53 | 54 | def test_variance(data): 55 | with pytest.raises(ValueError): 56 | variance() 57 | result = variance(data.a, data.b) 58 | assert isinstance(result, CombinedTrace) 59 | assert len(result.samples) == 2 60 | 61 | 62 | def test_average_and_variance(data): 63 | with pytest.raises(ValueError): 64 | average_and_variance() 65 | mean, var = average_and_variance(data.a, data.b) 66 | assert isinstance(mean, CombinedTrace) 67 | assert isinstance(var, CombinedTrace) 68 | assert len(mean.samples) == 2 69 | assert len(var.samples) == 2 70 | assert mean == average(data.a, data.b) 71 | assert var == variance(data.a, data.b) 72 | 73 | 74 | def test_add(data): 75 | with pytest.raises(ValueError): 76 | add() 77 | result = add(data.a, data.b) 78 | assert isinstance(result, CombinedTrace) 79 | assert result.samples[0] == 50 80 | assert result.samples[1] == 122 81 | np.testing.assert_equal(data.a.samples, add(data.a).samples) 82 | 83 | 84 | def test_subtract(data): 85 | result = subtract(data.a, data.b) 86 | assert isinstance(result, CombinedTrace) 87 | assert result.samples[0] == -10 88 | assert result.samples[1] == 38 89 | -------------------------------------------------------------------------------- /test/sca/test_edit.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyecsca.sca import Trace, trim, reverse, pad, stretch 5 | 6 | 7 | @pytest.fixture() 8 | def trace(): 9 | return Trace(np.array([10, 20, 30, 40, 50], dtype=np.dtype("i1"))) 10 | 11 | 12 | def test_trim(trace): 13 | result = trim(trace, 2) 14 | assert result is not None 15 | np.testing.assert_equal( 16 | result.samples, np.array([30, 40, 50], dtype=np.dtype("i1")) 17 | ) 18 | 19 | result = trim(trace, end=3) 20 | assert result is not None 21 | np.testing.assert_equal( 22 | result.samples, np.array([10, 20, 30], dtype=np.dtype("i1")) 23 | ) 24 | 25 | with pytest.raises(ValueError): 26 | trim(trace, 5, 1) 27 | 28 | 29 | def test_reverse(trace): 30 | result = reverse(trace) 31 | assert result is not None 32 | np.testing.assert_equal( 33 | result.samples, np.array([50, 40, 30, 20, 10], dtype=np.dtype("i1")) 34 | ) 35 | 36 | 37 | def test_pad(trace): 38 | result = pad(trace, 2) 39 | assert result is not None 40 | np.testing.assert_equal( 41 | result.samples, 42 | np.array([0, 0, 10, 20, 30, 40, 50, 0, 0], dtype=np.dtype("i1")), 43 | ) 44 | 45 | result = pad(trace, (1, 3)) 46 | assert result is not None 47 | np.testing.assert_equal( 48 | result.samples, 49 | np.array([0, 10, 20, 30, 40, 50, 0, 0, 0], dtype=np.dtype("i1")), 50 | ) 51 | 52 | 53 | def test_stretch(trace): 54 | result = stretch(trace, 10) 55 | assert result is not None 56 | assert np.min(result) == min(trace) 57 | assert np.max(result) == max(trace) 58 | assert len(result) == 10 59 | -------------------------------------------------------------------------------- /test/sca/test_filter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyecsca.sca import ( 5 | Trace, 6 | filter_lowpass, 7 | filter_highpass, 8 | filter_bandpass, 9 | filter_bandstop, 10 | ) 11 | 12 | 13 | @pytest.fixture() 14 | def trace(): 15 | return Trace( 16 | np.array( 17 | [5, 12, 15, 13, 15, 11, 7, 2, -4, -8, -10, -8, -13, -9, -11, -8, -5], 18 | dtype=np.dtype("i1"), 19 | ), 20 | None, 21 | ) 22 | 23 | 24 | def test_lowpass(trace, plot): 25 | result = filter_lowpass(trace, 100, 20) 26 | assert result is not None 27 | assert len(trace.samples) == len(result.samples) 28 | plot(trace, result) 29 | 30 | 31 | def test_highpass(trace, plot): 32 | result = filter_highpass(trace, 128, 20) 33 | assert result is not None 34 | assert len(trace.samples) == len(result.samples) 35 | plot(trace, result) 36 | 37 | 38 | def test_bandpass(trace, plot): 39 | result = filter_bandpass(trace, 128, 20, 60) 40 | assert result is not None 41 | assert len(trace.samples) == len(result.samples) 42 | plot(trace, result) 43 | 44 | 45 | def test_bandstop(trace, plot): 46 | result = filter_bandstop(trace, 128, 20, 60) 47 | assert result is not None 48 | assert len(trace.samples) == len(result.samples) 49 | plot(trace, result) 50 | -------------------------------------------------------------------------------- /test/sca/test_leakage_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyecsca.ec.context import local, DefaultContext 4 | from pyecsca.ec.formula import FormulaAction, OpResult 5 | from pyecsca.ec.mod import mod 6 | from pyecsca.ec.mult import LTRMultiplier 7 | from pyecsca.ec.op import OpType 8 | from pyecsca.sca.attack.leakage_model import Identity, Bit, Slice, HammingWeight, HammingDistance, BitLength 9 | 10 | 11 | def test_identity(): 12 | val = mod(3, 7) 13 | lm = Identity() 14 | assert lm(val) == 3 15 | 16 | 17 | def test_bit(): 18 | val = mod(3, 7) 19 | lm = Bit(0) 20 | assert lm(val) == 1 21 | lm = Bit(4) 22 | assert lm(val) == 0 23 | with pytest.raises(ValueError): 24 | Bit(-3) 25 | 26 | 27 | def test_slice(): 28 | val = mod(0b11110000, 0xf00) 29 | lm = Slice(0, 4) 30 | assert lm(val) == 0 31 | lm = Slice(1, 5) 32 | assert lm(val) == 0b1000 33 | lm = Slice(4, 8) 34 | assert lm(val) == 0b1111 35 | with pytest.raises(ValueError): 36 | Slice(7, 1) 37 | 38 | 39 | def test_hamming_weight(): 40 | val = mod(0b11110000, 0xf00) 41 | lm = HammingWeight() 42 | assert lm(val) == 4 43 | 44 | 45 | def test_hamming_distance(): 46 | a = mod(0b11110000, 0xf00) 47 | b = mod(0b00010000, 0xf00) 48 | lm = HammingDistance() 49 | assert lm(a, b) == 3 50 | 51 | 52 | def test_bit_length(): 53 | a = mod(0b11110000, 0xf00) 54 | lm = BitLength() 55 | assert lm(a) == 8 56 | 57 | 58 | @pytest.fixture() 59 | def context(secp128r1): 60 | scalar = 0x123456789 61 | mult = LTRMultiplier( 62 | secp128r1.curve.coordinate_model.formulas["add-1998-cmo"], 63 | secp128r1.curve.coordinate_model.formulas["dbl-1998-cmo"], 64 | secp128r1.curve.coordinate_model.formulas["z"], 65 | always=True, 66 | complete=False, 67 | short_circuit=True, 68 | ) 69 | with local(DefaultContext()) as ctx: 70 | mult.init(secp128r1, secp128r1.generator) 71 | mult.multiply(scalar) 72 | return ctx 73 | 74 | 75 | def test_mult_hw(context): 76 | lm = HammingWeight() 77 | trace = [] 78 | 79 | def callback(action): 80 | if isinstance(action, FormulaAction): 81 | for intermediate in action.op_results: 82 | leak = lm(intermediate.value) 83 | trace.append(leak) 84 | 85 | context.actions[0].walk(callback) 86 | assert len(trace) > 0 87 | 88 | 89 | def test_mult_hd(context): 90 | lm = HammingDistance() 91 | trace = [] 92 | 93 | def callback(action): 94 | if isinstance(action, FormulaAction): 95 | for intermediate in action.op_results: 96 | if intermediate.op == OpType.Mult: 97 | values = list(map(lambda v: v.value if isinstance(v, OpResult) else v, intermediate.parents)) 98 | leak = lm(*values) 99 | trace.append(leak) 100 | 101 | context.actions[0].walk(callback) 102 | assert len(trace) > 0 103 | -------------------------------------------------------------------------------- /test/sca/test_match.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pyecsca.sca import Trace, match_pattern, match_part, pad 4 | 5 | 6 | def test_simple_match(plot): 7 | pattern = Trace( 8 | np.array([1, 15, 12, -10, 0, 13, 17, -1, 0], dtype=np.dtype("i1")), None 9 | ) 10 | base = Trace( 11 | np.array( 12 | [0, 1, 3, 1, 2, -2, -3, 1, 15, 12, -10, 0, 13, 17, -1, 0, 3, 1], 13 | dtype=np.dtype("i1"), 14 | ), 15 | None, 16 | ) 17 | filtered = match_part(base, 7, 9) 18 | assert filtered == [7] 19 | plot(base=base, pattern=pad(pattern, (filtered[0], 0))) 20 | 21 | 22 | def test_multiple_match(plot): 23 | pattern = Trace( 24 | np.array([1, 15, 12, -10, 0, 13, 17, -1, 0], dtype=np.dtype("i1")), None 25 | ) 26 | base = Trace( 27 | np.array( 28 | [ 29 | 0, 30 | 1, 31 | 3, 32 | 1, 33 | 2, 34 | -2, 35 | -3, 36 | 1, 37 | 18, 38 | 10, 39 | -5, 40 | 0, 41 | 13, 42 | 17, 43 | -1, 44 | 0, 45 | 3, 46 | 1, 47 | 2, 48 | 5, 49 | 13, 50 | 8, 51 | -8, 52 | 1, 53 | 11, 54 | 15, 55 | 0, 56 | 1, 57 | 5, 58 | 2, 59 | 4, 60 | ], 61 | dtype=np.dtype("i1"), 62 | ), 63 | None, 64 | ) 65 | filtered = match_pattern(base, pattern, 0.9) 66 | assert filtered == [7, 19] 67 | plot( 68 | base=base, 69 | pattern1=pad(pattern, (filtered[0], 0)), 70 | pattern2=pad(pattern, (filtered[1], 0)), 71 | ) 72 | -------------------------------------------------------------------------------- /test/sca/test_plot.py: -------------------------------------------------------------------------------- 1 | import holoviews as hv 2 | import matplotlib as mpl 3 | import numpy as np 4 | import pytest 5 | 6 | from pyecsca.sca.trace import Trace 7 | from pyecsca.sca.trace.plot import ( 8 | plot_trace, 9 | save_figure, 10 | save_figure_png, 11 | save_figure_svg, 12 | plot_traces, 13 | ) 14 | 15 | 16 | @pytest.fixture() 17 | def trace1(): 18 | return Trace(np.array([6, 7, 3, -2, 5, 1], dtype=np.dtype("i1"))) 19 | 20 | 21 | @pytest.fixture() 22 | def trace2(): 23 | return Trace(np.array([2, 3, 7, 0, -1, 0], dtype=np.dtype("i1"))) 24 | 25 | 26 | def test_html(trace1, trace2, plot_path): 27 | hv.extension("bokeh") 28 | fig = plot_trace(trace1) 29 | save_figure(fig, str(plot_path())) 30 | other = plot_traces(trace1, trace2) 31 | save_figure(other, str(plot_path())) 32 | 33 | 34 | @pytest.mark.skip("Broken") 35 | def test_png(trace1, plot_path): 36 | hv.extension("matplotlib") 37 | mpl.use("agg") 38 | fig = plot_trace(trace1) 39 | save_figure_png(fig, str(plot_path())) 40 | 41 | 42 | @pytest.mark.skip("Broken without some backend.") 43 | def test_svg(trace1, plot_path): 44 | hv.extension("matplotlib") 45 | fig = plot_trace(trace1) 46 | save_figure_svg(fig, str(plot_path())) 47 | -------------------------------------------------------------------------------- /test/sca/test_process.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyecsca.sca import ( 5 | Trace, 6 | absolute, 7 | invert, 8 | threshold, 9 | rolling_mean, 10 | offset, 11 | recenter, 12 | normalize, 13 | normalize_wl, 14 | transform 15 | ) 16 | 17 | 18 | @pytest.fixture() 19 | def trace(): 20 | return Trace(np.array([30, -60, 145, 247], dtype=np.dtype("i2")), None) 21 | 22 | 23 | def test_absolute(trace): 24 | result = absolute(trace) 25 | assert result is not None 26 | assert result.samples[1] == 60 27 | 28 | 29 | def test_invert(trace): 30 | result = invert(trace) 31 | assert result is not None 32 | np.testing.assert_equal(result.samples, [-30, 60, -145, -247]) 33 | 34 | 35 | def test_threshold(trace): 36 | result = threshold(trace, 128) 37 | assert result is not None 38 | assert result.samples[0] == 0 39 | assert result.samples[2] == 1 40 | 41 | 42 | def test_rolling_mean(trace): 43 | result = rolling_mean(trace, 2) 44 | assert result is not None 45 | assert len(result.samples) == 3 46 | assert result.samples[0] == -15 47 | assert result.samples[1] == 42.5 48 | assert result.samples[2] == 196 49 | 50 | 51 | def test_offset(trace): 52 | result = offset(trace, 5) 53 | assert result is not None 54 | np.testing.assert_equal( 55 | result.samples, np.array([35, -55, 150, 252], dtype=np.dtype("i2")) 56 | ) 57 | 58 | 59 | def test_recenter(trace): 60 | assert recenter(trace) is not None 61 | 62 | 63 | def test_normalize(trace): 64 | result = normalize(trace) 65 | assert result is not None 66 | assert np.isclose(0, np.mean(result.samples)) 67 | assert np.isclose(1, np.var(result.samples)) 68 | 69 | 70 | def test_normalize_wl(trace): 71 | result = normalize_wl(trace) 72 | assert result is not None 73 | assert np.isclose(0, np.mean(result.samples)) 74 | assert np.isclose(1/len(result), np.std(result.samples)) 75 | 76 | 77 | def test_transform(trace): 78 | result = transform(trace, 5, 10) 79 | assert result is not None 80 | assert min(result) == 5 81 | assert max(result) == 10 82 | -------------------------------------------------------------------------------- /test/sca/test_scope.py: -------------------------------------------------------------------------------- 1 | def test_scopes(): 2 | # Just imports for now to fix bogus coverage 3 | try: 4 | from pyecsca.sca.scope.chipwhisperer import ChipWhispererScope 5 | except ImportError: 6 | pass 7 | try: 8 | from pyecsca.sca.scope.picoscope_alt import PicoScopeAlt 9 | except ImportError: 10 | pass 11 | try: 12 | from pyecsca.sca.scope.picoscope_sdk import PicoScopeSdk 13 | except ImportError: 14 | pass 15 | -------------------------------------------------------------------------------- /test/sca/test_stacked_traces.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | from pyecsca.sca import ( 5 | Trace, 6 | StackedTraces, 7 | TraceSet, 8 | ) 9 | 10 | TRACE_COUNT = 2 ** 10 11 | TRACE_LEN = 2 ** 15 12 | 13 | 14 | @pytest.fixture() 15 | def samples(): 16 | np.random.seed(0x1234) 17 | return np.random.rand(TRACE_COUNT, TRACE_LEN) 18 | 19 | 20 | def test_fromarray(samples): 21 | max_len = samples.shape[1] 22 | min_len = max_len // 2 23 | jagged_samples = [ 24 | t[min_len:np.random.randint(max_len)] 25 | for t 26 | in samples 27 | ] 28 | min_len = min(map(len, jagged_samples)) 29 | stacked = StackedTraces.fromarray(jagged_samples) 30 | 31 | assert isinstance(stacked, StackedTraces) 32 | assert stacked.samples.shape == \ 33 | (samples.shape[0], min_len) 34 | assert (stacked.samples == samples[:, :min_len]).all() 35 | 36 | 37 | def test_fromtraceset(samples): 38 | max_len = samples.shape[1] 39 | min_len = max_len // 2 40 | traces = [ 41 | Trace(t[min_len:np.random.randint(max_len)]) 42 | for t 43 | in samples 44 | ] 45 | tset = TraceSet(*traces) 46 | min_len = min(map(len, traces)) 47 | stacked = StackedTraces.fromtraceset(tset) 48 | 49 | assert isinstance(stacked, StackedTraces) 50 | assert stacked.samples.shape == \ 51 | (samples.shape[0], min_len) 52 | assert (stacked.samples == samples[:, :min_len]).all() 53 | -------------------------------------------------------------------------------- /test/sca/test_test.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from pyecsca.sca import Trace, welch_ttest, student_ttest, ks_test 7 | 8 | 9 | @pytest.fixture() 10 | def data(): 11 | Data = namedtuple("Data", ["a", "b", "c", "d"]) 12 | return Data( 13 | a=Trace(np.array([20, 80], dtype=np.dtype("i1"))), 14 | b=Trace(np.array([30, 42], dtype=np.dtype("i1"))), 15 | c=Trace(np.array([78, 56], dtype=np.dtype("i1"))), 16 | d=Trace(np.array([98, 36], dtype=np.dtype("i1"))), 17 | ) 18 | 19 | 20 | def test_welch_ttest(data): 21 | assert welch_ttest([data.a, data.b], [data.c, data.d]) is not None 22 | a = Trace(np.array([19.8, 20.4, 19.6, 17.8, 18.5, 18.9, 18.3, 18.9, 19.5, 22.0])) 23 | b = Trace(np.array([28.2, 26.6, 20.1, 23.3, 25.2, 22.1, 17.7, 27.6, 20.6, 13.7])) 24 | c = Trace(np.array([20.2, 21.6, 27.1, 13.3, 24.2, 20.1, 11.7, 25.6, 26.6, 21.4])) 25 | 26 | result = welch_ttest([a, b], [b, c], dof=True, p_value=True) 27 | assert result is not None 28 | 29 | 30 | def test_students_ttest(data): 31 | with pytest.raises(ValueError): 32 | student_ttest([], []) 33 | assert student_ttest([data.a, data.b], [data.c, data.d]) is not None 34 | 35 | 36 | def test_ks_test(data): 37 | with pytest.raises(ValueError): 38 | assert ks_test([], []) 39 | assert ks_test([data.a, data.b], [data.c, data.d]) is not None 40 | -------------------------------------------------------------------------------- /test/sca/test_trace.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pyecsca.sca import Trace 4 | 5 | 6 | def test_basic(): 7 | trace = Trace(np.array([10, 15, 24], dtype=np.dtype("i1"))) 8 | assert trace is not None 9 | assert "Trace" in str(trace) 10 | assert trace.trace_set is None 11 | 12 | 13 | def test_astype(): 14 | trace = Trace(np.array([10, 15, 24], dtype=np.dtype("i1"))) 15 | ta = trace.astype(np.float32) 16 | assert ta.samples.dtype == np.float32 17 | --------------------------------------------------------------------------------