diff --git a/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts b/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts index 5ca48252d4..822ef5bab4 100644 --- a/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts +++ b/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts @@ -31,6 +31,8 @@ type PreApproveResult = { halfSigned?: string; }; +// TODO(WCN-541): add optional attestation pass-through for PUT /pendingapprovals/:id (deferred +// until multisig attestation, WCN-539, is verified end-to-end on staging). type ApprovePendingApprovalRequestBody = { state: 'approved'; otp: string | undefined; diff --git a/modules/sdk-core/src/bitgo/tss/common.ts b/modules/sdk-core/src/bitgo/tss/common.ts index 7903038740..83ec34bad7 100644 --- a/modules/sdk-core/src/bitgo/tss/common.ts +++ b/modules/sdk-core/src/bitgo/tss/common.ts @@ -111,6 +111,8 @@ export async function sendSignatureShare( const urlPath = '/wallet/' + walletId + '/txrequests/' + txRequestId + addendum + '/signatureshares'; const reqTracer = reqId || new RequestTracer(); bitgo.setRequestTracer(reqTracer); + // TODO(WCN-541): add optional attestation pass-through for MPC /signatureshares, first round + // only (deferred until multisig attestation, WCN-539, is verified end-to-end on staging). return bitgo .post(bitgo.url(urlPath, 2)) .send({ diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index 2b983ccdd7..1d830a3ede 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -514,6 +514,8 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil preview?: boolean, reqId?: IRequestTracer ): Promise { + // TODO(WCN-541): add optional attestation pass-through for MPC /txrequests (deferred until + // multisig attestation, WCN-539, is verified end-to-end on staging). const whitelistedParams = { intent: { ...intentOptions, diff --git a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts index a635b2057b..cc748742a1 100644 --- a/modules/sdk-core/src/bitgo/wallet/BuildParams.ts +++ b/modules/sdk-core/src/bitgo/wallet/BuildParams.ts @@ -64,6 +64,23 @@ export const BuildParamsOffchain = t.partial({ idfUserId: t.unknown, }); +/** + * WebAuthn attestation proving a passkey user signed off on this withdrawal intent. + * Pure pass-through — the SDK does not validate or interpret this payload. + * + * TODO(WCN-541): move this codec to @bitgo/public-types as the shared AttestationPayload + * and add it to the canonical TxSendBody/BuildParams codecs there; this local copy only + * covers the multisig /tx/build, /tx/send, and /tx/initiate paths. + */ +export const AttestationPayload = t.type({ + signature: t.string, + credentialId: t.string, + clientDataJSON: t.string, + authenticatorData: t.string, +}); + +export type AttestationPayload = t.TypeOf; + export const BuildParams = t.exact( t.intersection([ BuildParamsUTXO, @@ -135,6 +152,8 @@ export const BuildParams = t.exact( feeToken: t.unknown, // Bridging parameters for cross-chain operations (e.g., BTC to sBTC) bridgingParams: t.unknown, + // WebAuthn attestation for the withdrawal intent (WCN-539) — pass-through only. + attestation: AttestationPayload, }), ]) ); diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index a9663e6bbc..067660bcb6 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -38,6 +38,7 @@ import { TxRequest, } from '../utils'; import { SerializedNtilde } from '../../account-lib/mpc/tss/ecdsa/types'; +import { AttestationPayload } from './BuildParams'; import { IAddressBook } from '../address-book'; import { WalletUser, AddressQueryResult } from '@bitgo/public-types'; import { SubmitTransactionResponse } from '../inscriptionBuilder'; @@ -203,6 +204,8 @@ export interface PrebuildTransactionOptions { idfUserId?: string; idfVersion?: number; comment?: string; + /** WebAuthn attestation for the withdrawal intent (WCN-539) — pass-through only. */ + attestation?: AttestationPayload; [index: string]: unknown; tokenName?: string; nftCollectionId?: string; @@ -883,6 +886,8 @@ export interface SubmitTransactionOptions { }; comment?: string; txRequestId?: string; + /** WebAuthn attestation for the withdrawal intent (WCN-539) — pass-through only. */ + attestation?: AttestationPayload; } export interface SendOptions { diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index a8c1952cf5..983c9d20c2 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -56,7 +56,7 @@ import { txParamsFromIntent } from '../utils/tss/baseTSSUtils'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa'; import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest'; -import { buildParamKeys, BuildParams } from './BuildParams'; +import { buildParamKeys, BuildParams, AttestationPayload } from './BuildParams'; import { AccelerateTransactionOptions, AddressesByBalanceOptions, @@ -143,7 +143,9 @@ const debug = require('debug')('bitgo:v2:wallet'); type ManageUnspents = 'consolidate' | 'fanout'; -const whitelistedSendParams = TxSendBody.type.types.flatMap((t) => Object.keys(t.props)); +// TODO(WCN-541): 'attestation' is appended locally because @bitgo/public-types' TxSendBody +// codec doesn't declare it yet. Once TxSendBody adds it upstream, drop this local addition. +const whitelistedSendParams = [...TxSendBody.type.types.flatMap((t) => Object.keys(t.props)), 'attestation']; export enum ManageUnspentsOptions { BUILD_ONLY, @@ -5064,10 +5066,13 @@ export class Wallet implements IWallet { const whitelistedParams = this.baseCoin.preprocessBuildParams(_.pick(params, whitelistedSendParams)); const reqTracer = reqId || new RequestTracer(); this.bitgo.setRequestTracer(reqTracer); + // TODO(WCN-541): attestation is added to this local intersection because @bitgo/public-types' + // TxSendBody codec (t.exact) doesn't declare it yet, and t.exact strips undeclared keys on + // encode. Once TxSendBody adds it upstream, drop `attestation` from this local partial. return postWithCodec( this.bitgo, this.baseCoin.url('/wallet/' + this.id() + '/tx/send'), - t.intersection([TxSendBody, t.partial({ locktime: t.number })]), + t.intersection([TxSendBody, t.partial({ locktime: t.number, attestation: AttestationPayload })]), whitelistedParams ).result(); } @@ -5079,10 +5084,11 @@ export class Wallet implements IWallet { const whitelistedParams = this.baseCoin.preprocessBuildParams(_.pick(params, whitelistedSendParams)); const reqTracer = reqId || new RequestTracer(); this.bitgo.setRequestTracer(reqTracer); + // TODO(WCN-541): see sendTransaction — same local-codec workaround for attestation pass-through. return postWithCodec( this.bitgo, this.baseCoin.url('/wallet/' + this.id() + '/tx/initiate'), - TxSendBody, + t.intersection([TxSendBody, t.partial({ attestation: AttestationPayload })]), whitelistedParams ).result(); } diff --git a/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts b/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts index e88da25a62..e1f619b3ba 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { BuildParams } from '../../../../src/bitgo/wallet/BuildParams'; +import { BuildParams, buildParamKeys, AttestationPayload } from '../../../../src/bitgo/wallet/BuildParams'; describe('BuildParams', function () { it('enforces codec', function () { @@ -74,4 +74,36 @@ describe('BuildParams', function () { } ); }); + + it('should whitelist attestation (WCN-539) while stripping unrelated unknown params', function () { + const attestation = { + signature: 'sig', + credentialId: 'cred-id', + clientDataJSON: 'client-data', + authenticatorData: 'auth-data', + }; + assert.deepStrictEqual( + BuildParams.encode({ + recipients: [{ amount: '10000', address: '2N9Ego9KidiZR8tMP82g6RaggQtcbR9zNzH' }], + attestation, + unknownField: 'should be stripped', + } as any), + { + recipients: [{ amount: '10000', address: '2N9Ego9KidiZR8tMP82g6RaggQtcbR9zNzH' }], + attestation, + } + ); + assert.ok(buildParamKeys.includes('attestation'), 'buildParamKeys must include attestation'); + }); + + it('AttestationPayload codec requires all four fields', function () { + const valid = { + signature: 'sig', + credentialId: 'cred-id', + clientDataJSON: 'client-data', + authenticatorData: 'auth-data', + }; + assert.strictEqual(AttestationPayload.is(valid), true); + assert.strictEqual(AttestationPayload.is({ ...valid, signature: undefined }), false); + }); }); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts b/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts index c1030ff859..de94f275b1 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/SendTransactionRequest.ts @@ -1,5 +1,7 @@ import * as assert from 'assert'; +import * as t from 'io-ts'; import { TxSendBody } from '@bitgo/public-types'; +import { AttestationPayload } from '../../../../src/bitgo/wallet/BuildParams'; describe('SendTransactionRequest', function () { it('enforces codec', function () { @@ -24,4 +26,31 @@ describe('SendTransactionRequest', function () { addressType: 'p2sh', }); }); + + it('TxSendBody alone drops attestation (upstream codec has no such field yet — TODO(WCN-541))', function () { + assert.deepStrictEqual( + TxSendBody.encode({ + txHex: '00', + attestation: { signature: 'sig', credentialId: 'c', clientDataJSON: 'cd', authenticatorData: 'ad' }, + } as any), + { txHex: '00' } + ); + }); + + it('the local intersection used by wallet.sendTransaction/initiateTransaction preserves attestation', function () { + // Mirrors the codec built inline in Wallet#sendTransaction/#initiateTransaction (WCN-539): + // TxSendBody is `t.exact` and strips unknown keys, so attestation is re-added via a sibling + // t.partial in the same intersection until @bitgo/public-types declares it upstream. + // + // Note: by the time this codec runs, the caller has already done + // `_.pick(params, whitelistedSendParams)`, so the object passed to `.encode()` never carries + // arbitrary unknown keys in production — this test reflects that, not raw request bodies. + const attestation = { signature: 'sig', credentialId: 'c', clientDataJSON: 'cd', authenticatorData: 'ad' }; + const sendBodyWithAttestation = t.intersection([TxSendBody, t.partial({ attestation: AttestationPayload })]); + + assert.deepStrictEqual(sendBodyWithAttestation.encode({ txHex: '00', attestation } as any), { + txHex: '00', + attestation, + }); + }); });