Guide

Symmetric crypto needs both sides to hold the same secret, and you can't email that secret safely. Public-key cryptography solves this with a key pair. The public key can be shared with anyone. The private key never leaves its owner. Two uses matter most in daily work: agreeing on a shared secret, and signing.

The big idea

A signature is made with the private key and checked with the public key. Anyone can verify it, but only the key holder can make it. That gives authenticity, integrity and non-repudiation. An HMAC can't give the last one, because the verifier holds the same key and could have forged the tag.

Use a signature when verifiers should not be able to sign. Examples:

  • A TLS certificate is a certificate authority's signature over a domain name and a public key.
  • An identity provider signs OIDC ID tokens with algorithms such as RS256, ES256 or EdDSA. Thousands of services verify them from a published key set, but none of them can mint tokens.
  • Package managers, container registries and Git commits (signed with SSH or GPG keys) carry signatures.

Public-key crypto rarely encrypts data directly. You exchange a symmetric key with it, and encrypt the data with an AEAD. That combination is called hybrid encryption.

Walking through it

Elliptic curves give the same strength with much smaller keys than RSA:

Security levelRSA keyECC keyExample curve
About 128 bits3072 bits256 bitsP-256, Curve25519
About 192 bits7680 bits384 bitsP-384

An Ed25519 public key is 32 bytes, and its signature is 64 bytes. An RSA-3072 signature is 384 bytes. Use elliptic curves for anything new. Use RSA only where old clients or hardware need it.

Here is a signature round trip:

Python
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.exceptions import InvalidSignature

priv = Ed25519PrivateKey.generate()
pub = priv.public_key()
msg = b'{"order_id": 991, "amount_cents": 4200}'
sig = priv.sign(msg)                # 64 bytes
pub.verify(sig, msg)                # returns None when valid
try:
    pub.verify(sig, msg.replace(b"4200", b"1"))
except InvalidSignature:
    print("tampered message rejected")
print(len(sig), "bytes")

Output:

tampered message rejected
64 bytes

Key exchange and forward secrecy

Diffie–Hellman lets two parties agree on a secret over a public channel. Each side combines its private key with the other's public key and gets the same result. An eavesdropper who sees both public keys can't compute it.

ECDHE runs Diffie–Hellman on elliptic curves with ephemeral keys, generated fresh for each connection. Ephemeral keys give forward secrecy. If a server's long-term key is stolen next year, traffic recorded today still can't be decrypted, because the session keys were thrown away. TLS 1.3 dropped every key exchange that lacks this property. See TLS for how ECDHE and signatures combine in a handshake.

Note

Large quantum computers would break RSA and elliptic-curve crypto. NIST standardized the post-quantum key exchange ML-KEM in 2024, and many TLS stacks now offer hybrid X25519 and ML-KEM key exchange. Keeping TLS libraries current gets you this.

Signing has its own trap. ECDSA needs a fresh random value for every signature. Reuse one, and the private key leaks. That's how the PlayStation 3 code-signing key was extracted in 2010. Ed25519 avoids the problem by deriving its per-signature value deterministically.

Common mistakes

  • Choosing RSA by default for a new system. Ed25519, X25519 and P-256 are the usual defaults.
  • Assuming an HMAC proves who signed a message.
  • Reusing the random value in ECDSA signatures, which leaks the private key.

Check yourself

  • Why does a TLS certificate use a signature rather than an HMAC? Answer: anyone must verify it, but only the authority should be able to create it. An HMAC verifier could forge it.
  • Why does ECDHE with ephemeral keys protect recorded traffic if the server's key leaks later? Answer: the per-session keys were discarded, so the stolen long-term key can't decrypt old sessions.

Key takeaways

  • A signature is made only with the private key and verified with the public key. That's what makes it non-repudiable.
  • Default to elliptic curves. Ed25519 and X25519 are small and fast.
  • Ephemeral key exchange, ECDHE, gives forward secrecy.
  • Use a signature when others must verify but not sign.

Further learning

Next, see password hashing, the one place where you must not use a fast hash.