From 185c039dc430c6040b895a1a36629c2ac2d4327e Mon Sep 17 00:00:00 2001 From: rajangarg047 Date: Tue, 9 Jun 2026 14:07:30 -0400 Subject: [PATCH] feat(sdk-core): send webauthnInfo with enterpriseId for MPC user keychain MPC/TSS wallet creation attached the user keychain's passkey by sending a bare webauthnDevices array (no enterpriseId) on POST /api/v2/:coin/key. The wallet-platform atomic key-creation endpoint only consumes webauthnInfo (a single object including enterpriseId, used to validate the PRF salt) and ignores webauthnDevices on input, so passkeys were never persisted for TSS/MPC user keychains. Switch MPC user-keychain creation to send webauthnInfo with enterpriseId, mirroring the onchain key-creation contract. Applied across all four MPC keychain implementations (ECDSA + EdDSA, MPCv1 + MPCv2), threading the existing createKeychains enterprise param down to the USER participant, and widen WebauthnInfo with optional enterpriseId. Add unit tests asserting webauthnInfo (with enterpriseId) is sent on the user keychain across all four MPC paths, that the deprecated webauthnDevices array is not sent, and that the PRF-encrypted prv decrypts with the webauthn passphrase. WCN-848 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/v2/unit/internal/tssUtils/ecdsa.ts | 43 ++++++++++++ .../tssUtils/ecdsaMPCv2/createKeychains.ts | 65 +++++++++++++++++++ .../test/v2/unit/internal/tssUtils/eddsa.ts | 58 +++++++++++++++++ .../tssUtils/eddsaMPCv2/createKeychains.ts | 46 +++++++++++++ .../sdk-core/src/bitgo/keychain/iKeychains.ts | 6 ++ .../src/bitgo/utils/tss/ecdsa/ecdsa.ts | 34 ++++++---- .../src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts | 37 ++++++----- .../src/bitgo/utils/tss/eddsa/eddsa.ts | 26 ++++---- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 37 ++++++----- 9 files changed, 297 insertions(+), 55 deletions(-) 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..fc26f8375d 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -10,6 +10,12 @@ export interface WebauthnInfo { prfSalt: string; otpDeviceId: string; encryptedPrv: string; + /** Enterprise the key is being created under. Required by the atomic POST /key + * creation endpoint so the backend can validate the PRF salt against the + * enterprise (the key is not yet attached to a wallet at creation time). The + * PUT /key/:id update path resolves the enterprise from the wallet and does + * not need this. */ + 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..78073680f1 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) { @@ -418,19 +422,21 @@ export class EcdsaUtils extends BaseEcdsaUtils { encryptionVersion, }), originalPasscodeEncryptionCode, - webauthnDevices: + // Send the passkey as `webauthnInfo` (single object, including `enterpriseId`) — the field + // the backend's atomic POST /key endpoint consumes. The deprecated `webauthnDevices` array + // is ignored by the create endpoint. + webauthnInfo: webauthnInfo && recipientIndex === ShareKeyPosition.USER - ? [ - { - otpDeviceId: webauthnInfo.otpDeviceId, - prfSalt: webauthnInfo.prfSalt, - encryptedPrv: await this.bitgo.encryptAsync({ - input: prv, - password: webauthnInfo.passphrase, - encryptionVersion, - }), - }, - ] + ? { + otpDeviceId: webauthnInfo.otpDeviceId, + prfSalt: webauthnInfo.prfSalt, + encryptedPrv: await this.bitgo.encryptAsync({ + input: prv, + password: webauthnInfo.passphrase, + encryptionVersion, + }), + enterpriseId: enterprise, + } : undefined, }; 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..4aff1879e5 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,20 @@ 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` (single object, including `enterpriseId`) — this is + // the field the backend's atomic POST /key endpoint consumes to register the device. + // The deprecated `webauthnDevices` array is ignored by the create endpoint. + 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 +579,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 +591,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..97c2df7996 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,19 @@ 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` (single object, including `enterpriseId`) — the field + // the backend's atomic POST /key endpoint consumes. The deprecated `webauthnDevices` array + // is ignored by the create endpoint. + 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 +415,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..26dcab8c32 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,20 @@ 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` (single object, including `enterpriseId`) — this is + // the field the backend's atomic POST /key endpoint consumes to register the device. + // The deprecated `webauthnDevices` array is ignored by the create endpoint. + 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 +308,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 +319,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { passphrase, originalPasscodeEncryptionCode, webauthnInfo, - encryptionVersion + encryptionVersion, + enterprise ); }