◐ Off-By-One · answer catalog

go-x25519-rfc7748-clamping-ladder

2 answer(s)gogo1.26gogo1.26

go-x25519-rfc7748-clamping-ladder

📦 Source in repository (JSON)

Answer 1

Done. The verified solution is written to ~/x25519/SOLUTION.md (self-contained, with the full code and verification), and the complete tested tree lives in ~/x25519/.

Diagnosis

Three bugs were found, two of them critical:

  1. Clamping (the security bug). The scalar must be clamped — s[0]&=248, s[31]&=127, s[31]|=64 — so it's a multiple of 8 and every low-order u (0, 1, p−1, order-8 point) annihilates to the all-zero output. Skipping it leaks small-subgroup elements (the demo shows unclamped u=1 → 0x01…00, u=p−1 → p−1, order-8 → nonzero).

  2. The hidden trap: RFC 7748 is little-endian, big.Int is big-endian. A naive pass of the raw 32 bytes to SetBytes/FillBytes silently breaks clamping (masks the wrong byte), runs the ladder over the wrong integer, and byte-reverses the output. Every byte-array boundary must be reversed — this was the root cause of "my clamp code looks right but low bits are still set."

  3. Ladder variant. Empirically, only the RFC 7748 §5 lazy conditional-swap form (swap ^= k_t, final swap after the loop) is correct; "swap-before" and "swap-after" folklore variants fail the RFC vectors. Ended with x2·z2^(p−2) (Fermat field inversion), encoded little-endian.

Verification (all green on go1.26)

$ go test ./lib/ -v
--- PASS: TestRFC52Vectors / TestRFC61Exchange / TestLowOrderU / TestCrossImplementation
--- PASS: TestExhaustiveSmallCurve (14000 pairs)
$ go run .
10 × [PASS] …  RESULT: ALL TESTS PASS

Evidence & signatures

# Evidence
- Problem class: go-x25519-rfc7748-clamping-ladder
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-29T16:55:45.024Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement X25519 (Curve25519 Diffie-Hellman) from scratch in Go using only the standard library (math/big allowed; crypto/ecdh, crypto/elliptic, and golang.org/x/crypto forbidden): decodeScalar MUST clamp the 32-byte scalar (byte[0] &= 248, byte[31] &= 127, byte[31] |= 64) and the u-coordinate's high bit must be masked, then run a Montgomery ladder with field inversion. Skipping clamping is a real security bug: the scalar is no longer a multiple of 8, so multiplying any low-order u-coordinate (0, 1, p-1, or the order-8 point 325606250916557431795983626356110631294008115727848805560023387167927233504) yields a NONZERO result instead of the required all-zero 32-byte string, enabling small-subgroup attacks. Verify against RFC 7748 section 5.2: X25519(a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4, e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c) = c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552, X25519(4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d, e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493) = 95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957, and the section 6.1 exchange (Alice 77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a, Bob 5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb, shared secret 4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742); every low-order u must produce the all-zero 32-byte output.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-x25519-rfc7748-clamping-ladder", "provider": "openrouter", "solved_at": "2026-08-29T16:55:45.024Z", "version": "1.26"}

Answer 2

Done. The verified solution is written to ~/x25519/SOLUTION.md (self-contained, with the full code and verification), and the complete tested tree lives in ~/x25519/.

Diagnosis

Three bugs were found, two of them critical:

  1. Clamping (the security bug). The scalar must be clamped — s[0]&=248, s[31]&=127, s[31]|=64 — so it's a multiple of 8 and every low-order u (0, 1, p−1, order-8 point) annihilates to the all-zero output. Skipping it leaks small-subgroup elements (the demo shows unclamped u=1 → 0x01…00, u=p−1 → p−1, order-8 → nonzero).

  2. The hidden trap: RFC 7748 is little-endian, big.Int is big-endian. A naive pass of the raw 32 bytes to SetBytes/FillBytes silently breaks clamping (masks the wrong byte), runs the ladder over the wrong integer, and byte-reverses the output. Every byte-array boundary must be reversed — this was the root cause of "my clamp code looks right but low bits are still set."

  3. Ladder variant. Empirically, only the RFC 7748 §5 lazy conditional-swap form (swap ^= k_t, final swap after the loop) is correct; "swap-before" and "swap-after" folklore variants fail the RFC vectors. Ended with x2·z2^(p−2) (Fermat field inversion), encoded little-endian.

Verification (all green on go1.26)

$ go test ./lib/ -v
--- PASS: TestRFC52Vectors / TestRFC61Exchange / TestLowOrderU / TestCrossImplementation
--- PASS: TestExhaustiveSmallCurve (14000 pairs)
$ go run .
10 × [PASS] …  RESULT: ALL TESTS PASS

Evidence & signatures

# Evidence
- Problem class: go-x25519-rfc7748-clamping-ladder
- Model: openrouter/deepseek/deepseek-v4-flash-0731
- Solved: 2026-08-29T16:55:45.024Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement X25519 (Curve25519 Diffie-Hellman) from scratch in Go using only the standard library (math/big allowed; crypto/ecdh, crypto/elliptic, and golang.org/x/crypto forbidden): decodeScalar MUST clamp the 32-byte scalar (byte[0] &= 248, byte[31] &= 127, byte[31] |= 64) and the u-coordinate's high bit must be masked, then run a Montgomery ladder with field inversion. Skipping clamping is a real security bug: the scalar is no longer a multiple of 8, so multiplying any low-order u-coordinate (0, 1, p-1, or the order-8 point 325606250916557431795983626356110631294008115727848805560023387167927233504) yields a NONZERO result instead of the required all-zero 32-byte string, enabling small-subgroup attacks. Verify against RFC 7748 section 5.2: X25519(a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4, e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c) = c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552, X25519(4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d, e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493) = 95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957, and the section 6.1 exchange (Alice 77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a, Bob 5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb, shared secret 4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742); every low-order u must produce the all-zero 32-byte output.", "environment": "go1.26", "language": "go", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "go-x25519-rfc7748-clamping-ladder", "provider": "openrouter", "solved_at": "2026-08-29T16:55:45.024Z", "version": "1.26"}
Generated from the verified corpus · MIT licensedBack to the catalog