Skip to content

fix(#2476): bind the HTTP method and path into the keeper request HMAC - #2534

Merged
dcccrypto merged 1 commit into
playgroundfrom
fix/2476-hmac-binding
Sep 2, 2026
Merged

fix(#2476): bind the HTTP method and path into the keeper request HMAC#2534
dcccrypto merged 1 commit into
playgroundfrom
fix/2476-hmac-binding

Conversation

@dcccrypto

Copy link
Copy Markdown
Owner

Closes #2476.

The signed string was "<timestamp>.<rawBody>" — no method, no path. Every route sharing KEEPER_REGISTER_SECRET therefore accepted every other route's signatures: the credential authenticated "someone who knows the secret", not "this request". A signature minted for one endpoint verified on another with the same body.

Now: [timestamp, METHOD, path, rawBody].join("\n").

Newline-separated, not dot-separated. A dot can appear in a path, so "POST./a.b" and "POST./a" + ".b" would be ambiguous under the old delimiter. A newline cannot appear in a method or a URL path. Method is upper-cased, path excludes origin and query — so the two ends cannot disagree over casing or a trailing host.

One endpoint is deliberately transitional

This is the part worth reading before approving.

PATCH /api/markets/[slab] verifies signatures from a caller that is not in this repo — the keeper service has no createHmac at all, so nothing I can inspect signs it. Making that verifier strict could break a live hop I cannot see.

So it accepts the legacy unbound form and logs every acceptance:

[keeper-hmac] Accepted a LEGACY unbound signature (#2476). The caller has not
migrated; it is not bound to PATCH /api/markets/<slab>. Remove allowLegacyUnbound
at this call site once these stop appearing.

That is not a fix — an unbound signature is still valid for any endpoint sharing the secret, which is the whole of #2476. The log exists so the transition can be finished rather than forgotten.

The internal markets → oracle-keeper hop has both ends in this repo, both updated here, so that verifier is strict immediately. A test pins that it rejects the legacy form.

What remains, exactly as the issue says

No nonce. A captured signature is still replayable against the same endpoint within MAX_SIGNATURE_AGE_MS. A nonce needs a store shared across serverless instances — the same constraint that shaped the keeper-register proof in #2505 — so it is a separate design problem, not a line change.

Found while doing this — filed as #2533

The launch app sends HMAC headers to the keeper's /register, but that endpoint requires x-shared-secret, which the app never sends. Hot-registration 401s and fails silently behind "Keeper unreachable — market will auto-discover on next cycle".

LAUNCH-16 migrated the sender and never the receiver — which is also why #2233 is still accurate.

Verification

  • Negative control: removing the binding fails 2 of the 9 tests in that file
  • an existing test signed the unbound message; split into sign() / signUnbound() so the legacy form is still constructible and now asserts rejection
  • launch suite 3138 passed / 16 skipped / 0 failed; tsc clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D

The signed string was `"<timestamp>.<rawBody>"` — no method, no path. Every route
sharing KEEPER_REGISTER_SECRET therefore accepted every other route's signatures:
the credential authenticated "someone who knows the secret", not "this request".
A signature minted for one endpoint verified on another with the same body.

Now signs `[timestamp, METHOD, path, rawBody].join("\n")`.

Newline-separated rather than dot-separated: a dot can appear in a path, so
"POST./a.b" and "POST./a" + ".b" would be ambiguous under the old delimiter. A
newline cannot appear in a method or a URL path. Method is upper-cased and path
excludes origin and query, so the two ends cannot disagree over casing or a host.

ONE ENDPOINT IS DELIBERATELY TRANSITIONAL, and this is the part worth reading.

`PATCH /api/markets/[slab]` verifies signatures from a caller that is NOT in this
repo — the keeper service has no createHmac at all, so nothing I can see signs
it. Making that verifier strict could break a live hop I cannot inspect, so it
accepts the legacy unbound form as well AND LOGS EVERY ACCEPTANCE. That is not a
fix; an unbound signature is still valid for any endpoint sharing the secret,
which is the whole of #2476. The log exists so the transition can be finished
rather than forgotten — once it goes quiet, drop the flag.

The internal markets -> oracle-keeper hop has both ends in this repo and both are
updated here, so that verifier is STRICT immediately. A test pins that it rejects
the legacy form.

WHAT REMAINS, exactly as #2476 says: there is still no nonce, so a captured
signature is replayable against the SAME endpoint within MAX_SIGNATURE_AGE_MS.
A nonce needs a store shared across serverless instances — the same constraint
that shaped the keeper-register proof in #2505 — so it is a separate design
problem, not a line change.

FOUND WHILE DOING THIS, filed as #2533: the launch app sends HMAC headers to the
keeper's /register, but that endpoint requires `x-shared-secret`, which the app
never sends. Hot-registration 401s and fails silently behind "Keeper unreachable
— market will auto-discover on next cycle". LAUNCH-16 migrated the sender and
never the receiver, which is also why #2233 is still accurate.

An existing test signed the unbound message; split into sign() and signUnbound()
so the legacy form is still constructible and is now used to assert REJECTION.

Negative control: removing the binding fails 2 of the 9 tests in that file.

Launch suite: 3138 passed / 16 skipped / 0 failed.

Refs: #2476

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgoNgagkvw7i5SSRC3FJ8D
@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
percolator-launch Building Building Preview Sep 2, 2026 9:49pm UTC
percolator-mainnet Ready Ready Preview Sep 2, 2026 9:49pm UTC
percolator-playground Ready Ready Preview Sep 2, 2026 9:49pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 50 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: a994f320-3c4f-48fd-a01c-7d63da146619

📥 Commits

Reviewing files that changed from the base of the PR and between 7f045dc and 4463d14.

📒 Files selected for processing (5)
  • app/__tests__/api/gh1692-timing-safe-keeper-auth.test.ts
  • app/app/api/markets/[slab]/route.ts
  • app/app/api/markets/route.ts
  • app/app/api/oracle-keeper/register/route.ts
  • app/lib/keeper-hmac.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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