diff options
| -rw-r--r-- | Pipfile | 28 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | docs/index.rst | 2 | ||||
| -rw-r--r-- | pyecsca/ec/curve.py | 2 | ||||
| -rw-r--r-- | pyecsca/ec/params.py | 130 | ||||
| -rw-r--r-- | setup.py | 1 | ||||
| -rw-r--r-- | test/data/curves.json | 37 | ||||
| -rw-r--r-- | test/ec/test_params.py | 13 |
8 files changed, 170 insertions, 44 deletions
diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 201a0a9..0000000 --- a/Pipfile +++ /dev/null @@ -1,28 +0,0 @@ -[[source]] -name = "pypi" -url = "https://pypi.org/simple" -verify_ssl = true - -[dev-packages] -nose2 = "*" -green = "*" -mypy = "*" -flake8 = "*" -sphinx = "*" -sphinx-autodoc-typehints = "*" -parameterized = "*" -coverage = "*" - -[packages] -numpy = "*" -scipy = "*" -atpublic = "*" -matplotlib = "*" -cython = "*" -fastdtw = "*" -asn1crypto = "*" -h5py = "*" -bokeh = "*" - -[requires] -python_version = "3.8" @@ -18,6 +18,7 @@ and ECC simulation in the [*pyecsca.ec*](pyecsca/ec) package. - [Numpy](https://www.numpy.org/) - [Scipy](https://www.scipy.org/) + - [sympy](https://sympy.org/) - [atpublic](https://public.readthedocs.io/) - [fastdtw](https://github.com/slaypni/fastdtw) - [asn1crypto](https://github.com/wbond/asn1crypto) diff --git a/docs/index.rst b/docs/index.rst index dd0bc67..6052cf5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -63,6 +63,7 @@ Requirements - Numpy_ - Scipy_ + - sympy_ - atpublic_ - fastdtw_ - asn1crypto_ @@ -139,6 +140,7 @@ this support is very appreciated. .. _Numpy: https://www.numpy.org .. _Scipy: https://www.scipy.org +.. _sympy: https://sympy.org/ .. _matplotlib: https://matplotlib.org/ .. _atpublic: https://public.readthedocs.io/ .. _fastdtw: https://github.com/slaypni/fastdtw diff --git a/pyecsca/ec/curve.py b/pyecsca/ec/curve.py index 9321826..44358d0 100644 --- a/pyecsca/ec/curve.py +++ b/pyecsca/ec/curve.py @@ -23,7 +23,7 @@ class EllipticCurve(object): prime: int, neutral: Point, parameters: MutableMapping[str, Union[Mod, int]]): if coordinate_model not in model.coordinates.values() and not isinstance(coordinate_model, AffineCoordinateModel): raise ValueError - if set(model.parameter_names).symmetric_difference(parameters.keys()): + if set(model.parameter_names).union(coordinate_model.parameters).symmetric_difference(parameters.keys()): raise ValueError self.model = model self.coordinate_model = coordinate_model diff --git a/pyecsca/ec/params.py b/pyecsca/ec/params.py index fea5988..0522e7b 100644 --- a/pyecsca/ec/params.py +++ b/pyecsca/ec/params.py @@ -1,8 +1,10 @@ import json +from sympy import Poly, PythonFiniteField, symbols, sympify +from ast import unparse from io import RawIOBase, BufferedIOBase from os.path import join from pathlib import Path -from typing import Optional, Dict, Union, BinaryIO +from typing import Optional, Dict, Union, BinaryIO, List, Callable from pkg_resources import resource_listdir, resource_isdir, resource_stream from public import public @@ -58,6 +60,34 @@ class DomainParameters(object): return f"{self.__class__.__name__}({self.curve!r}, {self.generator!r}, {self.order}, {self.cofactor})" +@public +class DomainParameterCategory(object): + """A category of domain parameters.""" + name: str + description: str + curves: List[DomainParameters] + + def __init__(self, name: str, description: str, curves: List[DomainParameters]): + self.name = name + self.description = description + self.curves = curves + + def __str__(self): + return f"{self.__class__.__name__}({self.name})" + + def __iter__(self): + yield from self.curves + + def __contains__(self, item): + return item in self.curves + + def __len__(self): + return len(self.curves) + + def __getitem__(self, item): + return self.curves[item] + + def _create_params(curve, coords, infty): if curve["field"]["type"] == "Binary": raise ValueError("Binary field curves are currently not supported.") @@ -94,13 +124,34 @@ def _create_params(curve, coords, infty): raise ValueError("Coordinate model not supported for curve.") coord_model = model.coordinates[coords] for assumption in coord_model.assumptions: - 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 ValueError( - f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (= {value}).") + # 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. + 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 ValueError( + f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (= {value}).") + except NameError: + k = PythonFiniteField(field) + assumption_string = unparse(assumption) + lhs, rhs = assumption_string.split(" = ") + expr = sympify(f"{rhs} - {lhs}") + for curve_param, value in params.items(): + expr = expr.subs(curve_param, k(value)) + if len(expr.free_symbols) > 1 or (param := str(expr.free_symbols.pop())) not in coord_model.parameters: + raise ValueError(f"This coordinate model couldn't be loaded due to unsupported asusmption ({assumption_string}).") + 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 + else: + raise ValueError(f"Coordinate model {coord_model} has an unsatisifed assumption on the {param} parameter (0 = {expr}).") # Construct the point at infinity infinity: Point @@ -132,28 +183,79 @@ def _create_params(curve, coords, infty): @public +def load_category(file: Union[str, Path, BinaryIO], coords: Union[str, Callable[[str], str]], + infty: Union[bool, Callable[[str], bool]] = True) -> DomainParameterCategory: + """ + Load a category of domain parameters containing several curves from a JSON file. + + :param file: The file to load from. + :param coords: The name of the coordinate system to use. Can be a callable that takes + as argument the name of the curve and produces the coordinate system to use for that curve. + :param infty: Whether to use the special :py:class:InfinityPoint (`True`) or try to use the + point at infinity of the coordinate system. Can be a callable that takes + as argument the name of the curve and returns the infinity option to use for that curve. + :return: The category. + """ + if isinstance(file, (str, Path)): + with open(file, "rb") as f: + data = json.load(f) + elif isinstance(file, (RawIOBase, BufferedIOBase, BinaryIO)): + data = json.load(file) + else: + raise TypeError + + curves = [] + for curve_data in data["curves"]: + curve_coords = coords(curve_data["name"]) if callable(coords) else coords + curve_infty = infty(curve_data["name"]) if callable(infty) else infty + try: + curve = _create_params(curve_data, curve_coords, curve_infty) + except ValueError: + continue + curves.append(curve) + + return DomainParameterCategory(data["name"], data["desc"], curves) + + +@public def load_params(file: Union[str, Path, BinaryIO], coords: str, infty: bool = True) -> DomainParameters: """ + Load a curve from a JSON file. - :param input: + :param file: The file to load from. :param coords: The name of the coordinate system to use. :param infty: Whether to use the special :py:class:InfinityPoint (`True`) or try to use the point at infinity of the coordinate system. :return: The curve. """ - curve = None if isinstance(file, (str, Path)): with open(file, "rb") as f: curve = json.load(f) elif isinstance(file, (RawIOBase, BufferedIOBase, BinaryIO)): curve = json.load(file) - if curve["field"]["type"] == "Binary": - raise ValueError("Binary field curves are currently not supported.") - if curve["field"]["type"] == "Extension": - raise ValueError("Extension field curves are currently not supported.") + else: + raise TypeError return _create_params(curve, coords, infty) +@public +def get_category(category: str, coords: Union[str, Callable[[str], str]], + infty: Union[bool, Callable[[str], bool]] = True) -> DomainParameterCategory: + """ + + :param category: + :param coords: + :param infty: + :return: + """ + listing = resource_listdir(__name__, "std") + categories = list(entry for entry in listing if resource_isdir(__name__, join("std", entry))) + if category not in categories: + raise ValueError("Category {} not found.".format(category)) + json_path = join("std", category, "curves.json") + with resource_stream(__name__, json_path) as f: + return load_category(f, coords, infty) + @public def get_params(category: str, name: str, coords: str, infty: bool = True) -> DomainParameters: @@ -28,6 +28,7 @@ setup( install_requires=[ "numpy", "scipy", + "sympy", "atpublic", "cython", "fastdtw", diff --git a/test/data/curves.json b/test/data/curves.json new file mode 100644 index 0000000..978ab36 --- /dev/null +++ b/test/data/curves.json @@ -0,0 +1,37 @@ +{ + "name": "test", + "desc": "Test description", + "curves": [ + { + "form": "Edwards", + "name": "small-8bits-edwards-curve-c1-d-has-sqrt", + "category": "test", + "desc": "A small 8-bit Edwards curve with with c = 1 and d has sqrt.", + "field": { + "type": "Prime", + "p": "0xDF", + "bits": 8 + }, + "params": { + "c": { + "raw": "0x1" + }, + "d": { + "raw": "0xF" + } + }, + "generator": { + "x": { + "raw": "0xC3" + }, + "y": { + "raw": "0xB4" + } + }, + "r": "0x62", + "s": "0xB0", + "order": "0x1F0", + "cofactor": "0x1" + } + ] +} diff --git a/test/ec/test_params.py b/test/ec/test_params.py index 13e45ec..77f5418 100644 --- a/test/ec/test_params.py +++ b/test/ec/test_params.py @@ -3,7 +3,7 @@ from unittest import TestCase from parameterized import parameterized from pyecsca.ec.coordinates import AffineCoordinateModel -from pyecsca.ec.params import get_params, load_params +from pyecsca.ec.params import get_params, load_params, load_category, get_category class DomainParameterTests(TestCase): @@ -35,6 +35,13 @@ class DomainParameterTests(TestCase): except NotImplementedError: pass + @parameterized.expand([ + ("anssi", "projective"), + ("brainpool", lambda name: "projective" if name.endswith("r1") else "jacobian") + ]) + def test_get_category(self, name, coords): + get_category(name, coords) + def test_load_params(self): params = load_params("test/data/curve.json", "projective") try: @@ -42,6 +49,10 @@ class DomainParameterTests(TestCase): except NotImplementedError: pass + def test_load_category(self): + category = load_category("test/data/curves.json", "yz") + self.assertEqual(len(category), 1) + @parameterized.expand([ ("no_category/some", "else"), ("secg/no_curve", "else"), |
