Skip to content

fix(tempo): support raw session voucher signing#455

Merged
deodad merged 10 commits into
wevm:mainfrom
deodad:dad/p256-session-vouchers
May 26, 2026
Merged

fix(tempo): support raw session voucher signing#455
deodad merged 10 commits into
wevm:mainfrom
deodad:dad/p256-session-vouchers

Conversation

@deodad
Copy link
Copy Markdown
Contributor

@deodad deodad commented May 16, 2026

Summary

Fixes Tempo session voucher signing for access-key accounts and tightens Tempo charge source handling.

Motivation

Access-key session vouchers need to sign the voucher digest as the access-key address. viem 2.51.0 added sign({ hash, raw: true }), which lets mppx produce escrow-compatible raw signatures without requiring callers to pass a separate direct signer.

Changes

  • Replaced Tempo session authorizedSigner options with method-level voucherSigner accounts in src/tempo/client/Session.ts and src/tempo/client/SessionManager.ts; session context no longer accepts a voucher signer.
  • Updated src/tempo/session/Voucher.ts to use raw access-key signing, unwrap legacy secp256k1 keychain signatures, canonicalize envelopes, and reject non-secp256k1 voucher signatures until TIP-1020 escrow verification is enabled.
  • Centralized access-key signer-address resolution in src/tempo/internal/account.ts.
  • Fixed src/tempo/client/Charge.ts to require a chain ID from the challenge or client and use canonical proof sources.
  • Bumped viem to 2.51.0 and kept ox on 0.14.24 with the strict workspace dependency policy.

Testing

  • ./node_modules/.bin/vp test src/tempo/client/Session.test.ts src/tempo/client/SessionManager.test.ts src/tempo/client/ChannelOps.test.ts src/tempo/client/Charge.test.ts src/tempo/session/Voucher.test.ts
  • ./node_modules/.bin/vp test src/tempo/session/Voucher.test.ts src/tempo/client/Session.test.ts src/tempo/client/SessionManager.test.ts src/tempo/client/ChannelOps.test.ts (4 files, 72 tests)
  • CI=true pnpm check:types
  • pnpm check
  • git diff --check

@deodad deodad marked this pull request as ready for review May 16, 2026 17:38
@deodad deodad force-pushed the dad/p256-session-vouchers branch from 6796108 to 1862b7b Compare May 16, 2026 17:49
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/mppx@455

commit: d378cf5

Comment thread src/tempo/client/Session.ts Outdated
type Parameters = Account.getResolver.Parameters &
Client.getResolver.Parameters & {
/** Address authorized to sign vouchers. Defaults to the account address. Use when a separate access key (e.g. secp256k1) signs vouchers while the root account funds the channel. */
/** Address authorized to sign vouchers. Defaults to the voucher signer address, access key address, or account address. */
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Copy Markdown

@tempoxyz-cyclops-bot tempoxyz-cyclops-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👁️ Cyclops Review

PR #455 adds delegated voucherSigner support for tempo/session, threading a separate signing account through session() / sessionManager() / ChannelOps, replacing recoverTypedDataAddress with SignatureEnvelope.verify in verifyVoucher, and extracting signVoucherDigest / normalizeVoucherSignature helpers. The plumbing is coherent inside session(), but the verifier change widens what the server accepts off-chain in ways the on-chain settlement path does not mirror, and a pre-existing channel-recovery branch becomes materially more exploitable when clients reuse a stable delegated signer across servers.


🚨 [SECURITY] Cross-channel voucher signing via unvalidated tryRecoverChannel in auto-managed sessions

Severity: Critical
File: src/tempo/client/Session.ts:166-193 (autoManageCredential recovery branch) — not in this PR's diff, so reported here instead of inline
Also affects: src/tempo/client/ChannelOps.ts:219-241 (tryRecoverChannel discards payee/payer/token/authorizedSigner)

In auto-managed mode, when there is no cached entry for the current challenge's (payee, currency, escrow) key, the client takes suggestedChannelId = context?.channelId ?? md?.channelId from the untrusted 402 challenge and calls tryRecoverChannel(). tryRecoverChannel only uses deposit/finalized/settled and discards payee, payer, token, and authorizedSigner. autoManageCredential then stores the recovered entry under the malicious server's key and signs a voucher for that arbitrary channelId.

Because vouchers sign only (channelId, cumulativeAmount) under the escrow/chain EIP-712 domain — not the HTTP challenge or server identity — a malicious payee (Bob) who has observed an unrelated Alice→Charlie channel can present its channelId in his challenge. Alice's client signs a higher cumulative voucher for the Alice→Charlie channel, which Bob can forward to Charlie to spend Alice's funds. This bug pre-dates the PR but is materially more exploitable now that the PR encourages a stable delegated voucherSigner reused across servers.

Recommended Fix: Have tryRecoverChannel return the full on-chain channel state, and in autoManageCredential validate the recovered channel before signing:

if (onChain.payee.toLowerCase() !== payee.toLowerCase()) throw new Error('payee mismatch')
if (onChain.token.toLowerCase() !== currency.toLowerCase()) throw new Error('token mismatch')
if (onChain.payer.toLowerCase() !== account.address.toLowerCase()) throw new Error('payer mismatch')

The server-side open path already does analogous (payee, token) checks at src/tempo/server/Session.ts:483-506; mirror them on the client recovery path.


The two in-diff verifyVoucher issues (P256 uncollectability and non-canonical-byte canonicalization mismatch) are detailed in an inline comment on src/tempo/session/Voucher.ts.

Reviewer Callouts
  • createOpenPayload latent gap (src/tempo/client/ChannelOps.ts:126-207): when called directly with only voucherSigner set and no authorizedSigner, the on-chain open(...) uses account.address as the authorized signer while the voucher is signed by voucherSigner.address, producing an immediately unspendable channel. Today only Session.ts calls this and always pre-fills authorizedSigner = voucherSigner.address, but the helper is exported. Consider deriving authorizedSigner = options.voucherSigner?.address ?? options.authorizedSigner ?? account.address inside the helper, or asserting the invariant.
  • sessionContextSchema.voucherSigner: z.custom<viem_Account>() (src/tempo/client/Session.ts:31): the custom check is empty, so context-supplied voucherSigner is not shape-validated. Safe today because all mi.context.parse(context) call-sites feed local code, but the schema is fragile if any future transport round-trips context from network input.
  • normalizeVoucherSignature silent-pass behavior: if SignatureEnvelope.from(signature) throws for an unrecognized format, the function returns the original bytes unchanged. Combined with the server never re-canonicalizing inbound bytes, the SDK could emit non-canonical bytes that are parseable elsewhere. Consider failing closed.
  • signVoucherDigest shortcut: the new if (account.sign) return account.sign({ hash }) path bypasses viem's signTypedData action and any EIP-1271 / EIP-6492 / smart-account wrapper logic on a LocalAccount-shaped object. Fine today but a footgun for integrators with custom wrappers.
  • Access-key voucher signing removed: the prior signVoucher(client, accessKeyAccount, …, authorizedSigner = accessKeyAddress) flow now throws via the new accessKeyAddress guard. Confirm no downstream consumer still wires an access-key account into session().

Comment thread src/tempo/session/Voucher.ts
@deodad deodad changed the title feat(tempo): support P256 session vouchers feat(tempo): add direct session voucher signing May 21, 2026
@deodad deodad force-pushed the dad/p256-session-vouchers branch 2 times, most recently from b82f50f to 617ac5a Compare May 21, 2026 18:19
Copy link
Copy Markdown
Collaborator

@brendanjryan brendanjryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@deodad deodad closed this May 26, 2026
@deodad deodad reopened this May 26, 2026
@deodad deodad force-pushed the dad/p256-session-vouchers branch from 617ac5a to 14f5bca Compare May 26, 2026 15:47
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 26, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedviem@​2.50.4 ⏵ 2.51.085 -13100100 +197100
Updatedox@​0.14.22 ⏵ 0.14.24100 +310099 +197 +1100

View full report

@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 26, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
HTTP dependency: npm viem depends on https://pkg.pr.new/ox@386a3439fe1ce76d237930f8c6e6bb493746069a

Dependency: ox@https://pkg.pr.new/ox@386a3439fe1ce76d237930f8c6e6bb493746069a

Location: Package overview

From: examples/charge-wagmi/package.jsonnpm/viem@2.51.0

ℹ Read more on: This package | This alert | What are http dependencies?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Publish the HTTP URL dependency to a public or private package repository and consume it from there.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/viem@2.51.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@deodad deodad changed the title feat(tempo): add direct session voucher signing fix(tempo): support raw session voucher signing May 26, 2026
@deodad deodad closed this May 26, 2026
@deodad deodad reopened this May 26, 2026
@deodad deodad force-pushed the dad/p256-session-vouchers branch from dddb595 to d378cf5 Compare May 26, 2026 18:40
@deodad deodad merged commit 5aed74b into wevm:main May 26, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants