Why zero dependencies is a security property in cryptographic libraries
Modern Python packaging is fast, ergonomic, and a security disaster waiting in the wings. Every package you pip install has dependencies. Each of those has dependencies. By the time pip finishes, a one-liner install has pulled in dozens of packages from dozens of maintainers, any of whom can ship a malicious release tomorrow and reach every machine that runs pip install --upgrade shortly after.
For most libraries this is a tolerable tradeoff. For a cryptographic library it is not. This is the case for commit-reveal shipping with an empty runtime dependency list, and why we will keep it that way.
What a cryptographic library is doing
A commit-reveal library has a small job: produce bytes that are tamper-evident commitments, and check whether a candidate (value, salt) pair matches a commitment. If you can convince the library to return True for a reveal that should be False, an entire protocol stack built on top falls over — sealed bids get re-opened after the close, votes get re-counted, MEV defences disappear.
The attack surface for this job is small. It is a few hundred lines of source. The interesting thing is not how much we wrote; it is how much someone else could ship into the same process. A single malicious transitive dependency that monkey-patches hmac.compare_digest to always return True would silently break every reveal check — and the import chain that delivered the patch is not something the application author necessarily ever sees.
Zero runtime dependencies removes that attack vector entirely. There is no transitive graph, because the graph has one node.
What we actually mean by “zero dependencies”
commit-reveal’s pyproject.toml lists exactly one runtime dependency:
[tool.poetry.dependencies]
python = "^3.8"
Nothing else. Dev tools — pytest, mypy, hypothesis, bandit, black — live under the dev group and never ship to users. The library imports hashlib, hmac, secrets, os, and typing from the standard library; the secure CLI adds getpass. That is the complete trusted base.
This is not free. It rules out:
- Native
secp256k1(coincurve,ecdsa,cryptography). The ZKP module re-implements the curve in pure Python instead, slower but auditable in one file. - Argparse subcommand frameworks (
click,typer). The CLI uses stdlibargparse. - Rich logging or progress bars. The CLI prints plain strings.
For a primitive whose job is correctness and not performance, these are easy tradeoffs to make. The Python ZKP is roughly 100x slower than a native secp256k1 binding for a single proof, and that is fine: commit-reveal is a protocol primitive, not a TLS terminator. If you are signing a million bids per second, this is the wrong library, and the right library has been audited at a level we do not claim.
Why supply-chain attacks are the threat model now
Five years ago the dominant attack on Python packages was typosquatting: an attacker publishes requets next to requests, hopes someone fat-fingers an install, and wins. The Python Package Index added typo detection, package signing improved, and the attack got harder.
The current dominant attack is account compromise on legitimate packages. An attacker phishes a maintainer, takes over their PyPI account, and ships a malicious release of a package that is already in millions of requirements.txt files. Pinning helps only if you never upgrade; not pinning means the malicious release shows up the next time CI runs pip install.
This is not theoretical. Recent years have seen successful compromises of eslint-scope, event-stream, ua-parser-js, colors.js, and others. Cryptographic libraries are higher-value targets than these were. A compromised TLS library reaches every HTTPS request a victim makes. A compromised commit-reveal library reaches every sealed bid.
The mitigation everyone reaches for is dependency pinning with a lockfile. That helps, but it is downstream of the real question: how big is the trusted base in the first place? Pinning a 47-package transitive graph just means you are auditing 47 packages, not 1.
The legibility argument
There is a second reason to keep the dependency list empty that has nothing to do with malicious actors: the library should be possible to read.
A reviewer who picks up commit-reveal can read every line of every imported module within an afternoon. Open commit_reveal/core.py and you can trace how a commitment is computed, byte by byte. Open commit_reveal/zkp.py and you can see secp256k1 written out in textbook form: the curve parameters, point addition, scalar multiplication, compression. Open commit_reveal/validation.py and you can see exactly which inputs are accepted and which raise.
The same audit, performed against a library that uses cryptography for hashing and coincurve for the curve, is impractical. cryptography is millions of lines of C and Rust, with bindings, build-time variants, and platform-specific code paths. coincurve wraps libsecp256k1, which is itself a few thousand lines of carefully-reviewed C. The reviewer trusts the audit work that has been done elsewhere, transitively, and hopes the linkage is what it claims to be.
For a protocol-primitive library, that tradeoff is wrong. The library should be the kind of thing a reviewer reads end to end before relying on it. Zero dependencies makes “end to end” mean a finite amount of code.
What we lose, and why it is acceptable
The honest cost of pure Python for the ZKP is:
- Throughput. Each Schnorr proof requires two scalar multiplications on secp256k1, each of which is a few thousand Python-level integer operations. We measure roughly 5–15 proofs per second per CPU core. A native binding does tens of thousands.
- Constant time. The pure-Python scalar multiply is not constant-time at the level a native implementation can guarantee. For an offline ZKP attached to a published commitment, where the ZKP secret is derived from the value-salt pair and never re-used, this is acceptable. For long-lived keys used in many proofs, it is not, and you should use a different library.
- Memory predictability. Python integer arithmetic allocates as it computes. A motivated attacker measuring allocation behaviour during scalar multiplication could in principle leak bits of the scalar. Again, for derived per-commitment secrets that are used once and discarded, this is acceptable; for long-lived keys, it is not.
SECURITY.md documents all three. The library is not pretending to be a high-throughput, side-channel-hardened ZKP toolkit. It is a small, auditable, zero-dependency implementation of two specific primitives.
What this buys an integrator
If you depend on commit-reveal, your transitive supply-chain risk for cryptographic correctness reduces to:
- CPython itself, including its
hashlib,hmac, andsecretsmodules. - Our package, which you can audit in an afternoon and pin to a specific git SHA.
That is roughly the smallest cryptographic-trust footprint Python permits. Everything else — CI, the application code, downstream consumers — depends on it. Reducing the footprint of this library does not eliminate supply-chain risk in your stack, but it does mean that risk is no longer compounded by the dependencies we would have brought along.
If, after reading this, you want to use a native secp256k1 binding anyway, that is a defensible choice for many protocols. The right place for that decision is in your project, where you can pin the binding and audit it, not buried under our requirements.txt where it is invisible to you.
What this does not buy
Zero dependencies is not a substitute for:
- Reading the source. The library is small enough that you can.
- Following the threat model.
SECURITY.mdis the source of truth for what we defend against. - Writing protocol-level mitigations. The library does not prevent selective non-reveal, key reuse across commitments, or denial of service. Those are your protocol’s problem.
- A formal external audit. This project has not had one. We do not pretend it has.
What zero dependencies does buy is the precondition for all of the above: a trusted base small enough that “trust” is a thing you can actually establish, not a thing you delegate to dozens of upstream maintainers you have never met.
That is the property we are trying to keep.