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.
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.
2 · The tempting wrong model
“Encrypt with the private key” once turned a signer into a decryption service.
3 · Two repair ladders
Fix each side properly and they end up sharing almost no parts.
4 · The one fact to remember
Whose private key is in play flips between the two.
5 · Keep this card
The whole thing on one index card.
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:
- Every JWT your auth provider hands you.
- Every TLS certificate your browser checks against a CA.
- Every signed container image, every
apt/dnfpackage, every Sigstore-attested build — and every.sigfile like the one above. - Every Git tag or commit verified against a maintainer’s key.
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:
- Ed25519 does elliptic-curve scalar multiplication with a nonce derived deterministically from the private key and the message.
- ECDSA is similar but needs a per-signature secret scalar — the nonce — that must never repeat. It was originally specified as a random value, but RFC 6979 specifies deriving it deterministically from the key and the message instead, exactly as Ed25519 does — “unique” is the requirement, “random” was only ever one way to get there. Repeat one and the private key falls out outright, which is how the PS3’s signing key was extracted. (See ECDSA nonce reuse.)
- RSA-PSS wraps the hash in randomized padding before exponentiation, precisely to destroy the algebraic structure that made raw RSA malleable.
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:
- Generate a fresh random symmetric key (for AES-GCM, say).
- Encrypt the payload with it using an AEAD, so tampering is detected.
- Wrap that symmetric key with the recipient’s public key (RSA-OAEP), or agree on it via an ECDH key-agreement step.
- 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
- The ‘RSA in reverse’ intuition isn’t a lie historically — it’s a trap operationally. PKCS#1 v1.5 signing really did look like “encrypt the hash with the private key,” and it’s also the family with the long list of padding and forgery attacks. The modern advice — RSA-PSS for signing, RSA-OAEP for encryption — gives the two jobs deliberately different padding, which closes that particular oracle. It is not a licence to point both schemes at one key pair: RFC 8017 still tells you to use a separate key pair for each, because a security proof for one scheme says nothing about what an attacker learns from watching the other scheme use the same key. Different padding, different keys — the two rules travel together.
- You cannot get privacy from a signature. Your release tarball is signed and still completely public. That’s not a flaw; it’s the point. If you also need confidentiality, you need encryption as well.
- You cannot get authenticity from encryption alone. Bob’s public key is public, so anyone can encrypt a message to Bob. Receiving one tells him nothing about who sent it. This is why TLS pairs a key-exchange step with a certificate signature — neither half suffices.
- Who holds the private key flips between the two. In encryption it’s the reader’s secret; in signatures it’s the author’s secret. If you can only remember one distinguishing fact, remember that one — it’s enough to re-derive which primitive you need.
- Some schemes blur the line deliberately. Signcryption combines both at once; KEM/DEM constructions split key encapsulation from data encryption cleanly. If you’re building either from scratch, stop and reach for a vetted library.
- Honest gap: this is the conceptual shape, not a deployment survey. The share of Ed25519 vs ECDSA vs RSA-PSS differs by ecosystem — TLS certificates, SSH keys, and Git signing have each landed in different places — and no central register counts them. The point that holds regardless is that all three are signature schemes, not encryption-in-reverse.
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.)
Famous related terms
- JWT —
signed JWT = header + claims + signature, all base64url— the form you almost always meet; the spec also allows an encrypted variant (JWE) and an unsecured one with no signature at all, which is where a chunk of JWT’s bad reputation comes from. See why JWTs are controversial. - TLS certificate —
cert ≈ public key + identity + CA's signature over both— the signature is what tells you whose key you just agreed a secret with; in TLS 1.3 the certificate arrives after the key exchange, so that check happens inside the encryption it authenticates. - Sigstore / cosign —
sigstore ≈ short-lived signing keys + a public transparency log— modern supply-chain signing for container images and artifacts. - HMAC —
HMAC ≈ "signature" with a shared secret instead of a key pair— integrity plus authenticity, but only between parties who already share a key; it can’t prove authorship to a third party. - AEAD (AES-GCM, ChaCha20-Poly1305) —
AEAD = symmetric encryption + built-in integrity tag— the workhorse inside hybrid encryption; gives integrity to whoever holds the key, not to the world. - ECDSA nonce reuse —
nonce reuse = two signatures sharing a k + solvable linear algebra = your private key— the sharpest example of a signature-specific failure with no encryption analogue.
Going deeper
- RFC 8017 (PKCS #1 v2.2) — for the question “what do OAEP and PSS actually specify, and why are they different?”; the padding is written out step by step, and it explicitly warns against reusing one RSA key pair across both schemes.
- Boneh & Shoup, A Graduate Course in Applied Cryptography (free PDF) — for the question “why do the textbooks define signatures and public-key encryption as separate primitives with separate security games?”
- Bleichenbacher’s 1998 chosen-ciphertext attack on PKCS#1 v1.5 — the rabbit hole: for the question “how does a padding detail become a full plaintext recovery?”, and why its ghost still haunts TLS stacks.