Skip to content

Commit 18a642a

Browse files
feat(sdk-core): add webauthnInfo support to createMpc
Add MpcWebauthnInfo parameter to CreateMpcOptions and thread it through all four createKeychains implementations (EDDSA, EdDSA MPCv2, ECDSA, ECDSA MPCv2). When provided, the user keychain is registered with an additional webauthnDevices entry containing a PRF-encrypted copy of the user private share, avoiding the need for a separate PUT /key/{id} call post-wallet-creation. Also wires webauthnInfo from generateWallet through generateMpcWallet so TSS wallet creation passes the authenticator info end-to-end. Ticket: WAL-761
1 parent 4b63462 commit 18a642a

12 files changed

Lines changed: 155 additions & 20 deletions

File tree

modules/bitgo/test/v2/unit/keychains.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,51 @@ describe('V2 Keychains', function () {
434434
keychains.should.deepEqual(stubbedKeychainsTriplet);
435435
});
436436
});
437+
438+
['tsol'].forEach((coin) => {
439+
it('should pass webauthnInfo to createKeychains for EDDSA TSS', async function () {
440+
const webauthnInfo = {
441+
otpDeviceId: 'device-1',
442+
prfSalt: 'salt-1',
443+
passphrase: 'prf-derived-passphrase',
444+
};
445+
const createKeychains = sandbox
446+
.stub(EDDSAUtils.default.prototype, 'createKeychains')
447+
.resolves(stubbedKeychainsTriplet);
448+
await bitgo.coin(coin).keychains().createMpc({
449+
multisigType: 'tss',
450+
passphrase: 'password',
451+
enterprise: 'enterprise',
452+
webauthnInfo,
453+
});
454+
createKeychains.calledOnce.should.be.true();
455+
createKeychains.firstCall.args[0].should.have.property('webauthnInfo', webauthnInfo);
456+
});
457+
});
458+
459+
['tbsc'].forEach((coin) => {
460+
it('should pass webauthnInfo to createKeychains for ECDSA TSS', async function () {
461+
nock(bgUrl).get('/api/v2/tss/settings').reply(200, {
462+
coinSettings: {},
463+
});
464+
const webauthnInfo = {
465+
otpDeviceId: 'device-1',
466+
prfSalt: 'salt-1',
467+
passphrase: 'prf-derived-passphrase',
468+
};
469+
const createKeychains = sandbox
470+
.stub(ECDSAUtils.EcdsaUtils.prototype, 'createKeychains')
471+
.resolves(stubbedKeychainsTriplet);
472+
await bitgo.coin(coin).keychains().createMpc({
473+
multisigType: 'tss',
474+
passphrase: 'password',
475+
enterprise: 'enterprise',
476+
webauthnInfo,
477+
});
478+
createKeychains.calledOnce.should.be.true();
479+
createKeychains.firstCall.args[0].should.have.property('webauthnInfo', webauthnInfo);
480+
});
481+
});
437482
});
438483

439484
describe('Recreate Keychains from MPCV1 to MPCV2', async function () {

modules/sdk-core/src/bitgo/keychain/iKeychains.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export interface WebauthnInfo {
1212
encryptedPrv: string;
1313
}
1414

15+
/** WebAuthn PRF-based encryption info passed to MPC key creation. The passphrase is the
16+
* PRF-derived key used to encrypt the user private share before it is persisted. */
17+
export interface MpcWebauthnInfo {
18+
otpDeviceId: string;
19+
prfSalt: string;
20+
passphrase: string;
21+
}
22+
1523
export type SourceType = 'bitgo' | 'backup' | 'user' | 'cold';
1624

1725
export type WebauthnFmt = 'none' | 'packed' | 'fido-u2f';
@@ -188,6 +196,7 @@ export interface CreateMpcOptions {
188196
originalPasscodeEncryptionCode?: string;
189197
enterprise?: string;
190198
retrofit?: DecryptedRetrofitPayload;
199+
webauthnInfo?: MpcWebauthnInfo;
191200
}
192201

193202
export interface RecreateMpcOptions extends Omit<CreateMpcOptions, 'retrofit' | 'multisigType'> {

modules/sdk-core/src/bitgo/keychain/keychains.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export class Keychains implements IKeychains {
326326
enterprise: params.enterprise,
327327
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
328328
retrofit: params.retrofit,
329+
webauthnInfo: params.webauthnInfo,
329330
});
330331
}
331332

modules/sdk-core/src/bitgo/utils/mpcUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import assert from 'assert';
55
import { decrypt, readMessage, readPrivateKey, SerializedKeyPair } from 'openpgp';
66
import { IBaseCoin, KeychainsTriplet } from '../baseCoin';
77
import { BitGoBase } from '../bitgoBase';
8-
import { AddKeychainOptions, Keychain, KeyType } from '../keychain';
8+
import { AddKeychainOptions, Keychain, KeyType, MpcWebauthnInfo } from '../keychain';
99
import { encryptText, getBitgoGpgPubKey } from './opengpgUtils';
1010
import {
1111
IntentRecipient,
@@ -105,6 +105,7 @@ export abstract class MpcUtils {
105105
passphrase: string;
106106
enterprise?: string;
107107
originalPasscodeEncryptionCode?: string;
108+
webauthnInfo?: MpcWebauthnInfo;
108109
}): Promise<KeychainsTriplet>;
109110

110111
/**

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as openpgp from 'openpgp';
33
import { Key, readKey, SerializedKeyPair } from 'openpgp';
44
import { IBaseCoin, KeychainsTriplet } from '../../baseCoin';
55
import { BitGoBase } from '../../bitgoBase';
6-
import { Keychain, KeyIndices } from '../../keychain';
6+
import { Keychain, KeyIndices, MpcWebauthnInfo } from '../../keychain';
77
import { getTxRequest } from '../../tss';
88
import { IWallet } from '../../wallet';
99
import { MpcUtils } from '../mpcUtils';
@@ -194,6 +194,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
194194
enterprise?: string | undefined;
195195
originalPasscodeEncryptionCode?: string | undefined;
196196
isThirdPartyBackup?: boolean;
197+
webauthnInfo?: MpcWebauthnInfo;
197198
}): Promise<KeychainsTriplet> {
198199
throw new Error('Method not implemented.');
199200
}

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Key, SerializedKeyPair } from 'openpgp';
22
import { IRequestTracer } from '../../../api';
33
import { KeychainsTriplet, ParsedTransaction, TransactionParams } from '../../baseCoin';
4-
import { ApiKeyShare, Keychain } from '../../keychain';
4+
import { ApiKeyShare, Keychain, MpcWebauthnInfo } from '../../keychain';
55
import { ApiVersion, Memo, WalletType } from '../../wallet';
66
import { EDDSA, GShare, Signature, SignShare } from '../../../account-lib/mpc/tss';
77
import { Signature as EcdsaSignature } from '../../../account-lib/mpc/tss/ecdsa/types';
@@ -482,6 +482,7 @@ export type CreateKeychainParamsBase = {
482482
passphrase?: string;
483483
enterprise?: string;
484484
originalPasscodeEncryptionCode?: string;
485+
webauthnInfo?: MpcWebauthnInfo;
485486
};
486487

487488
export type CreateBitGoKeychainParamsBase = Omit<CreateKeychainParamsBase, 'bitgoKeychain'>;

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes, hexToBigInt, minModulu
77
import { bip32 } from '@bitgo/utxo-lib';
88

99
import { ECDSA, Ecdsa } from '../../../../account-lib/mpc/tss';
10-
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
10+
import { AddKeychainOptions, Keychain, KeyType, MpcWebauthnInfo } from '../../../keychain';
1111
import ECDSAMethods, { ECDSAMethodTypes } from '../../../tss/ecdsa';
1212
import { KeychainsTriplet } from '../../../baseCoin';
1313
import {
@@ -106,6 +106,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
106106
passphrase: string;
107107
enterprise?: string | undefined;
108108
originalPasscodeEncryptionCode?: string | undefined;
109+
webauthnInfo?: MpcWebauthnInfo;
109110
}): Promise<KeychainsTriplet> {
110111
const MPC = new Ecdsa();
111112
const m = 2;
@@ -138,6 +139,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
138139
bitgoKeychain,
139140
passphrase: params.passphrase,
140141
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
142+
webauthnInfo: params.webauthnInfo,
141143
});
142144
const backupKeychainPromise = this.createBackupKeychain({
143145
userGpgKey,
@@ -177,6 +179,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
177179
bitgoKeychain,
178180
passphrase,
179181
originalPasscodeEncryptionCode,
182+
webauthnInfo,
180183
}: CreateEcdsaKeychainParams): Promise<Keychain> {
181184
if (!passphrase) {
182185
throw new Error('Please provide a wallet passphrase');
@@ -191,7 +194,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
191194
backupKeyShare.userHeldKeyShare,
192195
bitgoKeychain,
193196
passphrase,
194-
originalPasscodeEncryptionCode
197+
originalPasscodeEncryptionCode,
198+
webauthnInfo
195199
);
196200
}
197201

@@ -304,7 +308,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
304308
backupKeyShare: KeyShare,
305309
bitgoKeychain: Keychain,
306310
passphrase: string,
307-
originalPasscodeEncryptionCode?: string
311+
originalPasscodeEncryptionCode?: string,
312+
webauthnInfo?: MpcWebauthnInfo
308313
): Promise<Keychain> {
309314
const bitgoKeyShares = bitgoKeychain.keyShares;
310315
if (!bitgoKeyShares) {
@@ -389,7 +394,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
389394
);
390395

391396
const prv = JSON.stringify(recipientCombinedKey.signingMaterial);
392-
const recipientKeychainParams = {
397+
const recipientKeychainParams: AddKeychainOptions = {
393398
source: recipient,
394399
keyType: 'tss' as KeyType,
395400
commonKeychain: bitgoKeychain.commonKeychain,
@@ -401,6 +406,19 @@ export class EcdsaUtils extends BaseEcdsaUtils {
401406
originalPasscodeEncryptionCode,
402407
};
403408

409+
if (webauthnInfo && recipientIndex === 1) {
410+
recipientKeychainParams.webauthnDevices = [
411+
{
412+
otpDeviceId: webauthnInfo.otpDeviceId,
413+
prfSalt: webauthnInfo.prfSalt,
414+
encryptedPrv: this.bitgo.encrypt({
415+
input: prv,
416+
password: webauthnInfo.passphrase,
417+
}),
418+
},
419+
];
420+
}
421+
404422
const keychains = this.baseCoin.keychains();
405423
return recipientIndex === 1
406424
? await keychains.add(recipientKeychainParams)

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
} from '@bitgo/public-types';
2121

2222
import { Ecdsa } from '../../../../account-lib';
23-
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
23+
import { AddKeychainOptions, Keychain, KeyType, MpcWebauthnInfo } from '../../../keychain';
2424
import { DecryptedRetrofitPayload } from '../../../keychain/iKeychains';
2525
import { ECDSAMethodTypes, getTxRequest } from '../../../tss';
2626
import { sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
@@ -57,6 +57,7 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
5757
enterprise: string;
5858
originalPasscodeEncryptionCode?: string;
5959
retrofit?: DecryptedRetrofitPayload;
60+
webauthnInfo?: MpcWebauthnInfo;
6061
}): Promise<KeychainsTriplet> {
6162
const { userSession, backupSession } = this.getUserAndBackupSession(2, 3, params.retrofit);
6263
const userGpgKey = await generateGPGKeyPair('secp256k1');
@@ -318,7 +319,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
318319
userPrivateMaterial,
319320
userReducedPrivateMaterial,
320321
params.passphrase,
321-
params.originalPasscodeEncryptionCode
322+
params.originalPasscodeEncryptionCode,
323+
params.webauthnInfo
322324
);
323325
const backupKeychainPromise = this.addBackupKeychain(
324326
bitgoCommonKeychain,
@@ -350,20 +352,23 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
350352
privateMaterial?: Buffer,
351353
reducedPrivateMaterial?: Buffer,
352354
passphrase?: string,
353-
originalPasscodeEncryptionCode?: string
355+
originalPasscodeEncryptionCode?: string,
356+
webauthnInfo?: MpcWebauthnInfo
354357
): Promise<Keychain> {
355358
let source: string;
356359
let encryptedPrv: string | undefined = undefined;
357360
let reducedEncryptedPrv: string | undefined = undefined;
361+
let privateMaterialBase64: string | undefined = undefined;
358362
switch (participantIndex) {
359363
case MPCv2PartiesEnum.USER:
360364
case MPCv2PartiesEnum.BACKUP:
361365
source = participantIndex === MPCv2PartiesEnum.USER ? 'user' : 'backup';
362366
assert(privateMaterial, `Private material is required for ${source} keychain`);
363367
assert(reducedPrivateMaterial, `Reduced private material is required for ${source} keychain`);
364368
assert(passphrase, `Passphrase is required for ${source} keychain`);
369+
privateMaterialBase64 = privateMaterial.toString('base64');
365370
encryptedPrv = this.bitgo.encrypt({
366-
input: privateMaterial.toString('base64'),
371+
input: privateMaterialBase64,
367372
password: passphrase,
368373
});
369374
// Encrypts the CBOR-encoded ReducedKeyShare (which contains the party's private
@@ -393,6 +398,19 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
393398
isMPCv2: true,
394399
};
395400

401+
if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) {
402+
recipientKeychainParams.webauthnDevices = [
403+
{
404+
otpDeviceId: webauthnInfo.otpDeviceId,
405+
prfSalt: webauthnInfo.prfSalt,
406+
encryptedPrv: this.bitgo.encrypt({
407+
input: privateMaterialBase64,
408+
password: webauthnInfo.passphrase,
409+
}),
410+
},
411+
];
412+
}
413+
396414
const keychains = this.baseCoin.keychains();
397415
return { ...(await keychains.add(recipientKeychainParams)), reducedEncryptedPrv: reducedEncryptedPrv };
398416
}
@@ -512,15 +530,17 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
512530
privateMaterial: Buffer,
513531
reducedPrivateMaterial: Buffer,
514532
passphrase: string,
515-
originalPasscodeEncryptionCode?: string
533+
originalPasscodeEncryptionCode?: string,
534+
webauthnInfo?: MpcWebauthnInfo
516535
): Promise<Keychain> {
517536
return this.createParticipantKeychain(
518537
MPCv2PartiesEnum.USER,
519538
commonKeychain,
520539
privateMaterial,
521540
reducedPrivateMaterial,
522541
passphrase,
523-
originalPasscodeEncryptionCode
542+
originalPasscodeEncryptionCode,
543+
webauthnInfo
524544
);
525545
}
526546

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import assert from 'assert';
55
import * as openpgp from 'openpgp';
66
import Eddsa, { GShare, SignShare } from '../../../../account-lib/mpc/tss';
7-
import { AddKeychainOptions, CreateBackupOptions, Keychain } from '../../../keychain';
7+
import { AddKeychainOptions, CreateBackupOptions, Keychain, MpcWebauthnInfo } from '../../../keychain';
88
import { verifyWalletSignature } from '../../../tss/eddsa/eddsa';
99
import { createShareProof, encryptText, generateGPGKeyPair, getBitgoGpgPubKey } from '../../opengpgUtils';
1010
import {
@@ -128,6 +128,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
128128
bitgoKeychain,
129129
passphrase,
130130
originalPasscodeEncryptionCode,
131+
webauthnInfo,
131132
}: CreateEddsaKeychainParams): Promise<Keychain> {
132133
const MPC = await Eddsa.initialize();
133134
const bitgoKeyShares = bitgoKeychain.keyShares;
@@ -189,6 +190,18 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
189190
password: passphrase,
190191
});
191192
}
193+
if (webauthnInfo && userKeychainParams.encryptedPrv) {
194+
userKeychainParams.webauthnDevices = [
195+
{
196+
otpDeviceId: webauthnInfo.otpDeviceId,
197+
prfSalt: webauthnInfo.prfSalt,
198+
encryptedPrv: this.bitgo.encrypt({
199+
input: JSON.stringify(userSigningMaterial),
200+
password: webauthnInfo.passphrase,
201+
}),
202+
},
203+
];
204+
}
192205

193206
return await this.baseCoin.keychains().add(userKeychainParams);
194207
}
@@ -344,6 +357,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
344357
passphrase?: string;
345358
enterprise?: string;
346359
originalPasscodeEncryptionCode?: string;
360+
webauthnInfo?: MpcWebauthnInfo;
347361
}): Promise<KeychainsTriplet> {
348362
const MPC = await Eddsa.initialize();
349363
const m = 2;
@@ -370,6 +384,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
370384
bitgoKeychain,
371385
passphrase: params.passphrase,
372386
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
387+
webauthnInfo: params.webauthnInfo,
373388
});
374389
const backupKeychainPromise = this.createBackupKeychain({
375390
userGpgKey,

0 commit comments

Comments
 (0)