Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bridge pcurves into the main elliptic curve arithmetic #4143

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

randombit
Copy link
Owner

This results in roughly a 2x speedup for all elliptic curve algorithms, when using a supported curve.

@randombit randombit added this to the Botan 3.6.0 milestone Jun 22, 2024
@randombit randombit requested a review from reneme June 22, 2024 11:29
@randombit randombit changed the base branch from jack/new-ec-types to master June 24, 2024 11:26
@reneme
Copy link
Collaborator

reneme commented Jun 25, 2024

Without a full picture, yet: I think it would make sense to allow disabling the BN-based ECC implementation at compile time.

@randombit
Copy link
Owner Author

I think it would make sense to allow disabling the BN-based ECC implementation at compile time.

In principle yes I agree. However it's greatly complicated by the fact that we have large swaths of public API which expose various BigInt based ec functionality (EC_Group::multiply_mod_order, etc). This is one of those things that I think is best deferred to Botan 4 - once all of these various APIs are removed we have a much better shot at being able to do this cleanly.

@reneme
Copy link
Collaborator

reneme commented Jun 25, 2024

To be able to get an idea of the big picture, I extended the diagram I started at some point. Feedback appreciated. :)

image
excalidraw.com

Note that excalidraw isn't a shared collaboration space. To publish any modifications, one has to re-create a new snapshot via "save as... > shareable link"

Comment on lines +113 to +115
std::vector<uint8_t> bytes(m_order_bytes);
bn.serialize_to(bytes);
return this->scalar_deserialize(bytes);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
std::vector<uint8_t> bytes(m_order_bytes);
bn.serialize_to(bytes);
return this->scalar_deserialize(bytes);
return this->scalar_deserialize(bn.serialize(m_order_bytes));

const EC_Scalar_Data& y) const = 0;
};

class EC_Group_Data final : public std::enable_shared_from_this<EC_Group_Data> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given that virtually all method implementations have the form: if(m_pcurves) { /* pcurves */ } else { /* BigInt */ } I'm somewhat inclined to suggest (yet another) virtual interface here (or perhaps instead of the EC_Scalar_Data)? But I feel you've had that idea and needed to pick the lesser evil.

Copy link
Owner Author

Choose a reason for hiding this comment

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

That would probably be cleaner. Main issue is that even with pcurves enabled we still have to support the full set of BigInt based APIs so we're still carrying around all of the same fields. I guess we could have the pcurves EC_Group_Data derive from the generic one to avoid too much duplication ...

void EC_AffinePoint_Data_PC::serialize_compressed_to(std::span<uint8_t> bytes) const {
const size_t fe_bytes = this->field_element_bytes();
BOTAN_ARG_CHECK(bytes.size() == 1 + fe_bytes, "Invalid output size");
const bool y_is_odd = (m_bytes[m_bytes.size() - 1] & 0x01) == 0x01;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const bool y_is_odd = (m_bytes[m_bytes.size() - 1] & 0x01) == 0x01;
const bool y_is_odd = (m_bytes.back() & 0x01) == 0x01;

Comment on lines +86 to +96
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
PCurve::PrimeOrderCurve::AffinePoint pt) :
m_group(std::move(group)), m_pt(std::move(pt)) {
m_bytes = m_pt.serialize<secure_vector<uint8_t>>();
}

EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
std::span<const uint8_t> bytes) :
m_group(std::move(group)), m_pt(deserialize_pcurve_pt(m_group->pcurve(), bytes)) {
m_bytes = m_pt.serialize<secure_vector<uint8_t>>();
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Two things:

  1. m_bytes could be initialized in the initializer-list as well
Suggested change
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
PCurve::PrimeOrderCurve::AffinePoint pt) :
m_group(std::move(group)), m_pt(std::move(pt)) {
m_bytes = m_pt.serialize<secure_vector<uint8_t>>();
}
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
std::span<const uint8_t> bytes) :
m_group(std::move(group)), m_pt(deserialize_pcurve_pt(m_group->pcurve(), bytes)) {
m_bytes = m_pt.serialize<secure_vector<uint8_t>>();
}
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
PCurve::PrimeOrderCurve::AffinePoint pt) :
m_group(std::move(group)), m_pt(std::move(pt)), m_bytes(m_pt.serialize<secure_vector<uint8_t>>()) {
}
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
std::span<const uint8_t> bytes) :
m_group(std::move(group)), m_pt(deserialize_pcurve_pt(m_group->pcurve(), bytes)), m_bytes(m_pt.serialize<secure_vector<uint8_t>>()) {
}
  1. Given that m_bytes is never written to, perhaps its worthwhile to add std::span<>s that centrally refer to the segments of the serialization. That would avoid spreading the knowledge about the structure in m_bytes across several methods. Like so:
EC_AffinePoint_Data_PC::EC_AffinePoint_Data_PC(std::shared_ptr<const EC_Group_Data> group,
                                               PCurve::PrimeOrderCurve::AffinePoint pt) :
      m_group(std::move(group)), m_pt(std::move(pt)), m_bytes(m_pt.serialize<secure_vector<uint8_t>>()),
      m_bytes_x(std::span{m_bytes}.subspan(1, field_element_bytes())),
      m_bytes_y(std::span{m_bytes}.last(field_element_bytes())) {
}

Comment on lines +100 to +103
if(!p) {
abort();
throw Invalid_State("Failed conversion to EC_AffinePoint_Data_PC");
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

abort() seems a bit harsh.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Oops, was for debugging 🙈

auto x = BigInt::from_bytes(std::span{m_bytes}.subspan(1, fe_bytes));
auto y = BigInt::from_bytes(std::span{m_bytes}.last(fe_bytes));

return EC_Point(m_group->curve(), std::move(x), std::move(y));
Copy link
Collaborator

Choose a reason for hiding this comment

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

clang-tidy says: move won't happen, because this constructor takes the BigInt objects by const&. I think, it shouldn't do that, though. Perhaps change the constructor?

@@ -11,12 +11,14 @@ brief -> "Wrapper for elliptic curve groups"
<requires>
asn1
numbertheory
pcurves
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is that really required? If pcurves is not compiled, I can still fall back to the bigint implementation also for the pcurves-supported curves, no?

Or do we want to model "no pcurves at compile time" by disabling all pcurves_* submodules?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Not really required in that if pcurves wasn't available at all we could just fall back to the generic code without issue. I'm just trying to avoid too much combinatoric explosion (more than is already in place anyway ...) with yet another module / yet more ifdef. IDK. I guess there are probably users who would prefer just using the generic code for size reasons.

return nullptr;
}
} else {
BigInt r(bytes.data(), bytes.size());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
BigInt r(bytes.data(), bytes.size());
BigInt r(bytes);

if(auto s = m_pcurve->scalar_from_wide_bytes(bytes)) {
return std::make_unique<EC_Scalar_Data_PC>(shared_from_this(), std::move(*s));
} else {
throw Invalid_Argument("Failed to complete reduction of scalar bytes");
Copy link
Collaborator

Choose a reason for hiding this comment

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

That's somewhat inconsistent. Many other methods that return a unique_ptr simply return nullptr if something fails but don't throw. Frankly, I'd prefer things to throw over passing a nullptr that I have to remember to check for.

Copy link
Owner Author

Choose a reason for hiding this comment

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

The behavior between pcurves and the generic code are somewhat inconsistent on this - generic always succeeds no matter how big the input is. Maybe the answer here is to also introduce a (somewhat artificial) limit for the generic case so they behave the same.

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.

None yet

2 participants