Skip to content

fix(server): give every issued challenge a fresh ID - #37

Open
erhnysr wants to merge 1 commit into
stripe:mainfrom
erhnysr:fix/fresh-challenge-id-per-issuance
Open

erhnysr wants to merge 1 commit into
stripe:mainfrom
erhnysr:fix/fresh-challenge-id-per-issuance

Conversation

@erhnysr

@erhnysr erhnysr commented Oct 14, 2026

Copy link
Copy Markdown

Fixes tempoxyz/mpp-tools#260 (AGR-2026-112).

Verify.createChallenge builds the challenge from the realm, method, intent, request, caller metadata and expiry only. With an explicit expires, two issuances for the same route get the same HMAC ID, so a credential paid for one issuance also satisfies any other indistinguishable one. mppx avoids this by adding per-issuance randomness to the HMAC-bound opaque metadata (ChallengeMeta.withNonce in src/server/Mppx.ts) and ignoring that key when it compares route metadata (opaqueValuesMatch).

This does the same:

  • ChallengeMeta.withNonce copies the caller metadata and adds 32 random bytes from SecureRandom under the reserved _mppx_nonce key. Verify.createChallenge uses it, so MppHandler.charge, MppHandler.challenge and ComposedHandler all issue fresh IDs.
  • Verify.opaqueMatches strips that key from both sides before comparing the echoed opaque with the configured metadata. Every other metadata field is still pinned, and the nonce itself is still covered by the challenge HMAC.
  • Challenges issued before this change (no nonce) still verify.

Tests in IntegrationTest:

  • equivalentConcurrentIssuancesGetDistinctChallengeIds: ten concurrent issuances with the same realm, method, intent, request, metadata and explicit expiry get ten distinct IDs. Before the fix: Expected size: 10 but was: 1.
  • issuanceNonceKeepsApplicationMetadata: the issued opaque keeps route and adds the nonce. Before the fix the nonce key is missing.
  • challengesIssuedWithoutANonceStillVerify: a credential for a challenge created without a nonce is still accepted.
  • The existing opaque tests (specOpaqueCredentialPassesRouteBindingAndSplitLifecycle, differentOpaqueMetadataForcesAChallengeBeforeLifecycleHooks) pass unchanged.

./gradlew test passes on JDK 11, 17 and 21, compileExampleJava builds, and ./gradlew integrationTest against the docker-compose Tempo node succeeds.

Challenges created from the same realm, method, intent, request, metadata
and explicit expiry had identical IDs, so a credential for one issuance
also satisfied any other indistinguishable issuance.

Verify.createChallenge now adds 32 random bytes to the HMAC-bound opaque
metadata under the reserved `_mppx_nonce` key, as mppx does. The route
check strips that key before comparing the echoed opaque with the
configured metadata; every other field stays pinned, and challenges
issued without a nonce still verify.
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.

[Agricola] AGR-2026-112: Equivalent challenge issuances can reuse the same challenge ID

1 participant