From 70cdaffcd1803b77cc8856adbff791f147ab7e8a Mon Sep 17 00:00:00 2001 From: Suyash Bagad Date: Tue, 25 Apr 2023 23:46:45 +0530 Subject: [PATCH] [SQUASHED] ecdsa key recovery with test(s). (#346) init commit. Recover pubkey works! Make gcc happy. Make gcc happy (again) gcc fix. don't use y = 0 as error condition. instead use (x = 0, y = 0) as failure return. --- cpp/src/barretenberg/crypto/ecdsa/ecdsa.hpp | 6 ++ .../barretenberg/crypto/ecdsa/ecdsa.test.cpp | 46 ++++++++++- .../barretenberg/crypto/ecdsa/ecdsa_impl.hpp | 81 +++++++++++++++++++ .../ecc/groups/affine_element.hpp | 13 +++ .../ecc/groups/affine_element.test.cpp | 22 +++++ .../ecc/groups/affine_element_impl.hpp | 27 +++++++ 6 files changed, 194 insertions(+), 1 deletion(-) diff --git a/cpp/src/barretenberg/crypto/ecdsa/ecdsa.hpp b/cpp/src/barretenberg/crypto/ecdsa/ecdsa.hpp index a094701a5c..dea2e18658 100644 --- a/cpp/src/barretenberg/crypto/ecdsa/ecdsa.hpp +++ b/cpp/src/barretenberg/crypto/ecdsa/ecdsa.hpp @@ -14,11 +14,15 @@ template struct key_pair { struct signature { std::array r; std::array s; + uint8_t v; }; template signature construct_signature(const std::string& message, const key_pair& account); +template +typename G1::affine_element recover_public_key(const std::string& message, const signature& sig); + template bool verify_signature(const std::string& message, const typename G1::affine_element& public_key, @@ -39,12 +43,14 @@ template inline void read(B& it, signature& sig) { read(it, sig.r); read(it, sig.s); + read(it, sig.v); } template inline void write(B& buf, signature const& sig) { write(buf, sig.r); write(buf, sig.s); + write(buf, sig.v); } template inline void read(B& it, key_pair& keypair) diff --git a/cpp/src/barretenberg/crypto/ecdsa/ecdsa.test.cpp b/cpp/src/barretenberg/crypto/ecdsa/ecdsa.test.cpp index 2c028e101a..e45352f053 100644 --- a/cpp/src/barretenberg/crypto/ecdsa/ecdsa.test.cpp +++ b/cpp/src/barretenberg/crypto/ecdsa/ecdsa.test.cpp @@ -40,6 +40,50 @@ TEST(ecdsa, verify_signature_secp256r1_sha256) EXPECT_EQ(result, true); } +TEST(ecdsa, recover_public_key_secp256k1_sha256) +{ + std::string message = "The quick brown dog jumped over the lazy fox."; + + crypto::ecdsa::key_pair account; + account.private_key = secp256k1::fr::random_element(); + account.public_key = secp256k1::g1::one * account.private_key; + + crypto::ecdsa::signature signature = + crypto::ecdsa::construct_signature(message, account); + + bool result = crypto::ecdsa::verify_signature( + message, account.public_key, signature); + + auto recovered_public_key = + crypto::ecdsa::recover_public_key(message, + signature); + + EXPECT_EQ(result, true); + EXPECT_EQ(recovered_public_key, account.public_key); +} + +TEST(ecdsa, recover_public_key_secp256r1_sha256) +{ + std::string message = "The quick brown dog jumped over the lazy fox."; + + crypto::ecdsa::key_pair account; + account.private_key = secp256r1::fr::random_element(); + account.public_key = secp256r1::g1::one * account.private_key; + + crypto::ecdsa::signature signature = + crypto::ecdsa::construct_signature(message, account); + + bool result = crypto::ecdsa::verify_signature( + message, account.public_key, signature); + + auto recovered_public_key = + crypto::ecdsa::recover_public_key(message, + signature); + + EXPECT_EQ(result, true); + EXPECT_EQ(recovered_public_key, account.public_key); +} + std::vector HexToBytes(const std::string& hex) { std::vector bytes; @@ -131,7 +175,7 @@ TEST(ecdsa, verify_signature_secp256r1_sha256_NIST_1) 0xef, 0x97, 0xb2, 0x18, 0xe9, 0x6f, 0x17, 0x5a, 0x3c, 0xcd, 0xda, 0x2a, 0xcc, 0x05, 0x89, 0x03, }; - crypto::ecdsa::signature sig{ r, s }; + crypto::ecdsa::signature sig{ r, s, 27 }; std::vector message_vec = HexToBytes("5905238877c77421f73e43ee3da6f2d9e2ccad5fc942dcec0cbd25482935faaf416983fe165b1a045ee2bcd2e6dca3bdf46" "c4310a7461f9a37960ca672d3feb5473e253605fb1ddfd28065b53cb5858a8ad28175bf9bd386a5e471ea7a65c17cc934a9" diff --git a/cpp/src/barretenberg/crypto/ecdsa/ecdsa_impl.hpp b/cpp/src/barretenberg/crypto/ecdsa/ecdsa_impl.hpp index 3ff27bceb2..33cec9c433 100644 --- a/cpp/src/barretenberg/crypto/ecdsa/ecdsa_impl.hpp +++ b/cpp/src/barretenberg/crypto/ecdsa/ecdsa_impl.hpp @@ -29,9 +29,90 @@ signature construct_signature(const std::string& message, const key_pair Fr s_fr = (z + r_fr * account.private_key) / k; Fr::serialize_to_buffer(s_fr, &sig.s[0]); + + // compute recovery_id: given R = (x, y) + // 0: y is even && x < |Fr| + // 1: y is odd && x < |Fr| + // 2: y is even && |Fr| <= x < |Fq| + // 3: y is odd && |Fr| <= x < |Fq| + // v = offset + recovery_id + Fq r_fq = Fq(R.x); + bool is_r_finite = (uint256_t(r_fq) == uint256_t(r_fr)); + bool y_parity = uint256_t(R.y).get_bit(0); + constexpr uint8_t offset = 27; + + int value = offset + y_parity + static_cast(2) * !is_r_finite; + ASSERT(value <= UINT8_MAX); + sig.v = static_cast(value); return sig; } +template +typename G1::affine_element recover_public_key(const std::string& message, const signature& sig) +{ + using serialize::read; + uint256_t r_uint; + uint256_t s_uint; + uint8_t v_uint; + + const auto* r_buf = &sig.r[0]; + const auto* s_buf = &sig.s[0]; + const auto* v_buf = &sig.v; + read(r_buf, r_uint); + read(s_buf, s_uint); + read(v_buf, v_uint); + + // We need to check that r and s are in Field according to specification + if ((r_uint >= Fr::modulus) || (s_uint >= Fr::modulus)) { + throw_or_abort("r or s value exceeds the modulus"); + } + if ((r_uint == 0) || (s_uint == 0)) { + throw_or_abort("r or s value is zero"); + } + + // Check that v must either be in {27, 28, 29, 30} + Fr r = Fr(r_uint); + Fr s = Fr(s_uint); + Fq r_fq = Fq(r_uint); + bool is_r_finite = true; + + if ((v_uint == 27) || (v_uint == 28)) { + ASSERT(uint256_t(r) == uint256_t(r_fq)); + } else if ((v_uint == 29) || (v_uint == 30)) { + ASSERT(uint256_t(r) < uint256_t(r_fq)); + is_r_finite = false; + } else { + throw_or_abort("v value is not in {27, 28, 29, 30}"); + } + + // Decompress the x-coordinate r_uint to get two possible R points + // The first uncompressed R point is selected when r < |Fr| + // The second uncompressed R point is selected when |Fr| <= r < |Fq| + // Note that the second condition can occur with probability 1/2^128 so its highly unlikely. + auto uncompressed_points = G1::affine_element::from_compressed_unsafe(r_uint); + typename G1::affine_element point_R = uncompressed_points[!is_r_finite]; + + // Negate the y-coordinate point of R based on the parity of v + bool y_parity_R = uint256_t(point_R.y).get_bit(0); + if ((v_uint & 1) == y_parity_R) { + point_R.y = -point_R.y; + } + + // Start key recovery algorithm + std::vector message_buffer; + std::copy(message.begin(), message.end(), std::back_inserter(message_buffer)); + auto ev = Hash::hash(message_buffer); + Fr z = Fr::serialize_from_buffer(&ev[0]); + + Fr r_inv = r.invert(); + + Fr u1 = -(z * r_inv); + Fr u2 = s * r_inv; + + typename G1::affine_element recovered_public_key(typename G1::element(point_R) * u2 + G1::one * u1); + return recovered_public_key; +} + template bool verify_signature(const std::string& message, const typename G1::affine_element& public_key, const signature& sig) { diff --git a/cpp/src/barretenberg/ecc/groups/affine_element.hpp b/cpp/src/barretenberg/ecc/groups/affine_element.hpp index 81e6099030..82ba93bfef 100644 --- a/cpp/src/barretenberg/ecc/groups/affine_element.hpp +++ b/cpp/src/barretenberg/ecc/groups/affine_element.hpp @@ -31,6 +31,19 @@ template class alignas(64) affine_el typename CompileTimeEnabled = std::enable_if_t<(BaseField::modulus >> 255) == uint256_t(0), void>> static constexpr affine_element from_compressed(const uint256_t& compressed) noexcept; + /** + * @brief Reconstruct a point in affine coordinates from compressed form. + * @details #LARGE_MODULUS_AFFINE_POINT_COMPRESSION Point compression is implemented for curves of a prime + * field F_p with p being 256 bits. + * TODO(Suyash): Check with kesha if this is correct. + * + * @param compressed compressed point + * @return constexpr affine_element + */ + template > 255) == uint256_t(1), void>> + static constexpr std::array from_compressed_unsafe(const uint256_t& compressed) noexcept; + constexpr affine_element& operator=(const affine_element& other) noexcept; constexpr affine_element& operator=(affine_element&& other) noexcept; diff --git a/cpp/src/barretenberg/ecc/groups/affine_element.test.cpp b/cpp/src/barretenberg/ecc/groups/affine_element.test.cpp index 8fef38d0d3..b7dcf57d46 100644 --- a/cpp/src/barretenberg/ecc/groups/affine_element.test.cpp +++ b/cpp/src/barretenberg/ecc/groups/affine_element.test.cpp @@ -61,6 +61,19 @@ template class test_affine_element : public testing::Test { } } + static void test_point_compression_unsafe() + { + for (size_t i = 0; i < 100; i++) { + affine_element P = affine_element(element::random_element()); + uint256_t compressed = uint256_t(P.x); + + // Note that we do not check the point Q_points[1] because its highly unlikely to hit a point P on the curve + // such that r < P.x < q. + std::array Q_points = affine_element::from_compressed_unsafe(compressed); + EXPECT_EQ(P, Q_points[0]); + } + } + // Regression test to ensure that the point at infinity is not equal to its coordinate-wise reduction, which may lie // on the curve, depending on the y-coordinate. // TODO: add corresponding typed test class @@ -91,6 +104,15 @@ TYPED_TEST(test_affine_element, point_compression) } } +TYPED_TEST(test_affine_element, point_compression_unsafe) +{ + if constexpr (TypeParam::Fq::modulus.data[3] >= 0x4000000000000000ULL) { + TestFixture::test_point_compression_unsafe(); + } else { + GTEST_SKIP(); + } +} + // Regression test to ensure that the point at infinity is not equal to its coordinate-wise reduction, which may lie // on the curve, depending on the y-coordinate. TEST(affine_element, infinity_ordering_regression) diff --git a/cpp/src/barretenberg/ecc/groups/affine_element_impl.hpp b/cpp/src/barretenberg/ecc/groups/affine_element_impl.hpp index 691f03f942..f5b2e1705a 100644 --- a/cpp/src/barretenberg/ecc/groups/affine_element_impl.hpp +++ b/cpp/src/barretenberg/ecc/groups/affine_element_impl.hpp @@ -46,6 +46,33 @@ constexpr affine_element affine_element::from_compressed(c return affine_element(x, y); } +template +template +constexpr std::array, 2> affine_element::from_compressed_unsafe( + const uint256_t& compressed) noexcept +{ + auto get_y_coordinate = [](const uint256_t& x_coordinate) { + Fq x = Fq(x_coordinate); + Fq y2 = (x.sqr() * x + T::b); + if constexpr (T::has_a) { + y2 += (x * T::a); + } + return y2.sqrt(); + }; + + uint256_t x_1 = compressed; + uint256_t x_2 = compressed + Fr::modulus; + auto [is_quadratic_remainder_1, y_1] = get_y_coordinate(x_1); + auto [is_quadratic_remainder_2, y_2] = get_y_coordinate(x_2); + + auto output_1 = is_quadratic_remainder_1 ? affine_element(Fq(x_1), y_1) + : affine_element(Fq::zero(), Fq::zero()); + auto output_2 = is_quadratic_remainder_2 ? affine_element(Fq(x_2), y_2) + : affine_element(Fq::zero(), Fq::zero()); + + return { output_1, output_2 }; +} + template constexpr affine_element affine_element::operator+( const affine_element& other) const noexcept