UNPKG

bitcore-tss

Version:

Bitcore Library for Threshold Signature Schemes

229 lines (199 loc) 8.78 kB
const bitcoreLib = require('bitcore-lib'); const { DklsDsg, DklsTypes, DklsUtils } = require('@bitgo/sdk-lib-mpc'); const { DklsComms } = require('./dklsComms'); const { encrypt, decrypt } = require('./utils'); const $ = bitcoreLib.util.preconditions; class Sign { #keychain; #partyId; #partySize; #minSigners; #derivationPath; #messageHash; #round; #authKey; #dsg; #signature; /** * * @param {Object} params * @param {Object} params.keychain - keychain object generated by the KeyGen class * @param {Buffer} params.keychain.privateKeyShare * @param {Buffer} params.keychain.commonKeyChain * @param {number} params.partyId - party id of the signer * @param {number} params.n - total number of participants * @param {number} params.m - number of participants required to sign * @param {string} params.derivationPath - OPTIONAL derivation path of the key to sign * @param {Buffer} params.messageHash - hash of the message to sign * @param {Buffer} params.authKey - authentication key of the signer * @param {number} params.round - round number of the signing process. This should not be explicitly given. */ constructor({ keychain, partyId, n, m, derivationPath, messageHash, authKey, round }) { $.checkArgument(keychain != null, 'keychain is required'); $.checkArgument(partyId != null, 'partyId is required'); $.checkArgument(n != null, 'n is required'); $.checkArgument(m != null, 'm is required'); $.checkArgument(messageHash != null, 'messageHash is required'); $.checkArgument(authKey != null, 'authKey is required'); $.checkArgument(Buffer.isBuffer(keychain.privateKeyShare), 'keychain.privateKeyShare must be a buffer'); $.checkArgument(Buffer.isBuffer(keychain.commonKeyChain), 'keychain.commonKeyChain must be a buffer'); this.#keychain = keychain; $.checkArgument(partyId >= 0 && partyId < n, 'partyId must be in the range [0, n-1]'); this.#partyId = parseInt(partyId); $.checkArgument(n > 1, 'n must be at least 2'); this.#partySize = parseInt(n); $.checkArgument(m > 0 && m <= this.#partySize, 'm must be in the range [1, n]'); this.#minSigners = parseInt(m); $.checkArgument(derivationPath == null || (typeof derivationPath === 'string' && derivationPath.startsWith('m')), 'derivationPath must be a string starting with "m"'); this.#derivationPath = derivationPath || 'm'; $.checkArgument(Buffer.isBuffer(messageHash) && messageHash.length === 32, 'messageHash must be a 32 byte buffer'); this.#messageHash = messageHash; $.checkArgument(round == null || (round >= 0 && round <= 5), 'round must be in the range [0, 5]'); this.#round = parseInt(round) || 0; this.#authKey = new bitcoreLib.PrivateKey(authKey); $.checkArgument(this.#authKey.toString('hex') === authKey.toString('hex') || this.#authKey.toWIF() === authKey, 'Unrecognized authKey format'); this.#dsg = new DklsDsg.Dsg(this.#keychain.privateKeyShare, this.#partyId, this.#derivationPath, this.#messageHash); } /** * Export the signing session to a base64 encoded string * @returns {string} Base64 encoded session string */ export() { $.checkState(this.#round > 0, 'Cannot export a session that has not started'); $.checkState(!this.isSignatureReady(), 'Cannot export a completed session. The signature is ready with getSignature()'); const sessionBytes = this.#dsg.dsgSessionBytes || this.#dsg.dsgSession?.toBytes(); const payload = this.#round + ':' + this.#partySize + ':' + this.#minSigners + ':' + this.#partyId + ':' + this.#derivationPath + ':' + Buffer.from(sessionBytes).toString('base64') + ':' + this.#messageHash.toString('base64'); const buf = encrypt(Buffer.from(payload, 'utf8'), this.#authKey.publicKey, this.#authKey); return buf.toString('base64'); } /** * Restore a signing session from an exported session * @param {Object} params * @param {string} params.session Base64 encoded session string * @param {Keychain} params.keychain Keychain to use for signing * @param {bitcoreLib.PrivateKey} params.authKey Private key to use for decrypting the session * @returns {Sign} */ static async restore({ session, keychain, authKey }) { const _authKey = new bitcoreLib.PrivateKey(authKey); $.checkArgument(_authKey.toString('hex') === authKey.toString('hex') || _authKey.toWIF() === authKey, 'Unrecognized authKey format'); session = decrypt(Buffer.from(session, 'base64'), _authKey.publicKey, _authKey).toString('utf8'); const [ round, partySize, minSigners, partyId, derivationPath, dsgSessionBytes, messageHash, ] = session.split(':'); const signer = new Sign({ keychain, n: parseInt(partySize), m: parseInt(minSigners), partyId: parseInt(partyId), derivationPath, messageHash: Buffer.from(messageHash, 'base64'), authKey, round: parseInt(round) }); await signer.#dsg.setSession(Buffer.from(dsgSessionBytes, 'base64')); return signer; } /** * @private * Format the message to be sent to the other parties * @param {Object} signedMessage * @returns */ _formatMessage(signedMessage) { return { round: this.#round++, partyId: this.#partyId, publicKey: this.#authKey.publicKey.toString(), p2pMessages: signedMessage.p2pMessages, broadcastMessages: signedMessage.broadcastMessages, }; } /** * Initialize the signing session with a broadcast message to send to the other participants * @returns {Promise<{round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessages: Object[]}>} */ async initJoin() { $.checkState(this.#round == 0, 'initJoin must be called before the rounds '); const unsignedMessageR1 = await this.#dsg.init(); const serializedMsg = DklsTypes.serializeBroadcastMessage(unsignedMessageR1); const signedMessage = await DklsComms.encryptAndAuthOutgoingMessages( { broadcastMessages: [serializedMsg], p2pMessages: [] }, [], this.#authKey ); return this._formatMessage(signedMessage); } /** * Call this after receiving the initJoin broadcast messages from the other participants * and while isSignatureReady() is false * @param {Array<Object>} prevRoundMessages * @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessages: Object[] }} */ nextRound(prevRoundMessages) { $.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds'); $.checkState(this.#round < 5, 'Signing rounds are over'); $.checkArgument(Array.isArray(prevRoundMessages), 'prevRoundMessages must be an array'); $.checkArgument(prevRoundMessages.length === this.#minSigners - 1, 'Not ready to proceed to the next round'); $.checkArgument(prevRoundMessages.every(msg => msg.round === this.#round - 1), 'All messages must be from the previous round'); $.checkArgument(prevRoundMessages.every(msg => msg.partyId !== this.#partyId), 'Messages must not be from the yourself'); let prevRndMsg = DklsComms.decryptAndVerifyIncomingMessages(prevRoundMessages, this.#authKey); prevRndMsg = DklsTypes.deserializeMessages(prevRndMsg); const thisRoundMsg = this.#dsg.handleIncomingMessages(prevRndMsg); const thisRoundMessage = DklsTypes.serializeMessages(thisRoundMsg); const partyPubKeys = prevRoundMessages.map(m => ({ partyId: m.partyId, publicKey: m.publicKey })); const signedMessage = DklsComms.encryptAndAuthOutgoingMessages( thisRoundMessage, partyPubKeys, this.#authKey ); return this._formatMessage(signedMessage); } /** * Check if the signature is ready * @returns {boolean} */ isSignatureReady() { return this.#round === 5; } /** * Get the signature object once the rounds are complete * @returns {{ r: string, s: string, v: number, pubKey: string }} Signature object */ getSignature() { $.checkState(this.isSignatureReady(), 'Signature not ready'); if (this.#signature) { return this.#signature; } const convertedSignature = DklsUtils.verifyAndConvertDklsSignature( this.#messageHash, this.#dsg.signature, this.#keychain.commonKeyChain.toString('hex'), this.#derivationPath, null, false ); const [recoveryParam, R, S, pubKey] = convertedSignature.split(':'); const signature = { r: '0x' + R, s: '0x' + S, v: parseInt(recoveryParam), pubKey }; this.#signature = signature; return this.#signature; } }; module.exports.Sign = Sign;