Fifteen minutes, four tools, and a real vulnerability shape. Every command below was run to produce the output shown; if you paste them in order you get the same thing, because none of the numbers on this page were typed by hand.
python -m venv venv && source venv/bin/activate
pip install "certkit@git+https://github.com/nickharris808/certkit@main" \
"exploit-counter@git+https://github.com/nickharris808/exploit-counter@main"
Neither package has a runtime dependency. The install is fetching source, not a dependency tree.
This is the Heartbleed shape, in C:
/* attacker controls payload; record_len is the real record size */
if (19 + payload <= record_len) {
memcpy(dst, src + 3, payload); /* needs 3 + payload <= record_len */
}
The guard is 19 + payload <= record_len. The property the memcpy
needs is 3 + payload <= record_len. The claim is that the first implies the second,
for every attacker-controlled payload in range.
certkit init \
--domain "0 <= payload" --domain "payload <= 65535" \
--guard "19 + payload <= record_len" \
--safety "3 + payload <= record_len" \
--name heartbleed -o heartbleed.spec.json
You never hand-write JSON atoms. certkit init rejects == (that is two
relations — write both), chained comparisons, and anything nonlinear, rather than guessing what
you meant.
Before proving anything, find out whether there is anything to prove. This enumerates the declared box exactly:
exploit-counter check --spec heartbleed.spec.json --box "payload=0..255,record_len=0..255"
A sound guard reports zero escaping states. Try it with the guard weakened to
1 + payload <= record_len and you get a count and a concrete counterexample —
payload = 0, record_len = 1 passes that guard and violates safety. That is the
fastest way to discover you are about to try proving something false.
certkit ships no producer, so the multipliers come from you or from a solver. For this obligation
they are one line: weight 1 on the guard, weight 1 on the negated safety conjunct. Write
heartbleed.cert.json:
{
"schema": "certkit/farkas/v1",
"spec_fingerprint": "<paste the fingerprint from the spec>",
"obligations": [{ "multipliers": { "2": 1, "3": 1 } }]
}
Indices are positions in the rebuilt obligation: domain atoms first, then guard, then the negated safety conjunct.
certkit check --spec heartbleed.spec.json --cert heartbleed.cert.json
ACCEPTED
obligation 0: ok
To see the arithmetic rather than the verdict:
certkit explain --spec heartbleed.spec.json --cert heartbleed.cert.json
Change any multiplier and check again. The refusal names what failed —
non-strict combination needs const > 0, got -65535 — rather than saying
"invalid". Change the guard constant in the spec without recomputing the fingerprint and you get
a different refusal: certificate is bound to a different spec.
certkit export --spec heartbleed.spec.json -o obligation.smt2
z3 obligation.smt2 # unsat
cvc5 obligation.smt2 # unsat
unsat is the same claim ACCEPTED makes, reached by an implementation
that shares no code with certkit. If a solver ever said sat where certkit said
ACCEPTED, one of them would be unsound — which is why that comparison runs in CI on
every push rather than living in a paragraph like this one.
GitHub Actions:
- uses: nickharris808/certkit-action@main
with:
spec: heartbleed.spec.json
cert: heartbleed.cert.json
Locally, on every commit:
repos:
- repo: https://github.com/nickharris808/certkit
rev: main
hooks:
- id: certkit
Both treat exit 3 — UNVERIFIED, the tool declining to certify — as a failure. In a
gate, "declined to certify" and "refused" have the same consequence. Both also fail when
a certificate has no spec beside it, rather than skipping: a gate that silently skips what it
cannot check will happily report that all certificates verified while verifying none.
GitLab and CircleCI templates are in certkit/ci-templates/.
certkit lsp
Point an editor's LSP client at that for *.spec.json and the diagnostics arrive while
you are still thinking about the guard, instead of after CI stops you merging. It reports
structural problems, the verdict of a certificate sitting beside the spec, and variables the
domain never bounds. It never offers to fix a guard, and it never tells you one is correct.
You have a machine-checkable proof that these relations imply each other, re-checkable by anyone in seconds, in Python or in JavaScript, with no solver and no trust in whatever produced it. You do not have a proof that your C is safe — that depends on whether the spec models the code, and on whether the arithmetic in it can wrap. What this proves, and what it does not is the page about that.