aboutsummaryrefslogtreecommitdiffhomepage
path: root/pyecsca
diff options
context:
space:
mode:
Diffstat (limited to 'pyecsca')
-rw-r--r--pyecsca/ec/context.py21
-rw-r--r--pyecsca/ec/mult/comb.py2
-rw-r--r--pyecsca/ec/mult/naf.py3
-rw-r--r--pyecsca/ec/mult/window.py6
-rw-r--r--pyecsca/sca/re/epa.py105
-rw-r--r--pyecsca/sca/re/rpa.py117
-rw-r--r--pyecsca/sca/re/tree.py2
7 files changed, 218 insertions, 38 deletions
diff --git a/pyecsca/ec/context.py b/pyecsca/ec/context.py
index b58db2a..33c961d 100644
--- a/pyecsca/ec/context.py
+++ b/pyecsca/ec/context.py
@@ -352,3 +352,24 @@ def local(ctx: Optional[Context] = None, copy: bool = True) -> ContextManager:
:return: A context manager.
"""
return _ContextManager(ctx, copy)
+
+
+@public
+def compound(*contexts: Context) -> Context:
+ """
+ Create a context that compounds multiple contexts.
+
+ :param contexts: The contexts to compound.
+ :return: The compounded context.
+ """
+
+ class CompoundContext(Context):
+ def enter_action(self, action: Action) -> None:
+ for ctx in contexts:
+ ctx.enter_action(action)
+
+ def exit_action(self, action: Action) -> None:
+ for ctx in contexts:
+ ctx.exit_action(action)
+
+ return CompoundContext()
diff --git a/pyecsca/ec/mult/comb.py b/pyecsca/ec/mult/comb.py
index d24f557..ae6d0c1 100644
--- a/pyecsca/ec/mult/comb.py
+++ b/pyecsca/ec/mult/comb.py
@@ -198,7 +198,7 @@ class CombMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultiplier)
current_point = point
for i in range(self.width):
base_points[i] = current_point
- if i != d - 1:
+ if i != self.width - 1:
for _ in range(d):
current_point = self._dbl(current_point)
self._points = {}
diff --git a/pyecsca/ec/mult/naf.py b/pyecsca/ec/mult/naf.py
index 1d77056..4c6e0c2 100644
--- a/pyecsca/ec/mult/naf.py
+++ b/pyecsca/ec/mult/naf.py
@@ -270,7 +270,8 @@ class WindowNAFMultiplier(AccumulatorMultiplier, PrecompMultiplier, ScalarMultip
self._points[2 * i + 1] = current_point
if self.precompute_negation:
self._points_neg[2 * i + 1] = self._neg(current_point)
- current_point = self._add(current_point, double_point)
+ if i != 2 ** (self.width - 2) - 1:
+ current_point = self._add(current_point, double_point)
result = {**self._points}
if self.precompute_negation:
result.update({-k: v for k, v in self._points_neg.items()})
diff --git a/pyecsca/ec/mult/window.py b/pyecsca/ec/mult/window.py
index ed0cd05..e46179a 100644
--- a/pyecsca/ec/mult/window.py
+++ b/pyecsca/ec/mult/window.py
@@ -104,7 +104,8 @@ class SlidingWindowMultiplier(
double_point = self._dbl(point)
for i in range(0, 2 ** (self.width - 1)):
self._points[2 * i + 1] = current_point
- current_point = self._add(current_point, double_point)
+ if i != 2 ** (self.width - 1) - 1:
+ current_point = self._add(current_point, double_point)
action.exit(self._points)
def multiply(self, scalar: int) -> Point:
@@ -232,7 +233,8 @@ class FixedWindowLTRMultiplier(
converted = convert_base(scalar, self.m)
q = copy(self._params.curve.neutral)
for digit in reversed(converted):
- q = self._mult_m(q)
+ if q != self._params.curve.neutral:
+ q = self._mult_m(q)
if digit != 0:
q = self._accumulate(q, self._points[digit])
if "scl" in self.formulas:
diff --git a/pyecsca/sca/re/epa.py b/pyecsca/sca/re/epa.py
index 1a6b19f..4ef1b74 100644
--- a/pyecsca/sca/re/epa.py
+++ b/pyecsca/sca/re/epa.py
@@ -1,8 +1,12 @@
"""
Provides functionality inspired by the Exceptional Procedure Attack [EPA]_.
"""
+
from typing import Callable, Literal, Union, Optional
+import networkx as nx
+
+
from public import public
from pyecsca.ec.point import Point
@@ -26,7 +30,8 @@ def graph_to_check_inputs(
"affine" formula that checks the multiples of the points that need to be converted to affine coordinates. Which
points this "affine" formula checks depends on the `precomp_to_affine` parameter.
- :param ctx: The context containing the points and formulas.
+ :param precomp_ctx: The context containing the points and formulas (precomputation phase).
+ :param full_ctx: The context containing the points and formulas (full computation).
:param out: The output point of the computation.
:param check_condition: Whether to check all points or only those necessary for the output point.
:param precomp_to_affine: Whether to include the precomputed points in the to-affine checks.
@@ -73,9 +78,13 @@ def graph_to_check_inputs(
if use_init and use_multiply:
points = _necessary(full_ctx, affine_points)
elif use_init:
- points = _necessary(full_ctx, affine_points) & set(precomp_ctx.points.keys())
+ points = _necessary(full_ctx, affine_points) & set(
+ precomp_ctx.points.keys()
+ )
elif use_multiply:
- points = _necessary(full_ctx, affine_points) - set(precomp_ctx.points.keys())
+ points = _necessary(full_ctx, affine_points) - set(
+ precomp_ctx.points.keys()
+ )
else:
raise ValueError("check_condition must be 'all' or 'necessary'")
# Special case the "to affine" transform and checks
@@ -90,7 +99,9 @@ def graph_to_check_inputs(
for point in points:
formula = full_ctx.formulas[point]
- if not formula or (check_formulas is not None and formula not in check_formulas):
+ if not formula or (
+ check_formulas is not None and formula not in check_formulas
+ ):
# Skip input point or infty point (they magically appear and do not have an origin formula)
continue
inputs = tuple(map(get_point, full_ctx.parents[point]))
@@ -100,8 +111,79 @@ def graph_to_check_inputs(
@public
+def graph_plot_prepare(
+ precomp_ctx: MultipleContext,
+ full_ctx: MultipleContext,
+ out: Point,
+) -> nx.DiGraph:
+ """
+ Prepare the computation graph, highlighting necessary points and precomputed points.
+
+ Legend:
+ - Circles: Necessary points in the computation.
+ - Triangles: Unnecessary points in the computation.
+ - Blue: Points computed during the full computation phase.
+ - Green: Points computed during the precomputation phase.
+ - Cyan: Precomputed points (stored by the multiplier).
+
+ Plot the result using `nx.display`.
+
+ :param precomp_ctx: The context containing the points and formulas (precomputation phase).
+ :param full_ctx: The context containing the points and formulas (full computation).
+ :param out: The output point of the computation.
+ :return: The networkx graph.
+ """
+ graph = full_ctx.to_networkx()
+
+ for layer, nodes in enumerate(nx.topological_generations(graph)):
+ for node in nodes:
+ graph.nodes[node]["layer"] = layer
+ for node in graph.nodes():
+ graph.nodes[node]["necessary"] = False
+ queue = {out}
+ while queue:
+ node = queue.pop()
+ graph.nodes[node]["necessary"] = True
+ for n in graph.predecessors(node):
+ queue.add(n)
+
+ pos = nx.multipartite_layout(graph, subset_key="layer")
+ nx.set_node_attributes(graph, pos, "pos")
+
+ for point in graph.nodes():
+ node = graph.nodes[point]
+ node["label"] = {
+ "label": node["multiple"],
+ "color": "black",
+ "bbox": {"boxstyle": "square", "alpha": 0.7, "facecolor": "white"}
+ }
+
+ if point in precomp_ctx.points.keys():
+ node["color"] = "#208020"
+ if node["precomp"]:
+ node["pos"][1] -= 0.01
+ node["color"] = "#30a0a0"
+ else:
+ node["color"] = "#202080"
+
+ if node["necessary"]:
+ node["shape"] = "o"
+ else:
+ node["shape"] = "^"
+ node["pos"][1] += 0.01
+
+ for u, v in graph.edges():
+ edge = graph.edges[u, v]
+ edge["label"] = edge["formula"]
+ edge["curvature"] = "arc3,rad=0.05"
+
+ return graph
+
+
+@public
def evaluate_checks(
- check_funcs: dict[str, Union[Callable[[int, int], bool], Callable[[int], bool]]], check_inputs: dict[str, set[tuple[int, ...]]]
+ check_funcs: dict[str, Union[Callable[[int, int], bool], Callable[[int], bool]]],
+ check_inputs: dict[str, set[tuple[int, ...]]],
) -> bool:
"""
Evaluate the checks for each formula type based on the provided functions and inputs.
@@ -141,7 +223,8 @@ def errors_out(
"""
Check whether the computation errors out based on the provided context, output point, and check functions.
- :param ctx: The context containing the points and formulas.
+ :param precomp_ctx: The context containing the points and formulas (precomputation phase).
+ :param full_ctx: The context containing the points and formulas (full computation).
:param out: The output point of the computation.
:param check_funcs: The functions to apply for each formula type. There are two callable types:
- `check(k, l)`, that gets applied to binary formulas (like `add`), where `k` and `l` are the input multiples
@@ -157,5 +240,13 @@ def errors_out(
.. note::
The scalar multiplier must not short-circuit.
"""
- formula_checks = graph_to_check_inputs(precomp_ctx, full_ctx, out, check_condition, precomp_to_affine, use_init, use_multiply)
+ formula_checks = graph_to_check_inputs(
+ precomp_ctx,
+ full_ctx,
+ out,
+ check_condition,
+ precomp_to_affine,
+ use_init,
+ use_multiply,
+ )
return evaluate_checks(check_funcs, formula_checks)
diff --git a/pyecsca/sca/re/rpa.py b/pyecsca/sca/re/rpa.py
index e899f20..2fd5973 100644
--- a/pyecsca/sca/re/rpa.py
+++ b/pyecsca/sca/re/rpa.py
@@ -18,6 +18,8 @@ from typing import (
Tuple,
)
+import networkx as nx
+
from sympy import FF, sympify, Poly, symbols
from pyecsca.ec.error import NonInvertibleError
@@ -27,7 +29,7 @@ from pyecsca.sca.re.tree import Tree, Map
from pyecsca.ec.coordinates import AffineCoordinateModel
from pyecsca.ec.formula import (
FormulaAction,
- )
+)
from pyecsca.ec.mod import Mod, mod
from pyecsca.ec.mult import (
ScalarMultiplicationAction,
@@ -37,7 +39,7 @@ from pyecsca.ec.mult import (
from pyecsca.ec.params import DomainParameters
from pyecsca.ec.model import ShortWeierstrassModel, MontgomeryModel
from pyecsca.ec.point import Point
-from pyecsca.ec.context import Context, Action, local
+from pyecsca.ec.context import Context, Action, local, compound
from pyecsca.ec.mult.fake import cached_fake_mult, fake_params
from pyecsca.misc.utils import log, warn
@@ -58,22 +60,50 @@ class MultipleContext(Context):
"""The mapping of points to the formula types they are a result of."""
precomp: MutableMapping[int, Point]
"""The mapping of precomputed multiples to the points they represent."""
+ result: Optional[Point]
+ """The resulting point of the computation."""
inside: List[Action]
"""Whether we are inside a scalarmult/precomp action."""
keep_base: bool
"""Whether to keep the base point when building upon it."""
- def __init__(self, keep_base: bool = False):
+ def __init__(
+ self,
+ keep_base: bool = False,
+ track_precomp: bool = True,
+ track_scalarmult: bool = True,
+ ):
self.base = None
self.points = {}
self.parents = {}
self.formulas = {}
self.precomp = {}
self.inside = []
+ self.result = None
self.keep_base = keep_base
+ self._track_precomp = track_precomp
+ self._track_scalarmult = track_scalarmult
+
+ @property
+ def track_precomp(self) -> bool:
+ """Whether to track PrecomputationAction actions."""
+ return self._track_precomp
+
+ @track_precomp.setter
+ def track_precomp(self, value: bool) -> None:
+ self._track_precomp = value
+
+ @property
+ def track_scalarmult(self) -> bool:
+ """Whether to track ScalarMultiplicationAction actions."""
+ return self._track_scalarmult
+
+ @track_scalarmult.setter
+ def track_scalarmult(self, value: bool) -> None:
+ self._track_scalarmult = value
def enter_action(self, action: Action) -> None:
- if isinstance(action, (ScalarMultiplicationAction, PrecomputationAction)):
+ if (self._track_precomp and isinstance(action, PrecomputationAction)) or (self._track_scalarmult and isinstance(action, ScalarMultiplicationAction)):
self.inside.append(action)
if self.base:
# If we already did some computation with this context try to see if we are building on top of it.
@@ -100,10 +130,12 @@ class MultipleContext(Context):
self.precomp = {}
def exit_action(self, action: Action) -> None:
- if isinstance(action, (ScalarMultiplicationAction, PrecomputationAction)):
+ if (self._track_precomp and isinstance(action, PrecomputationAction)) or (self._track_scalarmult and isinstance(action, ScalarMultiplicationAction)):
self.inside.remove(action)
if isinstance(action, PrecomputationAction):
self.precomp.update(action.result)
+ if isinstance(action, ScalarMultiplicationAction):
+ self.result = action.result
if isinstance(action, FormulaAction) and self.inside:
action = cast(FormulaAction, action)
shortname = action.formula.shortname
@@ -156,6 +188,35 @@ class MultipleContext(Context):
def __repr__(self):
return f"{self.__class__.__name__}({self.base!r}, multiples={self.points.values()!r})"
+ def to_networkx(self) -> nx.DiGraph:
+ """
+ Convert the context to a NetworkX graph.
+
+ The nodes represent points, with attributes:
+ - multiple: The multiple of the base point.
+ - formula: The formula used to compute the point.
+ - precomp: Whether the point was the result of precomputation.
+ - result: Whether the point is the final result of the computation.
+
+ The edges represent the computation steps, with attribute:
+ - formula: The formula used to compute the child point from the parent point.
+
+ :return: A NetworkX DiGraph representing the computation.
+ """
+ graph = nx.DiGraph()
+ for point, multiple in self.points.items():
+ graph.add_node(
+ point,
+ multiple=multiple,
+ formula=self.formulas[point],
+ precomp=point in self.precomp.values(),
+ result=point == self.result,
+ )
+ for point, parents in self.parents.items():
+ for parent in parents:
+ graph.add_edge(parent, point, formula=self.formulas[point])
+ return graph
+
@public
def rpa_point_0y(params: DomainParameters) -> Optional[Point]:
@@ -431,28 +492,29 @@ def multiple_graph(
:param mult_factory: A callable that takes the formulas and instantiates the multiplier.
:param dlog: Make an assumption that the symbolic input point is the `dlog` multiple of the base point.
This is necessary if the multiplier does computation with the base point.
- :return: The context with the computed multiples and the resulting point.
+ :return: The context with the computed multiples during precomputation, the context with the computed multiples
+ during the full computation, and the resulting point.
"""
params = fake_params(params)
mult = cached_fake_mult(mult_class, mult_factory, params)
- ctx = MultipleContext(keep_base=True)
- with local(ctx, copy=False) as precomp_ctx:
+ precomp_ctx = MultipleContext(keep_base=True, track_scalarmult=False, track_precomp=True)
+ full_ctx = MultipleContext(keep_base=True, track_scalarmult=True, track_precomp=True)
+ with local(compound(precomp_ctx, full_ctx), copy=False):
point = FakePoint(params.curve.coordinate_model)
- if dlog:
- ctx.base = params.generator
- ctx.neutral = params.curve.neutral
- ctx.points[ctx.base] = 1
- ctx.points[point] = dlog
- ctx.points[ctx.neutral] = 0
- ctx.formulas[ctx.base] = ""
- ctx.formulas[point] = ""
- ctx.formulas[ctx.neutral] = ""
- ctx.parents[ctx.base] = []
- ctx.parents[point] = []
- ctx.parents[ctx.neutral] = []
+ if dlog is not None:
+ for ctx in (precomp_ctx, full_ctx):
+ ctx.base = params.generator
+ ctx.neutral = params.curve.neutral
+ ctx.points[ctx.base] = 1
+ ctx.points[point] = dlog
+ ctx.points[ctx.neutral] = 0
+ ctx.formulas[ctx.base] = ""
+ ctx.formulas[point] = ""
+ ctx.formulas[ctx.neutral] = ""
+ ctx.parents[ctx.base] = []
+ ctx.parents[point] = []
+ ctx.parents[ctx.neutral] = []
mult.init(params, point)
-
- with local(ctx, copy=True) as full_ctx:
out = mult.multiply(scalar)
return precomp_ctx, full_ctx, out
@@ -472,10 +534,11 @@ def multiples_from_graph(
use_multiply: bool = True,
):
"""
+ Compute the multiples computed for a given scalar and multiplier from the multiple graph.
- :param precomp_ctx:
- :param full_ctx:
- :param out:
+ :param precomp_ctx: The context containing the points and formulas (precomputation phase).
+ :param full_ctx: The context containing the points and formulas (full computation).
+ :param out: The output point of the computation.
:param kind: The kind of multiples to return. Can be one of "all", "input", "necessary", or "precomp+necessary".
:param use_init: Whether to consider the point multiples that happen in scalarmult initialization.
:param use_multiply: Whether to consider the point multiples that happen in scalarmult multiply (after initialization).
@@ -569,7 +632,9 @@ def multiples_computed(
.. note::
The scalar multiplier must not short-circuit.
"""
- precomp_ctx, full_ctx, out = multiple_graph(scalar, params, mult_class, mult_factory)
+ precomp_ctx, full_ctx, out = multiple_graph(
+ scalar, params, mult_class, mult_factory
+ )
return (
multiples_from_graph(precomp_ctx, full_ctx, out, kind, use_init, use_multiply)
diff --git a/pyecsca/sca/re/tree.py b/pyecsca/sca/re/tree.py
index a45faaa..6d07ba4 100644
--- a/pyecsca/sca/re/tree.py
+++ b/pyecsca/sca/re/tree.py
@@ -667,7 +667,7 @@ def _build_tree(
)
log(pad + f"Split {len(group_cfgs)} via dmap {best_i}.")
# And build the tree recursively
- child = _build_tree(group_cfgs, maps, response=output, depth=depth + 1, index=i, breadth=groups.ngroups, split=split)
+ child = _build_tree(group_cfgs, maps, response=output, depth=depth + 1, index=i, breadth=groups.ngroups, split=split, leaf_callback=leaf_callback)
child.parent = result
return result