From 31310ef8cf07d609851becc058cfcfd541f2f265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Mon, 4 Nov 2024 16:28:21 +0100 Subject: [PATCH 1/7] threshold plynomial and scheme --- lib/av_crypto/threshold/polynomial.ts | 22 +++++++++++++++ .../{threshold.ts => threshold/scheme.ts} | 14 ++++++++-- test/av_crypto/threshold/polynomial.test.ts | 28 +++++++++++++++++++ .../scheme.test.ts} | 22 +++++++++++---- 4 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 lib/av_crypto/threshold/polynomial.ts rename lib/av_crypto/{threshold.ts => threshold/scheme.ts} (63%) create mode 100644 test/av_crypto/threshold/polynomial.test.ts rename test/av_crypto/{threshold.test.ts => threshold/scheme.test.ts} (70%) 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/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'); From 905d49187300012187e487cf7c5d61e074b94349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Tue, 5 Nov 2024 15:15:30 +0100 Subject: [PATCH 2/7] FROST scheme partial sign --- .../schnorr/frost/commitment_share.ts | 25 ++++++++ lib/av_crypto/schnorr/frost/scheme.ts | 61 +++++++++++++++++++ .../schnorr/frost/single_use_nonce.ts | 11 ++++ lib/av_crypto/schnorr/scheme.ts | 6 +- lib/av_crypto/utils.ts | 12 ++++ test/av_crypto/schnorr/frost/scheme.test.ts | 48 +++++++++++++++ test/av_crypto/test_helpers.ts | 7 +++ 7 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 lib/av_crypto/schnorr/frost/commitment_share.ts create mode 100644 lib/av_crypto/schnorr/frost/scheme.ts create mode 100644 lib/av_crypto/schnorr/frost/single_use_nonce.ts create mode 100644 test/av_crypto/schnorr/frost/scheme.test.ts 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..77e0eaa5 --- /dev/null +++ b/lib/av_crypto/schnorr/frost/scheme.ts @@ -0,0 +1,61 @@ +import {BigNumber, SjclEllipticalPoint} from "../../sjcl"; +import {Curve} from "../../curve"; +import {SingleUseNonce} from "./single_use_nonce"; +import {CommitmentShare} from "./commitment_share"; +import {concatForHashing, hashIntoScalar, multiplyAndSumScalarsAndPoints, scalarToHex} from "../../utils"; +import {computeLambda} from "../../threshold/scheme"; +import * as sjcl from "sjcl-with-all"; +import {deriveChallenge} from "../scheme"; + +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, commitmentsContext, curve) + const c = deriveChallenge(message, r, curve) + + return computeSignature(nonce, privateShare, c, rho, lambda, curve) +} + +function computeBindingValue(i: BigNumber, message: string, commitmentsContext: string, curve: Curve): BigNumber { + const string = concatForHashing([scalarToHex(i, curve), message, commitmentsContext]) + return hashIntoScalar(string, curve) +} + +function computeGroupCommitment(commitments: Array, message: string, commitmentsContext: string, curve: Curve): SjclEllipticalPoint { + const scalars = new Array; + const points = new Array; + + 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 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/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 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 privateShare = addScalars([partialPrivateShare11, partialPrivateShare12], curve) + const nonce1 = new SingleUseNonce(fixedScalar1(curve), fixedScalar2(curve)) + const commitments = [ + new CommitmentShare(id1, fixedPoint1(curve), fixedPoint2(curve), curve), + new CommitmentShare(id2, fixedPoint2(curve), fixedPoint1(curve), curve), + ] + + describe("partial sign()", () => { + it("returns a partial signature", () => { + const signature = partialSign(message, privateShare, id1, nonce1, commitments, curve) + + expect(scalarToHex(signature, curve)).to.equal(hexString( + "1896251c 9dbcf4c7 f6c29cc4 88c531d7" + + "f9756907 658645e3 03193d5f d61c491b" + )); + }) + }) +}) 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) From 553f57ab3a25c2fbfee3402cc95a5d4e4107ac37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Wed, 6 Nov 2024 17:55:47 +0100 Subject: [PATCH 3/7] FROST isValid --- lib/av_crypto/schnorr/frost/scheme.ts | 39 ++++++++++++++++++++- test/av_crypto/schnorr/frost/scheme.test.ts | 22 ++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/av_crypto/schnorr/frost/scheme.ts b/lib/av_crypto/schnorr/frost/scheme.ts index 77e0eaa5..13c8d152 100644 --- a/lib/av_crypto/schnorr/frost/scheme.ts +++ b/lib/av_crypto/schnorr/frost/scheme.ts @@ -2,7 +2,14 @@ import {BigNumber, SjclEllipticalPoint} from "../../sjcl"; import {Curve} from "../../curve"; import {SingleUseNonce} from "./single_use_nonce"; import {CommitmentShare} from "./commitment_share"; -import {concatForHashing, hashIntoScalar, multiplyAndSumScalarsAndPoints, scalarToHex} from "../../utils"; +import { + addPoints, + concatForHashing, + hashIntoScalar, + multiplyAndSumScalarsAndPoints, + pointEquals, + scalarToHex +} from "../../utils"; import {computeLambda} from "../../threshold/scheme"; import * as sjcl from "sjcl-with-all"; import {deriveChallenge} from "../scheme"; @@ -25,6 +32,36 @@ export function partialSign( 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, commitmentsContext, 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); +} + +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) diff --git a/test/av_crypto/schnorr/frost/scheme.test.ts b/test/av_crypto/schnorr/frost/scheme.test.ts index 7a4c4c4e..550d9aa5 100644 --- a/test/av_crypto/schnorr/frost/scheme.test.ts +++ b/test/av_crypto/schnorr/frost/scheme.test.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import {describe} from "mocha"; import * as sjcl from "sjcl-with-all"; import {Curve} from "../../../../lib/av_crypto/curve"; -import {partialSign} from "../../../../lib/av_crypto/schnorr/frost/scheme"; +import {isValid, partialSign} from "../../../../lib/av_crypto/schnorr/frost/scheme"; import { fixedKeyPair, fixedKeyPair2, @@ -14,8 +14,9 @@ import { } 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 {addScalars, scalarToHex} from "../../../../lib/av_crypto/utils"; +import {addScalars, hexToScalar, scalarToHex} from "../../../../lib/av_crypto/utils"; import {Polynomial} from "../../../../lib/av_crypto/threshold/polynomial"; +import {computePublicShare} from "../../../../lib/av_crypto/threshold/scheme"; describe("Schnorr FROST threshold signature scheme", () => { const curve = new Curve('k256') @@ -35,7 +36,7 @@ describe("Schnorr FROST threshold signature scheme", () => { new CommitmentShare(id2, fixedPoint2(curve), fixedPoint1(curve), curve), ] - describe("partial sign()", () => { + describe("partialSign()", () => { it("returns a partial signature", () => { const signature = partialSign(message, privateShare, id1, nonce1, commitments, curve) @@ -45,4 +46,19 @@ describe("Schnorr FROST threshold signature scheme", () => { )); }) }) + + 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 + }) + }) }) From 73997ed8856140bd894b2298f5fbfd1ec259afd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Thu, 7 Nov 2024 09:51:55 +0100 Subject: [PATCH 4/7] unhappy paths tests for isValid? --- test/av_crypto/schnorr/frost/scheme.test.ts | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/av_crypto/schnorr/frost/scheme.test.ts b/test/av_crypto/schnorr/frost/scheme.test.ts index 550d9aa5..e7f98950 100644 --- a/test/av_crypto/schnorr/frost/scheme.test.ts +++ b/test/av_crypto/schnorr/frost/scheme.test.ts @@ -55,10 +55,46 @@ describe("Schnorr FROST threshold signature scheme", () => { 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 + }) + }) }) }) From 14d99c4ce40a70fab2cc2ff0eefaeab9fc6050a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Thu, 7 Nov 2024 10:37:20 +0100 Subject: [PATCH 5/7] aggregate partial signatures + tests --- lib/av_crypto/schnorr/frost/scheme.ts | 40 ++++++++++++--------- test/av_crypto/schnorr/frost/scheme.test.ts | 33 ++++++++++++++--- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/lib/av_crypto/schnorr/frost/scheme.ts b/lib/av_crypto/schnorr/frost/scheme.ts index 13c8d152..9399cc8d 100644 --- a/lib/av_crypto/schnorr/frost/scheme.ts +++ b/lib/av_crypto/schnorr/frost/scheme.ts @@ -3,7 +3,7 @@ import {Curve} from "../../curve"; import {SingleUseNonce} from "./single_use_nonce"; import {CommitmentShare} from "./commitment_share"; import { - addPoints, + addPoints, addScalars, concatForHashing, hashIntoScalar, multiplyAndSumScalarsAndPoints, @@ -13,6 +13,7 @@ import { 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, @@ -26,7 +27,7 @@ export function partialSign( const rho = computeBindingValue(id, message, commitmentsContext, curve) const otherIds = otherIdsThan(id, commitments); const lambda = computeLambda(id, otherIds, curve); - const r = computeGroupCommitment(commitments, message, commitmentsContext, curve) + const r = computeGroupCommitment(commitments, message, curve) const c = deriveChallenge(message, r, curve) return computeSignature(nonce, privateShare, c, rho, lambda, curve) @@ -37,7 +38,7 @@ export function isValid(message: string, partialSignature: BigNumber, publicShar const rho = computeBindingValue(id, message, commitmentsContext, curve) const otherIds = otherIdsThan(id, commitments); const lambda = computeLambda(id, otherIds, curve); - const r = computeGroupCommitment(commitments, message, commitmentsContext, 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) { @@ -50,6 +51,25 @@ export function isValid(message: string, partialSignature: BigNumber, publicShar 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) } @@ -67,20 +87,6 @@ function computeBindingValue(i: BigNumber, message: string, commitmentsContext: return hashIntoScalar(string, curve) } -function computeGroupCommitment(commitments: Array, message: string, commitmentsContext: string, curve: Curve): SjclEllipticalPoint { - const scalars = new Array; - const points = new Array; - - 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 renderCommitmentContext(commitments: Array): string { return concatForHashing(commitments.map(commitment => commitment.toString())) } diff --git a/test/av_crypto/schnorr/frost/scheme.test.ts b/test/av_crypto/schnorr/frost/scheme.test.ts index e7f98950..f6637cb5 100644 --- a/test/av_crypto/schnorr/frost/scheme.test.ts +++ b/test/av_crypto/schnorr/frost/scheme.test.ts @@ -2,7 +2,13 @@ import { expect } from "chai"; import {describe} from "mocha"; import * as sjcl from "sjcl-with-all"; import {Curve} from "../../../../lib/av_crypto/curve"; -import {isValid, partialSign} from "../../../../lib/av_crypto/schnorr/frost/scheme"; +import { + aggregateSignatures, + computeGroupCommitment, + isValid, + partialSign +} from "../../../../lib/av_crypto/schnorr/frost/scheme"; +import * as schnorrScheme from "../../../../lib/av_crypto/schnorr/scheme" import { fixedKeyPair, fixedKeyPair2, @@ -14,9 +20,10 @@ import { } 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 {addScalars, hexToScalar, scalarToHex} from "../../../../lib/av_crypto/utils"; +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') @@ -29,8 +36,12 @@ describe("Schnorr FROST threshold signature scheme", () => { const polynomial2 = new Polynomial([keyPair2, keyPair1], curve) const partialPrivateShare11 = polynomial1.evaluateAt(id1); const partialPrivateShare12 = polynomial2.evaluateAt(id1); - const privateShare = addScalars([partialPrivateShare11, partialPrivateShare12], curve) + 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), @@ -38,7 +49,7 @@ describe("Schnorr FROST threshold signature scheme", () => { describe("partialSign()", () => { it("returns a partial signature", () => { - const signature = partialSign(message, privateShare, id1, nonce1, commitments, curve) + const signature = partialSign(message, privateShare1, id1, nonce1, commitments, curve) expect(scalarToHex(signature, curve)).to.equal(hexString( "1896251c 9dbcf4c7 f6c29cc4 88c531d7" + @@ -97,4 +108,18 @@ describe("Schnorr FROST threshold signature scheme", () => { }) }) }) + + 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 + }) + }) }) From 99792555d2053c47385399484b74e21f64d14f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Thu, 7 Nov 2024 14:16:28 +0100 Subject: [PATCH 6/7] testing --- .../schnorr/frost/commitment_share.test.ts | 35 +++++++++++++++++++ .../schnorr/frost/single_use_nonce.test.ts | 19 ++++++++++ 2 files changed, 54 insertions(+) create mode 100644 test/av_crypto/schnorr/frost/commitment_share.test.ts create mode 100644 test/av_crypto/schnorr/frost/single_use_nonce.test.ts diff --git a/test/av_crypto/schnorr/frost/commitment_share.test.ts b/test/av_crypto/schnorr/frost/commitment_share.test.ts new file mode 100644 index 00000000..3ad00797 --- /dev/null +++ b/test/av_crypto/schnorr/frost/commitment_share.test.ts @@ -0,0 +1,35 @@ +import { expect } from "chai"; +import {Curve} from "../../../../lib/av_crypto/curve"; +import {fixedPoint1, fixedPoint2} from "../../test_helpers"; +import {CommitmentShare} from "../../../../lib/av_crypto/schnorr/frost/commitment_share"; +import * as sjcl from "sjcl-with-all"; +import {pointToHex, scalarToHex} from "../../../../lib/av_crypto/utils"; + +describe("Commitment share", () => { + 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/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) + }) + }) +}) From fb086a419cd751b3b6e7de1c16965ad1b6317407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C8=98tefan=20Patachi?= Date: Thu, 14 Nov 2024 14:43:58 +0100 Subject: [PATCH 7/7] updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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