Skip to content

feat: refuse developer JWTs whose signer was disabled - #120

Open
zer0stars wants to merge 12 commits into
mainfrom
feat/signer-check
Open

zer0stars wants to merge 12 commits into
mainfrom
feat/signer-check

Conversation

@zer0stars

@zer0stars zer0stars commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Why

Disabling a license signer (API key) stops dex from minting new developer JWTs with it, but tokens it already minted keep exchanging for vehicle JWTs for their full 336 hours. The console's team feature needs a removed member cut off within about 10 minutes, the vehicle JWT lifetime. dex now stamps the signer on developer JWTs as signer_address (DIMO-Network/dex#332).

What

  • /v1/tokens/exchange: checks every token with signer_address whose ethereum_address is a developer license (Identity), whatever its audience.
    • Before, tokens issued to the mobile client (aud dimo-driver) skipped the dev-license middleware, and the grant uses ethereum_address, so an audience-based check could be bypassed.
    • Disabled signer: 403 signer no longer authorized for this license.
    • Identity or chain failure: 503 could not verify signer.
    • Tokens without the claim (minted before dex set it, and every DIMO Mobile user's token) pass with no lookup at all, so the mobile path never touches Identity.
  • licensesigner.Checker: Identity lookup, then isSigner(signer) on the license account (new abigen binding).
    • 60 s cache for positive and negative answers, bounded at 10,000 entries; errors not cached;
    • singleflight for concurrent misses;
    • 3 s timeout per call.
  • gRPC SignerCheck(license, signer) → is_signer: shares that cache. It answers true for non-licenses, InvalidArgument for non-hex input, and Unavailable on errors.
  • pkg/signercheck: shared by vehicle-triggers-api, tesla-oracle and credit-tracker. It holds SIGNER_CHECK_MODE (enforce|log|off), the signer_check_total{service,result} metric, the Fiber middleware, and a SignerCheck client with a 5 s deadline.
  • SIGNER_CHECK_MODE: log in dev and prod values; production switches to enforce after a week.
  • Alert SignerCheckErrors (error above 1% of non-skipped checks, allowed + denied + error, over 5 min) as a PrometheusRule.
  • README documents settings, metric, alert and rollback.

Effect on existing developers

In enforce mode, disabling an API key ends its tokens within about a minute instead of two weeks.

Successor services

dauth and vt aren't live. If either ships, it needs the same check (use pkg/signercheck).

Tests

  • pkg/signercheck: every mode and result, claimless tokens skipped without a check, malformed claims, metric counts, gRPC client with timeout.
  • licensesigner: call shape, TTL for both answers, not-a-license cached, errors not cached, keyed, bounded, 3 s timeout, singleflight.
  • TestSignerCheckOnExchange: mobile-audience token for a license is checked; for a non-license it passes.
  • TestSignerCheck (gRPC).
  • make generate is clean.

Merge order

Merge after DIMO-Network/dex#332. Merging deploys to dev in log mode, which refuses nothing. vehicle-triggers-api, tesla-oracle and credit-tracker import pkg/signercheck from this branch until it merges.

… Identity is down

Once SIGNER_CLAIM_REQUIRED_AFTER is set, every claimless token issued after it
needs the license lookup, including every mobile user's token exchange. An
Identity outage then refused all of them with 503. The cutoff only backstops a
dex path that forgets the claim, so a failed lookup now counts as error and
lets the request through. Tokens that carry signer_address still fail closed.
@zer0stars

Copy link
Copy Markdown
Member Author

Final review change (9d1a6d8): with SIGNER_CLAIM_REQUIRED_AFTER set, a claimless token issued after it whose license lookup fails (Identity down or slower than 3 s) now passes and counts as error, instead of getting 503. Every mobile user's exchange is claimless and would need that lookup, so the old behavior turned an Identity outage into a mobile outage. Tokens that carry signer_address still fail closed (503 on errors). The README's settings table says so.

The cutoff only backstopped a hypothetical dex path that forgets the claim, and
once set it made every claimless token exchange, every DIMO Mobile user's
included, look up Identity. Tokens without signer_address are now never
checked, with no Identity lookup. This also removes the fail-open handling of
lookup errors added in 9d1a6d8: tokens that carry signer_address still fail
closed.
@zer0stars

Copy link
Copy Markdown
Member Author

Superseding my earlier comment: SIGNER_CLAIM_REQUIRED_AFTER is dropped entirely (24b46c9). It only backstopped a hypothetical dex path that forgets the claim, and once set it would have made every claimless exchange, DIMO Mobile users' included, look up Identity. Tokens without signer_address are never checked and never looked up; tokens with it still fail closed (503 on errors). The fail-open change from 9d1a6d8 is gone with it.

Timeout bounds each of the two calls a cold check makes in turn (Identity,
then isSigner), so at 3 s a slow check could run about 6 s and outlast the
5-second deadline that vehicle-triggers-api and tesla-oracle set, turning
into a 503 in enforce mode. At 2 s the worst case is 4 s. A test pins
2*Timeout below signercheck.GRPCTimeout.
A failed check (Identity or chain unreachable, timeout) went back to the
caller as codes.Unavailable with nothing in token-exchange-api's logs.
Log it at warn with the license, the signer and the error.
credit-tracker isn't part of this rollout, so it is no longer listed as a
SignerCheck caller or a rollback step. prometheus-operator's own service
target label wins at scrape time, which renames the app's service label to
exported_service; the README now says so and filters on it.
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