aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJán Jančár2023-03-15 15:10:18 +0100
committerGitHub2023-03-15 15:10:18 +0100
commit4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b (patch)
treef2b3ec1afad390e452333e538d655aaad51635c9
parentd9ebb0c6db27c5271b93f8faa0b5308d6e2c63cc (diff)
parent445eaa41f22ed82502ca813e98a92c2b078c9a79 (diff)
downloadpyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.tar.gz
pyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.tar.zst
pyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.zip
Merge branch 'master' into feat/stacked-perf-test
-rw-r--r--.github/workflows/lint.yml4
-rw-r--r--.github/workflows/test.yml4
-rw-r--r--.pre-commit-config.yaml10
-rw-r--r--LICENSE2
-rw-r--r--Makefile4
-rw-r--r--README.md9
-rw-r--r--docs/Makefile2
-rw-r--r--docs/_modules.rst10
-rw-r--r--docs/conf.py9
-rw-r--r--docs/index.rst168
m---------notebook0
-rw-r--r--pyecsca/ec/configuration.py2
-rw-r--r--pyecsca/ec/context.py72
-rw-r--r--pyecsca/ec/coordinates.py5
-rw-r--r--pyecsca/ec/curve.py5
-rw-r--r--pyecsca/ec/error.py2
-rw-r--r--pyecsca/ec/formula.py37
-rw-r--r--pyecsca/ec/key_agreement.py2
-rw-r--r--pyecsca/ec/key_generation.py2
-rw-r--r--pyecsca/ec/mod.py75
-rw-r--r--pyecsca/ec/model.py8
-rw-r--r--pyecsca/ec/mult.py20
-rw-r--r--pyecsca/ec/naf.py2
-rw-r--r--pyecsca/ec/op.py2
-rw-r--r--pyecsca/ec/params.py6
-rw-r--r--pyecsca/ec/point.py7
-rw-r--r--pyecsca/ec/signature.py11
m---------pyecsca/ec/std0
-rw-r--r--pyecsca/ec/transformations.py2
-rw-r--r--pyecsca/misc/cfg.py8
-rw-r--r--pyecsca/sca/re/rpa.py2
-rw-r--r--pyecsca/sca/scope/base.py2
-rw-r--r--pyecsca/sca/scope/chipwhisperer.py12
-rw-r--r--pyecsca/sca/scope/picoscope_alt.py2
-rw-r--r--pyecsca/sca/scope/picoscope_sdk.py306
-rw-r--r--pyecsca/sca/target/ISO7816.py57
-rw-r--r--pyecsca/sca/target/PCSC.py18
-rw-r--r--pyecsca/sca/target/__init__.py18
-rw-r--r--pyecsca/sca/target/base.py2
-rw-r--r--pyecsca/sca/target/binary.py8
-rw-r--r--pyecsca/sca/target/chipwhisperer.py2
-rw-r--r--pyecsca/sca/target/ectester.py193
-rw-r--r--pyecsca/sca/target/flash.py2
-rw-r--r--pyecsca/sca/target/leia.py45
-rw-r--r--pyecsca/sca/target/serial.py2
-rw-r--r--pyecsca/sca/target/simpleserial.py5
-rw-r--r--pyecsca/sca/trace/align.py4
-rw-r--r--pyecsca/sca/trace/combine.py2
-rw-r--r--pyecsca/sca/trace/edit.py6
-rw-r--r--pyecsca/sca/trace/filter.py2
-rw-r--r--pyecsca/sca/trace/match.py2
-rw-r--r--pyecsca/sca/trace/plot.py2
-rw-r--r--pyecsca/sca/trace/process.py2
-rw-r--r--pyecsca/sca/trace/sampling.py2
-rw-r--r--pyecsca/sca/trace/test.py2
-rw-r--r--pyecsca/sca/trace/trace.py14
-rw-r--r--pyecsca/sca/trace_set/base.py2
-rw-r--r--pyecsca/sca/trace_set/hdf5.py2
-rw-r--r--pyecsca/sca/trace_set/inspector.py2
-rw-r--r--pyecsca/sca/trace_set/pickle.py2
-rw-r--r--setup.py8
-rwxr-xr-xtest/ec/perf_formula.py6
-rw-r--r--test/ec/test_context.py15
-rw-r--r--test/ec/test_mod.py3
-rw-r--r--test/ec/test_params.py24
-rw-r--r--test/utils.py8
66 files changed, 800 insertions, 476 deletions
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 0c83e32..0d02c48 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -40,10 +40,10 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -U pip setuptools wheel
- pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, gmp, test, dev]"
+ pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, leia, gmp, test, dev]"
- name: Typecheck
run: |
- make typecheck-all
+ make typecheck
- name: Codestyle
run: |
make codestyle-all
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b58efa2..42eb8ba 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -50,8 +50,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -U pip setuptools wheel
- if [ $USE_GMP == 1 ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, gmp, test, dev]"; fi
- if [ $USE_GMP == 0 ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, test, dev]"; fi
+ if [ $USE_GMP == 1 ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, leia, gmp, test, dev]"; fi
+ if [ $USE_GMP == 0 ]; then pip install -e ".[picoscope_sdk, picoscope_alt, chipwhisperer, smartcard, leia, test, dev]"; fi
- name: Test
run: |
make test
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2ac2274..1969532 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v3.4.0
+ rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -11,12 +11,14 @@ repos:
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v0.812
+ rev: v0.991
hooks:
- id: mypy
- args: [--ignore-missing-imports, --show-error-codes]
+ additional_dependencies:
+ - "types-setuptools"
+ args: [--ignore-missing-imports, --show-error-codes, --namespace-packages, --explicit-package-bases, --check-untyped-defs]
- repo: https://github.com/PyCQA/flake8
- rev: 3.9.0
+ rev: 6.0.0
hooks:
- id: flake8
args: ["--extend-ignore=E501,F405,F403,F401,E126,E203"]
diff --git a/LICENSE b/LICENSE
index 2df33a6..109acf2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2018-2020 Jan Jancar
+Copyright (c) 2018-2023 Jan Jancar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index 4329feb..547797d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,10 +20,10 @@ test-all:
nose2 -s test -C -v ${TESTS}
typecheck:
- mypy pyecsca --ignore-missing-imports --show-error-codes
+ mypy --namespace-packages -p pyecsca --ignore-missing-imports --show-error-codes --check-untyped-defs
typecheck-all:
- mypy pyecsca test --ignore-missing-imports --show-error-codes
+ mypy --namespace-packages -p pyecsca -p test --ignore-missing-imports --show-error-codes --check-untyped-defs
codestyle:
flake8 --extend-ignore=E501,F405,F403,F401,E126,E203 pyecsca
diff --git a/README.md b/README.md
index b2a9b05..bb16813 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ It is currently in an alpha stage of development and thus only provides:
- Trace acquisition using PicoScope/ChipWhisperer oscilloscopes (see [notebook/measurement](https://neuromancer.sk/pyecsca/notebook/measurement.html))
- Trace processing capabilities, e.g. signal-processing, filtering, averaging, cutting, aligning ([pyecsca.sca](https://neuromancer.sk/pyecsca/api/pyecsca.sca.html))
- Trace visualization using holoviews and datashader (see [notebook/visualization](https://neuromancer.sk/pyecsca/notebook/visualization.html))
+ - Communication via PCSC/LEIA with a smartcard target (see [notebook/smartcards](https://neuromancer.sk/pyecsca/notebook/smartcards.html))
**pyecsca** consists of three packages:
- the core: https://github.com/J08nY/pyecsca
@@ -49,6 +50,8 @@ It is currently in an alpha stage of development and thus only provides:
- [chipwhisperer](https://github.com/newaetech/chipwhisperer)
- **Smartcard support:**
- [pyscard](https://pyscard.sourceforge.io/)
+ - **LEIA support:**
+ - [smartleia](https://pypi.org/project/smartleia/)
- **Faster arithmetic:**
- [gmpy2](https://gmpy2.readthedocs.io/) (and also GMP library)
@@ -80,13 +83,14 @@ Use [black](https://github.com/psf/black) for code-formatting.
- [sphinx-autodoc-typehints](https://pypi.org/project/sphinx-autodoc-typehints/)
- [nbsphinx](https://nbsphinx.readthedocs.io/)
- [sphinx-paramlinks](https://pypi.org/project/sphinx-paramlinks/)
+ - [sphinx-design](https://sphinx-design.readthedocs.io/)
## License
MIT License
- Copyright (c) 2018-2021 Jan Jancar
+ Copyright (c) 2018-2023 Jan Jancar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -107,5 +111,4 @@ Use [black](https://github.com/psf/black) for code-formatting.
SOFTWARE.
-*Development is supported by the Masaryk University grant [MUNI/C/1701/2018](https://www.muni.cz/en/research/projects/46834),
-this support is very appreciated.*
+*Development was supported by the Masaryk University grant [MUNI/C/1701/2018](https://www.muni.cz/en/research/projects/46834).*
diff --git a/docs/Makefile b/docs/Makefile
index cdf5beb..9f705ec 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -16,7 +16,7 @@ apidoc:
mkdir -p api/
sphinx-apidoc ../pyecsca/ --implicit-namespaces --ext-autodoc --no-toc -M -f -e -o api/
sphinx-apidoc ../../pyecsca-codegen/pyecsca/ --implicit-namespaces --ext-autodoc --no-toc -M -f -e -o api/
- cp _modules.rst api/modules.rst
+ mv _modules.rst api/modules.rst
.PHONY: help apidoc Makefile
diff --git a/docs/_modules.rst b/docs/_modules.rst
deleted file mode 100644
index ec37f15..0000000
--- a/docs/_modules.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-pyecsca
-=======
-
-.. toctree::
- :maxdepth: 4
-
- pyecsca.ec
- pyecsca.misc
- pyecsca.sca
- pyecsca.codegen
diff --git a/docs/conf.py b/docs/conf.py
index 7f20953..f2c1de2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,7 +20,7 @@ sys.path.insert(0, os.path.abspath('../notebook/'))
# -- Project information -----------------------------------------------------
project = 'pyecsca'
-copyright = '2018-2021, Jan Jancar'
+copyright = '2018-2023, Jan Jancar'
author = 'Jan Jancar'
sys.path.append(os.path.abspath('..'))
@@ -47,6 +47,7 @@ extensions = [
'sphinx.ext.mathjax',
'sphinx.ext.viewcode',
'sphinx_paramlinks',
+ 'sphinx_design',
'nbsphinx'
]
@@ -67,7 +68,7 @@ master_doc = 'index'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
-language = None
+language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@@ -108,7 +109,8 @@ html_favicon = "_static/logo_black.png"
html_css_files = [
'custom.css',
- 'graphik.css'
+ 'graphik.css',
+ 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.3.0/css/all.min.css'
]
# Custom sidebar templates, must be a dictionary that maps document names
@@ -212,3 +214,4 @@ autodoc_default_options = {
autoclass_content = "both"
nbsphinx_allow_errors = True
+nbsphinx_execute = "never"
diff --git a/docs/index.rst b/docs/index.rst
index bb95c4a..4d9a3c6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -25,24 +25,62 @@ is to be able to reverse engineer the curve model, coordinate system, addition f
multiplier and even finite-field implementation details.
It is currently in an alpha stage of development and thus only provides:
- - Enumeration of millions of possible ECC implementation configurations (see :doc:`notebook/configuration_space`)
- - Simulation and execution tracing of key generation, ECDH and ECDSA (see :doc:`notebook/simulation`)
- - Synthesis of C implementations of ECC for embedded devices, given any implementation configuration (see :doc:`notebook/codegen`)
- - Trace acquisition using PicoScope/ChipWhisperer oscilloscopes (see :doc:`notebook/measurement`)
- - Trace processing capabilities, e.g. signal-processing, filtering, averaging, cutting, aligning (:doc:`api/pyecsca.sca`)
- - Trace visualization using holoviews and datashader (see :doc:`notebook/visualization`)
-**pyecsca** consists of three packages:
- - the core: https://github.com/J08nY/pyecsca
- - the codegen package: https://github.com/J08nY/pyecsca-codegen
- - the notebook package: https://github.com/J08nY/pyecsca-notebook
+.. card:: Enumeration
-Notebooks
-=========
+ Enumeration of millions of possible ECC implementation configurations (see :doc:`notebook/configuration_space`)
+
+.. card:: Simulation
+
+ Simulation and execution tracing of key generation, ECDH and ECDSA (see :doc:`notebook/simulation`)
+
+.. card:: Code generation
+
+ Synthesis of C implementations of ECC for embedded devices, given any implementation configuration (see :doc:`notebook/codegen`)
+
+.. card:: Trace acquisition
+
+ Trace acquisition using PicoScope/ChipWhisperer oscilloscopes (see :doc:`notebook/measurement`)
+
+.. card:: Trace processing
+
+ Trace processing capabilities, e.g. signal-processing, filtering, averaging, cutting, aligning (:doc:`api/pyecsca.sca`)
+
+.. card:: Trace visualization
+
+ Trace visualization using holoviews and datashader (see :doc:`notebook/visualization`)
+
+.. card:: Smartcard communication
+
+ Communication via PCSC/LEIA with a smartcard target (see :doc:`notebook/smartcards`)
+
+**pyecsca** consists of three repositories:
+
+.. grid:: 3
+
+ .. grid-item-card:: Core
+
+ The `core <https://github.com/J08nY/pyecsca>`_ package contains the core of the
+ functionality, except the code generation and notebooks.
+
+ .. grid-item-card:: Codegen
+
+ The `codegen <https://github.com/J08nY/pyecsca-codegen>`_ package contains
+ the code generation functionality.
+
+ .. grid-item-card:: Notebook
+
+ The `notebook <https://github.com/J08nY/pyecsca-notebook>`_ repository contains
+ example notebooks that showcase functionality of the toolkit.
+
+
+:fas:`book` Notebooks
+=========================
The notebooks below contain a showcase of what is possible using **pyecsca** and
are the best source of documentation on how to use **pyecsca**.
.. toctree::
+ :caption: Notebooks
:titlesonly:
:maxdepth: 1
@@ -51,9 +89,10 @@ are the best source of documentation on how to use **pyecsca**.
notebook/codegen
notebook/measurement
notebook/visualization
+ notebook/smartcards
-API reference
-=============
+:fas:`code` API reference
+=========================
.. toctree::
:caption: API reference
@@ -65,70 +104,76 @@ API reference
Requirements
============
- - Numpy_
- - Scipy_
- - sympy_
- - atpublic_
- - fastdtw_
- - asn1crypto_
- - h5py_
- - holoviews_
- - bokeh_
- - datashader_
- - matplotlib_
- - xarray_
- - astunparse_
- - **Optionally**:
+.. dropdown:: General
- - **Oscilloscope support:**
+ - Numpy_
+ - Scipy_
+ - sympy_
+ - atpublic_
+ - fastdtw_
+ - asn1crypto_
+ - h5py_
+ - holoviews_
+ - bokeh_
+ - datashader_
+ - matplotlib_
+ - xarray_
+ - astunparse_
+ - **Optionally**:
- - picosdk_
- - picoscope_
- - chipwhisperer_
- - **Smartcard support:**
+ - **Oscilloscope support:**
- - pyscard_
+ - picosdk_
+ - picoscope_
+ - chipwhisperer_
+ - **Smartcard support:**
- - **Faster arithmetic:**
+ - pyscard_
+ - **LEIA support:**
- - gmpy2_ (and also GMP library)
+ - leia_
+ - **Faster arithmetic:**
-*pyecsca* contains data from the `Explicit-Formulas Database`_ by Daniel J. Bernstein and Tanja Lange.
+ - gmpy2_ (and also GMP library)
-It also supports working with Riscure_ Inspector trace sets, which are of a proprietary format.
+ *pyecsca* contains data from the `Explicit-Formulas Database`_ by Daniel J. Bernstein and Tanja Lange.
+ It also supports working with Riscure_ Inspector trace sets, which are of a proprietary format.
-Testing & Development
----------------------
-See the Makefile for tests, performance measurement, codestyle and type checking commands.
-Use black_ for code-formatting.
+.. dropdown:: Testing & Development
- - nose2_
- - green_
- - parameterized_
- - mypy_
- - flake8_
- - coverage_
- - interrogate_
- - pyinstrument_
- - pre-commit_
- - black_
+ See the Makefile for tests, performance measurement, codestyle and type checking commands.
+ Use black_ for code-formatting.
-Docs
-----
+ - nose2_
+ - green_
+ - parameterized_
+ - mypy_
+ - flake8_
+ - coverage_
+ - interrogate_
+ - pyinstrument_
+ - pre-commit_
+ - black_
- - sphinx_
- - sphinx-autodoc-typehints_
- - nbsphinx_
- - sphinx-paramlinks_
+
+.. dropdown:: Docs
+
+ - sphinx_
+ - sphinx-autodoc-typehints_
+ - nbsphinx_
+ - sphinx-paramlinks_
+ - sphinx-design_
License
=======
+.. dropdown:: MIT License
+
MIT License
- Copyright (c) 2018-2021 Jan Jancar
+ Copyright (c) 2018-2023 Jan Jancar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -148,8 +193,7 @@ License
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-Development is supported by the Masaryk University grant `MUNI/C/1707/2018 <https://www.muni.cz/en/research/projects/46834>`_,
-this support is very appreciated.
+Development was supported by the Masaryk University grant `MUNI/C/1707/2018 <https://www.muni.cz/en/research/projects/46834>`_.
.. _Numpy: https://www.numpy.org
.. _Scipy: https://www.scipy.org
@@ -168,6 +212,7 @@ this support is very appreciated.
.. _picoscope: https://github.com/colinoflynn/pico-python
.. _chipwhisperer: https://github.com/newaetech/chipwhisperer
.. _pyscard: https://pyscard.sourceforge.io/
+.. _leia: https://pypi.org/project/smartleia/
.. _gmpy2: https://gmpy2.readthedocs.io/
.. _nose2: https://nose2.readthedocs.io
.. _green: https://github.com/CleanCut/green
@@ -183,5 +228,6 @@ this support is very appreciated.
.. _sphinx-autodoc-typehints: https://pypi.org/project/sphinx-autodoc-typehints/
.. _nbsphinx: https://nbsphinx.readthedocs.io/
.. _sphinx-paramlinks: https://pypi.org/project/sphinx-paramlinks/
+.. _sphinx-design: https://pypi.org/project/sphinx_design/
.. _Explicit-Formulas Database: https://www.hyperelliptic.org/EFD/index.html
.. _Riscure: https://www.riscure.com/
diff --git a/notebook b/notebook
-Subproject 260f0305d6ccb5825c3d0872c25d49e61677322
+Subproject 06b53bea04b38564b1205e48d802601aa0d255a
diff --git a/pyecsca/ec/configuration.py b/pyecsca/ec/configuration.py
index 04ee41c..79b7f4c 100644
--- a/pyecsca/ec/configuration.py
+++ b/pyecsca/ec/configuration.py
@@ -1,4 +1,4 @@
-"""This module provides a way to work with and enumerate implementation configurations."""
+"""Provides a way to work with and enumerate implementation configurations."""
from dataclasses import dataclass
from enum import Enum
from itertools import product
diff --git a/pyecsca/ec/context.py b/pyecsca/ec/context.py
index 5357907..49c886d 100644
--- a/pyecsca/ec/context.py
+++ b/pyecsca/ec/context.py
@@ -1,5 +1,5 @@
"""
-This module provides classes for tracing the execution of operations.
+Provides classes for tracing the execution of operations.
The operations include key generation, scalar multiplication, formula execution and individual operation evaluation.
These operations are traced in `Context` classes using `Actions`. Different contexts trace actions differently.
@@ -14,7 +14,6 @@ A :py:class:`NullContext` does not trace any actions and is the default context.
"""
from abc import abstractmethod, ABC
from collections import OrderedDict
-from contextvars import ContextVar, Token
from copy import deepcopy
from typing import List, Optional, ContextManager, Any, Tuple, Sequence
@@ -31,12 +30,14 @@ class Action:
self.inside = False
def __enter__(self):
- getcontext().enter_action(self)
+ if current is not None:
+ current.enter_action(self)
self.inside = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
- getcontext().exit_action(self)
+ if current is not None:
+ current.exit_action(self)
self.inside = False
@@ -64,10 +65,10 @@ class ResultAction(Action):
def __exit__(self, exc_type, exc_val, exc_tb):
if (
- not self._has_result
- and exc_type is None
- and exc_val is None
- and exc_tb is None
+ not self._has_result
+ and exc_type is None
+ and exc_val is None
+ and exc_tb is None
):
raise RuntimeError("Result unset on action exit")
super().__exit__(exc_type, exc_val, exc_tb)
@@ -166,17 +167,6 @@ class Context(ABC):
@public
-class NullContext(Context):
- """Context that does not trace any actions."""
-
- def enter_action(self, action: Action) -> None:
- pass
-
- def exit_action(self, action: Action) -> None:
- pass
-
-
-@public
class DefaultContext(Context):
"""Context that traces executions of actions in a tree."""
@@ -240,48 +230,22 @@ class PathContext(Context):
)
-_actual_context: ContextVar[Context] = ContextVar(
- "operational_context", default=NullContext()
-)
+current: Optional[Context] = None
class _ContextManager:
def __init__(self, new_context):
self.new_context = deepcopy(new_context)
- def __enter__(self) -> Context:
- self.token = setcontext(self.new_context)
- return self.new_context
+ def __enter__(self) -> Optional[Context]:
+ global current
+ self.old_context = current
+ current = self.new_context
+ return current
def __exit__(self, t, v, tb):
- resetcontext(self.token)
-
-
-@public
-def getcontext() -> Context:
- """Get the current thread/task context."""
- return _actual_context.get()
-
-
-@public
-def setcontext(ctx: Context) -> Token:
- """
- Set the current thread/task context.
-
- :param ctx: A context to set.
- :return: A token to restore previous context.
- """
- return _actual_context.set(ctx)
-
-
-@public
-def resetcontext(token: Token):
- """
- Reset the context to a previous value.
-
- :param token: A token to restore.
- """
- _actual_context.reset(token)
+ global current
+ current = self.old_context
@public
@@ -293,5 +257,5 @@ def local(ctx: Optional[Context] = None) -> ContextManager:
:return: A context manager.
"""
if ctx is None:
- ctx = getcontext()
+ ctx = current
return _ContextManager(ctx)
diff --git a/pyecsca/ec/coordinates.py b/pyecsca/ec/coordinates.py
index a9a8a8b..b258f2d 100644
--- a/pyecsca/ec/coordinates.py
+++ b/pyecsca/ec/coordinates.py
@@ -1,4 +1,4 @@
-"""This module provides a coordinate model class."""
+"""Provides a coordinate model class."""
from ast import parse, Module
from os.path import join
from typing import List, Any, MutableMapping
@@ -66,6 +66,9 @@ class AffineCoordinateModel(CoordinateModel):
return False
return self.curve_model == other.curve_model
+ def __hash__(self):
+ return hash(self.curve_model) + hash(self.name)
+
class EFDCoordinateModel(CoordinateModel):
def __init__(self, dir_path: str, name: str, curve_model: Any):
diff --git a/pyecsca/ec/curve.py b/pyecsca/ec/curve.py
index 4a14139..8fc2793 100644
--- a/pyecsca/ec/curve.py
+++ b/pyecsca/ec/curve.py
@@ -1,4 +1,4 @@
-"""This module provides an elliptic curve class."""
+"""Provides an elliptic curve class."""
from ast import Module
from copy import copy
from typing import MutableMapping, Union, List, Optional
@@ -276,6 +276,9 @@ class EllipticCurve:
and self.parameters == other.parameters
)
+ def __hash__(self):
+ return hash((self.model, self.coordinate_model, self.prime, self.parameters))
+
def __str__(self):
return "EllipticCurve"
diff --git a/pyecsca/ec/error.py b/pyecsca/ec/error.py
index 87ca335..cf71c42 100644
--- a/pyecsca/ec/error.py
+++ b/pyecsca/ec/error.py
@@ -1,4 +1,4 @@
-"""This module contains exceptions and warnings used in the library."""
+"""Contains exceptions and warnings used in the library."""
import warnings
from public import public
from ..misc.cfg import getconfig
diff --git a/pyecsca/ec/formula.py b/pyecsca/ec/formula.py
index aac239f..ec8f9c0 100644
--- a/pyecsca/ec/formula.py
+++ b/pyecsca/ec/formula.py
@@ -1,6 +1,8 @@
-"""This module provides an abstract base class of a formula along with concrete instantiations."""
+"""Provides an abstract base class of a formula along with concrete instantiations."""
from abc import ABC, abstractmethod
from ast import parse, Expression
+from functools import cached_property
+
from astunparse import unparse
from itertools import product
from typing import List, Set, Any, ClassVar, MutableMapping, Tuple, Union, Dict
@@ -9,7 +11,8 @@ from pkg_resources import resource_stream
from public import public
from sympy import sympify, FF, symbols, Poly, Rational
-from .context import ResultAction, getcontext, NullContext
+from .context import ResultAction
+from . import context
from .error import UnsatisfiedAssumptionError, raise_unsatisified_assumption
from .mod import Mod, SymbolicMod
from .op import CodeOp, OpType
@@ -67,8 +70,6 @@ class FormulaAction(ResultAction):
self.output_points = []
def add_operation(self, op: CodeOp, value: Mod):
- if isinstance(getcontext(), NullContext):
- return
parents: List[Union[Mod, OpResult]] = []
for parent in {*op.variables, *op.parameters}:
if parent in self.intermediates:
@@ -79,8 +80,6 @@ class FormulaAction(ResultAction):
li.append(OpResult(op.result, value, op.operator, *parents))
def add_result(self, point: Any, **outputs: Mod):
- if isinstance(getcontext(), NullContext):
- return
for k in outputs:
self.outputs[k] = self.intermediates[k][-1]
self.output_points.append(point)
@@ -117,6 +116,10 @@ class Formula(ABC):
unified: bool
"""Whether the formula is specifies that it is unified."""
+ @cached_property
+ def assumptions_str(self):
+ return [unparse(assumption)[1:-2] for assumption in self.assumptions]
+
def __validate_params(self, field, params):
for key, value in params.items():
if not isinstance(value, Mod) or value.n != field:
@@ -141,8 +144,7 @@ class Formula(ABC):
# Validate assumptions and compute formula parameters.
# TODO: Should this also validate coordinate assumptions and compute their parameters?
is_symbolic = any(isinstance(x, SymbolicMod) for x in params.values())
- for assumption in self.assumptions:
- assumption_string = unparse(assumption)[1:-2]
+ for assumption, assumption_string in zip(self.assumptions, self.assumptions_str):
lhs, rhs = assumption_string.split(" == ")
if lhs in params:
# Handle an assumption check on value of input points.
@@ -181,7 +183,7 @@ class Formula(ABC):
f"This formula couldn't be executed due to an unsupported assumption ({assumption_string})."
)
- def resolve(expression):
+ def resolve(expression, k):
if not expression.args:
return expression
args = []
@@ -189,13 +191,13 @@ class Formula(ABC):
if isinstance(arg, Rational):
a = arg.p
b = arg.q
- arg = k(a) / k(b)
+ res = k(a) / k(b)
else:
- arg = resolve(arg)
- args.append(arg)
+ res = resolve(arg, k)
+ args.append(res)
return expression.func(*args)
- expr = resolve(expr)
+ expr = resolve(expr, k)
poly = Poly(expr, symbols(param), domain=k)
roots = poly.ground_roots()
for root in roots:
@@ -220,7 +222,8 @@ class Formula(ABC):
self.__validate_params(field, params)
self.__validate_points(field, points, params)
- self.__validate_assumptions(field, params)
+ if self.assumptions:
+ self.__validate_assumptions(field, params)
# Execute the actual formula.
with FormulaAction(self, *points, **params) as action:
for op in self.code:
@@ -234,7 +237,8 @@ class Formula(ABC):
)
if not isinstance(op_result, Mod):
op_result = Mod(op_result, field)
- action.add_operation(op, op_result)
+ if context.current is not None:
+ action.add_operation(op, op_result)
params[op.result] = op_result
result = []
# Go over the outputs and construct the resulting points.
@@ -248,7 +252,8 @@ class Formula(ABC):
full_resulting[full_variable] = params[full_variable]
point = Point(self.coordinate_model, **resulting)
- action.add_result(point, **full_resulting)
+ if context.current is not None:
+ action.add_result(point, **full_resulting)
result.append(point)
return action.exit(tuple(result))
diff --git a/pyecsca/ec/key_agreement.py b/pyecsca/ec/key_agreement.py
index 2141547..555005f 100644
--- a/pyecsca/ec/key_agreement.py
+++ b/pyecsca/ec/key_agreement.py
@@ -1,4 +1,4 @@
-"""This module provides an implementation of ECDH (Elliptic Curve Diffie-Hellman)."""
+"""Provides an implementation of ECDH (Elliptic Curve Diffie-Hellman)."""
import hashlib
from typing import Optional, Any
diff --git a/pyecsca/ec/key_generation.py b/pyecsca/ec/key_generation.py
index abc963d..43a20cd 100644
--- a/pyecsca/ec/key_generation.py
+++ b/pyecsca/ec/key_generation.py
@@ -1,4 +1,4 @@
-"""This module provides a key generator for elliptic curve keypairs."""
+"""Provides a key generator for elliptic curve keypairs."""
from typing import Tuple
from public import public
diff --git a/pyecsca/ec/mod.py b/pyecsca/ec/mod.py
index 47bf2b3..a43db53 100644
--- a/pyecsca/ec/mod.py
+++ b/pyecsca/ec/mod.py
@@ -1,5 +1,5 @@
"""
-This module provides several implementations of an element of ℤₙ.
+Provides several implementations of an element of ℤₙ.
The base class :py:class:`Mod` dynamically
dispatches to the implementation chosen by the runtime configuration of the library
@@ -10,7 +10,7 @@ dispatches to the implementation chosen by the runtime configuration of the libr
import random
import secrets
from functools import wraps, lru_cache
-from typing import Type, Dict, Any, Tuple
+from typing import Type, Dict, Any, Tuple, Union
from public import public
from sympy import Expr, FF
@@ -35,7 +35,7 @@ def gcd(a, b):
return gcd(b, a)
while abs(b) > 0:
- q, r = divmod(a, b)
+ _, r = divmod(a, b)
a, b = b, r
return a
@@ -43,9 +43,9 @@ def gcd(a, b):
@public
def extgcd(a, b):
- """Extended Euclid's greatest common denominator algorithm."""
+ """Compute the extended Euclid's greatest common denominator algorithm."""
if abs(b) > abs(a):
- (x, y, d) = extgcd(b, a)
+ x, y, d = extgcd(b, a)
return y, x, d
if abs(b) == 0:
@@ -116,9 +116,8 @@ def _check(func):
def method(self, other):
if type(self) is not type(other):
other = self.__class__(other, self.n)
- else:
- if self.n != other.n:
- raise ValueError
+ elif self.n != other.n:
+ raise ValueError
return func(self, other)
return method
@@ -147,6 +146,7 @@ class Mod:
x: Any
n: Any
+ __slots__ = ("x", "n")
def __new__(cls, *args, **kwargs):
if cls != Mod:
@@ -198,7 +198,7 @@ class Mod:
def sqrt(self) -> "Mod":
"""
- The modular square root of this element (only implemented for prime modulus).
+ Compute the modular square root of this element (only implemented for prime modulus).
Uses the `Tonelli-Shanks <https://en.wikipedia.org/wiki/Tonelli–Shanks_algorithm>`_ algorithm.
"""
@@ -263,6 +263,7 @@ class RawMod(Mod):
x: int
n: int
+ __slots__ = ("x", "n")
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
@@ -274,7 +275,7 @@ class RawMod(Mod):
def inverse(self) -> "RawMod":
if self.x == 0:
raise_non_invertible()
- x, y, d = extgcd(self.x, self.n)
+ x, _, d = extgcd(self.x, self.n)
if d != 1:
raise_non_invertible()
return RawMod(x, self.n)
@@ -367,6 +368,7 @@ _mod_classes["python"] = RawMod
@public
class Undefined(Mod):
"""A special undefined element."""
+ __slots__ = ("x", "n")
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
@@ -472,6 +474,7 @@ class SymbolicMod(Mod):
x: Expr
n: int
+ __slots__ = ("x", "n")
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
@@ -580,25 +583,30 @@ if has_gmp:
x: gmpy2.mpz
n: gmpy2.mpz
+ __slots__ = ("x", "n")
def __new__(cls, *args, **kwargs):
return object.__new__(cls)
- def __init__(self, x: int, n: int):
- self.x = gmpy2.mpz(x % n)
- self.n = gmpy2.mpz(n)
+ def __init__(self, x: Union[int, gmpy2.mpz], n: Union[int, gmpy2.mpz], ensure: bool = True):
+ if ensure:
+ self.n = gmpy2.mpz(n)
+ self.x = gmpy2.mpz(x % self.n)
+ else:
+ self.n = n
+ self.x = x
def inverse(self) -> "GMPMod":
if self.x == 0:
raise_non_invertible()
if self.x == 1:
- return GMPMod(1, self.n)
+ return GMPMod(gmpy2.mpz(1), self.n, ensure=False)
try:
res = gmpy2.invert(self.x, self.n)
except ZeroDivisionError:
raise_non_invertible()
- res = 0
- return GMPMod(res, self.n)
+ res = gmpy2.mpz(0)
+ return GMPMod(res, self.n, ensure=False)
def is_residue(self) -> bool:
if not _is_prime(self.n):
@@ -613,7 +621,7 @@ if has_gmp:
if not _is_prime(self.n):
raise NotImplementedError
if self.x == 0:
- return GMPMod(0, self.n)
+ return GMPMod(gmpy2.mpz(0), self.n, ensure=False)
if not self.is_residue():
raise_non_residue()
if self.n % 4 == 3:
@@ -624,12 +632,12 @@ if has_gmp:
q //= 2
s += 1
- z = 2
- while GMPMod(z, self.n).is_residue():
+ z = gmpy2.mpz(2)
+ while GMPMod(z, self.n, ensure=False).is_residue():
z += 1
m = s
- c = GMPMod(z, self.n) ** int(q)
+ c = GMPMod(z, self.n, ensure=False) ** int(q)
t = self ** int(q)
r_exp = (q + 1) // 2
r = self ** int(r_exp)
@@ -639,17 +647,32 @@ if has_gmp:
while not (t ** (2 ** i)) == 1:
i += 1
two_exp = m - (i + 1)
- b = c ** int(GMPMod(2, self.n) ** two_exp)
- m = int(GMPMod(i, self.n))
+ b = c ** int(GMPMod(gmpy2.mpz(2), self.n, ensure=False) ** two_exp)
+ m = int(GMPMod(gmpy2.mpz(i), self.n, ensure=False))
c = b ** 2
t *= c
r *= b
return r
@_check
+ def __add__(self, other) -> "GMPMod":
+ return GMPMod((self.x + other.x) % self.n, self.n, ensure=False)
+
+ @_check
+ def __sub__(self, other) -> "GMPMod":
+ return GMPMod((self.x - other.x) % self.n, self.n, ensure=False)
+
+ def __neg__(self) -> "GMPMod":
+ return GMPMod(self.n - self.x, self.n, ensure=False)
+
+ @_check
+ def __mul__(self, other) -> "GMPMod":
+ return GMPMod((self.x * other.x) % self.n, self.n, ensure=False)
+
+ @_check
def __divmod__(self, divisor) -> Tuple["GMPMod", "GMPMod"]:
q, r = gmpy2.f_divmod(self.x, divisor.x)
- return GMPMod(q, self.n), GMPMod(r, self.n)
+ return GMPMod(q, self.n, ensure=False), GMPMod(r, self.n, ensure=False)
def __bytes__(self):
return int(self.x).to_bytes((self.n.bit_length() + 7) // 8, byteorder="big")
@@ -677,11 +700,11 @@ if has_gmp:
if type(n) not in (int, gmpy2.mpz):
raise TypeError
if n == 0:
- return GMPMod(1, self.n)
+ return GMPMod(gmpy2.mpz(1), self.n, ensure=False)
if n < 0:
return self.inverse() ** (-n)
if n == 1:
- return GMPMod(self.x, self.n)
- return GMPMod(gmpy2.powmod(self.x, gmpy2.mpz(n), self.n), self.n)
+ return GMPMod(self.x, self.n, ensure=False)
+ return GMPMod(gmpy2.powmod(self.x, gmpy2.mpz(n), self.n), self.n, ensure=False)
_mod_classes["gmp"] = GMPMod
diff --git a/pyecsca/ec/model.py b/pyecsca/ec/model.py
index c6305a6..bea5554 100644
--- a/pyecsca/ec/model.py
+++ b/pyecsca/ec/model.py
@@ -1,4 +1,4 @@
-"""This module provides curve model classes for the supported curve models."""
+"""Provides curve model classes for the supported curve models."""
from ast import parse, Expression, Module
from os.path import join
from typing import List, MutableMapping
@@ -63,9 +63,8 @@ class EFDCurveModel(CurveModel):
return parse(line.replace("^", "**"), mode=mode)
with resource_stream(__name__, file_path) as f:
- line = f.readline()
- while line:
- line = line.decode("ascii").rstrip()
+ for raw in f.readlines():
+ line = raw.decode("ascii").rstrip()
if line.startswith("name"):
cls.name = line[5:]
elif line.startswith("parameter"):
@@ -90,7 +89,6 @@ class EFDCurveModel(CurveModel):
cls.from_weierstrass.append(format_eq(line[16:]))
else:
cls.full_weierstrass.append(format_eq(line))
- line = f.readline()
def __read_coordinate_dir(self, cls, dir_path, name):
cls.coordinates[name] = EFDCoordinateModel(dir_path, name, self)
diff --git a/pyecsca/ec/mult.py b/pyecsca/ec/mult.py
index 47c36b1..53e593e 100644
--- a/pyecsca/ec/mult.py
+++ b/pyecsca/ec/mult.py
@@ -1,4 +1,4 @@
-"""This module provides several classes implementing different scalar multiplication algorithms."""
+"""Provides several classes implementing different scalar multiplication algorithms."""
from abc import ABC, abstractmethod
from copy import copy
from typing import Mapping, Tuple, Optional, MutableMapping, ClassVar, Set, Type
@@ -203,7 +203,7 @@ class LTRMultiplier(ScalarMultiplier):
self,
add: AdditionFormula,
dbl: DoublingFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
always: bool = False,
complete: bool = True,
short_circuit: bool = True,
@@ -254,7 +254,7 @@ class RTLMultiplier(ScalarMultiplier):
self,
add: AdditionFormula,
dbl: DoublingFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
always: bool = False,
short_circuit: bool = True,
):
@@ -300,7 +300,7 @@ class CoronMultiplier(ScalarMultiplier):
self,
add: AdditionFormula,
dbl: DoublingFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
short_circuit: bool = True,
):
super().__init__(short_circuit=short_circuit, add=add, dbl=dbl, scl=scl)
@@ -334,8 +334,8 @@ class LadderMultiplier(ScalarMultiplier):
def __init__(
self,
ladd: LadderFormula,
- dbl: DoublingFormula = None,
- scl: ScalingFormula = None,
+ dbl: Optional[DoublingFormula] = None,
+ scl: Optional[ScalingFormula] = None,
complete: bool = True,
short_circuit: bool = True,
):
@@ -381,7 +381,7 @@ class SimpleLadderMultiplier(ScalarMultiplier):
self,
add: AdditionFormula,
dbl: DoublingFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
complete: bool = True,
short_circuit: bool = True,
):
@@ -424,7 +424,7 @@ class DifferentialLadderMultiplier(ScalarMultiplier):
self,
dadd: DifferentialAdditionFormula,
dbl: DoublingFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
complete: bool = True,
short_circuit: bool = True,
):
@@ -469,7 +469,7 @@ class BinaryNAFMultiplier(ScalarMultiplier):
add: AdditionFormula,
dbl: DoublingFormula,
neg: NegationFormula,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
short_circuit: bool = True,
):
super().__init__(
@@ -517,7 +517,7 @@ class WindowNAFMultiplier(ScalarMultiplier):
dbl: DoublingFormula,
neg: NegationFormula,
width: int,
- scl: ScalingFormula = None,
+ scl: Optional[ScalingFormula] = None,
precompute_negation: bool = False,
short_circuit: bool = True,
):
diff --git a/pyecsca/ec/naf.py b/pyecsca/ec/naf.py
index 2595a4e..fd1fa47 100644
--- a/pyecsca/ec/naf.py
+++ b/pyecsca/ec/naf.py
@@ -1,4 +1,4 @@
-"""This module provides functions for computing the Non-Adjacent Form (NAF) of integers."""
+"""Provides functions for computing the Non-Adjacent Form (NAF) of integers."""
from public import public
from typing import List
diff --git a/pyecsca/ec/op.py b/pyecsca/ec/op.py
index 406bd1f..d656bc3 100644
--- a/pyecsca/ec/op.py
+++ b/pyecsca/ec/op.py
@@ -1,4 +1,4 @@
-"""This module provides a class for a code operation."""
+"""Provides a class for a code operation."""
from ast import (
Module,
walk,
diff --git a/pyecsca/ec/params.py b/pyecsca/ec/params.py
index 63cdf05..8344901 100644
--- a/pyecsca/ec/params.py
+++ b/pyecsca/ec/params.py
@@ -1,5 +1,5 @@
"""
-This module provides functions for obtaining domain parameters from the `std-curves <https://github.com/J08nY/std-curves>`_ repository.
+Provides functions for obtaining domain parameters from the `std-curves <https://github.com/J08nY/std-curves>`_ repository.
It also provides a domain parameter class and a class for a whole category of domain parameters.
"""
@@ -66,6 +66,9 @@ class DomainParameters:
and self.cofactor == other.cofactor
)
+ def __hash__(self):
+ return hash((self.curve, self.generator, self.order, self.cofactor))
+
def __get_name(self):
if self.name and self.category:
return f"{self.category}/{self.name}"
@@ -340,6 +343,7 @@ def get_params(
json_path = join("std", category, "curves.json")
with resource_stream(__name__, json_path) as f:
category_json = json.load(f)
+ curve = None
for curve in category_json["curves"]:
if curve["name"] == name:
break
diff --git a/pyecsca/ec/point.py b/pyecsca/ec/point.py
index b12b312..c7f99db 100644
--- a/pyecsca/ec/point.py
+++ b/pyecsca/ec/point.py
@@ -1,4 +1,4 @@
-"""This module provides a :py:class:`.Point` class and a special :py:class:`.InfinityPoint` class for the point at infinity."""
+"""Provides a :py:class:`.Point` class and a special :py:class:`.InfinityPoint` class for the point at infinity."""
from copy import copy
from typing import Mapping, TYPE_CHECKING
@@ -196,7 +196,7 @@ class Point:
return self.coords == other.coords
def __hash__(self):
- return hash((tuple(self.coords.keys()), tuple(self.coords.values()))) + 1
+ return hash((self.coordinate_model.name, tuple(self.coords.keys()), tuple(self.coords.values()))) + 13
def __str__(self):
args = ", ".join([f"{key}={val}" for key, val in self.coords.items()])
@@ -240,6 +240,9 @@ class InfinityPoint(Point):
else:
return self.coordinate_model == other.coordinate_model
+ def __hash__(self):
+ return hash(self.coordinate_model.name) + 13
+
def __str__(self):
return "Infinity"
diff --git a/pyecsca/ec/signature.py b/pyecsca/ec/signature.py
index 972af19..a65adbc 100644
--- a/pyecsca/ec/signature.py
+++ b/pyecsca/ec/signature.py
@@ -1,4 +1,4 @@
-"""This module provides an implementation of ECDSA (Elliptic Curve Digital Signature Algorithm)."""
+"""Provides an implementation of ECDSA (Elliptic Curve Digital Signature Algorithm)."""
import hashlib
from typing import Optional, Any
@@ -42,6 +42,9 @@ class SignatureResult:
return False
return self.r == other.r and self.s == other.s
+ def __hash__(self):
+ return hash((self.r, self.s)) + 11
+
def __str__(self):
return f"(r={self.r}, s={self.s})"
@@ -64,7 +67,7 @@ class ECDSAAction(Action):
self.msg = msg
def __repr__(self):
- return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg})"
+ return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg!r})"
@public
@@ -84,7 +87,7 @@ class ECDSASignAction(ECDSAAction):
self.privkey = privkey
def __repr__(self):
- return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg}, {self.privkey})"
+ return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg!r}, {self.privkey})"
@public
@@ -107,7 +110,7 @@ class ECDSAVerifyAction(ECDSAAction):
self.pubkey = pubkey
def __repr__(self):
- return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg}, {self.signature}, {self.pubkey})"
+ return f"{self.__class__.__name__}({self.params}, {self.hash_algo}, {self.msg!r}, {self.signature}, {self.pubkey})"
@public
diff --git a/pyecsca/ec/std b/pyecsca/ec/std
-Subproject fd08ca9f90f1b1ba22d5bb47110d0decc81b939
+Subproject cd41e851723dfebd76a978717c0d89569c49597
diff --git a/pyecsca/ec/transformations.py b/pyecsca/ec/transformations.py
index 9d3d650..894421f 100644
--- a/pyecsca/ec/transformations.py
+++ b/pyecsca/ec/transformations.py
@@ -1,4 +1,4 @@
-"""This module provides functions for transforming curves to different models."""
+"""Provides functions for transforming curves to different models."""
from public import public
from sympy import FF, symbols, Poly
diff --git a/pyecsca/misc/cfg.py b/pyecsca/misc/cfg.py
index 24b83f8..2affb27 100644
--- a/pyecsca/misc/cfg.py
+++ b/pyecsca/misc/cfg.py
@@ -1,10 +1,11 @@
"""
-This module provides functions for runtime configuration of the toolkit.
+Provides functions for runtime configuration of the toolkit.
This includes how errors are handled, or which :py:class:`~pyecsca.ec.mod.Mod` implementation is used.
"""
from copy import deepcopy
from contextvars import ContextVar, Token
+from typing import Optional
from public import public
@@ -179,6 +180,8 @@ class TemporaryConfig:
...
"""
+ token: Optional[Token]
+
def __init__(self):
self.token = None
self.new_config = deepcopy(getconfig())
@@ -188,4 +191,5 @@ class TemporaryConfig:
return self.new_config
def __exit__(self, t, v, tb):
- resetconfig(self.token)
+ if self.token:
+ resetconfig(self.token)
diff --git a/pyecsca/sca/re/rpa.py b/pyecsca/sca/re/rpa.py
index 4330fb4..51b9738 100644
--- a/pyecsca/sca/re/rpa.py
+++ b/pyecsca/sca/re/rpa.py
@@ -1,5 +1,5 @@
"""
-This module provides functionality inspired by the Refined-Power Analysis attack by Goubin.
+Provides functionality inspired by the Refined-Power Analysis attack by Goubin.
A Refined Power-Analysis Attack on Elliptic Curve Cryptosystems, Louis Goubin, PKC '03
`<https://dl.acm.org/doi/10.5555/648120.747060>`_
diff --git a/pyecsca/sca/scope/base.py b/pyecsca/sca/scope/base.py
index 76b1272..08befda 100644
--- a/pyecsca/sca/scope/base.py
+++ b/pyecsca/sca/scope/base.py
@@ -1,4 +1,4 @@
-"""This module provides an abstract base class for oscilloscopes."""
+"""Provides an abstract base class for oscilloscopes."""
from enum import Enum, auto
from typing import Tuple, Sequence, Optional
diff --git a/pyecsca/sca/scope/chipwhisperer.py b/pyecsca/sca/scope/chipwhisperer.py
index f1c8d94..3706c0e 100644
--- a/pyecsca/sca/scope/chipwhisperer.py
+++ b/pyecsca/sca/scope/chipwhisperer.py
@@ -1,4 +1,4 @@
-"""This module provides an oscilloscope class using the ChipWhisperer-Lite scope."""
+"""Provides an oscilloscope class using the ChipWhisperer-Lite scope."""
from typing import Optional, Tuple, Sequence, Set
import numpy as np
@@ -31,13 +31,13 @@ class ChipWhispererScope(Scope): # pragma: no cover
if pretrig != 0:
raise ValueError("ChipWhisperer does not support pretrig samples.")
self.scope.clock.clkgen_freq = frequency
- self.scope.samples = posttrig
- return self.scope.clock.freq_ctr, self.scope.samples
+ self.scope.adc.samples = posttrig
+ return self.scope.clock.clkgen_freq, self.scope.adc.samples
def setup_channel(
self, channel: str, coupling: str, range: float, offset: float, enable: bool
) -> None:
- pass
+ pass # Nothing to setup
def setup_trigger(
self,
@@ -56,7 +56,7 @@ class ChipWhispererScope(Scope): # pragma: no cover
self.scope.trigger.triggers = " OR ".join(self.triggers)
def setup_capture(self, channel: str, enable: bool) -> None:
- pass
+ pass # Nothing to setup
def arm(self) -> None:
self.scope.arm()
@@ -76,7 +76,7 @@ class ChipWhispererScope(Scope): # pragma: no cover
)
def stop(self) -> None:
- pass
+ pass # Nothing to do
def close(self) -> None:
self.scope.dis()
diff --git a/pyecsca/sca/scope/picoscope_alt.py b/pyecsca/sca/scope/picoscope_alt.py
index e8f17a5..900d796 100644
--- a/pyecsca/sca/scope/picoscope_alt.py
+++ b/pyecsca/sca/scope/picoscope_alt.py
@@ -1,4 +1,4 @@
-"""This module provides an oscilloscope class for the PicoScope branded oscilloscopes using the alternative `pico-python <https://github.com/colinoflynn/pico-python>`_ bindings."""
+"""Provides an oscilloscope class for the PicoScope branded oscilloscopes using the alternative `pico-python <https://github.com/colinoflynn/pico-python>`_ bindings."""
from time import time_ns, sleep
import numpy as np
from typing import Optional, Tuple, Sequence, Union
diff --git a/pyecsca/sca/scope/picoscope_sdk.py b/pyecsca/sca/scope/picoscope_sdk.py
index 3ee54cf..e881e49 100644
--- a/pyecsca/sca/scope/picoscope_sdk.py
+++ b/pyecsca/sca/scope/picoscope_sdk.py
@@ -1,4 +1,4 @@
-"""This module provides an oscilloscope class for PicoScope branded oscilloscopes using the official `picosdk-python-wrappers <https://github.com/picotech/picosdk-python-wrappers>`_."""
+"""Provides an oscilloscope class for PicoScope branded oscilloscopes using the official `picosdk-python-wrappers <https://github.com/picotech/picosdk-python-wrappers>`_."""
import ctypes
from math import log2, floor
from time import time_ns, sleep
@@ -14,6 +14,10 @@ try:
except CannotFindPicoSDKError as exc:
ps3000 = exc
try:
+ from picosdk.ps3000a import ps3000a
+except CannotFindPicoSDKError as exc:
+ ps3000a = exc
+try:
from picosdk.ps4000 import ps4000
except CannotFindPicoSDKError as exc:
ps4000 = exc
@@ -32,10 +36,10 @@ from ..trace import Trace
def adc2volt(
- adc: Union[np.ndarray, ctypes.c_int16],
- volt_range: float,
- adc_minmax: int,
- dtype=np.float32,
+ adc: Union[np.ndarray, ctypes.c_int16],
+ volt_range: float,
+ adc_minmax: int,
+ dtype=np.float32,
) -> Union[np.ndarray, float]: # pragma: no cover
"""
Convert raw adc values to volts.
@@ -54,7 +58,7 @@ def adc2volt(
def volt2adc(
- volt: Union[np.ndarray, float], volt_range: float, adc_minmax: int, dtype=np.float32
+ volt: Union[np.ndarray, float], volt_range: float, adc_minmax: int, dtype=np.float32
) -> Union[np.ndarray, ctypes.c_int16]: # pragma: no cover
"""
Convert volt values to raw adc values.
@@ -100,7 +104,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
self._variant = variant
def open(self) -> None:
- assert_pico_ok(self.__dispatch_call("OpenUnit", ctypes.byref(self.handle)))
+ assert_pico_ok(self._dispatch_call("OpenUnit", ctypes.byref(self.handle)))
@property
def channels(self):
@@ -109,28 +113,34 @@ class PicoScopeSdk(Scope): # pragma: no cover
def get_variant(self):
if self._variant is not None:
return self._variant
- info = (ctypes.c_int8 * 6)()
+ info = ctypes.create_string_buffer(6)
size = ctypes.c_int16()
assert_pico_ok(
- self.__dispatch_call(
- "GetUnitInfo", self.handle, ctypes.byref(info), 6, ctypes.byref(size), 3
+ self._dispatch_call(
+ "GetUnitInfo", self.handle, info, ctypes.c_int16(6), ctypes.byref(size), ctypes.c_uint(3)
)
)
- self._variant = "".join(chr(i) for i in info[: size.value])
+ self._variant = "".join(chr(i) for i in info[: size.value - 1]) # type: ignore
return self._variant
def setup_frequency(
- self, frequency: int, pretrig: int, posttrig: int
+ self, frequency: int, pretrig: int, posttrig: int
) -> Tuple[int, int]:
return self.set_frequency(frequency, pretrig, posttrig)
def set_channel(
- self, channel: str, enabled: bool, coupling: str, range: float, offset: float
+ self, channel: str, enabled: bool, coupling: str, range: float, offset: float
):
if offset != 0.0:
raise ValueError("Nonzero offset not supported.")
+ if channel not in self.CHANNELS:
+ raise ValueError(f"Channel {channel} not in available channels: {self.CHANNELS.keys()}")
+ if coupling not in self.COUPLING:
+ raise ValueError(f"Coupling {coupling} not in available couplings: {self.COUPLING.keys()}")
+ if range not in self.RANGES:
+ raise ValueError(f"Range {range} not in available ranges: {self.RANGES.keys()}")
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"SetChannel",
self.handle,
self.CHANNELS[channel],
@@ -142,20 +152,20 @@ class PicoScopeSdk(Scope): # pragma: no cover
self.ranges[channel] = range
def setup_channel(
- self, channel: str, coupling: str, range: float, offset: float, enable: bool
+ self, channel: str, coupling: str, range: float, offset: float, enable: bool
):
self.set_channel(channel, enable, coupling, range, offset)
def _set_freq(
- self,
- frequency: int,
- pretrig: int,
- posttrig: int,
- period_bound: float,
- timebase_bound: int,
- low_freq: int,
- high_freq: int,
- high_subtract: int,
+ self,
+ frequency: int,
+ pretrig: int,
+ posttrig: int,
+ period_bound: float,
+ timebase_bound: int,
+ low_freq: int,
+ high_freq: int,
+ high_subtract: int,
) -> Tuple[int, int]:
samples = pretrig + posttrig
period = 1 / frequency
@@ -166,16 +176,17 @@ class PicoScopeSdk(Scope): # pragma: no cover
tb = min(floor(log2(low_freq) - log2(frequency)), timebase_bound)
actual_frequency = low_freq // 2 ** tb
max_samples = ctypes.c_int32()
+ interval_nanoseconds = ctypes.c_int32()
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"GetTimebase",
self.handle,
tb,
samples,
- None,
+ ctypes.byref(interval_nanoseconds),
0,
ctypes.byref(max_samples),
- 0,
+ 0
)
)
if max_samples.value < samples:
@@ -190,32 +201,32 @@ class PicoScopeSdk(Scope): # pragma: no cover
return actual_frequency, samples
def set_frequency(
- self, frequency: int, pretrig: int, posttrig: int
+ self, frequency: int, pretrig: int, posttrig: int
) -> Tuple[int, int]:
raise NotImplementedError
def setup_trigger(
- self,
- channel: str,
- threshold: float,
- direction: str,
- delay: int,
- timeout: int,
- enable: bool,
+ self,
+ channel: str,
+ threshold: float,
+ direction: str,
+ delay: int,
+ timeout: int,
+ enable: bool,
):
self.set_trigger(direction, enable, threshold, channel, delay, timeout)
def set_trigger(
- self,
- type: str,
- enabled: bool,
- value: float,
- channel: str,
- delay: int,
- timeout: int,
+ self,
+ type: str,
+ enabled: bool,
+ value: float,
+ channel: str,
+ delay: int,
+ timeout: int,
):
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"SetSimpleTrigger",
self.handle,
enabled,
@@ -238,7 +249,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
del self.buffers[channel]
buffer = (ctypes.c_int16 * self.samples)()
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"SetDataBuffer",
self.handle,
self.CHANNELS[channel],
@@ -249,7 +260,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
self.buffers[channel] = buffer
else:
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"SetDataBuffer",
self.handle,
self.CHANNELS[channel],
@@ -263,7 +274,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
if self.samples is None or self.timebase is None:
raise ValueError
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"RunBlock",
self.handle,
self.pretrig,
@@ -286,21 +297,21 @@ class PicoScopeSdk(Scope): # pragma: no cover
while ready.value == check.value:
sleep(0.001)
assert_pico_ok(
- self.__dispatch_call("IsReady", self.handle, ctypes.byref(ready))
+ self._dispatch_call("IsReady", self.handle, ctypes.byref(ready))
)
if timeout is not None and (time_ns() - start) / 1e6 >= timeout:
return False
return True
def retrieve(
- self, channel: str, type: SampleType, dtype=np.float32
+ self, channel: str, type: SampleType, dtype=np.float32
) -> Optional[Trace]:
if self.samples is None:
raise ValueError
actual_samples = ctypes.c_int32(self.samples)
overflow = ctypes.c_int16()
assert_pico_ok(
- self.__dispatch_call(
+ self._dispatch_call(
"GetValues",
self.handle,
0,
@@ -329,12 +340,15 @@ class PicoScopeSdk(Scope): # pragma: no cover
)
def stop(self):
- assert_pico_ok(self.__dispatch_call("Stop"))
+ assert_pico_ok(self._dispatch_call("Stop"))
def close(self):
- assert_pico_ok(self.__dispatch_call("CloseUnit", self.handle))
+ assert_pico_ok(self._dispatch_call("CloseUnit", self.handle))
- def __dispatch_call(self, name, *args, **kwargs):
+ def _dispatch_call(self, name, *args, **kwargs):
+ """
+ A unit-generic call of a picoscope SDK method.
+ """
method = getattr(self.MODULE, self.PREFIX + name)
if method is None:
raise ValueError
@@ -344,7 +358,7 @@ class PicoScopeSdk(Scope): # pragma: no cover
if isinstance(ps3000, CannotFindPicoSDKError):
@public
- class PS3000Scope(PicoScopeSdk): # pragma: no cover
+ class PS3000Scope(PicoScopeSdk): # noqa, pragma: no cover
"""PicoScope 3000 series oscilloscope is not available (Install `libps3000`)."""
def __init__(self, variant: Optional[str] = None):
@@ -389,29 +403,152 @@ else: # pragma: no cover
COUPLING = {"AC": ps3000.PICO_COUPLING["AC"], "DC": ps3000.PICO_COUPLING["DC"]}
+ def open(self) -> None:
+ assert_pico_ok(self._dispatch_call("_open_unit")) # , ctypes.byref(self.handle)
+
+ def stop(self):
+ assert_pico_ok(self._dispatch_call("_stop"))
+
+ def close(self):
+ assert_pico_ok(self._dispatch_call("_close_unit", self.handle))
+
def get_variant(self):
if self._variant is not None:
return self._variant
- info = (ctypes.c_int8 * 6)()
+ info = ctypes.create_string_buffer(6)
size = ctypes.c_int16(6)
+ info_variant = ctypes.c_int16(3)
assert_pico_ok(
- self.__dispatch_call(
- "GetUnitInfo", self.handle, ctypes.byref(info), size, 3
+ self._dispatch_call(
+ "_get_unit_info", self.handle, info, size, info_variant
)
)
- self._variant = "".join(chr(i) for i in info[: size.value])
+ self._variant = "".join(chr(i) for i in info[: size.value - 1]) # type: ignore
return self._variant
def set_frequency(
- self, frequency: int, pretrig: int, posttrig: int
+ self, frequency: int, pretrig: int, posttrig: int
): # TODO: fix
raise NotImplementedError
+if isinstance(ps3000a, CannotFindPicoSDKError):
+
+ @public
+ class PS3000aScope(PicoScopeSdk): # noqa, pragma: no cover
+ """PicoScope 3000 series (A API) oscilloscope is not available (Install `libps3000a`)."""
+
+ def __init__(self, variant: Optional[str] = None):
+ super().__init__(variant)
+ raise ps3000a
+
+
+else: # pragma: no cover
+
+ @public
+ class PS3000aScope(PicoScopeSdk): # type: ignore
+ """PicoScope 3000 series oscilloscope (A API)."""
+
+ MODULE = ps3000a
+ PREFIX = "ps3000a"
+ CHANNELS = {
+ "A": ps3000a.PS3000A_CHANNEL["PS3000A_CHANNEL_A"],
+ "B": ps3000a.PS3000A_CHANNEL["PS3000A_CHANNEL_B"],
+ "C": ps3000a.PS3000A_CHANNEL["PS3000A_CHANNEL_C"],
+ "D": ps3000a.PS3000A_CHANNEL["PS3000A_CHANNEL_D"],
+ }
+
+ RANGES = {
+ 0.01: ps3000a.PS3000A_RANGE["PS3000A_10MV"],
+ 0.02: ps3000a.PS3000A_RANGE["PS3000A_20MV"],
+ 0.05: ps3000a.PS3000A_RANGE["PS3000A_50MV"],
+ 0.10: ps3000a.PS3000A_RANGE["PS3000A_100MV"],
+ 0.20: ps3000a.PS3000A_RANGE["PS3000A_200MV"],
+ 0.50: ps3000a.PS3000A_RANGE["PS3000A_500MV"],
+ 1.00: ps3000a.PS3000A_RANGE["PS3000A_1V"],
+ 2.00: ps3000a.PS3000A_RANGE["PS3000A_2V"],
+ 5.00: ps3000a.PS3000A_RANGE["PS3000A_5V"],
+ 10.0: ps3000a.PS3000A_RANGE["PS3000A_10V"],
+ 20.0: ps3000a.PS3000A_RANGE["PS3000A_20V"],
+ 50.0: ps3000a.PS3000A_RANGE["PS3000A_50V"]
+ }
+
+ MAX_ADC_VALUE = 32767
+ MIN_ADC_VALUE = -32767
+
+ COUPLING = {"AC": ps3000a.PICO_COUPLING["AC"], "DC": ps3000a.PICO_COUPLING["DC"]}
+
+ def open(self) -> None:
+ assert_pico_ok(ps3000a.ps3000aOpenUnit(ctypes.byref(self.handle), None))
+
+ def set_channel(
+ self,
+ channel: str,
+ enabled: bool,
+ coupling: str,
+ range: float,
+ offset: float,
+ ):
+ if channel not in self.CHANNELS:
+ raise ValueError(f"Channel {channel} not in available channels: {self.CHANNELS.keys()}")
+ if coupling not in self.COUPLING:
+ raise ValueError(f"Coupling {coupling} not in available couplings: {self.COUPLING.keys()}")
+ if range not in self.RANGES:
+ raise ValueError(f"Range {range} not in available ranges: {self.RANGES.keys()}")
+ assert_pico_ok(
+ ps3000a.ps3000aSetChannel(
+ self.handle,
+ self.CHANNELS[channel],
+ enabled,
+ self.COUPLING[coupling],
+ self.RANGES[range],
+ offset
+ )
+ )
+ self.ranges[channel] = range
+
+ def set_buffer(self, channel: str, enable: bool):
+ if self.samples is None:
+ raise ValueError
+ if enable:
+ if channel in self.buffers:
+ del self.buffers[channel]
+ buffer = (ctypes.c_int16 * self.samples)()
+ assert_pico_ok(
+ ps3000a.ps3000aSetDataBuffer(
+ self.handle,
+ self.CHANNELS[channel],
+ ctypes.byref(buffer),
+ self.samples,
+ 0,
+ ps3000a.PS3000A_RATIO_MODE["PS3000A_RATIO_MODE_NONE"]
+ )
+ )
+ self.buffers[channel] = buffer
+ else:
+ assert_pico_ok(
+ ps3000a.ps3000aSetDataBuffer(
+ self.handle, self.CHANNELS[channel], None, self.samples, 0,
+ ps3000a.PS3000A_RATIO_MODE["PS3000A_RATIO_MODE_NONE"]
+ )
+ )
+ del self.buffers[channel]
+
+ def set_frequency(self, frequency: int, pretrig: int, posttrig: int):
+ variant = self.get_variant()
+ if variant in ("3000A", "3000B"):
+ # This only holds for the 2-channel versions
+ # 4-channel versions have the settings from branch "D".
+ return self._set_freq(frequency, pretrig, posttrig, 8e-9, 2, 500_000_000, 62_500_000, 2)
+ elif variant == "3000":
+ return self._set_freq(frequency, pretrig, posttrig, 4e-9, 1, 500_000_000, 125_000_000, 1)
+ elif variant.endswith("D"):
+ return self._set_freq(frequency, pretrig, posttrig, 4e-9, 2, 1_000_000_000, 125_000_000, 2)
+ # TODO: Needs more per-device settings to be generic.
if isinstance(ps4000, CannotFindPicoSDKError):
@public
- class PS4000Scope(PicoScopeSdk): # pragma: no cover
+ class PS4000Scope(PicoScopeSdk): # noqa, pragma: no cover
"""PicoScope 4000 series oscilloscope is not available (Install `libps4000`)."""
def __init__(self, variant: Optional[str] = None):
@@ -469,12 +606,13 @@ else: # pragma: no cover
return self._set_freq(
frequency, pretrig, posttrig, 0, 0, 0, 10_000_000, -1
)
-
+ else:
+ raise ValueError(f"Unknown variant: {variant}")
if isinstance(ps5000, CannotFindPicoSDKError):
@public
- class PS5000Scope(PicoScopeSdk): # pragma: no cover
+ class PS5000Scope(PicoScopeSdk): # noqa, pragma: no cover
"""PicoScope 5000 series oscilloscope is not available (Install `libps5000`)."""
def __init__(self, variant: Optional[str] = None):
@@ -498,18 +636,18 @@ else: # pragma: no cover
}
RANGES = {
- 0.01: 0,
- 0.02: 1,
- 0.05: 2,
- 0.10: 3,
- 0.20: 4,
- 0.50: 5,
- 1.00: 6,
- 2.00: 7,
- 5.00: 8,
- 10.0: 9,
- 20.0: 10,
- 50.0: 11,
+ 0.01: ps5000.PS5000_RANGE["PS5000_10MV"],
+ 0.02: ps5000.PS5000_RANGE["PS5000_20MV"],
+ 0.05: ps5000.PS5000_RANGE["PS5000_50MV"],
+ 0.10: ps5000.PS5000_RANGE["PS5000_100MV"],
+ 0.20: ps5000.PS5000_RANGE["PS5000_200MV"],
+ 0.50: ps5000.PS5000_RANGE["PS5000_500MV"],
+ 1.00: ps5000.PS5000_RANGE["PS5000_1V"],
+ 2.00: ps5000.PS5000_RANGE["PS5000_2V"],
+ 5.00: ps5000.PS5000_RANGE["PS5000_5V"],
+ 10.0: ps5000.PS5000_RANGE["PS5000_10V"],
+ 20.0: ps5000.PS5000_RANGE["PS5000_20V"],
+ 50.0: ps5000.PS5000_RANGE["PS5000_50V"],
}
MAX_ADC_VALUE = 32512
@@ -522,11 +660,10 @@ else: # pragma: no cover
frequency, pretrig, posttrig, 4e-9, 2, 1_000_000_000, 125_000_000, 2
)
-
if isinstance(ps6000, CannotFindPicoSDKError):
@public
- class PS6000Scope(PicoScopeSdk): # pragma: no cover
+ class PS6000Scope(PicoScopeSdk): # noqa, pragma: no cover
"""PicoScope 6000 series oscilloscope is not available (Install `libps6000`)."""
def __init__(self, variant: Optional[str] = None):
@@ -577,13 +714,19 @@ else: # pragma: no cover
assert_pico_ok(ps6000.ps6000OpenUnit(ctypes.byref(self.handle), None))
def set_channel(
- self,
- channel: str,
- enabled: bool,
- coupling: str,
- range: float,
- offset: float,
+ self,
+ channel: str,
+ enabled: bool,
+ coupling: str,
+ range: float,
+ offset: float,
):
+ if channel not in self.CHANNELS:
+ raise ValueError(f"Channel {channel} not in available channels: {self.CHANNELS.keys()}")
+ if coupling not in self.COUPLING:
+ raise ValueError(f"Coupling {coupling} not in available couplings: {self.COUPLING.keys()}")
+ if range not in self.RANGES:
+ raise ValueError(f"Range {range} not in available ranges: {self.RANGES.keys()}")
assert_pico_ok(
ps6000.ps6000SetChannel(
self.handle,
@@ -595,6 +738,7 @@ else: # pragma: no cover
ps6000.PS6000_BANDWIDTH_LIMITER["PS6000_BW_FULL"],
)
)
+ self.ranges[channel] = range
def set_buffer(self, channel: str, enable: bool):
if self.samples is None:
diff --git a/pyecsca/sca/target/ISO7816.py b/pyecsca/sca/target/ISO7816.py
index cca09d5..63326ca 100644
--- a/pyecsca/sca/target/ISO7816.py
+++ b/pyecsca/sca/target/ISO7816.py
@@ -1,6 +1,7 @@
-"""This module provides classes for working with ISO7816-4 APDUs and an abstract base class for an ISO7816-4 based target."""
+"""Provides classes for working with ISO7816-4 APDUs and an abstract base class for an ISO7816-4 based target."""
from abc import abstractmethod, ABC
from dataclasses import dataclass
+from enum import IntEnum
from typing import Optional
from public import public
@@ -9,6 +10,19 @@ from .base import Target
@public
+class CardConnectionException(Exception):
+ """Card could not be connected."""
+ pass
+
+
+@public
+class CardProtocol(IntEnum):
+ """Card protocol to use/negotiate."""
+ T0 = 0
+ T1 = 1
+
+
+@public
@dataclass
class CommandAPDU: # pragma: no cover
"""Command APDU that can be sent to an ISO7816-4 target."""
@@ -45,35 +59,35 @@ class CommandAPDU: # pragma: no cover
if len(self.data) <= 255:
# Case 3s
return (
- bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
- + self.data
+ bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
+ + self.data
)
else:
# Case 3e
return (
- bytes([self.cls, self.ins, self.p1, self.p2, 0])
- + len(self.data).to_bytes(2, "big")
- + self.data
+ bytes([self.cls, self.ins, self.p1, self.p2, 0])
+ + len(self.data).to_bytes(2, "big")
+ + self.data
)
else:
if len(self.data) <= 255 and self.ne <= 256:
# Case 4s
return (
- bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
- + self.data
- + bytes([self.ne if self.ne != 256 else 0])
+ bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
+ + self.data
+ + bytes([self.ne if self.ne != 256 else 0])
)
else:
# Case 4e
return (
- bytes([self.cls, self.ins, self.p1, self.p2, 0])
- + len(self.data).to_bytes(2, "big")
- + self.data
- + (
- self.ne.to_bytes(2, "big")
- if self.ne != 65536
- else bytes([0, 0])
- )
+ bytes([self.cls, self.ins, self.p1, self.p2, 0])
+ + len(self.data).to_bytes(2, "big")
+ + self.data
+ + (
+ self.ne.to_bytes(2, "big")
+ if self.ne != 65536
+ else bytes([0, 0])
+ )
)
@@ -90,6 +104,15 @@ class ResponseAPDU:
class ISO7816Target(Target, ABC):
"""ISO7816-4 target."""
+ @abstractmethod
+ def connect(self, protocol: Optional[CardProtocol] = None):
+ """
+ Connect to the card.
+
+ :param protocol: CardProtocol to use.
+ """
+ raise NotImplementedError
+
@property
@abstractmethod
def atr(self) -> bytes:
diff --git a/pyecsca/sca/target/PCSC.py b/pyecsca/sca/target/PCSC.py
index ca77d12..ace59cc 100644
--- a/pyecsca/sca/target/PCSC.py
+++ b/pyecsca/sca/target/PCSC.py
@@ -1,5 +1,5 @@
-"""This module provides a smartcard target communicating via PC/SC (Personal Computer/Smart Card)."""
-from typing import Union
+"""Provides a smartcard target communicating via PC/SC (Personal Computer/Smart Card)."""
+from typing import Union, Optional
from public import public
from smartcard.CardConnection import CardConnection
@@ -7,7 +7,7 @@ from smartcard.System import readers
from smartcard.pcsc.PCSCCardConnection import PCSCCardConnection
from smartcard.pcsc.PCSCReader import PCSCReader
-from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816
+from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816, CardProtocol, CardConnectionException
@public
@@ -27,8 +27,16 @@ class PCSCTarget(ISO7816Target): # pragma: no cover
self.reader = reader
self.connection: PCSCCardConnection = self.reader.createConnection()
- def connect(self):
- self.connection.connect(CardConnection.T0_protocol | CardConnection.T1_protocol)
+ def connect(self, protocol: Optional[CardProtocol] = None):
+ proto = CardConnection.T0_protocol | CardConnection.T1_protocol
+ if protocol == CardProtocol.T0:
+ proto = CardConnection.T0_protocol
+ elif protocol == CardProtocol.T1:
+ proto = CardConnection.T1_protocol
+ try:
+ self.connection.connect(proto)
+ except: # noqa
+ raise CardConnectionException()
@property
def atr(self) -> bytes:
diff --git a/pyecsca/sca/target/__init__.py b/pyecsca/sca/target/__init__.py
index f1555dd..bc9a54d 100644
--- a/pyecsca/sca/target/__init__.py
+++ b/pyecsca/sca/target/__init__.py
@@ -7,8 +7,9 @@ from .simpleserial import *
from .binary import *
from .flash import *
-has_chipwhisperer = False
-has_pyscard = False
+has_chipwhisperer: bool = False
+has_pyscard: bool = False
+has_leia: bool = False
try:
import chipwhisperer
@@ -24,9 +25,20 @@ try:
except ImportError: # pragma: no cover
pass
+try:
+ import smartleia
+
+ has_leia = True
+except ImportError: # pragma: no cover
+ pass
+
+from .ectester import ECTesterTarget # noqa
+
if has_pyscard:
from .PCSC import *
- from .ectester import ECTesterTarget
+
+if has_leia:
+ from .leia import *
if has_chipwhisperer:
from .chipwhisperer import *
diff --git a/pyecsca/sca/target/base.py b/pyecsca/sca/target/base.py
index 0c24f8d..53106d5 100644
--- a/pyecsca/sca/target/base.py
+++ b/pyecsca/sca/target/base.py
@@ -1,4 +1,4 @@
-"""This module provides an abstract base class for targets."""
+"""Provides an abstract base class for targets."""
from abc import ABC, abstractmethod
from public import public
diff --git a/pyecsca/sca/target/binary.py b/pyecsca/sca/target/binary.py
index 291f8bc..310b9e2 100644
--- a/pyecsca/sca/target/binary.py
+++ b/pyecsca/sca/target/binary.py
@@ -1,4 +1,4 @@
-"""This module provides a binary target class which represents a target that is a runnable binary on the host."""
+"""Provides a binary target class which represents a target that is a runnable binary on the host."""
import subprocess
from subprocess import Popen
from typing import Optional, Union, List
@@ -62,7 +62,9 @@ class BinaryTarget(SerialTarget):
def disconnect(self):
if self.process is None:
return
- self.process.stdin.close()
- self.process.stdout.close()
+ if self.process.stdin is not None:
+ self.process.stdin.close()
+ if self.process.stdout is not None:
+ self.process.stdout.close()
self.process.terminate()
self.process.wait()
diff --git a/pyecsca/sca/target/chipwhisperer.py b/pyecsca/sca/target/chipwhisperer.py
index 47e3ab0..49daef3 100644
--- a/pyecsca/sca/target/chipwhisperer.py
+++ b/pyecsca/sca/target/chipwhisperer.py
@@ -1,5 +1,5 @@
"""
-This module provides a `ChipWhisperer <https://github.com/newaetech/chipwhisperer/>`_ target class.
+Provides a `ChipWhisperer <https://github.com/newaetech/chipwhisperer/>`_ target class.
ChipWhisperer is a side-channel analysis tool and framework. A ChipWhisperer target is one
that uses the ChipWhisperer's SimpleSerial communication protocol and is communicated with
diff --git a/pyecsca/sca/target/ectester.py b/pyecsca/sca/target/ectester.py
index 4057e61..e360224 100644
--- a/pyecsca/sca/target/ectester.py
+++ b/pyecsca/sca/target/ectester.py
@@ -1,4 +1,4 @@
-"""This module provides an `ECTester <https://github.com/crocs-muni/ECTester/>`_ target class."""
+"""Provides an `ECTester <https://github.com/crocs-muni/ECTester/>`_ target class."""
from abc import ABC
from binascii import hexlify
from enum import IntEnum, IntFlag
@@ -8,11 +8,9 @@ from operator import or_
from typing import Optional, Mapping, List, Union
from public import public
-from smartcard.CardConnection import CardConnection
-from smartcard.Exceptions import CardConnectionException
-from .ISO7816 import CommandAPDU, ResponseAPDU, ISO7816
-from .PCSC import PCSCTarget
+from .ISO7816 import CommandAPDU, ResponseAPDU, ISO7816, ISO7816Target, CardProtocol, CardConnectionException
+from . import has_leia, has_pyscard
from ...ec.model import ShortWeierstrassModel
from ...ec.params import DomainParameters
from ...ec.point import Point
@@ -247,7 +245,7 @@ class Response(ABC): # pragma: no cover
offset = 0
for i in range(num_sw):
if len(resp.data) >= offset + 2:
- self.sws[i] = int.from_bytes(resp.data[offset : offset + 2], "big")
+ self.sws[i] = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
if self.sws[i] != ISO7816.SW_NO_ERROR:
self.success = False
@@ -264,13 +262,13 @@ class Response(ABC): # pragma: no cover
self.success = False
self.error = True
break
- param_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ param_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
if len(resp.data) < offset + param_len:
self.success = False
self.error = True
break
- self.params[i] = resp.data[offset : offset + param_len]
+ self.params[i] = resp.data[offset: offset + param_len]
offset += param_len
def __repr__(self):
@@ -342,11 +340,11 @@ class ExportResponse(Response): # pragma: no cover
parameters: ParameterEnum
def __init__(
- self,
- resp: ResponseAPDU,
- keypair: KeypairEnum,
- key: KeyEnum,
- params: ParameterEnum,
+ self,
+ resp: ResponseAPDU,
+ keypair: KeypairEnum,
+ key: KeyEnum,
+ params: ParameterEnum,
):
self.keypair = keypair
self.key = key
@@ -472,31 +470,31 @@ class InfoResponse(Response): # pragma: no cover
super().__init__(resp, 1, 0)
offset = 2
- version_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ version_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
- self.version = resp.data[offset : offset + version_len].decode()
+ self.version = resp.data[offset: offset + version_len].decode()
offset += version_len
self.base = AppletBaseEnum(
- int.from_bytes(resp.data[offset : offset + 2], "big")
+ int.from_bytes(resp.data[offset: offset + 2], "big")
)
offset += 2
- system_version = int.from_bytes(resp.data[offset : offset + 2], "big")
+ system_version = int.from_bytes(resp.data[offset: offset + 2], "big")
system_major = system_version >> 8
system_minor = system_version & 0xFF
minor_size = 1 if system_minor == 0 else ceil(log(system_minor, 10))
self.system_version = system_major + system_minor / (minor_size * 10)
offset += 2
self.object_deletion_supported = (
- int.from_bytes(resp.data[offset : offset + 2], "big") == 1
+ int.from_bytes(resp.data[offset: offset + 2], "big") == 1
)
offset += 2
- self.buf_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ self.buf_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
- self.ram1_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ self.ram1_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
- self.ram2_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ self.ram2_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
- self.apdu_len = int.from_bytes(resp.data[offset : offset + 2], "big")
+ self.apdu_len = int.from_bytes(resp.data[offset: offset + 2], "big")
offset += 2
def __repr__(self):
@@ -508,7 +506,7 @@ class InfoResponse(Response): # pragma: no cover
@public
-class ECTesterTarget(PCSCTarget): # pragma: no cover
+class ECTesterTarget(ISO7816Target, ABC): # pragma: no cover
"""Smartcard target which communicates with the `ECTester <https://github.com/crocs-muni/ECTester>`_ sapplet on smartcards of the JavaCard platform using PCSC."""
CLA_ECTESTER = 0xB0
@@ -516,18 +514,19 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
AID_CURRENT_VERSION = bytes([0x30, 0x33, 0x33]) # Version v0.3.3
AID_SUFFIX_221 = bytes([0x62])
AID_SUFFIX_222 = bytes([0x78])
+ AID_SUFFIX_304 = bytes([0x94])
chunking: bool
- def connect(self):
+ def connect(self, protocol: Optional[CardProtocol] = None):
self.chunking = False
try:
- self.connection.connect(CardConnection.T1_protocol)
+ super().connect(CardProtocol.T1)
except CardConnectionException:
- self.connection.connect(CardConnection.T0_protocol)
+ super().connect(CardProtocol.T0)
self.chunking = True
- def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU:
+ def send(self, apdu: CommandAPDU) -> ResponseAPDU:
if self.chunking:
data = bytes(apdu)
num_chunks = (len(data) + 254) // 255
@@ -536,23 +535,23 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
chunk_length = 255
if chunk_start + chunk_length > len(data):
chunk_length = len(data) - chunk_start
- chunk = data[chunk_start : chunk_start + chunk_length]
+ chunk = data[chunk_start: chunk_start + chunk_length]
chunk_apdu = CommandAPDU(
self.CLA_ECTESTER, InstructionEnum.INS_BUFFER, 0, 0, chunk
)
- resp = super().send_apdu(chunk_apdu)
+ resp = self.send_apdu(chunk_apdu)
if resp.sw != 0x9000:
raise ChunkingException()
apdu = CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_PERFORM, 0, 0)
- resp = super().send_apdu(apdu)
+ resp = self.send_apdu(apdu)
if resp.sw & 0xFF00 == ISO7816.SW_BYTES_REMAINING_00:
- resp = super().send_apdu(
+ resp = self.send_apdu(
CommandAPDU(0x00, 0xC0, 0x00, 0x00, None, resp.sw & 0xFF)
)
return resp
def select_applet(
- self, latest_version: bytes = AID_CURRENT_VERSION, count_back: int = 10
+ self, latest_version: bytes = AID_CURRENT_VERSION, count_back: int = 10
) -> bool:
"""
Select the *ECTester* applet, with a specified version or older.
@@ -563,13 +562,10 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
"""
version_bytes = bytearray(latest_version)
for _ in range(count_back):
- aid_222 = self.AID_PREFIX + version_bytes + self.AID_SUFFIX_222
- if self.select(aid_222):
- break
- else:
- aid_221 = self.AID_PREFIX + version_bytes + self.AID_SUFFIX_221
- if self.select(aid_221):
- break
+ for aid_suffix in (self.AID_SUFFIX_304, self.AID_SUFFIX_222, self.AID_SUFFIX_221):
+ aid = self.AID_PREFIX + version_bytes + aid_suffix
+ if self.select(aid):
+ return True
# Count down by versions
if version_bytes[2] == 0x30:
if version_bytes[1] == 0x30:
@@ -590,7 +586,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
@staticmethod
def encode_parameters(
- params: ParameterEnum, obj: Union[DomainParameters, Point, int]
+ params: ParameterEnum, obj: Union[DomainParameters, Point, int]
) -> Mapping[ParameterEnum, bytes]:
"""Encode values from `obj` into the byte parameters that the **ECTester** applet expects."""
@@ -603,7 +599,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
result = {}
if isinstance(obj, DomainParameters) and isinstance(
- obj.curve.model, ShortWeierstrassModel
+ obj.curve.model, ShortWeierstrassModel
):
for param in params & ParameterEnum.DOMAIN_FP:
if param == ParameterEnum.G:
@@ -623,7 +619,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
result[param] = convert_point(obj)
elif isinstance(obj, int):
for param in params & (
- (ParameterEnum.DOMAIN_FP ^ ParameterEnum.G) | ParameterEnum.S
+ (ParameterEnum.DOMAIN_FP ^ ParameterEnum.G) | ParameterEnum.S
):
result[param] = convert_int(obj)
else:
@@ -637,7 +633,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param ka_type: Which KeyAgreement type to allocate.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ALLOCATE_KA,
@@ -655,7 +651,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param sig_type: Which Signature type to allocate.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ALLOCATE_SIG,
@@ -667,11 +663,11 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return AllocateSigResponse(resp)
def allocate(
- self,
- keypair: KeypairEnum,
- builder: KeyBuildEnum,
- key_length: int,
- key_class: KeyClassEnum,
+ self,
+ keypair: KeypairEnum,
+ builder: KeyBuildEnum,
+ key_length: int,
+ key_class: KeyClassEnum,
) -> AllocateResponse:
"""
Send the Allocate KeyPair command.
@@ -682,7 +678,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param key_class: Type of the allocated keypair.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ALLOCATE,
@@ -700,17 +696,17 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param keypair: Which keypair to clear.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_CLEAR, keypair, 0, None)
)
return ClearResponse(resp, keypair)
def set(
- self,
- keypair: KeypairEnum,
- curve: CurveEnum,
- params: ParameterEnum,
- values: Optional[Mapping[ParameterEnum, bytes]] = None,
+ self,
+ keypair: KeypairEnum,
+ curve: CurveEnum,
+ params: ParameterEnum,
+ values: Optional[Mapping[ParameterEnum, bytes]] = None,
) -> SetResponse:
"""
Send the Set command.
@@ -732,7 +728,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
if e == ParameterEnum.S:
break
e <<= 1
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER, InstructionEnum.INS_SET, keypair, curve, payload
)
@@ -740,7 +736,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
elif values is not None:
raise ValueError("Values should be specified only if curve is external.")
else:
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_SET,
@@ -752,11 +748,11 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return SetResponse(resp, keypair)
def transform(
- self,
- keypair: KeypairEnum,
- key: KeyEnum,
- params: ParameterEnum,
- transformation: TransformationEnum,
+ self,
+ keypair: KeypairEnum,
+ key: KeyEnum,
+ params: ParameterEnum,
+ transformation: TransformationEnum,
) -> TransformResponse:
"""
Send the Transform command.
@@ -767,7 +763,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param transformation: What transformation to apply.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_TRANSFORM,
@@ -785,7 +781,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param keypair: Which keypair to generate.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER, InstructionEnum.INS_GENERATE, keypair, 0, None
)
@@ -793,7 +789,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return GenerateResponse(resp, keypair)
def export(
- self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum
+ self, keypair: KeypairEnum, key: KeyEnum, params: ParameterEnum
) -> ExportResponse:
"""
Send the Export command.
@@ -803,7 +799,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param params: Which parameters to export.
:return: The response, containing the exported parameters.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_EXPORT,
@@ -815,12 +811,12 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return ExportResponse(resp, keypair, key, params)
def ecdh(
- self,
- pubkey: KeypairEnum,
- privkey: KeypairEnum,
- export: bool,
- transformation: TransformationEnum,
- ka_type: KeyAgreementEnum,
+ self,
+ pubkey: KeypairEnum,
+ privkey: KeypairEnum,
+ export: bool,
+ transformation: TransformationEnum,
+ ka_type: KeyAgreementEnum,
) -> ECDHResponse:
"""
Send the ECDH command.
@@ -832,7 +828,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param ka_type: The key-agreement type to use.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ECDH,
@@ -846,12 +842,12 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return ECDHResponse(resp, export)
def ecdh_direct(
- self,
- privkey: KeypairEnum,
- export: bool,
- transformation: TransformationEnum,
- ka_type: KeyAgreementEnum,
- pubkey: bytes,
+ self,
+ privkey: KeypairEnum,
+ export: bool,
+ transformation: TransformationEnum,
+ ka_type: KeyAgreementEnum,
+ pubkey: bytes,
) -> ECDHResponse:
"""
Send the ECDH direct command.
@@ -863,7 +859,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param pubkey: The raw bytes that will be used as a pubkey in the key-agreement.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ECDH_DIRECT,
@@ -878,7 +874,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return ECDHResponse(resp, export)
def ecdsa(
- self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes
+ self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes
) -> ECDSAResponse:
"""
Send the ECDSA command.
@@ -889,7 +885,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param data: The data to sign and verify.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ECDSA,
@@ -901,7 +897,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return ECDSAResponse(resp, export)
def ecdsa_sign(
- self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes
+ self, keypair: KeypairEnum, export: bool, sig_type: SignatureEnum, data: bytes
) -> ECDSAResponse:
"""
Send the ECDSA sign command.
@@ -912,7 +908,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param data: The data to sign.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ECDSA_SIGN,
@@ -924,7 +920,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
return ECDSAResponse(resp, export)
def ecdsa_verify(
- self, keypair: KeypairEnum, sig_type: SignatureEnum, sig: bytes, data: bytes
+ self, keypair: KeypairEnum, sig_type: SignatureEnum, sig: bytes, data: bytes
) -> ECDSAResponse:
"""
Send the ECDSA verify command.
@@ -935,7 +931,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:param data: The data.
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_ECDSA_VERIFY,
@@ -952,7 +948,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_CLEANUP, 0, 0, None)
)
return CleanupResponse(resp)
@@ -963,7 +959,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(self.CLA_ECTESTER, InstructionEnum.INS_GET_INFO, 0, 0, None)
)
return InfoResponse(resp)
@@ -974,7 +970,7 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
:return: The response.
"""
- resp = self.send_apdu(
+ resp = self.send(
CommandAPDU(
self.CLA_ECTESTER,
InstructionEnum.INS_SET_DRY_RUN_MODE,
@@ -984,3 +980,20 @@ class ECTesterTarget(PCSCTarget): # pragma: no cover
)
)
return RunModeResponse(resp)
+
+
+if has_pyscard:
+ from .PCSC import PCSCTarget
+
+ @public
+ class ECTesterTargetPCSC(ECTesterTarget, PCSCTarget):
+ """An ECTester-applet-based target that is connected via a PCSC-compatible reader."""
+ pass
+
+if has_leia:
+ from .leia import LEIATarget
+
+ @public
+ class ECTesterTargetLEIA(ECTesterTarget, LEIATarget):
+ """An ECTester-applet-based target that is connected via the LEIA board."""
+ pass
diff --git a/pyecsca/sca/target/flash.py b/pyecsca/sca/target/flash.py
index b644c27..95c078d 100644
--- a/pyecsca/sca/target/flash.py
+++ b/pyecsca/sca/target/flash.py
@@ -1,4 +1,4 @@
-"""This module provides a mix-in class of a flashable target (e.g. one where the code gets flashed to it before running)."""
+"""Provides a mix-in class of a flashable target (e.g. one where the code gets flashed to it before running)."""
from public import public
from abc import ABC, abstractmethod
diff --git a/pyecsca/sca/target/leia.py b/pyecsca/sca/target/leia.py
new file mode 100644
index 0000000..f4d5643
--- /dev/null
+++ b/pyecsca/sca/target/leia.py
@@ -0,0 +1,45 @@
+"""Provides a smartcard target communicating via the LEIA board in solo mode."""
+from typing import Optional
+
+from smartleia import LEIA, create_APDU_from_bytes, T
+
+from .ISO7816 import ISO7816Target, CommandAPDU, ResponseAPDU, ISO7816, CardProtocol, CardConnectionException
+
+
+class LEIATarget(ISO7816Target): # pragma: no cover
+ """Smartcard target communicating via LEIA in solo mode."""
+
+ def __init__(self, leia: LEIA):
+ self.leia = leia
+
+ @property
+ def atr(self) -> bytes:
+ return self.leia.get_ATR().normalized()
+
+ @property
+ def card_present(self) -> bool:
+ return self.leia.is_card_inserted()
+
+ def select(self, aid: bytes) -> bool:
+ apdu = CommandAPDU(0x00, 0xA4, 0x04, 0x00, aid)
+ resp = self.send_apdu(apdu)
+ return resp.sw == ISO7816.SW_NO_ERROR
+
+ def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU:
+ leia_apdu = create_APDU_from_bytes(bytes(apdu))
+ resp = self.leia.send_APDU(leia_apdu)
+ return ResponseAPDU(resp.data, resp.sw1 << 8 | resp.sw2)
+
+ def connect(self, protocol: Optional[CardProtocol] = None):
+ proto = T.AUTO
+ if protocol == CardProtocol.T0:
+ proto = T.T0
+ elif protocol == CardProtocol.T1:
+ proto = T.T1
+ try:
+ self.leia.configure_smartcard(protocol_to_use=proto)
+ except: # noqa
+ raise CardConnectionException()
+
+ def disconnect(self):
+ pass
diff --git a/pyecsca/sca/target/serial.py b/pyecsca/sca/target/serial.py
index fba5d69..3b81851 100644
--- a/pyecsca/sca/target/serial.py
+++ b/pyecsca/sca/target/serial.py
@@ -1,4 +1,4 @@
-"""This module provides an abstract serial target, that communicates by writing and reading a stream of bytes."""
+"""Provides an abstract serial target, that communicates by writing and reading a stream of bytes."""
from abc import abstractmethod
from public import public
diff --git a/pyecsca/sca/target/simpleserial.py b/pyecsca/sca/target/simpleserial.py
index 882ac93..aeb5c5f 100644
--- a/pyecsca/sca/target/simpleserial.py
+++ b/pyecsca/sca/target/simpleserial.py
@@ -1,4 +1,5 @@
-"""This module provides an abstract target class communicating using the `ChipWhisperer's <https://github.com/newaetech/chipwhisperer/>`_ SimpleSerial protocol."""
+"""Provides an abstract target class communicating using the `ChipWhisperer's <https://github.com/newaetech/chipwhisperer/>`_ SimpleSerial protocol."""
+from abc import ABC
from time import time_ns, sleep
from typing import Mapping, Union
@@ -35,7 +36,7 @@ class SimpleSerialMessage:
@public
-class SimpleSerialTarget(SerialTarget):
+class SimpleSerialTarget(SerialTarget, ABC):
"""A SimpleSerial target, sends and receives SimpleSerial messages over a serial link."""
def recv_msgs(self, timeout: int) -> Mapping[str, SimpleSerialMessage]:
diff --git a/pyecsca/sca/trace/align.py b/pyecsca/sca/trace/align.py
index ac30fe5..71815b8 100644
--- a/pyecsca/sca/trace/align.py
+++ b/pyecsca/sca/trace/align.py
@@ -1,4 +1,4 @@
-"""This module provides functions for aligning traces in a trace set to a reference trace within it."""
+"""Provides functions for aligning traces in a trace set to a reference trace within it."""
import numpy as np
from copy import deepcopy
from fastdtw import fastdtw, dtw
@@ -157,7 +157,7 @@ def align_offset(
def align_func(trace):
length = len(trace.samples)
- best_distance = 0
+ best_distance = 0.0
best_offset = 0
for offset in range(-max_offset, max_offset):
start = reference_offset + offset
diff --git a/pyecsca/sca/trace/combine.py b/pyecsca/sca/trace/combine.py
index 9df6059..622a325 100644
--- a/pyecsca/sca/trace/combine.py
+++ b/pyecsca/sca/trace/combine.py
@@ -1,4 +1,4 @@
-"""This module provides functions for combining traces sample-wise."""
+"""Provides functions for combining traces sample-wise."""
from typing import Callable, Optional, Tuple
import numpy as np
diff --git a/pyecsca/sca/trace/edit.py b/pyecsca/sca/trace/edit.py
index 83653d0..52338e6 100644
--- a/pyecsca/sca/trace/edit.py
+++ b/pyecsca/sca/trace/edit.py
@@ -1,13 +1,13 @@
-"""This module provides functions for editing traces as if they were tapes you can trim, reverse, etc."""
+"""Provides functions for editing traces as if they were tapes you can trim, reverse, etc."""
import numpy as np
from public import public
-from typing import Union, Tuple, Any
+from typing import Union, Tuple, Any, Optional
from .trace import Trace
@public
-def trim(trace: Trace, start: int = None, end: int = None) -> Trace:
+def trim(trace: Trace, start: Optional[int] = None, end: Optional[int] = None) -> Trace:
"""
Trim the `trace` samples, output contains samples between the `start` and `end` indices.
diff --git a/pyecsca/sca/trace/filter.py b/pyecsca/sca/trace/filter.py
index 48d065c..40ec13e 100644
--- a/pyecsca/sca/trace/filter.py
+++ b/pyecsca/sca/trace/filter.py
@@ -1,4 +1,4 @@
-"""This module provides functions for filtering traces using digital (low/high/band)-pass and bandstop filters."""
+"""Provides functions for filtering traces using digital (low/high/band)-pass and bandstop filters."""
from public import public
from scipy.signal import butter, lfilter
from typing import Union, Tuple
diff --git a/pyecsca/sca/trace/match.py b/pyecsca/sca/trace/match.py
index bcd4334..ae41dff 100644
--- a/pyecsca/sca/trace/match.py
+++ b/pyecsca/sca/trace/match.py
@@ -1,4 +1,4 @@
-"""This module provides functions for matching a pattern within a trace to it."""
+"""Provides functions for matching a pattern within a trace to it."""
import numpy as np
from scipy.signal import find_peaks
from public import public
diff --git a/pyecsca/sca/trace/plot.py b/pyecsca/sca/trace/plot.py
index 63fa2dd..fd38a9e 100644
--- a/pyecsca/sca/trace/plot.py
+++ b/pyecsca/sca/trace/plot.py
@@ -1,4 +1,4 @@
-"""This module provides functions for plotting traces."""
+"""Provides functions for plotting traces."""
from functools import reduce
import holoviews as hv
diff --git a/pyecsca/sca/trace/process.py b/pyecsca/sca/trace/process.py
index dac24ed..eee487c 100644
--- a/pyecsca/sca/trace/process.py
+++ b/pyecsca/sca/trace/process.py
@@ -1,4 +1,4 @@
-"""This module provides functions for sample-wise processing of single traces."""
+"""Provides functions for sample-wise processing of single traces."""
from typing import cast
import numpy as np
diff --git a/pyecsca/sca/trace/sampling.py b/pyecsca/sca/trace/sampling.py
index c92a478..b04ec5e 100644
--- a/pyecsca/sca/trace/sampling.py
+++ b/pyecsca/sca/trace/sampling.py
@@ -1,4 +1,4 @@
-"""This module provides downsampling functions for traces."""
+"""Provides downsampling functions for traces."""
from typing import cast
import numpy as np
diff --git a/pyecsca/sca/trace/test.py b/pyecsca/sca/trace/test.py
index a0d98a5..82afc19 100644
--- a/pyecsca/sca/trace/test.py
+++ b/pyecsca/sca/trace/test.py
@@ -1,4 +1,4 @@
-"""This module provides statistical tests usable on groups of traces sample-wise (Welch's and Student's t-test, ...)."""
+"""Provides statistical tests usable on groups of traces sample-wise (Welch's and Student's t-test, ...)."""
from typing import Sequence, Optional, Tuple
import numpy as np
diff --git a/pyecsca/sca/trace/trace.py b/pyecsca/sca/trace/trace.py
index 4ddad23..367125b 100644
--- a/pyecsca/sca/trace/trace.py
+++ b/pyecsca/sca/trace/trace.py
@@ -1,6 +1,6 @@
-"""This module provides the Trace class."""
+"""Provides the Trace class."""
import weakref
-from typing import Any, Mapping, Sequence
+from typing import Any, Mapping, Sequence, Optional
from copy import copy, deepcopy
from numpy import ndarray
@@ -16,7 +16,7 @@ class Trace:
samples: ndarray
def __init__(
- self, samples: ndarray, meta: Mapping[str, Any] = None, trace_set: Any = None
+ self, samples: ndarray, meta: Optional[Mapping[str, Any]] = None, trace_set: Any = None
):
"""
Construct a new trace.
@@ -76,6 +76,10 @@ class Trace:
return False
return np.array_equal(self.samples, other.samples) and self.meta == other.meta
+ def __hash__(self):
+ # This will have collisions, but those can be sorted out by the equality check above.
+ return hash(str(self.samples)) + hash(self.meta)
+
def with_samples(self, samples: ndarray) -> "Trace":
"""
Construct a copy of this trace, with the same metadata, but samples replaced by `samples`.
@@ -106,9 +110,9 @@ class CombinedTrace(Trace):
def __init__(
self,
samples: ndarray,
- meta: Mapping[str, Any] = None,
+ meta: Optional[Mapping[str, Any]] = None,
trace_set: Any = None,
- parents: Sequence[Trace] = None,
+ parents: Optional[Sequence[Trace]] = None,
):
super().__init__(samples, meta, trace_set=trace_set)
self.parents = None
diff --git a/pyecsca/sca/trace_set/base.py b/pyecsca/sca/trace_set/base.py
index d2e5791..d757615 100644
--- a/pyecsca/sca/trace_set/base.py
+++ b/pyecsca/sca/trace_set/base.py
@@ -1,4 +1,4 @@
-"""This module provides a base traceset class."""
+"""Provides a base traceset class."""
from pathlib import Path
from typing import List, Union, BinaryIO
diff --git a/pyecsca/sca/trace_set/hdf5.py b/pyecsca/sca/trace_set/hdf5.py
index dabb266..a3e72aa 100644
--- a/pyecsca/sca/trace_set/hdf5.py
+++ b/pyecsca/sca/trace_set/hdf5.py
@@ -1,5 +1,5 @@
"""
-This module provides a traceset implemented on top of the Hierarchical Data Format (HDF5).
+Provides a traceset implemented on top of the Hierarchical Data Format (HDF5).
This traceset can be loaded "inplace" which means that it is not fully loaded into memory, and only parts of traces that
are operated on are in memory. This is very useful for working with huge sets of traces that do not fit in memory.
diff --git a/pyecsca/sca/trace_set/inspector.py b/pyecsca/sca/trace_set/inspector.py
index 00e8273..2c9d581 100644
--- a/pyecsca/sca/trace_set/inspector.py
+++ b/pyecsca/sca/trace_set/inspector.py
@@ -1,4 +1,4 @@
-"""This module provides a traceset implementation based on Riscure's Inspector traceset format (``.trs``)."""
+"""Provides a traceset implementation based on Riscure's Inspector traceset format (``.trs``)."""
import struct
from enum import IntEnum
from io import BytesIO, RawIOBase, BufferedIOBase, UnsupportedOperation
diff --git a/pyecsca/sca/trace_set/pickle.py b/pyecsca/sca/trace_set/pickle.py
index 08def8a..5390a71 100644
--- a/pyecsca/sca/trace_set/pickle.py
+++ b/pyecsca/sca/trace_set/pickle.py
@@ -1,5 +1,5 @@
"""
-This module provides a traceset implementation based on Python's pickle format.
+Provides a traceset implementation based on Python's pickle format.
This implementation of the traceset is thus very generic.
"""
diff --git a/setup.py b/setup.py
index eddc9fc..ac670a5 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,7 @@ setup(
# install_package_data=True,
python_requires='>=3.8',
install_requires=[
- "numpy",
+ "numpy==1.23.5",
"scipy",
"sympy>=1.7.1",
"atpublic",
@@ -40,16 +40,18 @@ setup(
"matplotlib",
"datashader",
"xarray",
- "astunparse"
+ "astunparse",
+ "numba==0.56.4"
],
extras_require={
"picoscope_sdk": ["picosdk"],
"picoscope_alt": ["picoscope"],
"chipwhisperer": ["chipwhisperer"],
"smartcard": ["pyscard"],
+ "leia": ["smartleia"],
"gmp": ["gmpy2"],
"dev": ["mypy", "flake8", "interrogate", "pyinstrument", "black", "types-setuptools"],
"test": ["nose2", "parameterized", "coverage"],
- "doc": ["sphinx", "sphinx-autodoc-typehints", "nbsphinx"]
+ "doc": ["sphinx", "sphinx-autodoc-typehints", "nbsphinx", "sphinx-paramlinks", "sphinx_design"]
}
)
diff --git a/test/ec/perf_formula.py b/test/ec/perf_formula.py
index b49daab..baa6347 100755
--- a/test/ec/perf_formula.py
+++ b/test/ec/perf_formula.py
@@ -31,7 +31,7 @@ def main(profiler, mod, operations, directory):
add = coords.formulas["add-2016-rcb"]
dbl = coords.formulas["dbl-2016-rcb"]
click.echo(
- f"Profiling {operations} {p256.curve.prime.bit_length()}-bit doubling formula executions..."
+ f"Profiling {operations} {p256.curve.prime.bit_length()}-bit doubling formula (dbl2016rcb) executions..."
)
one_point = p256.generator
with Profiler(
@@ -40,7 +40,7 @@ def main(profiler, mod, operations, directory):
for _ in range(operations):
one_point = dbl(p256.curve.prime, one_point, **p256.curve.parameters)[0]
click.echo(
- f"Profiling {operations} {p256.curve.prime.bit_length()}-bit addition formula executions..."
+ f"Profiling {operations} {p256.curve.prime.bit_length()}-bit addition formula (add2016rcb) executions..."
)
other_point = p256.generator
with Profiler(
@@ -54,7 +54,7 @@ def main(profiler, mod, operations, directory):
ecoords = ed25519.curve.coordinate_model
dblg = ecoords.formulas["mdbl-2008-hwcd"]
click.echo(
- f"Profiling {operations} {ed25519.curve.prime.bit_length()}-bit doubling formula executions (with assumption)..."
+ f"Profiling {operations} {ed25519.curve.prime.bit_length()}-bit doubling formula (mdbl2008hwcd) executions (with assumption)..."
)
eone_point = ed25519.generator
with Profiler(
diff --git a/test/ec/test_context.py b/test/ec/test_context.py
index 6691985..9cd74a3 100644
--- a/test/ec/test_context.py
+++ b/test/ec/test_context.py
@@ -3,12 +3,8 @@ from unittest import TestCase
from pyecsca.ec.context import (
local,
DefaultContext,
- NullContext,
- getcontext,
- setcontext,
- resetcontext,
Tree,
- PathContext,
+ PathContext
)
from pyecsca.ec.key_generation import KeyGeneration
from pyecsca.ec.params import get_params
@@ -68,18 +64,14 @@ class ContextTests(TestCase):
def test_null(self):
with local() as ctx:
self.mult.multiply(59)
- self.assertIsInstance(ctx, NullContext)
+ self.assertIs(ctx, None)
def test_default(self):
- token = setcontext(DefaultContext())
- self.addCleanup(resetcontext, token)
-
with local(DefaultContext()) as ctx:
result = self.mult.multiply(59)
self.assertEqual(len(ctx.actions), 1)
action = next(iter(ctx.actions.keys()))
self.assertIsInstance(action, ScalarMultiplicationAction)
- self.assertEqual(len(getcontext().actions), 0)
self.assertEqual(result, action.result)
def test_default_no_enter(self):
@@ -100,6 +92,5 @@ class ContextTests(TestCase):
self.mult.multiply(59)
str(default)
str(default.actions)
- with local(NullContext()) as null:
+ with local(None):
self.mult.multiply(59)
- str(null)
diff --git a/test/ec/test_mod.py b/test/ec/test_mod.py
index 7802e95..62022b0 100644
--- a/test/ec/test_mod.py
+++ b/test/ec/test_mod.py
@@ -176,6 +176,9 @@ class ModTests(TestCase):
"__hash__",
"__abstractmethods__",
"_abc_impl",
+ "__slots__",
+ "x",
+ "n"
):
continue
args = [5 for _ in range(meth.__code__.co_argcount - 1)]
diff --git a/test/ec/test_params.py b/test/ec/test_params.py
index b2a57b6..833272d 100644
--- a/test/ec/test_params.py
+++ b/test/ec/test_params.py
@@ -2,10 +2,14 @@ from unittest import TestCase
from parameterized import parameterized
+from pyecsca.ec.mod import Mod
+from pyecsca.ec.point import Point, InfinityPoint
from pyecsca.misc.cfg import TemporaryConfig
from pyecsca.ec.coordinates import AffineCoordinateModel
from pyecsca.ec.error import UnsatisfiedAssumptionError
-from pyecsca.ec.params import get_params, load_params, load_category, get_category
+from pyecsca.ec.params import get_params, load_params, load_category, get_category, DomainParameters
+from pyecsca.ec.model import ShortWeierstrassModel
+from pyecsca.ec.curve import EllipticCurve
class DomainParameterTests(TestCase):
@@ -98,3 +102,21 @@ class DomainParameterTests(TestCase):
def test_affine(self):
aff = get_params("secg", "secp128r1", "affine")
self.assertIsInstance(aff.curve.coordinate_model, AffineCoordinateModel)
+
+ def test_custom_params(self):
+ model = ShortWeierstrassModel()
+ coords = model.coordinates["projective"]
+ p = 0xd7d1247f
+ a = Mod(0xa4a44016, p)
+ b = Mod(0x73f76716, p)
+ n = 0xd7d2a475
+ h = 1
+ gx, gy, gz = Mod(0x54eed6d7, p), Mod(0x6f1e55ac, p), Mod(1, p)
+ generator = Point(coords, X=gx, Y=gy, Z=gz)
+ neutral = InfinityPoint(coords)
+
+ curve = EllipticCurve(model, coords, p, neutral, {"a": a, "b": b})
+ params = DomainParameters(curve, generator, n, h)
+ self.assertIsNotNone(params)
+ res = params.curve.affine_double(generator.to_affine())
+ self.assertIsNotNone(res)
diff --git a/test/utils.py b/test/utils.py
index a74fae4..e125813 100644
--- a/test/utils.py
+++ b/test/utils.py
@@ -62,14 +62,14 @@ class Profiler:
if self._state != "out":
raise ValueError
if self._prof_type == "py":
- print(self._prof.output_text(unicode=True, color=True))
+ print(self._prof.output_text(unicode=True, color=True, show_all=True))
else:
self._prof.print_stats("cumtime")
- def get_time(self):
+ def get_time(self) -> float:
if self._state != "out":
raise ValueError
if self._prof_type == "py":
- return self._root_frame.time()
+ return self._root_frame.time
else:
- return pstats.Stats(self._prof).total_tt
+ return pstats.Stats(self._prof).total_tt # type: ignore