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
73 changes: 73 additions & 0 deletions modules/bitgo/test/v2/unit/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4643,6 +4643,13 @@ describe('V2 Wallet:', function () {
});
});

const exampleAttestation = {
signature: 'sig',
credentialId: 'cred',
clientDataJSON: 'cdj',
authenticatorData: 'ad',
};

describe('getUserKeyAndSignTssTransaction', function () {
['eddsa', 'ecdsa'].forEach((keyCurve: string) => {
describe(keyCurve, () => {
Expand Down Expand Up @@ -4681,6 +4688,29 @@ describe('V2 Wallet:', function () {

getKeysStub.calledOnce.should.be.true();
signTransactionStub.calledOnce.should.be.true();
signTransactionStub.firstCall.args[0].should.not.have.property('attestation');
});

it('should forward the attestation to signTransaction', async function () {
getKeysStub.resolves([
{
commonKeychain: 'test',
id: '',
pub: '',
type: 'tss',
encryptedPrv:
'{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
},
]);

await wallet.getUserKeyAndSignTssTransaction({
walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE as string,
txRequestId: 'id',
attestation: exampleAttestation,
});

signTransactionStub.calledOnce.should.be.true();
signTransactionStub.firstCall.args[0].should.have.property('attestation', exampleAttestation);
});

it('should throw if the keychain doesnt have the encryptedKey', async function () {
Expand Down Expand Up @@ -4770,6 +4800,49 @@ describe('V2 Wallet:', function () {
sandbox.assert.calledOnce(getUserKeyAndSignTssTxSpy);
signedTx.should.deepEqual(exampleSignedTx);
});

it('should forward the attestation to first-round signing only, not the lite submit', async function () {
const deleteSignatureSharesSpy = sandbox.stub(TssUtils.prototype, 'deleteSignatureShares');
const getUserKeyAndSignTssTxSpy = sandbox.stub(tssSolWallet, 'getUserKeyAndSignTssTransaction');
getUserKeyAndSignTssTxSpy.resolves(exampleSignedTx);
const submitTxSpy = sandbox.stub(tssSolWallet, 'submitTransaction');
submitTxSpy.resolves(exampleSignedTx);

await tssSolWallet.signAndSendTxRequest({
walletPassphrase: 'passphrase',
txRequestId: 'id',
isTxRequestFull: false,
attestation: exampleAttestation,
});

sandbox.assert.callOrder(deleteSignatureSharesSpy, getUserKeyAndSignTssTxSpy, submitTxSpy);
sandbox.assert.calledOnce(getUserKeyAndSignTssTxSpy);
sandbox.assert.calledWithExactly(getUserKeyAndSignTssTxSpy, {
walletPassphrase: 'passphrase',
txRequestId: 'id',
attestation: exampleAttestation,
});
sandbox.assert.calledOnce(submitTxSpy);
sandbox.assert.calledWithExactly(submitTxSpy, { txRequestId: 'id' });
});

it('should not send an attestation key when none is provided', async function () {
sandbox.stub(TssUtils.prototype, 'deleteSignatureShares');
const getUserKeyAndSignTssTxSpy = sandbox.stub(tssSolWallet, 'getUserKeyAndSignTssTransaction');
getUserKeyAndSignTssTxSpy.resolves(exampleSignedTx);

await tssSolWallet.signAndSendTxRequest({
walletPassphrase: 'passphrase',
txRequestId: 'id',
isTxRequestFull: true,
});

sandbox.assert.calledOnce(getUserKeyAndSignTssTxSpy);
sandbox.assert.calledWithExactly(getUserKeyAndSignTssTxSpy, {
walletPassphrase: 'passphrase',
txRequestId: 'id',
});
});
});

describe('Message Signing', function () {
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 @@ -470,6 +470,11 @@ export interface SignAndSendTxRequestOptions {
txRequestId: string;
walletPassphrase: string;
isTxRequestFull: boolean;
/**
* Optional WebAuthn proof from the current signer for the request's withdrawal intent;
* pass-through only. Sent on the first signing round only, never on the lite submit.
*/
attestation?: AttestationPayload;
}

export interface SignAndSendMessageTxRequestOptions {
Expand Down
9 changes: 8 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa';
import { RedpallasMPCv2Utils } from '../utils/tss/redpallas';
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
import { buildParamKeys, BuildParams, ERC7984_INTENT_TX_TYPES } from './BuildParams';
import { AttestationPayload, buildParamKeys, BuildParams, ERC7984_INTENT_TX_TYPES } from './BuildParams';
import {
fetchRootKeychainForSafeChild,
isSafeChildPublicOnlyKeychain,
Expand Down Expand Up @@ -2315,9 +2315,12 @@ export class Wallet implements IWallet {
async getUserKeyAndSignTssTransaction({
txRequestId,
walletPassphrase,
attestation,
}: {
txRequestId: string;
walletPassphrase: string;
/** Optional WebAuthn proof for the first TSS withdrawal-signing round; pass-through only. */
attestation?: AttestationPayload;
}): Promise<SignedTransaction> {
if (this._wallet.multisigType !== 'tss') {
throw new Error('getUserKeyAndSignTssTransaction is only supported for TSS wallets');
Expand All @@ -2338,6 +2341,7 @@ export class Wallet implements IWallet {
walletPassphrase,
reqId,
keychain: userKeychain,
...(attestation !== undefined && { attestation }),
});
}

Expand Down Expand Up @@ -5149,9 +5153,12 @@ export class Wallet implements IWallet {
public async signAndSendTxRequest(params: SignAndSendTxRequestOptions): Promise<SignedTransaction> {
await this.tssUtils?.deleteSignatureShares(params.txRequestId);

// Clearing shares above resets the request to its first signing round, so the attestation
// rides on round one. It is not sent with the lite submit below: round one consumes its nonce.
const ret = await this.getUserKeyAndSignTssTransaction({
walletPassphrase: params.walletPassphrase,
txRequestId: params.txRequestId,
...(params.attestation !== undefined && { attestation: params.attestation }),
});
if (!params.isTxRequestFull) {
// It is assumed that if its not a full tx request, then it is a lite tx request
Expand Down
Loading