Crypto OS
Technical Crypto OSStage 1 · Blockchain Developer Mental Model

T2 · Cryptography Fundamentals

Why can a signature prove "it was me"?

Capabilities practiced
BuilderProtocol Literacy
Hands on
Generate a key pair locally, sign a message, verify it with the public key, then change one byte and see verification fail.
AI Lab
Have AI write a Merkle proof verification function. Construct a forged proof yourself and confirm it is rejected.

A Real-World Problem

Logging in to your DApp needs no password.

A user clicks "Connect Wallet," sees a dialog, clicks "Sign," and is logged in. Your backend receives two things: an address and a 132-character hexadecimal string. From those alone, it accepts that the request came from the owner of that address.

A traditional backend would need registration, a stored password hash, a comparison, and a session. None appears here. The server has never met this user and stores no secret about them.

More unsettling, that hexadecimal string is public. It travels in an HTTP request, might land in access logs, could be seen by an intermediary, and might be printed to your console by accident.

Once someone sees it, why can they not impersonate you?

How can characters anyone may copy prove "it was me"?

Thought Experiment

Reduce the problem: in a strange city, you must prove to someone you have never met that "I wrote this note."

Three constraints: you share no secret; you cannot reveal your secret; anyone may see and copy the note.

Version one: agree on a secret code and write it at the end of the note.

The problem is immediate. Once the reader sees the code, they can write an identical note.**The verifier becomes a forger.**This is a structural dead end, not an implementation detail.

Version two: write a one-way scrambled value calculated from the code instead of the code itself.

The reader cannot reverse the value to recover the code. But they do not need to: they can copy the value onto another note. The proof is not bound to the content, so one copy works forever.

Version three: combine the code with the note's contents, then scramble them together.

Now the proof is bound to the content. A different note will not match. But to recalculate it, the verifier must know the code—and we are back to version one.

Either the reader can verify it or they cannot forge it. With this design, you cannot have both.

An ordinary one-way function has reached its limit. To escape, you need something stranger:

A pair of numbers: one generates the proof, the other checks it, and the checking number cannot reveal the generating number.

Make the checking number public, so anyone can verify. Never give anyone the generating number, so nobody else can forge. The dead end is resolved.

Now return to the opening question: the 132-character string is not your secret. It is a one-time result calculated using your secret. Seeing it is useless for other messages, because it is valid only for the message you signed.

Your Decision

The backend must confirm that "this request came from the owner of address A." Which approach do you take?

What Happens

The four approaches expose the same dimensions:

ApproachMust the verifier know a secret?Bound to a specific message?Replayable?Consequence of leakage
Send the secretYesNoFreelyAll assets lost
Send a digest of the secretNoNoFreelyIdentity can be impersonated
Send a digest of secret plus messageYesYesNoEither party can forge
Send a publicly verifiable proofNoYesDepends on message contentsAffects only that message

The last row has an ambiguity worth examining: replay protection depends on what you put in the message, not on the cryptography.

A signature remains valid forever. If you sign only the word "login," it may be reused at any time, on any site, and on any chain. To make it single-use and context-specific, put a nonce, domain, chain ID, and expiration time inside the signed content.

Cryptography will not design that content for you. You must.

Build a Model

Three primitives and one composite structure.

**Primitive one: a one-way hash.**It maps input of any length to fixed-length output. Identical input always yields identical output; one changed byte radically changes the result; reversing the result is computationally infeasible.

It serves three purposes: give arbitrary data a stable short name; detect changes; and compress "a lot of data" into "one value."

**Primitive two: a key pair.**A private key is a random number; a public key is derived from it in one direction. Forward calculation is fast, reversal infeasible. An address is in turn derived from a hash of the public key, forming a one-way chain:

  1. Random number = private key
  2. Elliptic-curve operation → public key
  3. Hash and truncate → address
Each step goes only forward. If you lose the private key, nobody can derive it from the address or recover it for you.

**Primitive three: signing and verification.**Signing takes a private key and a message digest and returns a value—on Ethereum, the r, s, and v parts total 65 bytes. Verification takes the message and signature, recovers the signer's public key, and derives the address.

Note the direction: verification is not "decrypting with a public key." It is recovering the public key from the signature. You do not need to know in advance who signed; the verification process tells you. This is why an Ethereum transaction does not need to carry the sender address—it can be calculated.

A complete identity proof looks like this:

  1. Server provides a message with a nonce and domain
  2. Wallet signs its digest
  3. Client returns the message and signature
  4. Server recovers the address
  5. Server compares the address and invalidates the nonce
The final step is the easiest to miss. Without invalidating the nonce, the signature can be replayed indefinitely.

Composite structure: compress many items into one value while retaining proof that any individual item belongs.

Suppose a list contains 300,000 addresses, and an on-chain contract must decide whether one address is on it. Writing the full list into the contract would cost far too much.

Hash each of the 300,000 entries to get leaves. Combine and hash neighboring leaves to get 150,000 values. Repeat layer by layer until only one value remains. Store that one value on-chain.

