Add Montgomery curve

This commit is contained in:
Paul Miller 2022-12-11 17:16:20 +00:00
parent b92866d9b8
commit 6b0d9611a5
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
8 changed files with 350 additions and 336 deletions

@ -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';

@ -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,
});

@ -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);
// },
});

@ -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;
}

@ -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 = [
{

@ -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",

@ -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<typeof validateOpts>;
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,
};
}

201
src/montgomery.ts Normal file

@ -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,
};
}