Merge pull request #58 from ChainSafe/dapplion/verifyMultipleSignatures

Add verifyMultipleSignatures method
This commit is contained in:
Cayman 2020-12-04 07:53:06 -07:00 committed by GitHub
commit d834657542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 267 additions and 46 deletions

View File

@ -84,6 +84,35 @@ import {aggCount, runs} from "./params";
runs, 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 // Aggregate pubkeys
await runBenchmark<PublicKey[], void>({ await runBenchmark<PublicKey[], void>({

View File

@ -3,24 +3,23 @@ const webpackConfig = require("./webpack.config");
module.exports = function (config) { module.exports = function (config) {
config.set({ config.set({
basePath: "", basePath: "",
frameworks: ["mocha", "chai"], frameworks: ["mocha", "chai"],
files: ["test/unit/run-web-implementation.test.ts", "test/unit/index-named-exports.test.ts"], files: ["test/unit-web/run-web-implementation.test.ts", "test/unit/index-named-exports.test.ts"],
exclude: [], exclude: [],
preprocessors: { preprocessors: {
"test/**/*.ts": ["webpack"] "test/**/*.ts": ["webpack"],
}, },
webpack: { webpack: {
mode: "production", mode: "production",
node: webpackConfig.node, node: webpackConfig.node,
module: webpackConfig.module, module: webpackConfig.module,
resolve: webpackConfig.resolve resolve: webpackConfig.resolve,
}, },
reporters: ["spec"], reporters: ["spec"],
browsers: ["ChromeHeadless"], browsers: ["ChromeHeadless"],
singleRun: true singleRun: true,
}); });
}; };

View File

@ -28,6 +28,14 @@ export class Signature implements ISignature {
return new Signature(agg.toSignature()); 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 { verify(publicKey: PublicKey, message: Uint8Array): boolean {
// Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity // Individual infinity signatures are NOT okay. Aggregated signatures MAY be infinity
if (this.affine.value.is_inf()) { if (this.affine.value.is_inf()) {

View File

@ -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 * Computes a public key from a secret key
*/ */
@ -121,6 +155,7 @@ export function functionalInterfaceFactory({
verify, verify,
verifyAggregate, verifyAggregate,
verifyMultiple, verifyMultiple,
verifyMultipleSignatures,
secretKeyToPublicKey, secretKeyToPublicKey,
}; };
} }

View File

@ -26,7 +26,6 @@ export function hexToBytes(hex: string): Uint8Array {
* From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50 * From https://github.com/herumi/bls-eth-wasm/blob/04eedb77aa96e66b4f65a0ab477228adf8090c36/src/bls.js#L50
*/ */
export function bytesToHex(bytes: Uint8Array): string { export function bytesToHex(bytes: Uint8Array): string {
// return "0x" + Buffer.from(bytes).toString("hex");
let s = ""; let s = "";
const n = bytes.length; const n = bytes.length;

View File

@ -20,3 +20,17 @@ export function validateBytes(
export function isZeroUint8Array(bytes: Uint8Array): boolean { export function isZeroUint8Array(bytes: Uint8Array): boolean {
return bytes.every((byte) => byte === 0); 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;
}

View File

@ -6,9 +6,26 @@ type Bls = typeof bls;
let blsGlobal: Bls | null = null; let blsGlobal: Bls | null = null;
let blsGlobalPromise: Promise<void> | 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> { export async function setupBls(): Promise<void> {
if (!blsGlobal) { if (!blsGlobal) {
await bls.init(bls.BLS12_381); 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; blsGlobal = bls;
} }
} }

View File

@ -1,8 +1,8 @@
import {SIGNATURE_LENGTH} from "../constants"; import {SIGNATURE_LENGTH} from "../constants";
import {SignatureType} from "bls-eth-wasm"; import {SignatureType, multiVerify} from "bls-eth-wasm";
import {getContext} from "./context"; import {getContext} from "./context";
import {PublicKey} from "./publicKey"; import {PublicKey} from "./publicKey";
import {bytesToHex, hexToBytes, isZeroUint8Array} from "../helpers"; import {bytesToHex, concatUint8Arrays, hexToBytes, isZeroUint8Array} from "../helpers";
import {Signature as ISignature} from "../interface"; import {Signature as ISignature} from "../interface";
import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors"; import {EmptyAggregateError, InvalidLengthError, InvalidOrderError} from "../errors";
@ -45,6 +45,14 @@ export class Signature implements ISignature {
return new Signature(signature); 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 { verify(publicKey: PublicKey, message: Uint8Array): boolean {
return publicKey.value.verify(this.value, message); return publicKey.value.verify(this.value, message);
} }
@ -57,10 +65,9 @@ export class Signature implements ISignature {
} }
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean { verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean {
const msgs = Buffer.concat(messages);
return this.value.aggregateVerifyNoCheck( return this.value.aggregateVerifyNoCheck(
publicKeys.map((key) => key.value), publicKeys.map((key) => key.value),
msgs concatUint8Arrays(messages)
); );
} }

View File

@ -13,6 +13,7 @@ export interface IBls {
fromBytes(bytes: Uint8Array): Signature; fromBytes(bytes: Uint8Array): Signature;
fromHex(hex: string): Signature; fromHex(hex: string): Signature;
aggregate(signatures: Signature[]): Signature; aggregate(signatures: Signature[]): Signature;
verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean;
}; };
sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array; sign(secretKey: Uint8Array, message: Uint8Array): Uint8Array;
@ -21,6 +22,7 @@ export interface IBls {
verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean; verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean; verifyAggregate(publicKeys: Uint8Array[], message: Uint8Array, signature: Uint8Array): boolean;
verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean; verifyMultiple(publicKeys: Uint8Array[], messages: Uint8Array[], signature: Uint8Array): boolean;
verifyMultipleSignatures(publicKeys: Uint8Array[], messages: Uint8Array[], signatures: Uint8Array[]): boolean;
secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array; secretKeyToPublicKey(secretKey: Uint8Array): Uint8Array;
init(): Promise<void>; init(): Promise<void>;
@ -49,6 +51,7 @@ export declare class Signature {
static fromBytes(bytes: Uint8Array): Signature; static fromBytes(bytes: Uint8Array): Signature;
static fromHex(hex: string): Signature; static fromHex(hex: string): Signature;
static aggregate(signatures: Signature[]): Signature; static aggregate(signatures: Signature[]): Signature;
static verifyMultipleSignatures(publicKeys: PublicKey[], messages: Uint8Array[], signatures: Signature[]): boolean;
verify(publicKey: PublicKey, message: Uint8Array): boolean; verify(publicKey: PublicKey, message: Uint8Array): boolean;
verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean; verifyAggregate(publicKeys: PublicKey[], message: Uint8Array): boolean;
verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean; verifyMultiple(publicKeys: PublicKey[], messages: Uint8Array[]): boolean;

View File

@ -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],
};

View File

@ -1,7 +1,7 @@
import herumi from "../../src/herumi"; import herumi from "../../src/herumi";
import {runSecretKeyTests} from "./secretKey.test"; import {runSecretKeyTests} from "../unit/secretKey.test";
import {runPublicKeyTests} from "./publicKey.test"; import {runPublicKeyTests} from "../unit/publicKey.test";
import {runIndexTests} from "./index.test"; import {runIndexTests} from "../unit/index.test";
// This file is intended to be compiled and run by Karma // This file is intended to be compiled and run by Karma
// Do not import the node.bindings or it will break with: // Do not import the node.bindings or it will break with:

View File

@ -1,5 +1,6 @@
import {expect} from "chai"; 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("helpers / bytes", () => {
describe("isZeroUint8Array", () => { describe("isZeroUint8Array", () => {
@ -21,8 +22,22 @@ describe("helpers / bytes", () => {
}); });
} }
}); });
});
function hexToBytesNode(hex: string): Buffer { describe("concatUint8Arrays", () => {
return Buffer.from(hex.replace("0x", ""), "hex"); 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());
});
});
});

View File

@ -1,5 +1,6 @@
import {expect} from "chai"; import {expect} from "chai";
import {hexToBytes, bytesToHex} from "../../../src/helpers/hex"; import {hexToBytes, bytesToHex} from "../../../src/helpers/hex";
import {hexToBytesNode} from "../../util";
describe("helpers / hex", () => { describe("helpers / hex", () => {
const testCases: {id: string; hex: string}[] = [ 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");
}

View File

@ -1,6 +1,8 @@
import {expect} from "chai"; import {expect} from "chai";
import {IBls} from "../../src/interface"; import {IBls} from "../../src/interface";
import {getN, randomMessage} from "../util"; import {getN, randomMessage} from "../util";
import {hexToBytes} from "../../src/helpers";
import {maliciousVerifyMultipleSignaturesData} from "../data/malicious-signature-test-data";
export function runIndexTests(bls: IBls): void { export function runIndexTests(bls: IBls): void {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // 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; 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"
);
});
});
} }

View File

@ -13,3 +13,11 @@ export function range(n: number): number[] {
for (let i = 0; i < n; i++) nums.push(i); for (let i = 0; i < n; i++) nums.push(i);
return nums; 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");
}

View File

@ -4,7 +4,7 @@
"target": "esnext", "target": "esnext",
"module": "commonjs", "module": "commonjs",
"pretty": true, "pretty": true,
"lib": ["esnext.bigint"], "lib": ["esnext.bigint", "DOM"],
"typeRoots": ["./node_modules/@types"], "typeRoots": ["./node_modules/@types"],
"declaration": true, "declaration": true,
"strict": true, "strict": true,

View File

@ -2,17 +2,20 @@ module.exports = {
entry: "./src/index.ts", entry: "./src/index.ts",
mode: "production", mode: "production",
node: { node: {
fs: "empty" fs: "empty",
}, },
output: { output: {
filename: "dist/bundle.js" filename: "dist/bundle.js",
}, },
resolve: { resolve: {
extensions: [".ts", ".js"] extensions: [".ts", ".js"],
}, },
module: { module: {
rules: [ rules: [{test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}}],
{test: /\.ts$/, use: {loader: "ts-loader", options: {transpileOnly: true}}} },
] optimization: {
} // Disable minification for better debugging on Karma tests
minimize: false,
},
devtool: "source-map",
}; };