Merge pull request #58 from ChainSafe/dapplion/verifyMultipleSignatures
Add verifyMultipleSignatures method
This commit is contained in:
commit
d834657542
|
@ -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<PublicKey[], void>({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -106,6 +106,40 @@ export function functionalInterfaceFactory({
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies multiple signatures at once returning true if all valid or false
|
||||
* 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(
|
||||
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 +155,7 @@ export function functionalInterfaceFactory({
|
|||
verify,
|
||||
verifyAggregate,
|
||||
verifyMultiple,
|
||||
verifyMultipleSignatures,
|
||||
secretKeyToPublicKey,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,26 @@ type Bls = typeof bls;
|
|||
let blsGlobal: Bls | null = null;
|
||||
let blsGlobalPromise: Promise<void> | 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"];
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupBls(): Promise<void> {
|
||||
if (!blsGlobal) {
|
||||
await bls.init(bls.BLS12_381);
|
||||
|
||||
// Patch to fix multiVerify() calls on a browser with polyfilled NodeJS crypto
|
||||
if (typeof window === "object") {
|
||||
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);
|
||||
}
|
||||
|
||||
blsGlobal = bls;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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";
|
||||
import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers";
|
||||
import {Signature as ISignature} from "../interface";
|
||||
import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors";
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -57,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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<void>;
|
||||
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
};
|
|
@ -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:
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
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
|
||||
|
@ -90,4 +92,57 @@ 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.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(true, "class interface failed");
|
||||
|
||||
expect(
|
||||
bls.verifyMultipleSignatures(
|
||||
pks.map((pk) => pk.toBytes()),
|
||||
msgs,
|
||||
sigs.map((sig) => sig.toBytes())
|
||||
)
|
||||
).to.equal(true, "functional (bytes serialized) interface failed");
|
||||
});
|
||||
|
||||
it("Test fails correctly against a malicous signature", async () => {
|
||||
const pks = maliciousVerifyMultipleSignaturesData.pks.map((pk) => bls.PublicKey.fromHex(pk));
|
||||
const msgs = maliciousVerifyMultipleSignaturesData.msgs.map(hexToBytes);
|
||||
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(
|
||||
!isManipulated,
|
||||
isManipulated ? "Manipulated signature should not verify" : "Ok signature should verify"
|
||||
);
|
||||
});
|
||||
|
||||
// This method (AggregateVerify in BLS spec lingo) should verify
|
||||
|
||||
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.Signature.verifyMultipleSignatures(pks, msgs, sigs)).to.equal(
|
||||
false,
|
||||
"Malicous signature should not validate with bls.verifyMultipleSignatures"
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,17 +2,20 @@ module.exports = {
|
|||
entry: "./src/index.ts",
|
||||
mode: "production",
|
||||
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}}}
|
||||
]
|
||||
}
|
||||
};
|
||||
rules: [{test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}}],
|
||||
},
|
||||
optimization: {
|
||||
// Disable minification for better debugging on Karma tests
|
||||
minimize: false,
|
||||
},
|
||||
devtool: "source-map",
|
||||
};
|
||||
|
|
Reference in New Issue