UNPKG

bip-schnorr

Version:

Pure JavaScript implementation of the BIP schnorr signature scheme and the muSig multi-signature scheme

386 lines (338 loc) 16.2 kB
/* global describe, it, beforeEach */ const assert = require('assert'); const Buffer = require('safe-buffer').Buffer; const BigInteger = require('bigi'); const convert = require('../src/convert'); const muSig = require('../src/mu-sig'); const schnorr = require('../src/schnorr'); const math = require('../src/math'); const randomBytes = require('randombytes'); const ecurve = require('ecurve'); const curve = ecurve.getCurveByName('secp256k1'); const testVectors = require('./test-vectors-mu-sig.json'); const randomBuffer = (len) => Buffer.from(randomBytes(len)); describe('muSig', () => { describe('deriveKey', () => { testVectors.forEach(vec => { it('can compute pubkeys for privkey ' + vec.privKeys[0], () => { // given const privKeys = vec.privKeys.map(pk => BigInteger.fromHex(pk)); const pubKeys = vec.pubKeys; // when / then for (let i = 0; i < privKeys.length; i++) { const P = curve.G.multiply(privKeys[i]); assert.strictEqual(convert.intToBuffer(P.affineX).toString('hex'), pubKeys[i]); } }); }); }); describe('computeEll', () => { testVectors.forEach(vec => { it('can compute ell ' + vec.ell, () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); // when const result = muSig.computeEll(pubKeys); // then assert.strictEqual(result.toString('hex'), vec.ell); }); }); }); describe('pubKeyCombine', () => { testVectors.forEach(vec => { it('can combine public keys into ' + vec.pubKeyCombined, () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); // when const result = muSig.pubKeyCombine(pubKeys); // then assert.strictEqual(convert.intToBuffer(result.affineX).toString('hex'), vec.pubKeyCombined); }); }); }); describe('computeCoefficient', () => { testVectors.forEach(vec => { it('can compute coefficient for ' + vec.ell, () => { // given const ell = Buffer.from(vec.ell, 'hex'); // when / then for (let i = 0; i < vec.coefficients.length; i++) { const result = convert.intToBuffer(muSig.computeCoefficient(ell, i)); assert.strictEqual(result.toString('hex'), vec.coefficients[i]); } }); }); }); describe('sessionInitialize', () => { testVectors .forEach(vec => { it('can initialize session for combined key ' + vec.pubKeyCombined, () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); const pubKeyCombined = muSig.pubKeyCombine(pubKeys); const pkBuf = convert.intToBuffer(pubKeyCombined.affineX); const pkParity = math.isEven(pubKeyCombined); const ell = muSig.computeEll(pubKeys); const message = Buffer.from(vec.message, 'hex'); for (let i = 0; i < vec.privKeys.length; i++) { const sessionId = Buffer.from(vec.sessionIds[i], 'hex'); const privateKey = BigInteger.fromHex(vec.privKeys[i]); // when const session = muSig.sessionInitialize(sessionId, privateKey, message, pkBuf, pkParity, ell, i); // then assert.strictEqual(ell.toString('hex'), vec.ell); assert.strictEqual(session.commitment.toString('hex'), vec.commitments[i]); assert.strictEqual(convert.intToBuffer(session.secretKey).toString('hex'), vec.secretKeys[i]); assert.strictEqual(convert.intToBuffer(session.secretNonce).toString('hex'), vec.secretNonces[i]); } }); }); }); describe('sessionNonceCombine', () => { testVectors.forEach(vec => { it('can combine nonces into ' + vec.nonceCombined, () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); const pubKeyCombined = muSig.pubKeyCombine(pubKeys); const pkBuf = convert.intToBuffer(pubKeyCombined.affineX); const pkParity = math.isEven(pubKeyCombined); const ell = muSig.computeEll(pubKeys); const message = Buffer.from(vec.message, 'hex'); const sessions = []; for (let i = 0; i < vec.privKeys.length; i++) { const sessionId = Buffer.from(vec.sessionIds[i], 'hex'); const privateKey = BigInteger.fromHex(vec.privKeys[i]); sessions[i] = muSig.sessionInitialize(sessionId, privateKey, message, pkBuf, pkParity, ell, i); } // when const result = muSig.sessionNonceCombine(sessions[0], sessions.map(s => s.nonce)); // then assert.strictEqual(result.toString('hex'), vec.nonceCombined); }); }); }); describe('partialSign', () => { testVectors.forEach((vec, index) => { it('can create partial signatures #' + (index + 1), () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); const pubKeyCombined = muSig.pubKeyCombine(pubKeys); const pkBuf = convert.intToBuffer(pubKeyCombined.affineX); const pkParity = math.isEven(pubKeyCombined); const ell = muSig.computeEll(pubKeys); const message = Buffer.from(vec.message, 'hex'); const sessions = []; for (let i = 0; i < vec.privKeys.length; i++) { const sessionId = Buffer.from(vec.sessionIds[i], 'hex'); const privateKey = BigInteger.fromHex(vec.privKeys[i]); sessions[i] = muSig.sessionInitialize(sessionId, privateKey, message, pkBuf, pkParity, ell, i); } const signerSession = sessions[0]; const nonceCombined = muSig.sessionNonceCombine(signerSession, sessions.map(s => s.nonce)); for (let i = 0; i < sessions.length; i++) { // when sessions[i].combinedNonceParity = signerSession.combinedNonceParity; const result = muSig.partialSign(sessions[i], message, nonceCombined, pkBuf); // then assert.strictEqual(convert.intToBuffer(result).toString('hex'), vec.partialSigs[i]); } }); }); }); describe('partialSigVerify', () => { testVectors.forEach((vec, index) => { it('can verify partial signatures #' + (index + 1), () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); const pubKeyCombined = muSig.pubKeyCombine(pubKeys); const pkBuf = convert.intToBuffer(pubKeyCombined.affineX); const pkParity = math.isEven(pubKeyCombined); const ell = muSig.computeEll(pubKeys); const message = Buffer.from(vec.message, 'hex'); const sessions = []; for (let i = 0; i < vec.privKeys.length; i++) { const sessionId = Buffer.from(vec.sessionIds[i], 'hex'); const privateKey = BigInteger.fromHex(vec.privKeys[i]); sessions[i] = muSig.sessionInitialize(sessionId, privateKey, message, pkBuf, pkParity, ell, i); } const signerSession = sessions[0]; const nonceCombined = muSig.sessionNonceCombine(signerSession, sessions.map(s => s.nonce)); for (let i = 0; i < sessions.length; i++) { sessions[i].combinedNonceParity = signerSession.combinedNonceParity; const partialSig = muSig.partialSign(sessions[i], message, nonceCombined, pkBuf); // when / then try { muSig.partialSigVerify(sessions[i], partialSig, nonceCombined, i, pubKeys[i], sessions[i].nonce); } catch (e) { assert.fail(e); } } }); }); }); describe('partialSigCombine', () => { testVectors.forEach(vec => { it('can combine partial signatures into ' + vec.signature, () => { // given const pubKeys = vec.pubKeys.map(pk => Buffer.from(pk, 'hex')); const pubKeyCombined = muSig.pubKeyCombine(pubKeys); const pkBuf = convert.intToBuffer(pubKeyCombined.affineX); const pkParity = math.isEven(pubKeyCombined); const ell = muSig.computeEll(pubKeys); const message = Buffer.from(vec.message, 'hex'); const sessions = []; for (let i = 0; i < vec.privKeys.length; i++) { const sessionId = Buffer.from(vec.sessionIds[i], 'hex'); const privateKey = BigInteger.fromHex(vec.privKeys[i]); sessions[i] = muSig.sessionInitialize(sessionId, privateKey, message, pkBuf, pkParity, ell, i); } const signerSession = sessions[0]; const nonceCombined = muSig.sessionNonceCombine(signerSession, sessions.map(s => s.nonce)); const partialSignatures = sessions.map(s => { s.combinedNonceParity = signerSession.combinedNonceParity; return muSig.partialSign(s, message, nonceCombined, pkBuf) }); // when const result = muSig.partialSigCombine(nonceCombined, partialSignatures); // then assert.strictEqual(result.toString('hex'), vec.signature); }); }); }); describe('full example', () => { it('can sign and verify example in README', () => { // data known to every participant const publicData = { pubKeys: [ Buffer.from('846f34fdb2345f4bf932cb4b7d278fb3af24f44224fb52ae551781c3a3cad68a', 'hex'), Buffer.from('cd836b1d42c51d80cef695a14502c21d2c3c644bc82f6a7052eb29247cf61f4f', 'hex'), Buffer.from('b8c1765111002f09ba35c468fab273798a9058d1f8a4e276f45a1f1481dd0bdb', 'hex'), ], message: convert.hash(Buffer.from('muSig is awesome!', 'utf8')), pubKeyHash: null, pubKeyCombined: null, pubKeyParity: null, commitments: [], nonces: [], nonceCombined: null, partialSignatures: [], signature: null, }; // data only known by the individual party, these values are never shared // between the signers! const signerPrivateData = [ // signer 1 { privateKey: BigInteger.fromHex('add2b25e2d356bec3770305391cbc80cab3a40057ad836bcb49ef3eed74a3fee'), session: null, }, // signer 2 { privateKey: BigInteger.fromHex('0a1645eef5a10e1f5011269abba9fd85c4f0cc70820d6f102fb7137f2988ad78'), session: null, }, // signer 3 { privateKey: BigInteger.fromHex('2031e7fed15c770519707bb092a6337215530e921ccea42030c15d86e8eaf0b8'), session: null, } ]; // ----------------------------------------------------------------------- // Step 1: Combine the public keys // The public keys P_i are combined into the combined public key P. // This can be done by every signer individually or by the initializing // party and then be distributed to every participant. // ----------------------------------------------------------------------- publicData.pubKeyHash = muSig.computeEll(publicData.pubKeys); const pkCombined = muSig.pubKeyCombine(publicData.pubKeys, publicData.pubKeyHash); publicData.pubKeyCombined = convert.intToBuffer(pkCombined.affineX); publicData.pubKeyParity = math.isEven(pkCombined); // ----------------------------------------------------------------------- // Step 2: Create the private signing session // Each signing party does this in private. The session ID *must* be // unique for every call to sessionInitialize, otherwise it's trivial for // an attacker to extract the secret key! // ----------------------------------------------------------------------- signerPrivateData.forEach((data, idx) => { const sessionId = randomBuffer(32); // must never be reused between sessions! data.session = muSig.sessionInitialize( sessionId, data.privateKey, publicData.message, publicData.pubKeyCombined, publicData.pubKeyParity, publicData.pubKeyHash, idx ); }); const signerSession = signerPrivateData[0].session; // ----------------------------------------------------------------------- // Step 3: Exchange commitments (communication round 1) // The signers now exchange the commitments H(R_i). This is simulated here // by copying the values from the private data to public data array. // ----------------------------------------------------------------------- for (let i = 0; i < publicData.pubKeys.length; i++) { publicData.commitments[i] = signerPrivateData[i].session.commitment; } // ----------------------------------------------------------------------- // Step 4: Get nonces (communication round 2) // Now that everybody has commited to the session, the nonces (R_i) can be // exchanged. Again, this is simulated by copying. // ----------------------------------------------------------------------- for (let i = 0; i < publicData.pubKeys.length; i++) { publicData.nonces[i] = signerPrivateData[i].session.nonce; } // ----------------------------------------------------------------------- // Step 5: Combine nonces // The nonces can now be combined into R. Each participant should do this // and keep track of whether the nonce was negated or not. This is needed // for the later steps. // ----------------------------------------------------------------------- publicData.nonceCombined = muSig.sessionNonceCombine(signerSession, publicData.nonces); signerPrivateData.forEach(data => (data.session.combinedNonceParity = signerSession.combinedNonceParity)); // ----------------------------------------------------------------------- // Step 6: Generate partial signatures // Every participant can now create their partial signature s_i over the // given message. // ----------------------------------------------------------------------- signerPrivateData.forEach(data => { data.session.partialSignature = muSig.partialSign(data.session, publicData.message, publicData.nonceCombined, publicData.pubKeyCombined); }); // ----------------------------------------------------------------------- // Step 7: Exchange partial signatures (communication round 3) // The partial signature of each signer is exchanged with the other // participants. Simulated here by copying. // ----------------------------------------------------------------------- for (let i = 0; i < publicData.pubKeys.length; i++) { publicData.partialSignatures[i] = signerPrivateData[i].session.partialSignature; } // ----------------------------------------------------------------------- // Step 8: Verify individual partial signatures // Every participant should verify the partial signatures received by the // other participants. // ----------------------------------------------------------------------- for (let i = 0; i < publicData.pubKeys.length; i++) { muSig.partialSigVerify( signerSession, publicData.partialSignatures[i], publicData.nonceCombined, i, publicData.pubKeys[i], publicData.nonces[i] ); } // ----------------------------------------------------------------------- // Step 9: Combine partial signatures // Finally, the partial signatures can be combined into the full signature // (s, R) that can be verified against combined public key P. // ----------------------------------------------------------------------- publicData.signature = muSig.partialSigCombine(publicData.nonceCombined, publicData.partialSignatures); // ----------------------------------------------------------------------- // Step 10: Verify signature // The resulting signature can now be verified as a normal Schnorr // signature (s, R) over the message m and public key P. // ----------------------------------------------------------------------- schnorr.verify(publicData.pubKeyCombined, publicData.message, publicData.signature); }); }); });