Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/tss/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,8 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
preview?: boolean,
reqId?: IRequestTracer
): Promise<TxRequest> {
// 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,
Expand Down
19 changes: 19 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/BuildParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof AttestationPayload>;

export const BuildParams = t.exact(
t.intersection([
BuildParamsUTXO,
Expand Down Expand Up @@ -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,
}),
])
);
Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
14 changes: 10 additions & 4 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
}
Expand All @@ -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();
}
Expand Down
34 changes: 33 additions & 1 deletion modules/sdk-core/test/unit/bitgo/wallet/BuildParams.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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 () {
Expand All @@ -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 () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

let's just fix the upstream codec first instead of hacking around it

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,
});
});
});