diff --git a/.readthedocs.yml b/.readthedocs.yml index 704f96fe4..a42a41817 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,5 @@ python: - version: 3.5 + version: 3.8 pip_install: true extra_requirements: - docs diff --git a/docs/api/ristretto.rst b/docs/api/ristretto.rst new file mode 100644 index 000000000..d0429c9e9 --- /dev/null +++ b/docs/api/ristretto.rst @@ -0,0 +1,19 @@ +nacl.ristretto +============== +.. currentmodule:: nacl.ristretto + +The classes :py:class:`Ristretto255Scalar` and :py:class:`Ristretto255Point` +provide a high-level abstraction around the low-level bindings to `libsodium +`__. +Several functions are accessible through operator overloading. + +See :ref:`finite-field-arithmetic` for high-level documentation. + +.. autoclass:: Ristretto255Scalar + :members: + :special-members: __init__, __add__, __bool__, __bytes__, __eq__, __int__, __mul__, __truediv__, __neg__, __sub__ + + +.. autoclass:: Ristretto255Point + :members: + :special-members: __add__, __bool__, __bytes__, __eq__, __mul__, __neg__, __sub__ diff --git a/docs/index.rst b/docs/index.rst index 010352bae..b31e9104f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ Contents signing hashing password_hashing + ristretto Support Features @@ -31,6 +32,7 @@ Support Features api/hash api/pwhash api/hashlib + api/ristretto .. toctree:: :caption: The PyNaCl open source project diff --git a/docs/ristretto.rst b/docs/ristretto.rst new file mode 100644 index 000000000..7157f26e9 --- /dev/null +++ b/docs/ristretto.rst @@ -0,0 +1,317 @@ +.. currentmodule:: nacl.ristretto +.. _finite-field-arithmetic: + +Finite field arithmetic +======================= +`Ristretto255 `__ is a prime order elliptic curve +group based on Curve25519. It can be used as a building block for cryptographic +protocols such as `Zero-knowledge proofs of knowledge +`__, +`ElGamal encryption `__ or +`Schnorr signatures `__. + + +Two high-level classes are defined to wrap the `libsodium +`__ API: + +* :py:class:`Ristretto255Scalar` is the `finite field + `__ over the set of integers + modulo the prime ``2 ** 252 + 27742317777372353535851937790883648493`` and + the four operations *addition*, *subtraction*, *multiplication* and + *division*. Each operation takes two elements from the set and computes + another element from the same set. Most operations are accessible through + operator overloading. + +* :py:class:`Ristretto255Point` is the `cyclic group + `__ with points from the + Curve25519 elliptic curve. Thanks to the Ristretto construction, all elements + in the group are unique, and each element (other than the identity) is a + generator of the complete group. The order of :py:class:`Ristretto255Scalar` + matches this group's order. The basic operation in the group is *point + addition*. Repeated addition of the same point is called `multiplicaton + `__. + +An `isomorphism `__ exists between +the two groups. This means that for scalars ``s, t`` and a point ``p`` +equations such as this hold: ``p * (s + t) == (p * s) + (p * t)``. + + +Scalar field +------------ +Each instance of :py:class:`Ristretto255Scalar` is a scalar value (integer +reduced modulo the group order). The internal representation is a 32 byte array +in little-endian order. + +The operators and methods support arguments of various python types. They are +automatically reduced modulo the group order and converted into the internal +representation. + +* Another :py:class:`Ristretto255Scalar` +* :py:class:`bytes`, an 32 byte integer in little-endian encoding. +* :py:class:`int`, an arbitrary integer. +* :py:class:`fractions.Fraction`. + +Argument types can be mixed: + +.. testcode:: + + from fractions import Fraction + from nacl.ristretto import Ristretto255Scalar + + r = Ristretto255Scalar(42) / 11 * Fraction(5, 7) * (b"\x21" + bytes(31)) - -10 + print(int(r)) + +.. testoutput:: + + 100 + +Following table shows how to translate from libsodium functions: + +.. list-table:: Translating from libsodium to Ristretto255Scalar + :header-rows: 1 + :widths: auto + + * - `libsodium `__ + - PyNaCl + + * - ``crypto_core_ristretto255_nonreducedscalarbytes()`` + - :py:attr:`Ristretto255Scalar.NONREDUCED_SIZE` + + * - ``crypto_core_ristretto255_scalarbytes()`` + - :py:attr:`Ristretto255Scalar.SIZE` + + * - ``crypto_core_ristretto255_scalar_random(u)`` + - :py:meth:`u = Ristretto255Scalar.random() ` + + * - ``crypto_core_ristretto255_scalar_reduce(u, h)`` + - :py:meth:`u = Ristretto255Scalar.reduce(h) ` + + * - ``crypto_core_ristretto255_scalar_invert(u, s)`` + - :py:attr:`u = s.inverse ` + + * - ``crypto_core_ristretto255_scalar_complement(u, s)`` + - :py:attr:`u = s.complement ` + + * - ``crypto_core_ristretto255_scalar_add(u, s, t)`` + - :py:meth:`u = s + t ` + + * - ``crypto_core_ristretto255_scalar_sub(u, s, t)`` + - :py:meth:`u = s - t ` + + * - ``crypto_core_ristretto255_scalar_mul(u, s, t)`` + - :py:meth:`u = s * t ` + + * - ``crypto_core_ristretto255_scalar_mul(u, s, t.inverse)`` + - :py:meth:`u = s / t ` + + * - ``crypto_core_ristretto255_scalar_negate(u, s)`` + - :py:meth:`u = -s ` + + * - ``sodium_memcmp(s, t, 32)`` + - :py:meth:`s == t ` + + * - ``sodium_is_zero(s, 32)`` + - :py:meth:`bool(s) ` + +Ristretto group +--------------- +The multiplication operators take a scalar as operand which must be one of the +types from above list. All other operands and arguments must be points. + +Argument types can be mixed: + +.. testcode:: + + from fractions import Fraction + from nacl.ristretto import Ristretto255Point, Ristretto255Scalar + + p = Ristretto255Point.random() + q = (p * Fraction(5, 7) - p) * Ristretto255Scalar(7) + print(bytes(p * 2 + q).hex()) + + +.. testoutput:: + + 0000000000000000000000000000000000000000000000000000000000000000 + + +Following table shows how to translate from libsodium functions: + +.. list-table:: Translating from libsodium to Ristretto255Point + :header-rows: 1 + :widths: auto + + * - `libsodium `__ + - PyNaCl + + * - ``crypto_core_ristretto255_bytes()`` + - :py:attr:`Ristretto255Point.SIZE` + + * - ``crypto_core_ristretto255_hashbytes()`` + - :py:attr:`Ristretto255Point.HASH_SIZE` + + * - ``crypto_core_ristretto255_is_valid_point(p)`` + - :py:meth:`r = Ristretto255Point(p) ` + + * - ``crypto_core_ristretto255_from_hash(r, h)`` + - :py:meth:`r = Ristretto255Point.from_hash(h) ` + + * - ``crypto_core_ristretto255_random(r)`` + - :py:meth:`r = Ristretto255Point.random() ` + + * - ``crypto_scalarmult_ristretto255_base(r, s)`` + - :py:meth:`r = Ristretto255Point.base_mul(s) ` + + * - ``crypto_scalarmult_ristretto255(r, -1, p)`` + - :py:meth:`r = -p ` + + * - ``crypto_core_ristretto255_add(r, p, q)`` + - :py:meth:`r = p + q ` + + * - ``crypto_core_ristretto255_sub(r, p, q)`` + - :py:meth:`r = p - q ` + + * - ``crypto_scalarmult_ristretto255(r, s, p)`` + - :py:meth:`r = p * s ` + + * - ``sodium_memcmp(p, q, 32)`` + - :py:meth:`p == q ` + + * - ``sodium_is_zero(p, 32)`` + - :py:meth:`bool(p) ` + + +Examples +-------- +There are two code examples for `ElGamal encryption +`__ and `Shamir's Secret +Sharing `__ in the +test cases. Two simpler examples follow: + +Secure two-party computation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is the example from `libsodium +`__: + +.. testcode:: + + from os import urandom + from nacl.ristretto import Ristretto255Point, Ristretto255Scalar + + ## First party: Send blinded p(x) ## + x = urandom(Ristretto255Point.HASH_SIZE) + + # Compute px = p(x), a group element derived from x + px = Ristretto255Point.from_hash(x) + + # Compute a = p(x) * g^r + r = Ristretto255Scalar.random() + gr = Ristretto255Point.base_mul(r) + a = px + Ristretto255Point.base_mul(r) + + + ## Second party: Send g^k and a^k ## + k = Ristretto255Scalar.random() + + # Compute v = g^k + v = Ristretto255Point.base_mul(k) + + # Compute b = a^k + b = a * k + + + ## First party: Unblind f(x) ## + + # Compute f(x) = b * v^(-r) + # = (p(x) * g^r)^k * (g^k)^(-r) + # = (p(x) * g)^k * g^(-k) + # = p(x)^k + fx = b - v * r + + # Compare result + print(px * k == fx) + +.. testoutput:: + + True + +Schnorr signature +~~~~~~~~~~~~~~~~~ +The `Schnorr signature `__ +scheme can adopted to use Ristretto255: + +.. testcode:: + + from nacl.ristretto import Ristretto255Point, Ristretto255Scalar + import hashlib + + + ## Choosing parameters ## + + # Agree on group of prime order + G = Ristretto255Point + + # Choose a random generator + g = G.random() + + # Agree on a cryptographic hash function; needs to have 512 bits output + H = lambda data: Ristretto255Scalar.reduce(hashlib.sha3_512(data).digest()) + + + ## Key generation ## + + # Choose a private signing key + x = Ristretto255Scalar.random() + + # Compute the public verification key + y = g * x + + + ## Signing ## + + # Message to sign + M = b"Lorem ipsum dolor sit amet" + + # Choose a random nonce + k = Ristretto255Scalar.random() + + # Computate the signature + r = g * k + e = H(bytes(r) + M) + s = k - x * e + + # Signature is the scalars (s, e) + + + ## Verifying ## + + r_v = g * s + y * e + e_v = H(bytes(r_v) + M) + + if e_v == e: + print("Signature verified") + + + ## Key leakage from nonce reuse ## + + # Another message to sign + M_ = b"consectetur adipiscing elit" + + # Reuse nonce. Don't do that! + k_ = k + + # Computate the signature + r_ = g * k_ + e_ = H(bytes(r_) + M_) + s_ = k_ - x * e_ + + # Compute private key + x_ = (s_ - s) / (e - e_) + + if g * x_ == y: + print("Key was leaked") + +.. testoutput:: + + Signature verified + Key was leaked diff --git a/docs/vectors/index.rst b/docs/vectors/index.rst index 7f7e043fc..f555df4c8 100644 --- a/docs/vectors/index.rst +++ b/docs/vectors/index.rst @@ -55,6 +55,13 @@ In particular, the original expected results come from siphash's vectors.h, while the key and the input messages have been generated following the respective definitions in siphash's test.c. +ristretto255 +^^^^^^^^^^^^ + +The reference vectors for :ref:`ristretto255 ` in +``tests/data/ristretto255.json`` are taken from +https://ristretto.group/test_vectors/ristretto255.html. + Custom generated reference vectors ---------------------------------- diff --git a/src/bindings/crypto_core_ristretto255.h b/src/bindings/crypto_core_ristretto255.h new file mode 100644 index 000000000..fbd69011e --- /dev/null +++ b/src/bindings/crypto_core_ristretto255.h @@ -0,0 +1,37 @@ +/* Copyright 2021 Donald Stufft and individual contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +static const int PYNACL_HAS_CRYPTO_CORE_RISTRETTO25519; + +size_t crypto_core_ristretto255_scalarbytes(void); +size_t crypto_core_ristretto255_nonreducedscalarbytes(void); + +void crypto_core_ristretto255_scalar_add(unsigned char *z, const unsigned char *x, const unsigned char *y); +void crypto_core_ristretto255_scalar_complement(unsigned char *comp, const unsigned char *s); +int crypto_core_ristretto255_scalar_invert(unsigned char *recip, const unsigned char *s); +void crypto_core_ristretto255_scalar_mul(unsigned char *z, const unsigned char *x, const unsigned char *y); +void crypto_core_ristretto255_scalar_negate(unsigned char *neg, const unsigned char *s); +void crypto_core_ristretto255_scalar_random(unsigned char *r); +void crypto_core_ristretto255_scalar_reduce(unsigned char *r, const unsigned char *s); +void crypto_core_ristretto255_scalar_sub(unsigned char *z, const unsigned char *x, const unsigned char *y); + +size_t crypto_core_ristretto255_bytes(void); +size_t crypto_core_ristretto255_hashbytes(void); + +int crypto_core_ristretto255_add(unsigned char *r, const unsigned char *p, const unsigned char *q); +int crypto_core_ristretto255_from_hash(unsigned char *p, const unsigned char *r); +int crypto_core_ristretto255_is_valid_point(const unsigned char *p); +int crypto_core_ristretto255_sub(unsigned char *r, const unsigned char *p, const unsigned char *q); +void crypto_core_ristretto255_random(unsigned char *p); diff --git a/src/bindings/crypto_scalarmult_ristretto255.h b/src/bindings/crypto_scalarmult_ristretto255.h new file mode 100644 index 000000000..befe787a0 --- /dev/null +++ b/src/bindings/crypto_scalarmult_ristretto255.h @@ -0,0 +1,22 @@ +/* Copyright 2021 Donald Stufft and individual contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +static const int PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519; + +size_t crypto_scalarmult_ristretto255_bytes(void); +size_t crypto_scalarmult_ristretto255_scalarbytes(void); + +int crypto_scalarmult_ristretto255_base(unsigned char *q, const unsigned char *n); +int crypto_scalarmult_ristretto255(unsigned char *q, const unsigned char *n, const unsigned char *p); diff --git a/src/bindings/minimal/crypto_core_ristretto255.h b/src/bindings/minimal/crypto_core_ristretto255.h new file mode 100644 index 000000000..ef1d1ddbc --- /dev/null +++ b/src/bindings/minimal/crypto_core_ristretto255.h @@ -0,0 +1,41 @@ +/* Copyright 2021 Donald Stufft and individual contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef SODIUM_LIBRARY_MINIMAL +static const int PYNACL_HAS_CRYPTO_CORE_RISTRETTO25519 = 0; + +size_t (*crypto_core_ristretto255_scalarbytes)(void) = NULL; +size_t (*crypto_core_ristretto255_nonreducedscalarbytes)(void) = NULL; + +void (*crypto_core_ristretto255_scalar_add)(unsigned char *, const unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_complement)(unsigned char *, const unsigned char *) = NULL; +int (*crypto_core_ristretto255_scalar_invert)(unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_mul)(unsigned char *, const unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_negate)(unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_random)(unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_reduce)(unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_scalar_sub)(unsigned char *, const unsigned char *, const unsigned char *) = NULL; + +size_t (*crypto_core_ristretto255_bytes)(void) = NULL; +size_t (*crypto_core_ristretto255_hashbytes)(void) = NULL; + +int (*crypto_core_ristretto255_add)(unsigned char *, const unsigned char *, const unsigned char *) = NULL; +int (*crypto_core_ristretto255_from_hash)(unsigned char *, const unsigned char *) = NULL; +int (*crypto_core_ristretto255_is_valid_point)(const unsigned char *) = NULL; +int (*crypto_core_ristretto255_sub)(unsigned char *, const unsigned char *, const unsigned char *) = NULL; +void (*crypto_core_ristretto255_random)(unsigned char *) = NULL; +#else +static const int PYNACL_HAS_CRYPTO_CORE_RISTRETTO25519 = 1; +#endif diff --git a/src/bindings/minimal/crypto_scalarmult_ristretto255.h b/src/bindings/minimal/crypto_scalarmult_ristretto255.h new file mode 100644 index 000000000..f02a001fd --- /dev/null +++ b/src/bindings/minimal/crypto_scalarmult_ristretto255.h @@ -0,0 +1,26 @@ +/* Copyright 2021 Donald Stufft and individual contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef SODIUM_LIBRARY_MINIMAL +static const int PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519 = 0; + +size_t (*crypto_scalarmult_ristretto255_bytes)(void) = NULL; +size_t (*crypto_scalarmult_ristretto255_scalarbytes)(void) = NULL; + +int (*crypto_scalarmult_ristretto255_base)(unsigned char *q, const unsigned char *n) = NULL; +int (*crypto_scalarmult_ristretto255)(unsigned char *q, const unsigned char *n, const unsigned char *p) = NULL; +#else +static const int PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519 = 1; +#endif diff --git a/src/bindings/utils.h b/src/bindings/utils.h index 061061f14..e81efe9c7 100644 --- a/src/bindings/utils.h +++ b/src/bindings/utils.h @@ -13,6 +13,7 @@ * limitations under the License. */ +int sodium_is_zero(const unsigned char *n, const size_t nlen); void sodium_memzero(void * const pnt, const size_t len); int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len); diff --git a/src/nacl/bindings/__init__.py b/src/nacl/bindings/__init__.py index 1e3b10e82..dfea5f8c2 100644 --- a/src/nacl/bindings/__init__.py +++ b/src/nacl/bindings/__init__.py @@ -71,6 +71,27 @@ crypto_core_ed25519_sub, has_crypto_core_ed25519, ) +from nacl.bindings.crypto_core_ristretto255 import ( + crypto_core_ristretto255_BYTES, + crypto_core_ristretto255_GROUP_ORDER, + crypto_core_ristretto255_HASH_BYTES, + crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES, + crypto_core_ristretto255_SCALAR_BYTES, + crypto_core_ristretto255_add, + crypto_core_ristretto255_from_hash, + crypto_core_ristretto255_is_valid_point, + crypto_core_ristretto255_random, + crypto_core_ristretto255_scalar_add, + crypto_core_ristretto255_scalar_complement, + crypto_core_ristretto255_scalar_invert, + crypto_core_ristretto255_scalar_mul, + crypto_core_ristretto255_scalar_negate, + crypto_core_ristretto255_scalar_random, + crypto_core_ristretto255_scalar_reduce, + crypto_core_ristretto255_scalar_sub, + crypto_core_ristretto255_sub, + has_crypto_core_ristretto25519, +) from nacl.bindings.crypto_generichash import ( crypto_generichash_BYTES, crypto_generichash_BYTES_MAX, @@ -173,6 +194,13 @@ crypto_scalarmult_ed25519_noclamp, has_crypto_scalarmult_ed25519, ) +from nacl.bindings.crypto_scalarmult_ristretto255 import ( + crypto_scalarmult_ristretto255, + crypto_scalarmult_ristretto255_BYTES, + crypto_scalarmult_ristretto255_SCALAR_BYTES, + crypto_scalarmult_ristretto255_base, + has_crypto_scalarmult_ristretto25519, +) from nacl.bindings.crypto_secretbox import ( crypto_secretbox, crypto_secretbox_BOXZEROBYTES, @@ -236,6 +264,7 @@ from nacl.bindings.utils import ( sodium_add, sodium_increment, + sodium_is_zero, sodium_memcmp, sodium_pad, sodium_unpad, @@ -283,11 +312,9 @@ "crypto_box_seed_keypair", "has_crypto_core_ed25519", "crypto_core_ed25519_BYTES", - "crypto_core_ed25519_UNIFORMBYTES", "crypto_core_ed25519_SCALARBYTES", "crypto_core_ed25519_NONREDUCEDSCALARBYTES", "crypto_core_ed25519_add", - "crypto_core_ed25519_from_uniform", "crypto_core_ed25519_is_valid_point", "crypto_core_ed25519_sub", "crypto_core_ed25519_scalar_invert", @@ -297,6 +324,25 @@ "crypto_core_ed25519_scalar_sub", "crypto_core_ed25519_scalar_mul", "crypto_core_ed25519_scalar_reduce", + "has_crypto_core_ristretto25519", + "crypto_core_ristretto255_SCALAR_BYTES", + "crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES", + "crypto_core_ristretto255_GROUP_ORDER", + "crypto_core_ristretto255_scalar_add", + "crypto_core_ristretto255_scalar_complement", + "crypto_core_ristretto255_scalar_invert", + "crypto_core_ristretto255_scalar_mul", + "crypto_core_ristretto255_scalar_negate", + "crypto_core_ristretto255_scalar_random", + "crypto_core_ristretto255_scalar_reduce", + "crypto_core_ristretto255_scalar_sub", + "crypto_core_ristretto255_BYTES", + "crypto_core_ristretto255_HASH_BYTES", + "crypto_core_ristretto255_add", + "crypto_core_ristretto255_from_hash", + "crypto_core_ristretto255_is_valid_point", + "crypto_core_ristretto255_sub", + "crypto_core_ristretto255_random", "crypto_hash_BYTES", "crypto_hash_sha256_BYTES", "crypto_hash_sha512_BYTES", @@ -335,6 +381,11 @@ "crypto_scalarmult_ed25519_base", "crypto_scalarmult_ed25519_noclamp", "crypto_scalarmult_ed25519_base_noclamp", + "has_crypto_scalarmult_ristretto25519", + "crypto_scalarmult_ristretto255_BYTES", + "crypto_scalarmult_ristretto255_SCALAR_BYTES", + "crypto_scalarmult_ristretto255_base", + "crypto_scalarmult_ristretto255", "crypto_secretbox_KEYBYTES", "crypto_secretbox_NONCEBYTES", "crypto_secretbox_ZEROBYTES", @@ -441,6 +492,7 @@ "sodium_init", "sodium_add", "sodium_increment", + "sodium_is_zero", "sodium_memcmp", "sodium_pad", "sodium_unpad", diff --git a/src/nacl/bindings/crypto_core_ristretto255.py b/src/nacl/bindings/crypto_core_ristretto255.py new file mode 100644 index 000000000..f2c211173 --- /dev/null +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -0,0 +1,485 @@ +# Copyright 2021 Donald Stufft and individual contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from nacl import exceptions as exc +from nacl._sodium import ffi, lib +from nacl.exceptions import ensure + +has_crypto_core_ristretto25519 = bool( + lib.PYNACL_HAS_CRYPTO_CORE_RISTRETTO25519 +) + +# Group order L of both the scalar group and group of points. +crypto_core_ristretto255_GROUP_ORDER = ( + 2 ** 252 + 27742317777372353535851937790883648493 +) + +crypto_core_ristretto255_SCALAR_BYTES = 0 +crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = 0 + +if has_crypto_core_ristretto25519: # pragma: no branch + # Size of a Ristretto255 scalar. + crypto_core_ristretto255_SCALAR_BYTES = ( + lib.crypto_core_ristretto255_scalarbytes() + ) + + # Size of values that are reduced modulo the order to a Ristretto255 scalar. + crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = ( + lib.crypto_core_ristretto255_nonreducedscalarbytes() + ) + + +def crypto_core_ristretto255_scalar_add(x: bytes, y: bytes) -> bytes: + """ + Compute the sum of the scalars ``x`` and ``y`` modulo ``L``. + + :param x: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the first scalar + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(x, bytes) + and len(x) == crypto_core_ristretto255_SCALAR_BYTES, + "First scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(y, bytes) + and len(y) == crypto_core_ristretto255_SCALAR_BYTES, + "Second scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + z = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_add(z, x, y) + + return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_complement(s: bytes) -> bytes: + """ + Compute the complement of ``s`` such that ``s + comp = 1 (mod L)``. + + :param s: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(s, bytes) + and len(s) == crypto_core_ristretto255_SCALAR_BYTES, + "Scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + comp = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_complement(comp, s) + + return ffi.buffer(comp, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_invert(s: bytes) -> bytes: + """ + Compute the multiplicative inverse of ``s`` such that + ``recip * s = 1 (mod L)``. + + :param s: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the scalar + :raises ValueError: if the value is not invertible + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(s, bytes) + and len(s) == crypto_core_ristretto255_SCALAR_BYTES, + "Scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + recip = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + rc = lib.crypto_core_ristretto255_scalar_invert(recip, s) + + ensure(rc == 0, "Value is not invertible", raising=ValueError) + + return ffi.buffer(recip, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_mul(x: bytes, y: bytes) -> bytes: + """ + Compute the product of the scalars ``x`` and ``y`` modulo ``L``. + + :param x: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the first scalar + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(x, bytes) + and len(x) == crypto_core_ristretto255_SCALAR_BYTES, + "First scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(y, bytes) + and len(y) == crypto_core_ristretto255_SCALAR_BYTES, + "Second scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + z = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_mul(z, x, y) + + return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_negate(s: bytes) -> bytes: + """ + Compute the additive inverse of the scalar ``s`` such that + ``neg + s = 0 (mod L)``. + + :param s: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + ensure( + isinstance(s, bytes) + and len(s) == crypto_core_ristretto255_SCALAR_BYTES, + "Scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + neg = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_negate(neg, s) + + return ffi.buffer(neg, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_random() -> bytes: + """ + Generate a random non-zero scalar modulo ``L``. + + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + r = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_random(r) + + return ffi.buffer(r, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_reduce(s: bytes) -> bytes: + """ + Reduce little endian value ``s`` modulo ``L``. ``s`` should have at least + 317 bits to ensure almost uniformity of ``r`` over ``L``. + + :param s: a sequence of + :py:data:`.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES` + bytes in little endian order representing the value to reduce + to a Ristretto255 scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(s, bytes) + and len(s) == crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES, + "Input must be a {} bytes long sequence".format( + crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + r = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_reduce(r, s) + + return ffi.buffer(r, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +def crypto_core_ristretto255_scalar_sub(x: bytes, y: bytes) -> bytes: + """ + Subtract scalar ``y`` from scalar ``x`` modulo ``L``. + + :param x: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the first scalar + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(x, bytes) + and len(x) == crypto_core_ristretto255_SCALAR_BYTES, + "First scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(y, bytes) + and len(y) == crypto_core_ristretto255_SCALAR_BYTES, + "Second scalar must be a sequence of {} bytes".format( + crypto_core_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + z = ffi.new("unsigned char[]", crypto_core_ristretto255_SCALAR_BYTES) + lib.crypto_core_ristretto255_scalar_sub(z, x, y) + + return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] + + +if has_crypto_core_ristretto25519: + # Size of a Ristretto255 point. + crypto_core_ristretto255_BYTES = lib.crypto_core_ristretto255_bytes() + + # Size of the input to crypto_core_ristretto255_from_hash + crypto_core_ristretto255_HASH_BYTES = ( + lib.crypto_core_ristretto255_hashbytes() + ) +else: # pragma: no cover + crypto_core_ristretto255_BYTES = 0 + crypto_core_ristretto255_HASH_BYTES = 0 + + +def crypto_core_ristretto255_add(p: bytes, q: bytes) -> bytes: + """ + Compute the sum of the points ``p`` and ``q``. + + :param p: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the first point + :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the second point + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(p, bytes) and len(p) == crypto_core_ristretto255_BYTES, + "First point must be a sequence of {} bytes".format( + crypto_core_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(q, bytes) and len(q) == crypto_core_ristretto255_BYTES, + "Second point must be a sequence of {} bytes".format( + crypto_core_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + r = ffi.new("unsigned char[]", crypto_core_ristretto255_BYTES) + rc = lib.crypto_core_ristretto255_add(r, p, q) + + ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError) + + return ffi.buffer(r, crypto_core_ristretto255_BYTES)[:] + + +def crypto_core_ristretto255_from_hash(r: bytes) -> bytes: + """ + Map 64 bytes of input, e.g. the result of a hash function, to a group + point. This might be the zero point, e.g. if input is all zeros. + + :param r: a sequence of :py:data:`.crypto_core_ristretto255_HASH_BYTES` + bytes representing the value to convert + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(r, bytes) and len(r) == crypto_core_ristretto255_HASH_BYTES, + "Input must be a sequence of {} bytes".format( + crypto_core_ristretto255_HASH_BYTES + ), + raising=exc.TypeError, + ) + + q = ffi.new("unsigned char[]", crypto_core_ristretto255_BYTES) + rc = lib.crypto_core_ristretto255_from_hash(q, r) + + ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError) + + return ffi.buffer(q, crypto_core_ristretto255_BYTES)[:] + + +def crypto_core_ristretto255_is_valid_point(p: bytes) -> bytes: + """ + Check if ``p`` is a valid point. + + :param p: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the value to check + :return: False if invalid, True if valid + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(p, bytes) and len(p) == crypto_core_ristretto255_BYTES, + "Input must be a sequence of {} bytes".format( + crypto_core_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + rc = lib.crypto_core_ristretto255_is_valid_point(p) + + return rc == 1 + + +def crypto_core_ristretto255_random() -> bytes: + """ + Generate a random Ristretto255 point. This might be, + although astronomically unlikely, the zero point. + + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + p = ffi.new("unsigned char[]", crypto_core_ristretto255_BYTES) + lib.crypto_core_ristretto255_random(p) + + return ffi.buffer(p, crypto_core_ristretto255_BYTES)[:] + + +def crypto_core_ristretto255_sub(p: bytes, q: bytes) -> bytes: + """ + Subtract point ``q`` from ``p``. + + :param p: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the first point + :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the second point + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_core_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(p, bytes) and len(p) == crypto_core_ristretto255_BYTES, + "First point must be a sequence of {} bytes".format( + crypto_core_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(q, bytes) and len(q) == crypto_core_ristretto255_BYTES, + "Second point must be a sequence of {} bytes".format( + crypto_core_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + r = ffi.new("unsigned char[]", crypto_core_ristretto255_BYTES) + rc = lib.crypto_core_ristretto255_sub(r, p, q) + + ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError) + + return ffi.buffer(r, crypto_core_ristretto255_BYTES)[:] diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py new file mode 100644 index 000000000..058452a93 --- /dev/null +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -0,0 +1,129 @@ +# Copyright 2021 Donald Stufft and individual contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from nacl import exceptions as exc +from nacl._sodium import ffi, lib +from nacl.exceptions import ensure + +has_crypto_scalarmult_ristretto25519 = bool( + lib.PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519 +) + +crypto_scalarmult_ristretto255_BYTES = 0 +crypto_scalarmult_ristretto255_SCALAR_BYTES = 0 + +if has_crypto_scalarmult_ristretto25519: # pragma: no branch + # Size of a Ristretto255 point. + # Should equal crypto_core_ristretto255_BYTES + crypto_scalarmult_ristretto255_BYTES = ( + lib.crypto_scalarmult_ristretto255_bytes() + ) + + # Size of scalars for the two functions. + crypto_scalarmult_ristretto255_SCALAR_BYTES = ( + lib.crypto_scalarmult_ristretto255_scalarbytes() + ) + + +def crypto_scalarmult_ristretto255_base(n: bytes) -> bytes: + """ + Multiply the scalar ``n`` with the Ed25519 base point. + + :param n: a sequence of + :py:data:`.crypto_scalarmult_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the scalar + :raises exc.RuntimeError: on error or if result is zero + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_scalarmult_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(n, bytes) + and len(n) == crypto_scalarmult_ristretto255_SCALAR_BYTES, + "Scalar must be a sequence of {} bytes".format( + crypto_scalarmult_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + q = ffi.new("unsigned char[]", crypto_scalarmult_ristretto255_BYTES) + rc = lib.crypto_scalarmult_ristretto255_base(q, n) + + # An error is returned iff the result is zero. For consistency with + # crypto_scalarmult_ristretto255 and in case a future version of libsodium + # returns an error for other reasons, raise an error. + ensure( + rc == 0, + "Unexpected library error. Zero operand?", + raising=exc.RuntimeError, + ) + + return ffi.buffer(q, crypto_scalarmult_ristretto255_BYTES)[:] + + +def crypto_scalarmult_ristretto255(n: bytes, p: bytes) -> bytes: + """ + Multiply the scalar ``n`` with point ``p``. + + :param n: a sequence of + :py:data:`.crypto_scalarmult_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the scalar + :param p: a sequence of :py:data:`.crypto_scalarmult_ristretto255_BYTES` + bytes in little endian order representing the point + :raises exc.RuntimeError: on error or if result is zero + :raises nacl.exceptions.UnavailableError: If called when using a + minimal build of libsodium. + """ + ensure( + has_crypto_scalarmult_ristretto25519, + "Not available in minimal build", + raising=exc.UnavailableError, + ) + + ensure( + isinstance(n, bytes) + and len(n) == crypto_scalarmult_ristretto255_SCALAR_BYTES, + "Scalar must be a sequence of {} bytes".format( + crypto_scalarmult_ristretto255_SCALAR_BYTES + ), + raising=exc.TypeError, + ) + + ensure( + isinstance(p, bytes) + and len(p) == crypto_scalarmult_ristretto255_BYTES, + "Point must be a sequence of {} bytes".format( + crypto_scalarmult_ristretto255_BYTES + ), + raising=exc.TypeError, + ) + + q = ffi.new("unsigned char[]", crypto_scalarmult_ristretto255_BYTES) + rc = lib.crypto_scalarmult_ristretto255(q, n, p) + + # An error is returned also if the result is zero. This cannot be + # distinguished from other errors like invalid points. + # https://github.com/jedisct1/libsodium/issues/836#issuecomment-493710969 + ensure( + rc == 0, + "Unexpected library error. Zero operand?", + raising=exc.RuntimeError, + ) + + return ffi.buffer(q, crypto_scalarmult_ristretto255_BYTES)[:] diff --git a/src/nacl/bindings/utils.py b/src/nacl/bindings/utils.py index 0ff22e34f..ae13e0d40 100644 --- a/src/nacl/bindings/utils.py +++ b/src/nacl/bindings/utils.py @@ -17,6 +17,20 @@ from nacl.exceptions import ensure +def sodium_is_zero(inp: bytes) -> bool: + """ + Check if all bytes in ``inp`` are zero + + :param inp: input bytes string + :return: False if any byte is nonzero, else True + """ + ensure(isinstance(inp, bytes), raising=exc.TypeError) + ln = len(inp) + rc = lib.sodium_is_zero(inp, ln) + + return rc != 0 + + def sodium_memcmp(inp1: bytes, inp2: bytes) -> bool: """ Compare contents of two memory regions in constant time @@ -96,7 +110,6 @@ def sodium_increment(inp: bytes) -> bytes: unsigned big integer, the value ``to_int(inp)`` incremented by one. :rtype: bytes - """ ensure(isinstance(inp, bytes), raising=exc.TypeError) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py new file mode 100644 index 000000000..e382791cd --- /dev/null +++ b/src/nacl/ristretto.py @@ -0,0 +1,575 @@ +# Copyright 2021 Donald Stufft and individual contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from fractions import Fraction +from typing import ClassVar, Union + +import nacl.bindings +from nacl import exceptions as exc +from nacl.utils import random + + +# Python types accepted as scalars +_ScalarType = Union["Ristretto255Scalar", bytes, int, Fraction] + + +class Ristretto255Scalar: + """ + Scalar field modulo prime :py:const:`ORDER`. Each element is a scalar value. + + :cvar ZERO: Scalar with value 0 + :cvar ONE: Scalar with value 1 + :cvar MINUS_ONE: Scalar with value -1 (modulo :py:const:`ORDER`) + :cvar SIZE: Size of Scalars in bytes (32) + :cvar NONREDUCED_SIZE: Size of non reduced scalar (64); see :py:meth:`reduce`. + :cvar ORDER: Group order (``2 ** 252 + 27742317777372353535851937790883648493``) + """ + + ZERO: ClassVar["Ristretto255Scalar"] + ONE: ClassVar["Ristretto255Scalar"] + MINUS_ONE: ClassVar["Ristretto255Scalar"] + SIZE: ClassVar[int] = nacl.bindings.crypto_core_ristretto255_SCALAR_BYTES + NONREDUCED_SIZE: ClassVar[ + int + ] = nacl.bindings.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES + ORDER: ClassVar[int] = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + + # Actual value; 32 bytes in little endian order + _value: bytes + + def __init__(self, value: _ScalarType) -> None: + """ + Create a new :py:class:`Ristretto255Scalar`. + + :param value: Value of the scalar. Will be converted according to its type. + :raises exc.TypeError: Type not supported + + Value can be one of: + + * :py:class:`Ristretto255Scalar`: Create a new object with the same value. + * *bytes*: *value* must be :py:CONST:`SIZE` bytes in little-endian order. + * *int*: *value* will be reduced modulo :py:CONST:`ORDER`. + * `Fraction `__: + Numerator of *value* multiplied with the inverse of its denominator. + """ + self._value = self._convert(value) + + @staticmethod + def _convert(value: object) -> bytes: + """ + Convert various types to a byte array containing the reduced scalar value in little-endian order. + + :param value: Value of the scalar. Will be converted according to its type. + :return: Canonical represention of the passed value, as byte array. + :raises exc.TypeError: Type not supported + """ + if isinstance(value, Ristretto255Scalar): + return value._value + + if isinstance(value, bytes): + if len(value) != Ristretto255Scalar.SIZE: + raise exc.ValueError + + # Reduce value modulo the group order to ensure a canonical encoding. + zero = bytes(Ristretto255Scalar.SIZE) + return nacl.bindings.crypto_core_ristretto255_scalar_add( + value, zero + ) + + if isinstance(value, int): + return (value % Ristretto255Scalar.ORDER).to_bytes( + Ristretto255Scalar.SIZE, "little" + ) + + if isinstance(value, Fraction): + numerator = Ristretto255Scalar._convert(value.numerator) + denominator = Ristretto255Scalar._convert(value.denominator) + + # Compute fraction [a / b] as [a * (b ** -1)] + return nacl.bindings.crypto_core_ristretto255_scalar_mul( + numerator, + nacl.bindings.crypto_core_ristretto255_scalar_invert( + denominator + ), + ) + + raise exc.TypeError(f"Unsupported type: {type(value).__name__!r}") + + @classmethod + def random(cls) -> "Ristretto255Scalar": + """ + Create non-zero random scalar. + + :return: Random scalar + """ + return cls(nacl.bindings.crypto_core_ristretto255_scalar_random()) + + @classmethod + def random_zero(cls) -> "Ristretto255Scalar": + """ + Create a random scalar that could be zero. + + :return: Ristretto255Scalar: Random scalar + """ + return cls.reduce(random(cls.NONREDUCED_SIZE)) + + @classmethod + def reduce(cls, value: bytes) -> "Ristretto255Scalar": + """ + Reduce a larger value, e.g. the output of a hash function, to a scalar. + There should be at least 317 bits to ensure almost uniformity. + + :param value: :py:const:`NONREDUCED_SIZE` bytes in little-endian encoding + :return: Value reduced modulo :py:CONST:`ORDER` + """ + return cls(nacl.bindings.crypto_core_ristretto255_scalar_reduce(value)) + + @property + def inverse(self) -> "Ristretto255Scalar": + """ + Get multiplicative inverse such that ``x.inverse * x == Ristretto255Scalar.ONE``. + + :return: Multiplicative inverse reduced modulo :py:CONST:`ORDER` + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_invert(self._value) + ) + + @property + def complement(self) -> "Ristretto255Scalar": + """ + Get the complement such that ``x.complement + x == Ristretto255Scalar.ONE``. + + Note that this is *not* the two's complement where ``~x + x == -1``. + + :return: Complemental value reduced modulo :py:CONST:`ORDER` + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_complement( + self._value + ) + ) + + def __add__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Add two scalars. + + :param other: Any of the types supported by the constructor + :return: Sum of *self* and *other* reduced modulo :py:CONST:`ORDER` + """ + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_add( + self._value, value + ) + ) + + def __radd__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Add two scalars. + + :param other: Any of the types supported by the constructor + :return: Sum of *other* and *self* reduced modulo :py:CONST:`ORDER` + """ + return self + other + + def __sub__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Subtract *other* from *self*. + + :param other: Any of the types supported by the constructor + :return: Difference of *self* and *other* reduced modulo :py:CONST:`ORDER` + """ + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_sub( + self._value, value + ) + ) + + def __rsub__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Subtract *self* from *other*. + + :param other: Any of the types supported by the constructor + :return: Difference of *other* and *self* reduced modulo :py:CONST:`ORDER` + """ + return -(self - other) + + def __mul__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Multiply two scalars. + + :param other: Any of the types supported by the constructor + :return: Product of *self* and *other* modulo :py:CONST:`ORDER` + """ + + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_mul( + self._value, value + ) + ) + + def __rmul__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Multiply two scalars. + + :param other: Any of the types supported by the constructor + :return: Product of *other* and *self* modulo :py:CONST:`ORDER` + """ + return self * other + + def __truediv__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Divide two scalars. + + :param other: Any of the types supported by the constructor + :return: Product of *self* and inverse of *other* modulo :py:CONST:`ORDER` + """ + + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + + inverse = nacl.bindings.crypto_core_ristretto255_scalar_invert(value) + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_mul( + self._value, inverse + ) + ) + + def __rtruediv__(self, other: _ScalarType) -> "Ristretto255Scalar": + """ + Divide two scalars. + + :param other: Any of the types supported by the constructor + :return: Product of *other* and inverse of *self* modulo :py:CONST:`ORDER` + """ + + return self.inverse * other + + def __neg__(self) -> "Ristretto255Scalar": + """ + Get the additive inverse such that ``-x + x == Ristretto255Scalar.ZERO``. + + :return: Additive inverse + """ + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_negate(self._value) + ) + + def __eq__(self, other: object) -> bool: + """ + Check if two scalars are identical. Comparing with other types such as + ``int`` will return False. + + :return: True if equal, False otherwise + """ + if not isinstance(other, self.__class__): + return False + + return nacl.bindings.sodium_memcmp(self._value, other._value) + + def __ne__(self, other: object) -> bool: + """ + Check if two scalars are not identical. Comparing with other types such as + ``int`` will return True. + + :return: False if equal, True otherwise + """ + return not (self == other) + + def __hash__(self) -> int: + """ + Compute a hash value. + + :return: Hash value + """ + return hash(self._value) + + def __bytes__(self) -> bytes: + """ + Get byte representation of scalar. + + :return: Value of scalar in little-endian encoding + """ + return self._value + + def __int__(self) -> int: + """ + Get integer representation of scalar. + + :return: Value of scalar reduced modulo :py:CONST:`ORDER` + """ + return int.from_bytes(self._value, "little") + + def __bool__(self) -> bool: + """ + Check if scalar is non-zero. + + :return: True if non-zero, False otherwise + """ + return not nacl.bindings.sodium_is_zero(self._value) + + def __repr__(self) -> str: + """ + Get representation of scalar which, when evaluated, will yield an equal scalar. + + :return: Representation of scalar + """ + return f"Ristretto255Scalar({int(self)})" + + def __str__(self) -> str: + """ + Get human readable representation of scalar. + + :return: Representation of scalar + """ + return repr(self) + + +if nacl.bindings.has_crypto_core_ristretto25519: # pragma: no branch + # Neutral additive element + Ristretto255Scalar.ZERO = Ristretto255Scalar(0) + + # Neutral multiplicative element + Ristretto255Scalar.ONE = Ristretto255Scalar(1) + + # Constant needed for inverting points + Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar(-1) + + +class Ristretto255Point: + """ + Ristretto255 group. Each element is a curve point. + + :cvar ORDER: Group order + :cvar SIZE: Size of Points in bytes (32) + :cvar HASH_SIZE: Size input for :py:meth:`from_hash` (64). + :cvar ZERO: Neutral element + """ + + SIZE: ClassVar[int] = nacl.bindings.crypto_core_ristretto255_BYTES + HASH_SIZE: ClassVar[ + int + ] = nacl.bindings.crypto_core_ristretto255_HASH_BYTES + ORDER: ClassVar[int] = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + ZERO: ClassVar["Ristretto255Point"] + + # Actual value; 32 bytes in little endian order + _value: bytes + + def __init__(self, value: bytes, _assume_valid: bool = False) -> None: + """ + Create a new :py:class:`Ristretto255Point`. + + :param value: Value of point in little-endian order + :param _assume_valid: For internal use only: Skip check for valid point + :raises exc.ValueError: Invalid point + """ + if not _assume_valid: + if not nacl.bindings.crypto_core_ristretto255_is_valid_point( + value + ): + raise exc.ValueError("Not a valid point") + self._value = value + + @classmethod + def from_hash(cls, value: bytes) -> "Ristretto255Point": + """ + Map 64 bytes of input, e.g. the result of a hash function, to a group + point. This might be the zero point, e.g. if hash value is all zeros. + + :param value: :py:const:`HASH_SIZE` bytes in little-endian encoding + :return: Point created from *value* + """ + return cls( + nacl.bindings.crypto_core_ristretto255_from_hash(value), + _assume_valid=True, + ) + + @classmethod + def random(cls) -> "Ristretto255Point": + """ + Generate a random Ristretto255 point. This might be, + although astronomically unlikely, the zero point. + + :return: Random point + """ + return cls( + nacl.bindings.crypto_core_ristretto255_random(), _assume_valid=True + ) + + @classmethod + def base_mul(cls, n: _ScalarType) -> "Ristretto255Point": + """ + Multiply the non-zero scalar *n* with the Ed25519 base point. + + :param n: Scalar value, any type supported by :py:class:`Ristretto255Scalar`. + :return: Product of the Ed25519 base point and *n* + """ + return cls( + nacl.bindings.crypto_scalarmult_ristretto255_base( + Ristretto255Scalar._convert(n) + ), + _assume_valid=True, + ) + + def __neg__(self) -> "Ristretto255Point": + """ + Get inverse element such that ``-self + self == Ristretto255Point.ZERO``. + + :return: Inverse of *self* + """ + return self * Ristretto255Scalar.MINUS_ONE + + def __add__(self, other: "Ristretto255Point") -> "Ristretto255Point": + """ + Add two points. + + :arg other: A group point + :return: Sum of *self* and *other* + """ + if not isinstance(other, Ristretto255Point): + return NotImplemented # type: ignore[unreachable] + + return Ristretto255Point( + nacl.bindings.crypto_core_ristretto255_add( + self._value, other._value + ), + _assume_valid=True, + ) + + def __sub__(self, other: "Ristretto255Point") -> "Ristretto255Point": + """ + Subtract two points. + + :arg other: A group point + :return: Difference of *self* and *other* + """ + if not isinstance(other, Ristretto255Point): + return NotImplemented # type: ignore[unreachable] + + return Ristretto255Point( + nacl.bindings.crypto_core_ristretto255_sub( + self._value, other._value + ), + _assume_valid=True, + ) + + def __mul__(self, other: _ScalarType) -> "Ristretto255Point": + """ + Multiply the non-zero scalar *other* with the point. + + :param other: Scalar value, any type supported by :py:class:`Ristretto255Scalar`. + :return: Product of *self* and *other* + """ + return Ristretto255Point( + nacl.bindings.crypto_scalarmult_ristretto255( + Ristretto255Scalar._convert(other), self._value + ), + _assume_valid=True, + ) + + def __rmul__(self, other: _ScalarType) -> "Ristretto255Point": + """ + Multiply the point with the non-zero scalar *other*. + + :param other: Scalar value, any type supported by :py:class:`Ristretto255Scalar`. + :return: Product of *other and *self* + """ + return self * other + + def __bool__(self) -> bool: + """ + Check if this is *not* the zero / neutral / identity point. + + :return: False if zero point, True otherwise + """ + return not nacl.bindings.sodium_is_zero(self._value) + + def __eq__(self, other: object) -> bool: + """ + Compare this point to another point. + + :param other: Other point to compare to + :return: True if same point, False otherwise or if not a :py:class:`Ristretto255Scalar` + """ + if not isinstance(other, self.__class__): + return False + + return nacl.bindings.sodium_memcmp(self._value, other._value) + + def __ne__(self, other: object) -> bool: + """ + Compare this point to another point. + + :param other: Other point to compare to + :return: False if same point, True otherwise or if not a :py:class:`Ristretto255Scalar` + """ + return not (self == other) + + def __bytes__(self) -> bytes: + """ + Get byte representation of point. + + :return: Little-endian byte representation of point + """ + return self._value + + def __hash__(self) -> int: + """ + Compute a hash value. + + :return: Hash value + """ + return hash(self._value) + + def __repr__(self) -> str: + """ + Get representation of point which, when evaluated, will yield an equal point. + + :return: Representation of point + """ + return f"Ristretto255Point({bytes(self)!r})" + + def __str__(self) -> str: + """ + Get human readable representation of point. + + :return: Little-endian hex representation of point + """ + return f"Ristretto255Point({bytes(self).hex()})" + + +if nacl.bindings.has_crypto_core_ristretto25519: # pragma: no branch + # Neutral element + Ristretto255Point.ZERO = Ristretto255Point( + bytes(Ristretto255Point.SIZE), _assume_valid=True + ) diff --git a/tests/data/ristretto255.json b/tests/data/ristretto255.json new file mode 100644 index 000000000..7adec4920 --- /dev/null +++ b/tests/data/ristretto255.json @@ -0,0 +1,72 @@ +{ + "encodings_of_small_multiples": [ + "0000000000000000000000000000000000000000000000000000000000000000", + "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76", + "6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919", + "94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259", + "da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57", + "e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e", + "f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403", + "44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d", + "903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c", + "02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031", + "20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f", + "bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42", + "e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460", + "aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f", + "46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e", + "e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e" + ], + + "bad_encodings": [ + "00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "0100000000000000000000000000000000000000000000000000000000000000", + "01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", + "ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20", + "c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562", + "c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78", + "47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24", + "f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72", + "87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309", + "26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371", + "4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f", + "de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b", + "bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042", + "2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08", + "f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22", + "8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731", + "2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b", + "3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e", + "a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220", + "d483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e", + "8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32", + "32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b", + "227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165", + "5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e", + "445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b", + "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f" + ], + + "labels": [ + "Ristretto is traditionally a short shot of espresso coffee", + "made with the normal amount of ground coffee but extracted with", + "about half the amount of water in the same amount of time", + "by using a finer grind.", + "This produces a concentrated shot of coffee per volume.", + "Just pulling a normal shot short will produce a weaker shot", + "and is not a Ristretto as some believe." + ], + + "encoded_hash_to_points": [ + "3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46", + "f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b", + "006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826", + "f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a", + "ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179", + "e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628", + "80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065" + ] +} diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 3bb726ff5..7a0486886 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -327,6 +327,15 @@ def test_box_seed_keypair_short_seed(): c.crypto_box_seed_keypair(seed) +def test_sodium_is_zero(): + assert c.sodium_is_zero(b"") + assert c.sodium_is_zero(b"\x00" * 37) + assert not c.sodium_is_zero(b"\x00" * 13 + b"\xe1" + b"\x00" * 22) + assert not c.sodium_is_zero(b"no zero byte at all") + with pytest.raises(TypeError): + c.sodium_is_zero("zero") # type: ignore[arg-type] + + @given(integers(min_value=-2, max_value=0)) def test_pad_wrong_blocksize(bl_sz): with pytest.raises(ValueError): @@ -882,3 +891,51 @@ def test_scalarmult_ed25519_unavailable(): c.crypto_scalarmult_ed25519(zero, zero) with pytest.raises(UnavailableError): c.crypto_scalarmult_ed25519_noclamp(zero, zero) + + +@pytest.mark.skipif( + c.has_crypto_core_ristretto25519, + reason="Requires minimal build of libsodium", +) +def test_core_ristretto25519_unavailable(): + zero = 32 * b"\x00" + + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_add(zero, zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_complement(zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_invert(zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_mul(zero, zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_negate(zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_random() + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_reduce(zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_scalar_sub(zero, zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_add(zero, zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_from_hash(zero + zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_is_valid_point(zero) + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_random() + with pytest.raises(UnavailableError): + c.crypto_core_ristretto255_sub(zero, zero) + + +@pytest.mark.skipif( + c.has_crypto_scalarmult_ristretto25519, + reason="Requires minimal build of libsodium", +) +def test_scalarmult_ristretto25519_unavailable(): + zero = 32 * b"\x00" + + with pytest.raises(UnavailableError): + c.crypto_scalarmult_ristretto255_base(zero) + with pytest.raises(UnavailableError): + c.crypto_scalarmult_ristretto255(zero, zero) diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py new file mode 100644 index 000000000..957ea27b9 --- /dev/null +++ b/tests/test_ristretto.py @@ -0,0 +1,814 @@ +# Copyright 2021 Donald Stufft and individual contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import os +from fractions import Fraction +from functools import reduce +from hashlib import sha512 +from operator import mul +from random import randrange +from typing import List, Tuple + +import pytest + +import nacl.exceptions as exc +from nacl.bindings import ( + has_crypto_core_ristretto25519, + has_crypto_scalarmult_ristretto25519, +) +from nacl.ristretto import Ristretto255Point, Ristretto255Scalar + + +def _ristretto255_vectors() -> Tuple[ + List[Tuple[int, bytes]], List[bytes], List[Tuple[str, bytes]] +]: + """ + Test vectors from https://ristretto.group/test_vectors/ristretto255.html + """ + DATA = "ristretto255.json" + path = os.path.join(os.path.dirname(__file__), "data", DATA) + vectors = json.load(open(path)) + + return ( + [ + (idx, bytes.fromhex(enc)) + for idx, enc in enumerate(vectors["encodings_of_small_multiples"]) + ], + [bytes.fromhex(enc) for enc in vectors["bad_encodings"]], + [ + (label, bytes.fromhex(enc)) + for label, enc in zip( + vectors["labels"], vectors["encoded_hash_to_points"] + ) + ], + ) + + +class TestRistretto255Scalar: + order = 7237005577332262213973186563042994240857116359379907606001950938285454250989 + order_bytes = bytes.fromhex( + "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" + ) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_init(self): + value = bytes(reversed(range(32))) + scalar = Ristretto255Scalar(value) + assert bytes(scalar) == value + + reduced_scalar = Ristretto255Scalar( + (int.from_bytes(value, "little") + self.order * 13).to_bytes( + 32, "little" + ) + ) + assert bytes(reduced_scalar) == value + + assert bytes(Ristretto255Scalar(0xE2)) == b"\xe2" + b"\x00" * 31 + assert bytes(Ristretto255Scalar(0xABCD)) == b"\xcd\xab" + b"\x00" * 30 + assert bytes(Ristretto255Scalar(self.order)) == b"\x00" * 32 + assert ( + bytes(Ristretto255Scalar(-0xED)) == b"\x00" + self.order_bytes[1:] + ) + + assert ( + bytes(Ristretto255Scalar(Fraction(5, 1))) == b"\x05" + b"\x00" * 31 + ) + # (pow(3, -1, order) * 5 % order).to_bytes(32, "little").hex() + five_thirds = bytes.fromhex( + "a646a7c9082106c89c8952364a534a5c55555555555555555555555555555505" + ) + assert bytes(Ristretto255Scalar(Fraction(5, 3))) == five_thirds + + with pytest.raises(exc.ValueError): + Ristretto255Scalar(b"too short") + + with pytest.raises(exc.TypeError): + Ristretto255Scalar(3.14) # type: ignore[arg-type] + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_random(self): + s = Ristretto255Scalar.random() + t = Ristretto255Scalar.random() + + # Two random scalars *might* be the same in theory. But in practice + # it can only be a serious bug because of the huge group size. + assert s != t + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_random_zero(self): + s = Ristretto255Scalar.random_zero() + t = Ristretto255Scalar.random_zero() + + # Two random scalars *might* be the same in theory. But in practice + # it can only be a serious bug because of the huge group size. + assert s != t + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_reduce(self): + assert ( + bytes(Ristretto255Scalar.reduce(b"\xcd\xab" + b"\x00" * 62)) + == b"\xcd\xab" + b"\x00" * 30 + ) + dgst = sha512(b"hello").digest() + + # (int.from_bytes(sha512(b"hello").digest(), "little") % order).to_bytes(32, "little").hex() + reduced_dgst = bytes.fromhex( + "b586c3423482ab97d876ce24cab8bd8ab84e22ac3a52a8dfbb330bbe92a3260f" + ) + + assert bytes(Ristretto255Scalar.reduce(dgst)) == reduced_dgst + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_inverse(self): + assert Ristretto255Scalar(1).inverse == Ristretto255Scalar.ONE + s = Ristretto255Scalar.random() + assert s.inverse * s == Ristretto255Scalar.ONE + + t = Ristretto255Scalar(bytes(range(32))) + + # pow(int.from_bytes(bytes(range(32)), "little"), -1, order).to_bytes(32, "little").hex() + inv = bytes.fromhex( + "0cf17e6d77775ab76bd4f41cd2ef9ecc9ddd8242185bd685a60b49b5b3f16606" + ) + + assert bytes(t.inverse) == inv + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_complement(self): + assert Ristretto255Scalar(1).complement == Ristretto255Scalar.ZERO + assert Ristretto255Scalar(0).complement == Ristretto255Scalar.ONE + + s = Ristretto255Scalar.random() + assert s.complement + s == Ristretto255Scalar.ONE + + t = Ristretto255Scalar(bytes(range(32))) + # ((1 - int.from_bytes(bytes(range(32)), "little")) % order).to_bytes(32, "little").hex() + compl = bytes.fromhex( + "dba6e9b630c11ea9a430e53ab1e6af1af0eeedecebeae9e8e7e6e5e4e3e2e100" + ) + + assert bytes(t.complement) == compl + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_add(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(579) + + with pytest.raises(TypeError): + s + "foo" # type: ignore[operator] + + with pytest.raises(TypeError): + "foo" + s # type: ignore[operator] + + assert s + t == u + assert s + t == t + s + assert s != t + assert s + Ristretto255Scalar.ZERO == s + assert s + 456 == u + assert 456 + s == u + assert u + (self.order - 456) == s + + a = Ristretto255Scalar.random() + b = Ristretto255Scalar.random() + c = Ristretto255Scalar.random() + + assert (a + b) + c == (c + a) + b + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_sub(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(579) + + with pytest.raises(TypeError): + s - "foo" # type: ignore[operator] + + with pytest.raises(TypeError): + "foo" - s # type: ignore[operator] + + assert u - s == t + assert u - t == s + assert s - (self.order - 456) == u + assert u - 456 == s + assert 579 - t == s + + a = Ristretto255Scalar.random() + b = Ristretto255Scalar.random() + c = Ristretto255Scalar.random() + + assert (a - b) - c == a - (c + b) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_mul(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(123 * 456) + + assert s * t == t * s + assert bytes(s * t) == b"\x18\xdb" + b"\x00" * 30 + assert 456 * s == u + assert t * 123 == u + + assert ((s * -1) * t) * -1 == u + + v = Ristretto255Scalar(b"\x01" * 32) + w = Ristretto255Scalar(b"\x02" * 32) + # (int.from_bytes(b"\x01" * 32, "little") * int.from_bytes(b"\x02" * 32, "little") % order).to_bytes(32, "little").hex() + x = bytes.fromhex( + "7d808bf1fafea25f3ee660ef3c1793985190ba1413f9b714edf967ce6b8bdd06" + ) + assert bytes(v * w) == x + + a = Ristretto255Scalar.random() + b = Ristretto255Scalar.random() + c = Ristretto255Scalar.random() + + assert (a * b) * c == c * (b * a) + assert a * Ristretto255Scalar.ZERO == Ristretto255Scalar.ZERO + assert a * Ristretto255Scalar.ONE == a + + with pytest.raises(TypeError): + s * "foo" # type: ignore[operator] + + with pytest.raises(TypeError): + "foo" * s # type: ignore[operator] + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_div(self): + s = Ristretto255Scalar.random() + t = Ristretto255Scalar.random() + u = Ristretto255Scalar.random() + + assert s / s == Ristretto255Scalar.ONE + assert s / t == (t / s).inverse + assert s / (t / u) == s / t * u + + assert Ristretto255Scalar(123 * 456) / 123 == Ristretto255Scalar(456) + assert 123 * 456 / Ristretto255Scalar(123) == Ristretto255Scalar(456) + + v = Ristretto255Scalar(b"\x01" * 32) + w = Ristretto255Scalar(b"\x02" * 32) + # (int.from_bytes(b"\x01" * 32, "little") * pow(int.from_bytes(b"\x02" * 32, "little"), + # -1, order) % order).to_bytes(32, "little").hex() + x = bytes.fromhex( + "f7e97a2e8d31092c6bce7b51ef7c6f0a00000000000000000000000000000008" + ) + assert bytes(v / w) == x + + with pytest.raises(TypeError): + s / "foo" # type: ignore[operator] + + with pytest.raises(TypeError): + "foo" / s # type: ignore[operator] + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_neg(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(-123) + + assert -s == t + assert s == -t + assert -s * Ristretto255Scalar.MINUS_ONE == s + + a = Ristretto255Scalar.random() + assert a + -a == Ristretto255Scalar.ZERO + assert a - -a == 2 * a + assert -Ristretto255Scalar.ZERO == Ristretto255Scalar.ZERO + assert -Ristretto255Scalar.ONE == Ristretto255Scalar.MINUS_ONE + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_eq(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(123) + u = Ristretto255Scalar(456) + + assert s == s + assert s == t + assert t != u + + assert s != "foobar" + assert "foobar" != s + assert s != 123 + assert 123 != s + + p = Ristretto255Scalar.random() + q = Ristretto255Scalar.random() + + a = p * 17 + q + b = p * 8 + q * 5 + p * 9 - 4 * q + c = p * 17 + q * 2 + + assert a == b + assert a != c + assert b != c + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_hash(self): + p = Ristretto255Scalar.random() + q = Ristretto255Scalar.random() + + h0 = hash(p * 17 + q) + h1 = hash(p * 8 + q * 5 + p * 9 - 4 * q) + + assert h0 == h1 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_bytes(self): + s = Ristretto255Scalar(123) + assert type(bytes(s)) is bytes + assert len(bytes(s)) == 32 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_int(self): + s = Ristretto255Scalar(123) + t = -s + + assert int(s) == 123 + assert int(t) == self.order - 123 + assert int(Ristretto255Scalar.ZERO) == 0 + assert int(Ristretto255Scalar.ONE) == 1 + assert int(Ristretto255Scalar.MINUS_ONE) == self.order - 1 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_bool(self): + assert not Ristretto255Scalar.ZERO + assert Ristretto255Scalar.ONE + assert Ristretto255Scalar.MINUS_ONE + assert Ristretto255Scalar.random() + + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(579) + + assert s + assert u - t + assert not (u - t - s) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_repr(self): + s = Ristretto255Scalar(123) + assert repr(s) == "Ristretto255Scalar(123)" + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_str(self): + s = Ristretto255Scalar(123) + text = str(s) + assert text == "Ristretto255Scalar(123)" + + +class TestRistretto255Point: + _vectors_encodings_of_small_multiples: List[Tuple[int, bytes]] + _vectors_bad_encodings: List[bytes] + _vectors_label_hash_to_points: List[Tuple[str, bytes]] + + ( + _vectors_encodings_of_small_multiples, + _vectors_bad_encodings, + _vectors_label_hash_to_points, + ) = _ristretto255_vectors() + + _base = bytes.fromhex( + "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + ) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + @pytest.mark.parametrize( + ("idx", "encoding"), _vectors_encodings_of_small_multiples + ) + def test_small_multiples(self, idx, encoding): + base = Ristretto255Point(self._base) + point = Ristretto255Point.ZERO + + for __ in range(idx): + point += base + + assert bytes(point) == encoding + + if idx > 0: + # skip idx == 0 because libsodium would raise an error. + point = Ristretto255Point.base_mul(idx) + assert bytes(point) == encoding + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + @pytest.mark.parametrize(("encoding"), _vectors_bad_encodings) + def test_bad_encodings(self, encoding): + with pytest.raises(exc.ValueError): + Ristretto255Point(encoding) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + @pytest.mark.parametrize( + ("label", "encoding"), _vectors_label_hash_to_points + ) + def test_hash_to_point(self, label, encoding): + point = Ristretto255Point.from_hash( + sha512(label.encode("UTF-8")).digest() + ) + assert bytes(point) == encoding + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_init(self): + with pytest.raises(exc.TypeError): + Ristretto255Point(b"too short") + + with pytest.raises(exc.TypeError): + Ristretto255Point(3.14) # type: ignore[arg-type] + + # good code paths are tested elsewhere. + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_random(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + + # Two random points *might* be the same in theory. But in practice + # it can only be a serious bug because of the huge group size. + assert p != q + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_neg(self): + p = Ristretto255Point.random() + q = -p + assert p != q + assert p + q == Ristretto255Point.ZERO + assert p - q == p + p + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_add(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + r = Ristretto255Point.random() + + with pytest.raises(TypeError): + p + 123 # type: ignore[operator] + + assert p + Ristretto255Point.ZERO == p + assert Ristretto255Point.ZERO + p == p + assert p + q == q + p + assert (p + q) + r == p + (q + r) + assert (p + q) + r == (r + p) + q + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_sub(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + r = Ristretto255Point.random() + + with pytest.raises(TypeError): + p - 123 # type: ignore[operator] + + assert p - Ristretto255Point.ZERO == p + assert Ristretto255Point.ZERO - p != p + assert Ristretto255Point.ZERO - p == -p + assert p - q != q - p + assert (p - q) - r == p - (q + r) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_mul(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + + with pytest.raises(exc.TypeError): + p * q # type: ignore[operator] + + with pytest.raises(exc.TypeError): + p * "test" # type: ignore[operator] + + assert p * 3 == 3 * p + assert p + p + p == p * 3 + assert ((p * 2) * 3) * 5 == p * 30 + + assert p * Ristretto255Scalar(7) == p * 8 - p + assert Ristretto255Scalar(7) * p == 8 * p - p + assert p * Fraction(8, 1) == p * 8 + assert Fraction(8, 1) * p == 8 * p + assert 27 * p * Fraction(-11, 27) == p * -11 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_bool(self): + p = Ristretto255Point.random() + + assert not Ristretto255Point.ZERO + assert bool(Ristretto255Point.base_mul(1)) + assert not (p - p) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_eq(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + assert p != "foobar" + assert p == p + + a = p * 17 + q + b = p * 8 + q * 5 + p * 9 - 4 * q + c = p * 17 + q * 2 + + assert a == b + assert a != c + assert b != c + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_bytes(self): + base = Ristretto255Point.base_mul(1) + enc = bytes(base) + assert enc == self._base + + p = Ristretto255Point.random() + assert type(bytes(p)) is bytes + assert len(bytes(p)) == 32 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_hash(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + + h0 = hash(p * 17 + q) + h1 = hash(p * 8 + q * 5 + p * 9 - 4 * q) + + assert h0 == h1 + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_repr(self): + base = Ristretto255Point.base_mul(1) + assert repr(base) == "Ristretto255Point({})".format(repr(self._base)) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_str(self): + base = Ristretto255Point.base_mul(1) + base_hex = self._base.hex() + text = str(base) + assert text == "Ristretto255Point({})".format(base_hex) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_library_error(self): + p = Ristretto255Point( + self._vectors_bad_encodings[6], _assume_valid=True + ) + q = Ristretto255Point.random() + + with pytest.raises(exc.RuntimeError): + p + q + + with pytest.raises(exc.RuntimeError): + p - q + + with pytest.raises(exc.RuntimeError): + p * 2 + + +class TestElGamal: + """ + ElGamal encryption. + """ + + def gen_key(self) -> Tuple[Ristretto255Scalar, Ristretto255Point]: + x = Ristretto255Scalar.random() + h = Ristretto255Point.base_mul(x) + + return x, h + + def encrypt( + self, h: Ristretto255Point, m: Ristretto255Point + ) -> Tuple[Ristretto255Point, Ristretto255Point]: + y = Ristretto255Scalar.random() + s = h * y + c0 = Ristretto255Point.base_mul(y) + c1 = m + s + + return c0, c1 + + def decrypt( + self, + c0: Ristretto255Point, + c1: Ristretto255Point, + x: Ristretto255Scalar, + ) -> Ristretto255Point: + s = c0 * x + m = c1 - s + + return m + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_el_gamal(self) -> None: + x, h = self.gen_key() + orig_msg = b"The quick brown fox jumps over the lazy dog.".ljust(64) + + # Happens to be a valid point. + m0 = Ristretto255Point(orig_msg[:32]) + e0, f0 = self.encrypt(h, m0) + + # Happens to be a valid point too. Blessed be the lazy dog! + m1 = Ristretto255Point(orig_msg[32:]) + e1, f1 = self.encrypt(h, m1) + + d0 = self.decrypt(e0, f0, x) + d1 = self.decrypt(e1, f1, x) + decr_msg = bytes(d0) + bytes(d1) + + assert orig_msg == decr_msg + + +class TestShamir: + """ + Shamir's Secret Sharing + """ + + class Polynomial: + _coeffs: List[Ristretto255Scalar] + _zero: Ristretto255Scalar + + def __init__( + self, coeffs: List[Ristretto255Scalar], zero: Ristretto255Scalar + ) -> None: + self._coeffs = coeffs + self._zero = zero + + def __call__(self, i: int) -> Ristretto255Scalar: + return sum( + ( + coeff * Ristretto255Scalar(i ** j) + for j, coeff in enumerate(self._coeffs) + ), + self._zero, + ) + + def __getitem__(self, idx: int) -> Ristretto255Scalar: + return self._coeffs[idx] + + def share_secret( + self, share_count: int, qualified_size: int + ) -> Tuple[Ristretto255Point, List[Tuple[int, Ristretto255Point]]]: + gen = Ristretto255Point.random() + + alpha = self.Polynomial( + [Ristretto255Scalar.random() for __ in range(qualified_size)], + Ristretto255Scalar.ZERO, + ) + + secret = gen * alpha[0] + shares = [(i, gen * alpha(i)) for i in range(1, share_count + 1)] + + return secret, shares + + def reconstruct( + self, shares: List[Tuple[int, Ristretto255Point]] + ) -> Ristretto255Point: + return sum( + ( + share + * reduce( + mul, + ( + Fraction(idx1, idx1 - idx0) + for idx1, __ in shares + if idx0 != idx1 + ), + Fraction(1), + ) + for idx0, share in shares + ), + Ristretto255Point.ZERO, + ) + + @pytest.mark.skipif( + not has_crypto_core_ristretto25519 + or not has_crypto_scalarmult_ristretto25519, + reason="Requires full build of libsodium", + ) + def test_shamir(self) -> None: + secret0, shares = self.share_secret(5, 3) + + # Delete any two shares + del shares[randrange(len(shares))] + del shares[randrange(len(shares))] + + secret1 = self.reconstruct(shares) + + assert secret0 == secret1