verifying-crypto-with-lean/chapters/ch01-why-verify.tex

346 lines
17 KiB
TeX
Raw Normal View History

\chapter{Why Verify? The Bug That Testing Cannot Find}
\label{ch:why}
\section{A story about one carry bit}
In 2014, researchers examining widely deployed elliptic-curve code found
arithmetic bugs of a very particular species: the code was correct on
\emph{almost every} input. Not most inputs --- almost all of them, in a
precise sense. One famous example, a carry-propagation flaw in an
implementation of curve25519 arithmetic, produced a wrong answer with
probability on the order of $2^{-64}$ per random input.
Pause on that number. If you tested this function a billion times per second,
around the clock, you should expect to wait \emph{centuries} before a random
test happens to catch the bug. Every unit test passes. Every integration test
passes. Fuzzers shrug. The code ships.
\begin{pitfall}
``It passed all the tests'' means: it worked on the inputs we tried. For a
32-bit function there are four billion inputs and exhaustive testing is
feasible. A field element in Ed25519 is $255$ bits. The number of input
\emph{pairs} to a two-argument field operation is about $10^{153}$ --- more
than the square of the number of atoms in the observable universe. Testing
samples a raindrop from that ocean.
\end{pitfall}
\begin{worked}{feel what $2^{-64}$ means, with the real numbers}
Claims about astronomical improbability deserve to be checked by hand, so
check this one. A failure probability of $2^{-64}$ per random input means
you expect one hit per $2^{64}$ trials. First, get $2^{64}$ into scientific
notation the way you always can: $\log_{10} 2 \approx 0.30103$, so
\[
\log_{10} 2^{64} = 64 \times 0.30103 \approx 19.27
\qquad\Longrightarrow\qquad
2^{64} \approx 1.8 \times 10^{19}.
\]
At $10^9$ tests per second, the expected waiting time is
\[
\frac{1.8 \times 10^{19}}{10^{9}} = 1.8 \times 10^{10} \text{ seconds}.
\]
A year is $\approx 3.15 \times 10^{7}$ seconds (a number worth memorizing:
``$\pi \times 10^7$ seconds per year'' is accidentally almost exact), so
\[
\frac{1.8 \times 10^{10}}{3.15 \times 10^{7}} \approx 580 \text{ years}.
\]
So: a test farm hammering this function a \emph{billion} times per second,
started when Copernicus published, would be expected to see the bug for the
first time about now. And this is the \emph{optimistic} case where failing
inputs are hit by uniform sampling --- for carry bugs they are typically
\emph{correlated}, clustered in corners uniform sampling underweights.
Now the input space itself. A single \lean{FieldElement} is 255 bits; a
pair is 510 bits, and
\[
\log_{10} 2^{510} = 510 \times 0.30103 \approx 153.5
\qquad\Longrightarrow\qquad
2^{510} \approx 10^{153}.
\]
For comparison, the number of atoms in the observable universe is around
$10^{80}$. Testing all pairs is not ``hard''; it is not a thing that
happens in this universe.
\end{worked}
Why does cryptographic code have bugs of exactly this shape? Because of how it
must be written. To be fast and resistant to timing attacks, real
implementations represent a 255-bit number in several machine-word
\emph{limbs} (we will spend happy hours with limbs in
Chapter~\ref{ch:denotation}) and postpone expensive carry propagation as long
as possible. The rare inputs where a deferred carry finally overflows are
precisely the inputs no test generator stumbles on. The bug lives in the gap
between ``the arithmetic we meant'' and ``the arithmetic we wrote,'' and that
gap is only visible on a set of inputs of measure nearly zero.
\begin{worked}{where exactly the danger zone sits --- the headroom budget}
You can locate the habitat of every delayed-carry bug with one line of
arithmetic, using the real Ed25519 parameters. The implementation stores a
field element as five limbs, each meant to carry $51$ bits of payload, in
$64$-bit machine words. The slack between payload and word is the
\emph{headroom}:
\[
64 - 51 = 13 \text{ bits of headroom per limb.}
\]
Adding two elements limb-wise adds their limbs, so a freshly reduced limb
(value $< 2^{51}$) can absorb additions --- but each addition can roughly
double the limb, i.e.\ spend up to one bit of headroom. How many lazy
additions before a limb can reach the $64$-bit cliff? We need
\[
k \cdot (2^{51}-1) \;<\; 2^{64}
\qquad\Longleftrightarrow\qquad
k \;\le\; \frac{2^{64}}{2^{51}} = 2^{13} = 8192 .
\]
So the code may skip carry propagation for thousands of additions --- a huge
performance win --- \emph{provided someone, somewhere, is counting}. The
2014-species bug is precisely a miscount: a code path where the running
total of spent headroom exceeds the budget on inputs shaped just so. Notice
what kind of fact the budget is: a \emph{quantified arithmetic invariant}
(``for all reachable values, limb $< 2^{51+j}$ after $j$ additions'') ---
exactly the kind of statement a test cannot establish and a proof assistant
eats for breakfast. When Chapter~\ref{ch:field} makes ``bounds clauses''
feel bureaucratic, remember this box: the bounds clause \emph{is} the
headroom count, and the headroom count is where the bodies were buried.
\end{worked}
And in cryptography, ``rare wrong answer'' does not mean ``rare small
glitch.'' Wrong field arithmetic can leak private keys: several published
attacks turn a single faulty group operation into full key recovery. The
stakes are not a corrupted pixel; they are every signature your machine has
ever made.
\section{There is another way}
What if, instead of sampling inputs, we could make a statement about
\emph{all} of them --- and have a machine check that statement with the same
rigor a compiler checks syntax?
\begin{bigidea}
A \textbf{formal proof of correctness} is a mathematical argument, written in
a language precise enough for a computer to verify, that a program satisfies
its specification on \emph{every} input. Not sampled. Not probabilistic.
Every input, forever, or the proof does not check.
\end{bigidea}
The tool that checks such arguments is called a \emph{proof assistant}. This
book uses \textbf{Lean~4}, a modern proof assistant that is also a
full-fledged programming language. Others you may have heard of: Rocq
(formerly Coq), Isabelle/HOL, Agda. The ideas transfer; the syntax differs.
A proof assistant is built around a small, paranoid core called the
\emph{kernel}. Everything you will learn in this book --- clever tactics,
powerful automation, beautiful notation --- is scaffolding whose only job is
to produce a proof object the kernel accepts. The kernel is a few thousand
lines of code that does one thing: check that each step of a proof follows
from the previous ones by a fixed set of rules. If the kernel accepts, the
theorem holds. If it does not, no amount of confidence, seniority, or good
intentions makes the program correct.
\begin{aha}
Here is the emotional core of formal verification, and it is worth
internalizing early: \textbf{the proof assistant is not your examiner, it is
your collaborator}. It never gets tired, never skips a case, never says
``obviously.'' Every hour you spend arguing with it is an hour a bug did not
survive. People who love proof assistants love them the way climbers love a
good belayer.
\end{aha}
\section{What we will actually verify}
This book is not a tour of toy examples. It is the curriculum companion to a
set of real verification projects in which the arithmetic core of
\textbf{Ed25519} --- the elliptic-curve signature scheme used by SSH, Signal,
TLS, and most cryptocurrency systems --- was machine-checked in Lean~4,
starting from the actual Rust source code of the
\code{curve25519-dalek} library and several of its production forks.
The proofs are organized as a pyramid. Each layer states the correctness of
one abstraction level and rests on the layer beneath it:
\begin{center}
\begin{tikzpicture}[
lay/.style={draw=ink2,thick,rounded corners=2pt,align=center,minimum height=0.95cm},
note/.style={font=\small\color{ink2},align=left,anchor=west}
]
\node[lay,fill=accentsoft,minimum width=2.8cm] (sig) at (0,3.45) {\textbf{Signature}\\[-2pt]\small EdDSA verify};
\node[lay,fill=warnsoft,minimum width=5.2cm] (sca) at (0,2.3) {\textbf{Scalar arithmetic mod $\boldsymbol{\ell}$}};
\node[lay,fill=provensoft,minimum width=7.6cm] (grp) at (0,1.15) {\textbf{Group law} \small (twisted Edwards points)};
\node[lay,fill=codebg,minimum width=10cm] (fld) at (0,0) {\textbf{Field arithmetic in $\Fp$}, \small $p = 2^{255}-19$};
\node[note] at (5.6,0) {limbs, carries, multiplication};
\node[note] at (5.6,1.15) {point addition is complete \& correct};
\node[note] at (5.6,2.3) {the group order $\ell$, reduction};
Accuracy sweep: bring the book to the proven four-tier apex state The companion repos completed their signature apex (phases 1+2: four button-enforced tiers up to "accept <=> decompress(R) = [k](-A)+[s]B as points") and the scalar layer long ago crossed the kernel frontier - but the book still taught the pre-campaign state, including one real inaccuracy of the class coherence pass 3 purged from the repo READMEs: ch12's apex section and audit-drill solution described the COFACTORED equation (8sB = 8R + 8kA) with SIMD backends in the trusted base - neither matches the proven certificates (canonical-R byte equality, serial path pinned and proven, SHA-512 an oracle with NO assumed properties). Fixed: - ch12: pyramid status diagram (scalar + signature rows now "done"), the scalar-frontier paragraph (the wall was crossed, and how), the apex section (future tense -> the proven four-tier statement, honest trusted base), the "extend the pyramid" bullet (scalar -> pasta curve layer; CONTRIBUTING files never existed - now points at the control repo's METHOD/TIERS), exercise 12.2(c) solution (which lineage the cofactored robustness belongs to), exercise 12.3 + solution (audit the REAL certificate). - ch01: framing diagram states the equation actually proven (sB = R + kA from raw bytes, not the cofactored form). - glossary: Cofactor entry says which verifiers check which equation. - ch11: the companion repos' posture is stronger than the ideal-hash example - no hash properties assumed at all, backend question eliminated rather than assumed. - ch08: extraction notes (one merged universe; extract-scalar.sh was retired in coherence pass 3; SIMD scoped out, not assumed). - repo tour appendix: floor plan, reading order (item 5 now tours the apex capstone), Phase 3b described. - README: companion table rows say "the complete pyramid" with the four-tier apex; honesty ledger records this 2026-07-06 re-audit. main.pdf rebuilt from the updated sources (106 pages, zero errors, build-pass4.log retained). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 05:46:27 +00:00
\node[note] at (5.6,3.45) {the equation $sB = R + kA$,\\ checked from the raw bytes};
\end{tikzpicture}
\end{center}
By the end of this book you will be able to read --- and extend --- the real
proofs at every layer of this pyramid. The journey looks like this:
\begin{itemize}[leftmargin=1.4em]
\item \textbf{Chapters 2--5} teach Lean itself, from \code{\#eval 1+1} to
proofs by induction and the automation that dispatches arithmetic goals.
\item \textbf{Chapters 6--7} build the mathematics: modular arithmetic, finite
fields, and how to convince a paranoid kernel that a 77-digit number is
prime.
\item \textbf{Chapters 8--9} cross the bridge from Rust to Lean: how real
code is translated into a form we can reason about, and the single most
important idea in the whole enterprise --- the \emph{denotation function}.
\item \textbf{Chapters 10--12} assemble the pyramid: field correctness, the
ethics of axioms and honest boundaries, and the layers above.
\end{itemize}
\section{Proofs versus tests: the honest comparison}
Formal verification is not magic, and this book will never pretend otherwise.
It is worth being precise, right now, about what a machine-checked proof does
and does not give you.
\begin{center}
\begin{tabular}{@{}p{0.44\linewidth}p{0.48\linewidth}@{}}
\toprule
\textbf{Testing} & \textbf{Proving} \\
\midrule
Checks sampled inputs & Checks \emph{all} inputs \\
Cheap to start, cheap to run & Expensive to write, cheap to re-check \\
Finds bugs & Establishes their absence (w.r.t.\ the spec) \\
Trusts nothing & Trusts the spec, the model, the kernel \\
Silent about \emph{why} code is right & The proof \emph{is} the why \\
\bottomrule
\end{tabular}
\end{center}
That word \emph{spec} in the right column is the fine print, and it matters
enormously. A proof shows that code satisfies a specification. If the
specification says the wrong thing --- or says nothing, or is accidentally
trivial --- the proof is worthless no matter how green the checkmark. A
recurring theme of this book (it gets its own chapter,
Chapter~\ref{ch:honesty}) is how to read a verification claim skeptically:
What exactly was proven? Against which model of the code? Resting on which
axioms?
\begin{aha}
The most dangerous artifact in formal methods is not a wrong proof --- the
kernel prevents those. It is a \emph{correct proof of the wrong statement}.
Learning to smell those is as important as learning to write proofs at all.
\end{aha}
\section{Why Lean, and why now}
Twenty years ago, verifying real cryptographic C or Rust code was a heroic,
multi-year effort. Three things changed:
\begin{enumerate}[leftmargin=1.6em]
\item \textbf{Proof assistants matured.} Lean~4 is fast, pleasant, and comes
with \emph{Mathlib}, a library of over a million lines of formalized
mathematics --- finite fields and elliptic-curve ingredients included, so we
do not start from bare axioms.
\item \textbf{Translation pipelines appeared.} Tools like \emph{Charon} and
\emph{Aeneas} mechanically translate real Rust code into Lean definitions,
so the thing we verify is derived from the code that ships, not a
hand-transcribed approximation (Chapter~\ref{ch:rust}).
\item \textbf{Automation got serious.} Decision procedures like \lean{omega}
(linear integer arithmetic) and \lean{decide} discharge the boring 90\% of
goals, leaving humans the interesting 10\%.
\end{enumerate}
None of this made verification \emph{easy}. It made verification
\emph{possible for a well-prepared person in finite time} --- and preparing
you is exactly what this book is for.
\begin{tryit}
You do not need anything installed yet, but if you want to run code from
Chapter~2 onward, install Lean now. One command:
\begin{lstlisting}
curl https://elan.lean-lang.org/elan-init.sh -sSf | sh
\end{lstlisting}
Then open the \code{exercises/} folder of this repository in VS~Code with the
\emph{Lean 4} extension. The orange progress bar you will see is the proof
checker working through the file --- your new collaborator saying hello.
\end{tryit}
\section*{Exercises}
\exercise{A function takes two 255-bit inputs and is buggy on exactly one
input pair. Assume you can test $10^{9}$ random pairs per second. Estimate the
expected time to find the bug by random testing, in multiples of the age of
the universe ($\approx 4\times10^{17}$ seconds). You may approximate freely;
the point is the order of magnitude.}
\exercise{Give an example, from your own programming experience, of a bug that
survived a test suite. What property would a specification have needed to
state in order to exclude it?}
\exercise{(Discussion) A colleague says: ``Our crypto library is audited by
three firms every year; formal verification is redundant.'' Name one class of
defect audits are better at than proofs, and one class where proofs are
strictly stronger.}
\exercise{Redo the headroom budget for a hypothetical radix-$26$
representation on $32$-bit words (ten limbs of $26$ bits for a 255-bit
value, a real design used on small CPUs). How many bits of headroom per
limb? How many lazy additions fit in the budget? Compare with the
radix-51/64-bit numbers and state which design must reduce more often.}
\section*{Solutions and pathways}
\solutionsintro
\solhead{1.1}
\pathway The only inputs that reveal the bug form a set of size $1$ inside
a set of size $2^{510}$, so a uniformly random test hits it with
probability $2^{-510}$; expected number of trials is $2^{510}$ (waiting
time of a geometric distribution). Then it is the Chapter-1 conversion
drill: powers of two $\to$ powers of ten $\to$ seconds $\to$ universes.
\answer Expected trials $2^{510} \approx 10^{153.5}$. At $10^9$ per second:
$10^{153.5 - 9} = 10^{144.5}$ seconds. Divide by the age of the universe,
$4 \times 10^{17}$ s:
\[
\frac{10^{144.5}}{4 \times 10^{17}} \approx 10^{126.9}
\quad\text{--- about } 10^{127} \text{ universe-ages.}
\]
Any answer within a few orders of magnitude is ``correct'': the lesson is
that no engineering factor (faster farms, smarter fuzzing schedules) dents
a number with $127$ digits of margin.
\solhead{1.2}
\pathway Pick a bug whose trigger was a \emph{property of the input}, not a
coding typo --- those are the ones a spec excludes. Then ask: what is the
universally quantified sentence that is false in the buggy program?
\answer (Model answer.) A JSON parser that crashed on deeply nested arrays
survived a big test suite: no test nested past depth $50$. The
specification that excludes it must \emph{quantify over all inputs}:
``for every input string, the parser terminates and returns either a value
or a well-formed error'' --- termination-for-all is exactly what the tests
never said. The general shape to remember: test suites assert
$P(x_1), \dots, P(x_n)$; specifications assert $\forall x,\, P(x)$; bugs
live in the gap.
\solhead{1.3}
\pathway Sort defect classes by \emph{whether their badness is expressible
as a violated formal property of the code}. Audits see things that are not
properties of the code; proofs cover input space no human can.
\answer Audits win at: flaws in the \emph{specification itself} and its
surroundings --- wrong protocol choice, misuse-prone APIs, side channels
outside the model, deployment and key-handling practice. A proof of the
wrong spec passes; a good auditor smells that the spec is wrong. Proofs are
strictly stronger at: input-space coverage for the stated property ---
carry bugs, overflow corners, algebraic edge cases on a measure-zero slice.
The honest synthesis: audits examine the \emph{claim}, proofs guarantee the
\emph{claim's body}. A serious system wants both, aimed at their targets.
\solhead{1.4}
\pathway Same two lines as the worked example, new constants: headroom
$= \text{word} - \text{radix}$; budget $= 2^{\text{headroom}}$.
\answer Headroom $32 - 26 = 6$ bits, so at most $2^{32}/2^{26} = 2^{6} =
64$ lazy additions --- against $8192$ for radix-51/64-bit, a budget $128$
times tighter. The radix-26 design must interleave reductions far more
often, and its correctness argument has $128$ times less slack for
miscounting --- one concrete reason ports of crypto code to small targets
are disproportionately bug-prone, and why per-fork verification
(Chapter~\ref{ch:field}) is not paranoia.
\begin{checkpoint}
Before moving on, you should be able to explain to a friend:
(1) why testing fundamentally cannot establish correctness of a 255-bit
arithmetic function; (2) what a proof assistant's kernel is and why its small
size matters; (3) what a proof of correctness actually promises --- and the
role the specification plays in that promise.
\end{checkpoint}