aboutsummaryrefslogtreecommitdiff
path: root/analysis/scalarmults/common.py
diff options
context:
space:
mode:
authorJ08nY2025-10-25 16:07:31 +0200
committerJ08nY2025-10-25 16:07:31 +0200
commit4bf6a31c00a90cb5aea54c7a37c95f8f3413faaf (patch)
tree3c6b81eb26563919eb927c7bf46ba38d59be0e9e /analysis/scalarmults/common.py
parent1bf4c680fed1eab512e65ec6b7a0b4eb6ce8ca74 (diff)
downloadECTester-4bf6a31c00a90cb5aea54c7a37c95f8f3413faaf.tar.gz
ECTester-4bf6a31c00a90cb5aea54c7a37c95f8f3413faaf.tar.zst
ECTester-4bf6a31c00a90cb5aea54c7a37c95f8f3413faaf.zip
Diffstat (limited to 'analysis/scalarmults/common.py')
-rw-r--r--analysis/scalarmults/common.py448
1 files changed, 0 insertions, 448 deletions
diff --git a/analysis/scalarmults/common.py b/analysis/scalarmults/common.py
deleted file mode 100644
index b307278..0000000
--- a/analysis/scalarmults/common.py
+++ /dev/null
@@ -1,448 +0,0 @@
-import itertools
-import hashlib
-from datetime import timedelta
-from enum import Enum
-from operator import itemgetter
-
-from dataclasses import dataclass
-from functools import partial, cached_property, total_ordering
-from typing import Any, Optional, Type, Union, Literal
-
-from statsmodels.stats.proportion import proportion_confint
-
-from pyecsca.sca.re.rpa import MultipleContext
-from pyecsca.ec.mult import *
-from pyecsca.ec.point import Point
-from pyecsca.ec.countermeasures import GroupScalarRandomization, AdditiveSplitting, MultiplicativeSplitting, EuclideanSplitting, BrumleyTuveri
-
-
-def check_equal_multiples(k, l, q):
- """Checks whether the two multiples input into the formula are equal modulo q (the order of the base)."""
- return (k % q) == (l % q)
-
-
-def check_divides(k, l, q):
- """Checks whether q (the order of the base) divides any of the multiples input into the formula."""
- return (k != 0) and (l != 0) and (k % q == 0) or (l % q == 0)
-
-
-def check_half_add(k, l, q):
- return (q % 2 == 0) and ((k-l) % (q//2)) == 0
-
-
-def check_affine(k, q):
- """Checks whether q (the order of the base) divides the multiple that is to be converted to affine."""
- return k % q == 0
-
-
-def check_any(*checks, q=None):
- """Merge multiple checks together. The returned check function no longer takes the `q` parameter."""
- def check_func(k, l):
- for check in checks:
- if check(k, l, q):
- return True
- return False
- return check_func
-
-
-# These checks can be applied to add formulas. See the formulas notebook for background on them.
-checks_add = {
- "equal_multiples": check_equal_multiples,
- "divides": check_divides,
- "half_add": check_half_add
-}
-
-# This check can be applied to conversion to affine.
-checks_affine = {
- "affine": check_affine
-}
-
-def powerset(iterable):
- """Take an iterable and create a powerset of its elements."""
- s = list(iterable)
- return map(set, itertools.chain.from_iterable(itertools.combinations(s, r) for r in range(len(s)+1)))
-
-
-@dataclass(frozen=True)
-@total_ordering
-class ErrorModel:
- """
- An ErrorModel describes the behavior of an implementation with regards to errors on exceptional
- inputs to its addition formulas, to-affine conversion or general scalar multiplication.
-
- :param checks: A set of names of checks (from checks_add and checks_affine) that the implementation performs.
- Note that these may not be checks that the implementation explicitly performs, only that it behaves w.r.t.
- errors as if it were doing these checks, due to the formulas it chose and any actual checks it has.
- :param check_condition: Either "all" or "necessary". Specifies whether the checks are applied to all points
- that the implementation computes during a scalar multiplication or only those that end up being used -- thus
- affect -- the final result. If an implementation does not perform any dummy operations, these two are the same.
- :param precomp_to_affine: Specifies whether the implementation converts all results of the precomputation step
- to affine form. If it does, it means that additional checks on all outputs of the precomputation are done as
- they have to be "convertible" to affine form.
- """
- checks: set[str]
- check_condition: Union[Literal["all"], Literal["necessary"]]
- precomp_to_affine: bool
-
- def __init__(self, checks: set[str], check_condition: Union[Literal["all"], Literal["necessary"]], precomp_to_affine: bool):
- for check in checks:
- if check not in checks_add:
- raise ValueError(f"Unknown check: {check}")
- checks = set(checks)
- checks.add("affine") # always done in our model
- object.__setattr__(self, "checks", checks)
- if check_condition not in ("all", "necessary"):
- raise ValueError("Wrong check_condition")
- object.__setattr__(self, "check_condition", check_condition)
- object.__setattr__(self, "precomp_to_affine", precomp_to_affine)
-
- def check_add(self, q):
- """Get the add formula check function for the given q."""
- if self.checks == {"affine"}:
- return lambda k, l: False
- return check_any(*map(lambda name: checks_add[name], filter(lambda check: check in checks_add, self.checks)), q=q)
-
- def check_affine(self, q):
- """Get the to-affine check function for the given q."""
- return partial(check_affine, q=q)
-
- def __lt__(self, other):
- if not isinstance(other, ErrorModel):
- return NotImplemented
- return str(self) < str(other)
-
- def __str__(self):
- cs = []
- if "equal_multiples" in self.checks:
- cs.append("em")
- if "divides" in self.checks:
- cs.append("d")
- if "half_add" in self.checks:
- cs.append("ha")
- if "affine" in self.checks:
- cs.append("a")
- precomp = "+pre" if self.precomp_to_affine else ""
- return f"({','.join(cs)}+{self.check_condition}{precomp})"
-
- def __hash__(self):
- return hash((tuple(sorted(self.checks)), self.check_condition, self.precomp_to_affine))
-
-
-# All error models are a simple cartesian product of the individual options.
-all_error_models = []
-for checks in powerset(checks_add):
- for precomp_to_affine in (True, False):
- for check_condition in ("all", "necessary"):
- error_model = ErrorModel(checks, check_condition=check_condition, precomp_to_affine=precomp_to_affine)
- all_error_models.append(error_model)
-
-
-@dataclass(frozen=True)
-@total_ordering
-class MultIdent:
- """
- A MultIdent is a description of a scalar multiplication implementation, consisting of a scalar multiplier,
- (optionally) a countermeasure, and (optionally) an error model.
-
- The scalar multiplier is defined by the `klass` attribute, along with the `args` and `kwargs` attributes.
- One can reconstruct the raw multiplier (without the countermeasure) by doing:
-
- klass(*args, **kwargs)
-
- The countermeasure is simply in the `countermeasure` attribute and may be `None`.
-
- The error model is simply in the `error_model` attribute and may be `None`. If it is `None`, the MultIdent
- is not suitable for error simulation and merely represents the description of a scalar multiplication
- implementation we care about when reverse-engineering: the multiplier and the countermeasure, we do not
- really care about the error model, yet need it when simulating.
- """
- klass: Type[ScalarMultiplier]
- args: list[Any]
- kwargs: dict[str, Any]
- countermeasure: Optional[str] = None
- error_model: Optional[ErrorModel] = None
-
- def __init__(self, klass: Type[ScalarMultiplier], *args, **kwargs):
- object.__setattr__(self, "klass", klass)
- object.__setattr__(self, "args", args if args is not None else [])
- if kwargs is not None and "countermeasure" in kwargs:
- object.__setattr__(self, "countermeasure", kwargs["countermeasure"])
- del kwargs["countermeasure"]
- if kwargs is not None and "error_model" in kwargs:
- object.__setattr__(self, "error_model", kwargs["error_model"])
- del kwargs["error_model"]
- object.__setattr__(self, "kwargs", kwargs if kwargs is not None else {})
-
- @cached_property
- def partial(self):
- """Get the callable that constructs the scalar multiplier (with countermeasure if any)."""
- func = partial(self.klass, *self.args, **self.kwargs)
- if self.countermeasure is None:
- return func
- if self.countermeasure == "gsr":
- return lambda *args, **kwargs: GroupScalarRandomization(func(*args, **kwargs))
- elif self.countermeasure == "additive":
- return lambda *args, **kwargs: AdditiveSplitting(func(*args, **kwargs))
- elif self.countermeasure == "multiplicative":
- return lambda *args, **kwargs: MultiplicativeSplitting(func(*args, **kwargs))
- elif self.countermeasure == "euclidean":
- return lambda *args, **kwargs: EuclideanSplitting(func(*args, **kwargs))
- elif self.countermeasure == "bt":
- return lambda *args, **kwargs: BrumleyTuveri(func(*args, **kwargs))
-
- def with_countermeasure(self, countermeasure: str | None):
- """Return a new MultIdent with a given countermeasure."""
- if countermeasure not in (None, "gsr", "additive", "multiplicative", "euclidean", "bt"):
- raise ValueError(f"Unknown countermeasure: {countermeasure}")
- return MultIdent(self.klass, *self.args, **self.kwargs, countermeasure=countermeasure)
-
- def with_error_model(self, error_model: ErrorModel | None):
- """Return a new MultIdent with a given error model."""
- if not (isinstance(error_model, ErrorModel) or error_model is None):
- raise ValueError("Unknown error model.")
- return MultIdent(self.klass, *self.args, **self.kwargs, countermeasure=self.countermeasure, error_model=error_model)
-
- def __str__(self):
- name = self.klass.__name__.replace("Multiplier", "")
- args = ("_" + ",".join(list(map(str, self.args)))) if self.args else ""
- kwmap = {"recoding_direction": "recode",
- "direction": "dir",
- "width": "w"}
- kwargs = ("_" + ",".join(f"{kwmap.get(k, k)}:{v.name if isinstance(v, Enum) else str(v)}" for k,v in self.kwargs.items())) if self.kwargs else ""
- countermeasure = f"+{self.countermeasure}" if self.countermeasure is not None else ""
- error_model = f"+{self.error_model}" if self.error_model is not None else ""
- return f"{name}{args}{kwargs}{countermeasure}{error_model}"
-
- def __getstate__(self):
- state = self.__dict__.copy()
- # Remove cached properties
- state.pop("partial", None)
- return state
-
- def __lt__(self, other):
- if not isinstance(other, MultIdent):
- return NotImplemented
- return str(self) < str(other)
-
- def __repr__(self):
- return str(self)
-
- def __hash__(self):
- return hash((self.klass, self.countermeasure, self.error_model, tuple(self.args), tuple(self.kwargs.keys()), tuple(self.kwargs.values())))
-
-
-@dataclass
-class MultResults:
- """
- A MultResults instance represents many simulated scalar multiplciation computations, which were tracked
- using a `MultipleContext` (i.e. the outputs of the :func:`pyecsca.sca.re.rpa.multiple_graph` function).
- Generally, these would be for one MultIdent only, but that should be handled separately, for example
- in a dict[MultIdent, MultResults]. The `samples` describe how many computations
- are contained and must correspond to the length of the `multiplications` list.
- """
- multiplications: list[tuple[MultipleContext, MultipleContext, Point]]
- samples: int
- duration: Optional[float] = None
-
- def merge(self, other: "MultResults"):
- self.multiplications.extend(other.multiplications)
- self.samples += other.samples
-
- def __len__(self):
- return self.samples
-
- def __iter__(self):
- yield from self.multiplications
-
- def __getitem__(self, i):
- return self.multiplications[i]
-
- def __str__(self):
- duration = timedelta(seconds=int(self.duration)) if self.duration is not None else ""
- return f"MultResults({self.samples},{duration})"
-
- def __repr__(self):
- return str(self)
-
-
-@dataclass
-class ProbMap:
- """
- A ProbMap is a mapping from integers (base point order q) to floats (error probability for some scalar
- multiplication implementation, i.e. MultIdent). The probability map is constructed for a given set of
- `divisors` (the base point orders q). Probability maps can be narrowed or merged. A narrowing restricts
- the probability map to a smaller set of `divisors`. A merging takes another probability map using the
- same divisor set and updates the probabilities to a weighted average of the two probability maps
- (the weight is the number of samples).
- """
- probs: dict[int, float]
- divisors_hash: bytes
- samples: int
-
- def __len__(self):
- return len(self.probs)
-
- def __iter__(self):
- yield from self.probs
-
- def __getitem__(self, i):
- return self.probs[i] if i in self.probs else 0.0
-
- def __contains__(self, item):
- return item in self.probs
-
- def keys(self):
- return self.probs.keys()
-
- def values(self):
- return self.probs.values()
-
- def items(self):
- return self.probs.items()
-
- def narrow(self, divisors: set[int]):
- """Narrow the probability map to the new set of divisors (must be a subset of the current set)."""
- divisors_hash = hashlib.blake2b(str(sorted(divisors)).encode(), digest_size=8).digest()
- if self.divisors_hash == divisors_hash:
- # Already narrow.
- return
- for kdel in set(self.probs.keys()).difference(divisors):
- del self.probs[kdel]
- self.divisors_hash = divisors_hash
-
- def merge(self, other: "ProbMap") -> None:
- """Merge the `other` probability map into this one (must share the divisor set)."""
- if self.divisors_hash != other.divisors_hash:
- raise ValueError("Merging can only work on probmaps created for same divisors.")
- new_keys = set(self.keys()).union(other.keys())
- result = {}
- for key in new_keys:
- sk = self[key]
- ok = other[key]
- result[key] = (sk * self.samples + ok * other.samples) / (self.samples + other.samples)
- self.probs = result
- self.samples += other.samples
-
-
-# All dbl-and-add multipliers from https://github.com/J08nY/pyecsca/blob/master/pyecsca/ec/mult
-window_mults = [
- MultIdent(SlidingWindowMultiplier, width=2, recoding_direction=ProcessingDirection.LTR),
- MultIdent(SlidingWindowMultiplier, width=3, recoding_direction=ProcessingDirection.LTR),
- MultIdent(SlidingWindowMultiplier, width=4, recoding_direction=ProcessingDirection.LTR),
- MultIdent(SlidingWindowMultiplier, width=5, recoding_direction=ProcessingDirection.LTR),
- MultIdent(SlidingWindowMultiplier, width=6, recoding_direction=ProcessingDirection.LTR),
- MultIdent(SlidingWindowMultiplier, width=2, recoding_direction=ProcessingDirection.RTL),
- MultIdent(SlidingWindowMultiplier, width=3, recoding_direction=ProcessingDirection.RTL),
- MultIdent(SlidingWindowMultiplier, width=4, recoding_direction=ProcessingDirection.RTL),
- MultIdent(SlidingWindowMultiplier, width=5, recoding_direction=ProcessingDirection.RTL),
- MultIdent(SlidingWindowMultiplier, width=6, recoding_direction=ProcessingDirection.RTL),
- MultIdent(FixedWindowLTRMultiplier, m=2**1),
- MultIdent(FixedWindowLTRMultiplier, m=2**2),
- MultIdent(FixedWindowLTRMultiplier, m=2**3),
- MultIdent(FixedWindowLTRMultiplier, m=2**4),
- MultIdent(FixedWindowLTRMultiplier, m=2**5),
- MultIdent(FixedWindowLTRMultiplier, m=2**6),
- MultIdent(WindowBoothMultiplier, width=2),
- MultIdent(WindowBoothMultiplier, width=3),
- MultIdent(WindowBoothMultiplier, width=4),
- MultIdent(WindowBoothMultiplier, width=5),
- MultIdent(WindowBoothMultiplier, width=6)
-]
-naf_mults = [
- MultIdent(WindowNAFMultiplier, width=2),
- MultIdent(WindowNAFMultiplier, width=3),
- MultIdent(WindowNAFMultiplier, width=4),
- MultIdent(WindowNAFMultiplier, width=5),
- MultIdent(WindowNAFMultiplier, width=6),
- MultIdent(BinaryNAFMultiplier, always=False, direction=ProcessingDirection.LTR),
- MultIdent(BinaryNAFMultiplier, always=False, direction=ProcessingDirection.RTL),
- MultIdent(BinaryNAFMultiplier, always=True, direction=ProcessingDirection.LTR),
- MultIdent(BinaryNAFMultiplier, always=True, direction=ProcessingDirection.RTL)
-]
-comb_mults = [
- MultIdent(CombMultiplier, width=2, always=True),
- MultIdent(CombMultiplier, width=3, always=True),
- MultIdent(CombMultiplier, width=4, always=True),
- MultIdent(CombMultiplier, width=5, always=True),
- MultIdent(CombMultiplier, width=6, always=True),
- MultIdent(CombMultiplier, width=2, always=False),
- MultIdent(CombMultiplier, width=3, always=False),
- MultIdent(CombMultiplier, width=4, always=False),
- MultIdent(CombMultiplier, width=5, always=False),
- MultIdent(CombMultiplier, width=6, always=False),
- MultIdent(BGMWMultiplier, width=2, direction=ProcessingDirection.LTR),
- MultIdent(BGMWMultiplier, width=3, direction=ProcessingDirection.LTR),
- MultIdent(BGMWMultiplier, width=4, direction=ProcessingDirection.LTR),
- MultIdent(BGMWMultiplier, width=5, direction=ProcessingDirection.LTR),
- MultIdent(BGMWMultiplier, width=6, direction=ProcessingDirection.LTR),
- MultIdent(BGMWMultiplier, width=2, direction=ProcessingDirection.RTL),
- MultIdent(BGMWMultiplier, width=3, direction=ProcessingDirection.RTL),
- MultIdent(BGMWMultiplier, width=4, direction=ProcessingDirection.RTL),
- MultIdent(BGMWMultiplier, width=5, direction=ProcessingDirection.RTL),
- MultIdent(BGMWMultiplier, width=6, direction=ProcessingDirection.RTL)
-]
-binary_mults = [
- MultIdent(LTRMultiplier, always=False, complete=True),
- MultIdent(LTRMultiplier, always=True, complete=True),
- MultIdent(LTRMultiplier, always=False, complete=False),
- MultIdent(LTRMultiplier, always=True, complete=False),
- MultIdent(RTLMultiplier, always=False, complete=True),
- MultIdent(RTLMultiplier, always=True, complete=True),
- MultIdent(RTLMultiplier, always=False, complete=False),
- MultIdent(RTLMultiplier, always=True, complete=False),
- MultIdent(CoronMultiplier)
-]
-other_mults = [
- MultIdent(FullPrecompMultiplier, always=False, complete=True),
- MultIdent(FullPrecompMultiplier, always=True, complete=True),
- MultIdent(FullPrecompMultiplier, always=False, complete=False),
- MultIdent(FullPrecompMultiplier, always=True, complete=False),
- MultIdent(SimpleLadderMultiplier, complete=True),
- MultIdent(SimpleLadderMultiplier, complete=False)
-]
-
-# We can enumerate all mults and countermeasures here.
-all_mults = window_mults + naf_mults + binary_mults + other_mults + comb_mults
-all_mults_with_ctr = [mult.with_countermeasure(ctr) for mult in all_mults for ctr in (None, "gsr", "additive", "multiplicative", "euclidean", "bt")]
-
-
-def powers_of(k, max_power=20):
- """Take all powers of `k` up to `max_power`."""
- return [k**i for i in range(1, max_power)]
-
-def prod_combine(one, other):
- """Multiply all pairs of elements from `one` and `other`."""
- return [a * b for a, b in itertools.product(one, other)]
-
-
-# We have several sets of divisors, inspired by various "interesting" multiples the multipliers may compute.
-small_primes = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199]
-medium_primes = [211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397]
-large_primes = [401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]
-all_integers = list(range(1, 400))
-all_even = list(range(2, 400, 2))
-all_odd = list(range(1, 400, 2))
-all_primes = small_primes + medium_primes + large_primes
-
-divisor_map = {
- "small_primes": small_primes,
- "medium_primes": medium_primes,
- "large_primes": large_primes,
- "all_primes": all_primes,
- "all_integers": all_integers,
- "all_even": all_even,
- "all_odd": all_odd,
- "powers_of_2": powers_of(2),
- "powers_of_2_large": powers_of(2, 256),
- "powers_of_2_large_3": [i * 3 for i in powers_of(2, 256)],
- "powers_of_2_large_p1": [i + 1 for i in powers_of(2, 256)],
- "powers_of_2_large_m1": [i - 1 for i in powers_of(2, 256)],
- "powers_of_2_large_pmautobus": sorted(set([i + j for i in powers_of(2, 256) for j in range(-5,5) if i+j > 0])),
- "powers_of_3": powers_of(3),
-}
-divisor_map["all"] = list(sorted(set().union(*[v for v in divisor_map.values()])))
-
-
-def conf_interval(p: float, samples: int, alpha: float = 0.05) -> tuple[float, float]:
- """Compute a confidence interval for a Binomial distribution."""
- return proportion_confint(round(p*samples), samples, alpha, method="wilson")