From 2ad8605e48ac95e656e32e5cfcbb2c2847a8b0ff Mon Sep 17 00:00:00 2001 From: J08nY Date: Fri, 3 Oct 2025 12:43:02 +0200 Subject: Allow for combinations of countermeasures. --- pyecsca/ec/countermeasures.py | 134 ++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 50 deletions(-) (limited to 'pyecsca') diff --git a/pyecsca/ec/countermeasures.py b/pyecsca/ec/countermeasures.py index 217e12d..eff10c2 100644 --- a/pyecsca/ec/countermeasures.py +++ b/pyecsca/ec/countermeasures.py @@ -21,18 +21,25 @@ class ScalarMultiplierCountermeasure(ABC): and provides some scalar-splitting countermeasure. """ - mult: ScalarMultiplier + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure" + """The underlying scalar multiplier (or another countermeasure).""" params: Optional[DomainParameters] + """The domain parameters, if any.""" point: Optional[Point] + """The point to multiply, if any.""" + bits: Optional[int] + """The bit-length to use, if any.""" - def __init__(self, mult: ScalarMultiplier): + def __init__(self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure"): self.mult = mult - def init(self, params: DomainParameters, point: Point): + def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None): """Initialize the countermeasure with the parameters and the point.""" self.params = params self.point = point - self.mult.init(self.params, self.point) + if bits is None: + bits = params.full_order.bit_length() + self.bits = bits @abstractmethod def multiply(self, scalar: int) -> Point: @@ -66,7 +73,11 @@ class GroupScalarRandomization(ScalarMultiplierCountermeasure): rand_bits: int - def __init__(self, mult: ScalarMultiplier, rand_bits: int = 32): + def __init__( + self, + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rand_bits: int = 32, + ): """ :param mult: The multiplier to use. :param rand_bits: How many random bits to sample. @@ -74,22 +85,19 @@ class GroupScalarRandomization(ScalarMultiplierCountermeasure): super().__init__(mult) self.rand_bits = rand_bits - def init(self, params: DomainParameters, point: Point): - self.params = params - self.point = point - self.mult.init( - self.params, - self.point, - bits=params.full_order.bit_length() + self.rand_bits, - ) - def multiply(self, scalar: int) -> Point: - if self.params is None or self.point is None: + if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: order = self.params.order mask = int(Mod.random(1 << self.rand_bits)) masked_scalar = scalar + mask * order + bits = max(self.bits, self.rand_bits + order.bit_length()) + 1 + self.mult.init( + self.params, + self.point, + bits=bits, + ) return action.exit(self.mult.multiply(masked_scalar)) @@ -110,7 +118,11 @@ class AdditiveSplitting(ScalarMultiplierCountermeasure): add: Optional[AdditionFormula] - def __init__(self, mult: ScalarMultiplier, add: Optional[AdditionFormula] = None): + def __init__( + self, + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + add: Optional[AdditionFormula] = None, + ): """ :param mult: The multiplier to use. :param add: Addition formula to use, if None, the formula from the multiplier is used. @@ -118,21 +130,29 @@ class AdditiveSplitting(ScalarMultiplierCountermeasure): super().__init__(mult) self.add = add + def _add(self, R: Point, S: Point) -> Point: # noqa + if self.add is None: + try: + return self.mult._add(R, S) # type: ignore + except AttributeError: + raise ValueError("No addition formula available.") + else: + return self.add( + self.params.curve.prime, R, S, **self.params.curve.parameters # type: ignore + )[0] + def multiply(self, scalar: int) -> Point: - if self.params is None or self.point is None: + if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: order = self.params.order r = Mod.random(order) s = scalar - r + bits = max(self.bits, order.bit_length()) + 1 + self.mult.init(self.params, self.point, bits) R = self.mult.multiply(int(r)) S = self.mult.multiply(int(s)) - if self.add is None: - res = self.mult._add(R, S) # noqa: This is OK. - else: - res = self.add( - self.params.curve.prime, R, S, **self.params.curve.parameters - )[0] + res = self._add(R, S) return action.exit(res) @@ -154,7 +174,11 @@ class MultiplicativeSplitting(ScalarMultiplierCountermeasure): rand_bits: int - def __init__(self, mult: ScalarMultiplier, rand_bits: int = 32): + def __init__( + self, + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rand_bits: int = 32, + ): """ :param mult: The multiplier to use. :param rand_bits: How many random bits to sample. @@ -163,13 +187,16 @@ class MultiplicativeSplitting(ScalarMultiplierCountermeasure): self.rand_bits = rand_bits def multiply(self, scalar: int) -> Point: - if self.params is None or self.point is None: + if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: r = Mod.random(1 << self.rand_bits) + self.mult.init(self.params, self.point, self.rand_bits) R = self.mult.multiply(int(r)) - self.mult.init(self.params, R) + self.mult.init( + self.params, R, max(self.bits, self.params.order.bit_length()) + ) kr_inv = scalar * mod(int(r), self.params.order).inverse() return action.exit(self.mult.multiply(int(kr_inv))) @@ -195,7 +222,11 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure): add: Optional[AdditionFormula] - def __init__(self, mult: ScalarMultiplier, add: Optional[AdditionFormula] = None): + def __init__( + self, + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + add: Optional[AdditionFormula] = None, + ): """ :param mult: The multiplier to use. :param add: Addition formula to use, if None, the formula from the multiplier is used. @@ -203,27 +234,34 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure): super().__init__(mult) self.add = add + def _add(self, R: Point, S: Point) -> Point: # noqa + if self.add is None: + try: + return self.mult._add(R, S) # type: ignore + except AttributeError: + raise ValueError("No addition formula available.") + else: + return self.add( + self.params.curve.prime, R, S, **self.params.curve.parameters # type: ignore + )[0] + def multiply(self, scalar: int) -> Point: - if self.params is None or self.point is None: + if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: - half_bits = self.params.order.bit_length() // 2 + half_bits = self.bits // 2 r = Mod.random(1 << half_bits) - R = self.mult.multiply(int(r)) + self.mult.init(self.params, self.point, half_bits) + R = self.mult.multiply(int(r)) # r bounded by half_bits k1 = scalar % int(r) k2 = scalar // int(r) - T = self.mult.multiply(k1) + T = self.mult.multiply(k1) # k1 bounded by half_bits - self.mult.init(self.params, R) - S = self.mult.multiply(k2) + self.mult.init(self.params, R, self.bits) + S = self.mult.multiply(k2) # k2 (in worst case) bounded by bits, but in practice closer to half_bits - if self.add is None: - res = self.mult._add(S, T) # noqa: This is OK. - else: - res = self.add( - self.params.curve.prime, S, T, **self.params.curve.parameters - )[0] + res = self._add(S, T) return action.exit(res) @@ -246,20 +284,16 @@ class BrumleyTuveri(ScalarMultiplierCountermeasure): """ - def init(self, params: DomainParameters, point: Point): - self.params = params - self.point = point - self.mult.init( - self.params, - self.point, - bits=params.full_order.bit_length() + 1, - ) - def multiply(self, scalar: int) -> Point: - if self.params is None or self.point is None: + if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: - n = self.params.full_order + n = self.params.order + self.mult.init( + self.params, + self.point, + bits=max(self.bits, n.bit_length()) + 1, + ) scalar += n if scalar.bit_length() <= n.bit_length(): scalar += n -- cgit v1.2.3-70-g09d2 From 9c2cba84bac32d3bbe3c6f946ee76dc416cd6bb8 Mon Sep 17 00:00:00 2001 From: J08nY Date: Fri, 3 Oct 2025 12:50:48 +0200 Subject: Better bound in additive split. --- pyecsca/ec/countermeasures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'pyecsca') diff --git a/pyecsca/ec/countermeasures.py b/pyecsca/ec/countermeasures.py index eff10c2..a310ce4 100644 --- a/pyecsca/ec/countermeasures.py +++ b/pyecsca/ec/countermeasures.py @@ -148,7 +148,7 @@ class AdditiveSplitting(ScalarMultiplierCountermeasure): order = self.params.order r = Mod.random(order) s = scalar - r - bits = max(self.bits, order.bit_length()) + 1 + bits = max(self.bits, order.bit_length()) self.mult.init(self.params, self.point, bits) R = self.mult.multiply(int(r)) S = self.mult.multiply(int(s)) -- cgit v1.2.3-70-g09d2 From 2f869828b04fd1d7601d93799a2361ec81e0f4e2 Mon Sep 17 00:00:00 2001 From: J08nY Date: Fri, 3 Oct 2025 13:05:10 +0200 Subject: Add a way to control randomness in coountermeasures. --- pyecsca/ec/countermeasures.py | 33 ++++++++++++++++--------- test/ec/test_countermeasures.py | 54 +++++++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 18 deletions(-) (limited to 'pyecsca') diff --git a/pyecsca/ec/countermeasures.py b/pyecsca/ec/countermeasures.py index a310ce4..86f7177 100644 --- a/pyecsca/ec/countermeasures.py +++ b/pyecsca/ec/countermeasures.py @@ -1,7 +1,7 @@ """Provides several countermeasures against side-channel attacks.""" from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, Callable from public import public @@ -30,8 +30,13 @@ class ScalarMultiplierCountermeasure(ABC): bits: Optional[int] """The bit-length to use, if any.""" - def __init__(self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure"): + def __init__( + self, + mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rng: Callable[[int], Mod] = Mod.random, + ): self.mult = mult + self.rng = rng def init(self, params: DomainParameters, point: Point, bits: Optional[int] = None): """Initialize the countermeasure with the parameters and the point.""" @@ -76,13 +81,14 @@ class GroupScalarRandomization(ScalarMultiplierCountermeasure): def __init__( self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rng: Callable[[int], Mod] = Mod.random, rand_bits: int = 32, ): """ :param mult: The multiplier to use. :param rand_bits: How many random bits to sample. """ - super().__init__(mult) + super().__init__(mult, rng) self.rand_bits = rand_bits def multiply(self, scalar: int) -> Point: @@ -90,7 +96,7 @@ class GroupScalarRandomization(ScalarMultiplierCountermeasure): raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: order = self.params.order - mask = int(Mod.random(1 << self.rand_bits)) + mask = int(self.rng(1 << self.rand_bits)) masked_scalar = scalar + mask * order bits = max(self.bits, self.rand_bits + order.bit_length()) + 1 self.mult.init( @@ -121,13 +127,14 @@ class AdditiveSplitting(ScalarMultiplierCountermeasure): def __init__( self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rng: Callable[[int], Mod] = Mod.random, add: Optional[AdditionFormula] = None, ): """ :param mult: The multiplier to use. :param add: Addition formula to use, if None, the formula from the multiplier is used. """ - super().__init__(mult) + super().__init__(mult, rng) self.add = add def _add(self, R: Point, S: Point) -> Point: # noqa @@ -146,7 +153,7 @@ class AdditiveSplitting(ScalarMultiplierCountermeasure): raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: order = self.params.order - r = Mod.random(order) + r = self.rng(order) s = scalar - r bits = max(self.bits, order.bit_length()) self.mult.init(self.params, self.point, bits) @@ -177,20 +184,21 @@ class MultiplicativeSplitting(ScalarMultiplierCountermeasure): def __init__( self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rng: Callable[[int], Mod] = Mod.random, rand_bits: int = 32, ): """ :param mult: The multiplier to use. :param rand_bits: How many random bits to sample. """ - super().__init__(mult) + super().__init__(mult, rng) self.rand_bits = rand_bits def multiply(self, scalar: int) -> Point: if self.params is None or self.point is None or self.bits is None: raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: - r = Mod.random(1 << self.rand_bits) + r = self.rng(1 << self.rand_bits) self.mult.init(self.params, self.point, self.rand_bits) R = self.mult.multiply(int(r)) @@ -225,13 +233,14 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure): def __init__( self, mult: "ScalarMultiplier | ScalarMultiplierCountermeasure", + rng: Callable[[int], Mod] = Mod.random, add: Optional[AdditionFormula] = None, ): """ :param mult: The multiplier to use. :param add: Addition formula to use, if None, the formula from the multiplier is used. """ - super().__init__(mult) + super().__init__(mult, rng) self.add = add def _add(self, R: Point, S: Point) -> Point: # noqa @@ -250,7 +259,7 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure): raise ValueError("Not initialized.") with ScalarMultiplicationAction(self.point, self.params, scalar) as action: half_bits = self.bits // 2 - r = Mod.random(1 << half_bits) + r = self.rng(1 << half_bits) self.mult.init(self.params, self.point, half_bits) R = self.mult.multiply(int(r)) # r bounded by half_bits @@ -259,7 +268,9 @@ class EuclideanSplitting(ScalarMultiplierCountermeasure): T = self.mult.multiply(k1) # k1 bounded by half_bits self.mult.init(self.params, R, self.bits) - S = self.mult.multiply(k2) # k2 (in worst case) bounded by bits, but in practice closer to half_bits + S = self.mult.multiply( + k2 + ) # k2 (in worst case) bounded by bits, but in practice closer to half_bits res = self._add(S, T) return action.exit(res) diff --git a/test/ec/test_countermeasures.py b/test/ec/test_countermeasures.py index 5d059f9..8365be0 100644 --- a/test/ec/test_countermeasures.py +++ b/test/ec/test_countermeasures.py @@ -10,6 +10,7 @@ from pyecsca.ec.countermeasures import ( EuclideanSplitting, BrumleyTuveri, ) +from pyecsca.ec.mod import mod from pyecsca.ec.mult import * @@ -245,12 +246,15 @@ def test_brumley_tuveri(mults, secp128r1, num): assert raw.equals(masked) -@pytest.mark.parametrize("scalar", [ - 3253857902090173296443513219124437746, - 1234567893141592653589793238464338327, - 86728612699079982903603364383639280149, - 60032993417060801067503559426926851620 -]) +@pytest.mark.parametrize( + "scalar", + [ + 3253857902090173296443513219124437746, + 1234567893141592653589793238464338327, + 86728612699079982903603364383639280149, + 60032993417060801067503559426926851620, + ], +) @pytest.mark.parametrize( "one,two", product( @@ -269,7 +273,7 @@ def test_combination(scalar, one, two, secp128r1): pytest.skip("Skip identical combinations.") mult = LTRMultiplier( secp128r1.curve.coordinate_model.formulas["add-2015-rcb"], - secp128r1.curve.coordinate_model.formulas["dbl-2015-rcb"] + secp128r1.curve.coordinate_model.formulas["dbl-2015-rcb"], ) mult.init(secp128r1, secp128r1.generator) raw = mult.multiply(scalar) @@ -288,3 +292,39 @@ def test_combination(scalar, one, two, secp128r1): combo.init(secp128r1, secp128r1.generator) masked = combo.multiply(scalar) assert raw.equals(masked) + + +@pytest.mark.parametrize( + "scalar", + [ + 3253857902090173296443513219124437746, + 1234567893141592653589793238464338327, + 86728612699079982903603364383639280149, + 60032993417060801067503559426926851620, + ], +) +@pytest.mark.parametrize( + "ctr", + ( + GroupScalarRandomization, + AdditiveSplitting, + EuclideanSplitting, + MultiplicativeSplitting, + BrumleyTuveri, + ), +) +def test_rng(scalar, ctr, secp128r1): + mult = LTRMultiplier( + secp128r1.curve.coordinate_model.formulas["add-2015-rcb"], + secp128r1.curve.coordinate_model.formulas["dbl-2015-rcb"], + ) + mult.init(secp128r1, secp128r1.generator) + raw = mult.multiply(scalar) + + def rng(n): + return mod(123456789, n) + + m = ctr(mult, rng) + m.init(secp128r1, secp128r1.generator) + masked = m.multiply(scalar) + assert raw.equals(masked) -- cgit v1.2.3-70-g09d2