Skip to content

fix(tempo): reject TransferWithMemo logs bound to a different challenge - #22

Open
ygd58 wants to merge 1 commit into
stripe:mainfrom
ygd58:fix/tempo-memo-challenge-binding
Open

fix(tempo): reject TransferWithMemo logs bound to a different challenge#22
ygd58 wants to merge 1 commit into
stripe:mainfrom
ygd58:fix/tempo-memo-challenge-binding

Conversation

@ygd58

Copy link
Copy Markdown

What

matchTransferLogs accepted any TransferWithMemo (or plain Transfer) log that matched currency/recipient/sender/amount, without ever inspecting the memo content. Canonical mppx and mpp-go both require, when a TransferWithMemo log carries the MPP attribution memo layout, that its challenge nonce is bound to the specific challenge being verified (serverFingerprint = keccak256(realm)[0..9], nonce = keccak256(challengeId)[0..6]). This SDK skipped that check entirely, so a transaction whose attribution memo was minted for a different challenge (or realm) — but which otherwise matches this challenge's payment terms exactly — was accepted as satisfying this challenge.

Flagged by the cross-SDK audit as AGR-2026-036. See that thread for the design discussion on scope before I wrote this.

Fix

Adds Attribution.java, ported from wevm/mppx's src/tempo/Attribution.ts (and matching tempoxyz/mpp-go's pkg/tempo/attribution.go, which is why Go was clean in the audit) using BouncyCastle's Keccak.Digest256 — already a main dependency here via TempoRelay.java, so no new dependency needed.

Scope (discussed on #112 first): this only tightens TransferWithMemo handling. A memo that doesn't use the MPP attribution layout at all (an application-defined memo, or no memo/plain Transfer) is left untouched, so existing non-memo payment flows keep working unchanged — this closes the replay path without a breaking change to currently-supported plain-Transfer verification. A follow-up to require attribution memos unconditionally (matching canonical exactly) would be a separate, deliberately-breaking change if that's wanted later.

Testing

  • AttributionTest: direct coverage of the byte-layout verification (bound/unbound realm, bound/unbound challenge, non-attribution memo, malformed input never throws — always returns false rather than propagating an exception).
  • TempoChargeIntentTest: updates the existing transferWithMemoTopicAccepted test to use a properly bound memo (it previously used an arbitrary unbound value and only passed because binding wasn't checked at all — this was masking the exact bug this PR fixes), adds regression tests for memos bound to a different challenge and a different realm, and a test confirming the plain-Transfer (no memo) flow is unaffected.

As with #21 and #18, I couldn't compile/run this against the real com.stripe:stripe-java / BouncyCastle dependencies in my sandbox (no Maven Central access) — please double-check compilation before merge, happy to fix anything that doesn't match.

Fixestempoxyz/mpp-tools#112

matchTransferLogs accepted any TransferWithMemo (or plain Transfer) log
that matched currency/recipient/sender/amount, without ever inspecting
the memo content. Canonical mppx and mpp-go both require, when a
TransferWithMemo log carries the MPP attribution memo layout, that its
challenge nonce is bound to the specific challenge being verified
(server fingerprint = keccak256(realm)[0..9], nonce = keccak256(challengeId)[0..6]).
This SDK skipped that check entirely, so a transaction whose attribution
memo was minted for a *different* challenge (or realm) — but which
otherwise matches this challenge's payment terms exactly — was accepted
as satisfying this challenge.
Adds Attribution.java, ported from wevm/mppx's src/tempo/Attribution.ts
(and matching tempoxyz/mpp-go's pkg/tempo/attribution.go, which is why
Go was clean in the audit) using BouncyCastle's Keccak.Digest256 —
already a main dependency here via TempoRelay.java, so no new
dependency needed.
Scope, per the discussion on the linked issue: this only tightens
TransferWithMemo handling. A memo that doesn't use the MPP attribution
layout at all (an application-defined memo, or no memo/plain Transfer)
is left untouched, so existing non-memo payment flows keep working
unchanged — this closes the replay path without a breaking change to
currently-supported plain-Transfer verification.
Adds:
- AttributionTest: direct coverage of the byte-layout verification
(bound/unbound realm, bound/unbound challenge, non-attribution memo,
malformed input never throws).
- TempoChargeIntentTest: updates the existing transferWithMemoTopicAccepted
test to use a properly bound memo (previously used an arbitrary
unbound value and only happened to pass because binding wasn't
checked at all), adds regression tests for memos bound to a different
challenge and a different realm, and a test confirming the plain
Transfer (no memo) flow is unaffected.
As with stripe#21 and stripe#18, I could not compile/run this against the real
com.stripe:stripe-java / bouncycastle dependencies in my sandbox (no
Maven Central access) — please double-check compilation before merge.
Fixestempoxyz/mpp-tools#112 (AGR-2026-036)
Sign up for freeto 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-036: Tempo hash credentials are accepted without challenge-specific payment binding

1 participant

@ygd58