Post navigation

Elliptic Curves as Python Objects

Last time we saw a geometric version of the algorithm to add points on elliptic curves. We went quite deep into the formal setting for it (projective space ), and we spent a lot of time talking about the right way to define the “zero” object in our elliptic curve so that our issues with vertical lines would disappear.

With that understanding in mind we now finally turn to code, and write classes for curves and points and implement the addition algorithm. As usual, all of the code we wrote in this post is available on this blog’s Github page.

Points and Curves

Every introductory programming student has probably written the following program in some language for a class representing a point.

class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y

It’s the simplest possible nontrivial class: an x and y value initialized by a constructor (and in Python all member variables are public).

We want this class to represent a point on an elliptic curve, and overload the addition and negation operators so that we can do stuff like this:

p1 = Point(3,7)
p2 = Point(4,4)
p3 = p1 + p2

But as we’ve spent quite a while discussing, the addition operators depend on the features of the elliptic curve they’re on (we have to draw lines and intersect it with the curve). There are a few ways we could make this happen, but in order to make the code that uses these classes as simple as possible, we’ll have each point contain a reference to the curve they come from. So we need a curve class.

It’s pretty simple, actually, since the class is just a placeholder for the coefficients of the defining equation. We assume the equation is already in the Weierstrass normal form, but if it weren’t one could perform a whole bunch of algebra to get it in that form (and you can see how convoluted the process is in this short report or page 115 (pdf p. 21) of this book). To be safe, we’ll add a few extra checks to make sure the curve is smooth.

Note that this last check will serve as a coarse unit test for all of our examples. If we mess up then more likely than not the “added” point won’t be on the curve at all. More precise testing is required to be bullet-proof, of course, but we leave explicit tests to the reader as an excuse to get their hands wet with equations.

Before we go ahead and implement addition and the related functions, we need to be decide how we want to represent the ideal point . We have two options. The first is to do everything in projective coordinates and define a whole system for doing projective algebra. Considering we only have one point to worry about, this seems like overkill (but could be fun). The second option, and the one we’ll choose, is to have a special subclass of Point that represents the ideal point.

Note the inheritance is denoted by the parenthetical (Point) in the first line. Each function we define on a Point will require a 1-2 line overriding function in this subclass, so we will only need a small amount of extra bookkeeping. For example, negation is quite easy.

Note that Python allows one to override the prefix-minus operation by defining __neg__ on a custom object. There are similar functions for addition (__add__), subtraction, and pretty much every built-in python operation. And of course addition is where things get more interesting. For the ideal point it’s trivial.

class Ideal(Point):
...
def __add__(self, Q):
return Q

Why does this make sense? Because (as we’ve said last time) the ideal point is the additive identity in the group structure of the curve. So by all of our analysis, , and the code is satisfyingly short.

For distinct points we have to follow the algorithm we used last time. Remember that the trick was to form the line passing through the two points being added, substitute that line for in the elliptic curve, and then figure out the coefficient of in the resulting polynomial. Then, using the two existing points, we could solve for the third root of the polynomial using Vieta’s formula.

In order to do that, we need to analytically solve for the coefficient of the term of the equation . It’s tedious, but straightforward. First, write

The first step of expanding gives us

And we notice that the only term containing an part is the last one. Expanding that gives us

And again we can discard the parts that don’t involve . In other words, if we were to rewrite as , we’d expand all the terms and get something that looks like

where are some constants that we don’t need. Now using Vieta’s formula and calling the third root we seek, we know that

Which means that . Once we have , we can get from the equation of the line .

Note that this only works if the two points we’re trying to add are different! The other two cases were if the points were the same or lying on a vertical line. These gotchas will manifest themselves as conditional branches of our add function.

First, we check if the two points are the same, in which case we use the tangent method (which we do next). Supposing the points are different, if their values are the same then the line is vertical and the third point is the ideal point. Otherwise, we use the formula we defined above. Note the subtle and crucial minus sign at the end! The point is the third point of intersection, but we still have to do the reflection to get the sum of the two points.

Now for the case when the points are actually the same. We’ll call it , and we’re trying to find . As per our algorithm, we compute the tangent line at . In order to do this we need just a tiny bit of calculus. To find the slope of the tangent line we implicitly differentiate the equation and get

The only time we’d get a vertical line is when the denominator is zero (you can verify this by taking limits if you wish), and so implies that and we’re done. The fact that this can ever happen for a nonzero should be surprising to any reader unfamiliar with groups! But without delving into a deep conversation about the different kinds of group structures out there, we’ll have to settle for such nice surprises.

In the other case , we plug in our values into the derivative and read off the slope as . Then using the same point slope formula for a line, we get , and we can use the same technique (and the same code!) from the first case to finish.

There is only one minor wrinkle we need to smooth out: can we be sure Vieta’s formula works? In fact, the real problem is this: how do we know that is a double root of the resulting cubic? Well, this falls out again from that very abstract and powerful theorem of Bezout. There is a lot of technical algebraic geometry (and a very interesting but complicated notion of dimension) hiding behind the curtain here. But for our purposes it says that our tangent line intersects the elliptic curve with multiplicity 2, and this gives us a double root of the corresponding cubic.

And so in the addition function all we need to do is change the slope we’re using. This gives us a nice and short implementation

What’s interesting is how little the data of the curve comes into the picture. Nothing depends on , and only one of the two cases depends on . This is one reason the Weierstrass normal form is so useful, and it may bite us in the butt later in the few cases we don’t have it (for special number fields).

And so we crash headfirst into our first floating point arithmetic issue. We’ll vanquish this monster more permanently later in this series (in fact, we’ll just scrap it entirely and define our own number system!), but for now here’s a quick fix:

Now that we have addition and negation, the rest of the class is just window dressing. For example, we want to be able to use the subtraction symbol, and so we need to implement __sub__

def __sub__(self, Q):
return self + -Q

Note that because the Ideal point is a subclass of point, it inherits all of these special functions while it only needs to override __add__ and __neg__. Thank you, polymorphism! The last function we want is a scaling function, which efficiently adds a point to itself times.

The scaling function allows us to quickly compute ( times). Indeed, the fact that we can do this more efficiently than performing additions is what makes elliptic curve cryptography work. We’ll take a deeper look at this in the next post, but for now let’s just say what the algorithm is doing.

Given a number written in binary , we can write as

The advantage of this is that we can compute each of the iteratively using only additions by multiplying by 2 (adding something to itself) times. Since the number of bits in is , we’re getting a huge improvement over additions.

The algorithm is given above in code, but it’s a simple bit-shifting trick. Just have be some power of two, shifted by one at the end of every loop. Then start with being , and replace , and in typical programming fashion we drop the indices and overwrite the variable binding at each step (Q = Q+Q). Finally, we have a variable to which is added when the -th bit of is a 1 (and ignored when it’s 0). The rest is bookkeeping.

Note that __mul__ only allows us to write something like P * n, but the standard notation for scaling is n * P. This is what __rmul__ allows us to do.

We could add many other helper functions, such as ones to allow us to treat points as if they were lists, checking for equality of points, comparison functions to allow one to sort a list of points in lex order, or a function to transform points into more standard types like tuples and lists. We have done a few of these that you can see if you visit the code repository, but we’ll leave flushing out the class as an exercise to the reader.

As one can see, the precision gets very large very quickly. One thing we’ll do to avoid such large numbers (but hopefully not sacrifice security) is to work in finite fields, the simplest version of which is to compute modulo some prime.

So now we have a concrete understanding of the algorithm for adding points on elliptic curves, and a working Python program to do this for rational numbers or floating point numbers (if we want to deal with precision issues). Next time we’ll continue this train of thought and upgrade our program (with very little work!) to work over other simple number fields. Then we’ll delve into the cryptographic issues, and talk about how one might encode messages on a curve and use algebraic operations to encode their messages.

In practical crypto, it’s important to do these algorithms in constant time, to avoid side-channel attacks. How might you implement __add__ in constant time, taking into account vertical and tangent lines?

You cannot. That is because we’re working over the rational numbers in this step on the way to a more useful implementation. (I say that because I don’t intend anything on this blog to be really efficient, but it will be secure).

In the future of this series, I plan to adapt and extend this code to work for finite fields, where every operation will have take the same amount of time (constant time, since all points involved will have the same precision).

Thanks for the interesting post. Just thought I’d point out that none of the snippets show the __repr__ definition, only the __str__ definition. If you run the examples in the interpreter after importing, all initialisations just print the memory address, rather than the str(self) that you would otherwise want.

Also, perhaps worth noting that if someone is using python 2.x rather than python 3, then a / b automatically casts to int and so the gradients aren’t what they should be. You can fix it by adding ‘from __future__ import division’ at the top of the module. This way you get floats rather than ints. Clearly once you move on to use fractions and such this won’t matter, but I haven’t got that far yet 🙂

It will find the rational points for you, tell you the group structure of the rational points, and make many much more sophisticated calculations as well. And you can work over more or less arbitrary rings to boot.

The point of this series is to provide a clear implementation of the basic ideas behind elliptic curve cryptography. It’s pedagogical. Sage’s implementation is clearly geared toward math research, and it (and its documentation) are illegible to non experts. Who in their right mind would learn modern (scheme-centric) algebraic geometry just to experiment with cryptography?

Write code, not cover letters
Triplebyte's common application lets talented programmers skip resume and recruiter screens while applying to multiple top tech companies at once. Beat their online coding quiz to get started. People interested in math and physics tend to do well.