To claim, a user submits their entry and the sibling value at each level on the path from their leaf to the top—only about 19 values for 300,000 entries. The contract recomputes the path. If its top value equals the stored value, it accepts.

In one sentence, the structure proves that one item is in a collection without revealing the entire collection.

It appears everywhere on-chain: block headers commit to transaction lists, airdrops commit to recipient lists, and bridges commit to batches of messages. The shape is the same.

What It Is Called

HashHash / digest

A one-way function mapping input of arbitrary length to fixed-length output. Identical inputs produce identical outputs; changing one bit produces a very different output; reversal is infeasible.

Ethereum uses Keccak-256; Bitcoin uses SHA-256. They are not the same function. Mixing them up is a common beginner error and produces completely different addresses.

Private KeyPrivate key

A random number. It is not "generated" so much as "drawn"—the quality of randomness determines security.

It controls both identity and assets. Traditional systems have no exact counterpart: a reset password may recover an account there; losing this key means permanent loss here.

Public KeyPublic key

A value derived one-way from the private key that can be shared openly. An address is a further hash and truncation of that public key.

Exposing a public key is safe, and addresses are meant to be public. The danger is exposing the private key or anything that can reveal it—including a nonce used during signing.

ECDSAElliptic Curve Digital Signature Algorithm

The signature algorithm used by Ethereum and Bitcoin, on the secp256k1 curve. Solana uses a different scheme, Ed25519, with different key lengths and signature formats.

ECDSA needs a one-time random nonce for each signature. Reuse that nonce even once, and the private key can be derived directly. The Real-World Case section includes actual failures.

SignatureSignature

A proof tied to a particular message. On Ethereum, it is 65 bytes: r and s are 32 bytes each, and v is a one-byte recovery identifier.

A signature proves only two things: the message has not changed, and the signer held the private key when signing. It cannot prove the signer understood the message.

Merkle TreeMerkle tree

A structure that repeatedly hashes pairs of items into one root value. Given one item and the path from its leaf to the root, anyone can independently verify its membership without holding the entire collection.

Path length grows logarithmically with the number of items: about twenty hashes suffice for a million entries. That logarithmic scale is its key advantage.

Hands-on

Hands onGenerate a key pair locally, sign, verify, then change one byteopenssl (included with macOS and Linux)Free

Everything happens on your computer: no chain, testnet, or real asset is involved. You will send no transaction. Use the keys only for this experiment, then delete them.

Generate a secp256k1 private key and derive its public key.

openssl ecparam -name secp256k1 -genkey -noout -out priv.pem
openssl ec -in priv.pem -pubout -out pub.pem
cat priv.pem pub.pem

The relationship is one-way: priv.pem can regenerate pub.pem at any time, but not the other way around. Delete pub.pem, regenerate it, and confirm it is identical; there is no extra information in the public key.

Prepare and sign a message.

printf 'transfer 100 to alice' > msg.txt
openssl dgst -sha256 -sign priv.pem -out sig.bin msg.txt
xxd sig.bin | head

Look at the length of sig.bin. It does not depend on message length: the signature operates on a digest of the message, not its full bytes directly.

Verify with the public key.

openssl dgst -sha256 -verify pub.pem -signature sig.bin msg.txt

You should see Verified OK. Notice that verification uses only pub.pem; the private key is absent. That resolves the thought experiment's dead end.

Change one byte and verify again.

printf 'transfer 100 to blice' > tampered.txt
openssl dgst -sha256 -verify pub.pem -signature sig.bin tampered.txt

Verification fails. Only one letter in the name changed, but the digest is entirely different, so the signature no longer matches.

Try leaving the message alone and changing one byte in the signature file: that fails too. Signature verification has no "close enough" result—only pass or fail.

Inspect a real on-chain signature.

Open any wallet, find "Sign Message," sign a short message of your own, and copy the result.

Count its characters: 0x plus 130 hexadecimal characters is 65 bytes. The first 64 characters are r, the next 64 are s, and the final two are v.

Compare it with the OpenSSL output. The curve is the same, but Ethereum uses Keccak-256 rather than SHA-256, a fixed 65-byte format rather than DER encoding, and a fixed prefix before hashing a personal message.**The mathematics is the same; encoding and preprocessing differ.**This distinction often causes cross-language verification bugs.

Read the text you are signing.

Return to the wallet and find a real DApp login signature. Read the whole dialog. Does it contain a domain, nonce, expiration time, and chain ID?

Each missing field shows a dimension along which the signature can be replayed.

AI Lab

AI LabAsk AI to write a Merkle proof verifier, then try to fool it with a forged proofLevel 2 · AI Copilot

Work in two steps. The second is the important one.

Step one:
In my chosen language, write a function to verify a Merkle proof.
Inputs: leaf, proof array, root. Output: true or false.
State your assumptions about the hash function, child order, and distinction between leaves and internal nodes.

Step two:
Now act as an attacker. Given the implementation you just wrote,
construct inputs that make it return true for an item that is not actually in the tree.
If you think this is impossible, identify the exact line that prevents it.

