aboutsummaryrefslogtreecommitdiff
path: root/pyecsca/ec/params.py
blob: 81888c0943c675d9688c4539fec2657789abc64a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
"""
This module provides functions for obtaining domain parameters from the `std-curves <https://github.com/J08nY/std-curves>`_ repository.

It also provides a domain parameter class and a class for a whole category of domain parameters.
"""
import json
from sympy import Poly, FF, symbols, sympify
from astunparse import unparse
from io import RawIOBase, BufferedIOBase
from os.path import join
from pathlib import Path
from typing import Optional, Dict, Union, BinaryIO, List, Callable

from pkg_resources import resource_listdir, resource_isdir, resource_stream
from public import public

from .coordinates import AffineCoordinateModel, CoordinateModel
from .curve import EllipticCurve
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 ..misc.cfg import getconfig


@public
class DomainParameters(object):
    """Domain parameters which specify a subgroup on an elliptic curve."""

    curve: EllipticCurve
    generator: Point
    order: int
    cofactor: int
    name: Optional[str]
    category: Optional[str]

    def __init__(
        self,
        curve: EllipticCurve,
        generator: Point,
        order: int,
        cofactor: int,
        name: Optional[str] = None,
        category: Optional[str] = None,
    ):
        self.curve = curve
        self.generator = generator
        self.order = order
        self.cofactor = cofactor
        self.name = name
        self.category = category

    def __eq__(self, other):
        if not isinstance(other, DomainParameters):
            return False
        return (
            self.curve == other.curve
            and self.generator == other.generator
            and self.order == other.order
            and self.cofactor == other.cofactor
        )

    def __get_name(self):
        if self.name and self.category:
            return f"{self.category}/{self.name}"
        elif self.name:
            return self.name
        elif self.category:
            return self.category
        return ""

    def __str__(self):
        name = self.__get_name()
        if not name:
            name = str(self.curve)
        return f"{self.__class__.__name__}({name})"

    def __repr__(self):
        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.")
    if curve["field"]["type"] == "Extension":
        raise ValueError("Extension field curves are currently not supported.")

    # Get model and param names
    model: CurveModel
    field = int(curve["field"]["p"], 16)
    order = int(curve["order"], 16)
    cofactor = int(curve["cofactor"], 16)
    if curve["form"] == "Weierstrass":
        model = ShortWeierstrassModel()
        param_names = ["a", "b"]
    elif curve["form"] == "Montgomery":
        model = MontgomeryModel()
        param_names = ["a", "b"]
    elif curve["form"] == "Edwards":
        model = EdwardsModel()
        param_names = ["c", "d"]
    elif curve["form"] == "TwistedEdwards":
        model = TwistedEdwardsModel()
        param_names = ["a", "d"]
    else:
        raise ValueError("Unknown curve model.")
    params = {
        name: Mod(int(curve["params"][name]["raw"], 16), field) for name in param_names
    }

    # Check coordinate model name and assumptions
    coord_model: CoordinateModel
    if coords == "affine":
        coord_model = AffineCoordinateModel(model)
    else:
        if coords not in model.coordinates:
            raise ValueError("Coordinate model not supported for curve.")
        coord_model = model.coordinates[coords]
        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.
            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_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)
                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 an unsupported assumption ({assumption_string})."
                    )
                poly = Poly(expr, symbols(param), domain=k)
                roots = poly.ground_roots()
                for root in roots:
                    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})."
                    )

    # Construct the point at infinity
    infinity: Point
    if infty:
        infinity = InfinityPoint(coord_model)
    else:
        ilocals: Dict[str, Union[Mod, int]] = {**params}
        for line in coord_model.neutral:
            compiled = compile(line, "", mode="exec")
            exec(compiled, None, ilocals)
        infinity_coords = {}
        for coordinate in coord_model.variables:
            if coordinate not in ilocals:
                raise ValueError(
                    f"Coordinate model {coord_model} requires infty option."
                )
            value = ilocals[coordinate]
            if isinstance(value, int):
                value = Mod(value, field)
            infinity_coords[coordinate] = value
        infinity = Point(coord_model, **infinity_coords)
    elliptic_curve = EllipticCurve(model, coord_model, field, infinity, params)  # type: ignore[arg-type]
    affine = Point(
        AffineCoordinateModel(model),
        x=Mod(int(curve["generator"]["x"]["raw"], 16), field),
        y=Mod(int(curve["generator"]["y"]["raw"], 16), field),
    )
    if not isinstance(coord_model, AffineCoordinateModel):
        generator = affine.to_model(coord_model, elliptic_curve)
    else:
        generator = affine
    return DomainParameters(
        elliptic_curve, generator, order, cofactor, curve["name"], curve["category"]
    )


@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 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.
    """
    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)
    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:
    """
    Retrieve a category from the std-curves database at https://github.com/J08nY/std-curves.

    :param category: The category to retrieve.
    :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.
    """
    listing = resource_listdir(__name__, "std")
    categories = [
        entry for entry in listing if resource_isdir(__name__, join("std", entry))
    ]
    if category not in categories:
        raise ValueError(f"Category {category} not found.")
    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:
    """
    Retrieve a curve from a set of stored parameters.

    Uses the std-curves database at https://github.com/J08nY/std-curves.

    :param category: The category of the curve.
    :param name: The name of the curve.
    :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.
    """
    listing = resource_listdir(__name__, "std")
    categories = [
        entry for entry in listing if resource_isdir(__name__, join("std", entry))
    ]
    if category not in categories:
        raise ValueError(f"Category {category} not found.")
    json_path = join("std", category, "curves.json")
    with resource_stream(__name__, json_path) as f:
        category_json = json.load(f)
    for curve in category_json["curves"]:
        if curve["name"] == name:
            break
    else:
        raise ValueError(f"Curve {name} not found in category {category}.")

    return _create_params(curve, coords, infty)