Skip to content

Comments

feat: Native ECDSA audit#20658

Merged
federicobarbacovi merged 12 commits intomerge-train/barretenbergfrom
fb/native_ecdsa_audit
Feb 23, 2026
Merged

feat: Native ECDSA audit#20658
federicobarbacovi merged 12 commits intomerge-train/barretenbergfrom
fb/native_ecdsa_audit

Conversation

@federicobarbacovi
Copy link
Contributor

@federicobarbacovi federicobarbacovi commented Feb 18, 2026

🧾 Audit Context

Native ECDSA audit: test refactoring, deterministic nonce derivation according to RFC6979, small code refactoring.

🛠️ Changes Made

  • Derivation of the nonce (ephemeral key) is now in accordance with RFC6979
  • Testing refactored and extended
  • Added some Wycherproof testing infrastructure
  • Code refactoring to make it easier to read
  • Address TODOs about erasing data in HMAC

✅ Checklist

  • Audited all methods of the relevant module/class
  • Audited the interface of the module/class with other (relevant) components
  • Documented existing functionality and any changes made (as per Doxygen requirements)
  • Resolved and/or closed all issues/TODOs pertaining to the audited files
  • Confirmed and documented any security or other issues found (if applicable)
  • Verified that tests cover all critical paths (and added tests if necessary)
  • Updated audit tracking for the files audited (check the start of each file you audited)

📌 Notes for Reviewers


// 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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@federicobarbacovi federicobarbacovi self-assigned this Feb 19, 2026
@federicobarbacovi federicobarbacovi marked this pull request as ready for review February 19, 2026 17:01
@fcarreiro fcarreiro removed their request for review February 19, 2026 17:05
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch!

using namespace bb::crypto;

TEST(ecdsa, msgpack)
// Templated test fixture for ECDSA operations on different curves
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

@@ -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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious what happens if message is empty. What would be the hash_result?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I added tests for all three hashers to check that the hash of the empty string is correct.

Copy link
Contributor

@suyash67 suyash67 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

ev[0] &= (1 << remainder) - 1;
}
ev.insert(ev.begin(), Hash::OUTPUT_SIZE - bit_length, 0);
const uint8_t* ptr = ev.data();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imported from native testing

@federicobarbacovi federicobarbacovi merged commit 37a026d into merge-train/barretenberg Feb 23, 2026
10 checks passed
@federicobarbacovi federicobarbacovi deleted the fb/native_ecdsa_audit branch February 23, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants