Skip to main content
Cipher Visualizers home

Elliptic Curve Diffie-Hellman (ECDH)

ECDH lets two parties agree on a shared secret using points on an elliptic curve. Learn how P-256, P-384, and secp256k1 define the group, how public keys are encoded and validated, why the secret is only an x coordinate, and how an invalid-curve attack works.

Pashalis Laoutaris 23 min read

Try it

Interactive ECDH Key Exchange Visualizer

🔐 ECDH Key Exchange

6
Enter text and click a button to start!
–

Elliptic Curve Diffie-Hellman (ECDH)

Introduction

Two parties who have never met want a shared secret key, and the only channel they have is one that an eavesdropper can read. Diffie-Hellman key exchange solves this. ECDH is the version of it that works in the group of points on an elliptic curve. Each side picks a private number, publishes one point, and combines the other side’s point with its own number. Both arrive at the same point, and its x coordinate is the shared secret.

The elliptic curve guide explains the curve arithmetic with a toy curve of 97 elements. This guide works with the real thing. It uses the NIST curves P-256 and P-384 and the curve secp256k1, and it checks every number against the test vectors of RFC 5903 and against OpenSSL. It covers the domain parameters, the encoding of public keys, the validation that a receiver must do, the step from a point to a secret, and the attack that appears when validation is skipped.

X25519 is a different design of the same idea on a different curve. It has its own guide because its keys and its function look very different from the ones here.

Table of Contents

History and Standards

The Diffie-Hellman exchange itself dates from 1976, and the Diffie-Hellman guide covers it in its original form with integers modulo a prime. Neal Koblitz and Victor Miller proposed elliptic curves for cryptography in 1985, independently of each other. ECDH replaces the exponentiation of the original with the multiplication of a point by a number.

Several standards describe the curves and the protocol. RFC 5903, written by Fu and Solinas of the NSA and published in June 2010, defines three elliptic curve groups modulo a prime for the IKE and IKEv2 protocols. It obsoletes RFC 4753. The RFC lists the groups as P-256, P-384, and P-521 of NIST, and as secp256r1, secp384r1, and secp521r1 of the SECG. It states that the groups were chosen to align IKE with other elliptic curve standards, and that they correspond to symmetric key sizes of 128, 192, and 256 bits.

RFC 5903 also corrected a detail of its predecessor. RFC 4753 said that the shared secret has the same format as a public value, which is both coordinates. RFC 5903 replaced that with the x coordinate alone, to match other standards. The change had first appeared as an erratum, and the RFC warns that implementations that ignored the erratum may not interoperate with it. The choice of the x coordinate is the format that TLS 1.3 uses as well, as the section on secrets below describes.

The Domain Parameters

Both parties must agree on the domain parameters before anything else. For a curve over a prime field they are the prime p, the coefficients a and b of the equation y^2 = x^3 + a*x + b, a base point G, the order n of that point, and the cofactor h. The cofactor is the number of points on the curve divided by n. All three curves in this guide have a cofactor of 1.

Curve Equation Prime p Order n Also known as
P-256 y^2 = x^3 - 3x + b 2^256 - 2^224 + 2^192 + 2^96 - 1 256 bits secp256r1, prime256v1
P-384 y^2 = x^3 - 3x + b 2^384 - 2^128 - 2^96 + 2^32 - 1 384 bits secp384r1
secp256k1 y^2 = x^3 + 7 2^256 - 2^32 - 977 256 bits the curve of Bitcoin

The NIST primes are generalized Mersenne primes, as RFC 5903 calls them. Their shape makes reduction modulo p fast. RFC 5903 says that P-256 and P-384 were each chosen verifiably at random, using SHA-1 and a published seed. The Bitcoin wiki describes secp256k1 differently. It says the curve was constructed in a special non-random way, that this allows efficient computation, and that its constants were selected in a predictable way, which reduces the chance of a hidden backdoor. It also says the curve is often more than 30% faster than other curves if the implementation is well optimized.

I confirmed the parameters by computation. The test code reads p, b, n, and the base point of P-256 and P-384 from the text of RFC 5903 and compares them with the constants in the program. It checks that p and n are prime for all three curves, that the base point lies on the curve, that n * G is the point at infinity, and that (n - 1) * G is -G. It also checks that each prime has the closed form in the table. The order n is close to p + 1, within the bound of Hasse, which means that the cofactor is 1.

Keys and Their Encoding

A private key is an integer d from 1 to n - 1. The public key is the point Q = d * G, which is G added to itself d times. Computing Q from d is easy. Recovering d from Q is the elliptic curve discrete logarithm problem, and no efficient method is known for the curves here.

A point has two coordinates, and a public key must be sent as bytes. Two encodings are in common use.

  • Uncompressed. One byte with the value 4, then x, then y, each as a fixed number of big-endian bytes. TLS 1.3 uses this form for the NIST curves, with the byte named legacy_form. The Bitcoin wiki shows the same prefix for the base point of secp256k1.
  • Compressed. One byte, 2 or 3, then x. The prefix tells the receiver whether y is even or odd. The receiver recomputes y from the curve equation.

For P-256 the uncompressed key takes 65 bytes and the compressed key takes 33 bytes. For P-384 the sizes are 97 and 49 bytes.

Recovering y is cheap for these three primes, because each of them leaves a remainder of 3 when divided by 4. Then a square root of v modulo p is v^((p + 1) / 4), as long as v has a square root at all. If the result squared is not v, the value x is not the coordinate of any point and the key is invalid. My code uses this, and OpenSSL produces the same compressed keys for all 180 random keys that I compared.

The Key Exchange

The protocol has four steps. The domain parameters are public and agreed in advance.

  1. Alice picks a random private key d_A and computes the public key Q_A = d_A * G. Bob does the same with d_B and Q_B.
  2. They exchange Q_A and Q_B. An eavesdropper sees both.
  3. Alice computes the point d_A * Q_B. Bob computes the point d_B * Q_A.
  4. Both points are the same point, because d_A * (d_B * G) = (d_A * d_B) * G = d_B * (d_A * G). The x coordinate of that point is the shared secret.

Multiplying a point by a number looks like repeated addition, but it is much faster than that. The double and add method reads the bits of the number from the top. For each bit after the first it doubles the current point, and when the bit is 1 it also adds the starting point. A 256-bit key needs at most 255 doublings and at most 255 additions. Alice’s key in the example below takes 255 doublings and 118 additions.

An eavesdropper who knows G, Q_A, and Q_B must find (d_A * d_B) * G. This is the elliptic curve Diffie-Hellman problem. The only known way to solve it in general is to solve the discrete logarithm problem for one of the keys.

Validating a Public Key

A receiver must never use a public key that it has not checked. RFC 8446 states this for TLS 1.3 in strong words: for P-256, P-384, and P-521 the peers MUST validate each other’s public value. The check has three steps:

  1. The point is not the point at infinity.
  2. The coordinates x and y are integers in the range from 0 to p - 1.
  3. The pair (x, y) is a solution of the curve equation.

For these curves the RFC adds that implementers do not need to check membership in the correct subgroup. That is the effect of the cofactor 1. Every point of the curve other than infinity has the order n.

Why does the check matter? The formulas for adding and doubling points use the coefficient a, but they never use b. A point from a different curve with the same p and a goes through exactly the same arithmetic. My test confirms this. For each of the three curves it takes a random pair (x, y), computes the b of the other curve through that pair, and multiplies by random numbers. The result is the same under the formulas of both curves, and it never lands on the real curve.

This is the basis of the invalid-curve attack. An attacker chooses a curve of the same shape whose order has small prime factors, and sends a point of small order as a public key. The victim multiplies it by the private key and uses the result. The result reveals the private key modulo that small order. Repeating this with several points collects enough remainders to rebuild the whole key. Wikipedia notes that several TLS libraries were found to be vulnerable to this attack. The defense is the three-step check, and it costs almost nothing.

My code does the check inside decode_point. A test changes one bit of the y coordinate of a valid P-256 key. All 32 such changes are refused. Six malformed encodings are refused too: an empty string, a point of all zeros, an x that is too large, a wrong prefix, a key one byte too short, and a key one byte too long.

From a Point to a Secret

The shared point has two coordinates, but the secret is the x coordinate only. RFC 5903 says that the shared secret consists of the x value of the common value. RFC 8446 says the same for TLS 1.3 and adds a detail that is easy to miss. The x coordinate is written as an octet string of constant length for the field, and leading zeros MUST NOT be truncated.

This detail has a practical effect. In about one exchange in 256, the x coordinate of a P-256 shared point begins with a zero byte. In my tests, one such secret is 001ed9a7f3ab29f7f525855225aafce31289a22a9ef3746fa00eff61511c5f50. As a number, it has only 62 hexadecimal digits, so code that converts the number to text and back would produce 31 bytes. OpenSSL returns all 32 bytes, and my code matches it.

The x coordinate is not a key. A point and its negative have the same x coordinate, so the secret takes only about half of the possible values. Wikipedia says for this reason the raw secret should not be used directly as a symmetric key, but only as input to a key derivation function. Most standardized protocols do this. TLS 1.3 uses the identity function as the formal step, because the secret is then fed into the key schedule of the protocol, and RFC 8446 notes that the full picture is a non-trivial key derivation. RFC 7748 advises, for its own curves, that the derivation include both public keys as well as the secret.

Interactive Visualizer

The visualizer above runs real ECDH on any of the three curves. Choose a curve, then enter a private key for Alice and one for Bob, in hexadecimal. The defaults for P-256 and P-384 are the private keys from the test vectors of RFC 5903. The secp256k1 defaults reuse the P-256 keys, because I know of no standard test vector for that curve.

The log shows the curve and the base point, then the first four steps of Alice’s double and add with the point after each bit. It then lists both public keys in compressed form, the validation that each side does on the other’s key, both shared points, the comparison of the two, and the final secret. Try a private key of 1 to see that the public key is the base point itself. Try the largest valid key n - 1 to see the negative of the base point, which has the same x coordinate.

A Worked Example

The test vector of section 8.1 of RFC 5903 uses P-256. Alice’s private key is c88f01f510d9ac3f70a292daa2316de544e9aab8afe84049c62a9c57862d1433, and Bob’s is c6ef9c5d78ae012a011164acb397ce2088685d8f06bf9be0b283ab46476bee53. Alice’s public key has these coordinates:

x: dad0b65394221cf9b051e1feca5787d098dfe637fc90b9ef945d0c3772581180
y: 5271a0461cdb8252d61f1c456fa3e59ab1f45b33accf5f58389e0577b8990bb3

Its compressed form starts with 03, because y is odd:

03dad0b65394221cf9b051e1feca5787d098dfe637fc90b9ef945d0c3772581180

Bob’s public key is (d12dfb5289c8d4f81208b70270398c342296970a0bccb74c736fc7554494bf63, 56fbf3ca366cc23e8157854c13c58d6aac23f046ada30f8353e74f33039872ab). Alice multiplies Bob’s key by her private key, and Bob multiplies Alice’s key by his. Both get a point with this y coordinate:

522bde0af0d8585b8def9c183b5ae38f50235206a8674ecb5d98edb20eb153a2

The shared secret is the x coordinate of that point, and it equals the value girx printed in the RFC:

d6840f6b42f6edafd13116e0e12565202fef8e9ece7dce03812464d04b9442de

For the first four bits of Alice’s key after the leading 1, the double and add method runs like this. Her key starts with the bits 11001. After the bit 1 the partial scalar is 3 and the x coordinate of the point is 5ecbe4d1...c6e7fd6c. After the bit 0 the scalar is 6 and x is b01a172a...3c2291a9. After the bit 0 the scalar is 12 and x is 741dd5bd...8624e3c4. After the bit 1 the scalar is 25 and x is 3a67e255...f200d687. The code checks each point against a direct multiplication of G by the partial scalar. The value for scalar 3 is the well-known x coordinate of 3 * G.

The P-384 vector of section 8.2 gives a secret of 48 bytes:

11187331c279962d93d604243fd592cb9d0a926f422e47187521287e7156c5c4d603135569b9e9d09cf5d4a270f59746

Python Implementation

This is a complete ECDH for the three curves. It has the point arithmetic, the encoding and decoding of keys with validation, and the shared secret. Points are pairs of integers, and the point at infinity is None.

# ecdh.py
# Elliptic Curve Diffie-Hellman on curves of the form y^2 = x^3 + a*x + b over GF(p).
# Three curves: P-256 and P-384 (NIST), and secp256k1 (used by Bitcoin).
# Teaching code. It is not constant time, so it must not handle real secrets.


class Curve:
    def __init__(self, name, p, a, b, gx, gy, n):
        self.name, self.p, self.a, self.b = name, p, a, b
        self.g = (gx, gy)
        self.n = n  # the order of the base point. The cofactor is 1 for all three curves.
        self.size = (p.bit_length() + 7) // 8  # bytes in a coordinate


P256 = Curve(
    "P-256",
    0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF,
    -3,
    0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B,
    0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,
    0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5,
    0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551,
)
P384 = Curve(
    "P-384",
    2 ** 384 - 2 ** 128 - 2 ** 96 + 2 ** 32 - 1,
    -3,
    0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF,
    0xAA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7,
    0x3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F,
    0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973,
)
SECP256K1 = Curve(
    "secp256k1",
    0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F,
    0,
    7,
    0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
    0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8,
    0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,
)
CURVES = {c.name: c for c in (P256, P384, SECP256K1)}

# A point is a pair (x, y). The point at infinity, the neutral element, is None.


def on_curve(curve, point):
    if point is None:
        return True
    x, y = point
    p = curve.p
    return 0 <= x < p and 0 <= y < p and (y * y - (x * x * x + curve.a * x + curve.b)) % p == 0


def negate(curve, point):
    return None if point is None else (point[0], (-point[1]) % curve.p)


def add(curve, p1, p2):
    """The sum of two points. The same formulas double a point and add the neutral element."""
    if p1 is None:
        return p2
    if p2 is None:
        return p1
    p = curve.p
    (x1, y1), (x2, y2) = p1, p2
    if x1 == x2:
        if (y1 + y2) % p == 0:
            return None  # a point plus its negative
        slope = (3 * x1 * x1 + curve.a) * pow(2 * y1, -1, p) % p  # doubling: the slope of the tangent
    else:
        slope = (y2 - y1) * pow(x2 - x1, -1, p) % p  # the slope of the line through the two points
    x3 = (slope * slope - x1 - x2) % p
    y3 = (slope * (x1 - x3) - y1) % p
    return (x3, y3)


def multiply(curve, k, point):
    """k * point by double-and-add."""
    result = None
    while k:
        if k & 1:
            result = add(curve, result, point)
        point = add(curve, point, point)
        k >>= 1
    return result


def public_key(curve, private_key):
    if not 1 <= private_key < curve.n:
        raise ValueError("the private key must be between 1 and n - 1")
    return multiply(curve, private_key, curve.g)


# ---------- encoding of points (SEC 1) ----------

def encode_point(curve, point, compressed=False):
    x = point[0].to_bytes(curve.size, "big")
    if compressed:
        return bytes([2 + (point[1] & 1)]) + x
    return b"\x04" + x + point[1].to_bytes(curve.size, "big")


