Skip to content

fix(x402): sign an authorization window that has not already expired - #31

Open
pucedoteth wants to merge 1 commit into
Allium-Science:mainfrom
pucedoteth:fix/x402-authorization-validity-window
Open

pucedoteth wants to merge 1 commit into
Allium-Science:mainfrom
pucedoteth:fix/x402-authorization-validity-window

Conversation

@pucedoteth

Copy link
Copy Markdown

The bug

EIP-3009 validAfter / validBefore are absolute unix timestamps — the token contract enforces block.timestamp < validBefore. _build_payment_header signs the challenge's maxTimeoutSeconds directly:

"validAfter": "0",
"validBefore": str(option["maxTimeoutSeconds"]),

maxTimeoutSeconds is a duration (how long the resource server will wait for settlement), not a deadline. For a typical challenge advertising 60 it produces validBefore = 60, which is 1 January 1970, 00:01:00 UTC.

Running _build_payment_header against main and decoding the header it returns:

validAfter  = 0
validBefore = 60          -> 1970-01-01 00:01:00 UTC
now         = 1787752097  -> 2026-08-26 13:48:17 UTC
EXPIRED

Every authorization the CLI signs is already expired when it is submitted, so it cannot settle — the transfer reverts on the require(block.timestamp < validBefore) check.

The same pair is written twice, once into the EIP-712 message that is signed and once into the authorization object that is sent, so both copies carry it.

The fix

Derive the window from the clock:

  • validBefore = now + maxTimeoutSeconds — the advertised timeout, as a deadline
  • validAfter = now - 600 — backdated so a small clock difference against the settling node can't make a freshly signed authorization look not-yet-valid

This matches what the x402 reference client does. Both call sites now read from one computed pair rather than repeating the expression.

After:

validAfter  = 1787751497
validBefore = 1787752157  -> 2026-08-26 13:49:17 UTC
now         = 1787752097  -> 2026-08-26 13:48:17 UTC
valid

Test plan

tests/test_x402.py is new — there was no coverage of this module. Six cases: the two _select_payment_option branches, then the validity window (deadline in the future, validAfter already past but bounded, window spanning the advertised timeout), plus one that pins the signed EIP-712 message to the submitted authorization field-by-field, so the two copies can't drift apart again.

./test.sh    # 73 passed
./lint.sh    # All checks passed! / 47 files already formatted

Reverting only cli/clients/x402.py and keeping the tests reproduces the table above.

One assumption worth confirming

I read maxTimeoutSeconds as a duration, per the x402 spec's "maximum number of seconds the resource server will wait for settlement", and the 1970 timestamp it currently yields is consistent with that. If your facilitator instead expects the raw advertised value passed through untouched, this is wrong and I'd want to know.

EIP-3009 validAfter/validBefore are absolute unix timestamps — the token
contract enforces `block.timestamp < validBefore`. _build_payment_header signed
the challenge's maxTimeoutSeconds directly:

    "validAfter": "0",
    "validBefore": str(option["maxTimeoutSeconds"]),

maxTimeoutSeconds is a duration (how long the resource server waits), so a
typical challenge produced validBefore = 60, i.e. 1970-01-01 00:01:00 UTC. Every
authorization was signed already expired and cannot settle:

    validBefore = 60 -> 1970-01-01 00:01:00 UTC
    now         = 1787752097 -> 2026-08-26 13:48:17 UTC

Compute the window from the clock instead: validBefore = now +
maxTimeoutSeconds, and backdate validAfter by 600s so clock skew against the
settling node can't make a fresh authorization look not-yet-valid — matching
what the x402 reference client does.

The value is written twice, into the EIP-712 message that gets signed and into
the authorization that is submitted, so both now read from one computed pair; a
test pins them to each other.

Co-Authored-By: Claude Opus 5 <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant