From 79a996be02df53d588e7175d168baf06fa68af7e Mon Sep 17 00:00:00 2001 From: j-berman Date: Tue, 17 Feb 2026 17:51:03 -0800 Subject: [PATCH] crypto: torsion clearing for FCMP++ --- src/CMakeLists.txt | 1 + src/crypto/crypto.h | 4 ++ src/fcmp_pp/CMakeLists.txt | 42 +++++++++++++++ src/fcmp_pp/fcmp_pp_crypto.cpp | 68 ++++++++++++++++++++++++ src/fcmp_pp/fcmp_pp_crypto.h | 68 ++++++++++++++++++++++++ src/ringct/CMakeLists.txt | 1 + src/ringct/rctSigs.cpp | 32 ++++++++++++ src/ringct/rctSigs.h | 3 ++ src/ringct/rctTypes.h | 2 + tests/crypto/main.cpp | 4 -- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/crypto.cpp | 92 +++++++++++++++++++++++++++++++++ 12 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 src/fcmp_pp/CMakeLists.txt create mode 100644 src/fcmp_pp/fcmp_pp_crypto.cpp create mode 100644 src/fcmp_pp/fcmp_pp_crypto.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 85116ae05..8217a2c6d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -83,6 +83,7 @@ monero_add_library(version SOURCES ${CMAKE_BINARY_DIR}/version.cpp DEPENDS genve add_subdirectory(common) add_subdirectory(crypto) +add_subdirectory(fcmp_pp) add_subdirectory(ringct) add_subdirectory(checkpoints) add_subdirectory(cryptonote_basic) diff --git a/src/crypto/crypto.h b/src/crypto/crypto.h index 1145192cc..4a6b20a57 100644 --- a/src/crypto/crypto.h +++ b/src/crypto/crypto.h @@ -320,6 +320,9 @@ namespace crypto { inline std::ostream &operator <<(std::ostream &o, const crypto::view_tag &v) { epee::to_hex::formatted(o, epee::as_byte_span(v)); return o; } + inline std::ostream &operator <<(std::ostream &o, const crypto::ec_point &v) { + epee::to_hex::formatted(o, epee::as_byte_span(v)); return o; + } const extern crypto::public_key null_pkey; const extern crypto::secret_key null_skey; @@ -336,6 +339,7 @@ inline const unsigned char* to_bytes(const crypto::ec_scalar &scalar) { return & inline unsigned char* to_bytes(crypto::ec_point &point) { return &reinterpret_cast(point); } inline const unsigned char* to_bytes(const crypto::ec_point &point) { return &reinterpret_cast(point); } +CRYPTO_MAKE_HASHABLE(ec_point) CRYPTO_MAKE_HASHABLE(public_key) CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(secret_key) CRYPTO_MAKE_HASHABLE_CONSTANT_TIME(public_key_memsafe) diff --git a/src/fcmp_pp/CMakeLists.txt b/src/fcmp_pp/CMakeLists.txt new file mode 100644 index 000000000..32ca1b4f2 --- /dev/null +++ b/src/fcmp_pp/CMakeLists.txt @@ -0,0 +1,42 @@ +# Copyright (c) 2024, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(fcmp_pp_sources + fcmp_pp_crypto.cpp) + +monero_find_all_headers(fcmp_pp_headers "${CMAKE_CURRENT_SOURCE_DIR}") + +monero_add_library(fcmp_pp + ${fcmp_pp_sources} + ${fcmp_pp_headers}) + +target_link_libraries(fcmp_pp + PUBLIC + cncrypto + PRIVATE + ${EXTRA_LIBRARIES}) diff --git a/src/fcmp_pp/fcmp_pp_crypto.cpp b/src/fcmp_pp/fcmp_pp_crypto.cpp new file mode 100644 index 000000000..39b95dbbc --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_crypto.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "fcmp_pp_crypto.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +bool mul8_is_identity_vartime(const ge_p3 &point) { + ge_p2 point_ge_p2; + ge_p3_to_p2(&point_ge_p2, &point); + ge_p1p1 point_mul8; + ge_mul8(&point_mul8, &point_ge_p2); + ge_p3 point_mul8_p3; + ge_p1p1_to_p3(&point_mul8_p3, &point_mul8); + return ge_p3_is_point_at_infinity_vartime(&point_mul8_p3); +} +//---------------------------------------------------------------------------------------------------------------------- +crypto::ec_point clear_torsion_vartime(const ge_p3 &point) { + // mul by inv 8, then mul by 8 + ge_p2 point_inv_8; + ge_scalarmult(&point_inv_8, to_bytes(EC_INV_EIGHT), &point); + ge_p1p1 point_inv_8_mul_8; + ge_mul8(&point_inv_8_mul_8, &point_inv_8); + ge_p3 torsion_cleared_point; + ge_p1p1_to_p3(&torsion_cleared_point, &point_inv_8_mul_8); + crypto::ec_point k_out; + ge_p3_tobytes(to_bytes(k_out), &torsion_cleared_point); + return k_out; +} +//---------------------------------------------------------------------------------------------------------------------- +bool get_valid_torsion_cleared_point_vartime(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out) { + ge_p3 p3; + if (ge_frombytes_vartime(&p3, to_bytes(point)) != 0) + return false; + torsion_cleared_out = fcmp_pp::clear_torsion_vartime(p3); + if (torsion_cleared_out == EC_I) + return false; + return true; +} +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/fcmp_pp/fcmp_pp_crypto.h b/src/fcmp_pp/fcmp_pp_crypto.h new file mode 100644 index 000000000..d1d2b1bd4 --- /dev/null +++ b/src/fcmp_pp/fcmp_pp_crypto.h @@ -0,0 +1,68 @@ +// Copyright (c) 2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +extern "C" +{ +#include "crypto/crypto-ops.h" +} +#include "crypto/crypto.h" + +namespace fcmp_pp +{ +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +static const crypto::ec_point EC_I = {1}; + +static const crypto::ec_scalar EC_INV_EIGHT = {{ + static_cast(static_cast(121)), static_cast(static_cast(47)), + static_cast(static_cast(-36)), static_cast(static_cast(-30)), + static_cast(static_cast(41)), static_cast(static_cast(-27)), + static_cast(static_cast(6)), static_cast(static_cast(97)), + static_cast(static_cast(-48)), static_cast(static_cast(-38)), + static_cast(static_cast(28)), static_cast(static_cast(125)), + static_cast(static_cast(-77)), static_cast(static_cast(-99)), + static_cast(static_cast(-45)), static_cast(static_cast(7)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(0)), + static_cast(static_cast(0)), static_cast(static_cast(6)) + }}; +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +bool mul8_is_identity_vartime(const ge_p3 &point); +crypto::ec_point clear_torsion_vartime(const ge_p3 &point); +bool get_valid_torsion_cleared_point_vartime(const crypto::ec_point &point, crypto::ec_point &torsion_cleared_out); +//---------------------------------------------------------------------------------------------------------------------- +//---------------------------------------------------------------------------------------------------------------------- +}//namespace fcmp_pp diff --git a/src/ringct/CMakeLists.txt b/src/ringct/CMakeLists.txt index 8a6ed9f8f..c1a6d1463 100644 --- a/src/ringct/CMakeLists.txt +++ b/src/ringct/CMakeLists.txt @@ -70,6 +70,7 @@ target_link_libraries(ringct common cncrypto device + fcmp_pp PRIVATE ${OPENSSL_LIBRARIES} ${EXTRA_LIBRARIES}) diff --git a/src/ringct/rctSigs.cpp b/src/ringct/rctSigs.cpp index 6a2e32a91..90e3c3a09 100644 --- a/src/ringct/rctSigs.cpp +++ b/src/ringct/rctSigs.cpp @@ -30,6 +30,8 @@ #include "rctSigs.h" +#include + #include "misc_log_ex.h" #include "misc_language.h" #include "common/perf_timer.h" @@ -39,6 +41,7 @@ #include "bulletproofs_plus.h" #include "cryptonote_config.h" #include "device/device.hpp" +#include "fcmp_pp/fcmp_pp_crypto.h" #include "serialization/crypto.h" using namespace crypto; @@ -1574,4 +1577,33 @@ namespace rct { key mask; return decodeRct(rv, sk, i, mask, hwdev); } + + bool verPointsForTorsion(const std::vector & pts) { + if (pts.empty()) + return true; + + tools::threadpool& tpool = tools::threadpool::getInstanceForCompute(); + tools::threadpool::waiter waiter(tpool); + + std::atomic all_valid{true}; + for (std::size_t i = 0; i < pts.size(); ++i) + { + tpool.submit(&waiter, [&pts, &all_valid, i] + { + const crypto::ec_point &point = rct::rct2pt(pts[i]); + crypto::ec_point torsion_cleared_point; + if (fcmp_pp::get_valid_torsion_cleared_point_vartime(point, torsion_cleared_point) + && point == torsion_cleared_point) + { + // Point is torsion free if it's equal to itself after clearing torsion + return; + } + all_valid.store(false); + }); + } + + CHECK_AND_ASSERT_MES(waiter.wait(), false, "threadpool waiter failed in torsion check"); + CHECK_AND_ASSERT_MES(all_valid.load(), false, "Torsion check failed"); + return true; + } } diff --git a/src/ringct/rctSigs.h b/src/ringct/rctSigs.h index 6cc7e7497..3b9fe098a 100644 --- a/src/ringct/rctSigs.h +++ b/src/ringct/rctSigs.h @@ -136,6 +136,9 @@ namespace rct { xmr_amount decodeRct(const rctSig & rv, const key & sk, unsigned int i, key & mask, hw::device &hwdev); xmr_amount decodeRct(const rctSig & rv, const key & sk, unsigned int i, hw::device &hwdev); key get_pre_mlsag_hash(const rctSig &rv, hw::device &hwdev); + + // Make sure points are valid points, don't have torsion, and are not equal to identity + bool verPointsForTorsion(const std::vector & pts); } #endif /* RCTSIGS_H */ diff --git a/src/ringct/rctTypes.h b/src/ringct/rctTypes.h index 038c2e150..608aa5bfd 100644 --- a/src/ringct/rctTypes.h +++ b/src/ringct/rctTypes.h @@ -741,10 +741,12 @@ namespace rct { static inline const rct::key &sk2rct(const crypto::secret_key &sk) { return (const rct::key&)sk; } static inline const rct::key &ki2rct(const crypto::key_image &ki) { return (const rct::key&)ki; } static inline const rct::key &hash2rct(const crypto::hash &h) { return (const rct::key&)h; } + static inline const rct::key &pt2rct(const crypto::ec_point &pt) { return (const rct::key&)pt; } static inline const crypto::public_key &rct2pk(const rct::key &k) { return (const crypto::public_key&)k; } static inline const crypto::secret_key &rct2sk(const rct::key &k) { return (const crypto::secret_key&)k; } static inline const crypto::key_image &rct2ki(const rct::key &k) { return (const crypto::key_image&)k; } static inline const crypto::hash &rct2hash(const rct::key &k) { return (const crypto::hash&)k; } + static inline const crypto::ec_point &rct2pt(const rct::key &k) { return (const crypto::ec_point&)k; } static inline bool operator==(const rct::key &k0, const crypto::public_key &k1) { return !crypto_verify_32(k0.bytes, (const unsigned char*)&k1); } static inline bool operator!=(const rct::key &k0, const crypto::public_key &k1) { return crypto_verify_32(k0.bytes, (const unsigned char*)&k1); } } diff --git a/tests/crypto/main.cpp b/tests/crypto/main.cpp index 668c04ea1..913f8abfd 100644 --- a/tests/crypto/main.cpp +++ b/tests/crypto/main.cpp @@ -49,10 +49,6 @@ bool operator !=(const ec_scalar &a, const ec_scalar &b) { return 0 != memcmp(&a, &b, sizeof(ec_scalar)); } -bool operator !=(const ec_point &a, const ec_point &b) { - return 0 != memcmp(&a, &b, sizeof(ec_point)); -} - bool operator !=(const key_derivation &a, const key_derivation &b) { return 0 != memcmp(&a, &b, sizeof(key_derivation)); } diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index dc30a241c..00e425963 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -121,6 +121,7 @@ target_link_libraries(unit_tests daemon_messages daemon_rpc_server blockchain_db + fcmp_pp lmdb_lib rpc net diff --git a/tests/unit_tests/crypto.cpp b/tests/unit_tests/crypto.cpp index 8583185a6..2cd260c9e 100644 --- a/tests/unit_tests/crypto.cpp +++ b/tests/unit_tests/crypto.cpp @@ -39,8 +39,11 @@ extern "C" } #include "crypto/generators.h" #include "cryptonote_basic/merge_mining.h" +#include "fcmp_pp/fcmp_pp_crypto.h" #include "ringct/rctOps.h" +#include "ringct/rctSigs.h" #include "ringct/rctTypes.h" +#include "string_tools.h" namespace { @@ -429,3 +432,92 @@ TEST(Crypto, fe_equals) ASSERT_NE(memcmp(fe_d2_reduced, fe_d2, sizeof(fe)), 0); ASSERT_EQ(fe_equals(fe_d2_reduced, fe_d2), 1); } + +TEST(Crypto, ec_constants_rct_parity) +{ + ASSERT_TRUE(memcmp(&fcmp_pp::EC_I, &rct::I, 32) == 0); + ASSERT_TRUE(memcmp(&fcmp_pp::EC_INV_EIGHT, &rct::INV_EIGHT, 32) == 0); +} + +#define CHECK_CLEARED(k, cleared) \ + crypto::ec_point cleared2; \ + const bool r = fcmp_pp::get_valid_torsion_cleared_point_vartime(rct::rct2pt(k), cleared2); \ + ASSERT_TRUE(r); \ + ASSERT_EQ(cleared, cleared2); + +TEST(Crypto, torsion_check_pass_random) +{ + std::vector pts; + for (int i = 0; i < 1000; ++i) + { + const rct::key pk = rct::pkGen(); + ge_p3 x; + ASSERT_EQ(ge_frombytes_vartime(&x, pk.bytes), 0); + ASSERT_TRUE(rct::isInMainSubgroup(pk)); + ASSERT_FALSE(fcmp_pp::mul8_is_identity_vartime(x)); + const crypto::ec_point cleared = fcmp_pp::clear_torsion_vartime(x); + ASSERT_EQ(rct::rct2pt(pk), cleared); + CHECK_CLEARED(pk, cleared); + pts.emplace_back(pk); + } + ASSERT_TRUE(rct::verPointsForTorsion(pts)); +} + +TEST(Crypto, torsion_check_hardcoded) +{ + struct TorsionTestPoints { std::string point; bool torsion_free; }; + static const std::vector torsion_test_points = { + {"b10ba13e303cbe9abf7d5d44f1d417727abcc14903a74e071abd652ce1bf76dd", false}, + {"9b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd088071", false}, // genesis (see config::GENESIS_TX) + {"785eda585dca4f3d27976106008ccfbca13146c8b21b8c7e4909032639a776e1", true}, + {"9a7b10563aa266032cd075f4e347f348a3841ae4f41572633351a97dd44066b4", true}, + }; + + std::vector all_pts, torsion_free_pts, torsioned_pts, torsion_cleared_pts; + for (const auto &point : torsion_test_points) + { + rct::key k; + epee::string_tools::hex_to_pod(point.point, k); + ge_p3 x; + ASSERT_EQ(ge_frombytes_vartime(&x, k.bytes), 0); + ASSERT_EQ(rct::isInMainSubgroup(k), point.torsion_free); + ASSERT_FALSE(fcmp_pp::mul8_is_identity_vartime(x)); + const crypto::ec_point cleared = fcmp_pp::clear_torsion_vartime(x); + if (point.torsion_free) + { + ASSERT_EQ(rct::rct2pt(k), cleared); + torsion_free_pts.push_back(k); + } + else + { + ASSERT_NE(rct::rct2pt(k), cleared); + torsioned_pts.push_back(k); + } + CHECK_CLEARED(k, cleared); + all_pts.emplace_back(k); + torsion_cleared_pts.emplace_back(rct::pt2rct(cleared)); + } + + ASSERT_TRUE(rct::verPointsForTorsion(torsion_free_pts)); + ASSERT_FALSE(rct::verPointsForTorsion(torsioned_pts)); + ASSERT_FALSE(rct::verPointsForTorsion(all_pts)); + ASSERT_TRUE(rct::verPointsForTorsion(torsion_cleared_pts)); +} + +TEST(Crypto, mul8_is_identity_vartime) +{ + static const std::vector mul8_identity_points = { + rct::I, + rct::Z + }; + + for (const auto &point : mul8_identity_points) + { + ge_p3 x; + ASSERT_EQ(ge_frombytes_vartime(&x, point.bytes), 0); + ASSERT_TRUE(fcmp_pp::mul8_is_identity_vartime(x)); + + crypto::ec_point _; + ASSERT_FALSE(fcmp_pp::get_valid_torsion_cleared_point_vartime(rct::rct2pt(point), _)); + } +}