Skip to content

[BWC/BWS] Add TSS to BWC & BWS #3895

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: v11
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4488baf
more BWC cleanup, type enhancements, async request methods
kajoseph Apr 17, 2025
9b5ffaa
imports lint
kajoseph Apr 17, 2025
b53f140
WIP: add TSS to BWS/BWC
kajoseph Apr 17, 2025
b2d6027
Merge branch 'master' of github.com:bitpay/bitcore into addTssToBws
kajoseph Apr 28, 2025
dbc2cdc
Merge branch 'master' of github.com:bitpay/bitcore into addTssToBws
kajoseph Apr 29, 2025
4a92ea3
Merge branch 'master' of github.com:bitpay/bitcore into addTssToBws
kajoseph Apr 30, 2025
05cf09f
bws/bwc keygen complete
kajoseph Apr 30, 2025
1e9769a
cleanup
kajoseph May 1, 2025
cb5bb8c
tss signature generation
kajoseph May 6, 2025
d1f8a98
add input validation
kajoseph May 6, 2025
d95b343
modernize ClientErrors
kajoseph May 7, 2025
95bdd8b
lint test helper files & rm tingodb refs
kajoseph May 8, 2025
d3d1921
bit o cleanup
kajoseph May 8, 2025
f208e13
tss sig generation
kajoseph May 8, 2025
7a76612
Merge branch 'master' of github.com:bitpay/bitcore into addTssToBws
kajoseph May 14, 2025
7d25ea0
merge fix - rm dup interface; lint
kajoseph May 14, 2025
93ce0b6
fix tests after merge
kajoseph May 20, 2025
f221b7f
add keygen password; add tests
kajoseph May 21, 2025
46115b5
cleanup key class
kajoseph May 21, 2025
0fe8009
cleanup bwc key class
kajoseph May 21, 2025
5f6134f
add sign() JSdoc
kajoseph May 21, 2025
fee4490
check fn presence on algo
kajoseph May 21, 2025
1d59f1f
Merge branch 'bwcCleanup' of github.com:kajoseph/bitcore into addTssT…
kajoseph May 21, 2025
e522468
add btc key check to ecdsa test
kajoseph May 22, 2025
07bb210
wallet creation
kajoseph May 23, 2025
cbebb48
lint
kajoseph May 23, 2025
a47b688
fix tests
kajoseph May 23, 2025
4b0f6fd
Merge branch 'v11' of github.com:bitpay/bitcore into addTssToBws
kajoseph May 23, 2025
d40133c
add key backup
kajoseph May 23, 2025
f9b3352
update documentation & tweak data structures for clarity
kajoseph May 26, 2025
eb36c94
improve intellisense typing; make typing more consistent
kajoseph May 26, 2025
7f67427
lint
kajoseph May 26, 2025
6c748c4
fix bws tss tests
kajoseph May 26, 2025
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: 25 additions & 18 deletions packages/bitcore-tss/ecdsa/keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ class KeyGen {
#round;

/**
*
* @param {Object} params
* @param {number} params.n - number of participants
* @param {number} params.m - minimum number of signers
* @param {number} params.partyId - party id
* @param {Buffer} params.seed - OPTIONAL seed for the DKG
* @param {Buffer} params.authKey - authentication key for the DKG
* @param {number} params.round - round number for the DKG
* Create a new Threshold Signature Scheme (TSS) key generation instance
* @param {object} params
* @param {number} params.n Number of participants
* @param {number} params.m Minimum number of signers
* @param {number} params.partyId Party id
* @param {Buffer} [params.seed] Seed for the DKG. Randomly generated if not given
* @param {Buffer} params.authKey Authentication key for the DKG
* @param {number} params.round Round number for the DKG
*/
constructor({ n, m, partyId, seed, authKey, round }) {
$.checkArgument(n != null, 'n is required');
Expand Down Expand Up @@ -51,6 +51,10 @@ class KeyGen {
this.#dkg = new DklsDkg.Dkg(this.#partySize, this.#minSigners, this.#partyId, this.#seed);
}

getRound() {
return this.#round;
}

/**
* Export the keygen session to a base64 encoded string
* @returns {string} Base64 encoded session string
Expand All @@ -60,25 +64,27 @@ class KeyGen {
$.checkState(!this.isKeyChainReady(), 'Cannot export a completed session. The keychain is ready with getKeyChain()');
const chainCodeCommitment = this.#dkg.chainCodeCommitment;
const sessionBytes = this.#dkg.dkgSessionBytes || this.#dkg.dkgSession?.toBytes();
const seedBytes = this.#dkg.seed;
const payload = this.#round +
':' + this.#partySize +
':' + this.#minSigners +
':' + this.#partyId +
':' + Buffer.from(sessionBytes).toString('base64') +
':' + Buffer.from(chainCodeCommitment || []).toString('base64');
':' + Buffer.from(chainCodeCommitment || []).toString('base64') +
':' + seedBytes.toString('base64');

const buf = encrypt(Buffer.from(payload, 'utf8'), this.#authKey.publicKey, this.#authKey);
return buf.toString('base64');
}

/**
* Restore a keygen session from an exported session
* @param {Object} params
* @param {object} params
* @param {string} params.session Base64 encoded session string
* @param {bitcoreLib.PrivateKey} params.authKey Private key to use for decrypting the session
* @param {Buffer} params.seed Seed used for key generation
* @returns {Sign}
*/
static async restore({ session, authKey, seed }) {
static async restore({ session, 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');
Expand All @@ -88,15 +94,16 @@ class KeyGen {
minSigners,
partyId,
sessionBytes,
chainCodeCommitment
chainCodeCommitment,
seedBytes
] = session.split(':');
const initParams = {
round: parseInt(round),
n: parseInt(partySize),
m: parseInt(minSigners),
partyId: parseInt(partyId),
authKey,
seed: Buffer.from(seed, 'base64'),
seed: Buffer.from(seedBytes, 'base64')
};
const keygen = new KeyGen(initParams);
await keygen.#dkg.loadDklsWasm();
Expand All @@ -109,7 +116,7 @@ class KeyGen {
/**
* @private
* Format the message to be sent to the other parties
* @param {Object} signedMessage
* @param {object} signedMessage
* @returns
*/
_formatMessage(signedMessage) {
Expand All @@ -124,7 +131,7 @@ class KeyGen {

/**
* Initialize the keygen session with a broadcast message to send to the other participants
* @returns {Promise<{round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessages: Object[]}>}
* @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 ');
Expand All @@ -143,8 +150,8 @@ class KeyGen {
/**
* Call this after receiving the initJoin broadcast messages from the other participants
* and while isKeyChainReady() is false
* @param {Array<Object>} prevRoundMessages
* @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: Object[], broadcastMessage: Object[] }}
* @param {Array<object>} prevRoundMessages
* @returns {{ round: number, partyId: number, publicKey: string, p2pMessages: object[], broadcastMessage: object[] }}
*/
nextRound(prevRoundMessages) {
$.checkState(this.#round > 0, 'initJoin must be called before participating in the rounds');
Expand Down
25 changes: 15 additions & 10 deletions packages/bitcore-tss/ecdsa/sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { DklsComms } = require('./dklsComms');
const { encrypt, decrypt } = require('./utils');

const $ = bitcoreLib.util.preconditions;
const jsUtil = bitcoreLib.util.js;

class Sign {
#keychain;
Expand All @@ -18,18 +19,18 @@ class Sign {
#signature;

/**
*
* Create a new Threshold Signature Scheme (TSS) signature generation instance
* @param {Object} params
* @param {Object} params.keychain - keychain object generated by the KeyGen class
* @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.
* @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] 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');
Expand All @@ -40,7 +41,7 @@ class Sign {
$.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');
$.checkArgument(Buffer.isBuffer(keychain.commonKeyChain) || jsUtil.isHexa(keychain.commonKeyChain), 'keychain.commonKeyChain must be a buffer or hex string');
this.#keychain = keychain;

$.checkArgument(partyId >= 0 && partyId < n, 'partyId must be in the range [0, n-1]');
Expand All @@ -67,6 +68,10 @@ class Sign {
this.#dsg = new DklsDsg.Dsg(this.#keychain.privateKeyShare, this.#partyId, this.#derivationPath, this.#messageHash);
}

getRound() {
return this.#round;
}

/**
* Export the signing session to a base64 encoded string
* @returns {string} Base64 encoded session string
Expand Down
43 changes: 23 additions & 20 deletions packages/bitcore-tss/ecies/ecies.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,20 @@ function KDF(privateKey, publicKey) {

/**
* Encrypts the message (String or Buffer) using EC keys.
* @param {Object} params
* @param {Buffer|string} params.message - Message to be encrypted.
* @param {PublicKey} params.publicKey - Receipient's public key is used to encrypt the message.
* @param {PrivateKey} params.privateKey - Your private key is used to sign the payload.
* @param {Buffer} params.ivbuf (optional) 16-byte initialization vector (IV) Buffer to be used in AES-CBC.
* By default, `ivbuf` is computed deterministically from message and private key using HMAC-SHA256.
* Deterministic IV enables end-to-end test vectors for alternative implementations.
* @param {object} params
* @param {Buffer|string} params.message Message to be encrypted.
* @param {PublicKey} params.publicKey Receipient's public key is used to encrypt the message.
* @param {PrivateKey} params.privateKey Your private key is used to sign the payload.
* @param {Buffer} [params.ivbuf] 16-byte initialization vector (IV) Buffer to be used in AES-CBC.
* By default, `ivbuf` is randomly generated.
* @param {object} [params.opts] Options object. Every field is optional.
* @param {boolean} [params.opts.noKey] Do not include pubkey in the output.
* @param {boolean} [params.opts.shortTag] Use 4-byte tag instead of 32-byte. This must be communicated to the payload recipient.
* @param {boolean} [params.opts.deterministicIv] Compute IV deterministically from message and private key using HMAC-SHA256.
* A deterministic IV enables end-to-end test vectors for alternative implementations.
* Note that identical messages have identical ciphertexts. If it is important to not allow an attacker
* to learn that a message is repeated, then you should use a custom IV *or* use some sequence identifier
* or "salt" inside the message.
* @param {Object} params.opts (optional) Options object. Every field is optional.
* @param {boolean} params.opts.noKey - Do not include pubkey in the output.
* @param {boolean} params.opts.shortTag - Use 4-byte tag instead of 32-byte. This must be communicated to the payload recipient.
* to learn that a message is repeated, then you should leave opts.deterministicIv to false, pass in a custom IV
* with `ivbuf`, or use a salt inside the message.
* @returns {Buffer} Payload buffer with `pubkey|iv|ciphertext|tag` (pubkey is excluded if `noKey` is given).
*/
function encrypt({ message, publicKey, privateKey, ivbuf, opts = {} }) {
Expand All @@ -49,8 +50,10 @@ function encrypt({ message, publicKey, privateKey, ivbuf, opts = {} }) {
if (!Buffer.isBuffer(message)) {
message = Buffer.from(message);
}
if (!ivbuf) {
if (opts.deterministicIv) {
ivbuf = Hash.sha256hmac(message, privateKey.toBuffer()).subarray(0, 16);
} else if (!ivbuf) {
ivbuf = crypto.randomBytes(16);
}
if (!(publicKey instanceof PublicKey)) {
publicKey = new PublicKey(publicKey);
Expand Down Expand Up @@ -81,18 +84,18 @@ function encrypt({ message, publicKey, privateKey, ivbuf, opts = {} }) {

/**
* Decrypt the payload
* @param {Object} params
* @param {Buffer} params.payload - Encrypted payload buffer.
* @param {PrivateKey} params.privateKey - Your private key is used to decrypt the payload.
* @param {PublicKey} params.publicKey - Sender's public key is used to verify the payload.
* @param {object} params
* @param {Buffer} params.payload Encrypted payload buffer.
* @param {PrivateKey} params.privateKey Your private key is used to decrypt the payload.
* @param {PublicKey} params.publicKey Sender's public key is used to verify the payload.
* *Only* include this if the encrypter specified the `noKey` option, otherwise the public key is included in the payload.
* @param {Object} params.opts (optional) Options object. Every field is optional.
* @param {boolean} params.opts.shortTag - Use 4-byte tag instead of 32-byte.
* @param {object} [params.opts] Options object. Every field is optional.
* @param {boolean} [params.opts.shortTag] - Use 4-byte tag instead of 32-byte.
* This was decided during encryption and must be communicated by the sender.
* @returns {Buffer} Decrypted message buffer.
*/
function decrypt({ payload, privateKey, publicKey, opts = {} }) {
$.checkArgument(Buffer.isBuffer(payload), '`buffer` must be a Buffer');
$.checkArgument(Buffer.isBuffer(payload), 'payload must be a Buffer');
$.checkArgument(privateKey, 'privateKey is required');

if (!(privateKey instanceof PrivateKey)) {
Expand Down
12 changes: 6 additions & 6 deletions packages/bitcore-tss/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module.exports = {
// ECDSA
...require('./ecdsa/keygen'),
...require('./ecdsa/sign'),

// ECIES
...require('./ecies/ecies')
ECDSA: {
KeyGen: require('./ecdsa/keygen').KeyGen,
Sign: require('./ecdsa/sign').Sign,
},
utils: require('./ecdsa/utils'),
ECIES: require('./ecies/ecies')
}
9 changes: 9 additions & 0 deletions packages/bitcore-tss/test/data/vectors.ecdsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ module.exports.vectors = [
evmAddress: {
address: '0xD57cF5ac4CC763D83E0892a07a02fE1BBD123b27'
},
btcAddress: {
xPub: 'xpub661MyMwAqRbcGvRAUJpL6nMhYqanMb6xT7fJw7au2CxhA4Ye661AS6JHze8vGLdaxsxwmqw7g8iXKJn4TgC4Rzq87GueZbzzv1jz6XMsnr2',
},
party0: {
seed: Buffer.from('0d18dd84ff2e7e462bdca9fb362dce0590badac80438234a6be4b859d674355d', 'hex'),
keychain: twoOfThree.party0Key,
Expand Down Expand Up @@ -206,6 +209,9 @@ module.exports.vectors = [
evmAddress: {
address: '0xD57cF5ac4CC763D83E0892a07a02fE1BBD123b27'
},
btcAddress: {
xPub: 'xpub661MyMwAqRbcGMxfc55KDe3yBDLATHWyyERQuybvR7umom3z8QAQB9q2zDB1GQcnvcJfhiytC5sBxt8HsGLq3823rvNMEdp2sRZWHCpbvow',
},
party0: {
seed: Buffer.from('0d18dd84ff2e7e462bdca9fb362dce0590badac80438234a6be4b859d674355d', 'hex'),
keychain: threeOfThree.party0Key,
Expand Down Expand Up @@ -403,6 +409,9 @@ module.exports.vectors = [
evmAddress: {
address: '0xEa08Bdc953DFd1Fd017c3Bb17B781Be13A830aD7'
},
btcAddress: {
xPub: 'xpub661MyMwAqRbcEdUKkUS4CfQNmWXn74P1HM9GPsKtkhgFL6GfJNH4QZJ9XeYibd6mtCn35yzsx5i2BQXY9Tqd6YPaXaMsbnpmBw9AWuNwNCB',
},
party0: {
seed: Buffer.from('0d18dd84ff2e7e462bdca9fb362dce0590badac80438234a6be4b859d674355d', 'hex'),
keychain: oneOfFour.party0Key,
Expand Down
14 changes: 11 additions & 3 deletions packages/bitcore-tss/test/ecdsa.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ describe('ECDSA', function() {
assert.strictEqual(typeof session, 'string');

const keygen = await KeyGen.restore({
seed: seeds[party],
authKey: authKeys[party],
session
});
Expand Down Expand Up @@ -166,7 +165,6 @@ describe('ECDSA', function() {
assert.strictEqual(typeof session, 'string');

const keygen = await KeyGen.restore({
seed: seeds[party],
authKey: authKeys[party],
session
});
Expand Down Expand Up @@ -209,7 +207,6 @@ describe('ECDSA', function() {
assert.strictEqual(typeof session, 'string');

const keygen = await KeyGen.restore({
seed: seeds[party],
authKey: authKeys[party],
session
});
Expand Down Expand Up @@ -265,6 +262,17 @@ describe('ECDSA', function() {
const evmAddress = CWC.Deriver.getAddress('ETH', 'mainnet', pubkey);
assert.strictEqual(evmAddress, vector.evmAddress.address);
}
if (vector.btcAddress) {
const xPubKey = new bitcoreLib.HDPublicKey({
network: 'livenet',
depth: 0,
parentFingerPrint: 0,
childIndex: 0,
publicKey: pubkey,
chainCode: chaincode
});
assert.strictEqual(xPubKey.toString(), vector.btcAddress.xPub);
}
});
}
});
Expand Down
25 changes: 20 additions & 5 deletions packages/bitcore-tss/test/ecies.test.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A roundtrip failure test may be good - add shortTag to either the encrypt or decrypt, but not both.

Truthiness tests may be good. What's the expected behavior if these flags are "true"? "false"?

Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('ECIES', function() {
});

it('correctly encrypts a message', function() {
const ciphertext = alice.encrypt(message);
const ciphertext = alice.encrypt(message, { deterministicIv: true });
assert.strictEqual(Buffer.isBuffer(ciphertext), true);
assert.strictEqual(ciphertext.toString('hex'), encrypted)
});
Expand All @@ -51,29 +51,34 @@ describe('ECIES', function() {
});

it('correctly encrypts a message without key', function() {
const ciphertext = alice.encrypt(message, { noKey: true });
const ciphertext = alice.encrypt(message, { noKey: true, deterministicIv: true });
assert.strictEqual(Buffer.isBuffer(ciphertext), true);
assert.strictEqual(ciphertext.toString('hex'), encryptedNoKey)
});

it('correctly decrypts a message without key', function() {
const decrypted = bob.decrypt(encNoKeyBuf, { noKey: true });
const decrypted = bob.decrypt(encNoKeyBuf, { noKey: true, deterministicIv: true });
assert.strictEqual(Buffer.isBuffer(decrypted), true);
assert.strictEqual(decrypted.toString(), message);
});

it('correctly encrypts a message with short tag', function() {
const ciphertext = alice.encrypt(message, { shortTag: true });
const ciphertext = alice.encrypt(message, { shortTag: true, deterministicIv: true });
assert.strictEqual(Buffer.isBuffer(ciphertext), true);
assert.strictEqual(ciphertext.toString('hex'), encryptedShortTag)
});

it('correctly decrypts a message with short tag', function() {
const decrypted = bob.decrypt(encShortTagBuf, { shortTag: true });
const decrypted = bob.decrypt(encShortTagBuf, { shortTag: true, deterministicIv: true });
assert.strictEqual(Buffer.isBuffer(decrypted), true);
assert.strictEqual(decrypted.toString(), message);
});

it('encrypts a message with random IV', function() {
const ciphertext = alice.encrypt(message);
assert.strictEqual(Buffer.isBuffer(ciphertext), true);
assert.notEqual(ciphertext.toString('hex'), encrypted);
});

it('roundtrips', function() {
const secret = 'some secret message!!!';
Expand Down Expand Up @@ -104,6 +109,16 @@ describe('ECIES', function() {
assert.strictEqual(decrypted, secret);
});

it('roundtrips (deterministic iv)', function() {
const opts = { deterministicIv: true };
const secret = 'some secret message!!!';
const encrypted = alice.encrypt(secret, opts);
const decrypted = bob
.decrypt(encrypted, opts)
.toString();
assert.strictEqual(decrypted, secret);
});

it('roundtrips (no public key & short tag)', function() {
const opts = { noKey: true, shortTag: true };
const secret = 'some secret message!!!';
Expand Down
Loading