Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FROST threshold signatures #308

Open
wants to merge 7 commits into
base: TECH-1665-new-crypto-library
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ dist
node_modules
package
**/bundle.js
.idea
25 changes: 25 additions & 0 deletions lib/av_crypto/schnorr/frost/commitment_share.ts
Original file line number Diff line number Diff line change
@@ -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)
])
}
}
104 changes: 104 additions & 0 deletions lib/av_crypto/schnorr/frost/scheme.ts
Original file line number Diff line number Diff line change
@@ -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<CommitmentShare>,
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<CommitmentShare>, 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<BigNumber>, curve: Curve): Signature {
return new Signature(c, addScalars(z, curve), curve)
}

export function computeGroupCommitment(commitments: Array<CommitmentShare>, message: string, curve: Curve): SjclEllipticalPoint {
const scalars = new Array<BigNumber>;
const points = new Array<SjclEllipticalPoint>;

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<CommitmentShare>): string {
return concatForHashing(commitments.map(commitment => commitment.toString()))
}

function otherIdsThan(id: BigNumber, commitments: Array<CommitmentShare>): Array<BigNumber> {
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())
}
11 changes: 11 additions & 0 deletions lib/av_crypto/schnorr/frost/single_use_nonce.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 3 additions & 3 deletions lib/av_crypto/schnorr/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -30,12 +30,12 @@ export function sign(
curve: Curve,
randomness: SjclKeyPair<SjclECCPublicKey, SjclECCSecretKey> = 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
Expand Down
22 changes: 22 additions & 0 deletions lib/av_crypto/threshold/polynomial.ts
Original file line number Diff line number Diff line change
@@ -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<SjclKeyPair<SjclECCPublicKey, SjclECCSecretKey>>
private curve: Curve

constructor(coefficients: Array<SjclKeyPair<SjclECCPublicKey, SjclECCSecretKey>>, 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());
}
}
14 changes: 11 additions & 3 deletions lib/av_crypto/threshold.ts → lib/av_crypto/threshold/scheme.ts
Original file line number Diff line number Diff line change
@@ -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<SjclECCPublicKey, SjclECCSecretKey>, curve: Curve): Polynomial {
const coefficients = Array(degree - 1).fill(generateKeyPair(curve));
coefficients.unshift(firstCoefficient);

return new Polynomial(coefficients, curve);
}

export function computePublicShare(
id: BigNumber,
Expand Down
12 changes: 12 additions & 0 deletions lib/av_crypto/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export function multiplyAndSumScalarsAndPoints(scalars: Array<BigNumber>, points
return result.toAffine()
}

export function addScalars(scalars: Array<BigNumber>, curve: Curve): BigNumber {
if (scalars.length == 0) {
throw new Error("array must not be empty")
}

let sum = scalars[0]
for (let i=1; i<scalars.length; i++) {
sum = sum.add(scalars[i])
}
return sum.mod(curve.order())
}

export function pointEquals(point1: SjclEllipticalPoint, point2: SjclEllipticalPoint): boolean {
if (point1.isIdentity) {
return point2.isIdentity
Expand Down
35 changes: 35 additions & 0 deletions test/av_crypto/schnorr/frost/commitment_share.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
125 changes: 125 additions & 0 deletions test/av_crypto/schnorr/frost/scheme.test.ts
Original file line number Diff line number Diff line change
@@ -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
})
})
})
Loading
Loading