diff --git a/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts index f4b5e4162f..ec5b8f1cdf 100644 --- a/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts @@ -309,75 +309,6 @@ export async function encryptText(text: string, key: Key): Promise { }); } -/** - * Encrypts and detach signs a string - * @param text string to encrypt and sign - * @param publicArmor public key to encrypt with - * @param privateArmor private key to sign with - */ -export async function encryptAndDetachSignText( - text: string, - publicArmor: string, - privateArmor: string -): Promise { - const publicKey = await readKey({ armoredKey: publicArmor }); - const privateKey = await readPrivateKey({ armoredKey: privateArmor }); - const message = await createMessage({ text }); - const encryptedMessage = await encrypt({ - message, - encryptionKeys: publicKey, - format: 'armored', - config: { - rejectCurves: new Set(), - showVersion: false, - showComment: false, - }, - }); - const signature = await sign({ - message, - signingKeys: privateKey, - format: 'armored', - detached: true, - config: { - rejectCurves: new Set(), - showVersion: false, - showComment: false, - }, - }); - return { - encryptedMessage: encryptedMessage, - signature: signature, - }; -} - -/** - * Encrypts and detach signs a string - * @param text string to encrypt and sign - * @param publicArmor public key to verify signature with - * @param privateArmor private key to decrypt with - */ -export async function decryptAndVerifySignedText( - encryptedAndSignedMessage: AuthEncMessage, - publicArmor: string, - privateArmor: string -): Promise { - const publicKey = await readKey({ armoredKey: publicArmor }); - const privateKey = await readPrivateKey({ armoredKey: privateArmor }); - const decryptedMessage = await decrypt({ - message: await readMessage({ armoredMessage: encryptedAndSignedMessage.encryptedMessage }), - decryptionKeys: privateKey, - signature: await readSignature({ armoredSignature: encryptedAndSignedMessage.signature }), - verificationKeys: publicKey, - expectSigned: true, - config: { - rejectCurves: new Set(), - showVersion: false, - showComment: false, - }, - }); - return decryptedMessage.data; -} - /** * Encrypts and signs a string * @param text string to encrypt and sign diff --git a/modules/sdk-lib-mpc/package.json b/modules/sdk-lib-mpc/package.json index cf41a2823f..94a8d9a0bc 100644 --- a/modules/sdk-lib-mpc/package.json +++ b/modules/sdk-lib-mpc/package.json @@ -38,11 +38,14 @@ "dependencies": { "@noble/secp256k1": "1.6.3", "@types/superagent": "4.1.15", + "@silencelaboratories/dkls-wasm-ll-node": "0.1.0-pre.2", "@wasmer/wasi": "^1.2.2", "bigint-crypto-utils": "3.1.4", "bigint-mod-arith": "3.1.2", "libsodium-wrappers-sumo": "^0.7.9", - "paillier-bigint": "3.3.0" + "paillier-bigint": "3.3.0", + "cbor": "^9.0.1", + "openpgp": "5.10.1" }, "devDependencies": { "@types/lodash": "^4.14.151", diff --git a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/commsLayer.ts b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/commsLayer.ts new file mode 100644 index 0000000000..221d2d5ea5 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/commsLayer.ts @@ -0,0 +1,131 @@ +import { SerializedMessages, AuthEncMessage, AuthEncMessages } from './types'; +import * as pgp from 'openpgp'; + +/** + * Detach signs a binary and encodes it in base64 + * @param data binary to encode in base64 and sign + * @param privateArmor private key to sign with + */ +export async function detachSignData(data: Buffer, privateArmor: string): Promise { + const message = await pgp.createMessage({ binary: data }); + const privateKey = await pgp.readPrivateKey({ armoredKey: privateArmor }); + const signature = await pgp.sign({ + message, + signingKeys: privateKey, + format: 'armored', + detached: true, + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + return { + encryptedMessage: data.toString('base64'), + signature: signature, + }; +} + +/** + * Encrypts and detach signs a binary + * @param data binary to encrypt and sign + * @param publicArmor public key to encrypt with + * @param privateArmor private key to sign with + */ +export async function encryptAndDetachSignData( + data: Buffer, + publicArmor: string, + privateArmor: string +): Promise { + const message = await pgp.createMessage({ binary: data }); + const publicKey = await pgp.readKey({ armoredKey: publicArmor }); + const privateKey = await pgp.readPrivateKey({ armoredKey: privateArmor }); + const encryptedMessage = await pgp.encrypt({ + message, + encryptionKeys: publicKey, + format: 'armored', + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + const signature = await pgp.sign({ + message, + signingKeys: privateKey, + format: 'armored', + detached: true, + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + return { + encryptedMessage: encryptedMessage, + signature: signature, + }; +} + +/** + * Decrypts and verifies signature on a binary + * @param encryptedAndSignedMessage message to decrypt and verify + * @param publicArmor public key to verify signature with + * @param privateArmor private key to decrypt with + */ +export async function decryptAndVerifySignedData( + encryptedAndSignedMessage: AuthEncMessage, + publicArmor: string, + privateArmor: string +): Promise { + const publicKey = await pgp.readKey({ armoredKey: publicArmor }); + const privateKey = await pgp.readPrivateKey({ armoredKey: privateArmor }); + const decryptedMessage = await pgp.decrypt({ + message: await pgp.readMessage({ armoredMessage: encryptedAndSignedMessage.encryptedMessage }), + decryptionKeys: [privateKey], + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + format: 'binary', + }); + const verificationResult = await pgp.verify({ + message: await pgp.createMessage({ binary: decryptedMessage.data }), + signature: await pgp.readSignature({ armoredSignature: encryptedAndSignedMessage.signature }), + verificationKeys: publicKey, + }); + await verificationResult.signatures[0].verified; + return Buffer.from(decryptedMessage.data).toString('base64'); +} + +export async function encryptAndAuthOutgoingMessages( + messages: SerializedMessages, + pubEncryptionGpgKey: string, + prvAuthenticationGpgKey: string +): Promise { + return { + p2pMessages: await Promise.all( + messages.p2pMessages.map(async (m) => { + return { + to: m.to, + from: m.from, + payload: await encryptAndDetachSignData( + Buffer.from(m.payload, 'base64'), + pubEncryptionGpgKey, + prvAuthenticationGpgKey + ), + commitment: m.commitment, + }; + }) + ), + broadcastMessages: await Promise.all( + messages.broadcastMessages.map(async (m) => { + return { + from: m.from, + payload: await detachSignData(Buffer.from(m.payload, 'base64'), prvAuthenticationGpgKey), + }; + }) + ), + }; +} diff --git a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts new file mode 100644 index 0000000000..7a23d5f391 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts @@ -0,0 +1,138 @@ +import { KeygenSession, Keyshare, Message } from '@silencelaboratories/dkls-wasm-ll-node'; +import { DeserializedBroadcastMessage, DeserializedMessages, DkgState } from './types'; +import { decode } from 'cbor'; + +export class Dkg { + protected dkgSession: KeygenSession; + protected dkgKeyShare: Keyshare; + protected n: number; + protected t: number; + protected chainCodeCommitment: Uint8Array | undefined; + protected partyIdx: number; + protected dkgState: DkgState = DkgState.Uninitialized; + + constructor(n: number, t: number, partyIdx: number) { + this.n = n; + this.t = t; + this.partyIdx = partyIdx; + this.chainCodeCommitment = undefined; + } + + private _deserializeState() { + const round = decode(this.dkgSession.toBytes()).round; + switch (round) { + case 'WaitMsg1': + this.dkgState = DkgState.Round1; + break; + case 'WaitMsg2': + this.dkgState = DkgState.Round2; + break; + case 'WaitMsg3': + this.dkgState = DkgState.Round3; + break; + case 'WaitMsg4': + this.dkgState = DkgState.Round4; + break; + case 'Ended': + this.dkgState = DkgState.Complete; + break; + default: + this.dkgState = DkgState.InvalidState; + throw `Invalid State: ${round}`; + } + } + + async initDkg(): Promise { + if (this.t > this.n || this.partyIdx >= this.n) { + throw 'Invalid parameters for DKG'; + } + if (this.dkgState != DkgState.Uninitialized) { + throw 'DKG session already initialized'; + } + this.dkgSession = new KeygenSession(this.n, this.t, this.partyIdx); + try { + const payload = this.dkgSession.createFirstMessage().payload; + this._deserializeState(); + return { + payload: payload, + from: this.partyIdx, + }; + } catch (e) { + throw `Error while creating the first message from party ${this.partyIdx}: ${e}`; + } + } + + getKeyShare(): Buffer { + const keyShareBuff = Buffer.from(this.dkgKeyShare.toBytes()); + this.dkgKeyShare.free(); + return keyShareBuff; + } + + handleIncomingMessages(messagesForIthRound: DeserializedMessages): DeserializedMessages { + try { + let nextRoundMessages: Message[]; + if (this.dkgState == DkgState.Round3) { + const commitmentsUnsorted = messagesForIthRound.p2pMessages + .map((m) => { + return { from: m.from, commitment: m.commitment }; + }) + .concat([{ from: this.partyIdx, commitment: this.chainCodeCommitment }]); + const commitmentsSorted = commitmentsUnsorted + .sort((a, b) => { + return a.from - b.from; + }) + .map((c) => c.commitment); + nextRoundMessages = this.dkgSession.handleMessages( + messagesForIthRound.broadcastMessages + .map((m) => new Message(m.payload, m.from, undefined)) + .concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), + commitmentsSorted + ); + } else { + nextRoundMessages = this.dkgSession.handleMessages( + messagesForIthRound.broadcastMessages + .map((m) => new Message(m.payload, m.from, undefined)) + .concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), + undefined + ); + } + if (this.dkgState == DkgState.Round4) { + this.dkgKeyShare = this.dkgSession.keyshare(); + this.dkgState = DkgState.Complete; + return { broadcastMessages: [], p2pMessages: [] }; + } else { + // Update ronud data. + this._deserializeState(); + } + if (this.dkgState == DkgState.Round3) { + this.chainCodeCommitment = this.dkgSession.calculateChainCodeCommitment(); + } + const nextRoundSerializedMessages = { + p2pMessages: nextRoundMessages + .filter((m) => m.to_id !== undefined) + .map((m) => { + const p2pReturn = { + payload: m.payload, + from: m.from_id, + to: m.to_id!, + commitment: this.chainCodeCommitment, + }; + return p2pReturn; + }), + broadcastMessages: nextRoundMessages + .filter((m) => m.to_id === undefined) + .map((m) => { + const broadcastReturn = { + payload: m.payload, + from: m.from_id, + }; + return broadcastReturn; + }), + }; + nextRoundMessages.forEach((m) => m.free()); + return nextRoundSerializedMessages; + } catch (e) { + throw `Error while creating messages from party ${this.partyIdx}, round ${this.dkgState}: ${e}`; + } + } +} diff --git a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/index.ts b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/index.ts new file mode 100644 index 0000000000..d0be25342b --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/index.ts @@ -0,0 +1,2 @@ +export * as DklsDkg from './dkg'; +export * as DklsTypes from './types'; diff --git a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/types.ts b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/types.ts new file mode 100644 index 0000000000..4495cb0878 --- /dev/null +++ b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/types.ts @@ -0,0 +1,92 @@ +// Broadcast message meant to be sent to multiple parties +interface BroadcastMessage { + payload: T; + from: number; +} + +// P2P message meant to be sent to a specific party +interface P2PMessage { + payload: T; + from: number; + commitment?: G; + to: number; +} + +export enum DkgState { + Uninitialized = 0, + Round1, + Round2, + Round3, + Round4, + Complete, + InvalidState, +} + +export type AuthEncMessage = { + encryptedMessage: string; + signature: string; +}; +export type SerializedBroadcastMessage = BroadcastMessage; +export type DeserializedBroadcastMessage = BroadcastMessage; +export type SerializedP2PMessage = P2PMessage; +export type DeserializedP2PMessage = P2PMessage; +export type AuthEncP2PMessage = P2PMessage; +export type AuthBroadcastMessage = BroadcastMessage; +export type SerializedMessages = { + p2pMessages: SerializedP2PMessage[]; + broadcastMessages: SerializedBroadcastMessage[]; +}; +export type AuthEncMessages = { + p2pMessages: AuthEncP2PMessage[]; + broadcastMessages: AuthBroadcastMessage[]; +}; +export type DeserializedMessages = { + p2pMessages: DeserializedP2PMessage[]; + broadcastMessages: DeserializedBroadcastMessage[]; +}; + +/** + * Serializes messages payloads to base64 strings. + * @param messages + */ +export function serializeMessages(messages: DeserializedMessages): SerializedMessages { + return { + p2pMessages: messages.p2pMessages.map((m) => { + return { + to: m.to, + from: m.from, + payload: Buffer.from(m.payload).toString('base64'), + commitment: m.commitment ? Buffer.from(m.commitment).toString('base64') : m.commitment, + }; + }), + broadcastMessages: messages.broadcastMessages.map((m) => { + return { + from: m.from, + payload: Buffer.from(m.payload).toString('base64'), + }; + }), + }; +} + +/** + * Desrializes messages payloads to Uint8Array. + * @param messages + */ +export function deserializeMessages(messages: SerializedMessages): DeserializedMessages { + return { + p2pMessages: messages.p2pMessages.map((m) => { + return { + to: m.to, + from: m.from, + payload: new Uint8Array(Buffer.from(m.payload, 'base64')), + commitment: m.commitment ? new Uint8Array(Buffer.from(m.commitment, 'base64')) : undefined, + }; + }), + broadcastMessages: messages.broadcastMessages.map((m) => { + return { + from: m.from, + payload: new Uint8Array(Buffer.from(m.payload, 'base64')), + }; + }), + }; +} diff --git a/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsComms.ts b/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsComms.ts new file mode 100644 index 0000000000..1190f27caf --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsComms.ts @@ -0,0 +1,97 @@ +import { decryptAndVerifySignedData, encryptAndDetachSignData } from '../../../../src/tss/ecdsa-dkls/commsLayer'; +import * as openpgp from 'openpgp'; + +describe('DKLS Communication Layer', function () { + let senderKey: { publicKey: string; privateKey: string }; + let recipientKey: { publicKey: string; privateKey: string }; + let otherKey: { publicKey: string; privateKey: string }; + + before(async function () { + openpgp.config.rejectCurves = new Set(); + senderKey = await openpgp.generateKey({ + userIDs: [ + { + name: 'sender', + email: 'sender@username.com', + }, + ], + curve: 'secp256k1', + }); + recipientKey = await openpgp.generateKey({ + userIDs: [ + { + name: 'recipient', + email: 'recipient@username.com', + }, + ], + curve: 'secp256k1', + }); + otherKey = await openpgp.generateKey({ + userIDs: [ + { + name: 'other', + email: 'other@username.com', + }, + ], + curve: 'secp256k1', + }); + }); + + it('should succeed on encryption with detached signature and decryption with verification', async function () { + const text = 'ffffffff'; + + const signedMessage = await encryptAndDetachSignData( + Buffer.from(text, 'base64'), + recipientKey.publicKey, + senderKey.privateKey + ); + (await decryptAndVerifySignedData(signedMessage, senderKey.publicKey, recipientKey.privateKey)).should.equal(text); + }); + + it('should fail on encryption with detached signature and decryption with wrong private key', async function () { + const text = 'ffffffff'; + + const signedMessage = await encryptAndDetachSignData( + Buffer.from(text, 'base64'), + recipientKey.publicKey, + senderKey.privateKey + ); + await decryptAndVerifySignedData(signedMessage, senderKey.publicKey, otherKey.privateKey).should.be.rejectedWith( + 'Error decrypting message: Session key decryption failed.' + ); + }); + + it('should fail on encryption with detached signature and decryption verification with wrong sender public key', async function () { + const text = 'ffffffff'; + + const signedMessage = await encryptAndDetachSignData( + Buffer.from(text, 'base64'), + recipientKey.publicKey, + senderKey.privateKey + ); + await decryptAndVerifySignedData(signedMessage, otherKey.publicKey, recipientKey.privateKey).should.be.rejectedWith( + `Could not find signing key with key ID ${(await openpgp.readKey({ armoredKey: senderKey.publicKey })) + .getKeyID() + .toHex()}` + ); + }); + + it('should fail on encryption with detached signature by unintended sender and decryption verification', async function () { + const text = 'ffffffff'; + + const signedMessage = await encryptAndDetachSignData( + Buffer.from(text, 'base64'), + recipientKey.publicKey, + otherKey.privateKey + ); + await decryptAndVerifySignedData( + signedMessage, + senderKey.publicKey, + recipientKey.privateKey + ).should.be.rejectedWith( + `Could not find signing key with key ID ${(await openpgp.readKey({ armoredKey: otherKey.publicKey })) + .getKeyID() + .toHex()}` + ); + }); +}); diff --git a/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsDkg.ts b/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsDkg.ts new file mode 100644 index 0000000000..81e894e114 --- /dev/null +++ b/modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsDkg.ts @@ -0,0 +1,72 @@ +import { DklsDkg } from '../../../../src/tss/ecdsa-dkls'; + +describe('DKLS Dkg 2x3', function () { + it(`should create key shares`, async function () { + const user = new DklsDkg.Dkg(3, 2, 0); + const backup = new DklsDkg.Dkg(3, 2, 1); + const bitgo = new DklsDkg.Dkg(3, 2, 2); + const userRound1Message = await user.initDkg(); + const backupRound1Message = await backup.initDkg(); + const bitgoRound1Message = await bitgo.initDkg(); + const userRound2Messages = user.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [bitgoRound1Message, backupRound1Message], + }); + const backupRound2Messages = backup.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [userRound1Message, bitgoRound1Message], + }); + const bitgoRound2Messages = bitgo.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [userRound1Message, backupRound1Message], + }); + const userRound3Messages = user.handleIncomingMessages({ + p2pMessages: backupRound2Messages.p2pMessages + .filter((m) => m.to == 0) + .concat(bitgoRound2Messages.p2pMessages.filter((m) => m.to == 0)), + broadcastMessages: [], + }); + const backupRound3Messages = backup.handleIncomingMessages({ + p2pMessages: bitgoRound2Messages.p2pMessages + .filter((m) => m.to == 1) + .concat(userRound2Messages.p2pMessages.filter((m) => m.to == 1)), + broadcastMessages: [], + }); + const bitgoRound3Messages = bitgo.handleIncomingMessages({ + p2pMessages: backupRound2Messages.p2pMessages + .filter((m) => m.to == 2) + .concat(userRound2Messages.p2pMessages.filter((m) => m.to == 2)), + broadcastMessages: [], + }); + const userRound4Messages = user.handleIncomingMessages({ + p2pMessages: backupRound3Messages.p2pMessages + .filter((m) => m.to == 0) + .concat(bitgoRound3Messages.p2pMessages.filter((m) => m.to == 0)), + broadcastMessages: [], + }); + const backupRound4Messages = backup.handleIncomingMessages({ + p2pMessages: bitgoRound3Messages.p2pMessages + .filter((m) => m.to == 1) + .concat(userRound3Messages.p2pMessages.filter((m) => m.to == 1)), + broadcastMessages: [], + }); + const bitgoRound4Messages = bitgo.handleIncomingMessages({ + p2pMessages: backupRound3Messages.p2pMessages + .filter((m) => m.to == 2) + .concat(userRound3Messages.p2pMessages.filter((m) => m.to == 2)), + broadcastMessages: [], + }); + user.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: bitgoRound4Messages.broadcastMessages.concat(backupRound4Messages.broadcastMessages), + }); + bitgo.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: backupRound4Messages.broadcastMessages.concat(userRound4Messages.broadcastMessages), + }); + backup.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: bitgoRound4Messages.broadcastMessages.concat(userRound4Messages.broadcastMessages), + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 733b5deffe..f524edf164 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4318,6 +4318,11 @@ resolved "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@silencelaboratories/dkls-wasm-ll-node@0.1.0-pre.2": + version "0.1.0-pre.2" + resolved "https://registry.yarnpkg.com/@silencelaboratories/dkls-wasm-ll-node/-/dkls-wasm-ll-node-0.1.0-pre.2.tgz#c73eeb7f65744e443aaf19391db4b3c43923cba2" + integrity sha512-fnzRABvsBy17Z8wIWEF5dy2/ABoYU4vdvch0z3UdnUA3yl7kRdIqF8vgMHfvnK5W3x0t6JY6uJCVmpxICWkBog== + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz"