diff --git a/crypto/elliptic-curve-examples.ts b/crypto/elliptic-curve-examples.ts index 105e7947..a9d5bf15 100644 --- a/crypto/elliptic-curve-examples.ts +++ b/crypto/elliptic-curve-examples.ts @@ -1,7 +1,12 @@ -import { CurveParams, Pallas, Vesta } from './elliptic-curve.js'; +import { + CurveParams, + Pallas, + Vesta, + TwistedCurveParams, +} from './elliptic-curve.js'; import { exampleFields } from './finite-field-examples.js'; -export { CurveParams }; +export { CurveParams, TwistedCurveParams }; const secp256k1Params: CurveParams = { name: 'secp256k1', @@ -55,3 +60,21 @@ const CurveParams = { Pallas: pallasParams, Vesta: vestaParams, }; + +// https://datatracker.ietf.org/doc/html/rfc8032#section-5.1 +const ed25519Params: TwistedCurveParams = { + name: 'Ed25519', + modulus: exampleFields.f25519.modulus, // 2^255 - 19 + order: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn, //2^252 + 27742317777372353535851937790883648493, + cofactor: 8n, + generator: { + x: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an, // <=> 15112221349535400772501151409588531511454012693041857206046113283949847762202 + y: 0x6666666666666666666666666666666666666666666666666666666666666658n, // <=> 4/5 mod p <=> 46316835694926478169428394003475163141307993866256225615783033603165251855960 + }, + a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn, // <=> -1 mod p <=> 57896044618658097711785492504343953926634992332820282019728792003956564819948 + d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n, // -121665/121666 mod p <=> 37095705934669439343138083508754565189542113879843219016388785533085940283555 +}; + +const TwistedCurveParams = { + Ed25519: ed25519Params, +}; diff --git a/crypto/elliptic-curve.ts b/crypto/elliptic-curve.ts index 4e823e9d..10bbfdb5 100644 --- a/crypto/elliptic-curve.ts +++ b/crypto/elliptic-curve.ts @@ -9,17 +9,21 @@ import { q, } from './finite-field.js'; import { Endomorphism } from './elliptic-curve-endomorphism.js'; +import { assert } from '../../lib/util/errors.js'; export { Pallas, PallasAffine, Vesta, CurveParams, GroupAffine, + GroupTwisted, GroupProjective, GroupMapPallas, createCurveProjective, createCurveAffine, + createCurveTwisted, CurveAffine, + CurveTwisted, ProjectiveCurve, affineAdd, affineDouble, @@ -30,6 +34,11 @@ export { projectiveAdd, getProjectiveDouble, projectiveNeg, + TwistedCurveParams, + twistedAdd, + twistedDouble, + twistedScale, + projectiveZeroTwisted, }; // TODO: constants, like generator points and cube roots for endomorphisms, should be drawn from @@ -56,11 +65,15 @@ const b = 5n; const a = 0n; const projectiveZero = { x: 1n, y: 1n, z: 0n }; +const projectiveZeroTwisted = { x: 0n, y: 1n, z: 1n }; type GroupProjective = { x: bigint; y: bigint; z: bigint }; type PointAtInfinity = { x: bigint; y: bigint; infinity: true }; type FinitePoint = { x: bigint; y: bigint; infinity: false }; type GroupAffine = PointAtInfinity | FinitePoint; +type GroupTwisted = PointAtInfinity | FinitePoint; + +const twistedZero: PointAtInfinity = { x: 0n, y: 1n, infinity: true }; /** * Parameters defining an elliptic curve in short Weierstraß form @@ -703,3 +716,369 @@ function createCurveAffine({ }, }; } + +/** + * Parameters defining an elliptic curve in twisted Edwards form + * ax^2 + y^2 = 1 + dx^2y^2 + */ +type TwistedCurveParams = { + /** + * Human-friendly name for the curve + */ + name: string; + /** + * Base field modulus + */ + modulus: bigint; + /** + * Scalar field modulus = group order + */ + order: bigint; + /** + * Cofactor = size of EC / order + * + * This can be left undefined if the cofactor is 1. + */ + cofactor?: bigint; + /** + * Generator point + */ + generator: { x: bigint; y: bigint }; + /** + * The `a` parameter in the curve equation ax^2 + y^2 = 1 + dx^2y^2 + */ + a: bigint; + /** + * The `d` parameter in the curve equation ax^2 + y^2 = 1 + dx^2y^2 + */ + d: bigint; + endoBase?: bigint; + endoScalar?: bigint; +}; + +function twistedOnCurve( + { x, y, infinity }: GroupTwisted, + p: bigint, + a: bigint, + d: bigint +) { + if (infinity) return true; + // a * x^2 + y^2 = 1 + d * x^2 * y^2 + let x2 = mod(x * x, p); + let y2 = mod(y * y, p); + return mod(a * x2 + y2 - 1n - d * x2 * y2, p) === 0n; +} + +function twistedAdd( + g: GroupTwisted, + h: GroupTwisted, + p: bigint, + a: bigint, + d: bigint +): GroupTwisted { + if (g.infinity) return h; + if (h.infinity) return g; + + let { x: x1, y: y1 } = g; + let { x: x2, y: y2 } = h; + + if (y1 === y2) { + // g + g --> double + if (x1 === x2) return twistedDouble(g, p, a, d); + // g - g --> return zero + if (x1 === mod(p - x2, p)) return twistedZero; + } + + // x3 = (x1 * y2 + y1 * x2) / (1 + d * x1 * x2 * y1 * y2) + // y3 = (y1 * y2 - a * x1 * x2) / (1 - d * x1 * x2 * y1 * y2) + let x1x2 = mod(x1 * x2, p); + let y1y2 = mod(y1 * y2, p); + let x1y2 = mod(x1 * y2, p); + let y1x2 = mod(y1 * x2, p); + let ax1x2 = mod(a * x1x2, p); + + let x3Num = mod(x1y2 + y1x2, p); + let y3Num = mod(y1y2 - ax1x2, p); + + let dx1x2y1y2 = mod(d * x1x2 * y1y2, p); + + let x3Denom = inverse(mod(1n + dx1x2y1y2, p), p); + if (x3Denom === undefined) + throw Error('X denominator used in twisted addition is 0'); + + let y3Denom = inverse(mod(1n - dx1x2y1y2, p), p); + if (y3Denom === undefined) + throw Error('Y denominator used in twisted addition is 0'); + + let x3 = mod(x3Num * x3Denom, p); + let y3 = mod(y3Num * y3Denom, p); + + return { x: x3, y: y3, infinity: false }; +} + +function twistedDouble( + g: GroupTwisted, + p: bigint, + a: bigint, + d: bigint +): GroupTwisted { + let { x: x1, y: y1 } = g; + + if (g.infinity) return twistedZero; + + // x3 = 2*x1*y1 / (1 + d * x1^2 * y1^2) + // y3 = (y1^2 - a * x1^2) / (1 - d * x1^2 * y1^2) + let x1x1 = x1 * x1; + let y1y1 = y1 * y1; + let x1y1 = x1 * y1; + + let x3Num = mod(2n * x1y1, p); + let y3Num = mod(y1y1 - a * x1x1, p); + + let dx1x1y1y1 = mod(d * x1x1 * y1y1, p); + + let x3Den = inverse(1n + dx1x1y1y1, p); + if (x3Den === undefined) throw Error('impossible'); + let y3Den = inverse(1n - dx1x1y1y1, p); + if (y3Den === undefined) throw Error('impossible'); + + let x3 = mod(x3Num * x3Den, p); + let y3 = mod(y3Num * y3Den, p); + + return { x: x3, y: y3, infinity: false }; +} + +function twistedNegate( + { x, y, infinity }: GroupTwisted, + p: bigint +): GroupTwisted { + if (infinity) return twistedZero; + return { x: x === 0n ? 0n : p - x, y, infinity }; +} + +function twistedScale( + g: GroupAffine, + s: bigint | boolean[], + p: bigint, + a: bigint, + d: bigint +) { + let gProj = projectiveFromTwisted(g); + let sgProj = projectiveScaleTwisted(gProj, s, p, a, d); + return projectiveToTwisted(sgProj, p); +} + +// https://www.hyperelliptic.org/EFD/g1p/auto-twisted-projective.html +// https://eprint.iacr.org/2008/013.pdf Section 6 +function projectiveAddTwisted( + g: GroupProjective, + h: GroupProjective, + p: bigint, + a: bigint, + d: bigint +): GroupProjective { + let { x: X1, y: Y1, z: Z1 } = g; + let { x: X2, y: Y2, z: Z2 } = h; + + // A = Z1 * Z2 + let A = mod(Z1 * Z2, p); + // B = A^2 + let B = mod(A * A, p); + // C = X1 * X2 + let C = mod(X1 * X2, p); + // D = Y1 * Y2 + let D = mod(Y1 * Y2, p); + // E = d * C * D + let E = mod(d * C * D, p); + // F = B - E + let F = mod(B - E, p); + // G = B + E + let G = mod(B + E, p); + + return { + x: mod(A * F * ((X1 + Y1) * (X2 + Y2) - C - D), p), + y: mod(A * G * (D - a * C), p), + z: mod(F * G, p), + }; +} + +// https://www.hyperelliptic.org/EFD/g1p/auto-twisted-projective.html +// https://eprint.iacr.org/2008/013.pdf Section 6 +function projectiveDoubleTwisted( + g: GroupProjective, + p: bigint, + a: bigint +): GroupProjective { + let { x: X, y: Y, z: Z } = g; + + // B = (X + Y)^2 + let B = mod((X + Y) ** 2n, p); + // C = X^2 + let C = mod(X * X, p); + // D = Y^2 + let D = mod(Y * Y, p); + // E = a * C + let E = mod(a * C, p); + // F = E + D + let F = mod(E + D, p); + // H = Z^2 + let H = mod(Z * Z, p); + // J = F - 2 * H + let J = mod(F - 2n * H, p); + + return { + x: mod((B - C - D) * J, p), + y: mod(F * (E - D), p), + z: mod(F * J, p), + }; +} + +function projectiveScaleTwisted( + g: GroupProjective, + x: bigint | boolean[], + p: bigint, + a: bigint, + d: bigint +) { + let bits = typeof x === 'bigint' ? bigIntToBits(x) : x; + let h = projectiveZeroTwisted; + for (let bit of bits) { + if (bit) h = projectiveAddTwisted(h, g, p, a, d); + g = projectiveDoubleTwisted(g, p, a); + } + return h; +} + +function projectiveFromTwisted({ + x, + y, + infinity, +}: GroupTwisted): GroupProjective { + if (infinity) return projectiveZeroTwisted; + return { x, y, z: 1n }; +} + +// The twisted curve with equation +// a * x^2 + y^2 = 1 + d * x^2 * y^2 +// in projective coordinates is represented as +// a * X^2 * Z^2 + Y^2 Z^2 = Z^4 + d * X^2 * Y^2 +// where x = X/Z, y = Y/Z, and Z ≠ 0 +function projectiveToTwisted(g: GroupProjective, p: bigint): GroupTwisted { + let z = g.z; + if (z === 0n) { + // degenerate case + return twistedZero; + } else if (z === 1n && g.x === 0n && g.y === 1n) { + // special case for the zero point + return twistedZero; + } else if (z === 1n) { + // any other normalized affine form + return { x: g.x, y: g.y, infinity: false }; + } else { + let zinv = inverse(z, p)!; // we checked for z === 0, so inverse exists + // x/z + let x = mod(g.x * zinv, p); + // y/z + let y = mod(g.y * zinv, p); + return { x: x, y: y, infinity: false }; + } +} + +type CurveTwisted = ReturnType; + +// Creates twisted Edwards curves in the form +// a * x^2 + y^2 = 1 + d * x^2 * y^2 +// with a ≠ 0, d ≠ 0 and a ≠ d +function createCurveTwisted({ + name, + modulus: p, + order, + cofactor, + generator, + a, + d, +}: TwistedCurveParams) { + let hasCofactor = cofactor !== undefined && cofactor !== 1n; + + const Field = createField(p); + const Scalar = createField(order); + const one = { ...generator, infinity: false }; + const Endo = undefined; // for Ed25519 + + assert(a !== 0n, 'a must not be zero'); + assert(d !== 0n, 'd must not be zero'); + assert(a !== d, 'a must not be equal to d'); + + return { + name, + /** + * Arithmetic over the base field + */ + Field, + /** + * Arithmetic over the scalar field + */ + Scalar, + + modulus: p, + order, + a, + d, + cofactor, + hasCofactor, + + zero: twistedZero, + one, + + hasEndomorphism: Endo !== undefined, + get Endo() { + if (Endo === undefined) throw Error(`no endomorphism defined on ${name}`); + return Endo; + }, + + from(g: { x: bigint; y: bigint }): GroupTwisted { + if (g.x === 0n && g.y === 1n) return twistedZero; + return { ...g, infinity: false }; + }, + + fromNonzero(g: { x: bigint; y: bigint }): GroupTwisted { + if (g.x === 0n && g.y === 1n) { + throw Error( + 'fromNonzero: got (0, 1), which is reserved for the zero point' + ); + } + return { ...g, infinity: false }; + }, + + equal(g: GroupTwisted, h: GroupTwisted) { + if (g.infinity && h.infinity) { + return true; + } else if (g.infinity || h.infinity) { + return false; + } else { + return mod(g.x - h.x, p) === 0n && mod(g.y - h.y, p) === 0n; + } + }, + isOnCurve(g: GroupTwisted) { + return twistedOnCurve(g, p, a, d); + }, + isInSubgroup(g: GroupAffine) { + return projectiveInSubgroup(projectiveFromTwisted(g), p, order, a); + }, + add(g: GroupTwisted, h: GroupTwisted) { + return twistedAdd(g, h, p, a, d); + }, + double(g: GroupTwisted) { + return twistedDouble(g, p, a, d); + }, + negate(g: GroupTwisted) { + return twistedNegate(g, p); + }, + sub(g: GroupTwisted, h: GroupTwisted) { + return twistedAdd(g, twistedNegate(h, p), p, a, d); + }, + scale(g: GroupTwisted, s: bigint | boolean[]) { + return twistedScale(g, s, p, a, d); + }, + }; +} diff --git a/crypto/elliptic-curve.unit-test.ts b/crypto/elliptic-curve.unit-test.ts index 228967db..9bc5314d 100644 --- a/crypto/elliptic-curve.unit-test.ts +++ b/crypto/elliptic-curve.unit-test.ts @@ -1,13 +1,14 @@ import { createCurveAffine, createCurveProjective, + createCurveTwisted, Pallas, Vesta, } from './elliptic-curve.js'; import { Fp, Fq } from './finite-field.js'; import assert from 'node:assert/strict'; import { test, Random } from '../../lib/testing/property.js'; -import { CurveParams } from './elliptic-curve-examples.js'; +import { CurveParams, TwistedCurveParams } from './elliptic-curve-examples.js'; for (let [G, Field, Scalar] of [ [Pallas, Fp, Fq] as const, @@ -164,3 +165,73 @@ function curveWithFields(params: CurveParams) { // return Curve, Field and Scalar return [Projective, Affine.Field, Affine.Scalar] as const; } + +// Twisted Edwards curve tests + +const Ed25519 = createCurveTwisted(TwistedCurveParams.Ed25519); + +let [G, Field, Scalar] = [Ed25519, Ed25519.Field, Ed25519.Scalar] as const; + +const { zero, one, add, double, negate, scale, isOnCurve, equal } = Ed25519; + +let randomScalar = Random(Scalar.random); +let randomField = Random(Field.random); +// create random points by scaling 1 with a random scalar +let randomPoint = Random(() => G.scale(G.one, Scalar.random())); +// let one / zero be sampled 20% of times each +randomPoint = Random.oneOf(zero, one, randomPoint, randomPoint, randomPoint); + +test( + randomPoint, + randomPoint, + randomPoint, + randomScalar, + randomScalar, + randomField, + (X, Y, Z, x, y) => { + // check on curve + assert(isOnCurve(X) && isOnCurve(Y) && isOnCurve(Z), 'on curve'); + + // equal + assert(equal(X, X), 'equal'); + assert( + !equal(X, add(X, X)) || equal(X, zero), + 'not equal to double of itself (or zero)' + ); + assert( + !equal(X, negate(X)) || equal(X, zero), + 'not equal to negation of itself (or zero)' + ); + assert(!equal(X, Y) || X == Y, 'not equal (random points)'); + + // algebraic laws - addition + assert(equal(add(X, Y), add(Y, X)), 'commutative'); + assert(equal(add(X, add(Y, Z)), add(add(X, Y), Z)), 'associative'); + assert(equal(add(X, zero), X), 'identity'); + assert(equal(add(X, negate(X)), zero), 'inverse'); + + // addition does doubling + assert(equal(add(X, X), double(X)), 'double'); + + // scaling by small factors + assert(equal(scale(X, 0n), zero), 'scale by 0'); + assert(equal(scale(X, 1n), X), 'scale by 1'); + assert(equal(scale(X, 2n), add(X, X)), 'scale by 2'); + assert(equal(scale(X, 3n), add(X, add(X, X))), 'scale by 3'); + assert(equal(scale(X, 4n), double(double(X))), 'scale by 4'); + + // algebraic laws - scaling + assert( + equal(scale(X, Scalar.add(x, y)), add(scale(X, x), scale(X, y))), + 'distributive' + ); + assert( + equal(scale(X, Scalar.negate(x)), negate(scale(X, x))), + 'distributive (negation)' + ); + assert( + equal(scale(X, Scalar.mul(x, y)), scale(scale(X, x), y)), + 'scale / multiply is associative' + ); + } +);