diff options
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 22 | ||||
| -rw-r--r-- | docs/conf.py | 3 | ||||
| -rw-r--r-- | docs/index.rst | 2 | ||||
| -rw-r--r-- | pyecsca/ec/coordinates.py | 10 | ||||
| -rw-r--r-- | pyecsca/ec/curve.py | 50 | ||||
| -rw-r--r-- | pyecsca/ec/mult.py | 30 | ||||
| -rw-r--r-- | pyecsca/ec/op.py | 9 | ||||
| -rw-r--r-- | pyecsca/ec/point.py | 6 | ||||
| m--------- | pyecsca/ec/std | 0 | ||||
| -rw-r--r-- | setup.py | 2 |
11 files changed, 119 insertions, 20 deletions
@@ -22,8 +22,11 @@ typecheck: codestyle: flake8 --ignore=E501,F405,F403,F401,E126 pyecsca +doc-coverage: + interrogate -vv -nmps pyecsca + docs: $(MAKE) -C docs apidoc $(MAKE) -C docs html -.PHONY: test test-plots test-all typecheck codestyle docs
\ No newline at end of file +.PHONY: test test-plots test-all typecheck codestyle doc-coverage docs
\ No newline at end of file @@ -8,11 +8,24 @@ For more info, see the [ package) -and ECC simulation in the [*pyecsca.ec*](pyecsca/ec) package. +about a black-box implementation of ECC through side-channels. The main goal of **pyecsca** +is to be able to reverse engineer the curve model, coordinate system, addition formulas, scalar +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 [notebook/configuration_space](https://neuromancer.sk/pyecsca/notebook/configuration_space.html)) + - Simulation and execution tracing of key generation, ECDH and ECDSA (see [notebook/simulation](https://neuromancer.sk/pyecsca/notebook/simulation.html)) + - Synthesis of C implementations of ECC for embedded devices, given any implementation configuration (see [notebook/codegen](https://neuromancer.sk/pyecsca/notebook/codegen.html)) + - 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)) + +**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 ## Requirements @@ -28,6 +41,7 @@ and ECC simulation in the [*pyecsca.ec*](pyecsca/ec) package. - [datashader](https://datashader.org) - [matplotlib](https://matplotlib.org/) - [xarray](https://xarray.pydata.org/en/stable/) + - [astunparse](https://astunparse.readthedocs.io/) - **Optionally**: - **Oscilloscope support:** - [picosdk](https://github.com/picotech/picosdk-python-wrappers/) diff --git a/docs/conf.py b/docs/conf.py index 7f4489b..5ceee32 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -206,7 +206,8 @@ autodoc_default_options = { "members": True, "undoc-members": True, "inherited-members": True, - "show-inheritance": True + "show-inheritance": True, + "member-order": "bysource" } nbsphinx_allow_errors = True diff --git a/docs/index.rst b/docs/index.rst index 6052cf5..1ae054f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -73,6 +73,7 @@ Requirements - datashader_ - matplotlib_ - xarray_ + - astunparse_ - **Optionally**: - **Oscilloscope support:** @@ -150,6 +151,7 @@ this support is very appreciated. .. _bokeh: https://bokeh.org .. _datashader: https://datashader.org .. _xarray: https://xarray.pydata.org/en/stable/ +.. _astunparse: https://astunparse.readthedocs.io/ .. _picosdk: https://github.com/picotech/picosdk-python-wrappers/ .. _picoscope: https://github.com/colinoflynn/pico-python .. _chipwhisperer: https://github.com/newaetech/chipwhisperer diff --git a/pyecsca/ec/coordinates.py b/pyecsca/ec/coordinates.py index 48a8f76..6e001a9 100644 --- a/pyecsca/ec/coordinates.py +++ b/pyecsca/ec/coordinates.py @@ -14,14 +14,24 @@ from .formula import (Formula, EFDFormula, AdditionEFDFormula, DoublingEFDFormul class CoordinateModel(object): """A coordinate system for a particular model(form) of an elliptic curve.""" name: str + """Name of the coordinate model""" full_name: str + """Full name.""" curve_model: Any + """The curve model.""" variables: List[str] + """Variables that the coordinate model uses.""" satisfying: List[Module] + """Relationship between the coordinate system and affine coordinates.""" parameters: List[str] + """Coordinate system parameters.""" assumptions: List[Module] + """Assumptions that need to hold for the curve to use this coordinate system, + also used to compute the values of the coordinate system parameters.""" neutral: List[Module] + """Coordinates of the neutral point in the coordinate system, might contain expressions of parameters.""" formulas: MutableMapping[str, Formula] + """Formulas available on the coordinate system.""" def __repr__(self): return f"{self.__class__.__name__}(\"{self.name}\" on {self.curve_model.name})" diff --git a/pyecsca/ec/curve.py b/pyecsca/ec/curve.py index 44358d0..7351583 100644 --- a/pyecsca/ec/curve.py +++ b/pyecsca/ec/curve.py @@ -14,10 +14,15 @@ from .point import Point, InfinityPoint class EllipticCurve(object): """An elliptic curve.""" model: CurveModel + """The model of the curve.""" coordinate_model: CoordinateModel + """The coordinate system of the curve.""" prime: int + """The prime specifying the base prime field of the curve.""" parameters: MutableMapping[str, Mod] + """The values of the parameters defining the curve, these cover the curve model and coordinate system parameters.""" neutral: Point + """The neutral point on the curve.""" def __init__(self, model: CurveModel, coordinate_model: CoordinateModel, prime: int, neutral: Point, parameters: MutableMapping[str, Union[Mod, int]]): @@ -59,6 +64,10 @@ class EllipticCurve(object): """ Add two affine points using the affine addition formula. Handles the case of point at infinity gracefully. + + :param one: One point. + :param other: Another point. + :return: The addition of the two points. """ if isinstance(one, InfinityPoint): return other @@ -72,6 +81,9 @@ class EllipticCurve(object): """ Double an affine point using the affine doubling formula. Handles the case of point at infinity gracefully. + + :param one: A point. + :return: The doubling of the point. """ if isinstance(one, InfinityPoint): return one @@ -81,6 +93,9 @@ class EllipticCurve(object): """ Negate an affine point using the affine negation formula. Handles the case of point at infinity gracefully. + + :param one: A point. + :return: The negation of the point. """ if isinstance(one, InfinityPoint): return one @@ -90,6 +105,10 @@ class EllipticCurve(object): """ Multiply an affine point by a scalar using the affine doubling and addition formulas. Handles the case of point at infinity gracefully. + + :param point: The point to multiply. + :param scalar: The scalar to use. + :return: The scalar multiplication of `point`. """ if isinstance(point, InfinityPoint): return point @@ -110,6 +129,7 @@ class EllipticCurve(object): def affine_neutral(self) -> Optional[Point]: """ Get the neutral point in affine form, if it has one, otherwise `None`. + :return: The affine neutral point or `None`. """ if not self.neutral_is_affine: @@ -129,11 +149,20 @@ class EllipticCurve(object): return bool(self.model.base_neutral) def is_neutral(self, point: Point) -> bool: - """Check whether the point is the neutral point.""" + """Check whether the point is the neutral point. + + :param point: The point to test. + :return: Whether it is the neutral point. + """ return self.neutral == point def is_on_curve(self, point: Point) -> bool: - """Check whether the point is on the curve.""" + """ + Check whether the point is on the curve. + + :param point: The point to test. + :return: Whether it is on the curve. + """ if point.coordinate_model.curve_model != self.model: return False if self.is_neutral(point): @@ -142,12 +171,25 @@ class EllipticCurve(object): return eval(compile(self.model.equation, "", mode="eval"), loc) def to_affine(self) -> "EllipticCurve": - """Convert this curve into the affine coordinate model, if possible.""" + """ + Convert this curve into the affine coordinate model, if possible. + + :return: The transformed elliptic curve. + """ coord_model = AffineCoordinateModel(self.model) return EllipticCurve(self.model, coord_model, self.prime, self.neutral.to_affine(), self.parameters) # type: ignore[arg-type] def decode_point(self, encoded: bytes) -> Point: - """Decode a point encoded as a sequence of bytes (ANSI X9.62).""" + """ + Decode a point encoded as a sequence of bytes (ANSI X9.62). This decoding is the same as ANSI X9.63 for + the affine coordinate system and for others it only implements the uncompressed variant. + + .. warning:: + The point is not validated to be on the curve (if the uncompressed encoding is used). + + :param encoded: The encoded representation of a point. + :return: The decoded point. + """ if encoded[0] == 0x00 and len(encoded) == 1: return InfinityPoint(self.coordinate_model) coord_len = (self.prime.bit_length() + 7) // 8 diff --git a/pyecsca/ec/mult.py b/pyecsca/ec/mult.py index 7a8e23e..5804eef 100644 --- a/pyecsca/ec/mult.py +++ b/pyecsca/ec/mult.py @@ -27,6 +27,7 @@ class ScalarMultiplicationAction(ResultAction): return f"{self.__class__.__name__}({self.point}, {self.scalar})" +@public class ScalarMultiplier(ABC): """ A scalar multiplication algorithm. @@ -36,7 +37,9 @@ class ScalarMultiplier(ABC): :param formulas: Formulas this instance will use. """ requires: ClassVar[Set[Type]] # Type[Formula] but mypy has a false positive + """The set of formulas that the multiplier requires.""" optionals: ClassVar[Set[Type]] # Type[Formula] but mypy has a false positive + """The optional set of formulas that the multiplier can use.""" short_circuit: bool formulas: Mapping[str, Formula] _params: DomainParameters @@ -99,7 +102,15 @@ class ScalarMultiplier(ABC): return self.formulas["neg"](point, **self._params.curve.parameters)[0] def init(self, params: DomainParameters, point: Point): - """Initialize the scalar multiplier with params and a point.""" + """ + Initialize the scalar multiplier with params and a point. + + .. warning:: + The point is not verified to be on the curve represented in the domain parameters. + + :param params: The domain parameters to initialize the multiplier with. + :param point: The point to initialize the multiplier with. + """ coord_model = set(self.formulas.values()).pop().coordinate_model if params.curve.coordinate_model != coord_model or point.coordinate_model != coord_model: raise ValueError @@ -109,14 +120,22 @@ class ScalarMultiplier(ABC): @abstractmethod def multiply(self, scalar: int) -> Point: - """Multiply the point with the scalar.""" + """ + Multiply the point with the scalar. + + .. note:: + The multiplier needs to be initialized by a call to the :py:meth:`.init` method. + + :param scalar: The scalar to use. + :return: The resulting multiple. + """ ... @public class LTRMultiplier(ScalarMultiplier): """ - Classic double and add scalar multiplication algorithm, that scans the scalar left-to-right (msb to lsb) + Classic double and add scalar multiplication algorithm, that scans the scalar left-to-right (msb to lsb). The `always` parameter determines whether the double and add always method is used. """ @@ -161,7 +180,7 @@ class LTRMultiplier(ScalarMultiplier): @public class RTLMultiplier(ScalarMultiplier): """ - Classic double and add scalar multiplication algorithm, that scans the scalar right-to-left (lsb to msb) + Classic double and add scalar multiplication algorithm, that scans the scalar right-to-left (lsb to msb). The `always` parameter determines whether the double and add always method is used. """ @@ -195,6 +214,7 @@ class RTLMultiplier(ScalarMultiplier): return action.exit(r) +@public class CoronMultiplier(ScalarMultiplier): """ Coron's double and add resistant against SPA, from: @@ -407,7 +427,7 @@ class WindowNAFMultiplier(ScalarMultiplier): self._points_neg = {} current_point = point double_point = self._dbl(point) - for i in range(0, 2**(self.width - 2)): + for i in range(0, 2 ** (self.width - 2)): self._points[2 * i + 1] = current_point if self.precompute_negation: self._points_neg[2 * i + 1] = self._neg(current_point) diff --git a/pyecsca/ec/op.py b/pyecsca/ec/op.py index d72716d..ee23e51 100644 --- a/pyecsca/ec/op.py +++ b/pyecsca/ec/op.py @@ -6,12 +6,12 @@ from typing import FrozenSet, cast, Any, Optional, Union from public import public -from .context import ResultAction from .mod import Mod @public class OpType(Enum): + """A type of binary and unary operators.""" Add = (2, "+") Sub = (2, "-") Neg = (1, "-") @@ -29,12 +29,19 @@ class OpType(Enum): @public class CodeOp(object): + """An operation that can be executed.""" result: str + """The result variable of the operation (e.g. the `r` in `r = 2*a`).""" parameters: FrozenSet[str] + """The parameters used in the operation (e.g. `a`, `b`).""" variables: FrozenSet[str] + """The variables used in the operation (e.g. `X1`, `Z2`).""" code: Module + """The code of the operation.""" operator: OpType + """The operator type that executes in the operation.""" compiled: CodeType + """The compiled code of the operation.""" def __init__(self, code: Module): self.code = code diff --git a/pyecsca/ec/point.py b/pyecsca/ec/point.py index 19c2483..62e4b05 100644 --- a/pyecsca/ec/point.py +++ b/pyecsca/ec/point.py @@ -4,7 +4,7 @@ from typing import Mapping, Any from public import public from .context import ResultAction -from .coordinates import AffineCoordinateModel, CoordinateModel +from .coordinates import AffineCoordinateModel, CoordinateModel, EFDCoordinateModel from .mod import Mod, Undefined from .op import CodeOp @@ -97,11 +97,11 @@ class Point(object): result[var] = locls[var] elif var == "X": result[var] = self.coords["x"] - if coordinate_model.name == "inverted": + if isinstance(coordinate_model, EFDCoordinateModel) and coordinate_model.name == "inverted": result[var] = result[var].inverse() elif var == "Y": result[var] = self.coords["y"] - if coordinate_model.name == "inverted": + if isinstance(coordinate_model, EFDCoordinateModel) and coordinate_model.name == "inverted": result[var] = result[var].inverse() elif var.startswith("Z"): result[var] = Mod(1, curve.prime) diff --git a/pyecsca/ec/std b/pyecsca/ec/std -Subproject 4af0d222a5de43f650efbb639cca6de6bb8cfe6 +Subproject 73397a4d61733da53e87415826f3133cf3ff8a0 @@ -47,7 +47,7 @@ setup( "chipwhisperer": ["chipwhisperer"], "smartcard": ["pyscard"], "gmp": ["gmpy2"], - "dev": ["mypy", "flake8"], + "dev": ["mypy", "flake8", "interrogate"], "test": ["nose2", "parameterized", "green", "coverage"] } ) |
