From ff5f89d4cb75e478fae54b511305ecf496d44bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Wed, 9 Dec 2020 11:25:33 +0100 Subject: [PATCH 01/16] Remove references to ed25519_from_uniform from __all__. See #408 and #493. --- src/nacl/bindings/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/nacl/bindings/__init__.py b/src/nacl/bindings/__init__.py index 1e3b10e8..64e7b0b4 100644 --- a/src/nacl/bindings/__init__.py +++ b/src/nacl/bindings/__init__.py @@ -283,11 +283,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", From 2ffb1725d910147e5bb7ef24cc07de838aeb96b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Fri, 11 Dec 2020 01:50:22 +0100 Subject: [PATCH 02/16] Add bindings to Ristretto finite group arithmetics --- src/bindings/crypto_core_ristretto255.h | 35 ++ src/bindings/crypto_scalarmult_ristretto255.h | 20 + src/bindings/utils.h | 1 + src/nacl/bindings/__init__.py | 50 ++ src/nacl/bindings/crypto_core_ristretto255.py | 400 ++++++++++++ .../crypto_scalarmult_ristretto255.py | 103 ++++ src/nacl/bindings/utils.py | 17 +- src/nacl/ristretto.py | 340 +++++++++++ src/nacl/utils.py | 23 + tests/data/ristretto255.json | 72 +++ tests/test_bindings.py | 8 + tests/test_ristretto.py | 573 ++++++++++++++++++ 12 files changed, 1641 insertions(+), 1 deletion(-) create mode 100644 src/bindings/crypto_core_ristretto255.h create mode 100644 src/bindings/crypto_scalarmult_ristretto255.h create mode 100644 src/nacl/bindings/crypto_core_ristretto255.py create mode 100644 src/nacl/bindings/crypto_scalarmult_ristretto255.py create mode 100644 src/nacl/ristretto.py create mode 100644 tests/data/ristretto255.json create mode 100644 tests/test_ristretto.py diff --git a/src/bindings/crypto_core_ristretto255.h b/src/bindings/crypto_core_ristretto255.h new file mode 100644 index 00000000..5e6f20ad --- /dev/null +++ b/src/bindings/crypto_core_ristretto255.h @@ -0,0 +1,35 @@ +/* Copyright 2020 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. + */ + +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 00000000..35459242 --- /dev/null +++ b/src/bindings/crypto_scalarmult_ristretto255.h @@ -0,0 +1,20 @@ +/* Copyright 2020 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. + */ + +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/utils.h b/src/bindings/utils.h index 061061f1..e81efe9c 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 64e7b0b4..de00f26f 100644 --- a/src/nacl/bindings/__init__.py +++ b/src/nacl/bindings/__init__.py @@ -71,6 +71,26 @@ 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, +) from nacl.bindings.crypto_generichash import ( crypto_generichash_BYTES, crypto_generichash_BYTES_MAX, @@ -173,6 +193,12 @@ 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, +) from nacl.bindings.crypto_secretbox import ( crypto_secretbox, crypto_secretbox_BOXZEROBYTES, @@ -236,6 +262,7 @@ from nacl.bindings.utils import ( sodium_add, sodium_increment, + sodium_is_zero, sodium_memcmp, sodium_pad, sodium_unpad, @@ -295,6 +322,24 @@ "crypto_core_ed25519_scalar_sub", "crypto_core_ed25519_scalar_mul", "crypto_core_ed25519_scalar_reduce", + "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", @@ -333,6 +378,10 @@ "crypto_scalarmult_ed25519_base", "crypto_scalarmult_ed25519_noclamp", "crypto_scalarmult_ed25519_base_noclamp", + "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", @@ -439,6 +488,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 00000000..5a63e9c9 --- /dev/null +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -0,0 +1,400 @@ +# Copyright 2020 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 __future__ import absolute_import, division, print_function + +from nacl import exceptions as exc +from nacl._sodium import ffi, lib +from nacl.exceptions import ensure + + +# Group order L of both the scalar group and group of points. +crypto_core_ristretto255_GROUP_ORDER = ( + 2 ** 252 + 27742317777372353535851937790883648493 +) + +# 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, y): + """ + 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 + :type x: bytes + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :type y: bytes + :rtype: bytes + """ + 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): + """ + 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 + :type s: bytes + :rtype: bytes + """ + 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): + """ + 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 + :type s: bytes + :rtype: bytes + :raises ValueError: if the value is not invertible + """ + 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, y): + """ + 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 + :type x: bytes + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :type y: bytes + :rtype: bytes + """ + 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): + """ + 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 + :type s: bytes + :rtype: bytes + """ + 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(): + """ + Generate a random non-zero scalar modulo ``L``. + + :rtype: bytes + """ + 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): + """ + 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 + :type s: bytes + :rtype: bytes + """ + 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, y): + """ + 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 + :type x: bytes + :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` + bytes in little endian order representing the second scalar + :type y: bytes + :rtype: bytes + """ + 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)[:] + + +# 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() + + +def crypto_core_ristretto255_add(p, q): + """ + 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 + :type p: bytes + :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the second point + :type q: bytes + :rtype: bytes + """ + 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): + """ + 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 + :type r: bytes + :rtype: bytes + """ + 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): + """ + Check if ``p`` is a valid point. + + :param p: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the value to check + :type p: bytes + :return: False if invalid, True if valid + :rtype: bool + """ + 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(): + """ + Generate a random Ristretto255 point. This might be, + although astronomically unlikely, the zero point. + + :rtype: bytes + """ + 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, q): + """ + Subtract point ``q`` from ``p``. + + :param p: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the first point + :type p: bytes + :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` + bytes representing the second point + :type q: bytes + :rtype: bytes + """ + 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 00000000..2d89f61b --- /dev/null +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -0,0 +1,103 @@ +# Copyright 2020 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 __future__ import absolute_import, division, print_function + +from nacl import exceptions as exc +from nacl._sodium import ffi, lib +from nacl.exceptions import ensure + +# 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): + """ + 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 + :type n: bytes + :rtype: bytes + :raises exc.RuntimeError: on error or if result is zero + """ + 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", raising=exc.RuntimeError) + + return ffi.buffer(q, crypto_scalarmult_ristretto255_BYTES)[:] + + +def crypto_scalarmult_ristretto255(n, p): + """ + 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 + :type n: bytes + :param p: a sequence of :py:data:`.crypto_scalarmult_ristretto255_BYTES` + bytes in little endian order representing the point + :type p: bytes + :rtype: bytes + :raises exc.RuntimeError: on error or if result is zero + """ + 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. + ensure(rc == 0, "Unexpected library error", 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 0ff22e34..cfef6d5f 100644 --- a/src/nacl/bindings/utils.py +++ b/src/nacl/bindings/utils.py @@ -17,6 +17,22 @@ from nacl.exceptions import ensure +def sodium_is_zero(inp): + """ + Check if all bytes in ``inp`` are zero + + :param inp: input bytes string + :type inp: bytes + :return: False if any byte is nonzero, else True + :rtype: bool + """ + 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 +112,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 00000000..47c417c8 --- /dev/null +++ b/src/nacl/ristretto.py @@ -0,0 +1,340 @@ +# Copyright 2013 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 __future__ import absolute_import, division, print_function + +from binascii import hexlify +from fractions import Fraction + +import six + +import nacl.bindings +from nacl import exceptions as exc +from nacl.utils import ( + bytes_as_string, + int_to_little_endian, + little_endian_to_int, + random, +) + + +class Ristretto255Scalar(object): + SIZE = nacl.bindings.crypto_core_ristretto255_SCALAR_BYTES + NONREDUCED_SIZE = ( + nacl.bindings.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES + ) + ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + + def __init__(self, value): + if isinstance(value, Ristretto255Scalar): + self._value = value._value + elif isinstance(value, bytes): + if len(value) != Ristretto255Scalar.SIZE: + raise exc.ValueError + self._value = value + elif isinstance(value, six.integer_types): + self._value = int_to_little_endian( + value % Ristretto255Scalar.ORDER, Ristretto255Scalar.SIZE + ) + elif isinstance(value, Fraction): + self._value = ( + Ristretto255Scalar(value.numerator) + * Ristretto255Scalar(value.denominator).inverse + )._value + else: + raise exc.TypeError + + @classmethod + def random(cls): + """ + Get a non-zero random scalar. + """ + + return cls(nacl.bindings.crypto_core_ristretto255_scalar_random()) + + @classmethod + def random_zero(cls): + """ + Get a random scalar that could also be zero. + """ + return cls.reduce(random(cls.NONREDUCED_SIZE)) + + @classmethod + def reduce(cls, value): + """ + 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. + """ + return cls(nacl.bindings.crypto_core_ristretto255_scalar_reduce(value)) + + @property + def inverse(self): + """ + Get multiplicative inverse such that ``x.inverse * x == 1``. + """ + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_invert(self._value) + ) + + @property + def complement(self): + """ + Get the complement such that ``x.complement + x == 1``. + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_complement( + self._value + ) + ) + + def __add__(self, other): + """ + Add two scalars. + """ + + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_add( + self._value, Ristretto255Scalar(other)._value + ) + ) + + def __radd__(self, other): + return self + other + + def __sub__(self, other): + """ + Subtract to scalars. + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_sub( + self._value, Ristretto255Scalar(other)._value + ) + ) + + def __rsub__(self, other): + return -(self - other) + + def __mul__(self, other): + """ + Multiply two scalars. + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_mul( + self._value, Ristretto255Scalar(other)._value + ) + ) + + def __rmul__(self, other): + return self * other + + def __neg__(self): + """ + Get the additive inverse such that ``-x + x == 0``. + """ + return Ristretto255Scalar( + nacl.bindings.crypto_core_ristretto255_scalar_negate(self._value) + ) + + def __eq__(self, other): + """ + Check if two scalars are identical. Comparing with other types such as + ``int`` will return False. + """ + if not isinstance(other, self.__class__): + return False + + return nacl.bindings.sodium_memcmp(self._value, other._value) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(self._value) + + def __bytes__(self): + return self._value + + def __int__(self): + return little_endian_to_int(self._value) + + def __bool__(self): + return not nacl.bindings.sodium_is_zero(self._value) + + def __nonzero__(self): + return self.__bool__() + + def __repr__(self): + return "Ristretto255Scalar({})".format(int(self)) + + def __str__(self): + if six.PY2: + return self.__bytes__() + else: + return repr(self) + + def __unicode__(self): + return repr(self).decode() + + +# 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(object): + SIZE = nacl.bindings.crypto_core_ristretto255_BYTES + HASH_SIZE = nacl.bindings.crypto_core_ristretto255_HASH_BYTES + ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + + def __init__(self, value, _assume_valid=False): + 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): + """ + 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. + """ + return cls( + nacl.bindings.crypto_core_ristretto255_from_hash(value), + _assume_valid=True, + ) + + @classmethod + def random(cls): + """ + Generate a random Ristretto255 point. This might be, + although astronomically unlikely, the zero point. + """ + return cls( + nacl.bindings.crypto_core_ristretto255_random(), _assume_valid=True + ) + + @classmethod + def base_mul(cls, n): + """ + Multiply the scalar ``n`` with the Ed25519 base point. + """ + return cls( + nacl.bindings.crypto_scalarmult_ristretto255_base( + bytes(Ristretto255Scalar(n)) + ), + _assume_valid=True, + ) + + def __neg__(self): + """ + Get inverse element such that ``-x + x == Ristretto255Point.ZERO``. + """ + return self * Ristretto255Scalar.MINUS_ONE + + def __add__(self, other): + """ + Add two points. + """ + if not isinstance(other, Ristretto255Point): + raise exc.TypeError("Operand must be another Ristretto255Point") + + return Ristretto255Point( + nacl.bindings.crypto_core_ristretto255_add( + self._value, other._value + ), + _assume_valid=True, + ) + + def __sub__(self, other): + """ + Subtract two points. + """ + if not isinstance(other, Ristretto255Point): + raise exc.TypeError("Operand must be another Ristretto255Point") + + return Ristretto255Point( + nacl.bindings.crypto_core_ristretto255_sub( + self._value, other._value + ), + _assume_valid=True, + ) + + def __mul__(self, other): + """ + Multiply the scalar ``n`` with the point. + """ + return Ristretto255Point( + nacl.bindings.crypto_scalarmult_ristretto255( + bytes(Ristretto255Scalar(other)), self._value + ), + _assume_valid=True, + ) + + def __rmul__(self, other): + return self * other + + def __bool__(self): + """ + Check if this is *not* the zero / neutral / identity point. + + :return: False if zero point, else True + :rtype: bool + """ + return not nacl.bindings.sodium_is_zero(self._value) + + def __nonzero__(self): + return self.__bool__() + + def __eq__(self, other): + if not isinstance(other, self.__class__): + return False + + return nacl.bindings.sodium_memcmp(self._value, other._value) + + def __ne__(self, other): + return not (self == other) + + def __bytes__(self): + return self._value + + def __hash__(self): + return hash(self._value) + + def __repr__(self): + return "Ristretto255Point('{}')".format( + bytes_as_string(hexlify(bytes(self))) + ) + + def __str__(self): + if six.PY2: + return self.__bytes__() + else: + return repr(self) + + def __unicode__(self): + return repr(self).decode() + + +# Neutral element +Ristretto255Point.ZERO = Ristretto255Point( + b"\x00" * Ristretto255Point.SIZE, _assume_valid=True +) diff --git a/src/nacl/utils.py b/src/nacl/utils.py index d19d236a..a2e29b3c 100644 --- a/src/nacl/utils.py +++ b/src/nacl/utils.py @@ -86,3 +86,26 @@ def randombytes_deterministic( raw_data = nacl.bindings.randombytes_buf_deterministic(size, seed) return encoder.encode(raw_data) + + +def int_to_little_endian(value, size): + if six.PY3: + return value.to_bytes(size, "little") + else: + result = b"" + for i in range(size): + result += chr(value & 0xFF) + value >>= 8 + if value: + raise OverflowError + return result + + +def little_endian_to_int(value): + if six.PY3: + return int.from_bytes(value, "little") + else: + result = 0 + for byte in reversed(value): + result = (result << 8) | ord(byte) + return result diff --git a/tests/data/ristretto255.json b/tests/data/ristretto255.json new file mode 100644 index 00000000..7adec492 --- /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 3bb726ff..7089a5be 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -327,6 +327,14 @@ 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) + with pytest.raises(TypeError): + c.sodium_is_zero(u"zero") + + @given(integers(min_value=-2, max_value=0)) def test_pad_wrong_blocksize(bl_sz): with pytest.raises(ValueError): diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py new file mode 100644 index 00000000..d13a2bb4 --- /dev/null +++ b/tests/test_ristretto.py @@ -0,0 +1,573 @@ +# Copyright 2020 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 __future__ import absolute_import, division, print_function + +import json +import os +from binascii import unhexlify +from fractions import Fraction +from functools import reduce +from hashlib import sha256, sha512 +from operator import mul +from random import randrange + +import pytest + +from six import int2byte + +import nacl.exceptions as exc +from nacl.ristretto import Ristretto255Point, Ristretto255Scalar + + +def _ristretto255_vectors(): + """ + 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 { + "encodings_of_small_multiples": [ + (idx, unhexlify(enc)) + for idx, enc in enumerate(vectors["encodings_of_small_multiples"]) + ], + "bad_encodings": [unhexlify(enc) for enc in vectors["bad_encodings"]], + "label_hash_to_points": [ + (label, unhexlify(enc)) + for label, enc in zip( + vectors["labels"], vectors["encoded_hash_to_points"] + ) + ], + } + + +class TestRistretto255Scalar(object): + order = 7237005577332262213973186563042994240857116359379907606001950938285454250989 + order_bytes = unhexlify( + "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" + ) + + def test_init(self): + + dgst = sha256(b"hello").digest() + s = Ristretto255Scalar(dgst) + assert bytes(s) == dgst + + 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 = unhexlify( + "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) + + 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 + + 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 + + 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 = unhexlify( + "b586c3423482ab97d876ce24cab8bd8ab84e22ac3a52a8dfbb330bbe92a3260f" + ) + + assert bytes(Ristretto255Scalar.reduce(dgst)) == reduced_dgst + + def test_inverse(self): + assert Ristretto255Scalar(1).inverse == Ristretto255Scalar.ONE + s = Ristretto255Scalar.random() + assert s.inverse * s == Ristretto255Scalar.ONE + + t = Ristretto255Scalar(b"".join(int2byte(i) for i in range(32))) + + # pow(int.from_bytes(bytes(range(32)), "little"), -1, order).to_bytes(32, "little").hex() + inv = unhexlify( + "0cf17e6d77775ab76bd4f41cd2ef9ecc9ddd8242185bd685a60b49b5b3f16606" + ) + + assert bytes(t.inverse) == inv + + 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(b"".join(int2byte(i) for i in range(32))) + # ((1 - int.from_bytes(bytes(range(32)), "little")) % order).to_bytes(32, "little").hex() + compl = unhexlify( + "dba6e9b630c11ea9a430e53ab1e6af1af0eeedecebeae9e8e7e6e5e4e3e2e100" + ) + + assert bytes(t.complement) == compl + + def test_add(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(579) + + 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 + + def test_sub(self): + s = Ristretto255Scalar(123) + t = Ristretto255Scalar(456) + u = Ristretto255Scalar(579) + + 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) + + 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 = unhexlify( + "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 + + 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 + + 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 s != 123 + + 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 + + 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 + + def test_bytes(self): + s = Ristretto255Scalar(123) + assert type(bytes(s)) is bytes + assert len(bytes(s)) == 32 + + 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 + + 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) + + def test_repr(self): + s = Ristretto255Scalar(123) + assert repr(s) == "Ristretto255Scalar(123)" + + +class TestRistretto255Point(object): + _vectors = _ristretto255_vectors() + + @pytest.mark.parametrize( + ("idx", "encoding"), _vectors["encodings_of_small_multiples"] + ) + def test_small_multiples(self, idx, encoding): + base = Ristretto255Point( + unhexlify( + "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + ) + ) + point = Ristretto255Point.ZERO + + for i 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.parametrize(("encoding"), _vectors["bad_encodings"]) + def test_bad_encodings(self, encoding): + with pytest.raises(exc.ValueError): + Ristretto255Point(encoding) + + @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 + + def test_init(self): + with pytest.raises(exc.TypeError): + Ristretto255Point(b"too short") + + with pytest.raises(exc.TypeError): + Ristretto255Point(3.14) + + # good code paths are tested elsewhere. + + 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 + + def test_neg(self): + p = Ristretto255Point.random() + q = -p + assert p != q + assert p + q == Ristretto255Point.ZERO + assert p - q == p + p + + def test_add(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + r = Ristretto255Point.random() + + with pytest.raises(exc.TypeError): + p + 123 + + 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 + + def test_sub(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + r = Ristretto255Point.random() + + with pytest.raises(exc.TypeError): + p - 123 + + 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) + + def test_mul(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + + with pytest.raises(exc.TypeError): + p * q + + with pytest.raises(exc.TypeError): + p * u"test" + + 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 p * Fraction(8, 1) == p * 8 + assert 27 * p * Fraction(-11, 27) == p * -11 + + def test_bool(self): + p = Ristretto255Point.random() + + assert not Ristretto255Point.ZERO + assert bool(Ristretto255Point.base_mul(1)) + assert not (p - p) + + def test_eq(self): + p = Ristretto255Point.random() + q = Ristretto255Point.random() + assert p != u"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 + + def test_bytes(self): + base = Ristretto255Point.base_mul(1) + enc0 = bytes(base) + enc1 = unhexlify( + "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" + ) + assert enc0 == enc1 + + p = Ristretto255Point.random() + assert type(bytes(p)) is bytes + assert len(bytes(p)) == 32 + + 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 + + def test_repr(self): + base = Ristretto255Point.base_mul(1) + assert ( + repr(base) + == "Ristretto255Point('e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76')" + ) + + 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(object): + """ + ElGamal encryption. + """ + + def gen_key(self): + x = Ristretto255Scalar.random() + h = Ristretto255Point.base_mul(x) + + return x, h + + def encrypt(self, h, m): + y = Ristretto255Scalar.random() + s = h * y + c0 = Ristretto255Point.base_mul(y) + c1 = m + s + + return c0, c1 + + def decrypt(self, c0, c1, x): + s = c0 * x + m = c1 - s + + return m + + def test_el_gamal(self): + 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(object): + """ + Shamir's Secret Sharing + """ + + class Polynomial: + def __init__(self, coeffs, zero): + self._coeffs = list(coeffs) + self._zero = zero + + def __call__(self, i): + return sum( + ( + coeff * Ristretto255Scalar(i ** j) + for j, coeff in enumerate(self._coeffs) + ), + self._zero, + ) + + def __getitem__(self, idx): + return self._coeffs[idx] + + def share_secret(self, share_count, qualified_size): + 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): + 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, + ) + + def test_shamir(self): + 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 From fa4800130139cc3aceed83863bf6a8b54cf22871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Sun, 16 May 2021 18:38:45 +0200 Subject: [PATCH 03/16] Rebase; remove py2 support; support minimal build --- src/bindings/crypto_core_ristretto255.h | 4 +- src/bindings/crypto_scalarmult_ristretto255.h | 4 +- .../minimal/crypto_core_ristretto255.h | 41 ++++ .../minimal/crypto_scalarmult_ristretto255.h | 26 +++ src/nacl/bindings/__init__.py | 4 + src/nacl/bindings/crypto_core_ristretto255.py | 142 +++++++++-- .../crypto_scalarmult_ristretto255.py | 42 +++- src/nacl/ristretto.py | 68 ++---- src/nacl/utils.py | 23 -- tests/test_bindings.py | 50 +++- tests/test_ristretto.py | 220 +++++++++++++++--- 11 files changed, 502 insertions(+), 122 deletions(-) create mode 100644 src/bindings/minimal/crypto_core_ristretto255.h create mode 100644 src/bindings/minimal/crypto_scalarmult_ristretto255.h diff --git a/src/bindings/crypto_core_ristretto255.h b/src/bindings/crypto_core_ristretto255.h index 5e6f20ad..fbd69011 100644 --- a/src/bindings/crypto_core_ristretto255.h +++ b/src/bindings/crypto_core_ristretto255.h @@ -1,4 +1,4 @@ -/* Copyright 2020 Donald Stufft and individual contributors +/* 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. @@ -13,6 +13,8 @@ * 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); diff --git a/src/bindings/crypto_scalarmult_ristretto255.h b/src/bindings/crypto_scalarmult_ristretto255.h index 35459242..befe787a 100644 --- a/src/bindings/crypto_scalarmult_ristretto255.h +++ b/src/bindings/crypto_scalarmult_ristretto255.h @@ -1,4 +1,4 @@ -/* Copyright 2020 Donald Stufft and individual contributors +/* 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. @@ -13,6 +13,8 @@ * 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); diff --git a/src/bindings/minimal/crypto_core_ristretto255.h b/src/bindings/minimal/crypto_core_ristretto255.h new file mode 100644 index 00000000..ef1d1ddb --- /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 00000000..f02a001f --- /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/nacl/bindings/__init__.py b/src/nacl/bindings/__init__.py index de00f26f..dfea5f8c 100644 --- a/src/nacl/bindings/__init__.py +++ b/src/nacl/bindings/__init__.py @@ -90,6 +90,7 @@ 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, @@ -198,6 +199,7 @@ 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, @@ -322,6 +324,7 @@ "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", @@ -378,6 +381,7 @@ "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", diff --git a/src/nacl/bindings/crypto_core_ristretto255.py b/src/nacl/bindings/crypto_core_ristretto255.py index 5a63e9c9..39323fa5 100644 --- a/src/nacl/bindings/crypto_core_ristretto255.py +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -1,4 +1,4 @@ -# Copyright 2020 Donald Stufft and individual contributors +# 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. @@ -18,21 +18,28 @@ 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 ) -# Size of a Ristretto255 scalar. -crypto_core_ristretto255_SCALAR_BYTES = ( - lib.crypto_core_ristretto255_scalarbytes() -) +if has_crypto_core_ristretto25519: + # 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() -) + # Size of values that are reduced modulo the order to a Ristretto255 scalar. + crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = ( + lib.crypto_core_ristretto255_nonreducedscalarbytes() + ) +else: # pragma: no cover + crypto_core_ristretto255_SCALAR_BYTES = 0 + crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = 0 def crypto_core_ristretto255_scalar_add(x, y): @@ -46,7 +53,15 @@ def crypto_core_ristretto255_scalar_add(x, y): bytes in little endian order representing the second scalar :type y: bytes :rtype: bytes + :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, @@ -79,7 +94,15 @@ def crypto_core_ristretto255_scalar_complement(s): bytes in little endian order representing the scalar :type s: bytes :rtype: bytes + :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, @@ -105,7 +128,15 @@ def crypto_core_ristretto255_scalar_invert(s): :type s: bytes :rtype: bytes :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, @@ -134,7 +165,15 @@ def crypto_core_ristretto255_scalar_mul(x, y): bytes in little endian order representing the second scalar :type y: bytes :rtype: bytes + :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, @@ -168,7 +207,14 @@ def crypto_core_ristretto255_scalar_negate(s): bytes in little endian order representing the scalar :type s: bytes :rtype: bytes + :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, @@ -189,7 +235,15 @@ def crypto_core_ristretto255_scalar_random(): Generate a random non-zero scalar modulo ``L``. :rtype: bytes + :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) @@ -207,7 +261,15 @@ def crypto_core_ristretto255_scalar_reduce(s): to a Ristretto255 scalar :type s: bytes :rtype: bytes + :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, @@ -234,7 +296,15 @@ def crypto_core_ristretto255_scalar_sub(x, y): bytes in little endian order representing the second scalar :type y: bytes :rtype: bytes + :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, @@ -259,11 +329,17 @@ def crypto_core_ristretto255_scalar_sub(x, y): return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] -# Size of a Ristretto255 point. -crypto_core_ristretto255_BYTES = lib.crypto_core_ristretto255_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() + # 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, q): @@ -277,7 +353,15 @@ def crypto_core_ristretto255_add(p, q): bytes representing the second point :type q: bytes :rtype: bytes + :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( @@ -311,7 +395,15 @@ def crypto_core_ristretto255_from_hash(r): bytes representing the value to convert :type r: bytes :rtype: bytes + :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( @@ -337,7 +429,15 @@ def crypto_core_ristretto255_is_valid_point(p): :type p: bytes :return: False if invalid, True if valid :rtype: bool + :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( @@ -357,7 +457,15 @@ def crypto_core_ristretto255_random(): although astronomically unlikely, the zero point. :rtype: bytes + :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) @@ -375,7 +483,15 @@ def crypto_core_ristretto255_sub(p, q): bytes representing the second point :type q: bytes :rtype: bytes + :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( diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py index 2d89f61b..66b31c75 100644 --- a/src/nacl/bindings/crypto_scalarmult_ristretto255.py +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -1,4 +1,4 @@ -# Copyright 2020 Donald Stufft and individual contributors +# 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. @@ -18,16 +18,24 @@ from nacl._sodium import ffi, lib from nacl.exceptions import ensure -# Size of a Ristretto255 point. -# Should equal crypto_core_ristretto255_BYTES -crypto_scalarmult_ristretto255_BYTES = ( - lib.crypto_scalarmult_ristretto255_bytes() +has_crypto_scalarmult_ristretto25519 = bool( + lib.PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519 ) -# Size of scalars for the two functions. -crypto_scalarmult_ristretto255_SCALAR_BYTES = ( - lib.crypto_scalarmult_ristretto255_scalarbytes() -) +if has_crypto_scalarmult_ristretto25519: + # 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() + ) +else: # pragma: no cover + crypto_scalarmult_ristretto255_BYTES = 0 + crypto_scalarmult_ristretto255_SCALAR_BYTES = 0 def crypto_scalarmult_ristretto255_base(n): @@ -40,7 +48,15 @@ def crypto_scalarmult_ristretto255_base(n): :type n: bytes :rtype: bytes :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, @@ -74,7 +90,15 @@ def crypto_scalarmult_ristretto255(n, p): :type p: bytes :rtype: bytes :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, diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index 47c417c8..aba1ffc9 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -1,4 +1,4 @@ -# Copyright 2013 Donald Stufft and individual contributors +# 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. @@ -14,19 +14,11 @@ from __future__ import absolute_import, division, print_function -from binascii import hexlify from fractions import Fraction -import six - import nacl.bindings from nacl import exceptions as exc -from nacl.utils import ( - bytes_as_string, - int_to_little_endian, - little_endian_to_int, - random, -) +from nacl.utils import random class Ristretto255Scalar(object): @@ -43,9 +35,9 @@ def __init__(self, value): if len(value) != Ristretto255Scalar.SIZE: raise exc.ValueError self._value = value - elif isinstance(value, six.integer_types): - self._value = int_to_little_endian( - value % Ristretto255Scalar.ORDER, Ristretto255Scalar.SIZE + elif isinstance(value, int): + self._value = (value % Ristretto255Scalar.ORDER).to_bytes( + Ristretto255Scalar.SIZE, "little" ) elif isinstance(value, Fraction): self._value = ( @@ -167,35 +159,36 @@ def __bytes__(self): return self._value def __int__(self): - return little_endian_to_int(self._value) + return int.from_bytes(self._value, "little") def __bool__(self): return not nacl.bindings.sodium_is_zero(self._value) - def __nonzero__(self): - return self.__bool__() - def __repr__(self): return "Ristretto255Scalar({})".format(int(self)) def __str__(self): - if six.PY2: - return self.__bytes__() - else: - return repr(self) + return repr(self) - def __unicode__(self): - return repr(self).decode() +if nacl.bindings.has_crypto_core_ristretto25519: + # Neutral additive element + Ristretto255Scalar.ZERO = Ristretto255Scalar(0) -# Neutral additive element -Ristretto255Scalar.ZERO = Ristretto255Scalar(0) + # Neutral multiplicative element + Ristretto255Scalar.ONE = Ristretto255Scalar(1) -# Neutral multiplicative element -Ristretto255Scalar.ONE = Ristretto255Scalar(1) + # Constant needed for inverting points + Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar(-1) +else: # pragma: no cover -# Constant needed for inverting points -Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar(-1) + Ristretto255Scalar.ZERO = Ristretto255Scalar( + bytes(Ristretto255Scalar.SIZE) + ) + Ristretto255Scalar.ONE = Ristretto255Scalar(bytes(Ristretto255Scalar.SIZE)) + Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar( + bytes(Ristretto255Scalar.SIZE) + ) class Ristretto255Point(object): @@ -301,9 +294,6 @@ def __bool__(self): """ return not nacl.bindings.sodium_is_zero(self._value) - def __nonzero__(self): - return self.__bool__() - def __eq__(self, other): if not isinstance(other, self.__class__): return False @@ -320,21 +310,13 @@ def __hash__(self): return hash(self._value) def __repr__(self): - return "Ristretto255Point('{}')".format( - bytes_as_string(hexlify(bytes(self))) - ) + return "Ristretto255Point({!r})".format(bytes(self)) def __str__(self): - if six.PY2: - return self.__bytes__() - else: - return repr(self) - - def __unicode__(self): - return repr(self).decode() + return "Ristretto255Point({})".format(bytes(self).hex()) # Neutral element Ristretto255Point.ZERO = Ristretto255Point( - b"\x00" * Ristretto255Point.SIZE, _assume_valid=True + bytes(Ristretto255Point.SIZE), _assume_valid=True ) diff --git a/src/nacl/utils.py b/src/nacl/utils.py index a2e29b3c..d19d236a 100644 --- a/src/nacl/utils.py +++ b/src/nacl/utils.py @@ -86,26 +86,3 @@ def randombytes_deterministic( raw_data = nacl.bindings.randombytes_buf_deterministic(size, seed) return encoder.encode(raw_data) - - -def int_to_little_endian(value, size): - if six.PY3: - return value.to_bytes(size, "little") - else: - result = b"" - for i in range(size): - result += chr(value & 0xFF) - value >>= 8 - if value: - raise OverflowError - return result - - -def little_endian_to_int(value): - if six.PY3: - return int.from_bytes(value, "little") - else: - result = 0 - for byte in reversed(value): - result = (result << 8) | ord(byte) - return result diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 7089a5be..82ea38bb 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -332,7 +332,7 @@ def test_sodium_is_zero(): assert c.sodium_is_zero(b"\x00" * 37) assert not c.sodium_is_zero(b"\x00" * 13 + b"\xe1" + b"\x00" * 22) with pytest.raises(TypeError): - c.sodium_is_zero(u"zero") + c.sodium_is_zero("zero") @given(integers(min_value=-2, max_value=0)) @@ -890,3 +890,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 index d13a2bb4..b42300b6 100644 --- a/tests/test_ristretto.py +++ b/tests/test_ristretto.py @@ -1,4 +1,4 @@ -# Copyright 2020 Donald Stufft and individual contributors +# 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. @@ -16,7 +16,6 @@ import json import os -from binascii import unhexlify from fractions import Fraction from functools import reduce from hashlib import sha256, sha512 @@ -25,9 +24,11 @@ import pytest -from six import int2byte - import nacl.exceptions as exc +from nacl.bindings import ( + has_crypto_core_ristretto25519, + has_crypto_scalarmult_ristretto25519, +) from nacl.ristretto import Ristretto255Point, Ristretto255Scalar @@ -41,12 +42,14 @@ def _ristretto255_vectors(): return { "encodings_of_small_multiples": [ - (idx, unhexlify(enc)) + (idx, bytes.fromhex(enc)) for idx, enc in enumerate(vectors["encodings_of_small_multiples"]) ], - "bad_encodings": [unhexlify(enc) for enc in vectors["bad_encodings"]], + "bad_encodings": [ + bytes.fromhex(enc) for enc in vectors["bad_encodings"] + ], "label_hash_to_points": [ - (label, unhexlify(enc)) + (label, bytes.fromhex(enc)) for label, enc in zip( vectors["labels"], vectors["encoded_hash_to_points"] ) @@ -56,10 +59,14 @@ def _ristretto255_vectors(): class TestRistretto255Scalar(object): order = 7237005577332262213973186563042994240857116359379907606001950938285454250989 - order_bytes = unhexlify( + order_bytes = bytes.fromhex( "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" ) + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) def test_init(self): dgst = sha256(b"hello").digest() @@ -77,7 +84,7 @@ def test_init(self): bytes(Ristretto255Scalar(Fraction(5, 1))) == b"\x05" + b"\x00" * 31 ) # (pow(3, -1, order) * 5 % order).to_bytes(32, "little").hex() - five_thirds = unhexlify( + five_thirds = bytes.fromhex( "a646a7c9082106c89c8952364a534a5c55555555555555555555555555555505" ) assert bytes(Ristretto255Scalar(Fraction(5, 3))) == five_thirds @@ -88,6 +95,10 @@ def test_init(self): with pytest.raises(exc.TypeError): Ristretto255Scalar(3.14) + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) def test_random(self): s = Ristretto255Scalar.random() t = Ristretto255Scalar.random() @@ -96,6 +107,10 @@ def test_random(self): # 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() @@ -104,6 +119,10 @@ def test_random_zero(self): # 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)) @@ -112,26 +131,34 @@ def test_reduce(self): dgst = sha512(b"hello").digest() # (int.from_bytes(sha512(b"hello").digest(), "little") % order).to_bytes(32, "little").hex() - reduced_dgst = unhexlify( + 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(b"".join(int2byte(i) for i in range(32))) + t = Ristretto255Scalar(bytes(range(32))) # pow(int.from_bytes(bytes(range(32)), "little"), -1, order).to_bytes(32, "little").hex() - inv = unhexlify( + 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 @@ -139,14 +166,18 @@ def test_complement(self): s = Ristretto255Scalar.random() assert s.complement + s == Ristretto255Scalar.ONE - t = Ristretto255Scalar(b"".join(int2byte(i) for i in range(32))) + t = Ristretto255Scalar(bytes(range(32))) # ((1 - int.from_bytes(bytes(range(32)), "little")) % order).to_bytes(32, "little").hex() - compl = unhexlify( + 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) @@ -166,6 +197,10 @@ def test_add(self): 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) @@ -183,6 +218,10 @@ def test_sub(self): 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) @@ -198,7 +237,7 @@ def test_mul(self): 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 = unhexlify( + x = bytes.fromhex( "7d808bf1fafea25f3ee660ef3c1793985190ba1413f9b714edf967ce6b8bdd06" ) assert bytes(v * w) == x @@ -211,6 +250,10 @@ def test_mul(self): assert a * Ristretto255Scalar.ZERO == Ristretto255Scalar.ZERO assert a * Ristretto255Scalar.ONE == a + @pytest.mark.skipif( + not has_crypto_core_ristretto25519, + reason="Requires full build of libsodium", + ) def test_neg(self): s = Ristretto255Scalar(123) t = Ristretto255Scalar(-123) @@ -225,6 +268,10 @@ def test_neg(self): 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) @@ -248,6 +295,10 @@ def test_eq(self): 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() @@ -257,11 +308,19 @@ def test_hash(self): 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 @@ -272,6 +331,10 @@ def test_int(self): 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 @@ -286,23 +349,40 @@ def test_bool(self): 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(object): _vectors = _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( - unhexlify( - "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" - ) - ) + base = Ristretto255Point(self._base) point = Ristretto255Point.ZERO for i in range(idx): @@ -315,11 +395,19 @@ def test_small_multiples(self, idx, encoding): 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"] ) @@ -329,6 +417,10 @@ def test_hash_to_point(self, label, encoding): ) 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") @@ -338,6 +430,10 @@ def test_init(self): # 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() @@ -346,6 +442,10 @@ def test_random(self): # 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 @@ -353,6 +453,10 @@ def test_neg(self): 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() @@ -367,6 +471,10 @@ def test_add(self): 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() @@ -381,6 +489,11 @@ def test_sub(self): 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() @@ -389,7 +502,7 @@ def test_mul(self): p * q with pytest.raises(exc.TypeError): - p * u"test" + p * "test" assert p * 3 == 3 * p assert p + p + p == p * 3 @@ -399,6 +512,11 @@ def test_mul(self): assert p * Fraction(8, 1) == p * 8 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() @@ -406,10 +524,15 @@ def test_bool(self): 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 != u"foobar" + assert p != "foobar" assert p == p a = p * 17 + q @@ -420,18 +543,25 @@ def test_eq(self): 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) - enc0 = bytes(base) - enc1 = unhexlify( - "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" - ) - assert enc0 == enc1 + 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() @@ -441,13 +571,31 @@ def test_hash(self): 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('e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76')" - ) + 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 @@ -489,6 +637,11 @@ def decrypt(self, c0, c1, x): 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): x, h = self.gen_key() orig_msg = b"The quick brown fox jumps over the lazy dog.".ljust(64) @@ -561,6 +714,11 @@ def reconstruct(self, 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): secret0, shares = self.share_secret(5, 3) From 974bc439558fd7825e93d598d5c10332ede42261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Mon, 17 May 2021 06:32:24 +0200 Subject: [PATCH 04/16] Remove more py2 code --- src/nacl/bindings/crypto_core_ristretto255.py | 2 -- src/nacl/bindings/crypto_scalarmult_ristretto255.py | 2 -- src/nacl/ristretto.py | 6 ++---- tests/test_ristretto.py | 12 +++++------- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/nacl/bindings/crypto_core_ristretto255.py b/src/nacl/bindings/crypto_core_ristretto255.py index 39323fa5..116ff039 100644 --- a/src/nacl/bindings/crypto_core_ristretto255.py +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function - from nacl import exceptions as exc from nacl._sodium import ffi, lib from nacl.exceptions import ensure diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py index 66b31c75..77ab8759 100644 --- a/src/nacl/bindings/crypto_scalarmult_ristretto255.py +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function - from nacl import exceptions as exc from nacl._sodium import ffi, lib from nacl.exceptions import ensure diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index aba1ffc9..cdf498fc 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function - from fractions import Fraction import nacl.bindings @@ -21,7 +19,7 @@ from nacl.utils import random -class Ristretto255Scalar(object): +class Ristretto255Scalar: SIZE = nacl.bindings.crypto_core_ristretto255_SCALAR_BYTES NONREDUCED_SIZE = ( nacl.bindings.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES @@ -191,7 +189,7 @@ def __str__(self): ) -class Ristretto255Point(object): +class Ristretto255Point: SIZE = nacl.bindings.crypto_core_ristretto255_BYTES HASH_SIZE = nacl.bindings.crypto_core_ristretto255_HASH_BYTES ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py index b42300b6..931644c0 100644 --- a/tests/test_ristretto.py +++ b/tests/test_ristretto.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function - import json import os from fractions import Fraction @@ -57,7 +55,7 @@ def _ristretto255_vectors(): } -class TestRistretto255Scalar(object): +class TestRistretto255Scalar: order = 7237005577332262213973186563042994240857116359379907606001950938285454250989 order_bytes = bytes.fromhex( "edd3f55c1a631258d69cf7a2def9de1400000000000000000000000000000010" @@ -367,7 +365,7 @@ def test_str(self): assert text == "Ristretto255Scalar(123)" -class TestRistretto255Point(object): +class TestRistretto255Point: _vectors = _ristretto255_vectors() _base = bytes.fromhex( "e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76" @@ -385,7 +383,7 @@ def test_small_multiples(self, idx, encoding): base = Ristretto255Point(self._base) point = Ristretto255Point.ZERO - for i in range(idx): + for __ in range(idx): point += base assert bytes(point) == encoding @@ -612,7 +610,7 @@ def test_library_error(self): p * 2 -class TestElGamal(object): +class TestElGamal: """ ElGamal encryption. """ @@ -661,7 +659,7 @@ def test_el_gamal(self): assert orig_msg == decr_msg -class TestShamir(object): +class TestShamir: """ Shamir's Secret Sharing """ From d60b3ad28a73d46ce8dd79416491032ab3a233de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:14:49 +0100 Subject: [PATCH 05/16] Add type annotations for sodium_is_zero --- src/nacl/bindings/utils.py | 4 +--- tests/test_bindings.py | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/nacl/bindings/utils.py b/src/nacl/bindings/utils.py index cfef6d5f..ae13e0d4 100644 --- a/src/nacl/bindings/utils.py +++ b/src/nacl/bindings/utils.py @@ -17,14 +17,12 @@ from nacl.exceptions import ensure -def sodium_is_zero(inp): +def sodium_is_zero(inp: bytes) -> bool: """ Check if all bytes in ``inp`` are zero :param inp: input bytes string - :type inp: bytes :return: False if any byte is nonzero, else True - :rtype: bool """ ensure(isinstance(inp, bytes), raising=exc.TypeError) ln = len(inp) diff --git a/tests/test_bindings.py b/tests/test_bindings.py index 82ea38bb..7a048688 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -331,8 +331,9 @@ 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") + c.sodium_is_zero("zero") # type: ignore[arg-type] @given(integers(min_value=-2, max_value=0)) From 3a577f769091025b50948ac9cf12b5519b4edbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:17:28 +0100 Subject: [PATCH 06/16] Use "# pragma: no branch" instead of else: block --- src/nacl/bindings/crypto_core_ristretto255.py | 8 ++++---- src/nacl/bindings/crypto_scalarmult_ristretto255.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/nacl/bindings/crypto_core_ristretto255.py b/src/nacl/bindings/crypto_core_ristretto255.py index 116ff039..4cb60328 100644 --- a/src/nacl/bindings/crypto_core_ristretto255.py +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -25,7 +25,10 @@ 2 ** 252 + 27742317777372353535851937790883648493 ) -if has_crypto_core_ristretto25519: +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() @@ -35,9 +38,6 @@ crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = ( lib.crypto_core_ristretto255_nonreducedscalarbytes() ) -else: # pragma: no cover - crypto_core_ristretto255_SCALAR_BYTES = 0 - crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES = 0 def crypto_core_ristretto255_scalar_add(x, y): diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py index 77ab8759..65b788cf 100644 --- a/src/nacl/bindings/crypto_scalarmult_ristretto255.py +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -20,7 +20,10 @@ lib.PYNACL_HAS_CRYPTO_SCALARMULT_RISTRETTO25519 ) -if 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 = ( @@ -31,9 +34,6 @@ crypto_scalarmult_ristretto255_SCALAR_BYTES = ( lib.crypto_scalarmult_ristretto255_scalarbytes() ) -else: # pragma: no cover - crypto_scalarmult_ristretto255_BYTES = 0 - crypto_scalarmult_ristretto255_SCALAR_BYTES = 0 def crypto_scalarmult_ristretto255_base(n): From 0fa13152655cd314f94e3cdf51520d8c2e098890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:20:18 +0100 Subject: [PATCH 07/16] Add type annotations for ristretto bindings --- src/nacl/bindings/crypto_core_ristretto255.py | 55 +++++-------------- .../crypto_scalarmult_ristretto255.py | 9 +-- 2 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/nacl/bindings/crypto_core_ristretto255.py b/src/nacl/bindings/crypto_core_ristretto255.py index 4cb60328..f2c21117 100644 --- a/src/nacl/bindings/crypto_core_ristretto255.py +++ b/src/nacl/bindings/crypto_core_ristretto255.py @@ -40,17 +40,14 @@ ) -def crypto_core_ristretto255_scalar_add(x, y): +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 - :type x: bytes :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` bytes in little endian order representing the second scalar - :type y: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -84,14 +81,12 @@ def crypto_core_ristretto255_scalar_add(x, y): return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_complement(s): +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 - :type s: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -116,15 +111,13 @@ def crypto_core_ristretto255_scalar_complement(s): return ffi.buffer(comp, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_invert(s): +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 - :type s: bytes - :rtype: bytes :raises ValueError: if the value is not invertible :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. @@ -152,17 +145,14 @@ def crypto_core_ristretto255_scalar_invert(s): return ffi.buffer(recip, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_mul(x, y): +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 - :type x: bytes :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` bytes in little endian order representing the second scalar - :type y: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -196,15 +186,13 @@ def crypto_core_ristretto255_scalar_mul(x, y): return ffi.buffer(z, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_negate(s): +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 - :type s: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -228,11 +216,10 @@ def crypto_core_ristretto255_scalar_negate(s): return ffi.buffer(neg, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_random(): +def crypto_core_ristretto255_scalar_random() -> bytes: """ Generate a random non-zero scalar modulo ``L``. - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -248,7 +235,7 @@ def crypto_core_ristretto255_scalar_random(): return ffi.buffer(r, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_reduce(s): +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``. @@ -257,8 +244,6 @@ def crypto_core_ristretto255_scalar_reduce(s): :py:data:`.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES` bytes in little endian order representing the value to reduce to a Ristretto255 scalar - :type s: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -283,17 +268,14 @@ def crypto_core_ristretto255_scalar_reduce(s): return ffi.buffer(r, crypto_core_ristretto255_SCALAR_BYTES)[:] -def crypto_core_ristretto255_scalar_sub(x, y): +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 - :type x: bytes :param y: a sequence of :py:data:`.crypto_core_ristretto255_SCALAR_BYTES` bytes in little endian order representing the second scalar - :type y: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -340,17 +322,14 @@ def crypto_core_ristretto255_scalar_sub(x, y): crypto_core_ristretto255_HASH_BYTES = 0 -def crypto_core_ristretto255_add(p, q): +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 - :type p: bytes :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` bytes representing the second point - :type q: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -384,15 +363,13 @@ def crypto_core_ristretto255_add(p, q): return ffi.buffer(r, crypto_core_ristretto255_BYTES)[:] -def crypto_core_ristretto255_from_hash(r): +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 - :type r: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -418,15 +395,13 @@ def crypto_core_ristretto255_from_hash(r): return ffi.buffer(q, crypto_core_ristretto255_BYTES)[:] -def crypto_core_ristretto255_is_valid_point(p): +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 - :type p: bytes :return: False if invalid, True if valid - :rtype: bool :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -449,12 +424,11 @@ def crypto_core_ristretto255_is_valid_point(p): return rc == 1 -def crypto_core_ristretto255_random(): +def crypto_core_ristretto255_random() -> bytes: """ Generate a random Ristretto255 point. This might be, although astronomically unlikely, the zero point. - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ @@ -470,17 +444,14 @@ def crypto_core_ristretto255_random(): return ffi.buffer(p, crypto_core_ristretto255_BYTES)[:] -def crypto_core_ristretto255_sub(p, q): +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 - :type p: bytes :param q: a sequence of :py:data:`.crypto_core_ristretto255_BYTES` bytes representing the second point - :type q: bytes - :rtype: bytes :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. """ diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py index 65b788cf..975262b8 100644 --- a/src/nacl/bindings/crypto_scalarmult_ristretto255.py +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -36,15 +36,13 @@ ) -def crypto_scalarmult_ristretto255_base(n): +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 - :type n: bytes - :rtype: bytes :raises exc.RuntimeError: on error or if result is zero :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. @@ -75,18 +73,15 @@ def crypto_scalarmult_ristretto255_base(n): return ffi.buffer(q, crypto_scalarmult_ristretto255_BYTES)[:] -def crypto_scalarmult_ristretto255(n, p): +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 - :type n: bytes :param p: a sequence of :py:data:`.crypto_scalarmult_ristretto255_BYTES` bytes in little endian order representing the point - :type p: bytes - :rtype: bytes :raises exc.RuntimeError: on error or if result is zero :raises nacl.exceptions.UnavailableError: If called when using a minimal build of libsodium. From f565999cca527a165838910479a8015e950a4996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:24:53 +0100 Subject: [PATCH 08/16] Document that multiplication with zero scalar is not allowed --- src/nacl/bindings/crypto_scalarmult_ristretto255.py | 13 +++++++++++-- src/nacl/ristretto.py | 7 +++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/nacl/bindings/crypto_scalarmult_ristretto255.py b/src/nacl/bindings/crypto_scalarmult_ristretto255.py index 975262b8..058452a9 100644 --- a/src/nacl/bindings/crypto_scalarmult_ristretto255.py +++ b/src/nacl/bindings/crypto_scalarmult_ristretto255.py @@ -68,7 +68,11 @@ def crypto_scalarmult_ristretto255_base(n: bytes) -> bytes: # 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", raising=exc.RuntimeError) + ensure( + rc == 0, + "Unexpected library error. Zero operand?", + raising=exc.RuntimeError, + ) return ffi.buffer(q, crypto_scalarmult_ristretto255_BYTES)[:] @@ -115,6 +119,11 @@ def crypto_scalarmult_ristretto255(n: bytes, p: bytes) -> bytes: # An error is returned also if the result is zero. This cannot be # distinguished from other errors like invalid points. - ensure(rc == 0, "Unexpected library error", raising=exc.RuntimeError) + # 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/ristretto.py b/src/nacl/ristretto.py index cdf498fc..f21167d8 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -226,7 +226,7 @@ def random(cls): @classmethod def base_mul(cls, n): """ - Multiply the scalar ``n`` with the Ed25519 base point. + Multiply the non-zero scalar *n* with the Ed25519 base point. """ return cls( nacl.bindings.crypto_scalarmult_ristretto255_base( @@ -271,7 +271,7 @@ def __sub__(self, other): def __mul__(self, other): """ - Multiply the scalar ``n`` with the point. + Multiply the non-zero scalar *other* with the point. """ return Ristretto255Point( nacl.bindings.crypto_scalarmult_ristretto255( @@ -281,6 +281,9 @@ def __mul__(self, other): ) def __rmul__(self, other): + """ + Multiply the point with the non-zero scalar *other*. + """ return self * other def __bool__(self): From 897c141f0ffa84137a94842e8fad85b7260ba034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:39:39 +0100 Subject: [PATCH 09/16] Return NotImplemented instead of raising a TypeError --- src/nacl/ristretto.py | 83 +++++++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index f21167d8..2489f8b3 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -13,12 +13,17 @@ # 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: SIZE = nacl.bindings.crypto_core_ristretto255_SCALAR_BYTES NONREDUCED_SIZE = ( @@ -27,23 +32,48 @@ class Ristretto255Scalar: ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER def __init__(self, value): + 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): - self._value = value._value - elif isinstance(value, bytes): + return value._value + + if isinstance(value, bytes): if len(value) != Ristretto255Scalar.SIZE: raise exc.ValueError - self._value = value - elif isinstance(value, int): - self._value = (value % Ristretto255Scalar.ORDER).to_bytes( + + # 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" ) - elif isinstance(value, Fraction): - self._value = ( - Ristretto255Scalar(value.numerator) - * Ristretto255Scalar(value.denominator).inverse - )._value - else: - raise exc.TypeError + + 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): @@ -93,10 +123,14 @@ def __add__(self, other): """ Add two scalars. """ + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented return Ristretto255Scalar( nacl.bindings.crypto_core_ristretto255_scalar_add( - self._value, Ristretto255Scalar(other)._value + self._value, value ) ) @@ -107,9 +141,14 @@ def __sub__(self, other): """ Subtract to scalars. """ + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + return Ristretto255Scalar( nacl.bindings.crypto_core_ristretto255_scalar_sub( - self._value, Ristretto255Scalar(other)._value + self._value, value ) ) @@ -120,9 +159,15 @@ def __mul__(self, other): """ Multiply two scalars. """ + + try: + value = self._convert(other) + except exc.TypeError: + return NotImplemented + return Ristretto255Scalar( nacl.bindings.crypto_core_ristretto255_scalar_mul( - self._value, Ristretto255Scalar(other)._value + self._value, value ) ) @@ -230,7 +275,7 @@ def base_mul(cls, n): """ return cls( nacl.bindings.crypto_scalarmult_ristretto255_base( - bytes(Ristretto255Scalar(n)) + Ristretto255Scalar._convert(n) ), _assume_valid=True, ) @@ -246,7 +291,7 @@ def __add__(self, other): Add two points. """ if not isinstance(other, Ristretto255Point): - raise exc.TypeError("Operand must be another Ristretto255Point") + return NotImplemented # type: ignore[unreachable] return Ristretto255Point( nacl.bindings.crypto_core_ristretto255_add( @@ -260,7 +305,7 @@ def __sub__(self, other): Subtract two points. """ if not isinstance(other, Ristretto255Point): - raise exc.TypeError("Operand must be another Ristretto255Point") + return NotImplemented # type: ignore[unreachable] return Ristretto255Point( nacl.bindings.crypto_core_ristretto255_sub( @@ -275,7 +320,7 @@ def __mul__(self, other): """ return Ristretto255Point( nacl.bindings.crypto_scalarmult_ristretto255( - bytes(Ristretto255Scalar(other)), self._value + Ristretto255Scalar._convert(other), self._value ), _assume_valid=True, ) From 6a45561fa44c630d4bad9bc2cbe7edc45ef16fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:50:47 +0100 Subject: [PATCH 10/16] Rework workaround for minimal build --- src/nacl/ristretto.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index 2489f8b3..179205d1 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -214,7 +214,7 @@ def __str__(self): return repr(self) -if nacl.bindings.has_crypto_core_ristretto25519: +if nacl.bindings.has_crypto_core_ristretto25519: # pragma: no branch # Neutral additive element Ristretto255Scalar.ZERO = Ristretto255Scalar(0) @@ -223,15 +223,6 @@ def __str__(self): # Constant needed for inverting points Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar(-1) -else: # pragma: no cover - - Ristretto255Scalar.ZERO = Ristretto255Scalar( - bytes(Ristretto255Scalar.SIZE) - ) - Ristretto255Scalar.ONE = Ristretto255Scalar(bytes(Ristretto255Scalar.SIZE)) - Ristretto255Scalar.MINUS_ONE = Ristretto255Scalar( - bytes(Ristretto255Scalar.SIZE) - ) class Ristretto255Point: @@ -362,7 +353,10 @@ def __str__(self): return "Ristretto255Point({})".format(bytes(self).hex()) -# Neutral element -Ristretto255Point.ZERO = Ristretto255Point( - bytes(Ristretto255Point.SIZE), _assume_valid=True -) + + +if nacl.bindings.has_crypto_core_ristretto25519: # pragma: no branch + # Neutral element + Ristretto255Point.ZERO = Ristretto255Point( + bytes(Ristretto255Point.SIZE), _assume_valid=True + ) From 1ec493ef3dd6b3b978654e520c9de501c8618066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 21:55:44 +0100 Subject: [PATCH 11/16] Add / operator to scalar --- src/nacl/ristretto.py | 23 +++++++++++++++++++++++ tests/test_ristretto.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index 179205d1..67b4ed83 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -182,6 +182,29 @@ def __neg__(self): nacl.bindings.crypto_core_ristretto255_scalar_negate(self._value) ) + def __truediv__(self, other): + """ + Divide two scalars. + """ + + 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): + Divide two scalars. + """ + + return self.inverse * other + def __eq__(self, other): """ Check if two scalars are identical. Comparing with other types such as diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py index 931644c0..152b06bb 100644 --- a/tests/test_ristretto.py +++ b/tests/test_ristretto.py @@ -248,6 +248,37 @@ def test_mul(self): assert a * Ristretto255Scalar.ZERO == Ristretto255Scalar.ZERO assert a * Ristretto255Scalar.ONE == a + @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", From 247e3d93bcedb352001e616c5021acb48c4521d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 22:14:13 +0100 Subject: [PATCH 12/16] use f-strings for formatting --- src/nacl/ristretto.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index 67b4ed83..502a4a32 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -231,7 +231,7 @@ def __bool__(self): return not nacl.bindings.sodium_is_zero(self._value) def __repr__(self): - return "Ristretto255Scalar({})".format(int(self)) + return f"Ristretto255Scalar({int(self)})" def __str__(self): return repr(self) @@ -370,12 +370,10 @@ def __hash__(self): return hash(self._value) def __repr__(self): - return "Ristretto255Point({!r})".format(bytes(self)) + return f"Ristretto255Point({bytes(self)!r})" def __str__(self): - return "Ristretto255Point({})".format(bytes(self).hex()) - - + return f"Ristretto255Point({bytes(self).hex()})" if nacl.bindings.has_crypto_core_ristretto25519: # pragma: no branch From 6ede376d10526012d02a738372f0fb6ac742c980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 22:21:14 +0100 Subject: [PATCH 13/16] Improve tests --- tests/test_ristretto.py | 48 ++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py index 152b06bb..38602758 100644 --- a/tests/test_ristretto.py +++ b/tests/test_ristretto.py @@ -16,7 +16,7 @@ import os from fractions import Fraction from functools import reduce -from hashlib import sha256, sha512 +from hashlib import sha512 from operator import mul from random import randrange @@ -66,10 +66,16 @@ class TestRistretto255Scalar: reason="Requires full build of libsodium", ) def test_init(self): + value = bytes(reversed(range(32))) + scalar = Ristretto255Scalar(value) + assert bytes(scalar) == value - dgst = sha256(b"hello").digest() - s = Ristretto255Scalar(dgst) - assert bytes(s) == dgst + 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 @@ -181,6 +187,12 @@ def test_add(self): t = Ristretto255Scalar(456) u = Ristretto255Scalar(579) + with pytest.raises(TypeError): + s + "foo" + + with pytest.raises(TypeError): + "foo" + s + assert s + t == u assert s + t == t + s assert s != t @@ -204,6 +216,12 @@ def test_sub(self): t = Ristretto255Scalar(456) u = Ristretto255Scalar(579) + with pytest.raises(TypeError): + s - "foo" + + with pytest.raises(TypeError): + "foo" - s + assert u - s == t assert u - t == s assert s - (self.order - 456) == u @@ -248,6 +266,12 @@ def test_mul(self): assert a * Ristretto255Scalar.ZERO == Ristretto255Scalar.ZERO assert a * Ristretto255Scalar.ONE == a + with pytest.raises(TypeError): + s * "foo" + + with pytest.raises(TypeError): + "foo" * s + @pytest.mark.skipif( not has_crypto_core_ristretto25519, reason="Requires full build of libsodium", @@ -311,7 +335,9 @@ def test_eq(self): assert t != u assert s != "foobar" + assert "foobar" != s assert s != 123 + assert 123 != s p = Ristretto255Scalar.random() q = Ristretto255Scalar.random() @@ -491,7 +517,7 @@ def test_add(self): q = Ristretto255Point.random() r = Ristretto255Point.random() - with pytest.raises(exc.TypeError): + with pytest.raises(TypeError): p + 123 assert p + Ristretto255Point.ZERO == p @@ -509,7 +535,7 @@ def test_sub(self): q = Ristretto255Point.random() r = Ristretto255Point.random() - with pytest.raises(exc.TypeError): + with pytest.raises(TypeError): p - 123 assert p - Ristretto255Point.ZERO == p @@ -538,7 +564,9 @@ def test_mul(self): 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( @@ -675,11 +703,11 @@ def test_el_gamal(self): 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. + # 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! + # Happens to be a valid point too. Blessed be the lazy dog! m1 = Ristretto255Point(orig_msg[32:]) e1, f1 = self.encrypt(h, m1) @@ -697,7 +725,7 @@ class TestShamir: class Polynomial: def __init__(self, coeffs, zero): - self._coeffs = list(coeffs) + self._coeffs = coeffs self._zero = zero def __call__(self, i): @@ -716,7 +744,7 @@ def share_secret(self, share_count, qualified_size): gen = Ristretto255Point.random() alpha = self.Polynomial( - (Ristretto255Scalar.random() for __ in range(qualified_size)), + [Ristretto255Scalar.random() for __ in range(qualified_size)], Ristretto255Scalar.ZERO, ) From b3f96a26d8aab8a76123e59b3be6084e91985576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 22:22:01 +0100 Subject: [PATCH 14/16] Add type annotations + inline docs for ristretto and tests --- src/nacl/ristretto.py | 320 ++++++++++++++++++++++++++++++++-------- tests/test_ristretto.py | 96 +++++++----- 2 files changed, 317 insertions(+), 99 deletions(-) diff --git a/src/nacl/ristretto.py b/src/nacl/ristretto.py index 502a4a32..e382791c 100644 --- a/src/nacl/ristretto.py +++ b/src/nacl/ristretto.py @@ -25,13 +25,44 @@ class Ristretto255Scalar: - SIZE = nacl.bindings.crypto_core_ristretto255_SCALAR_BYTES - NONREDUCED_SIZE = ( - nacl.bindings.crypto_core_ristretto255_NONREDUCED_SCALAR_BYTES - ) - ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + """ + 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): + 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 @@ -76,42 +107,53 @@ def _convert(value: object) -> bytes: raise exc.TypeError(f"Unsupported type: {type(value).__name__!r}") @classmethod - def random(cls): - """ - Get a non-zero random scalar. + 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): + def random_zero(cls) -> "Ristretto255Scalar": """ - Get a random scalar that could also be zero. + Create a random scalar that could be zero. + + :return: Ristretto255Scalar: Random scalar """ return cls.reduce(random(cls.NONREDUCED_SIZE)) @classmethod - def reduce(cls, value): + 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): - """ - Get multiplicative inverse such that ``x.inverse * x == 1``. + 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): + def complement(self) -> "Ristretto255Scalar": """ - Get the complement such that ``x.complement + x == 1``. + 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( @@ -119,9 +161,12 @@ def complement(self): ) ) - def __add__(self, other): + 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) @@ -134,12 +179,21 @@ def __add__(self, other): ) ) - def __radd__(self, other): + 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): + def __sub__(self, other: _ScalarType) -> "Ristretto255Scalar": """ - Subtract to scalars. + 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) @@ -152,12 +206,21 @@ def __sub__(self, other): ) ) - def __rsub__(self, other): + 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): + 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: @@ -171,20 +234,21 @@ def __mul__(self, other): ) ) - def __rmul__(self, other): - return self * other - - def __neg__(self): + def __rmul__(self, other: _ScalarType) -> "Ristretto255Scalar": """ - Get the additive inverse such that ``-x + x == 0``. + Multiply two scalars. + + :param other: Any of the types supported by the constructor + :return: Product of *other* and *self* modulo :py:CONST:`ORDER` """ - return Ristretto255Scalar( - nacl.bindings.crypto_core_ristretto255_scalar_negate(self._value) - ) + return self * other - def __truediv__(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: @@ -199,41 +263,94 @@ def __truediv__(self, other): ) ) - def __rtruediv__(self, other): + 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 __eq__(self, 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): + 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): + def __hash__(self) -> int: + """ + Compute a hash value. + + :return: Hash value + """ return hash(self._value) - def __bytes__(self): + def __bytes__(self) -> bytes: + """ + Get byte representation of scalar. + + :return: Value of scalar in little-endian encoding + """ return self._value - def __int__(self): + 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): + 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): + 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): + def __str__(self) -> str: + """ + Get human readable representation of scalar. + + :return: Representation of scalar + """ return repr(self) @@ -249,11 +366,33 @@ def __str__(self): class Ristretto255Point: - SIZE = nacl.bindings.crypto_core_ristretto255_BYTES - HASH_SIZE = nacl.bindings.crypto_core_ristretto255_HASH_BYTES - ORDER = nacl.bindings.crypto_core_ristretto255_GROUP_ORDER + """ + 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 + """ - def __init__(self, value, _assume_valid=False): + 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 @@ -262,10 +401,13 @@ def __init__(self, value, _assume_valid=False): self._value = value @classmethod - def from_hash(cls, value): + 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 input is all zeros. + 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), @@ -273,19 +415,24 @@ def from_hash(cls, value): ) @classmethod - def random(cls): + 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): + 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( @@ -294,15 +441,20 @@ def base_mul(cls, n): _assume_valid=True, ) - def __neg__(self): + def __neg__(self) -> "Ristretto255Point": """ - Get inverse element such that ``-x + x == Ristretto255Point.ZERO``. + Get inverse element such that ``-self + self == Ristretto255Point.ZERO``. + + :return: Inverse of *self* """ return self * Ristretto255Scalar.MINUS_ONE - def __add__(self, other): + 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] @@ -314,9 +466,12 @@ def __add__(self, other): _assume_valid=True, ) - def __sub__(self, other): + 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] @@ -328,9 +483,12 @@ def __sub__(self, other): _assume_valid=True, ) - def __mul__(self, other): + 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( @@ -339,40 +497,74 @@ def __mul__(self, other): _assume_valid=True, ) - def __rmul__(self, other): + 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): + def __bool__(self) -> bool: """ Check if this is *not* the zero / neutral / identity point. - :return: False if zero point, else True - :rtype: bool + :return: False if zero point, True otherwise """ return not nacl.bindings.sodium_is_zero(self._value) - def __eq__(self, other): + 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): + 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): + def __bytes__(self) -> bytes: + """ + Get byte representation of point. + + :return: Little-endian byte representation of point + """ return self._value - def __hash__(self): + def __hash__(self) -> int: + """ + Compute a hash value. + + :return: Hash value + """ return hash(self._value) - def __repr__(self): + 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): + def __str__(self) -> str: + """ + Get human readable representation of point. + + :return: Little-endian hex representation of point + """ return f"Ristretto255Point({bytes(self).hex()})" diff --git a/tests/test_ristretto.py b/tests/test_ristretto.py index 38602758..957ea27b 100644 --- a/tests/test_ristretto.py +++ b/tests/test_ristretto.py @@ -19,6 +19,7 @@ from hashlib import sha512 from operator import mul from random import randrange +from typing import List, Tuple import pytest @@ -30,7 +31,9 @@ from nacl.ristretto import Ristretto255Point, Ristretto255Scalar -def _ristretto255_vectors(): +def _ristretto255_vectors() -> Tuple[ + List[Tuple[int, bytes]], List[bytes], List[Tuple[str, bytes]] +]: """ Test vectors from https://ristretto.group/test_vectors/ristretto255.html """ @@ -38,21 +41,19 @@ def _ristretto255_vectors(): path = os.path.join(os.path.dirname(__file__), "data", DATA) vectors = json.load(open(path)) - return { - "encodings_of_small_multiples": [ + return ( + [ (idx, bytes.fromhex(enc)) for idx, enc in enumerate(vectors["encodings_of_small_multiples"]) ], - "bad_encodings": [ - bytes.fromhex(enc) for enc in vectors["bad_encodings"] - ], - "label_hash_to_points": [ + [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: @@ -97,7 +98,7 @@ def test_init(self): Ristretto255Scalar(b"too short") with pytest.raises(exc.TypeError): - Ristretto255Scalar(3.14) + Ristretto255Scalar(3.14) # type: ignore[arg-type] @pytest.mark.skipif( not has_crypto_core_ristretto25519, @@ -188,10 +189,10 @@ def test_add(self): u = Ristretto255Scalar(579) with pytest.raises(TypeError): - s + "foo" + s + "foo" # type: ignore[operator] with pytest.raises(TypeError): - "foo" + s + "foo" + s # type: ignore[operator] assert s + t == u assert s + t == t + s @@ -217,10 +218,10 @@ def test_sub(self): u = Ristretto255Scalar(579) with pytest.raises(TypeError): - s - "foo" + s - "foo" # type: ignore[operator] with pytest.raises(TypeError): - "foo" - s + "foo" - s # type: ignore[operator] assert u - s == t assert u - t == s @@ -267,10 +268,10 @@ def test_mul(self): assert a * Ristretto255Scalar.ONE == a with pytest.raises(TypeError): - s * "foo" + s * "foo" # type: ignore[operator] with pytest.raises(TypeError): - "foo" * s + "foo" * s # type: ignore[operator] @pytest.mark.skipif( not has_crypto_core_ristretto25519, @@ -423,7 +424,16 @@ def test_str(self): class TestRistretto255Point: - _vectors = _ristretto255_vectors() + _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" ) @@ -434,7 +444,7 @@ class TestRistretto255Point: reason="Requires full build of libsodium", ) @pytest.mark.parametrize( - ("idx", "encoding"), _vectors["encodings_of_small_multiples"] + ("idx", "encoding"), _vectors_encodings_of_small_multiples ) def test_small_multiples(self, idx, encoding): base = Ristretto255Point(self._base) @@ -454,7 +464,7 @@ def test_small_multiples(self, idx, encoding): not has_crypto_core_ristretto25519, reason="Requires full build of libsodium", ) - @pytest.mark.parametrize(("encoding"), _vectors["bad_encodings"]) + @pytest.mark.parametrize(("encoding"), _vectors_bad_encodings) def test_bad_encodings(self, encoding): with pytest.raises(exc.ValueError): Ristretto255Point(encoding) @@ -464,7 +474,7 @@ def test_bad_encodings(self, encoding): reason="Requires full build of libsodium", ) @pytest.mark.parametrize( - ("label", "encoding"), _vectors["label_hash_to_points"] + ("label", "encoding"), _vectors_label_hash_to_points ) def test_hash_to_point(self, label, encoding): point = Ristretto255Point.from_hash( @@ -481,7 +491,7 @@ def test_init(self): Ristretto255Point(b"too short") with pytest.raises(exc.TypeError): - Ristretto255Point(3.14) + Ristretto255Point(3.14) # type: ignore[arg-type] # good code paths are tested elsewhere. @@ -518,7 +528,7 @@ def test_add(self): r = Ristretto255Point.random() with pytest.raises(TypeError): - p + 123 + p + 123 # type: ignore[operator] assert p + Ristretto255Point.ZERO == p assert Ristretto255Point.ZERO + p == p @@ -536,7 +546,7 @@ def test_sub(self): r = Ristretto255Point.random() with pytest.raises(TypeError): - p - 123 + p - 123 # type: ignore[operator] assert p - Ristretto255Point.ZERO == p assert Ristretto255Point.ZERO - p != p @@ -554,10 +564,10 @@ def test_mul(self): q = Ristretto255Point.random() with pytest.raises(exc.TypeError): - p * q + p * q # type: ignore[operator] with pytest.raises(exc.TypeError): - p * "test" + p * "test" # type: ignore[operator] assert p * 3 == 3 * p assert p + p + p == p * 3 @@ -655,7 +665,7 @@ def test_str(self): ) def test_library_error(self): p = Ristretto255Point( - self._vectors["bad_encodings"][6], _assume_valid=True + self._vectors_bad_encodings[6], _assume_valid=True ) q = Ristretto255Point.random() @@ -674,13 +684,15 @@ class TestElGamal: ElGamal encryption. """ - def gen_key(self): + def gen_key(self) -> Tuple[Ristretto255Scalar, Ristretto255Point]: x = Ristretto255Scalar.random() h = Ristretto255Point.base_mul(x) return x, h - def encrypt(self, h, m): + def encrypt( + self, h: Ristretto255Point, m: Ristretto255Point + ) -> Tuple[Ristretto255Point, Ristretto255Point]: y = Ristretto255Scalar.random() s = h * y c0 = Ristretto255Point.base_mul(y) @@ -688,7 +700,12 @@ def encrypt(self, h, m): return c0, c1 - def decrypt(self, c0, c1, x): + def decrypt( + self, + c0: Ristretto255Point, + c1: Ristretto255Point, + x: Ristretto255Scalar, + ) -> Ristretto255Point: s = c0 * x m = c1 - s @@ -699,7 +716,7 @@ def decrypt(self, c0, c1, x): or not has_crypto_scalarmult_ristretto25519, reason="Requires full build of libsodium", ) - def test_el_gamal(self): + 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) @@ -724,11 +741,16 @@ class TestShamir: """ class Polynomial: - def __init__(self, coeffs, zero): + _coeffs: List[Ristretto255Scalar] + _zero: Ristretto255Scalar + + def __init__( + self, coeffs: List[Ristretto255Scalar], zero: Ristretto255Scalar + ) -> None: self._coeffs = coeffs self._zero = zero - def __call__(self, i): + def __call__(self, i: int) -> Ristretto255Scalar: return sum( ( coeff * Ristretto255Scalar(i ** j) @@ -737,10 +759,12 @@ def __call__(self, i): self._zero, ) - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> Ristretto255Scalar: return self._coeffs[idx] - def share_secret(self, share_count, qualified_size): + def share_secret( + self, share_count: int, qualified_size: int + ) -> Tuple[Ristretto255Point, List[Tuple[int, Ristretto255Point]]]: gen = Ristretto255Point.random() alpha = self.Polynomial( @@ -753,7 +777,9 @@ def share_secret(self, share_count, qualified_size): return secret, shares - def reconstruct(self, shares): + def reconstruct( + self, shares: List[Tuple[int, Ristretto255Point]] + ) -> Ristretto255Point: return sum( ( share @@ -776,7 +802,7 @@ def reconstruct(self, shares): or not has_crypto_scalarmult_ristretto25519, reason="Requires full build of libsodium", ) - def test_shamir(self): + def test_shamir(self) -> None: secret0, shares = self.share_secret(5, 3) # Delete any two shares From 2f658ff495bec22f3c84e7010fe479a50f09aebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 22:25:23 +0100 Subject: [PATCH 15/16] Add high-level documentation for ristretto --- docs/api/ristretto.rst | 19 +++ docs/index.rst | 2 + docs/ristretto.rst | 317 +++++++++++++++++++++++++++++++++++++++++ docs/vectors/index.rst | 7 + 4 files changed, 345 insertions(+) create mode 100644 docs/api/ristretto.rst create mode 100644 docs/ristretto.rst diff --git a/docs/api/ristretto.rst b/docs/api/ristretto.rst new file mode 100644 index 00000000..d0429c9e --- /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 010352ba..b31e9104 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 00000000..7157f26e --- /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 7f7e043f..f555df4c 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 ---------------------------------- From 2d0f7e9c0d17bb8ed39013603e2a8a3b0986628c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Heissler?= Date: Tue, 28 Dec 2021 22:46:49 +0100 Subject: [PATCH 16/16] Bump readthedocs python version to 3.8 --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 704f96fe..a42a4181 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,5 +1,5 @@ python: - version: 3.5 + version: 3.8 pip_install: true extra_requirements: - docs