Skip to content

feat(transfer): migrate KMS signing to challenge-bound WebAuthn ceremony#356

Merged
jhfnetboy merged 2 commits into
masterfrom
feat/transfer-webauthn-ceremony
Jun 22, 2026
Merged

feat(transfer): migrate KMS signing to challenge-bound WebAuthn ceremony#356
jhfnetboy merged 2 commits into
masterfrom
feat/transfer-webauthn-ceremony

Conversation

@jhfnetboy

Copy link
Copy Markdown
Member

Background — the transfer 500

The old path sent a legacy raw passkey assertion to KMS /SignHash. KMS v0.20.0+ rejects it (replayable, no challenge binding) → the root cause of executeTransfer failures (esp. Tier-2 BLS). Fix per @aastar/sdk@0.26.1 + the #354 handoff.

The SDK migrated signing to a challenge-bound WebAuthn ceremony (signHashWithWebAuthn, WYSIWYS commitment). YAA now wires the SDK's KmsSignerAdapter and forwards the ceremony assertion.

Changes

Deps: @aastar/sdk ^0.24.2^0.26.1 (both workspaces; lockfile bumped surgically incl. the backend's nested copy — identical dep tree).

Backend

  • kms.service: createSignerAdapter(resolveKey)new KmsSignerAdapter(kms, resolveKey)
  • auth.service: resolveKmsKey(userId){ keyId, address }
  • sdk.providers: signer = KmsSignerAdapter(...); inject KmsService + AuthService (drop legacy BackendSignerAdapter)
  • remove dead BackendSignerAdapter (module/index/file)
  • execute-transfer.dto: add WebAuthnAssertionDto { ChallengeId, Credential }; deprecate passkeyAssertion
  • transfer.service: require webAuthnAssertion; pass it + useAirAccountTiering to tiered & BLS-fallback calls

Frontend

  • transfer/page: drop extractLegacyAssertion; send raw { ChallengeId, Credential } as webAuthnAssertion (begin→get order — already correct)
  • api.ts: execute payload type → webAuthnAssertion

Deferred (per #354 §5–6, not needed for basic transfer)

  • commitChallenge device-passkey path — SDK auto-binds commitment on the server-held key path, so the KMS strict flip is zero-change here
  • agent/session key create+refresh, 24h TTL re-mint — only if using agent-key gasless sponsorship

Verification

  • ✅ backend type-check, frontend type-check, backend build, lint (1 pre-existing unrelated warning)
  • On-chain acceptance (needs a device passkey): Sepolia Tier-2 transfer → KMS /SignHash 200 + on-chain UserOperationEvent success=true

KMS v0.20.0+ rejects the legacy raw passkey assertion (replayable, no
challenge binding) — the root cause of the transfer 500 (Tier-2 BLS).
Migrate to the SDK's KmsSignerAdapter, which carries a per-call,
challenge-bound WebAuthn ceremony assertion through to KMS /SignHash
(WYSIWYS commitment, replay-safe). Per aastar-sdk@0.26.1 + #354 handoff.

Backend:
- kms.service: add createSignerAdapter(resolveKey) → KmsSignerAdapter
- auth.service: add resolveKmsKey(userId) → { keyId, address }
- sdk.providers: wire signer = KmsSignerAdapter(kms, resolveKey); inject
  KmsService + AuthService instead of the legacy BackendSignerAdapter
- remove dead BackendSignerAdapter (sdk.module/index + file)
- execute-transfer.dto: add WebAuthnAssertionDto { ChallengeId, Credential };
  deprecate passkeyAssertion (optional, transition only)
- transfer.service: require webAuthnAssertion; pass it (+ useAirAccountTiering)
  to both the tiered and BLS-fallback executeTransfer calls

Frontend:
- transfer page: drop extractLegacyAssertion; send the raw ceremony
  { ChallengeId, Credential } as webAuthnAssertion (begin→get order)
- api.ts: execute payload type → webAuthnAssertion

Deps: @aastar/sdk ^0.24.2 → ^0.26.1 (both workspaces; lockfile incl. the
backend's nested copy). Identical dep tree, so surgical lockfile bump.

Deferred (per #354 §5-6, not needed for basic transfer): commitChallenge
device-passkey path (SDK auto-binds commitment on the server-held key path,
so strict flip is zero-change here), agent/session key create+refresh,
24h TTL re-mint.

Verification: backend+frontend type-check, backend build, lint all green.
On-chain Sepolia Tier-2 acceptance to follow (needs a device passkey).
@jhfnetboy jhfnetboy requested a review from fanhousanbu as a code owner June 22, 2026 07:17
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

The dev-board KMS is in strict mode, which requires the WebAuthn challenge
to be the WYSIWYS commitment SHA-256(nonce || payload) — not a bare nonce.
The payload (userOpHash) is only known after the SDK builds the UserOp, so a
one-shot executeTransfer can't work for the browser device-passkey path
(ordering inversion). @aastar/sdk@0.26.2 adds the two-phase API; YAA wires it.

Flow (per #354 guide):
  prepare  → SDK builds UserOp, derives tier-aware payload, calls KMS
             BeginAuthentication, returns publicKeyOptions whose challenge is
             already the SDK-computed commitment (+ transferId, challengeId)
  ceremony → browser device passkey signs that commitment verbatim
  submit   → SDK signs the matching digest + submits; KMS accepts under strict

YAA never computes commitChallenge, never selects the payload, never builds the
UserOp — all owned by the SDK.

Backend:
- transfer.service: replace executeTransfer/Inner with prepareTransfer +
  submitPreparedTransfer (useAirAccountTiering: true required); keep PMv4 gas-token
  resolution in resolvePaymasterToken; address-book recording uses result.to
- transfer.controller: /transfer/execute → /transfer/prepare + /transfer/submit
- dto: add PrepareTransferDto + SubmitTransferDto; remove execute-transfer.dto

Frontend:
- transfer page: prepare → startAuthentication(prep.publicKeyOptions) → submit;
  the frontend no longer calls KMS directly (the SDK does BeginAuthentication in
  prepare). Drop the now-unused kmsClient import.
- api.ts: transferAPI.execute → prepare + submit

Deps: @aastar/sdk ^0.26.1 → ^0.26.2 (both workspaces + lockfile root & nested).

Verification: backend+frontend type-check, backend build, lint all green.
On-chain Sepolia Tier-2 acceptance (strict): /SignHash 200 + success=true — to follow.

@clestons clestons left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

clestons review — #356 [4-round: DeepSeek R1a → Opus R2 (cross-system) → Codex R3 PK → Opus R4]

feat(transfer): migrate KMS signing to challenge-bound WebAuthn ceremony — the YAA app-layer integration of the SDK's two-phase transfer (consumes @aastar/sdk 0.26.x, the prepareTransfer/submitPreparedTransfer + KmsSignerAdapter I verified in aastar-sdk#143). 17 files, +272/-294. The crypto/WYSIWYS lives in the SDK (verified); YAA's job is correct wiring + no legacy bypass.

Wiring — correct ✅

  • Transfer is now exclusively two-phase ceremony: the controller exposes only @Post("prepare") / @Post("submit") — the old one-shot executeTransfer endpoint is removed, BackendSignerAdapter (raw-passkey, KMS-rejected) is deleted, and there's no functional passkeyAssertion path. Every transfer goes through the challenge-bound ceremony (Codex C1).
  • userId from req.user.sub (JWT), not a body field; resolveKmsKey(userId) resolves the key via findUserById(userId) → only the authenticated user's own kmsKeyId/walletAddress (no cross-user) (Codex C2, verified).
  • SDK contract matched: assertion forwarded as { ChallengeId, Credential }, useAirAccountTiering: true set (required — a one-time device-passkey can't do non-tiered BLS dual-sign) (Codex C3).
  • KmsSignerAdapter wired via createSignerAdapter(resolveKey); dep @aastar/sdk bumped (lockfile shows a single @aastar/sdk 0.26.2 — no duplicate/runtime-mixed SDK).

Whole-system check (this is where I looked beyond the diff)

Codex flagged "any OTHER live legacy signing path?" — I grepped the backend:

  • kms.service.signHashWithAssertion(LegacyPasskeyAssertion) and auth.service.getUserWallet() → legacy createKmsSigner(assertionProvider) still EXIST, but both have zero live callers (orphaned by this migration). So they're dead code, not a live raw-passkey path — no security risk, but please delete them (you removed BackendSignerAdapter; these two legacy KMS-signing methods are the same vintage and should go too, to remove the future-misuse surface).

Minor

  • DeepSeek [Medium]: the address-book recording uses result.to, which may be undefined from the submit response — it's after the on-chain submit, so cosmetic (bookkeeping), not fund-routing. Guard with the original dto.to fallback.
  • Frontend transfer path goes through the server (/transfer/prepare+/submit); the lib/yaaa.ts kmsClient (direct-KMS-via-proxy) is for the separate guardian-recovery flow, not transfers.

Verdict

APPROVE. The KMS-signing migration is functionally complete and correct: transfers run exclusively through the SDK's challenge-bound two-phase ceremony, there's no live legacy raw-passkey path, the userId is JWT-scoped, key resolution is user-scoped, and the wiring matches the SDK contract (the crypto being verified in #143). This closes the transfer-500 root cause end-to-end (TA → SDK → YAA). Cleanup before/after merge: delete the now-orphaned signHashWithAssertion / getUserWallet+legacy createKmsSigner (dead code), and add the result.to fallback. Merge is the maintainer's call.

@jhfnetboy jhfnetboy merged commit d611f8d into master Jun 22, 2026
11 of 14 checks passed
@jhfnetboy jhfnetboy deleted the feat/transfer-webauthn-ceremony branch June 22, 2026 09:20
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants