diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts index 6e9c010a43..cb3cc68216 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts @@ -14,6 +14,7 @@ import { import { BitGo, createSharedDataProof, TssUtils, RequestType } from '../../../../../src'; import { BackupGpgKey, + AddKeychainOptions, BackupKeyShare, BaseCoin, BitgoGPGPublicKey, @@ -309,6 +310,48 @@ describe('TSS Ecdsa Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + // Keep the real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures) and + // capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(16) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitGoGPGKeyPair.publicKey } } }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createParticipantKeychain( + userGpgKey, + userLocalBackupGpgKey, + bitgoPublicKey, + 1, + userKeyShare, + backupKeyShare, + nockedBitGoKeychain, + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains with optional params', async function () { const enterprise = 'enterprise_id'; const backupShareHolder: BackupKeyShare = { diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts index dee40f849f..94e6d08830 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/createKeychains.ts @@ -57,6 +57,9 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { }); before(async function () { + // Allow secp256k1 GPG keys used by these fixtures (the full suite enables this + // globally via sibling test files; set it here so this file also runs in isolation). + openpgp.config.rejectCurves = new Set(); bitGoGgpKey = await openpgp.generateKey({ userIDs: [ { @@ -176,6 +179,68 @@ describe('TSS Ecdsa MPCv2 Utils:', async function () { assert.equal(bitgoKeychain.source, 'bitgo'); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const bitgoSession = new DklsDkg.Dkg(3, 2, 2); + + const round1Nock = await nockKeyGenRound1(bitgoSession, 1); + const round2Nock = await nockKeyGenRound2(bitgoSession, 1); + const round3Nock = await nockKeyGenRound3(bitgoSession, 1); + + // Capture each keychain POST body by source so we can assert what the user key sends. + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(3) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + const key = { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + nock('https://bitgo.fakeurl').get(`/api/v2/${coinName}/key/${requestBody.source}`).reply(200, key); + return key; + }); + + const webauthnInfo = { + otpDeviceId: 'device-123', + prfSalt: 'salt-abc', + passphrase: 'prf-derived-passphrase', + }; + const params = { + passphrase: 'test', + enterprise: enterpriseId, + originalPasscodeEncryptionCode: '123456', + webauthnInfo, + }; + await tssUtils.createKeychains(params); + assert.ok(round1Nock.isDone()); + assert.ok(round2Nock.isDone()); + assert.ok(round3Nock.isDone()); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), + // including enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + // encryptedPrv is the user key share encrypted with the PRF-derived passphrase. + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + + // Backup keychain must never carry passkey material. + const backupBody = capturedBodies['backup']; + assert.ok(backupBody, 'backup keychain should have been created'); + assert.strictEqual(backupBody.webauthnInfo, undefined); + assert.strictEqual(backupBody.webauthnDevices, undefined); + }); + it('should generate TSS MPCv2 keys with v2 encryption envelopes', async function () { const bitgoSession = new DklsDkg.Dkg(3, 2, 2); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index d3dea3383b..f1daae1f91 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -1,3 +1,4 @@ +import * as assert from 'assert'; import * as sodium from 'libsodium-wrappers-sumo'; import * as _ from 'lodash'; import nock = require('nock'); @@ -8,6 +9,7 @@ import * as sinon from 'sinon'; import { TestableBG, TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../../../src'; import { + AddKeychainOptions, BaseCoin, BitgoGPGPublicKey, CommitmentShareRecord, @@ -269,6 +271,62 @@ describe('TSS Utils:', async function () { should.exist(backupKeychain.encryptedPrv); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const userKeyShare = MPC.keyShare(1, 2, 3); + const backupKeyShare = MPC.keyShare(2, 2, 3); + + // Real crypto deps (constants w/ bitgo gpg key for verifyWalletSignatures + bitgo keychain), + // then capture the user keychain add() params by stubbing baseCoin.keychains(). + nock.cleanAll(); + nock(bgUrl) + .get('/api/v1/client/constants') + .times(23) + .reply(200, { ttl: 3600, constants: { mpc: { bitgoPublicKey: bitgoGpgKey.publicKey } } }); + await nockBitgoKeychain({ + coin: coinName, + userKeyShare, + backupKeyShare, + bitgoKeyShare, + userGpgKey, + backupGpgKey, + bitgoGpgKey, + }); + const bitgoKeychain = await tssUtils.createBitgoKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + }); + + const addStub = sandbox.stub().resolves({ id: '1', pub: '', type: 'tss' }); + sandbox.stub(baseCoin, 'keychains').returns({ add: addStub } as unknown as ReturnType); + + const enterpriseId = 'enterprise_id'; + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + await tssUtils.createUserKeychain({ + userGpgKey, + backupGpgKey, + userKeyShare, + backupKeyShare, + bitgoKeychain, + passphrase: 'passphrase', + webauthnInfo, + enterprise: enterpriseId, + }); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + assert.ok(addStub.calledOnce, 'keychains().add should have been called for the user keychain'); + const body = addStub.firstCall.args[0] as AddKeychainOptions; + assert.ok(body.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(body.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(body.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(body.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(body.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: body.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(body.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should generate TSS key chains without passphrase', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts index 6cb2f33216..53ae6fe795 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts @@ -109,6 +109,52 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { assert.equal(userKeychain.commonKeychain, bitgoKeychain.commonKeychain); }); + it('should send webauthnInfo (with enterpriseId) on the user keychain when webauthnInfo is provided', async function () { + const commonKeychain = 'a'.repeat(64); + const capturedBodies: Record = {}; + const addKeyNock = nock('https://bitgo.fakeurl') + .post(`/api/v2/${coinName}/key`, (body) => body.keyType === 'tss' && body.isMPCv2) + .times(1) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + capturedBodies[requestBody.source as string] = requestBody; + return { + id: requestBody.source, + source: requestBody.source, + type: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + }); + + const webauthnInfo = { otpDeviceId: 'device-123', prfSalt: 'salt-abc', passphrase: 'prf-derived-passphrase' }; + // Direct participant-keychain call avoids the EdDSA DKG ceremony while still exercising the + // user-keychain webauthn assembly that POSTs to /key. + await tssUtils.createParticipantKeychain( + MPCv2PartiesEnum.USER, + commonKeychain, + Buffer.from('userPrivate'), + Buffer.from('userReduced'), + 'passphrase', + undefined, + webauthnInfo, + undefined, + enterpriseId + ); + assert.ok(addKeyNock.isDone()); + + // User keychain must carry webauthnInfo (the field the backend POST /key consumes), including + // enterpriseId, and must NOT use the deprecated webauthnDevices array. + const userBody = capturedBodies['user']; + assert.ok(userBody, 'user keychain should have been created'); + assert.ok(userBody.webauthnInfo, 'user keychain body should include webauthnInfo'); + assert.equal(userBody.webauthnInfo.otpDeviceId, webauthnInfo.otpDeviceId); + assert.equal(userBody.webauthnInfo.prfSalt, webauthnInfo.prfSalt); + assert.equal(userBody.webauthnInfo.enterpriseId, enterpriseId); + assert.ok(userBody.webauthnInfo.encryptedPrv, 'encryptedPrv should be set'); + assert.ok(bitgo.decrypt({ input: userBody.webauthnInfo.encryptedPrv, password: webauthnInfo.passphrase })); + assert.strictEqual(userBody.webauthnDevices, undefined, 'deprecated webauthnDevices should not be sent'); + }); + it('should create TSS key chains', async function () { const fakeCommonKeychain = 'a'.repeat(64); diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index e6c7d3dcb9..9feb9e6686 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -10,6 +10,8 @@ export interface WebauthnInfo { prfSalt: string; otpDeviceId: string; encryptedPrv: string; + /** Required by POST /key to validate the PRF salt; not needed on the PUT /key/:id update path. */ + enterpriseId?: string; } import type { WebauthnKeyEncryptionInfo } from '../wallet/iWallets'; diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts index 3a3f7a5099..b386a23871 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -145,6 +145,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, webauthnInfo: params.webauthnInfo, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, @@ -187,6 +188,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionVersion, + enterprise, }: CreateEcdsaKeychainParams): Promise { if (!passphrase) { throw new Error('Please provide a wallet passphrase'); @@ -203,7 +205,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); } @@ -322,7 +325,8 @@ export class EcdsaUtils extends BaseEcdsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { const bitgoKeyShares = bitgoKeychain.keyShares; if (!bitgoKeyShares) { @@ -407,7 +411,7 @@ export class EcdsaUtils extends BaseEcdsaUtils { ); const prv = JSON.stringify(recipientCombinedKey.signingMaterial); - const recipientKeychainParams = { + const recipientKeychainParams: AddKeychainOptions & { prv: string } = { source: recipient, keyType: 'tss' as KeyType, commonKeychain: bitgoKeychain.commonKeychain, @@ -418,22 +422,23 @@ export class EcdsaUtils extends BaseEcdsaUtils { encryptionVersion, }), originalPasscodeEncryptionCode, - webauthnDevices: - webauthnInfo && recipientIndex === ShareKeyPosition.USER - ? [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: prv, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ] - : undefined, }; + if (webauthnInfo && recipientIndex === ShareKeyPosition.USER) { + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: prv, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; + } + const keychains = this.baseCoin.keychains(); return recipientIndex === 1 ? await keychains.add(recipientKeychainParams) diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index 337d853157..03d16dd215 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -336,7 +336,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { params.originalPasscodeEncryptionCode, params.webauthnInfo, encryptionSession, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( bitgoCommonKeychain, @@ -380,7 +381,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -435,17 +437,18 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - recipientKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + recipientKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -574,7 +577,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { decrypt(ciphertext: string): Promise; destroy(): void; }, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -585,7 +589,8 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { originalPasscodeEncryptionCode, webauthnInfo, encryptionSession, - encryptionVersion + encryptionVersion, + enterprise ); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index a439e47dae..5cad5b6b82 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -135,6 +135,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo, encryptionSession, encryptionVersion, + enterprise, }: CreateEddsaKeychainParams): Promise { const MPC = await Eddsa.initialize(); const bitgoKeyShares = bitgoKeychain.keyShares; @@ -202,17 +203,18 @@ export class EddsaUtils extends baseTSSUtils { } } if (webauthnInfo && userKeychainParams.encryptedPrv) { - userKeychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: JSON.stringify(userSigningMaterial), - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + userKeychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: JSON.stringify(userSigningMaterial), + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } return await this.baseCoin.keychains().add(userKeychainParams); @@ -412,6 +414,7 @@ export class EddsaUtils extends baseTSSUtils { webauthnInfo: params.webauthnInfo, encryptionSession, encryptionVersion: params.encryptionVersion, + enterprise: params.enterprise, }); const backupKeychainPromise = this.createBackupKeychain({ userGpgKey, diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index e746fbe282..70f29e81ab 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -195,7 +195,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { params.passphrase, params.originalPasscodeEncryptionCode, params.webauthnInfo, - params.encryptionVersion + params.encryptionVersion, + params.enterprise ); const backupKeychainPromise = this.addBackupKeychain( backupCommonKeychain, @@ -230,7 +231,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase?: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { let source: string; let encryptedPrv: string | undefined = undefined; @@ -279,17 +281,18 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { }; if (webauthnInfo && participantIndex === MPCv2PartiesEnum.USER && privateMaterialBase64) { - keychainParams.webauthnDevices = [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: privateMaterialBase64, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ]; + // Send the passkey as `webauthnInfo`; the deprecated `webauthnDevices` array is ignored by POST /key. + assert(enterprise, 'enterprise is required to attach a webauthn device to the user keychain'); + keychainParams.webauthnInfo = { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: privateMaterialBase64, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + }; } const keychains = this.baseCoin.keychains(); @@ -303,7 +306,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase: string, originalPasscodeEncryptionCode?: string, webauthnInfo?: WebauthnKeyEncryptionInfo, - encryptionVersion?: EncryptionVersion + encryptionVersion?: EncryptionVersion, + enterprise?: string ): Promise { return this.createParticipantKeychain( MPCv2PartiesEnum.USER, @@ -313,7 +317,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); }