def decode_point(curve, data):
    """Decode and validate a public key. Raises ValueError for anything that is not a point of the curve."""
    p = curve.p
    if len(data) == 1 + 2 * curve.size and data[0] == 4:
        point = (int.from_bytes(data[1:1 + curve.size], "big"), int.from_bytes(data[1 + curve.size:], "big"))
    elif len(data) == 1 + curve.size and data[0] in (2, 3):
        x = int.from_bytes(data[1:], "big")
        if x >= p:
            raise ValueError("x is too large")
        y = pow((x * x * x + curve.a * x + curve.b) % p, (p + 1) // 4, p)  # a square root, because p = 3 mod 4
        if y * y % p != (x * x * x + curve.a * x + curve.b) % p:
            raise ValueError("x is not the coordinate of a point")
        point = (x, y if (y & 1) == data[0] - 2 else p - y)
    else:
        raise ValueError("unknown point format")
    if not on_curve(curve, point):
        raise ValueError("the point is not on the curve")
    return point


# ---------- Diffie-Hellman ----------

def shared_secret(curve, private_key, peer_public):
    """The x coordinate of private_key * peer_public, as bytes of the full length of a coordinate."""
    if not 1 <= private_key < curve.n:
        raise ValueError("the private key must be between 1 and n - 1")
    peer = decode_point(curve, peer_public)  # validates the peer's key
    shared = multiply(curve, private_key, peer)
    if shared is None:
        raise ValueError("the shared point is the point at infinity")
    return shared[0].to_bytes(curve.size, "big")


if __name__ == "__main__":
    # The example of RFC 5903, section 8.1: the 256-bit random ECP group, which is P-256
    curve = P256
    alice = 0xC88F01F510D9AC3F70A292DAA2316DE544E9AAB8AFE84049C62A9C57862D1433
    bob = 0xC6EF9C5D78AE012A011164ACB397CE2088685D8F06BF9BE0B283AB46476BEE53
    alice_public = encode_point(curve, public_key(curve, alice))
    bob_public = encode_point(curve, public_key(curve, bob))
    print("Alice public:", alice_public.hex())
    print("Bob public:  ", bob_public.hex())
    print("Alice secret:", shared_secret(curve, alice, bob_public).hex())
    print("Bob secret:  ", shared_secret(curve, bob, alice_public).hex())
    # A public key that is not on the curve is refused
    bad = alice_public[:-1] + bytes([alice_public[-1] ^ 1])
    try:
        shared_secret(curve, bob, bad)
    except ValueError as error:
        print("a changed public key is refused:", error)

Running it produces this output:

Alice public: 04dad0b65394221cf9b051e1feca5787d098dfe637fc90b9ef945d0c37725811805271a0461cdb8252d61f1c456fa3e59ab1f45b33accf5f58389e0577b8990bb3
Bob public:   04d12dfb5289c8d4f81208b70270398c342296970a0bccb74c736fc7554494bf6356fbf3ca366cc23e8157854c13c58d6aac23f046ada30f8353e74f33039872ab
Alice secret: d6840f6b42f6edafd13116e0e12565202fef8e9ece7dce03812464d04b9442de
Bob secret:   d6840f6b42f6edafd13116e0e12565202fef8e9ece7dce03812464d04b9442de
a changed public key is refused: the point is not on the curve

I checked this code against several sources before writing it up. The constants of P-256 and P-384 are read from the text of RFC 5903 and compared. So are the public keys, the shared points, and the shared secrets of the test vectors in sections 8.1 and 8.2, which all match. The code agrees with the closed forms of the three primes, and the base point of every curve has the order that the standard prints. On random keys, addition agrees with scalar multiplication. In Node.js, which uses OpenSSL, 180 random exchanges on the three curves give the same public keys, the same compressed keys, and the same shared secrets. OpenSSL accepts the private key n - 1 and refuses n on the curves where I tried it, and it refuses a public key with one changed bit. It also keeps the leading zero byte of the secret shown above. The visualizer code matches all 147 test vectors that this Python code produces, and I ran its page in a simulated browser.

Where It Is Used

  • TLS 1.3. RFC 8446 says a compliant application MUST support key exchange with secp256r1, which is P-256, and SHOULD support key exchange with X25519. The key shares of the NIST curves use the uncompressed encoding. The TLS guide covers the handshake.
  • IKE and IKEv2. RFC 5903 defines the three groups for these key exchange protocols, and the shared secret is the x coordinate.
  • Ephemeral keys. Most uses generate a fresh key pair for each connection. The variant is called ECDHE, and it gives forward secrecy, because the private keys are discarded after use. The Diffie-Hellman guide explains the idea.
  • secp256k1. The Bitcoin wiki says that Bitcoin uses this curve with the ECDSA algorithm, and that the same curve and keys can be used in some other algorithms.

Limitations

This is a teaching implementation, not a production one:

  • Not constant time. The double and add loop branches on the bits of the private key, and Python integers take time that depends on their size. Real code must not leak the key through timing or memory access.
  • Slow arithmetic. Each point operation inverts a number modulo p. Real libraries use projective coordinates and delay the inversion to the end.
  • No key generation. The code takes the private key as given. A real program must draw it from a secure random source, in the range from 1 to n - 1.
  • No key derivation. The code returns the raw x coordinate. A real protocol runs it through a key derivation function together with both public keys.
  • No authentication. ECDH gives a secret with whoever sent the public key. It does not tell you who that is. Wikipedia notes that ephemeral keys are not necessarily authenticated, and that authentication is needed to avoid a man in the middle. Protocols add certificates or signatures such as ECDSA.
  • Static keys. Wikipedia says static public keys provide neither forward secrecy nor resilience to key-compromise impersonation. It also says holders of static private keys should validate the other public key and apply a secure key derivation function.
  • Three curves only. The P-521 curve of RFC 5903 is not included.

Security Status

RFC 5903 maps the three NIST groups to the strength of AES: 128 bits for P-256, 192 bits for P-384, and 256 bits for P-521. That is the level the RFC claims for the groups, and it assumes that the implementation does its part.

Several weaknesses depend on the implementation and not on the curve. The invalid-curve attack needs a missing check. A weak random source for private keys is fatal, because the public key is a fixed function of the private key. RFC 7748 names timing and cache attacks as the side channels that a good implementation must resist.

A large quantum computer running Shor’s algorithm would solve the discrete logarithm problem on these curves, as on every other elliptic curve and on the integer version of the exchange. RFC 7748 makes the same statement for its curves. For key exchange that has to resist quantum computers, see the post-quantum schemes in the Kyber guide.

FAQ

What is the difference between ECDH and ECDHE?

ECDH is the key agreement. ECDHE is the same agreement with an ephemeral key pair, which means that at least one party creates a new pair for each session and then discards the private key. That gives forward secrecy. In TLS 1.3 the elliptic curve key shares are ephemeral, and the RFC calls them ECDHE parameters. The term describes how the agreement is used and not a different algorithm.

Why is only the x coordinate used as the secret?

Because the x coordinate is enough to define the shared secret, and both sides can compute it. A point and its negative have the same x coordinate, so the sign of the point carries no information that both sides share. The standards RFC 5903 and RFC 8446 both name the x coordinate as the secret. RFC 8446 also says that the string must keep its full length, even with leading zeros.

Do I have to validate the public key I receive?

You must validate every key you receive. RFC 8446 requires it for the NIST curves, and the invalid-curve attack shows what can go wrong. The check is short. Make sure the point is not infinity, that both coordinates are in range, and that the point satisfies the curve equation. With a cofactor of 1, no subgroup check is needed.

Can I use the shared secret directly as an AES key?

No, you should not. The x coordinate is not uniformly random, because only about half of the values can occur. Wikipedia recommends using it as input to a key derivation function instead. RFC 7748 advises including both public keys in the derivation, so that the key depends on the whole exchange.

Should I choose P-256 or X25519?

Both are widely supported. TLS 1.3 requires P-256 and recommends X25519. X25519 has simpler formulas that are easier to write in constant time and accepts any 32-byte public key, as the X25519 guide explains. P-256 needs the validation described above, but it is the curve that standards and hardware most often require.

Does ECDH resist quantum computers?

No, it does not. Shor’s algorithm would break the discrete logarithm problem on elliptic curves. Plan for a hybrid or post-quantum scheme when the secrets must stay safe for many years. The Kyber guide covers one.

References

  1. Fu and Solinas, both of the NSA. “Elliptic Curve Groups modulo a Prime (ECP Groups) for IKE and IKEv2.” RFC 5903, Internet Engineering Task Force, June 2010. Available at: https://www.rfc-editor.org/rfc/rfc5903

  2. Rescorla, Eric. “The Transport Layer Security (TLS) Protocol Version 1.3.” RFC 8446, August 2018. Available at: https://www.rfc-editor.org/rfc/rfc8446

  3. Langley, Adam, Hamburg, Mike, and Turner, Sean. “Elliptic Curves for Security.” RFC 7748, January 2016. Available at: https://www.rfc-editor.org/rfc/rfc7748

  4. Wikipedia contributors. “Elliptic-curve Diffie-Hellman.” Wikipedia. Available at: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman

  5. “Secp256k1.” Bitcoin Wiki, revision of 14 December 2025. Available at: https://en.bitcoin.it/wiki/Secp256k1

  6. Koblitz, Neal. “Elliptic curve cryptosystems.” Mathematics of Computation, 48(177), 1987, pp. 203-209.

  7. Miller, Victor S. “Use of elliptic curves in cryptography.” CRYPTO 1985 Proceedings, 1986, pp. 417-426.

Modern Asymmetric Algorithms Advanced

ECDSA: Elliptic Curve Digital Signature Algorithm

ECDSA is the elliptic curve version of DSA, used to sign TLS handshakes and Bitcoin transactions. Learn how signing and verifying work, why a repeated nonce reveals the private key, how RFC 6979 removes that risk, and how a signature is encoded.

23 min read

Modern Asymmetric Algorithms Advanced

X25519 and Curve25519

X25519 is the Diffie-Hellman function on Curve25519, defined in RFC 7748. Learn how a 32-byte private key is clamped, how the Montgomery ladder multiplies a point using only its u coordinate, why the curve was chosen, and what the all-zero check protects against.

18 min read

Modern Asymmetric Algorithms Advanced

BLS Signatures

BLS signatures use a bilinear pairing on the curve BLS12-381 to make short, deterministic signatures that can be added together into one. Learn how a pairing works, how a message is hashed to a curve, how aggregation saves space, and how a rogue key attack is stopped.

39 min read