diff options
| author | Ján Jančár | 2023-03-15 15:10:18 +0100 |
|---|---|---|
| committer | GitHub | 2023-03-15 15:10:18 +0100 |
| commit | 4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b (patch) | |
| tree | f2b3ec1afad390e452333e538d655aaad51635c9 | |
| parent | d9ebb0c6db27c5271b93f8faa0b5308d6e2c63cc (diff) | |
| parent | 445eaa41f22ed82502ca813e98a92c2b078c9a79 (diff) | |
| download | pyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.tar.gz pyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.tar.zst pyecsca-4cd1dc32cc87179445b3b4f377a2d3e23e9ef49b.zip | |
Merge branch 'master' into feat/stacked-perf-test
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"] @@ -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 @@ -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 @@ -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. """ @@ -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 |
