feat: Native ECDSA audit#20658
Conversation
|
|
||
| // Ensure that the value of s is "low", i.e. s := min{ s_fr, (|Fr| - s_fr) } | ||
| const bool is_s_low = (uint256_t(s_fr) < (uint256_t(Fr::modulus) / 2)); | ||
| const bool is_s_low = (uint256_t(s_fr) < (uint256_t(Fr::modulus) + 1) / 2); |
There was a problem hiding this comment.
The previous comparison was wrong: if modulus = 5 and s = 2 then 2 < (5 / 2) is false and we'd get s = 3 in the signature
| std::vector<uint8_t> pkey_buffer; | ||
| write(pkey_buffer, account.private_key); | ||
| Fr k = crypto::get_unbiased_field_from_hmac<Hash, Fr>(message, pkey_buffer); | ||
| Fr k = crypto::deterministic_nonce_rfc6979<Hash, Fr>(message, pkey_buffer); |
There was a problem hiding this comment.
Glad we're now generating nonce as per NIST standard, I always used to be a bit worried about the HMAC usage.
| const bool is_s_low = (uint256_t(s_fr) < (uint256_t(Fr::modulus) / 2)); | ||
| uint256_t s_uint256 = is_s_low ? uint256_t(s_fr) : (uint256_t(Fr::modulus) - uint256_t(s_fr)); | ||
| // Ensure that the value of s is "low", i.e. s := min{ s, (|Fr| - s) } | ||
| const bool is_s_low = (uint256_t(s) < (uint256_t(Fr::modulus) + 1) / 2); |
| using namespace bb::crypto; | ||
|
|
||
| TEST(ecdsa, msgpack) | ||
| // Templated test fixture for ECDSA operations on different curves |
| @@ -19,12 +19,12 @@ struct KeccakHasher { | |||
| static constexpr size_t OUTPUT_SIZE = 32; | |||
| static std::vector<uint8_t> hash(const std::vector<uint8_t>& message) | |||
| { | |||
| keccak256 hash_result = ethash_keccak256(&message[0], message.size()); | |||
| const uint8_t* ptr = message.empty() ? nullptr : message.data(); | |||
| keccak256 hash_result = ethash_keccak256(ptr, message.size()); | |||
There was a problem hiding this comment.
I am curious what happens if message is empty. What would be the hash_result?
There was a problem hiding this comment.
Good point, I added tests for all three hashers to check that the hash of the empty string is correct.
5cd7d00 to
29b68e7
Compare
| ev[0] &= (1 << remainder) - 1; | ||
| } | ||
| ev.insert(ev.begin(), Hash::OUTPUT_SIZE - bit_length, 0); | ||
| const uint8_t* ptr = ev.data(); |
There was a problem hiding this comment.
I feel this is easier to understand
| // Temporary buffer for first HMAC round: V || 0x00 || seed_material | ||
| std::vector<uint8_t> tmp_buffer; | ||
| tmp_buffer.reserve(INITIAL_BUFFER_SIZE + 1 + seed_material.size()); | ||
| std::ranges::copy(v_buffer, std::back_inserter(tmp_buffer)); |
There was a problem hiding this comment.
Preventing the modification of V but not updating tmp_buffer
|
|
||
| TYPED_TEST(EcdsaTests, HighS) | ||
| { | ||
| // Disable asserts because native ecdsa verification raises an error if s >= (n+1)/2 |
There was a problem hiding this comment.
The native functions don't fail anymore, so we shouldn't disable these asserts
| .is_circuit_satisfied = true, | ||
| .comment = "Edge case public key, y coordinate is small", | ||
| }, | ||
| // Modular inverse edge case |
There was a problem hiding this comment.
Imported from native testing
🧾 Audit Context
Native ECDSA audit: test refactoring, deterministic nonce derivation according to RFC6979, small code refactoring.
🛠️ Changes Made
✅ Checklist
📌 Notes for Reviewers