├── .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 |
--------------------------------------------------------------------------------