From a8f7256fcd7c6aeb5132320a8266fb36f0e92b20 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 21:44:25 +0000 Subject: [PATCH 01/15] Add verifyMultipleSignatures method --- src/blst/signature.ts | 8 ++++++++ src/functional.ts | 31 +++++++++++++++++++++++++++++++ src/herumi/signature.ts | 10 +++++++++- src/interface.ts | 3 +++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/blst/signature.ts b/src/blst/signature.ts index 0385233..69b0e0f 100644 --- a/src/blst/signature.ts +++ b/src/blst/signature.ts @@ -28,6 +28,14 @@ export class Signature implements ISignature { return new Signature(agg.toSignature()); } + static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean { + return blst.verifyMultipleAggregateSignatures( + messages, + publicKeys.map((publicKey) => publicKey.affine), + signatures.map((signature) => signature.affine) + ); + } + verify(publicKey: PublicKey, message: Uint8Array): boolean { // Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity if (this.affine.value.is_inf()) { diff --git a/src/functional.ts b/src/functional.ts index 61e04c8..aac05ca 100644 --- a/src/functional.ts +++ b/src/functional.ts @@ -106,6 +106,36 @@ export function functionalInterfaceFactory({ } } + /** + * Verifies multiple signatures at once returning true if all valid or false + * if at least one is not. Optimized method when knowing which signature is + * wrong is not relevant, i.e. verifying an Eth2.0 block. + * https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 + */ + function verifyMultipleSignatures( + publicKeys: Uint8Array[], + messages: Uint8Array[], + signatures: Uint8Array[] + ): boolean { + validateBytes(publicKeys, "publicKey"); + validateBytes(messages, "message"); + validateBytes(signatures, "signatures"); + + if (publicKeys.length === 0 || publicKeys.length !== messages.length || publicKeys.length !== signatures.length) { + return false; + } + try { + return Signature.verifyMultipleSignatures( + publicKeys.map((publicKey) => PublicKey.fromBytes(publicKey)), + messages.map((msg) => msg), + signatures.map((signature) => Signature.fromBytes(signature)) + ); + } catch (e) { + if (e instanceof NotInitializedError) throw e; + return false; + } + } + /** * Computes a public key from a secret key */ @@ -121,6 +151,7 @@ export function functionalInterfaceFactory({ verify, verifyAggregate, verifyMultiple, + verifyMultipleSignatures, secretKeyToPublicKey, }; } diff --git a/src/herumi/signature.ts b/src/herumi/signature.ts index 6a23c89..cca6857 100644 --- a/src/herumi/signature.ts +++ b/src/herumi/signature.ts @@ -1,5 +1,5 @@ import {SIGNATURE_LENGTH} from "../constants"; -import {SignatureType} from "bls-eth-wasm"; +import {SignatureType, multiVerify} from "bls-eth-wasm"; import {getContext} from "./context"; import {PublicKey} from "./publicKey"; import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers"; @@ -45,6 +45,14 @@ export class Signature implements ISignature { return new Signature(signature); } + static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean { + return multiVerify( + publicKeys.map((publicKey) => publicKey.value), + signatures.map((signature) => signature.value), + messages + ); + } + verify(publicKey: PublicKey, message: Uint8Array): boolean { return publicKey.value.verify(this.value, message); } diff --git a/src/interface.ts b/src/interface.ts index 5e9dc5b..822e657 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -13,6 +13,7 @@ export interface IBls { fromBytes(bytes: Uint8Array): Signature; fromHex(hex: string): Signature; aggregate(signatures: Signature[]): Signature; + verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean; }; sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array; @@ -21,6 +22,7 @@ export interface IBls { verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean; verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean; verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean; + verifyMultipleSignatures(publicKeys: Uint8Array[], messages: Uint8Array[], signatures: Uint8Array[]): boolean; secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array; init(): Promise; @@ -49,6 +51,7 @@ export declare class Signature { static fromBytes(bytes: Uint8Array): Signature; static fromHex(hex: string): Signature; static aggregate(signatures: Signature[]): Signature; + static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean; verify(publicKey: PublicKey, message: Uint8Array): boolean; verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean; verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean; From 8d5206c31feb9c1e6576fd8e1d05180a236d2372 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:12:40 +0000 Subject: [PATCH 02/15] Test verifyMultipleSignatures against malicious input --- src/functional.ts | 8 +++- test/unit/index.test.ts | 82 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/functional.ts b/src/functional.ts index aac05ca..e59d0b4 100644 --- a/src/functional.ts +++ b/src/functional.ts @@ -108,8 +108,12 @@ export function functionalInterfaceFactory({ /** * Verifies multiple signatures at once returning true if all valid or false - * if at least one is not. Optimized method when knowing which signature is - * wrong is not relevant, i.e. verifying an Eth2.0 block. + * if at least one is not. Optimization useful when knowing which signature is + * wrong is not relevant, i.e. verifying an entire Eth2.0 block. + * + * This method provides a safe way to do so by multiplying each signature by + * a random number so an attacker cannot craft a malicious signature that won't + * verify on its own but will if it's added to a specific predictable signature * https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 */ function verifyMultipleSignatures( diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index cf50d4a..627919f 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -1,6 +1,7 @@ import {expect} from "chai"; import {IBls} from "../../src/interface"; import {getN, randomMessage} from "../util"; +import {hexToBytes} from "../../src/helpers"; export function runIndexTests(bls: IBls): void { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -90,4 +91,85 @@ export function runIndexTests(bls: IBls): void { expect(isValid).to.be.false; }); }); + + describe("verifyMultipleSignatures", () => { + it("Should verify multiple signatures", () => { + const n = 4; + const dataArr = getN(n, () => { + const sk = bls.SecretKey.fromKeygen(); + const pk = sk.toPublicKey(); + const msg = randomMessage(); + const sig = sk.sign(msg); + return {pk, msg, sig}; + }); + const pks = dataArr.map((data) => data.pk); + const msgs = dataArr.map((data) => data.msg); + const sigs = dataArr.map((data) => data.sig); + + expect( + bls.verifyMultipleSignatures( + pks.map((pk) => pk.toBytes()), + msgs, + sigs.map((sig) => sig.toBytes()) + ) + ).to.equal(true, "functional (bytes serialized) interface failed"); + + expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed"); + }); + + it("Test fails correctly against a malicous signature", async () => { + // Data computed with lower level BLS primitives + // https://github.com/dapplion/eth2-bls-wasm/blob/2d2f3e6a0487e96706bfd8a1b8039c7d6c79f71f/verifyMultipleSignatures.test.js#L40 + // It creates N valid signatures + // It messes with the last 2 signatures, modifying their values + // such that they wqould not fail in aggregate signature verification. + // Creates a random G2 point with is added to the last signature + // and substracted to the second to last signature + const maliciousSignatureTestData = { + pks: [ + "b836ccf44fa01e46745ccc3a47855e959783ef5df5cdcc607354b98d52c16b6613761339bfb833fd525cdca7c8071c6b", + "a317ce36dcf2bf6fd262dbad80427f890bc166152682cb6c600a66eb7d525f200839ab798ca4877c3143a31201905de4", + "b9b7b4f4a88d98f34b4c9ba8ae10e935ba51164ddc045d6ae26b403c87a6934e6c75f9fb5cc4b3b29a1255b316d08de5", + "a386a2bc7e9d13cf9b4ad3c819547534c768aeae6a2414bfcebee50f38aaf85a9d610974db931278c08fe86a91eb2999", + ], + msgs: [ + "690a91fc0a7a49bbc5afe9516c1831ca8845f281ef2e414f7dfeb71b5e91a902", + "3829d4fc2332afc2634079823b89598f3674be5da324b1092b3d8aeb7af5e164", + "9a9406647ed6af16b5ce3e828c5f5ef35f1221ed10476209476c12776ce417ac", + "3e8e4bcb78fda59a43ebfb90970cc6036ce18dc3d3a1b714cc4c1bfc00b8258e", + ], + sigs: [ + "864ed65f224cf4e49e9bbf313d3dc243649885d9bd432a15e6c1259f2e4c29fcefa7a4c3aafaac01519f7c92239702d7096df2971b1801cd26d0ca0d5e7743ccb0abe79d8c383f9bb04ebe553a3094e84d55bc79be7eff5ffdb9b322205acfd1", + "90efd8c82c356956fc170bec2aed874d14cea079625dfe69d8bc375e10fcd96e2c0348dfeb713f1889629ccb9ec95fee0e0c9cc7a728d8a7068701a04192ed585ec761edf6e2c1e44ceaaa61732052af81a6033fa7d375d7f7157909549322da", + "9023f43cc8e05a3e842b242b9f6781a9e2eadbfcbebd1242563e56bb47cd273ef20fc0c5099e05e83093581907bfd02915b5ef8c553918d4524c274a8856950c87c6314a2c003a2ed28e5fb56ddfdb233a2b895c2397bd15629325d95ca43b83", + "82c8fedc6ad43e945bbf7529d55b73d7ce593bc9ea94dfaf91d720b2ab0e51ce551f7fcda96d428b627ff776c94d6f360af425fe7fb4e4469b893071149db747f27a8bd488af7ba7f0edf86c7e551af89d7a55d4fc86968e10f91ed76e68e373", + ], + manipulated: [false, false, true, true], + }; + + const pks = maliciousSignatureTestData.pks.map(hexToBytes); + const msgs = maliciousSignatureTestData.msgs.map(hexToBytes); + const sigs = maliciousSignatureTestData.sigs.map(hexToBytes); + + maliciousSignatureTestData.manipulated.forEach((isManipulated, i) => { + expect(bls.verify(pks[i], msgs[i], sigs[i])).to.equal( + !isManipulated, + isManipulated ? "Manipulated signature should not verify" : "Ok signature should verify" + ); + }); + + // This method (AggregateVerify in BLS spec lingo) should verify + const dangerousAggSig = bls.aggregateSignatures(sigs); + expect(bls.verifyMultiple(pks, msgs, dangerousAggSig)).to.equal( + true, + "Malicious signature should be validated with bls.verifyMultiple" + ); + + // This method is expected to catch the malicious signature and not verify + expect(bls.verifyMultipleSignatures(pks, msgs, sigs)).to.equal( + false, + "Malicous signature should not validate with bls.verifyMultipleSignatures" + ); + }); + }); } From 092165a5f4d4a90c7448aa83e25c07a537339c5b Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:15:39 +0000 Subject: [PATCH 03/15] Move test data to file --- test/data/malicious-signature-test-data.ts | 32 ++++++++++++++++++ test/unit/index.test.ts | 38 +++------------------- 2 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 test/data/malicious-signature-test-data.ts diff --git a/test/data/malicious-signature-test-data.ts b/test/data/malicious-signature-test-data.ts new file mode 100644 index 0000000..1909aac --- /dev/null +++ b/test/data/malicious-signature-test-data.ts @@ -0,0 +1,32 @@ +/* eslint-disable max-len */ + +/** + * Data computed with lower level BLS primitives + * https://github.com/dapplion/eth2-bls-wasm/blob/2d2f3e6a0487e96706bfd8a1b8039c7d6c79f71f/verifyMultipleSignatures.test.js#L40 + * It creates N valid signatures + * It messes with the last 2 signatures, modifying their values + * such that they wqould not fail in aggregate signature verification. + * Creates a random G2 point with is added to the last signature + * and substracted to the second to last signature + */ +export const maliciousVerifyMultipleSignaturesData = { + pks: [ + "b836ccf44fa01e46745ccc3a47855e959783ef5df5cdcc607354b98d52c16b6613761339bfb833fd525cdca7c8071c6b", + "a317ce36dcf2bf6fd262dbad80427f890bc166152682cb6c600a66eb7d525f200839ab798ca4877c3143a31201905de4", + "b9b7b4f4a88d98f34b4c9ba8ae10e935ba51164ddc045d6ae26b403c87a6934e6c75f9fb5cc4b3b29a1255b316d08de5", + "a386a2bc7e9d13cf9b4ad3c819547534c768aeae6a2414bfcebee50f38aaf85a9d610974db931278c08fe86a91eb2999", + ], + msgs: [ + "690a91fc0a7a49bbc5afe9516c1831ca8845f281ef2e414f7dfeb71b5e91a902", + "3829d4fc2332afc2634079823b89598f3674be5da324b1092b3d8aeb7af5e164", + "9a9406647ed6af16b5ce3e828c5f5ef35f1221ed10476209476c12776ce417ac", + "3e8e4bcb78fda59a43ebfb90970cc6036ce18dc3d3a1b714cc4c1bfc00b8258e", + ], + sigs: [ + "864ed65f224cf4e49e9bbf313d3dc243649885d9bd432a15e6c1259f2e4c29fcefa7a4c3aafaac01519f7c92239702d7096df2971b1801cd26d0ca0d5e7743ccb0abe79d8c383f9bb04ebe553a3094e84d55bc79be7eff5ffdb9b322205acfd1", + "90efd8c82c356956fc170bec2aed874d14cea079625dfe69d8bc375e10fcd96e2c0348dfeb713f1889629ccb9ec95fee0e0c9cc7a728d8a7068701a04192ed585ec761edf6e2c1e44ceaaa61732052af81a6033fa7d375d7f7157909549322da", + "9023f43cc8e05a3e842b242b9f6781a9e2eadbfcbebd1242563e56bb47cd273ef20fc0c5099e05e83093581907bfd02915b5ef8c553918d4524c274a8856950c87c6314a2c003a2ed28e5fb56ddfdb233a2b895c2397bd15629325d95ca43b83", + "82c8fedc6ad43e945bbf7529d55b73d7ce593bc9ea94dfaf91d720b2ab0e51ce551f7fcda96d428b627ff776c94d6f360af425fe7fb4e4469b893071149db747f27a8bd488af7ba7f0edf86c7e551af89d7a55d4fc86968e10f91ed76e68e373", + ], + manipulated: [false, false, true, true], +}; diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 627919f..9cc1672 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -2,6 +2,7 @@ import {expect} from "chai"; import {IBls} from "../../src/interface"; import {getN, randomMessage} from "../util"; import {hexToBytes} from "../../src/helpers"; +import {maliciousVerifyMultipleSignaturesData} from "../data/malicious-signature-test-data"; export function runIndexTests(bls: IBls): void { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -118,40 +119,11 @@ export function runIndexTests(bls: IBls): void { }); it("Test fails correctly against a malicous signature", async () => { - // Data computed with lower level BLS primitives - // https://github.com/dapplion/eth2-bls-wasm/blob/2d2f3e6a0487e96706bfd8a1b8039c7d6c79f71f/verifyMultipleSignatures.test.js#L40 - // It creates N valid signatures - // It messes with the last 2 signatures, modifying their values - // such that they wqould not fail in aggregate signature verification. - // Creates a random G2 point with is added to the last signature - // and substracted to the second to last signature - const maliciousSignatureTestData = { - pks: [ - "b836ccf44fa01e46745ccc3a47855e959783ef5df5cdcc607354b98d52c16b6613761339bfb833fd525cdca7c8071c6b", - "a317ce36dcf2bf6fd262dbad80427f890bc166152682cb6c600a66eb7d525f200839ab798ca4877c3143a31201905de4", - "b9b7b4f4a88d98f34b4c9ba8ae10e935ba51164ddc045d6ae26b403c87a6934e6c75f9fb5cc4b3b29a1255b316d08de5", - "a386a2bc7e9d13cf9b4ad3c819547534c768aeae6a2414bfcebee50f38aaf85a9d610974db931278c08fe86a91eb2999", - ], - msgs: [ - "690a91fc0a7a49bbc5afe9516c1831ca8845f281ef2e414f7dfeb71b5e91a902", - "3829d4fc2332afc2634079823b89598f3674be5da324b1092b3d8aeb7af5e164", - "9a9406647ed6af16b5ce3e828c5f5ef35f1221ed10476209476c12776ce417ac", - "3e8e4bcb78fda59a43ebfb90970cc6036ce18dc3d3a1b714cc4c1bfc00b8258e", - ], - sigs: [ - "864ed65f224cf4e49e9bbf313d3dc243649885d9bd432a15e6c1259f2e4c29fcefa7a4c3aafaac01519f7c92239702d7096df2971b1801cd26d0ca0d5e7743ccb0abe79d8c383f9bb04ebe553a3094e84d55bc79be7eff5ffdb9b322205acfd1", - "90efd8c82c356956fc170bec2aed874d14cea079625dfe69d8bc375e10fcd96e2c0348dfeb713f1889629ccb9ec95fee0e0c9cc7a728d8a7068701a04192ed585ec761edf6e2c1e44ceaaa61732052af81a6033fa7d375d7f7157909549322da", - "9023f43cc8e05a3e842b242b9f6781a9e2eadbfcbebd1242563e56bb47cd273ef20fc0c5099e05e83093581907bfd02915b5ef8c553918d4524c274a8856950c87c6314a2c003a2ed28e5fb56ddfdb233a2b895c2397bd15629325d95ca43b83", - "82c8fedc6ad43e945bbf7529d55b73d7ce593bc9ea94dfaf91d720b2ab0e51ce551f7fcda96d428b627ff776c94d6f360af425fe7fb4e4469b893071149db747f27a8bd488af7ba7f0edf86c7e551af89d7a55d4fc86968e10f91ed76e68e373", - ], - manipulated: [false, false, true, true], - }; + const pks = maliciousVerifyMultipleSignaturesData.pks.map(hexToBytes); + const msgs = maliciousVerifyMultipleSignaturesData.msgs.map(hexToBytes); + const sigs = maliciousVerifyMultipleSignaturesData.sigs.map(hexToBytes); - const pks = maliciousSignatureTestData.pks.map(hexToBytes); - const msgs = maliciousSignatureTestData.msgs.map(hexToBytes); - const sigs = maliciousSignatureTestData.sigs.map(hexToBytes); - - maliciousSignatureTestData.manipulated.forEach((isManipulated, i) => { + maliciousVerifyMultipleSignaturesData.manipulated.forEach((isManipulated, i) => { expect(bls.verify(pks[i], msgs[i], sigs[i])).to.equal( !isManipulated, isManipulated ? "Manipulated signature should not verify" : "Ok signature should verify" From fc868ffd277025408e2a2cd26654b52e48041f91 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:21:48 +0000 Subject: [PATCH 04/15] Add benchmark --- benchmark/index.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/benchmark/index.ts b/benchmark/index.ts index e936fd3..f1998fe 100644 --- a/benchmark/index.ts +++ b/benchmark/index.ts @@ -84,6 +84,35 @@ import {aggCount, runs} from "./params"; runs, }); + // Verify multiple signatures + + await runBenchmark<{pks: PublicKey[]; msgs: Uint8Array[]; sigs: Signature[]}, boolean>({ + id: `${implementation} verifyMultipleSignatures (${aggCount})`, + + prepareTest: () => { + const dataArr = range(aggCount).map(() => { + const sk = bls.SecretKey.fromKeygen(); + const pk = sk.toPublicKey(); + const msg = randomMessage(); + const sig = sk.sign(msg); + return {pk, msg, sig}; + }); + + const pks = dataArr.map((data) => data.pk); + const msgs = dataArr.map((data) => data.msg); + const sigs = dataArr.map((data) => data.sig); + + return { + input: {pks, msgs, sigs}, + resultCheck: (valid) => valid === true, + }; + }, + testRunner: ({pks, msgs, sigs}) => { + return bls.Signature.verifyMultipleSignatures(pks, msgs, sigs); + }, + runs, + }); + // Aggregate pubkeys await runBenchmark({ From d2c11ed16ca415c758578ce7ef9cb22cf36714d8 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:32:42 +0000 Subject: [PATCH 05/15] Use class interface for more transparent errors --- test/unit/index.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 9cc1672..47d6074 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -107,6 +107,8 @@ export function runIndexTests(bls: IBls): void { const msgs = dataArr.map((data) => data.msg); const sigs = dataArr.map((data) => data.sig); + expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed"); + expect( bls.verifyMultipleSignatures( pks.map((pk) => pk.toBytes()), @@ -114,31 +116,30 @@ export function runIndexTests(bls: IBls): void { sigs.map((sig) => sig.toBytes()) ) ).to.equal(true, "functional (bytes serialized) interface failed"); - - expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed"); }); it("Test fails correctly against a malicous signature", async () => { - const pks = maliciousVerifyMultipleSignaturesData.pks.map(hexToBytes); + const pks = maliciousVerifyMultipleSignaturesData.pks.map(bls.PublicKey.fromHex); const msgs = maliciousVerifyMultipleSignaturesData.msgs.map(hexToBytes); - const sigs = maliciousVerifyMultipleSignaturesData.sigs.map(hexToBytes); + const sigs = maliciousVerifyMultipleSignaturesData.sigs.map(bls.Signature.fromHex); maliciousVerifyMultipleSignaturesData.manipulated.forEach((isManipulated, i) => { - expect(bls.verify(pks[i], msgs[i], sigs[i])).to.equal( + expect(sigs[i].verify(pks[i], msgs[i])).to.equal( !isManipulated, isManipulated ? "Manipulated signature should not verify" : "Ok signature should verify" ); }); // This method (AggregateVerify in BLS spec lingo) should verify - const dangerousAggSig = bls.aggregateSignatures(sigs); - expect(bls.verifyMultiple(pks, msgs, dangerousAggSig)).to.equal( + + const dangerousAggSig = bls.Signature.aggregate(sigs); + expect(dangerousAggSig.verifyMultiple(pks, msgs)).to.equal( true, "Malicious signature should be validated with bls.verifyMultiple" ); // This method is expected to catch the malicious signature and not verify - expect(bls.verifyMultipleSignatures(pks, msgs, sigs)).to.equal( + expect(bls.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal( false, "Malicous signature should not validate with bls.verifyMultipleSignatures" ); From 93499b3f34f47000459eae51deab39a8fd69f7b8 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:33:44 +0000 Subject: [PATCH 06/15] Fix class method usage --- test/unit/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 47d6074..b9e16a0 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -119,9 +119,9 @@ export function runIndexTests(bls: IBls): void { }); it("Test fails correctly against a malicous signature", async () => { - const pks = maliciousVerifyMultipleSignaturesData.pks.map(bls.PublicKey.fromHex); + const pks = maliciousVerifyMultipleSignaturesData.pks.map((pk) => bls.PublicKey.fromHex(pk)); const msgs = maliciousVerifyMultipleSignaturesData.msgs.map(hexToBytes); - const sigs = maliciousVerifyMultipleSignaturesData.sigs.map(bls.Signature.fromHex); + const sigs = maliciousVerifyMultipleSignaturesData.sigs.map((sig) => bls.Signature.fromHex(sig)); maliciousVerifyMultipleSignaturesData.manipulated.forEach((isManipulated, i) => { expect(sigs[i].verify(pks[i], msgs[i])).to.equal( From 2f0db3f39fd4f630622e46e670ba39c7c2e0f878 Mon Sep 17 00:00:00 2001 From: dapplion Date: Wed, 2 Dec 2020 23:36:58 +0000 Subject: [PATCH 07/15] Disable webpack minification for better debugging on Karma tests --- webpack.config.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 3953a1f..823c4e7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,18 +1,21 @@ module.exports = { entry: "./src/index.ts", - mode: "production", + mode: "development", node: { - fs: "empty" + fs: "empty", }, output: { - filename: "dist/bundle.js" + filename: "dist/bundle.js", }, resolve: { - extensions: [".ts", ".js"] + extensions: [".ts", ".js"], }, module: { - rules: [ - {test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}} - ] - } -}; \ No newline at end of file + rules: [{test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}}], + }, + optimization: { + // Disable minification for better debugging on Karma tests + minimize: false, + }, + devtool: "source-map", +}; From d4d97795cae1516dbc11c23b82dc8b2c37fc4d42 Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:05:14 +0000 Subject: [PATCH 08/15] Remove comment --- src/helpers/hex.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/helpers/hex.ts b/src/helpers/hex.ts index 9470cb8..2ba1236 100644 --- a/src/helpers/hex.ts +++ b/src/helpers/hex.ts @@ -26,7 +26,6 @@ export function hexToBytes(hex: string): Uint8Array { * From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50 */ export function bytesToHex(bytes: Uint8Array): string { - // return "0x" + Buffer.from(bytes).toString("hex"); let s = ""; const n = bytes.length; From 13ea412c3faba523222e49a3991aee29c1fec3ad Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:05:49 +0000 Subject: [PATCH 09/15] Move web test to a different folder to not run herumi tests twice --- karma.conf.js | 39 +++++++++---------- .../run-web-implementation.test.ts | 6 +-- 2 files changed, 22 insertions(+), 23 deletions(-) rename test/{unit => unit-web}/run-web-implementation.test.ts (76%) diff --git a/karma.conf.js b/karma.conf.js index 1423525..09c45ac 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,26 +1,25 @@ // eslint-disable-next-line @typescript-eslint/no-require-imports const webpackConfig = require("./webpack.config"); -module.exports = function(config) { - config.set({ +module.exports = function (config) { + config.set({ + basePath: "", + frameworks: ["mocha", "chai"], + files: ["test/unit-web/run-web-implementation.test.ts", "test/unit/index-named-exports.test.ts"], + exclude: [], + preprocessors: { + "test/**/*.ts": ["webpack"], + }, + webpack: { + mode: "production", + node: webpackConfig.node, + module: webpackConfig.module, + resolve: webpackConfig.resolve, + }, + reporters: ["spec"], - basePath: "", - frameworks: ["mocha", "chai"], - files: ["test/unit/run-web-implementation.test.ts", "test/unit/index-named-exports.test.ts"], - exclude: [], - preprocessors: { - "test/**/*.ts": ["webpack"] - }, - webpack: { - mode: "production", - node: webpackConfig.node, - module: webpackConfig.module, - resolve: webpackConfig.resolve - }, - reporters: ["spec"], + browsers: ["ChromeHeadless"], - browsers: ["ChromeHeadless"], - - singleRun: true - }); + singleRun: true, + }); }; diff --git a/test/unit/run-web-implementation.test.ts b/test/unit-web/run-web-implementation.test.ts similarity index 76% rename from test/unit/run-web-implementation.test.ts rename to test/unit-web/run-web-implementation.test.ts index 1ef54f0..c1cc7ed 100644 --- a/test/unit/run-web-implementation.test.ts +++ b/test/unit-web/run-web-implementation.test.ts @@ -1,7 +1,7 @@ import herumi from "../../src/herumi"; -import {runSecretKeyTests} from "./secretKey.test"; -import {runPublicKeyTests} from "./publicKey.test"; -import {runIndexTests} from "./index.test"; +import {runSecretKeyTests} from "../unit/secretKey.test"; +import {runPublicKeyTests} from "../unit/publicKey.test"; +import {runIndexTests} from "../unit/index.test"; // This file is intended to be compiled and run by Karma // Do not import the node.bindings or it will break with: From 7f76672a40e4f606d98e136b70374226faed4cba Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:06:36 +0000 Subject: [PATCH 10/15] Use browser friendly concatUint8Arrays instead of Buffer.concat --- src/helpers/utils.ts | 14 ++++++++++++++ src/herumi/signature.ts | 5 ++--- test/unit/helpers/bytes.test.ts | 25 ++++++++++++++++++++----- test/unit/helpers/hex.test.ts | 5 +---- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index 114ab7b..76eae27 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -20,3 +20,17 @@ export function validateBytes( export function isZeroUint8Array(bytes: Uint8Array): boolean { return bytes.every((byte) => byte === 0); } + +export function concatUint8Arrays(bytesArr: Uint8Array[]): Uint8Array { + const totalLen = bytesArr.reduce((total, bytes) => total + bytes.length, 0); + + const merged = new Uint8Array(totalLen); + let mergedLen = 0; + + for (const bytes of bytesArr) { + merged.set(bytes, mergedLen); + mergedLen += bytes.length; + } + + return merged; +} diff --git a/src/herumi/signature.ts b/src/herumi/signature.ts index cca6857..e8c41be 100644 --- a/src/herumi/signature.ts +++ b/src/herumi/signature.ts @@ -2,7 +2,7 @@ import {SIGNATURE_LENGTH} from "../constants"; import {SignatureType, multiVerify} from "bls-eth-wasm"; import {getContext} from "./context"; import {PublicKey} from "./publicKey"; -import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers"; +import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers"; import {Signature as ISignature} from "../interface"; import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors"; @@ -65,10 +65,9 @@ export class Signature implements ISignature { } verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean { - const msgs = Buffer.concat(messages); return this.value.aggregateVerifyNoCheck( publicKeys.map((key) => key.value), - msgs + concatUint8Arrays(messages) ); } diff --git a/test/unit/helpers/bytes.test.ts b/test/unit/helpers/bytes.test.ts index 2b14df7..952aaa5 100644 --- a/test/unit/helpers/bytes.test.ts +++ b/test/unit/helpers/bytes.test.ts @@ -1,5 +1,6 @@ import {expect} from "chai"; -import {isZeroUint8Array} from "../../../src/helpers/utils"; +import {concatUint8Arrays, isZeroUint8Array} from "../../../src/helpers/utils"; +import {hexToBytesNode} from "../../util"; describe("helpers / bytes", () => { describe("isZeroUint8Array", () => { @@ -21,8 +22,22 @@ describe("helpers / bytes", () => { }); } }); -}); -function hexToBytesNode(hex: string): Buffer { - return Buffer.from(hex.replace("0x", ""), "hex"); -} + describe("concatUint8Arrays", () => { + it("Should merge multiple Uint8Array", () => { + const bytesArr = [ + new Uint8Array([1, 2, 3]), + new Uint8Array([4, 5]), + new Uint8Array([6]), + new Uint8Array([7, 8]), + new Uint8Array([9, 10, 11]), + ]; + + const expectedBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + + const bytes = concatUint8Arrays(bytesArr); + + expect(bytes.toString()).to.equal(expectedBytes.toString()); + }); + }); +}); diff --git a/test/unit/helpers/hex.test.ts b/test/unit/helpers/hex.test.ts index 93d5c09..cca61a3 100644 --- a/test/unit/helpers/hex.test.ts +++ b/test/unit/helpers/hex.test.ts @@ -1,5 +1,6 @@ import {expect} from "chai"; import {hexToBytes, bytesToHex} from "../../../src/helpers/hex"; +import {hexToBytesNode} from "../../util"; describe("helpers / hex", () => { const testCases: {id: string; hex: string}[] = [ @@ -23,7 +24,3 @@ describe("helpers / hex", () => { }); } }); - -function hexToBytesNode(hex: string): Buffer { - return Buffer.from(hex.replace("0x", ""), "hex"); -} From 2a27b1928781ef53bf453c20b4b71c7747b2bd1d Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:06:46 +0000 Subject: [PATCH 11/15] Run webpack on production mode --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 823c4e7..62f6c6e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,6 +1,6 @@ module.exports = { entry: "./src/index.ts", - mode: "development", + mode: "production", node: { fs: "empty", }, From 84488456de90643d05965f698f50c903b60e05ec Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:24:30 +0000 Subject: [PATCH 12/15] Patch herumi's multiVerify() on browsers --- src/herumi/context.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/herumi/context.ts b/src/herumi/context.ts index 7780283..5a56065 100644 --- a/src/herumi/context.ts +++ b/src/herumi/context.ts @@ -9,6 +9,16 @@ let blsGlobalPromise: Promise | null = null; export async function setupBls(): Promise { if (!blsGlobal) { await bls.init(bls.BLS12_381); + + // Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto + // @ts-ignore + if (typeof window === "object") { + // @ts-ignore + const crypto = window.crypto || window.msCrypto; + // @ts-ignore + bls.getRandomValues = (x) => crypto.getRandomValues(x); + } + blsGlobal = bls; } } From 0b10c2cb4e4d850b1789999836f8f995daa7e03a Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 00:31:52 +0000 Subject: [PATCH 13/15] Add deleted method in tests util --- test/util.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/util.ts b/test/util.ts index 37e835a..be1f2ad 100644 --- a/test/util.ts +++ b/test/util.ts @@ -13,3 +13,11 @@ export function range(n: number): number[] { for (let i = 0; i < n; i++) nums.push(i); return nums; } + +/** + * ONLY for NodeJS tests, for any other use src/helpers/hex hexToBytes() + * Serves as a "ground-truth" reference + */ +export function hexToBytesNode(hex: string): Buffer { + return Buffer.from(hex.replace("0x", ""), "hex"); +} From c8798707d9ba0b32d93eff23cef78f3ceca087f0 Mon Sep 17 00:00:00 2001 From: dapplion Date: Thu, 3 Dec 2020 19:09:15 +0000 Subject: [PATCH 14/15] Remove 2 tsconfig-ignore --- src/herumi/context.ts | 10 ++++++++-- tsconfig.json | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/herumi/context.ts b/src/herumi/context.ts index 5a56065..b23997c 100644 --- a/src/herumi/context.ts +++ b/src/herumi/context.ts @@ -6,15 +6,21 @@ type Bls = typeof bls; let blsGlobal: Bls | null = null; let blsGlobalPromise: Promise | null = null; +// Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto +declare global { + interface Window { + msCrypto: typeof window["crypto"]; + } +} + export async function setupBls(): Promise { if (!blsGlobal) { await bls.init(bls.BLS12_381); // Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto - // @ts-ignore if (typeof window === "object") { - // @ts-ignore const crypto = window.crypto || window.msCrypto; + // getRandomValues is not typed in `bls-eth-wasm` because it's not meant to be exposed // @ts-ignore bls.getRandomValues = (x) => crypto.getRandomValues(x); } diff --git a/tsconfig.json b/tsconfig.json index a15932d..5b70769 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "target": "esnext", "module": "commonjs", "pretty": true, - "lib": ["esnext.bigint"], + "lib": ["esnext.bigint", "DOM"], "typeRoots": ["./node_modules/@types"], "declaration": true, "strict": true, From 7ef455157a41579042155890808f323c92de3b9a Mon Sep 17 00:00:00 2001 From: dapplion Date: Fri, 4 Dec 2020 09:31:35 +0000 Subject: [PATCH 15/15] Allow window to not be prefixed --- src/herumi/context.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/herumi/context.ts b/src/herumi/context.ts index b23997c..9527630 100644 --- a/src/herumi/context.ts +++ b/src/herumi/context.ts @@ -8,6 +8,7 @@ let blsGlobalPromise: Promise | null = null; // Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto declare global { + // eslint-disable-next-line @typescript-eslint/interface-name-prefix interface Window { msCrypto: typeof window["crypto"]; }