atproto utils for zig zat.dev

jwt: isExpiredAt; crypto: real RFC 6979 nonces #33

Open zzstoatzz.io
targeting main from cell-extras
Open opened by and assigned to zzstoatzz.io
targeting main from cell-extras

Requested by nate

ECDSA signing now derives nonces with real RFC 6979, so zat's signatures match @atproto/crypto byte for byte. Before this, zat used std's draft-based deterministic nonce, which produced valid signatures with different bytes. Jwt.isExpiredAt(now_seconds) lets callers check expiry against their own clock.

Signatures from a given key now have different bytes and still verify. Commit CIDs signed by zat change too.

jwt: isExpiredAt
  • isExpiredAt(self, now_seconds: i64) bool: unix seconds, matching Payload.exp. A token stays valid during the second named by exp.
  • isExpired(io) reads the clock and calls it.
  • Tests cover exp - 1, exp, exp + 1, the clock wrapper with past and future tokens, and a payload without exp. That payload still fails to parse with MissingExpiration.
crypto: RFC 6979 nonces
  • signEcdsa derives k with RFC 6979 section 3.2: HMAC-DRBG with HMAC-SHA-256 and no extra entropy. For these 256-bit orders with SHA-256, bits2int is the identity and bits2octets is one reduction. It then computes r and s with std.crypto.ecc point and scalar operations and normalizes to low-S.
  • The public API is unchanged: signSecp256k1, signP256, Keypair.sign.
  • The doc comments now say exactly what the code does.
verification
  • RFC 6979 A.2.5 P-256/SHA-256 "sample" and "test": k and r match exactly. s matches after low-S normalization; the RFC's "sample" s is high-S.
  • 16 fixed vectors from @atproto/crypto 0.5.7 (@noble/curves 1.9.7) are hardcoded in jwt.zig: two keys and four messages on each curve. zat's output is byte-identical, verifies with zat's strict verify, and verifies with @atproto/crypto's verifySignature. The same test fails on origin/main.
  • zig build check, zig build test (605/605) and CI's arm1176 compile all pass. Each commit passes on its own.
  • wasm32-freestanding ReleaseSmall: src/root.zig, compiled as a module with build-exe and called through Keypair.sign, builds and produces the same 16 signatures under node. The full package build cannot target freestanding wasm because websocket's bundled C zlib needs libc headers. That was already true before this change.
  • No zat fixture hardcodes zat-produced signatures or CIDs. Interop fixtures are untouched.

🤖 Generated with Claude Code

Commits 0