From 6b0d9611a5508ba55f10a17fb0a99e748a68dc00 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sun, 11 Dec 2022 17:16:20 +0000 Subject: [PATCH] Add Montgomery curve --- README.md | 11 +- curve-definitions/src/ed25519.ts | 48 +++--- curve-definitions/src/ed448.ts | 60 ++++--- curve-definitions/test/ed25519.test.js | 61 ++++--- curve-definitions/test/ed448.test.js | 60 +++---- package.json | 15 +- src/edwards.ts | 230 ++----------------------- src/montgomery.ts | 201 +++++++++++++++++++++ 8 files changed, 350 insertions(+), 336 deletions(-) create mode 100644 src/montgomery.ts diff --git a/README.md b/README.md index 48e5c8c..9c25f5e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Minimal, zero-dependency JS implementation of elliptic curve cryptography. -Implements Short Weierstrass curve with ECDSA signatures & Twisted Edwards curve with EdDSA signatures. +- Short Weierstrass curve with ECDSA signatures +- Twisted Edwards curve with EdDSA signatures +- Montgomery curve for ECDH key agreement To keep the package minimal, no curve definitions are provided out-of-box. Use `micro-curve-definitions` module: @@ -13,8 +15,7 @@ To keep the package minimal, no curve definitions are provided out-of-box. Use ` Future plans: -- Edwards and Montgomery curves -- hash-to-curve standard +- hash to curve standard - point indistinguishability - pairings @@ -45,8 +46,8 @@ npm install @noble/curves ``` ```ts -import weierstrass from '@noble/curves/weierstrass'; // Short Weierstrass curve -import twistedEdwards from '@noble/curves/edwards'; // Twisted Edwards curve +import { weierstrass } from '@noble/curves/weierstrass'; // Short Weierstrass curve +import { twistedEdwards } from '@noble/curves/edwards'; // Twisted Edwards curve import { sha256 } from '@noble/hashes/sha256'; import { hmac } from '@noble/hashes/hmac'; import { concatBytes, randomBytes } from '@noble/hashes/utils'; diff --git a/curve-definitions/src/ed25519.ts b/curve-definitions/src/ed25519.ts index 6d162a0..ea9af62 100644 --- a/curve-definitions/src/ed25519.ts +++ b/curve-definitions/src/ed25519.ts @@ -1,6 +1,7 @@ import { sha512 } from '@noble/hashes/sha512'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { twistedEdwards } from '@noble/curves/edwards'; +import { montgomery } from '@noble/curves/montgomery'; import { mod, pow2, isNegativeLE } from '@noble/curves/modular'; const ed25519P = BigInt( @@ -35,6 +36,17 @@ function ed25519_pow_2_252_3(x: bigint) { // ^ To pow to (p+3)/8, multiply it by x. return { pow_p_5_8, b2 }; } +function adjustScalarBytes(bytes: Uint8Array): Uint8Array { + // Section 5: For X25519, in order to decode 32 random bytes as an integer scalar, + // set the three least significant bits of the first byte + bytes[0] &= 248; // 0b1111_1000 + // and the most significant bit of the last to zero, + bytes[31] &= 127; // 0b0111_1111 + // set the second most significant bit of the last byte to 1 + bytes[31] |= 64; // 0b0100_0000 + return bytes; +} + // Just in case export const ED25519_TORSION_SUBGROUP = [ '0100000000000000000000000000000000000000000000000000000000000000', @@ -65,16 +77,7 @@ const ED25519_DEF = { Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'), hash: sha512, randomBytes, - adjustScalarBytes: (bytes: Uint8Array): Uint8Array => { - // Section 5: For X25519, in order to decode 32 random bytes as an integer scalar, - // set the three least significant bits of the first byte - bytes[0] &= 248; // 0b1111_1000 - // and the most significant bit of the last to zero, - bytes[31] &= 127; // 0b0111_1111 - // set the second most significant bit of the last byte to 1 - bytes[31] |= 64; // 0b0100_0000 - return bytes; - }, + adjustScalarBytes, // dom2 // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. // Constant-time, u/√v @@ -96,16 +99,6 @@ const ED25519_DEF = { if (isNegativeLE(x, P)) x = mod(-x, P); return { isValid: useRoot1 || useRoot2, value: x }; }, - // ECDH - // The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519 - a24: BigInt('121665'), - montgomeryBits: 255, // n is 253 bits - powPminus2: (x: bigint): bigint => { - const P = ed25519P; - // x^(p-2) aka x^(2^255-21) - const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x); - return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P); - }, } as const; export const ed25519 = twistedEdwards(ED25519_DEF); @@ -124,3 +117,18 @@ export const ed25519ph = twistedEdwards({ domain: ed25519_domain, preHash: sha512, }); + +export const x25519 = montgomery({ + P: ed25519P, + a24: BigInt('121665'), + montgomeryBits: 255, // n is 253 bits + nByteLength: 32, + Gu: '0900000000000000000000000000000000000000000000000000000000000000', + powPminus2: (x: bigint): bigint => { + const P = ed25519P; + // x^(p-2) aka x^(2^255-21) + const { pow_p_5_8, b2 } = ed25519_pow_2_252_3(x); + return mod(pow2(pow_p_5_8, BigInt(3), P) * b2, P); + }, + adjustScalarBytes, +}); diff --git a/curve-definitions/src/ed448.ts b/curve-definitions/src/ed448.ts index 9f1a644..5fda47b 100644 --- a/curve-definitions/src/ed448.ts +++ b/curve-definitions/src/ed448.ts @@ -3,6 +3,7 @@ import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/h import { PointType, twistedEdwards } from '@noble/curves/edwards'; import { mod, pow2, invert } from '@noble/curves/modular'; import { numberToBytesLE } from '@noble/curves/utils'; +import { montgomery } from '../../lib/montgomery.js'; const _0n = BigInt(0); @@ -32,6 +33,17 @@ function ed448_pow_Pminus3div4(x: bigint): bigint { return (pow2(b223, 223n, P) * b222) % P; } +function adjustScalarBytes(bytes: Uint8Array): Uint8Array { + // Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most + // significant bit of the last byte to 1. + bytes[0] &= 252; // 0b11111100 + // and the most significant bit of the last byte to 1. + bytes[55] |= 128; // 0b10000000 + // NOTE: is is NOOP for 56 bytes scalars (X25519/X448) + bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits) + return bytes; +} + const ED448_DEF = { // Param: a a: BigInt(1), @@ -59,16 +71,7 @@ const ED448_DEF = { // SHAKE256(dom4(phflag,context)||x, 114) hash: shake256_114, randomBytes, - adjustScalarBytes: (bytes: Uint8Array): Uint8Array => { - // Section 5: Likewise, for X448, set the two least significant bits of the first byte to 0, and the most - // significant bit of the last byte to 1. - bytes[0] &= 252; // 0b11111100 - // and the most significant bit of the last byte to 1. - bytes[55] |= 128; // 0b10000000 - // NOTE: is is NOOP for 56 bytes scalars (X25519/X448) - bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits) - return bytes; - }, + adjustScalarBytes, // dom4 domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => { if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`); @@ -101,34 +104,37 @@ const ED448_DEF = { // square root exists, and the decoding fails. return { isValid: mod(x2 * v, P) === u, value: x }; }, - // ECDH - // basePointU: - // '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', - // The constant a24 is (156326 - 2) / 4 = 39081 for curve448/X448. +} as const; + +export const ed448 = twistedEdwards(ED448_DEF); +// NOTE: there is no ed448ctx, since ed448 supports ctx by default +export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 }); + +export const x448 = montgomery({ a24: BigInt('39081'), montgomeryBits: 448, + nByteLength: 57, + P: ed448P, + Gu: '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', powPminus2: (x: bigint): bigint => { const P = ed448P; const Pminus3div4 = ed448_pow_Pminus3div4(x); const Pminus3 = pow2(Pminus3div4, BigInt(2), P); return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2 }, + adjustScalarBytes, // The 4-isogeny maps between the Montgomery curve and this Edwards // curve are: // (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3) // (x, y) = (4*v*(u^2 - 1)/(u^4 - 2*u^2 + 4*v^2 + 1), // -(u^5 - 2*u^3 - 4*u*v^2 + u)/ // (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u)) - UfromPoint: (p: PointType) => { - const P = ed448P; - const { x, y } = p; - if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`); - const invX = invert(x * x, P); // x^2 - const u = mod(y * y * invX, P); // (y^2/x^2) - return numberToBytesLE(u, 56); - }, -} as const; - -export const ed448 = twistedEdwards(ED448_DEF); -// NOTE: there is no ed448ctx, since ed448 supports ctx by default -export const ed448ph = twistedEdwards({ ...ED448_DEF, preHash: shake256_64 }); + // xyToU: (p: PointType) => { + // const P = ed448P; + // const { x, y } = p; + // if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`); + // const invX = invert(x * x, P); // x^2 + // const u = mod(y * y * invX, P); // (y^2/x^2) + // return numberToBytesLE(u, 56); + // }, +}); diff --git a/curve-definitions/test/ed25519.test.js b/curve-definitions/test/ed25519.test.js index 468200a..b1b2ed6 100644 --- a/curve-definitions/test/ed25519.test.js +++ b/curve-definitions/test/ed25519.test.js @@ -1,13 +1,12 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; import * as fc from 'fast-check'; -import { ed25519, ed25519ctx, ed25519ph } from '../lib/ed25519.js'; +import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/ed25519.js'; import { readFileSync } from 'fs'; import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' }; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' }; import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' }; -import { sha512 } from '@noble/hashes/sha512'; const ed = ed25519; const hex = bytesToHex; @@ -455,7 +454,7 @@ const rfc7748Mul = [ for (let i = 0; i < rfc7748Mul.length; i++) { const v = rfc7748Mul[i]; should(`RFC7748: scalarMult (${i})`, () => { - deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU); + deepStrictEqual(hex(x25519.scalarMult(v.u, v.scalar)), v.outputU); }); } @@ -467,8 +466,8 @@ const rfc7748Iter = [ for (let i = 0; i < rfc7748Iter.length; i++) { const { scalar, iters } = rfc7748Iter[i]; should(`RFC7748: scalarMult iteration (${i})`, () => { - let k = ed.montgomeryCurve.BASE_POINT_U; - for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k]; + let k = x25519.Gu; + for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(u, k), k]; deepStrictEqual(hex(k), scalar); }); } @@ -479,33 +478,33 @@ should('RFC7748 getSharedKey', () => { const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb'; const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f'; const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742'; - deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate))); - deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate))); - deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared); - deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared); + deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate))); + deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate))); + deepStrictEqual(hex(x25519.scalarMult(bobPublic, alicePrivate)), shared); + deepStrictEqual(hex(x25519.scalarMult(alicePublic, bobPrivate)), shared); }); -should('X25519/getSharedSecret() should be commutative', () => { - for (let i = 0; i < 512; i++) { - const asec = ed.utils.randomPrivateKey(); - const apub = ed.getPublicKey(asec); - const bsec = ed.utils.randomPrivateKey(); - const bpub = ed.getPublicKey(bsec); - try { - deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub)); - } catch (error) { - console.error('not commutative', { asec, apub, bsec, bpub }); - throw error; - } - } -}); +// should('X25519/getSharedSecret() should be commutative', () => { +// for (let i = 0; i < 512; i++) { +// const asec = ed.utils.randomPrivateKey(); +// const apub = ed.getPublicKey(asec); +// const bsec = ed.utils.randomPrivateKey(); +// const bpub = ed.getPublicKey(bsec); +// try { +// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub)); +// } catch (error) { +// console.error('not commutative', { asec, apub, bsec, bpub }); +// throw error; +// } +// } +// }); -should('X25519: should convert base point to montgomery using fromPoint', () => { - deepStrictEqual( - hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), - ed.montgomeryCurve.BASE_POINT_U - ); -}); +// should('X25519: should convert base point to montgomery using fromPoint', () => { +// deepStrictEqual( +// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), +// ed.montgomeryCurve.BASE_POINT_U +// ); +// }); { const group = x25519vectors.testGroups[0]; @@ -514,7 +513,7 @@ should('X25519: should convert base point to montgomery using fromPoint', () => should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => { if (v.result === 'valid' || v.result === 'acceptable') { try { - const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public)); + const shared = hex(x25519.scalarMult(v.public, v.private)); deepStrictEqual(shared, v.shared, 'valid'); } catch (e) { // We are more strict @@ -525,7 +524,7 @@ should('X25519: should convert base point to montgomery using fromPoint', () => } else if (v.result === 'invalid') { let failed = false; try { - ed.montgomeryCurve.getSharedSecret(v.private, v.public); + x25519.scalarMult(v.public, v.private); } catch (error) { failed = true; } diff --git a/curve-definitions/test/ed448.test.js b/curve-definitions/test/ed448.test.js index cbe9589..5dc2942 100644 --- a/curve-definitions/test/ed448.test.js +++ b/curve-definitions/test/ed448.test.js @@ -1,7 +1,7 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; import * as fc from 'fast-check'; -import { ed448, ed448ph } from '../lib/ed448.js'; +import { ed448, ed448ph, x448 } from '../lib/ed448.js'; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' }; import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' }; @@ -478,7 +478,7 @@ const rfc7748Mul = [ for (let i = 0; i < rfc7748Mul.length; i++) { const v = rfc7748Mul[i]; should(`RFC7748: scalarMult (${i})`, () => { - deepStrictEqual(hex(ed.montgomeryCurve.scalarMult(v.u, v.scalar)), v.outputU); + deepStrictEqual(hex(x448.scalarMult(v.u, v.scalar)), v.outputU); }); } @@ -498,8 +498,8 @@ const rfc7748Iter = [ for (let i = 0; i < rfc7748Iter.length; i++) { const { scalar, iters } = rfc7748Iter[i]; should(`RFC7748: scalarMult iteration (${i})`, () => { - let k = ed.montgomeryCurve.BASE_POINT_U; - for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k]; + let k = x448.Gu; + for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(u, k), k]; deepStrictEqual(hex(k), scalar); }); } @@ -515,10 +515,10 @@ should('RFC7748 getSharedKey', () => { '3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609'; const shared = '07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d'; - deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate))); - deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate))); - deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared); - deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared); + deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate))); + deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate))); + deepStrictEqual(hex(x448.scalarMult(bobPublic, alicePrivate)), shared); + deepStrictEqual(hex(x448.scalarMult(alicePublic, bobPrivate)), shared); }); { @@ -528,7 +528,7 @@ should('RFC7748 getSharedKey', () => { should(`Wycheproof/X448(${i}, ${v.result}) ${v.comment}`, () => { if (v.result === 'valid' || v.result === 'acceptable') { try { - const shared = hex(ed.montgomeryCurve.getSharedSecret(v.private, v.public)); + const shared = hex(x448.scalarMult(v.public, v.private)); deepStrictEqual(shared, v.shared, 'valid'); } catch (e) { // We are more strict @@ -540,7 +540,7 @@ should('RFC7748 getSharedKey', () => { } else if (v.result === 'invalid') { let failed = false; try { - ed.montgomeryCurve.getSharedSecret(v.private, v.public); + x448.scalarMult(v.public, v.private); } catch (error) { failed = true; } @@ -550,27 +550,27 @@ should('RFC7748 getSharedKey', () => { } } -should('X448: should convert base point to montgomery using fromPoint', () => { - deepStrictEqual( - hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), - ed.montgomeryCurve.BASE_POINT_U - ); -}); +// should('X448: should convert base point to montgomery using fromPoint', () => { +// deepStrictEqual( +// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)), +// ed.montgomeryCurve.BASE_POINT_U +// ); +// }); -should('X448/getSharedSecret() should be commutative', async () => { - for (let i = 0; i < 512; i++) { - const asec = ed.utils.randomPrivateKey(); - const apub = ed.getPublicKey(asec); - const bsec = ed.utils.randomPrivateKey(); - const bpub = ed.getPublicKey(bsec); - try { - deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub)); - } catch (error) { - console.error('not commutative', { asec, apub, bsec, bpub }); - throw error; - } - } -}); +// should('X448/getSharedSecret() should be commutative', async () => { +// for (let i = 0; i < 512; i++) { +// const asec = ed.utils.randomPrivateKey(); +// const apub = ed.getPublicKey(asec); +// const bsec = ed.utils.randomPrivateKey(); +// const bpub = ed.getPublicKey(bsec); +// try { +// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub)); +// } catch (error) { +// console.error('not commutative', { asec, apub, bsec, bpub }); +// throw error; +// } +// } +// }); const VECTORS_RFC8032_CTX = [ { diff --git a/package.json b/package.json index a4b9bab..731f429 100644 --- a/package.json +++ b/package.json @@ -32,21 +32,26 @@ }, "main": "index.js", "exports": { + "./edwards": { + "types": "./lib/edwards.d.ts", + "import": "./lib/esm/edwards.js", + "default": "./lib/edwards.js" + }, "./modular": { "types": "./lib/modular.d.ts", "import": "./lib/esm/modular.js", "default": "./lib/modular.js" }, + "./montgomery": { + "types": "./lib/montgomery.d.ts", + "import": "./lib/esm/montgomery.js", + "default": "./lib/montgomery.js" + }, "./weierstrass": { "types": "./lib/weierstrass.d.ts", "import": "./lib/esm/weierstrass.js", "default": "./lib/weierstrass.js" }, - "./edwards": { - "types": "./lib/edwards.d.ts", - "import": "./lib/esm/edwards.js", - "default": "./lib/edwards.js" - }, "./utils": { "types": "./lib/utils.d.ts", "import": "./lib/esm/utils.js", diff --git a/src/edwards.ts b/src/edwards.ts index 8cb829e..a584741 100644 --- a/src/edwards.ts +++ b/src/edwards.ts @@ -16,6 +16,7 @@ import { concatBytes, ensureBytes, numberToBytesLE, + bytesToNumberLE, nLength, hashToPrivateScalar, } from './utils.js'; @@ -56,14 +57,6 @@ export type CurveType = { domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; preHash?: CHash; - // ECDH related - // Other constants - a24: bigint; // Related to d, but cannot be derived from it - // ECDH bits (can be different from N bits) - montgomeryBits?: number; - basePointU?: string; // TODO: why not bigint? - powPminus2?: (x: bigint) => bigint; - UfromPoint?: (p: PointType) => Uint8Array; }; // We accept hex strings besides Uint8Array for simplicity @@ -75,11 +68,11 @@ type PrivKey = Hex | bigint | number; function validateOpts(curve: CurveType) { if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen)) throw new Error('Invalid hash function'); - for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy', 'a24'] as const) { + for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy'] as const) { if (typeof curve[i] !== 'bigint') throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); } - for (const i of ['nBitLength', 'nByteLength', 'montgomeryBits'] as const) { + for (const i of ['nBitLength', 'nByteLength'] as const) { if (curve[i] === undefined) continue; // Optional if (!Number.isSafeInteger(curve[i])) throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); @@ -87,21 +80,10 @@ function validateOpts(curve: CurveType) { for (const fn of ['randomBytes'] as const) { if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); } - for (const fn of [ - 'adjustScalarBytes', - 'domain', - 'uvRatio', - 'powPminus2', - 'UfromPoint', - ] as const) { + for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio'] as const) { if (curve[fn] === undefined) continue; // Optional if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); } - for (const i of ['basePointU'] as const) { - if (curve[i] === undefined) continue; // Optional - if (typeof curve[i] !== 'string') - throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); - } // Set defaults return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const); } @@ -176,20 +158,11 @@ export type SigType = Hex | SignatureType; export type CurveFn = { CURVE: ReturnType; getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; - getSharedSecret: (privateKey: PrivKey, publicKey: Hex) => Uint8Array; sign: (message: Hex, privateKey: Hex) => Uint8Array; verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean; Point: PointConstructor; ExtendedPoint: ExtendedPointConstructor; Signature: SignatureConstructor; - montgomeryCurve: { - BASE_POINT_U: string; - UfromPoint: (p: PointType) => Uint8Array; - scalarMult: (u: Hex, scalar: Hex) => Uint8Array; - scalarMultBase: (scalar: Hex) => Uint8Array; - getPublicKey: (privateKey: Hex) => Uint8Array; - getSharedSecret: (privateKey: Hex, publicKey: Hex) => Uint8Array; - }; utils: { mod: (a: bigint, b?: bigint) => bigint; invert: (number: bigint, modulo?: bigint) => bigint; @@ -229,8 +202,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } const uvRatio = CURVE.uvRatio || _uvRatio; - const _powPminus2 = (x: bigint) => mod.pow(x, P - _2n, P); - const powPminus2 = CURVE.powPminus2 || _powPminus2; const _adjustScalarBytes = (bytes: Uint8Array) => bytes; // NOOP const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes; function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { @@ -553,10 +524,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } // Little Endian - function bytesToNumberLE(uint8a: Uint8Array): bigint { - if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); - return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse())); - } + // function bytesToNumberLE(uint8a: Uint8Array): bigint { + // if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); + // return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse())); + // } // ------------------------- // Little-endian SHA512 with modulo n @@ -684,194 +655,19 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { // Enable precomputes. Slows down first publicKey computation by 20ms. Point.BASE._setWindowSize(8); - // ECDH (X22519/X448) - // https://datatracker.ietf.org/doc/html/rfc7748 - // Every twisted Edwards curve is birationally equivalent to an elliptic curve in Montgomery form and vice versa. - const montgomeryBits = CURVE.montgomeryBits || CURVE.nBitLength; - const montgomeryBytes = Math.ceil(montgomeryBits / 8); - - // cswap from RFC7748 - function cswap(swap: bigint, x_2: bigint, x_3: bigint): [bigint, bigint] { - const dummy = modP(swap * (x_2 - x_3)); - x_2 = modP(x_2 - dummy); - x_3 = modP(x_3 + dummy); - return [x_2, x_3]; - } - - // x25519 from 4 - /** - * - * @param pointU u coordinate (x) on Montgomery Curve 25519 - * @param scalar by which the point would be multiplied - * @returns new Point on Montgomery curve - */ - function montgomeryLadder(pointU: bigint, scalar: bigint): bigint { - const { P } = CURVE; - const u = normalizeScalar(pointU, P); - // Section 5: Implementations MUST accept non-canonical values and process them as - // if they had been reduced modulo the field prime. - const k = normalizeScalar(scalar, P); - // The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519 - const a24 = CURVE.a24; - const x_1 = u; - let x_2 = _1n; - let z_2 = _0n; - let x_3 = u; - let z_3 = _1n; - let swap = _0n; - let sw: [bigint, bigint]; - for (let t = BigInt(montgomeryBits - 1); t >= _0n; t--) { - const k_t = (k >> t) & _1n; - swap ^= k_t; - sw = cswap(swap, x_2, x_3); - x_2 = sw[0]; - x_3 = sw[1]; - sw = cswap(swap, z_2, z_3); - z_2 = sw[0]; - z_3 = sw[1]; - swap = k_t; - - const A = x_2 + z_2; - const AA = modP(A * A); - const B = x_2 - z_2; - const BB = modP(B * B); - const E = AA - BB; - const C = x_3 + z_3; - const D = x_3 - z_3; - const DA = modP(D * A); - const CB = modP(C * B); - const dacb = DA + CB; - const da_cb = DA - CB; - x_3 = modP(dacb * dacb); - z_3 = modP(x_1 * modP(da_cb * da_cb)); - x_2 = modP(AA * BB); - z_2 = modP(E * (AA + modP(a24 * E))); - } - // (x_2, x_3) = cswap(swap, x_2, x_3) - sw = cswap(swap, x_2, x_3); - x_2 = sw[0]; - x_3 = sw[1]; - // (z_2, z_3) = cswap(swap, z_2, z_3) - sw = cswap(swap, z_2, z_3); - z_2 = sw[0]; - z_3 = sw[1]; - // z_2^(p - 2) - const z2 = powPminus2(z_2); - // Return x_2 * (z_2^(p - 2)) - return modP(x_2 * z2); - } - - function encodeUCoordinate(u: bigint): Uint8Array { - return numberToBytesLE(modP(u), montgomeryBytes); - } - - function decodeUCoordinate(uEnc: Hex): bigint { - const u = ensureBytes(uEnc, montgomeryBytes); - // Section 5: When receiving such an array, implementations of X25519 - // MUST mask the most significant bit in the final byte. - // This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP - // fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519 - u[fieldLen - 1] &= 127; // 0b0111_1111 - return bytesToNumberLE(u); - } - - function decodeScalar(n: Hex): bigint { - const bytes = ensureBytes(n); - if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen) - throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`); - return bytesToNumberLE(adjustScalarBytes(bytes)); - } - /* - Converts Point to Montgomery Curve - - u, v: curve25519 coordinates - - x, y: ed25519 coordinates - RFC 7748 (https://www.rfc-editor.org/rfc/rfc7748) says - - The birational maps are (25519): - (u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x) - (x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1)) - - The birational maps are (448): - (u, v) = ((y-1)/(y+1), sqrt(156324)*u/x) - (x, y) = (sqrt(156324)*u/v, (1+u)/(1-u)) - - But original Twisted Edwards paper (https://eprint.iacr.org/2008/013.pdf) and hyperelliptics (http://hyperelliptic.org/EFD/g1p/data/twisted/coordinates) - says that mapping is always: - - u = (1+y)/(1-y) - - v = 2 (1+y)/(x(1-y)) - - x = 2 u/v - - y = (u-1)/(u+1) - - Which maps correctly, but to completely different curve. There is different mapping for ed448 (which done with replaceble function). - Returns 'u' coordinate of curve25519 point. - - NOTE: jubjub will need full mapping, for now only Point -> U is enough - */ - function _UfromPoint(p: Point): Uint8Array { - if (!(p instanceof Point)) throw new Error('Wrong point'); - const { y } = p; - const u = modP((y + _1n) * mod.invert(_1n - y, P)); - return numberToBytesLE(u, montgomeryBytes); - } - const UfromPoint = CURVE.UfromPoint || _UfromPoint; - - const BASE_POINT_U = CURVE.basePointU || bytesToHex(UfromPoint(Point.BASE)); - // Multiply point u by scalar - function scalarMult(u: Hex, scalar: Hex): Uint8Array { - const pointU = decodeUCoordinate(u); - const _scalar = decodeScalar(scalar); - const pu = montgomeryLadder(pointU, _scalar); - // The result was not contributory - // https://cr.yp.to/ecdh.html#validate - if (pu === _0n) throw new Error('Invalid private or public key received'); - return encodeUCoordinate(pu); - } - // Multiply base point by scalar - const scalarMultBase = (scalar: Hex): Uint8Array => - montgomeryCurve.scalarMult(montgomeryCurve.BASE_POINT_U, scalar); - - const montgomeryCurve = { - BASE_POINT_U, - UfromPoint, - // NOTE: we can get 'y' coordinate from 'u', but Point.fromHex also wants 'x' coordinate oddity flag, and we cannot get 'x' without knowing 'v' - // Need to add generic conversion between twisted edwards and complimentary curve for JubJub - scalarMult, - scalarMultBase, - // NOTE: these function work on complimentary montgomery curve - getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(publicKey, privateKey), - getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey), - }; - - /** - * Calculates X25519 DH shared secret from ed25519 private & public keys. - * Curve25519 used in X25519 consumes private keys as-is, while ed25519 hashes them with sha512. - * Which means we will need to normalize ed25519 seeds to "hashed repr". - * @param privateKey ed25519 private key - * @param publicKey ed25519 public key - * @returns X25519 shared key - */ - function getSharedSecret(privateKey: PrivKey, publicKey: Hex): Uint8Array { - const { head } = getExtendedPublicKey(privateKey); - const u = montgomeryCurve.UfromPoint(Point.fromHex(publicKey)); - return montgomeryCurve.getSharedSecret(head, u); - } - const utils = { getExtendedPublicKey, mod: modP, invert: (a: bigint, m = CURVE.P) => mod.invert(a, m), /** - * Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF - * and convert them into private scalar, with the modulo bias being neglible. - * As per FIPS 186 B.4.1. * Not needed for ed25519 private keys. Needed if you use scalars directly (rare). - * @param hash hash output from sha512, or a similar function - * @returns valid private scalar */ hashToPrivateScalar: (hash: Hex): bigint => hashToPrivateScalar(hash, CURVE_ORDER, true), /** * ed25519 private keys are uniform 32-bit strings. We do not need to check for - * modulo bias like we do in noble-secp256k1 randomPrivateKey() + * modulo bias like we do in secp256k1 randomPrivateKey() */ randomPrivateKey: (): Uint8Array => randomBytes(fieldLen), @@ -891,14 +687,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { return { CURVE, - montgomeryCurve, - getSharedSecret, + getPublicKey, + sign, + verify, ExtendedPoint, Point, Signature, - getPublicKey, utils, - sign, - verify, }; } diff --git a/src/montgomery.ts b/src/montgomery.ts new file mode 100644 index 0000000..8cdc810 --- /dev/null +++ b/src/montgomery.ts @@ -0,0 +1,201 @@ +import * as mod from './modular.js'; +import { + ensureBytes, + numberToBytesLE, + bytesToNumberLE, + // nLength, +} from './utils.js'; + +const _0n = BigInt(0); +const _1n = BigInt(1); +type Hex = string | Uint8Array; + +export type CurveType = { + // Field over which we'll do calculations. Verify with: + P: bigint; + nByteLength: number; + adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; + domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; + a24: bigint; // Related to d, but cannot be derived from it + montgomeryBits: number; + powPminus2?: (x: bigint) => bigint; + xyToU?: (x: bigint, y: bigint) => bigint; + Gu: string; +}; +export type CurveFn = { + scalarMult: (u: Hex, scalar: Hex) => Uint8Array; + scalarMultBase: (scalar: Hex) => Uint8Array; + getPublicKey: (privateKey: Hex) => Uint8Array; + Gu: string; +}; + +function validateOpts(curve: CurveType) { + for (const i of ['a24'] as const) { + if (typeof curve[i] !== 'bigint') + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const i of ['montgomeryBits', 'nByteLength'] as const) { + if (curve[i] === undefined) continue; // Optional + if (!Number.isSafeInteger(curve[i])) + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const fn of ['adjustScalarBytes', 'domain', 'powPminus2'] as const) { + if (curve[fn] === undefined) continue; // Optional + if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); + } + for (const i of ['Gu'] as const) { + if (curve[i] === undefined) continue; // Optional + if (typeof curve[i] !== 'string') + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + // Set defaults + // ...nLength(curve.n, curve.nBitLength), + return Object.freeze({ ...curve } as const); +} + +export function montgomery(curveDef: CurveType): CurveFn { + const CURVE = validateOpts(curveDef); + const { P } = CURVE; + const modP = (a: bigint) => mod.mod(a, P); + const montgomeryBits = CURVE.montgomeryBits; + const montgomeryBytes = Math.ceil(montgomeryBits / 8); + const fieldLen = CURVE.nByteLength; + const adjustScalarBytes = CURVE.adjustScalarBytes || ((bytes: Uint8Array) => bytes); + const powPminus2 = CURVE.powPminus2 || ((x: bigint) => mod.pow(x, P - BigInt(2), P)); + + /** + * Checks for num to be in range: + * For strict == true: `0 < num < max`. + * For strict == false: `0 <= num < max`. + * Converts non-float safe numbers to bigints. + */ + function normalizeScalar(num: number | bigint, max: bigint, strict = true): bigint { + if (!max) throw new TypeError('Specify max value'); + if (typeof num === 'number' && Number.isSafeInteger(num)) num = BigInt(num); + if (typeof num === 'bigint' && num < max) { + if (strict) { + if (_0n < num) return num; + } else { + if (_0n <= num) return num; + } + } + throw new TypeError('Expected valid scalar: 0 < scalar < max'); + } + + // cswap from RFC7748 + function cswap(swap: bigint, x_2: bigint, x_3: bigint): [bigint, bigint] { + const dummy = modP(swap * (x_2 - x_3)); + x_2 = modP(x_2 - dummy); + x_3 = modP(x_3 + dummy); + return [x_2, x_3]; + } + + // x25519 from 4 + /** + * + * @param pointU u coordinate (x) on Montgomery Curve 25519 + * @param scalar by which the point would be multiplied + * @returns new Point on Montgomery curve + */ + function montgomeryLadder(pointU: bigint, scalar: bigint): bigint { + const { P } = CURVE; + const u = normalizeScalar(pointU, P); + // Section 5: Implementations MUST accept non-canonical values and process them as + // if they had been reduced modulo the field prime. + const k = normalizeScalar(scalar, P); + // The constant a24 is (486662 - 2) / 4 = 121665 for curve25519/X25519 + const a24 = CURVE.a24; + const x_1 = u; + let x_2 = _1n; + let z_2 = _0n; + let x_3 = u; + let z_3 = _1n; + let swap = _0n; + let sw: [bigint, bigint]; + for (let t = BigInt(montgomeryBits - 1); t >= _0n; t--) { + const k_t = (k >> t) & _1n; + swap ^= k_t; + sw = cswap(swap, x_2, x_3); + x_2 = sw[0]; + x_3 = sw[1]; + sw = cswap(swap, z_2, z_3); + z_2 = sw[0]; + z_3 = sw[1]; + swap = k_t; + + const A = x_2 + z_2; + const AA = modP(A * A); + const B = x_2 - z_2; + const BB = modP(B * B); + const E = AA - BB; + const C = x_3 + z_3; + const D = x_3 - z_3; + const DA = modP(D * A); + const CB = modP(C * B); + const dacb = DA + CB; + const da_cb = DA - CB; + x_3 = modP(dacb * dacb); + z_3 = modP(x_1 * modP(da_cb * da_cb)); + x_2 = modP(AA * BB); + z_2 = modP(E * (AA + modP(a24 * E))); + } + // (x_2, x_3) = cswap(swap, x_2, x_3) + sw = cswap(swap, x_2, x_3); + x_2 = sw[0]; + x_3 = sw[1]; + // (z_2, z_3) = cswap(swap, z_2, z_3) + sw = cswap(swap, z_2, z_3); + z_2 = sw[0]; + z_3 = sw[1]; + // z_2^(p - 2) + const z2 = powPminus2(z_2); + // Return x_2 * (z_2^(p - 2)) + return modP(x_2 * z2); + } + + function encodeUCoordinate(u: bigint): Uint8Array { + return numberToBytesLE(modP(u), montgomeryBytes); + } + + function decodeUCoordinate(uEnc: Hex): bigint { + const u = ensureBytes(uEnc, montgomeryBytes); + // Section 5: When receiving such an array, implementations of X25519 + // MUST mask the most significant bit in the final byte. + // This is very ugly way, but it works because fieldLen-1 is outside of bounds for X448, so this becomes NOOP + // fieldLen - scalaryBytes = 1 for X448 and = 0 for X25519 + u[fieldLen - 1] &= 127; // 0b0111_1111 + return bytesToNumberLE(u); + } + + function decodeScalar(n: Hex): bigint { + const bytes = ensureBytes(n); + if (bytes.length !== montgomeryBytes && bytes.length !== fieldLen) + throw new Error(`Expected ${montgomeryBytes} or ${fieldLen} bytes, got ${bytes.length}`); + return bytesToNumberLE(adjustScalarBytes(bytes)); + } + // Multiply point u by scalar + function scalarMult(u: Hex, scalar: Hex): Uint8Array { + const pointU = decodeUCoordinate(u); + const _scalar = decodeScalar(scalar); + const pu = montgomeryLadder(pointU, _scalar); + // The result was not contributory + // https://cr.yp.to/ecdh.html#validate + if (pu === _0n) throw new Error('Invalid private or public key received'); + return encodeUCoordinate(pu); + } + // Multiply base point by scalar + function scalarMultBase(scalar: Hex): Uint8Array { + return scalarMult(CURVE.Gu, scalar); + } + + return { + // NOTE: we can get 'y' coordinate from 'u', but Point.fromHex also wants 'x' coordinate oddity flag, and we cannot get 'x' without knowing 'v' + // Need to add generic conversion between twisted edwards and complimentary curve for JubJub + scalarMult, + scalarMultBase, + // NOTE: these function work on complimentary montgomery curve + // getSharedSecret: (privateKey: Hex, publicKey: Hex) => scalarMult(publicKey, privateKey), + getPublicKey: (privateKey: Hex): Uint8Array => scalarMultBase(privateKey), + Gu: CURVE.Gu, + }; +}