r/crypto • u/Nervous_Ad_2710 • 10h ago
Title: Break my stdlib-only Ed25519 verifier (Python, RFC 8032, cofactorless strict)
I maintain SMRFORGE, an open-source (MIT) tool that seals simulation results into evidence bundles and ships a standalone offline verifier. The verifier re-implements RFC 8032 Ed25519 verification in the Python standard library with its own field arithmetic. I know the advice against rolling your own. That is why I am asking people who do this for a living to try to break it.
Each bundle section (inputs, result, provenance) is canonicalized and hashed with SHA-256. The section digests roll up into bundle_sha256, which is SHA-256 of the canonical digest list. The signature is over the ASCII string "sha256:<hex>" rather than the raw 32 bytes. The verifier first checks the signature under the public key embedded in the bundle. That is the integrity verdict. It then compares the embedded key to a pinned key as a separate origin verdict. So the verifier does process arbitrary keys, which is why the input rules below matter.
Decisions I want reviewed
- Cofactorless and strict. It checks [s]B == R + [k]A exactly, with k = SHA-512(R || A || M) read little-endian. There is no cofactor multiply.
- It rejects a point encoding with y >= p, a scalar s >= L, and any point not on the curve.
- It rejects small-order points where [8]A or [8]R is the identity. It also rejects a recovered x == 0 with the sign bit set, per RFC 8032 section 5.1.3 step 4.
This intentionally differs from libsodium, which is cofactored, and from original ref10, which is permissive, on small-order and mixed-order inputs. The right reference is a cofactorless strict verifier such as ed25519-dalek verify_strict, or the strict profile in Chalkias, Garillot, and Nikolaenko, "Taming the Many EdDSAs." The challenge is to find a (public key, message, signature) where my accept/reject disagrees with that profile.
About testing. There are 24 in-tree vectors, but the verifier itself produced their expected outcomes. They are regression and self-consistency checks, not independent known-answer tests. The independent checks were cross-validation against OpenSSL on random keys and messages in both directions, and the Chalkias et al. speccheck vectors. Every copy was run against those during development, but they are not vendored in the repo. There are five copies of the verifier, four in Python that largely mirror each other and one separate JavaScript implementation in the browser checker.
Known weaknesses. The RFC 3161 timestamp token is only shape-checked offline (valid base64 and a leading DER SEQUENCE tag). A separate openssl tool verifies it against a pinned TSA CA. And this is hand-rolled Ed25519 in pure Python.
My questions. Is rolling section digests into one signed bundle_sha256 sound? Should the trust-critical path use a vetted library, with the stdlib code kept only as a cross-check? Is shape-checking the timestamp offline and deferring crypto verification to openssl an acceptable split?
AI disclosure. I used Claude to write most of the verifier and to check this post against the source. I used Grok to draft and tidy the post. The prompts are in a comment below.
Sources
- Verifier: https://gitlab.com/smrforge/smrforge/-/blob/23ce423/smrf_verify/verify.py#L450
- Vectors: https://gitlab.com/smrforge/smrforge/-/blob/23ce423/conformance/ed25519/small_order_vectors.json
- Generator: https://gitlab.com/smrforge/smrforge/-/blob/23ce423/tools/gen_ed25519_kat.py
- RFC 8032: https://www.rfc-editor.org/rfc/rfc8032#section-5.1.7
- Taming the Many EdDSAs: https://eprint.iacr.org/2020/1244
- ed25519-dalek verify_strict: https://docs.rs/ed25519-dalek/latest/ed25519_dalek/struct.VerifyingKey.html#method.verify_strict