Skip to content

[proposal] std.crypto roadmap: payment-protocol primitives via staged PRs #326

Description

@PeterXMR

Filing this as a roadmap-shaped proposal rather than a single design issue, because the underlying capability (real digests, HMAC/HKDF, Keccak, secp256k1, EIP-712) splits cleanly into ~6 mergeable PRs of <500 LOC each, and the order is forced by dependencies that shouldn't have to be re-derived later. No ask for immediate action — same posture as #47 took before PR #64 landed — but recording the sequence now lets any single PR be picked up in isolation when stdlib expansion next resumes after the 2026-05-25 std.math / std.path / std.str triple.

Load-bearing architectural choice up front: std.crypto today exposes hash32 / hmac32 / constantTimeEql / secureRandomU32 and is explicitly "not a full cryptography suite" (crypto.md). Source-backed expansion would be the natural shape matching PR #273's std.math precedent — except Zero has no surface-level bitwise operators today (verified: no ^ & | << >> ~ in std/math.0, std/str.0, std/path.0, no token names in native/zero-c/src/parser.c / checker.c / ir.c). Every hash in this roadmap is XOR + rotate + shift at its core, so pure .0 source-backed implementations are blocked on a language change. Recommendation throughout: runtime intrinsics following the zero_http_curl.c pattern — typed std.crypto.* signatures in native/zero-c/src/std_sig.c, C bodies in new native/zero-c/runtime/zero_crypto_*.c files. For SHA-2 / HMAC / HKDF / Keccak the C body can wrap any vetted source (BoringSSL crypto/, libsodium, HACL*). For secp256k1, libsecp256k1 is the only mainstream vetted option — BoringSSL and libsodium don't ship it. Rolling crypto in source carries CVE risk that the README's "Security vulnerabilities should be expected" posture acknowledges but applications touching real money shouldn't take on.

Motivation

satring is the concrete external project driving this — a payment-protocol client that talks to three rails (Ethereum/EIP-712, Lightning, and an HTTPS REST provider). Two of the three need primitives std.crypto doesn't expose today. The same primitives unlock a much wider class of agent-facing apps:

Use case Needs Touches existing std?
HMAC-signed webhook verification (Stripe, GitHub, generic) HMAC-SHA-256 std.http.fetch receive side
JWT signing/verify (HS256, HS512) HMAC-SHA-256/512 std.codec for base64url
Ethereum signing (EIP-191 personal_sign, EIP-712 typed data) Keccak-256 + secp256k1 ECDSA std.codec, std.mem
Lightning / L402 (rfc.l402.org) SHA-256 + secp256k1 + (optionally) macaroons std.http.fetch, std.parse
Content addressing (Git blob IDs, IPFS CIDs, ETags) SHA-256 / Keccak-256 std.fs, std.mem
General payment-protocol clients (satring is one) All of the above std.http.fetch

The roadmap below sequences low-risk wins (SHA-2, HMAC, HKDF) before higher-risk ECC, and gates each PR on explicit dependencies so no PR ships an orphaned API or blocks on a not-yet-merged sibling.

PR roadmap

Each row is sized to (a) compile and add user-visible value alone, (b) not orphan APIs if the next slips, (c) stay under ~500 LOC of diff.

