aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJ08nY2020-12-17 23:41:23 +0100
committerJ08nY2020-12-17 23:41:23 +0100
commitc2ad6329efa5d0024dc0b281b86738fbadaf7b92 (patch)
tree30020f6bc3f014cd218770a0629b57b13b89249d
parente6d9e4882af80560d0353bcd5bd22b438e54c0d7 (diff)
downloadpyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.tar.gz
pyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.tar.zst
pyecsca-c2ad6329efa5d0024dc0b281b86738fbadaf7b92.zip
Make certain aspects of the library configurable.
Fixes #5.
-rw-r--r--pyecsca/cfg.py145
-rw-r--r--pyecsca/ec/error.py31
-rw-r--r--pyecsca/ec/formula.py9
-rw-r--r--pyecsca/ec/mod.py31
-rw-r--r--pyecsca/ec/params.py14
-rw-r--r--test/ec/test_formula.py5
-rw-r--r--test/ec/test_mod.py33
-rw-r--r--test/ec/test_params.py8
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):