diff --git a/.gitignore b/.gitignore index 9703db81..ef3e8ead 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist node_modules package **/bundle.js +.idea diff --git a/lib/av_crypto/schnorr/frost/commitment_share.ts b/lib/av_crypto/schnorr/frost/commitment_share.ts new file mode 100644 index 00000000..a090cceb --- /dev/null +++ b/lib/av_crypto/schnorr/frost/commitment_share.ts @@ -0,0 +1,25 @@ +import {BigNumber, SjclEllipticalPoint} from "../../sjcl"; +import {Curve} from "../../curve"; +import {concatForHashing, pointToHex, scalarToHex} from "../../utils"; + +export class CommitmentShare { + public i: BigNumber + public d: SjclEllipticalPoint + public e: SjclEllipticalPoint + private curve: Curve + + constructor(i: BigNumber, d: SjclEllipticalPoint, e: SjclEllipticalPoint, curve: Curve) { + this.i = i; + this.d = d; + this.e = e; + this.curve = curve + } + + public toString(): string { + return concatForHashing([ + scalarToHex(this.i, this.curve), + pointToHex(this.d), + pointToHex(this.e) + ]) + } +} diff --git a/lib/av_crypto/schnorr/frost/scheme.ts b/lib/av_crypto/schnorr/frost/scheme.ts new file mode 100644 index 00000000..9399cc8d --- /dev/null +++ b/lib/av_crypto/schnorr/frost/scheme.ts @@ -0,0 +1,104 @@ +import {BigNumber, SjclEllipticalPoint} from "../../sjcl"; +import {Curve} from "../../curve"; +import {SingleUseNonce} from "./single_use_nonce"; +import {CommitmentShare} from "./commitment_share"; +import { + addPoints, addScalars, + concatForHashing, + hashIntoScalar, + multiplyAndSumScalarsAndPoints, + pointEquals, + scalarToHex +} from "../../utils"; +import {computeLambda} from "../../threshold/scheme"; +import * as sjcl from "sjcl-with-all"; +import {deriveChallenge} from "../scheme"; +import {Signature} from "../signature"; + +export function partialSign( + message: string, + privateShare: BigNumber, + id: BigNumber, + nonce: SingleUseNonce, + commitments: Array, + curve: Curve +): BigNumber { + const commitmentsContext = renderCommitmentContext(commitments) + const rho = computeBindingValue(id, message, commitmentsContext, curve) + const otherIds = otherIdsThan(id, commitments); + const lambda = computeLambda(id, otherIds, curve); + const r = computeGroupCommitment(commitments, message, curve) + const c = deriveChallenge(message, r, curve) + + return computeSignature(nonce, privateShare, c, rho, lambda, curve) +} + +export function isValid(message: string, partialSignature: BigNumber, publicShare: SjclEllipticalPoint, id: BigNumber, commitments: Array, curve: Curve): boolean { + const commitmentsContext = renderCommitmentContext(commitments) + const rho = computeBindingValue(id, message, commitmentsContext, curve) + const otherIds = otherIdsThan(id, commitments); + const lambda = computeLambda(id, otherIds, curve); + const r = computeGroupCommitment(commitments, message, curve) + const c = deriveChallenge(message, r, curve) + const commitmentShare = commitments.find(c => c.i.equals(id)) + if (commitmentShare === undefined) { + throw new Error("id must be included in the list of commitments") + } + + const lhs = computeLHS(partialSignature, curve) + const rhs = computeRHS(commitmentShare, c, lambda, rho, publicShare, curve) + + return pointEquals(lhs, rhs); +} + +export function aggregateSignatures(c: BigNumber, z: Array, curve: Curve): Signature { + return new Signature(c, addScalars(z, curve), curve) +} + +export function computeGroupCommitment(commitments: Array, message: string, curve: Curve): SjclEllipticalPoint { + const scalars = new Array; + const points = new Array; + + const commitmentsContext = renderCommitmentContext(commitments) + commitments.forEach(commitment => { + const rho = computeBindingValue(commitment.i, message, commitmentsContext, curve) + + scalars.push(new sjcl.bn(1), rho) + points.push(commitment.d, commitment.e) + }) + + return multiplyAndSumScalarsAndPoints(scalars, points); +} + +function computeLHS(z: BigNumber, curve: Curve): SjclEllipticalPoint { + return curve.G().mult(z) +} + +function computeRHS(commitment: CommitmentShare, c: BigNumber, lambda: BigNumber, rho: BigNumber, y: SjclEllipticalPoint, curve: Curve): SjclEllipticalPoint { + return addPoints([ + commitment.d, + commitment.e.mult(rho), + y.mult(c.mulmod(lambda, curve.order())).negate() + ]) +} + +function computeBindingValue(i: BigNumber, message: string, commitmentsContext: string, curve: Curve): BigNumber { + const string = concatForHashing([scalarToHex(i, curve), message, commitmentsContext]) + return hashIntoScalar(string, curve) +} + +function renderCommitmentContext(commitments: Array): string { + return concatForHashing(commitments.map(commitment => commitment.toString())) +} + +function otherIdsThan(id: BigNumber, commitments: Array): Array { + return commitments + .map(commitment => commitment.i) + .filter(otherId => !id.equals(otherId)) +} + +function computeSignature(nonce: SingleUseNonce, privateShare: BigNumber, c: BigNumber, rho: BigNumber, lambda: BigNumber, curve: Curve): BigNumber { + // sjcl mod() always returns a positive number. + // There is no need add the curve order if it's negative. + return nonce.d.add(nonce.e.mul(rho)).sub(c.mul(privateShare).mul(lambda)).mod(curve.order()) +} diff --git a/lib/av_crypto/schnorr/frost/single_use_nonce.ts b/lib/av_crypto/schnorr/frost/single_use_nonce.ts new file mode 100644 index 00000000..a8b2170e --- /dev/null +++ b/lib/av_crypto/schnorr/frost/single_use_nonce.ts @@ -0,0 +1,11 @@ +import {BigNumber} from "../../sjcl"; + +export class SingleUseNonce { + public d: BigNumber + public e: BigNumber + + constructor(d: BigNumber, e: BigNumber) { + this.d = d; + this.e = e; + } +} diff --git a/lib/av_crypto/schnorr/scheme.ts b/lib/av_crypto/schnorr/scheme.ts index c7091e02..92b0e673 100644 --- a/lib/av_crypto/schnorr/scheme.ts +++ b/lib/av_crypto/schnorr/scheme.ts @@ -19,7 +19,7 @@ export function isValid( curve: Curve ): boolean { const r = computeR(signature, publicKey, curve); - const recomputedE = computeE(message, r, curve) + const recomputedE = deriveChallenge(message, r, curve) return signature.e.equals(recomputedE) } @@ -30,12 +30,12 @@ export function sign( curve: Curve, randomness: SjclKeyPair = generateKeyPair(curve) ): Signature { - const e = computeE(message, randomness.pub.H, curve) + const e = deriveChallenge(message, randomness.pub.H, curve) const s = computeS(privateKey, randomness.sec.S, e, curve) return new Signature(e, s, curve) } -function computeE(message: string, r: SjclEllipticalPoint, curve: Curve): BigNumber { +export function deriveChallenge(message: string, r: SjclEllipticalPoint, curve: Curve): BigNumber { const string = concatForHashing([ pointToHex(r), message diff --git a/lib/av_crypto/threshold/polynomial.ts b/lib/av_crypto/threshold/polynomial.ts new file mode 100644 index 00000000..1ecb756c --- /dev/null +++ b/lib/av_crypto/threshold/polynomial.ts @@ -0,0 +1,22 @@ +import {Curve} from "../curve"; +import {BigNumber, SjclECCPublicKey, SjclECCSecretKey, SjclKeyPair} from "../sjcl"; +import * as sjcl from "sjcl-with-all"; + +export class Polynomial { + public coefficients: Array> + private curve: Curve + + constructor(coefficients: Array>, curve: Curve) { + this.coefficients = coefficients; + this.curve = curve + } + + public evaluateAt(x: BigNumber): BigNumber { + let result = new sjcl.bn(0); + for (let i = 0; i < this.coefficients.length; i++) { + const term = this.coefficients[i].sec.S.mul(x.power(i)).mod(this.curve.order()) + result = result.add(term); + } + return result.mod(this.curve.order()); + } +} diff --git a/lib/av_crypto/threshold.ts b/lib/av_crypto/threshold/scheme.ts similarity index 63% rename from lib/av_crypto/threshold.ts rename to lib/av_crypto/threshold/scheme.ts index 4f7d2509..649cab47 100644 --- a/lib/av_crypto/threshold.ts +++ b/lib/av_crypto/threshold/scheme.ts @@ -1,7 +1,15 @@ -import {BigNumber, SjclEllipticalPoint} from "./sjcl"; -import {multiplyAndSumScalarsAndPoints} from "./utils"; -import {Curve} from "./curve"; +import {BigNumber, SjclECCPublicKey, SjclECCSecretKey, SjclEllipticalPoint, SjclKeyPair} from "../sjcl"; +import {generateKeyPair, multiplyAndSumScalarsAndPoints} from "../utils"; +import {Curve} from "../curve"; import * as sjcl from "sjcl-with-all"; +import {Polynomial} from "./polynomial"; + +export function generatePolynomial(degree: number, firstCoefficient: SjclKeyPair, curve: Curve): Polynomial { + const coefficients = Array(degree - 1).fill(generateKeyPair(curve)); + coefficients.unshift(firstCoefficient); + + return new Polynomial(coefficients, curve); +} export function computePublicShare( id: BigNumber, diff --git a/lib/av_crypto/utils.ts b/lib/av_crypto/utils.ts index d5666be2..1f56b09d 100644 --- a/lib/av_crypto/utils.ts +++ b/lib/av_crypto/utils.ts @@ -27,6 +27,18 @@ export function multiplyAndSumScalarsAndPoints(scalars: Array, points return result.toAffine() } +export function addScalars(scalars: Array, curve: Curve): BigNumber { + if (scalars.length == 0) { + throw new Error("array must not be empty") + } + + let sum = scalars[0] + for (let i=1; i { + const curve = new Curve('k256') + const i = new sjcl.bn(2) + const d = fixedPoint1(curve) + const e = fixedPoint2(curve) + + describe("constructor", () => { + const commitmentShare = new CommitmentShare(i, d, e, curve) + + it ("constructs a CommitmentShare", () => { + expect(commitmentShare.i).to.equal(i) + expect(commitmentShare.d).to.equal(d) + expect(commitmentShare.e).to.equal(e) + }) + }) + + describe("toString()", () => { + const commitmentShare = new CommitmentShare(i, d, e, curve) + + it ("renders hex values concatenated by dashes", () => { + const i_hex = scalarToHex(commitmentShare.i, curve) + const d_hex = pointToHex(commitmentShare.d) + const e_hex = pointToHex(commitmentShare.e) + + expect(commitmentShare.toString()).to.equal(i_hex + "-" + d_hex + "-" + e_hex) + }) + }) +}) diff --git a/test/av_crypto/schnorr/frost/scheme.test.ts b/test/av_crypto/schnorr/frost/scheme.test.ts new file mode 100644 index 00000000..f6637cb5 --- /dev/null +++ b/test/av_crypto/schnorr/frost/scheme.test.ts @@ -0,0 +1,125 @@ +import { expect } from "chai"; +import {describe} from "mocha"; +import * as sjcl from "sjcl-with-all"; +import {Curve} from "../../../../lib/av_crypto/curve"; +import { + aggregateSignatures, + computeGroupCommitment, + isValid, + partialSign +} from "../../../../lib/av_crypto/schnorr/frost/scheme"; +import * as schnorrScheme from "../../../../lib/av_crypto/schnorr/scheme" +import { + fixedKeyPair, + fixedKeyPair2, + fixedPoint1, + fixedPoint2, + fixedScalar1, + fixedScalar2, + hexString +} from "../../test_helpers"; +import {SingleUseNonce} from "../../../../lib/av_crypto/schnorr/frost/single_use_nonce"; +import {CommitmentShare} from "../../../../lib/av_crypto/schnorr/frost/commitment_share"; +import {addPoints, addScalars, hexToScalar, scalarToHex} from "../../../../lib/av_crypto/utils"; +import {Polynomial} from "../../../../lib/av_crypto/threshold/polynomial"; +import {computePublicShare} from "../../../../lib/av_crypto/threshold/scheme"; +import {deriveChallenge} from "../../../../lib/av_crypto/schnorr/scheme"; + +describe("Schnorr FROST threshold signature scheme", () => { + const curve = new Curve('k256') + const message = "hello" + const id1 = new sjcl.bn(42) + const id2 = new sjcl.bn(2) + const keyPair1 = fixedKeyPair(curve); + const keyPair2 = fixedKeyPair2(curve); + const polynomial1 = new Polynomial([keyPair1, keyPair2], curve) + const polynomial2 = new Polynomial([keyPair2, keyPair1], curve) + const partialPrivateShare11 = polynomial1.evaluateAt(id1); + const partialPrivateShare12 = polynomial2.evaluateAt(id1); + const partialPrivateShare21 = polynomial1.evaluateAt(id2); + const partialPrivateShare22 = polynomial2.evaluateAt(id2); + const privateShare1 = addScalars([partialPrivateShare11, partialPrivateShare12], curve) + const privateShare2 = addScalars([partialPrivateShare21, partialPrivateShare22], curve) + const nonce1 = new SingleUseNonce(fixedScalar1(curve), fixedScalar2(curve)) + const nonce2 = new SingleUseNonce(fixedScalar2(curve), fixedScalar1(curve)) + const commitments = [ + new CommitmentShare(id1, fixedPoint1(curve), fixedPoint2(curve), curve), + new CommitmentShare(id2, fixedPoint2(curve), fixedPoint1(curve), curve), + ] + + describe("partialSign()", () => { + it("returns a partial signature", () => { + const signature = partialSign(message, privateShare1, id1, nonce1, commitments, curve) + + expect(scalarToHex(signature, curve)).to.equal(hexString( + "1896251c 9dbcf4c7 f6c29cc4 88c531d7" + + "f9756907 658645e3 03193d5f d61c491b" + )); + }) + }) + + describe("isValid()", () => { + const partialSignature = hexToScalar(hexString( + "1896251c 9dbcf4c7 f6c29cc4 88c531d7" + + "f9756907 658645e3 03193d5f d61c491b" + ), curve) + const publicKeys = [keyPair1.pub.H, keyPair2.pub.H] + const coefficients = [[keyPair2.pub.H], [keyPair1.pub.H]] + const publicShare = computePublicShare(id1, publicKeys, coefficients, curve) + + it("returns true", () => { + const valid = isValid(message, partialSignature, publicShare, id1, commitments, curve) + + expect(valid).to.be.true + }) + + context("with id not included in commitments", () => { + const id = new sjcl.bn(100) + it("throws error", () => { + expect(() => { + isValid(message, partialSignature, publicShare, id, commitments, curve) + }).to.throw("id must be included in the list of commitments") + }) + }) + + context("with different message", () => { + const message = "different" + it("returns false", () => { + const valid = isValid(message, partialSignature, publicShare, id1, commitments, curve) + + expect(valid).to.be.false + }) + }) + + context("with different partial signature", () => { + const partialSignature = new sjcl.bn(100) + it("returns false", () => { + const valid = isValid(message, partialSignature, publicShare, id1, commitments, curve) + + expect(valid).to.be.false + }) + }) + + context("with different id", () => { + it("returns false", () => { + const valid = isValid(message, partialSignature, publicShare, id2, commitments, curve) + + expect(valid).to.be.false + }) + }) + }) + + describe("aggregateSignatures()", () => { + const groupCommitment = computeGroupCommitment(commitments, message, curve) + const challenge = deriveChallenge(message, groupCommitment, curve) + const partialSignature1 = partialSign(message, privateShare1, id1, nonce1, commitments, curve) + const partialSignature2 = partialSign(message, privateShare2, id2, nonce2, commitments, curve) + const publicKey = addPoints([keyPair1.pub.H, keyPair2.pub.H]) + + it("returns a valid Schnorr signature", () => { + const signature = aggregateSignatures(challenge, [partialSignature1, partialSignature2], curve) + + expect(schnorrScheme.isValid(signature, message, publicKey, curve)).to.be.true + }) + }) +}) diff --git a/test/av_crypto/schnorr/frost/single_use_nonce.test.ts b/test/av_crypto/schnorr/frost/single_use_nonce.test.ts new file mode 100644 index 00000000..dab2c61b --- /dev/null +++ b/test/av_crypto/schnorr/frost/single_use_nonce.test.ts @@ -0,0 +1,19 @@ +import { expect } from "chai"; +import {Curve} from "../../../../lib/av_crypto/curve"; +import {SingleUseNonce} from "../../../../lib/av_crypto/schnorr/frost/single_use_nonce"; +import {fixedScalar1, fixedScalar2} from "../../test_helpers"; + +describe("Single use nonce", () => { + const curve = new Curve('k256') + const d = fixedScalar1(curve) + const e = fixedScalar2(curve) + + describe("constructor", () => { + const nonce = new SingleUseNonce(d, e) + + it ("constructs a SingleUseNonce", () => { + expect(nonce.d).to.equal(d) + expect(nonce.e).to.equal(e) + }) + }) +}) diff --git a/test/av_crypto/test_helpers.ts b/test/av_crypto/test_helpers.ts index e8882ef3..684010d9 100644 --- a/test/av_crypto/test_helpers.ts +++ b/test/av_crypto/test_helpers.ts @@ -13,6 +13,13 @@ export function fixedKeyPair(curve: Curve): SjclKeyPair { + const seed = "other_fixed_keypair" + const private_key = hashIntoScalar(seed, curve) + + return generateKeyPair(curve, private_key); +} + export function fixedScalar1(curve: Curve): BigNumber { const seed = "fixed value 1" const private_key = hashIntoScalar(seed, curve) diff --git a/test/av_crypto/threshold/polynomial.test.ts b/test/av_crypto/threshold/polynomial.test.ts new file mode 100644 index 00000000..4d5fc13d --- /dev/null +++ b/test/av_crypto/threshold/polynomial.test.ts @@ -0,0 +1,28 @@ +import { expect } from "chai"; +import {Curve} from "../../../lib/av_crypto/curve"; +import {Polynomial} from "../../../lib/av_crypto/threshold/polynomial"; +import {generateKeyPair} from "../../../lib/av_crypto/utils"; +import * as sjcl from "sjcl-with-all"; + +describe("Threshold polynomial", () => { + const curve = new Curve('k256') + const keyPair1 = generateKeyPair(curve, new sjcl.bn(1)) + const keyPair2 = generateKeyPair(curve, new sjcl.bn(2)) + const keyPair3 = generateKeyPair(curve, new sjcl.bn(3)) + const coefficients = [keyPair1, keyPair2, keyPair3]; + const polynomial = new Polynomial(coefficients, curve) + + describe ("constructor", () => { + it ("constructs a polynomial", () => { + expect(polynomial.coefficients).to.equal(coefficients) + }) + }) + + describe("evaluateAt()", () => { + const x = new sjcl.bn(10) + + it ("returns the correct value", () => { + expect(polynomial.evaluateAt(x)).to.be.eql(new sjcl.bn((1 * (10**0)) + (2 * (10**1)) + (3 * (10**2)))) + }) + }) +}) diff --git a/test/av_crypto/threshold.test.ts b/test/av_crypto/threshold/scheme.test.ts similarity index 70% rename from test/av_crypto/threshold.test.ts rename to test/av_crypto/threshold/scheme.test.ts index 89b39f8a..eea6ae52 100644 --- a/test/av_crypto/threshold.test.ts +++ b/test/av_crypto/threshold/scheme.test.ts @@ -1,11 +1,23 @@ import { expect } from "chai"; -import {fixedPoint1, fixedPoint2, hexString} from "./test_helpers"; -import {Curve} from "../../lib/av_crypto/curve"; -import {addPoints, scalarToHex} from "../../lib/av_crypto/utils"; -import {computeLambda, computePublicShare} from "../../lib/av_crypto/threshold"; +import {fixedKeyPair, fixedPoint1, fixedPoint2, hexString} from "../test_helpers"; +import {Curve} from "../../../lib/av_crypto/curve"; +import {addPoints, scalarToHex} from "../../../lib/av_crypto/utils"; +import {computeLambda, computePublicShare, generatePolynomial} from "../../../lib/av_crypto/threshold/scheme"; import * as sjcl from "sjcl-with-all"; -describe("Threshold ceremony computation", () => { +describe("Threshold ceremony computation ========================================================", () => { + + describe("generatePolynomial()", () => { + const curve = new Curve('k256'); + const firstCoefficient = fixedKeyPair(curve); + const degree = 2; + const polynomial = generatePolynomial(degree, firstCoefficient, curve); + + it("constructs a polynomial", () => { + expect(polynomial.coefficients.length).to.eql(degree) + expect(polynomial.coefficients[0]).to.eql(firstCoefficient) + }) + }) describe("computePublicShare()", () => { const curve = new Curve('k256');