# Title Depends on Scope Acceptance vectors Files touched ~LOC Unlocks on landing
1 std.crypto.sha256 / sha512 Streaming Sha256 / Sha512 types with init / update / finalize + convenience sha256(span) → [32]u8. Wrap vetted C. NIST FIPS 180-4 + CAVP byte-test vectors native/zero-c/runtime/zero_crypto_sha2.c (new), native/zero-c/src/std_sig.c, docs/articles/modules/crypto.md, conformance/run.mjs cases ~400 File hashing, Git-style blob IDs, ETag generation, content addressing
2 std.crypto.hmacSha256 / hmacSha512 PR 1 Pure HMAC construction on top of PR 1; same streaming shape. RFC 4231 §4.2–4.8 zero_crypto_hmac.c (new), std_sig.c, doc, conformance ~150 GitHub/Stripe webhook signature verify, JWT HS256/HS512, AWS Signature v4
3 std.crypto.hkdfExtract / hkdfExpand PR 1 + PR 2 (transitive) Two-stage HKDF for SHA-256; SHA-512 variant follows. RFC 5869 Appendix A test cases 1–7 zero_crypto_hkdf.c (new), std_sig.c, doc, conformance ~120 TLS-style key schedule, secure cookie keying, deterministic sub-key derivation
4 std.crypto.keccak256 + sha3_256 (independent of SHA-2 chain) Both pre-NIST Keccak (Ethereum) and NIST SHA-3 padding flavors. NIST FIPS 202 + ethereum/tests GeneralStateTests + eth-hash fixtures zero_crypto_keccak.c (new), std_sig.c, doc, conformance ~350 Ethereum address derivation, EVM function selectors, IPFS CID v1 with keccak
5 std.crypto.secp256k1Sign + Verify + Recover + secureRandomBytes PR 1 (SHA-256 for hashing) Wrap libsecp256k1. Must pass secp256k1_nonce_function_rfc6979 explicitly to secp256k1_ecdsa_sign — the default nonce function on some build configurations falls back to OS entropy without RFC 6979 hardening, which leaks the key on entropy failure. secureRandomBytes(MutSpan<u8>) ships here because key generation needs ≥256 bits of entropy and secureRandomU32 only gives 32. Wycheproof ecdsa_secp256k1_sha256_test.json (Google's adversarial test suite, language-neutral JSON, hundreds of ECDSA cases including known edge cases like high-S, leading zeros, malleability) + libsecp256k1's own src/modules/ecdsa/tests/ zero_crypto_secp256k1.c (new wrapper), std_sig.c, doc, conformance, vendored libsecp256k1 under native/zero-c/runtime/vendor/secp256k1/ (~20–30k LOC of standalone C — reviewed as "do we trust the Bitcoin Core team?" not line-by-line) ~300 Zero/glue Bitcoin/Ethereum/Lightning signing, EIP-191 personal_sign, ECDSA webhook auth
6 std.crypto.eip712Digest (+ supporting std.mem.writeU64BE / writeU32BE intrinsics) PR 4 (Keccak) Produces the 32-byte EIP-712 digest. Encoding requires byte-level decomposition of integers, which without bitwise ops needs new std.mem.writeU{32,64}BE intrinsics (~30 LOC each). First form accepts bytes32 / address / bool / uintN<=64; arbitrary uint256 fields deferred to a follow-up gated on bigint or callers supplying pre-encoded [32]u8. Signing is a separate secp256k1Sign call (PR 5) — splits cleanly so a digest can be relayed to a hardware wallet. EIP-712 "Mail" reference + ethereum/eth-abi-utils typed-data fixtures + Foundry forge-std EIP712.t.sol; cross-check digests against ethers.js TypedDataEncoder std/eip712.0 (new, source-backed — pure structural encoding using PR 4 hashes + new writeBE intrinsics), updates to zero_runtime.c for the BE-writes, doc, conformance ~300 EIP-712 typed-data signing — Permit2, OpenSea-style orders, gasless meta-transactions, most modern dapp signature flows
7a (optional) std.crypto.macaroon PR 2 L402 / payment-auth scaffolding mirroring libmacaroons' ~10-function surface. Macaroons paper §3 test fixtures; libmacaroons reference impl TBD TBD L402 HTTP 402 payment auth, capability tokens with caveats
7b (optional) std.crypto.x25519 + ed25519 (curve25519 family, independent) Wrap libsodium. Decoupled from the secp256k1 chain — different curve, different use cases (Noise / age / WireGuard / Signal-style). RFC 7748 §6.1 + RFC 8032 §7.1 TBD TBD Noise protocols, age encryption, modern non-Ethereum signing
7c (optional, high-leverage) std.codec.bech32 + bech32m (no crypto deps; just polymod) Bech32 / bech32m encoding (BIP-173 / BIP-350). No bitwise required for the polymod — it's table-driven mod arithmetic over u32 (verified against std/math.0 operator set). Naturally source-backed; lives in std.codec not std.crypto. BIP-173 §6 test vectors + BIP-350 reference vectors std/bech32.0 (new, source-backed), docs/articles/modules/codec.md, conformance ~180 Lightning invoice addressing (BOLT11 prefix lnbc...), Lightning Address local-part, modern Bitcoin addresses (bc1...), NIP-19 Nostr entities
7d (optional, high-leverage) std.bolt11.decode (+ optional encode) PR 1 (SHA-256) + PR 5 (secp256k1 recover) + PR 7c (bech32) BOLT11 invoice decoder — extract amount, payment hash, payee pubkey (via signature recovery), routing hints, expiry. Most consumers only need decode; encode gated on signer availability. BOLT11 spec test vectors std/bolt11.0 (new, source-backed; pure structural decode + intrinsic hash/recover calls), docs/articles/modules/bolt11.md (new), conformance ~250 Any Lightning Network client work — invoice display, amount/payee verification before payment, payment-hash extraction for HTLC tracking

Total mainline (PR 1–6): ~1500–1700 LOC of Zero + C glue + one vendored library. PR 1, PR 2, PR 4 are independently mergeable from each other; the rest form a partial order.

Ecosystem coverage

Four shipped agent-payment toolkits, sampled across LN / Cashu / Ethereum surfaces, show what this primitive set actually unblocks beyond satring:

  • lightning-agent-tools (Lightning Labs) — MCP + skills for LN payments via L402; macaroon bakery, aperture reverse proxy, remote signer.
  • lightning-wallet-mcp — MCP wallet bridging L402 (Lightning) and x402 (USDC on Base via EIP-712 permits) in one tool; BOLT11 decode, keysend, webhook HMAC.
  • Alby AI — Bitcoin/LN agent tools over Nostr Wallet Connect (NWC) + MCP; sub-wallet budgeting, zero-custody.
  • cocod — Cashu wallet daemon; sends/receives Cashu + BOLT11, handles HTTP 402 with X-Cashu.
PR LN Agent Tools LN Wallet MCP Alby AI cocod Unblocked
1 SHA-2 4 / 4
2 HMAC ✓ macaroons ✓ L402 + webhook partial partial 2 firm
3 HKDF ✓ LN keys 1
4 Keccak-256 ✓ ETH addr 1
5 secp256k1 ✓ LN sign ✓ invoice verify ✓ Nostr sign ✓ Cashu / BOLT11 4 / 4
6 EIP-712 ✓ x402 permit 1
7a macaroons core ✓ L402 2
7c bech32 ✓ Lightning Addr 4 / 4
7d BOLT11 4 / 4

PRs 1 + 5 + 7c + 7d form the minimum set that touches every project in the table. PRs 6 + 7a together cover the LN↔Ethereum payment-rail bridge (lightning-wallet-mcp ships both today). A single primitive landing usually unblocks 2–4 projects, not one — the agent-payment ecosystem has converged on a small shared substrate.

Open questions

  1. Implementation path direction. Recommendation above is intrinsics-with-vendored-C throughout. Alternative paths exist: wait for extern fun (#70 / #194) and bind via FFI, or add bitwise ops first and ship source-backed. A one-line direction-of-travel signal lets PRs be reshaped before the first one lands.
  2. Constant-time discipline at the language level. Is there appetite for a secret<T> marker type the compiler refuses to use in data-dependent branches / table lookups, or are crypto authors expected to write constant-time helpers by hand in C and trust the compiler not to undo them? Note: Keccak's permutation is naturally data-independent in any sane implementation; the practical concern is SHA-2's S-box / table-driven implementations (and AES, which isn't in this roadmap). See Almeida et al., USENIX Security 2016.
  3. Vendoring policy. zero_http_curl.c links against the system libcurl. libsecp256k1 is typically vendored. Is native/zero-c/runtime/vendor/secp256k1/ acceptable, or should the roadmap assume system linkage? (Vendoring is the BoringSSL / Node.js precedent; system linkage is the Python / Ruby precedent.)

Notes & caveats

  • Don't roll your own crypto. Every primitive in this roadmap has a vetted reference that has survived adversarial review. The Zero surface should be the typed wrapper; the bytes-in / bytes-out core should come from BoringSSL crypto/, libsodium, HACL*, or libsecp256k1. This isn't conservatism — lone-maintainer crypto implementations have produced Lucky 13 (CVE-2013-0169), Schannel CurveBall (CVE-2020-0601), and many more.
  • ECDSA nonce reuse is the classic ECC disaster (Sony PS3 firmware signing key recovered in 2010, Android Bitcoin wallets 2013 from a SecureRandom bug). PR 5's explicit secp256k1_nonce_function_rfc6979 requirement is the mitigation; documenting it in the PR body and conformance test names it directly.
  • Cashu (BDHKE) needs secp256k1 point ops, not just ECDSA. PR 5 as scoped covers sign / verify / recover. Full Cashu support — like cocod's X-Cashu payment flow — additionally needs secp256k1_ec_pubkey_tweak_mul / _tweak_add / _combine exposed. ~50 LOC of glue against the same vendored library; scope-extension decision when PR 5 lands.
  • This is not a TLS proposal. TLS sits on top of these primitives but has its own tracking surface — separating them prevents either from blocking the other. EIP-712 / secp256k1 / Keccak don't require TLS.
  • No claim any of this should be picked up soon. Filing because the order is forced by dependencies. Happy to leave open indefinitely.

Related issues / PRs

  • #47 / PR #64 — HTTP client (tracking-then-merged precedent)
  • #70 / #194extern fun (would unlock an alternative implementation path)
  • #273std.math (source-backed stdlib precedent; doesn't apply here due to bitwise gap)
  • #305std.decimal (adjacent: same bigint-shaped gap surfaces for EIP-712 uint256 support)
  • #316Maybe<primitive> construction (gates fallible API shapes like Maybe<Signature>)
  • #317 — user-record return types (gates returning KeyPair / Signature records from constructors)

Filing as enhancement. Happy to keep open indefinitely as a parking spot, or to start with PR 1 in draft form if seeing the intrinsic-registration shape concretely would help triage.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions