Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why → how

Why public-key signatures are not just 'encryption in reverse'

They look symmetric — encrypt with one key, decrypt with the other — but signatures and encryption answer different questions, and conflating them is how real cryptosystems get broken.

Security intermediate Apr 29, 2026 · updated Aug 25, 2026 · 13 min read

On this page

The picture version

Five pictures for a reader who has only ever heard “two keys that undo each other,” following one download: a release tarball and the tiny .sig file next to it.

1 · The puzzle

Nothing here was ever secret.

tool.tar.gz 40 MB, in the open, on a public page anyone at all can read it tool.tar.gz.sig under a kilobyte, also in the open also readable by anyone So the small file cannot be hiding anything. It answers a different question. encryption asks: can anyone but the intended reader see this? a signature asks: who vouched for these exact bytes, and are they still intact? same key-pair shape, opposite questions — which is why the tidy “two keys that undo each other” story goes wrong
Start from what is visibly true on the download page: the artifact was public the whole time. Whatever the signature bought, it was never confidentiality — and that was never the job.

2 · The tempting wrong model

“Encrypt with the private key” once turned a signer into a decryption service.

textbook RSA really can be pointed either way — that coincidence is where the story came from a ciphertext dressed up as a message “sign this” your signing routine one key, one unpadded operation returns the plaintext as the “signature” One protocol became an oracle for the other. not a thought experiment — this is what happens when the two operations are not separated the modern repair is two things, not one RSA-PSS for signing and RSA-OAEP for encrypting use deliberately different padding… …and the spec still says give each scheme its own key pair. Different padding, different keys.
The “same operation, opposite direction” intuition is not a lie historically — it is a trap operationally. Different padding closes that particular oracle; separate key pairs are what close the general question, because a proof about one scheme says nothing about what an attacker learns from watching the other use the same key.

3 · Two repair ladders

Fix each side properly and they end up sharing almost no parts.

signing the tarball too big to sign → sign the 32-byte digest still malleable → padding that kills the structure Ed25519 · ECDSA · RSA-PSS encrypting to someone too big, and padding oracles leak plaintext → fresh symmetric key + an AEAD over the payload → wrap that key, or agree it via ECDH A hash and padding on one side. A key, a nonce and an AEAD tag on the other. almost none of that machinery is shared — the repairs do not rhyme They are two constructions that happen to use the same kind of key pair.
This is the scene that settles the question. If signatures really were encryption run backwards, the two ladders would have converged on the same parts — and instead each one needed machinery the other has no use for.

4 · The one fact to remember

Whose private key is in play flips between the two.

encryption the private key is the reader’s flows toward confidentiality flows from authenticity signature the private key is the author’s That single fact re-derives which primitive you need. a signature gives no privacy — your tarball is signed and still completely public encryption gives no authorship — the recipient’s public key is public, so anyone can encrypt to them need both? sign with your key, then encrypt the signed bundle to theirs two key pairs, two questions answered — which is also why TLS pairs a key exchange with a certificate signature
If you keep one distinguishing fact, keep this one. Confidentiality flows toward the private-key holder and authenticity flows from one — so any design that tries to get both out of a single private key is answering only one of the two questions.

5 · Keep this card

The whole thing on one index card.

not one primitive, two: signature — private key vouches, everyone verifies encryption — everyone encrypts, private key reads ∴ the fixes don’t rhyme — that is the proof they are different so “same operation, opposite direction” stops being tempting once you have seen both repair ladders and the tiny file next to the tarball says: the holder of this private key saw precisely these bytes and vouched for them
Picture to keep: a wax seal versus a locked box. The seal doesn’t hide the letter — anyone can read it — it just proves who closed it and that nobody opened it since. Where it breaks: a wax seal can be lifted onto a different letter, while a real signature is welded to the exact bytes it signed.

Why it exists

You download a release of some CLI tool — a .tar.gz, and sitting next to it on the download page, a second file: tool.tar.gz.sig. A rounding error next to the tarball — well under a kilobyte. You run the verify command, it prints Good signature from ..., and you install. The tarball you just verified was not encrypted at any point. It was sitting there in the open, and anyone could read it. So what did that tiny file actually do?

That release artifact and its .sig are the running example for this post.

You probably carry the tidy model most engineers do: there are two keys, and they undo each other. Encrypt with the public key, decrypt with the private key. Flip it around — encrypt with the private key, decrypt with the public key — and out comes a signature. Same operation, opposite direction.

It’s a satisfying story and it is, for anything you should actually deploy, wrong. It survives because it happens to describe textbook RSA, where the same modular exponentiation really can be pointed either way. That coincidence is specific to RSA’s math, it was never true of public-key crypto in general, and it caused real damage: where an implementation used one unpadded RSA key for both jobs, an attacker could hand the signing routine a ciphertext dressed up as a message to be signed, and get the plaintext back as the “signature.” The two operations weren’t separated, so one became an oracle for the other.

Modern signature schemes (Ed25519, ECDSA, RSA-PSS) and modern encryption schemes (RSA-OAEP, ECIES, hybrid AEAD constructions) are not the same operation reversed. They are built from different pieces, aimed at different goals. Treating them as interchangeable remains one of the classic ways to put a real vulnerability into otherwise sane code.

Why it matters now

You rely on signatures constantly and mostly invisibly — far more often than you rely on public-key encryption:

Published model weights are a clean illustration of why the distinction is not academic. “Did this 70 GB checkpoint actually come from the lab whose name is on it, unmodified?” is a signature question, and no amount of encryption answers it. The weights aren’t secret; they’re public downloads. What’s at stake is provenance, and asking the wrong primitive for it gets you a vibes-based promise instead.

The short answer

signature = private key vouches, everyone verifies · encryption = everyone encrypts, private key reads

Picture to keep: a wax seal versus a locked box. The seal doesn’t hide the letter — anyone can read it — it just proves who closed it and that nobody opened it since. The box hides the contents and says nothing about who sent it. Where the picture breaks: a wax seal can be lifted and pressed onto a different letter, while a real signature is welded to the exact bytes it signed — change one and verification fails.

They share the same key-pair shape but answer opposite questions. A signature is about authenticity and integrity: who said this, and is it intact? Encryption is about confidentiality: can anyone but the intended reader see this?

How it works

Take the naive model seriously and watch it fail, one step at a time.

Naive attempt: signing is encrypt(message, private_key). The recipient “decrypts” with your public key and compares to the message.

Why it breaks — three ways, escalating:

First, it doesn’t scale. RSA operations work on numbers smaller than the modulus — a couple hundred bytes. Your release tarball is 40 MB. You would have to chop it into thousands of blocks and sign each one, which is slow and introduces a new problem: nothing stops an attacker from reordering or dropping your validly-signed blocks.

Second, it’s algebraically malleable. Raw RSA has structure an attacker can exploit: signatures multiply. Given valid signatures for two messages, an attacker can compute a valid signature for their product without ever touching the private key. If an attacker gets to choose what you sign, this is a forgery engine.

Third, it collides with the other use of the key. That’s the signing oracle from earlier — one key, one unpadded operation, two protocols that can be pointed at each other.

Fix 1: sign a hash, not the message. Run the tarball through SHA-256, get 32 bytes, sign those. The size problem is solved: the expensive asymmetric operation is now a fixed cost regardless of whether the artifact is 40 MB or 40 GB. (You still hash every byte, so total work is still linear in file size — but hashing is cheap and streaming.) And the signature over the digest binds the whole file, not because a changed bit produces a wildly different digest, but because the hash is collision- and second-preimage-resistant: producing a different tarball that hashes to the same 32 bytes should be infeasible, so the signature can’t be transplanted onto other content. This is also why the hash underneath a sign-the-digest scheme is part of its security rather than a preprocessing detail: break the hash and you break the signature without ever touching the key.

Why that alone still breaks: you’ve fixed size, not malleability. A bare digest dropped into raw RSA still sits in an algebraic structure, and verifiers that parse the padding sloppily have been forged against — Bleichenbacher demonstrated in 2006 that some PKCS#1 v1.5 signature verifiers could be fooled for low public exponents when they didn’t check the padding strictly.

Fix 2: use a primitive built to be a signature. Not “encryption with the other key” — a scheme designed from the start so that forging requires the private key:

Verification re-hashes the message and runs the verification half of the primitive against the signature and the public key. Two properties come out: a valid signature is computationally hard to produce without the private key, and it certifies the exact bytes you hold.

Now run the same exercise on the encryption side, and notice it lands somewhere completely different.

Naive attempt: encrypt the payload directly with the recipient’s public key.

Why it breaks: same size ceiling, and the same malleability — plus a new one that has no analogue in signing. If an attacker can submit ciphertexts and learn whether they decrypted to valid padding, they can recover the plaintext without the key. That’s Bleichenbacher’s other, earlier result: the 1998 adaptive chosen-ciphertext attack against PKCS#1 v1.5 encryption.

The fix: a hybrid construction:

  1. Generate a fresh random symmetric key (for AES-GCM, say).
  2. Encrypt the payload with it using an AEAD, so tampering is detected.
  3. Wrap that symmetric key with the recipient’s public key (RSA-OAEP), or agree on it via an ECDH key-agreement step.
  4. Send both pieces.

Look at what the two fixes needed. The signature path needed a hash and padding that kills algebraic structure. The encryption path needed a fresh random key, a key-derivation step, nonces, and an AEAD tag. Almost none of that machinery is shared. They are not one primitive facing two directions; they are two constructions that happen to use the same kind of key pair.

So: that tiny file next to the tarball is a statement about authorship and exact contents — “the holder of this private key saw precisely these bytes and vouched for them” — published in the clear, verifiable by anyone, and worth nothing at all for keeping the tarball secret. That was never the job.

Show the seams

You started with signature = private key vouches; encryption = private key reads. What did the two failure ladders add? — + the fixes don't rhyme. A hash and structure-destroying padding on one side; a fresh symmetric key, an AEAD, and nonces on the other. Once you’ve seen that the repairs share almost no parts, “same operation, opposite direction” stops being tempting.

Check yourself

Before you go — a service needs to send a customer a document that is both secret and provably from them. An engineer proposes: encrypt it with the customer’s public key, and since only we could have produced this ciphertext with our key material, that also proves it came from us. What’s wrong?

Answer

Nothing about producing a ciphertext requires anything secret. The customer’s public key is public, so literally anyone — including the attacker — can encrypt a document to that customer. When the customer decrypts something that reads plausibly, they’ve learned it was intended for them and nothing whatsoever about who wrote it.

Confidentiality flows toward the private-key holder; authenticity flows from one. Here the private key in play belongs to the customer, so the construction can only deliver the first property. You need both primitives: sign the document with the service’s private key, then encrypt the whole signed bundle to the customer’s public key. Sign-then-encrypt, two key pairs, two questions answered.

And: a team signs each 4 MB chunk of a large artifact separately — each signature covering that chunk’s bytes and nothing else — reasoning that every byte is therefore still covered by a valid signature. What can an attacker do?

Answer

Rearrange, duplicate, or drop chunks — and every chunk that survives still carries a perfectly valid signature. The scheme proves each piece is authentic while proving nothing about the file, because “which chunks, in what order, how many” was never signed. Serve chunks 1, 2, 2, 5 and every individual verification passes.

Note where the fix has to go: the problem isn’t per-chunk signing as such, it’s that the assembly is unsigned. Include the index and total count inside each signed chunk, or sign a manifest listing every chunk’s digest in order, and the attack closes. Hashing the whole artifact first — the first fix above — is just the simplest way to get that commitment for free. The generalization is worth keeping: whenever you verify parts instead of the whole, ask what statement about the assembly is going unsigned. (Merkle trees are the standard answer when you genuinely need per-chunk verification, but only when the tree commits to leaf position — an unordered set of leaves reintroduces exactly this hole.)

Going deeper