diff options
| author | J08nY | 2020-12-17 23:41:23 +0100 |
|---|---|---|
| committer | J08nY | 2020-12-17 23:41:23 +0100 |
| commit | c2ad6329efa5d0024dc0b281b86738fbadaf7b92 (patch) | |
| tree | 30020f6bc3f014cd218770a0629b57b13b89249d | |
| parent | e6d9e4882af80560d0353bcd5bd22b438e54c0d7 (diff) | |
| download | pyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.tar.gz pyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.tar.zst pyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.zip | |
Make certain aspects of the library configurable.
Fixes #5.
| -rw-r--r-- | pyecsca/cfg.py | 145 | ||||
| -rw-r--r-- | pyecsca/ec/error.py | 31 | ||||
| -rw-r--r-- | pyecsca/ec/formula.py | 9 | ||||
| -rw-r--r-- | pyecsca/ec/mod.py | 31 | ||||
| -rw-r--r-- | pyecsca/ec/params.py | 14 | ||||
| -rw-r--r-- | test/ec/test_formula.py | 5 | ||||
| -rw-r--r-- | test/ec/test_mod.py | 33 | ||||
| -rw-r--r-- | test/ec/test_params.py | 8 |
8 files changed, 248 insertions, 28 deletions
diff --git a/pyecsca/cfg.py b/pyecsca/cfg.py new file mode 100644 index 0000000..b1c4007 --- /dev/null +++ b/pyecsca/cfg.py @@ -0,0 +1,145 @@ +from copy import deepcopy +from contextvars import ContextVar, Token + +from public import public + + +@public +class ECConfig(object): + """Configuration for the :py:mod:`pyecsca.ec` package.""" + _no_inverse_action: str = "error" + _non_residue_action: str = "error" + _unsatisfied_formula_assumption_action: str = "error" + _unsatisfied_coordinate_assumption_action: str = "error" + _mod_implementation: str = "gmp" + + @property + def no_inverse_action(self) -> str: + """ + The action to take when a non-invertible element is to be inverted. One of: + + - `"error"`: Raise :py:class:`pyecsca.ec.error.NonInvertibleError`. + - `"warning"`: Raise :py:class:`pyecsca.ec.error.NonInvertibleWarning`. + - `"ignore"`: Ignore the event and compute as if nothing happened.""" + return self._no_inverse_action + + @no_inverse_action.setter + def no_inverse_action(self, value: str): + if value not in ("error", "warning", "ignore"): + raise ValueError("Action has to be one of 'error', 'warning', 'ignore'.") + self._no_inverse_action = value + + @property + def non_residue_action(self) -> str: + """ + The action to take when a the square-root of a non-residue is to be computed. One of: + + - `"error"`: Raise :py:class:`pyecsca.ec.error.NonResidueError`. + - `"warning"`: Raise :py:class:`pyecsca.ec.error.NonResidueWarning`. + - `"ignore"`: Ignore the event and compute as if nothing happened.""" + return self._non_residue_action + + @non_residue_action.setter + def non_residue_action(self, value: str): + if value not in ("error", "warning", "ignore"): + raise ValueError("Action has to be one of 'error', 'warning', 'ignore'.") + self._non_residue_action = value + + @property + def unsatisfied_formula_assumption_action(self) -> str: + """ + The action to take when a formula assumption is unsatisfied during execution. + This works for assumption that can be ignored without a fatal error, + which are those that are not used to compute a value of an undefined parameter. + For example, things of the form `Z1 = 1`. + One of: + + - `"error"`: Raise :py:class:`pyecsca.ec.error.UnsatisfiedAssumptionError`. + - `"warning"`: Raise :py:class:`pyecsca.ec.error.UnsatisfiedAssumptionWarning`. + - `"ignore"`: Ignore the event and compute as if nothing happened. + """ + return self._unsatisfied_formula_assumption_action + + @unsatisfied_formula_assumption_action.setter + def unsatisfied_formula_assumption_action(self, value: str): + if value not in ("error", "warning", "ignore"): + raise ValueError("Action has to be one of 'error', 'warning', 'ignore'.") + self._unsatisfied_formula_assumption_action = value + + @property + def unsatisfied_coordinate_assumption_action(self) -> str: + """ + The action to take when a coordinate assumption is unsatisfied during curve creation. + This works for assumption that can be ignored without a fatal error, + which are those that are not used to compute a value of an undefined parameter. + For example, things of the form `a = -1`. + One of: + + - `"error"`: Raise :py:class:`pyecsca.ec.error.UnsatisfiedAssumptionError`. + - `"warning"`: Raise :py:class:`pyecsca.ec.error.UnsatisfiedAssumptionWarning`. + - `"ignore"`: Ignore the event and compute as if nothing happened. + """ + return self._unsatisfied_coordinate_assumption_action + + @unsatisfied_coordinate_assumption_action.setter + def unsatisfied_coordinate_assumption_action(self, value: str): + if value not in ("error", "warning", "ignore"): + raise ValueError("Action has to be one of 'error', 'warning', 'ignore'.") + self._unsatisfied_coordinate_assumption_action = value + + @property + def mod_implementation(self) -> str: + """ + The selected :py:class:`pyecsca.ec.mod.Mod` implementation. One of: + + - `"gmp"`: Requires the GMP library and `gmpy2` package. + - `"python"`: Doesn't require anything. + """ + return self._mod_implementation + + @mod_implementation.setter + def mod_implementation(self, value: str): + if value not in ("python", "gmp"): + raise ValueError(f"Bad Mod implementaiton, can be one of 'python' or 'gmp'.") + self._mod_implementation = value + + +@public +class Config(object): + """A runtime configuration for the library.""" + ec: ECConfig + """Configuration for the :py:mod:`pyecsca.ec` package.""" + + def __init__(self): + self.ec = ECConfig() + + +_config: ContextVar[Config] = ContextVar("config", default=Config()) + + +@public +def getconfig() -> Config: + return _config.get() + + +@public +def setconfig(cfg: Config) -> Token: + return _config.set(cfg) + + +@public +def resetconfig(token: Token): + _config.reset(token) + + +@public +class TemporaryConfig(object): + def __init__(self): + self.new_config = deepcopy(getconfig()) + + def __enter__(self) -> Config: + self.token = setconfig(self.new_config) + return self.new_config + + def __exit__(self, t, v, tb): + resetconfig(self.token)
\ No newline at end of file diff --git a/pyecsca/ec/error.py b/pyecsca/ec/error.py index f350d9e..8aabe06 100644 --- a/pyecsca/ec/error.py +++ b/pyecsca/ec/error.py @@ -1,5 +1,5 @@ from public import public - +from ..cfg import getconfig @public class NonInvertibleError(ArithmeticError): @@ -11,6 +11,16 @@ class NonInvertibleWarning(UserWarning): pass +def raise_non_invertible(): + """Raise either :py:class:`NonInvertibleError` or :py:class:`NonInvertiblerWarning` or ignore. + Depends on the current config value of `no_inverse_action`.""" + cfg = getconfig() + if cfg.ec.no_inverse_action == "error": + raise NonInvertibleError("Element not invertible.") + elif cfg.ec.no_inverse_action == "warning": + raise NonInvertibleWarning("Element not invertible.") + + @public class NonResidueError(ArithmeticError): pass @@ -21,6 +31,16 @@ class NonResidueWarning(UserWarning): pass +def raise_non_residue(): + """Raise either :py:class:`NonResidueError` or :py:class:`NonResidueWarning` or ignore. + Depends on the current config value of `non_residue_action`.""" + cfg = getconfig() + if cfg.ec.non_residue_action == "error": + raise NonResidueError("No square root exists.") + elif cfg.ec.non_residue_action == "warning": + raise NonResidueWarning("No square root exists.") + + @public class UnsatisfiedAssumptionError(ValueError): pass @@ -29,3 +49,12 @@ class UnsatisfiedAssumptionError(ValueError): @public class UnsatisfiedAssumptionWarning(UserWarning): pass + + +def raise_unsatisified_assumption(action: str, msg: str): + """Raise either :py:class:`UnsatisfiedAssumptionError` or :py:class:`UnsatisfiedAssumptionWarning` or ignore. + Depends on the value of `action`.""" + if action == "error": + raise UnsatisfiedAssumptionError(msg) + elif action == "warning": + raise UnsatisfiedAssumptionWarning(msg) diff --git a/pyecsca/ec/formula.py b/pyecsca/ec/formula.py index 03681ec..741bb99 100644 --- a/pyecsca/ec/formula.py +++ b/pyecsca/ec/formula.py @@ -9,9 +9,10 @@ from public import public from sympy import sympify, FF, symbols, Poly from .context import ResultAction, getcontext, NullContext -from .error import UnsatisfiedAssumptionError +from .error import UnsatisfiedAssumptionError, raise_unsatisified_assumption from .mod import Mod from .op import CodeOp, OpType +from ..cfg import getconfig @public @@ -131,7 +132,7 @@ class Formula(ABC): for coord, value in point.coords.items(): params[coord + str(i + 1)] = value # Validate assumptions and compute formula parameters. - field = int(params[next(iter(params.keys()))].n) # This is nasty... + field = int(params[next(iter(params.keys()))].n) # TODO: This is nasty... for assumption in self.assumptions: assumption_string = unparse(assumption)[1:-2] lhs, rhs = assumption_string.split(" == ") @@ -141,7 +142,9 @@ class Formula(ABC): compiled = compile(assumption, "", mode="eval") holds = eval(compiled, None, alocals) if not holds: - raise UnsatisfiedAssumptionError(f"Unsatisfied assumption in the formula ({assumption_string}).") + # The assumption doesn't hold, see what is the current configured action and do it. + raise_unsatisified_assumption(getconfig().ec.unsatisfied_formula_assumption_action, + f"Unsatisfied assumption in the formula ({assumption_string}).") else: k = FF(field) expr = sympify(f"{rhs} - {lhs}") diff --git a/pyecsca/ec/mod.py b/pyecsca/ec/mod.py index 65018f7..3943b0c 100644 --- a/pyecsca/ec/mod.py +++ b/pyecsca/ec/mod.py @@ -4,9 +4,9 @@ from functools import wraps, lru_cache from abc import abstractmethod from public import public -from .error import NonInvertibleError, NonResidueError +from .error import raise_non_invertible, raise_non_residue from .context import ResultAction - +from ..cfg import getconfig has_gmp = False try: @@ -104,7 +104,7 @@ class RandomModAction(ResultAction): return f"{self.__class__.__name__}({self.order:x})" -_mod_classes = [] +_mod_classes = {} @public @@ -114,8 +114,12 @@ class Mod(object): if cls != Mod: return cls.__new__(cls, *args, **kwargs) if not _mod_classes: - raise ValueError("Cannot find a working Mod class.") - return _mod_classes[-1].__new__(_mod_classes[-1], *args, **kwargs) + raise ValueError("Cannot find any working Mod class.") + selected_class = getconfig().ec.mod_implementation + if selected_class not in _mod_classes: + # Fallback to something + selected_class = next(iter(_mod_classes.keys())) + return _mod_classes[selected_class].__new__(_mod_classes[selected_class], *args, **kwargs) def __init__(self, x, n): self.x = x @@ -245,10 +249,10 @@ class RawMod(Mod): def inverse(self): if self.x == 0: - raise NonInvertibleError("Inverting zero.") + raise_non_invertible() x, y, d = extgcd(self.x, self.n) if d != 1: - raise NonInvertibleError("Element not invertible.") + raise_non_invertible() return RawMod(x, self.n) def is_residue(self): @@ -267,7 +271,7 @@ class RawMod(Mod): if self.x == 0: return RawMod(0, self.n) if not self.is_residue(): - raise NonResidueError("No square root exists.") + raise_non_residue() if self.n % 4 == 3: return self ** int((self.n + 1) // 4) q = self.n - 1 @@ -330,7 +334,7 @@ class RawMod(Mod): return RawMod(pow(self.x, n, self.n), self.n) -_mod_classes.append(RawMod) +_mod_classes["python"] = RawMod @public @@ -430,13 +434,14 @@ if has_gmp: def inverse(self): if self.x == 0: - raise NonInvertibleError("Inverting zero!") + raise_non_invertible() if self.x == 1: return GMPMod(1, self.n) try: res = gmpy2.invert(self.x, self.n) except ZeroDivisionError: - raise NonInvertibleError("Element not invertible.") + raise_non_invertible() + res = 0 return GMPMod(res, self.n) def is_residue(self): @@ -460,7 +465,7 @@ if has_gmp: if self.x == 0: return GMPMod(0, self.n) if not self.is_residue(): - raise NonResidueError("No square root exists.") + raise_non_residue() if self.n % 4 == 3: return self ** int((self.n + 1) // 4) q = self.n - 1 @@ -527,4 +532,4 @@ if has_gmp: return GMPMod(gmpy2.powmod(self.x, gmpy2.mpz(n), self.n), self.n) - _mod_classes.append(GMPMod) + _mod_classes["gmp"] = GMPMod diff --git a/pyecsca/ec/params.py b/pyecsca/ec/params.py index 2ec9706..b9495b8 100644 --- a/pyecsca/ec/params.py +++ b/pyecsca/ec/params.py @@ -11,11 +11,12 @@ from public import public from .coordinates import AffineCoordinateModel, CoordinateModel from .curve import EllipticCurve -from .error import UnsatisfiedAssumptionError +from .error import UnsatisfiedAssumptionError, raise_unsatisified_assumption from .mod import Mod from .model import (CurveModel, ShortWeierstrassModel, MontgomeryModel, EdwardsModel, TwistedEdwardsModel) from .point import Point, InfinityPoint +from ..cfg import getconfig @public @@ -127,15 +128,15 @@ def _create_params(curve, coords, infty): for assumption in coord_model.assumptions: # Try to execute assumption, if it works, check with curve parameters # if it doesn't work, move all over to rhs and construct a sympy polynomial of it - # then find roots and take first one for new value for new coordinate parameter. + # then find roots and take first one for new value for new coordinate parameter. try: alocals: Dict[str, Union[Mod, int]] = {} compiled = compile(assumption, "", mode="exec") exec(compiled, None, alocals) for param, value in alocals.items(): if params[param] != value: - raise UnsatisfiedAssumptionError( - f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (= {value}).") + raise_unsatisified_assumption(getconfig().ec.unsatisfied_coordinate_assumption_action, + f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (= {value}).") except NameError: k = FF(field) assumption_string = unparse(assumption) @@ -148,9 +149,8 @@ def _create_params(curve, coords, infty): poly = Poly(expr, symbols(param), domain=k) roots = poly.ground_roots() for root in roots.keys(): - if root >= 0: - params[param] = Mod(int(root), field) - break + params[param] = Mod(int(root), field) + break else: raise UnsatisfiedAssumptionError(f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (0 = {expr}).") diff --git a/test/ec/test_formula.py b/test/ec/test_formula.py index ed4e6de..3353426 100644 --- a/test/ec/test_formula.py +++ b/test/ec/test_formula.py @@ -1,5 +1,6 @@ from unittest import TestCase +from pyecsca.cfg import TemporaryConfig from pyecsca.ec.error import UnsatisfiedAssumptionError from pyecsca.ec.params import get_params from pyecsca.ec.point import Point @@ -50,6 +51,10 @@ class FormulaTests(TestCase): other = Point(self.secp128r1.generator.coordinate_model, **coords) with self.assertRaises(UnsatisfiedAssumptionError): self.mdbl(other, **self.secp128r1.curve.parameters) + with TemporaryConfig() as cfg: + cfg.ec.unsatisfied_formula_assumption_action = "ignore" + pt = self.mdbl(other, **self.secp128r1.curve.parameters) + self.assertIsNotNone(pt) def test_parameters(self): res = self.jac_dbl(self.jac_secp128r1.generator, **self.jac_secp128r1.curve.parameters) diff --git a/test/ec/test_mod.py b/test/ec/test_mod.py index bf08cd9..aafef43 100644 --- a/test/ec/test_mod.py +++ b/test/ec/test_mod.py @@ -1,7 +1,8 @@ from unittest import TestCase -from pyecsca.ec.mod import Mod, gcd, extgcd, Undefined, miller_rabin -from pyecsca.ec.error import NonInvertibleError, NonResidueError +from pyecsca.ec.mod import Mod, gcd, extgcd, Undefined, miller_rabin, has_gmp, RawMod +from pyecsca.ec.error import NonInvertibleError, NonResidueError, NonInvertibleWarning, NonResidueWarning +from pyecsca.cfg import getconfig, TemporaryConfig class ModTests(TestCase): @@ -26,6 +27,15 @@ class ModTests(TestCase): Mod(0, p).inverse() with self.assertRaises(NonInvertibleError): Mod(5, 10).inverse() + getconfig().ec.no_inverse_action = "warning" + with self.assertRaises(NonInvertibleWarning): + Mod(0, p).inverse() + with self.assertRaises(NonInvertibleWarning): + Mod(5, 10).inverse() + getconfig().ec.no_inverse_action = "ignore" + Mod(0, p).inverse() + Mod(5, 10).inverse() + getconfig().ec.no_inverse_action = "error" def test_is_residue(self): self.assertTrue(Mod(4, 11).is_residue()) @@ -38,9 +48,19 @@ class ModTests(TestCase): self.assertIn(Mod(0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc, p).sqrt(), (0x9add512515b70d9ec471151c1dec46625cd18b37bde7ca7fb2c8b31d7033599d, 0x6522aed9ea48f2623b8eeae3e213b99da32e74c9421835804d374ce28fcca662)) with self.assertRaises(NonResidueError): Mod(0x702bdafd3c1c837b23a1cb196ed7f9fadb333c5cfe4a462be32adcd67bfb6ac1, p).sqrt() + getconfig().ec.non_residue_action = "warning" + with self.assertRaises(NonResidueWarning): + Mod(0x702bdafd3c1c837b23a1cb196ed7f9fadb333c5cfe4a462be32adcd67bfb6ac1, p).sqrt() + getconfig().ec.non_residue_action = "ignore" + Mod(0x702bdafd3c1c837b23a1cb196ed7f9fadb333c5cfe4a462be32adcd67bfb6ac1, p).sqrt() + with TemporaryConfig() as cfg: + cfg.ec.non_residue_action = "warning" + with self.assertRaises(NonResidueWarning): + Mod(0x702bdafd3c1c837b23a1cb196ed7f9fadb333c5cfe4a462be32adcd67bfb6ac1, p).sqrt() self.assertEqual(Mod(0, p).sqrt(), Mod(0, p)) q = 0x75d44fee9a71841ae8403c0c251fbad self.assertIn(Mod(0x591e0db18cf1bd81a11b2985a821eb3, q).sqrt(), (0x113b41a1a2b73f636e73be3f9a3716e, 0x64990e4cf7ba44b779cc7dcc8ae8a3f)) + getconfig().ec.non_residue_action = "error" def test_eq(self): self.assertEqual(Mod(1, 7), 1) @@ -98,4 +118,11 @@ class ModTests(TestCase): assert not meth(u, *args) else: with self.assertRaises(NotImplementedError): - meth(u, *args)
\ No newline at end of file + meth(u, *args) + + def test_implementation(self): + if not has_gmp: + self.skipTest("Only makes sense if more Mod implementations are available.") + with TemporaryConfig() as cfg: + cfg.ec.mod_implementation = "python" + self.assertIsInstance(Mod(5, 7), RawMod) diff --git a/test/ec/test_params.py b/test/ec/test_params.py index 77f5418..f4b647c 100644 --- a/test/ec/test_params.py +++ b/test/ec/test_params.py @@ -2,7 +2,9 @@ from unittest import TestCase from parameterized import parameterized +from pyecsca.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 @@ -63,8 +65,12 @@ class DomainParameterTests(TestCase): get_params(*name.split("/"), coords) def test_assumption(self): - with self.assertRaises(ValueError): + with self.assertRaises(UnsatisfiedAssumptionError): get_params("secg", "secp128r1", "projective-1") + with TemporaryConfig() as cfg: + cfg.ec.unsatisfied_coordinate_assumption_action = "ignore" + params = get_params("secg", "secp128r1", "projective-1") + self.assertIsNotNone(params) self.assertIsNotNone(get_params("secg", "secp128r1", "projective-3")) def test_infty(self): |
