Signature lab
A valid signature proves one narrow thing: whoever holds a particular private key signed these exact bytes. We'll take that apart in six exhibits, from the fingerprint that gets signed to Ed25519 to signatures built from a hash function alone.
Everything runs in this tab. SHA-256 and Ed25519 come from Web Crypto, and the hash-based signatures are built here and checked against RFC 8032 and RFC 8554. Keys are random per visit and never leave the tab, and only your progress and the demo tree's counter are saved.
Lab exploredThat's the whole signature lab, from one hash up to a tree of keys. The Vault lab seals and proves data with the same primitives.
What a signature signs
Signature schemes sign a fingerprint of the file instead of the file itself. SHA-256 turns any number of bytes into 32. Flip one input bit and about half of the 256 output bits change, with no pattern to which ones.
Hash your own text, flip one bit and count what moves. Then run a thousand seeded trials against the curve they should follow, and try to change the message without changing its fingerprint. good luck lol.
not hashed yet
Pick a bit of the message and flip it.
Each trial hashes a seeded random 32-byte input, flips one random bit, hashes again and counts the output bits that differ.
Pin a hash, then edit a copy of the message. If any edit left the hash alone, a signature on the original would sign the edit too.
nothing pinned yet
The maths
P(k of 256 bits change) = C(256, k) / 2256, mean = 256 × ½ = 128, standard deviation = √(256 × ½ × ½) = 8
standard error of a mean over T trials = 8 / √T, which is 0.25 for T = 1,000
Why the binomial. For an ideal hash, any change to the input gives an output that looks freshly random. Each of the 256 output bits then differs with probability ½, independently of the others, so the count follows Binomial(256, ½) with mean np and variance np(1 − p). Nobody has proved that SHA-256 behaves this way. The histogram only checks that it does on these inputs.
Three properties. Preimage resistance: given a hash y, finding any x with H(x) = y should take about 2256 tries. Second-preimage resistance: given x, finding a different x′ with the same hash should also take about 2256. Collision resistance: finding any two inputs with one hash takes about 2128, the birthday bound.
Why a signature signs the hash. The signing maths works on a fixed-size value, and the hash lets a 64-byte signature cover a file of any length. The price is that the signature is exactly as strong as the hash: two messages with one fingerprint share every signature. That's why signatures lean on collision resistance, and why SHA-1 was retired from signing once its collisions became practical.
In practiceLockfiles, container digests and release manifests pin an artifact by its SHA-256 digest, and the signature covers the small manifest instead of the whole download.
DefenceHash what you received and compare the full digest with the signed one, never a prefix. Refuse MD5 and SHA-1 for anything signed: SHA-1 collisions have been public since 2017.
Sign, verify, and watch one byte break it
Ed25519 is what most new systems pick: a 32-byte public key, a 64-byte signature and a verification equation anyone can check. Here Web Crypto makes a key pair whose private half can't be exported, even by this page, then signs your message and verifies it.
Change a single byte of the message, the signature or the public key and verification fails. Then check the browser against the test vectors published in RFC 8032, and sign one message twice.
Not in this browserThis browser's Web Crypto has no Ed25519, so this exhibit can't run here. The rest of the page still works: Exhibit III signs with ECDSA P-256 instead, and Exhibit IV compares against the sizes RFC 8032 publishes.
Generate a key pair to begin.
32 bytes, not made yetfirst 32 bytes of the signaturelast 32 bytes of the signaturethe bytes you signSizes appear once you sign.
Sign first, then change a byte and press Verify.
The maths
A = [s]B, r = SHA-512(prefix ‖ M) mod L, R = [r]B, k = SHA-512(R ‖ A ‖ M) mod L, S = (r + k s) mod L
verify: [S]B = R + [k]A, with 0 ≤ S < L = 2252 + 27742317777372353535851937790883648493
The terms. B is the curve's fixed base point and L is the number of distinct multiples it has. [n]P means P added to itself n times. The 32-byte private key gets hashed with SHA-512. The first half, with a few bits fixed, is the secret scalar s, and the second half is the prefix that seeds every nonce. The public key A and the first half of the signature, R, are curve points of 32 bytes each, and the second half, S, is a number below L.
Why it holds. If the signer follows the algorithm, [S]B = [r + k s]B = [r]B + [k]([s]B) = R + [k]A, and reducing mod L changes nothing because [L]B is the neutral point. The verifier needs only public values. Because k hashes R, A and M together, changing any one of them changes k completely and the equation fails.
Deterministic nonces. RFC 8032 derives r from the prefix and the message, so one key signing one message always produces the same signature, with no random number generator involved. Schemes that need a fresh secret nonce per signature, such as ECDSA, can leak the private key if a nonce ever repeats.
In practiceEd25519 signs OpenSSH keys, signify and minisign releases, SSH-signed Git commits and DNSSEC zones, and TLS 1.3 lists it among its signature schemes.
Watch outA valid signature means the holder of this key signed these bytes, and nothing more. Pin the public key you expect, verify with a library (this page's hand check isn't constant time), and keep private keys non-extractable or in hardware.
Exact bytes, one purpose, a known key
A signature binds exact bytes and knows nothing about meaning. Sign a release manifest, then hand the verifier the same JSON with its keys reordered or spaced out: the meaning is the same, the bytes differ, and verification fails. One agreed canonical form fixes that.
The bytes also have to fix the purpose, with a context string. And the key has to be one you trust, because a valid signature says nothing about who owns the key that made it.
Checking which signature scheme this browser offers.
Nothing signed yet.
The verifier pinned one thing in advance: the SHA-256 fingerprint of the publisher's root key. The root never signs releases. It signs a short statement naming the release key and what it may sign, under its own context, “dbd key v1”.
made when you first signmade when you first signNo statement yet: the root has not certified anything.
The maths
Verify(A, m, σ) ∈ {valid, invalid}, m = len(c) ‖ c ‖ C(manifest), fingerprint = SHA-256(C(manifest))
Bytes in, verdict out. A verifier is a function of three inputs: the public key, the exact bytes and the signature. Two serialisations of one manifest are two different messages, and a signature covers at most one of them. Canonical form. C parses the JSON and writes it back one fixed way. Keys are sorted by their UTF-16 code units, there's no whitespace between tokens, and strings and numbers use ECMAScript's own serialisation (the rules RFC 8785 fixes). Texts that parse to the same value give the same bytes, and a changed value gives different ones.
Context. The signed bytes start with one length byte and the context string, the prefix shape RFC 8032 gives Ed25519ctx. The verifier rebuilds m from the context it expects, so a signature made for “dbd release v1” fails under any other context. The length byte stops one context from being read as the start of another.
Trust. The chain check is two verifications and three comparisons. The root key must match the pinned fingerprint, and the root's signature on the statement must verify under “dbd key v1”. The statement must name this release key and purpose, and the manifest's signature must verify under “dbd release v1”. All five must pass.
In practiceJWS signs the exact bytes it sends, TUF signs canonical JSON, and DSSE (used by in-toto and Sigstore) binds the payload type. SSH signatures carry a namespace such as “git” or “file”.
DefenceShip the exact bytes you signed, or define one canonical form and refuse duplicate keys. Bind a purpose into every signature, pin the root key out of band, and check the whole chain on every verification.
A signature made of hashes
Lamport's scheme of 1979 needs nothing but a hash function. The private key is 256 pairs of random 32-byte values, and the public key is their 512 hashes. To sign, hash the message. For each of its 256 bits, reveal one value of the matching pair: the first for a 0, the second for a 1. To verify, hash each revealed value and compare it with the public key.
Each key may sign one message. Signing reveals half of the private key, so this page marks the key used before the signature exists and refuses a second. The key is random per visit.
- Private key
- not made yet
- Public key
- not made yet
Generate a key to begin.
| Object | Lamport | Ed25519 | Ratio |
|---|
The maths
private: xi,b, public: yi,b = H(xi,b), i = 0 … 255, b ∈ {0, 1}
sign: d = H(M), σi = xi,di; verify: H(σi) = yi,di for all 256 values of i
sizes: 2 × 256 × 32 = 16,384 bytes each for the private and public keys, 256 × 32 = 8,192 bytes per signature
Why it's secure. A signature on another message needs, for every bit where the two hashes differ, a value the signer never revealed. Only its hash is public, so producing it means finding a preimage of SHA-256. The scheme rests on the hash alone, which is why hash-based signatures are the conservative choice when other assumptions are in doubt.
Why only once. Each signature publishes one value from every pair. A second signature would publish both values of about 128 pairs, and the argument above assumes each pair has shown at most one.
In practiceNobody ships raw Lamport signatures: 8 KB per signature and one message per key cost too much. The idea lives on in every hash-based scheme, from the Winternitz chains of the next exhibit to the trees of the one after it.
DefenceTreat a one-time key as spent the moment it's asked to sign, record that before the signature leaves, and never let two copies of the key exist. Where you can't guarantee that bookkeeping, use a scheme that does it for you: a tree of keys, or the stateless SLH-DSA.
Trade signature size for hashing
Winternitz shrinks Lamport by signing w bits at a time. Each w-bit digit of the message hash gets a private value at the foot of a chain of 2w − 1 hashes, whose top is public. To sign a digit a, reveal the value a steps up its chain. The verifier walks the rest of the way and compares.
This is LM-OTS as RFC 8554 section 4 specifies it, checked against the values the RFC publishes. Pick w and watch size and hashing move in opposite directions. Each one-time key here is random and signs once.
- Parameter set
- Chains p
- Signature
- Signing, expected
| w | Signature bytes | Sign hashes | Verify hashes | Key hashes | Time |
|---|
| Set | p | ls | sig_len | Agrees |
|---|
The maths
n = 32, u = ⌈8n / w⌉, v = ⌈(⌊lg((2w − 1) u)⌋ + 1) / w⌉, ls = 16 − v w, p = u + v
Q = H(I ‖ q ‖ D_MESG ‖ C ‖ M), Cksm(Q) = ∑i < u (2w − 1 − ai), shifted left by ls, ai = coef(Q ‖ Cksm(Q), i, w)
yi = Hai(xi), the verifier's zi = H2w − 1 − ai(yi), K = H(I ‖ q ‖ D_PBLC ‖ z0 ‖ … ‖ zp − 1)
signature = 4 + n + n p bytes, signing walks about p(2w − 1) / 2 chain hashes, signing and verifying together walk exactly p(2w − 1)
Why the checksum. Anyone can walk a chain forward, so without it a signature on digits a would also sign any message with digits at least as large. The checksum records how much climbing is left: message digits and checksum always add up to u(2w − 1). Raising any message digit lowers the checksum, which would mean inverting SHA-256, so one message's signature is useless for another.
Why every hash is labelled. Each chain step hashes I, q, the chain index i and the step j with the value, so no two hashes share an input. D_MESG (0x8181) and D_PBLC (0x8080) set the message and public-key hashes apart in the same way.
In practiceLM-OTS is the one-time layer inside LMS and HSS (RFC 8554), and XMSS (RFC 8391) uses a close relative, WOTS+. NIST SP 800-208 approves both families, for uses such as firmware signing.
DefenceUse the registered parameter sets exactly, keep the randomizer C fresh for every signature, and never let one LM-OTS key sign twice.
Many one-time keys, one public key
An LMS tree (RFC 8554 section 5) hangs 2h one-time keys under one public key, the root. Each leaf is the hash of an LM-OTS public key and each node the hash of its two children. A signature carries the leaf number q, the one-time signature and the h sibling hashes up to the root, so the verifier can rebuild it.
This demo tree has height 5 (32 leaves, w = 4) and grows from a seed printed below. Every visitor builds the same tree, and only the counter is stored. A real seed is random and never leaves the signer.
not built yetBuild the tree first.
None yet.
| Scheme | State | Signature |
|---|
The maths
leaf: T[r] = H(I ‖ r ‖ D_LEAF ‖ Kr − 2h) for r ≥ 2h, node: T[r] = H(I ‖ r ‖ D_INTR ‖ T[2r] ‖ T[2r + 1])
signatures per key = 2h, size = (4 + n(p + 1)) + 4 + 4 + h × 32 bytes, verification = the LM-OTS candidate, 1 leaf hash and h node hashes
Why the path is enough. The root depends on a leaf only through the nodes on its path, and the verifier recomputes those from the siblings in the signature. Every hash includes its node number r, so a node can't move, and a different leaf would need a SHA-256 collision to reach the same root.
Why state is the risk. Two signatures from one leaf are two signatures from one LM-OTS key. So q must advance with every signature, and RFC 8554 section 5.4.1 requires the new value to be stored before the signature is released. A restored backup, a virtual machine snapshot or two servers sharing a key each hold an old counter and would reuse leaves.
In context. LMS and XMSS (RFC 8391) are the standardised stateful schemes (NIST SP 800-208). SLH-DSA (FIPS 205) is hash-based but stateless, so it needs no counter, at the price of signatures of about 7.9 KB to 17 KB. ML-DSA (FIPS 204) rests on lattices, with signatures of about 2.4 KB. Both were chosen for post-quantum use.
In practiceStateful hash-based signatures suit places where signing is rare and happens on one guarded machine, such as firmware and boot images. NIST SP 800-208 approves LMS and XMSS and requires their keys and state to be generated and used inside hardware modules that never export them.
DefenceCommit the counter before a signature leaves the signer, and never back up, snapshot or clone a stateful key in use. Raise an alarm long before 2h signatures, and use SLH-DSA where you can't guarantee state.