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
43 changes: 43 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { BitGo, createSharedDataProof, TssUtils, RequestType } from '../../../../../src';
import {
BackupGpgKey,
AddKeychainOptions,
BackupKeyShare,
BaseCoin,
BitgoGPGPublicKey,
Expand Down Expand Up @@ -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<BaseCoin['keychains']>);

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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
{
Expand Down Expand Up @@ -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<string, AddKeychainOptions> = {};
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);

Expand Down
58 changes: 58 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as assert from 'assert';
import * as sodium from 'libsodium-wrappers-sumo';
import * as _ from 'lodash';
import nock = require('nock');
Expand All @@ -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,
Expand Down Expand Up @@ -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<BaseCoin['keychains']>);

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, AddKeychainOptions> = {};
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);

Expand Down
6 changes: 6 additions & 0 deletions modules/sdk-core/src/bitgo/keychain/iKeychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Comment on lines +13 to +17

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.

please trim this comment.

enterpriseId?: string;
}

import type { WebauthnKeyEncryptionInfo } from '../wallet/iWallets';
Expand Down
34 changes: 20 additions & 14 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -187,6 +188,7 @@ export class EcdsaUtils extends BaseEcdsaUtils {
originalPasscodeEncryptionCode,
webauthnInfo,
encryptionVersion,
enterprise,
}: CreateEcdsaKeychainParams): Promise<Keychain> {
if (!passphrase) {
throw new Error('Please provide a wallet passphrase');
Expand All @@ -203,7 +205,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
passphrase,
originalPasscodeEncryptionCode,
webauthnInfo,
encryptionVersion
encryptionVersion,
enterprise
);
}

Expand Down Expand Up @@ -322,7 +325,8 @@ export class EcdsaUtils extends BaseEcdsaUtils {
passphrase: string,
originalPasscodeEncryptionCode?: string,
webauthnInfo?: WebauthnKeyEncryptionInfo,
encryptionVersion?: EncryptionVersion
encryptionVersion?: EncryptionVersion,
enterprise?: string
): Promise<Keychain> {
const bitgoKeyShares = bitgoKeychain.keyShares;
if (!bitgoKeyShares) {
Expand Down Expand Up @@ -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.
Comment on lines +425 to +427

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.

here.

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

Expand Down
Loading
Loading