Articles / Cryptography

  • cryptography
  • key-management
  • revocation
  • forward-secrecy
  • go
  • tls

Why a Leaked Signing Key Can Be Revoked but an Encryption Key Can't

"Revoke the key" sounds like one operation, but it does very different things depending on what the key was for. For a signing key it closes the door. For an encryption key it locks the door after the burglar has already copied everything in the house.

The difference is about which direction the damage points: signing harm is in the future, encryption harm is in the past.

A signing key is only dangerous from now on

A signature proves "the holder of this key said this". If someone steals the key, they can say new things in your name. That is bad, but it is a problem about what happens next.

Revocation attacks exactly that. You publish "stop trusting this key" and verifiers who check will reject anything signed afterwards. The thief holds a key that nobody accepts, and the damage is capped at whatever they signed between the theft and the revocation reaching people.

The real systems for this are all variations on one theme:

  • X.509 certificates have CRLs and OCSP.
  • OpenPGP has revocation certificates, which are themselves signatures made by the key (so make one when you create the key and store it somewhere safe).
  • Code signing relies on trusted timestamps, so a signature made before the compromise stays valid after the certificate is revoked.

That last one is the tidy part. The signature carries evidence of when it was made, so verifiers can separate "signed before the leak" from "signed after".

Where signing revocation is still messy

It is not magic. Revocation only works if verifiers actually check, and plenty do not, or fail open when the check cannot be reached. Browsers have a long history of treating revocation as best effort, which is part of why certificate lifetimes keep getting shorter.

There is also a date problem. If you do not know when the key was stolen, you do not know which old signatures to doubt. An attacker can also claim a signature is older than it is, unless an independent timestamp or a transparency log pins it down. So signing revocation is imperfect, but it has something to grab hold of.

An encryption key is dangerous backwards

Now take an encryption key, say the private half of a key pair that people encrypt mail to. Anyone who recorded your old ciphertext, such as a mail relay, a backup tape or a tap on the wire, can decrypt all of it once they have the key.

Revocation says "do not encrypt new things to this key". Fine, but the attacker was never waiting for new things. The valuable material is already sitting in their archive, and no notice you publish can reach into that archive.

Worse, you cannot even destroy your own copy sensibly. If you delete the revoked private key, you lose access to your own old messages. So people keep it, and the key stays a target long after it has been "revoked".

Quick detour: this is why TLS dropped static RSA

Hang on, this is not just theory, it shaped TLS. In older TLS 1.2 setups using static RSA key exchange, the server's certificate key also decrypted the session secret. Record the traffic today, steal the key in two years, read everything.

Revoking that certificate afterwards does nothing for the recordings. The fix was ephemeral Diffie-Hellman (ECDHE): the certificate key only signs the handshake, and the session secret comes from throwaway keys that are deleted. TLS 1.3 removed static RSA key exchange entirely.

Notice what happened there. The long-lived key was demoted to a signing role, where revocation works, and the encryption job went to keys that cease to exist.

The pattern in code

Here is that split in Go. The long-term Ed25519 key signs an ephemeral X25519 public key; the shared secret comes only from the ephemeral keys. This is simplified: a real protocol would sign a transcript including both public keys and a context string, and would run the secret through HKDF rather than using it raw.

package main

import (
	"crypto/ecdh"
	"crypto/ed25519"
	"crypto/rand"
	"fmt"
	"log"
)

func main() {
	// Long-term identity key: signs only, so revocation is meaningful.
	idPub, idPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}

	// Fresh ephemeral keys for this session on each side.
	mine, err := ecdh.X25519().GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}
	theirs, err := ecdh.X25519().GenerateKey(rand.Reader)
	if err != nil {
		log.Fatal(err)
	}

	// Authenticate the ephemeral key with the long-term key.
	sig := ed25519.Sign(idPriv, mine.PublicKey().Bytes())
	if !ed25519.Verify(idPub, mine.PublicKey().Bytes(), sig) {
		log.Fatal("bad signature")
	}

	secret, err := mine.ECDH(theirs.PublicKey())
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("shared secret: %d bytes\n", len(secret))
	// Once mine and theirs are discarded, stealing idPriv cannot
	// recompute secret. It can only forge future signatures.
}

If idPriv leaks, the damage is "someone can impersonate me from now on", and revocation answers that. Old sessions stay safe because the keys that protected them no longer exist anywhere.

What you can do about storage encryption

Forward secrecy works for conversations because both ends can throw the key away. Stored data is awkward: you want to read it later, so some key must survive. The practical options are limited:

  1. Keep the long-term decryption key as small a target as possible (hardware token, offline, split between people).
  2. Encrypt each item under its own data key and wrap those with a key you can rotate.
  3. After a leak, re-encrypt what you still hold under a new key.
  4. Assume anything an attacker already copied is gone for good, and treat it as a breach accordingly.

Step three only helps with copies you control. The attacker's copy of the old ciphertext, plus the old key, still opens.

So the useful design question is not "can I revoke this key?" but "what is this key allowed to do, and does that job survive being stolen?" Give long-lived keys jobs where theft only hurts going forward.