The second step matters because Merkle verification has a classic flaw: a second-preimage attack. If leaves and internal nodes are hashed the same way, an internal node can be submitted as a leaf with a shorter proof and still reach the correct root. Prevent it by adding different prefixes to leaves and internal nodes or enforcing different lengths.

The model may write a vulnerable first version and then claim the attack is impossible. Paste the flaw described above and ask it to rewrite the function.

Your exercise is not merely writing this function; it is learning what to ask.

If it supplies a runnable example and invents an airdrop contract address or Merkle root, check it on-chain. It may not exist.

Verify these points yourself after using AI

  • It uses the hash function of the target chain and does not mix up SHA-256 and Keccak-256
  • It specifies the order for combining child nodes instead of leaving it implicit
  • It distinguishes leaves from internal nodes so an internal node cannot be passed off as a leaf
  • The library and function signature it cites exist in official documentation
  • It converts hexadecimal strings and byte arrays correctly, without counting the two 0x characters as bytes
  • It does not confuse a valid proof with valid data; whether the root itself is trustworthy is a separate question

Real-World Case

Nonce reuse revealed private keys2010 and 2013

ECDSA requires a one-time nonce for every signature. If the same private key signs two different messages with the same nonce, anyone holding both signatures can derive the private key with basic algebra.

In 2010, a game console's code-signing system used a constant nonce. Its private key was publicly calculated, invalidating the signing system. In 2013, a mobile system's random-number generator had a flaw that caused some wallets to reuse nonces, and the coins at those addresses were gradually drained.

The lesson: **the cryptographic algorithm was not broken; its implementation was.**The nonce generation in signature logic is one of the last things you should implement yourself.

Blind-signing phishing

A user clicks "Sign" on a fake site. The dialog shows incomprehensible hexadecimal text. The user signs.

The message is actually an order authorization to sell an NFT from the wallet to the attacker for zero. The signature is valid, the chain shows nothing abnormal, and the wallet has no reason to block it: the user agreed, without understanding what they agreed to.

Such attacks caused extensive losses in 2022–2023. Structured signing helps: expose the signed fields so the wallet can render them in plain language, and bind the domain and chain ID. T13 covers this layer in detail.

Reusing a signature on another chain

Early transaction formats did not include a chain ID. A transaction broadcast on a testnet could therefore also be valid when broadcast unchanged on mainnet.

Later standards put the chain ID into signed transaction data to prevent that replay. But applications must still handle off-chain signed messages themselves. Many DApp login signatures still lack a chain ID or expiration, so a signature captured a year ago may still log in today.

An airdrop for 300,000 addresses with only 32 bytes on-chain

Writing the full recipient list into a contract costs too much. Requiring a project signature for each claim would force its server to stay online.

With a Merkle root, the chain stores one 32-byte value, while the list can live anywhere public. Users bring their own proof path. Claims still work if the project server goes down.

This is one of the most common application-level uses of Merkle trees, and one you may implement yourself.

Change One Variable

What ifSomeone finds a collision in the hash function

Two different inputs yield the same digest. A Merkle proof could then point to two different items, or a contract bytecode check could accept replacement code.

The entire approach of "a digest stands for data" would fail across applications, not just in one product. This is why chains are extremely conservative when choosing hash functions and may favor a slower, established one.

What ifYou choose a memorable phrase instead of a random private key

That is a brain wallet. An attacker need not break the algorithm; they can try common poems, lyrics, and password dictionaries one by one.

Such addresses can be drained within seconds. Security lies in the random number's entropy, not just the algorithm.

What ifThe login signature has no nonce or expiration

The signature becomes a permanent pass. Server logs, error-reporting systems, and captured network traffic can all leak the credential.

More subtly, even if the user later revokes all approvals for the address, the login signature still works. It never went on-chain, so nothing on-chain can revoke it.

What ifSwitch to Solana

The curve and algorithm differ. An address is the public key itself rather than a digest of it, so the public key can be read directly from the address.

One consequence: a Solana transaction must explicitly list all signers, whereas Ethereum recovers the sender from a signature. T6 explains the account-model difference behind this.

Takeaway Questions

2
Why do we need blockchains?

Signature verification itself does not require a blockchain; SSH and HTTPS have used it for decades. What requires a chain here is that "the verification rules are the same for everyone, and nobody can alter them." Public-key recovery belongs to the protocol, not one company's server.

9
Who bears the risk?

If a private key is lost, its holder bears all the risk with no recourse. Traditional finance allocates responsibility very differently. That is why every key-related design later in this course needs special care: multisignature, social recovery, and layered permissions all redistribute that risk.

16
What permissions does an agent have?

If an AI Agent receives a private key, its permission is "everything at this address," with no intermediate level. Giving it narrower authority requires a signed authorization limited by amount, recipient, and time. Decide what authority to grant before deciding how to sign. Reverse that order and a prompt may be enough to drain the Agent's funds.

Chapter Check

Key idea

You never send your private key; you only send a proof that anyone can verify.

Finished this chapter’s lab? Mark it completeView all progress

On this page