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
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ COPY --from=builder /tmp/bitgo/modules/express /var/bitgo-express/
#COPY_START
COPY --from=builder /tmp/bitgo/modules/abstract-lightning /var/modules/abstract-lightning/
COPY --from=builder /tmp/bitgo/modules/sdk-core /var/modules/sdk-core/
COPY --from=builder /tmp/bitgo/modules/passkey-crypto /var/modules/passkey-crypto/
COPY --from=builder /tmp/bitgo/modules/sjcl /var/modules/sjcl/
COPY --from=builder /tmp/bitgo/modules/sdk-lib-mpc /var/modules/sdk-lib-mpc/
COPY --from=builder /tmp/bitgo/modules/sdk-opensslbytes /var/modules/sdk-opensslbytes/
COPY --from=builder /tmp/bitgo/modules/sjcl /var/modules/sjcl/
COPY --from=builder /tmp/bitgo/modules/secp256k1 /var/modules/secp256k1/
COPY --from=builder /tmp/bitgo/modules/statics /var/modules/statics/
COPY --from=builder /tmp/bitgo/modules/utxo-lib /var/modules/utxo-lib/
Expand Down Expand Up @@ -145,9 +146,10 @@ COPY --from=builder /tmp/bitgo/modules/sdk-coin-zec /var/modules/sdk-coin-zec/

RUN cd /var/modules/abstract-lightning && yarn link && \
cd /var/modules/sdk-core && yarn link && \
cd /var/modules/passkey-crypto && yarn link && \
cd /var/modules/sjcl && yarn link && \
cd /var/modules/sdk-lib-mpc && yarn link && \
cd /var/modules/sdk-opensslbytes && yarn link && \
cd /var/modules/sjcl && yarn link && \
cd /var/modules/secp256k1 && yarn link && \
cd /var/modules/statics && yarn link && \
cd /var/modules/utxo-lib && yarn link && \
Expand Down Expand Up @@ -250,9 +252,10 @@ cd /var/modules/sdk-coin-zec && yarn link
RUN cd /var/bitgo-express && \
yarn link @bitgo/abstract-lightning && \
yarn link @bitgo/sdk-core && \
yarn link @bitgo/passkey-crypto && \
yarn link @bitgo/sjcl && \
yarn link @bitgo/sdk-lib-mpc && \
yarn link @bitgo/sdk-opensslbytes && \
yarn link @bitgo/sjcl && \
yarn link @bitgo/secp256k1 && \
yarn link @bitgo/statics && \
yarn link @bitgo/utxo-lib && \
Expand Down
1 change: 1 addition & 0 deletions modules/passkey-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"access": "public"
},
"dependencies": {
"@bitgo/public-types": "6.1.0",
"@bitgo/sjcl": "^1.1.0"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions modules/passkey-crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { derivePassword } from './derivePassword';
export { deriveEnterpriseSalt } from './deriveEnterpriseSalt';
export { buildEvalByCredential, matchDeviceByCredentialId } from './prfHelpers';
34 changes: 34 additions & 0 deletions modules/passkey-crypto/src/prfHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { WebauthnDevice } from '@bitgo/public-types';

/**
* Builds the PRF eval map and credential-to-device lookup from a wallet
* keychain's webauthn devices. Devices without a prfSalt are skipped.
*/
export function buildEvalByCredential(devices: WebauthnDevice[]): {
evalByCredential: Record<string, string>;
credIdToDevice: Map<string, WebauthnDevice>;
} {
const evalByCredential: Record<string, string> = {};
const credIdToDevice = new Map<string, WebauthnDevice>();

for (const device of devices) {
if (!device.prfSalt) continue;
const { credID } = device.authenticatorInfo;
evalByCredential[credID] = device.prfSalt;
credIdToDevice.set(credID, device);
}

return { evalByCredential, credIdToDevice };
}

/**
* Returns the WebauthnDevice matching the given credential ID.
* @throws if no matching device is found
*/
export function matchDeviceByCredentialId(devices: WebauthnDevice[], credentialId: string): WebauthnDevice {
const device = devices.find((d) => d.authenticatorInfo.credID === credentialId);
if (!device) {
throw new Error('Could not identify which passkey device was used');
}
return device;
}
79 changes: 79 additions & 0 deletions modules/passkey-crypto/test/unit/prfHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as assert from 'assert';
import { buildEvalByCredential, matchDeviceByCredentialId } from '../../src';
import { WebauthnDevice } from '@bitgo/public-types';

const device1: WebauthnDevice = {
otpDeviceId: 'oid-1',
authenticatorInfo: { credID: 'cred-aaa', fmt: 'none', publicKey: 'pk-1' },
prfSalt: 'salt-aaa',
encryptedPrv: 'enc-prv-1',
};

const device2: WebauthnDevice = {
otpDeviceId: 'oid-2',
authenticatorInfo: { credID: 'cred-bbb', fmt: 'none', publicKey: 'pk-2' },
prfSalt: 'salt-bbb',
encryptedPrv: 'enc-prv-2',
};

describe('buildEvalByCredential', function () {
it('maps each device credID to its prfSalt in evalByCredential', function () {
const { evalByCredential } = buildEvalByCredential([device1, device2]);
assert.deepStrictEqual(evalByCredential, { 'cred-aaa': 'salt-aaa', 'cred-bbb': 'salt-bbb' });
});

it('populates credIdToDevice with both devices', function () {
const { credIdToDevice } = buildEvalByCredential([device1, device2]);
assert.strictEqual(credIdToDevice.get('cred-aaa'), device1);
assert.strictEqual(credIdToDevice.get('cred-bbb'), device2);
});

it('returns empty maps for an empty device list', function () {
const { evalByCredential, credIdToDevice } = buildEvalByCredential([]);
assert.deepStrictEqual(evalByCredential, {});
assert.strictEqual(credIdToDevice.size, 0);
});

it('skips devices with empty prfSalt', function () {
const deviceNoPrf = { ...device1, prfSalt: '' };
const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]);
assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' });
assert.strictEqual(credIdToDevice.has('cred-aaa'), false);
});

it('skips devices with undefined prfSalt', function () {
const deviceNoPrf = { ...device1, prfSalt: undefined as unknown as string };
const { evalByCredential, credIdToDevice } = buildEvalByCredential([deviceNoPrf, device2]);
assert.deepStrictEqual(evalByCredential, { 'cred-bbb': 'salt-bbb' });
assert.strictEqual(credIdToDevice.has('cred-aaa'), false);
});
});

describe('matchDeviceByCredentialId', function () {
it('returns the matching device', function () {
assert.strictEqual(matchDeviceByCredentialId([device1, device2], 'cred-bbb'), device2);
});

it('returns the first device when it matches', function () {
assert.strictEqual(matchDeviceByCredentialId([device1, device2], 'cred-aaa'), device1);
});

it('returns a device even when it has no prfSalt', function () {
const deviceNoPrf = { ...device1, prfSalt: '' };
assert.strictEqual(matchDeviceByCredentialId([deviceNoPrf, device2], 'cred-aaa'), deviceNoPrf);
});

it('throws the expected error message when no device matches', function () {
assert.throws(
() => matchDeviceByCredentialId([device1, device2], 'cred-unknown'),
(err: Error) => {
assert.strictEqual(err.message, 'Could not identify which passkey device was used');
return true;
}
);
});

it('throws when the device list is empty', function () {
assert.throws(() => matchDeviceByCredentialId([], 'cred-aaa'), Error);
});
});
3 changes: 2 additions & 1 deletion modules/sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
]
},
"dependencies": {
"@bitgo/public-types": "5.96.2",
"@bitgo/passkey-crypto": "^0.1.0",
"@bitgo/public-types": "6.1.0",
"@bitgo/sdk-lib-mpc": "^10.11.1",
"@bitgo/secp256k1": "^1.11.0",
"@bitgo/sjcl": "^1.1.0",
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * from './internal';
export * from './keychain';
export * as bitcoin from './legacyBitcoin';
export * from './market';
export * from './passkey';
export * from './pendingApproval';
export { WalletProofs } from './proofs';
export * from './recovery';
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/passkey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { WebAuthnOtpDevice, PasskeyAuthResult, PasskeyGetOptions, WebAuthnProvider } from './types';
export { buildEvalByCredential, matchDeviceByCredentialId } from '@bitgo/passkey-crypto';
22 changes: 22 additions & 0 deletions modules/sdk-core/src/bitgo/passkey/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export type { WebAuthnOtpDevice } from '@bitgo/public-types';

/** Result of a WebAuthn assertion with the PRF extension. */
export interface PasskeyAuthResult {
// undefined if the authenticator does not support PRF
prfResult: ArrayBuffer | undefined;
credentialId: string;
otpCode: string;
}

/** Options for WebAuthnProvider.get(). */
export interface PasskeyGetOptions {
publicKey: PublicKeyCredentialRequestOptions;
// PRF eval map: { [credentialId]: salt }
evalByCredential?: Record<string, string>;
}

/** Abstraction over the WebAuthn credential API. */
export interface WebAuthnProvider {
create(options: PublicKeyCredentialCreationOptions): Promise<PublicKeyCredential>;
get(options: PasskeyGetOptions): Promise<PasskeyAuthResult>;
}
Comment on lines +1 to +22
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.

why aren't these defined in passkey-crypto ?

Loading