forked from tornado-packages/noble-curves
Add Montgomery curve
This commit is contained in:
parent
b92866d9b8
commit
6b0d9611a5
11
README.md
11
README.md
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Minimal, zero-dependency JS implementation of elliptic curve cryptography.
|
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:
|
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:
|
Future plans:
|
||||||
|
|
||||||
- Edwards and Montgomery curves
|
- hash to curve standard
|
||||||
- hash-to-curve standard
|
|
||||||
- point indistinguishability
|
- point indistinguishability
|
||||||
- pairings
|
- pairings
|
||||||
|
|
||||||
@ -45,8 +46,8 @@ npm install @noble/curves
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import weierstrass from '@noble/curves/weierstrass'; // Short Weierstrass curve
|
import { weierstrass } from '@noble/curves/weierstrass'; // Short Weierstrass curve
|
||||||
import twistedEdwards from '@noble/curves/edwards'; // Twisted Edwards curve
|
import { twistedEdwards } from '@noble/curves/edwards'; // Twisted Edwards curve
|
||||||
import { sha256 } from '@noble/hashes/sha256';
|
import { sha256 } from '@noble/hashes/sha256';
|
||||||
import { hmac } from '@noble/hashes/hmac';
|
import { hmac } from '@noble/hashes/hmac';
|
||||||
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes } from '@noble/hashes/utils';
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { sha512 } from '@noble/hashes/sha512';
|
import { sha512 } from '@noble/hashes/sha512';
|
||||||
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
|
||||||
import { twistedEdwards } from '@noble/curves/edwards';
|
import { twistedEdwards } from '@noble/curves/edwards';
|
||||||
|
import { montgomery } from '@noble/curves/montgomery';
|
||||||
import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
|
import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
|
||||||
|
|
||||||
const ed25519P = BigInt(
|
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.
|
// ^ To pow to (p+3)/8, multiply it by x.
|
||||||
return { pow_p_5_8, b2 };
|
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
|
// Just in case
|
||||||
export const ED25519_TORSION_SUBGROUP = [
|
export const ED25519_TORSION_SUBGROUP = [
|
||||||
'0100000000000000000000000000000000000000000000000000000000000000',
|
'0100000000000000000000000000000000000000000000000000000000000000',
|
||||||
@ -65,16 +77,7 @@ const ED25519_DEF = {
|
|||||||
Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'),
|
Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'),
|
||||||
hash: sha512,
|
hash: sha512,
|
||||||
randomBytes,
|
randomBytes,
|
||||||
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
|
adjustScalarBytes,
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
// dom2
|
// dom2
|
||||||
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
|
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
|
||||||
// Constant-time, u/√v
|
// Constant-time, u/√v
|
||||||
@ -96,16 +99,6 @@ const ED25519_DEF = {
|
|||||||
if (isNegativeLE(x, P)) x = mod(-x, P);
|
if (isNegativeLE(x, P)) x = mod(-x, P);
|
||||||
return { isValid: useRoot1 || useRoot2, value: x };
|
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;
|
} as const;
|
||||||
|
|
||||||
export const ed25519 = twistedEdwards(ED25519_DEF);
|
export const ed25519 = twistedEdwards(ED25519_DEF);
|
||||||
@ -124,3 +117,18 @@ export const ed25519ph = twistedEdwards({
|
|||||||
domain: ed25519_domain,
|
domain: ed25519_domain,
|
||||||
preHash: sha512,
|
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 { PointType, twistedEdwards } from '@noble/curves/edwards';
|
||||||
import { mod, pow2, invert } from '@noble/curves/modular';
|
import { mod, pow2, invert } from '@noble/curves/modular';
|
||||||
import { numberToBytesLE } from '@noble/curves/utils';
|
import { numberToBytesLE } from '@noble/curves/utils';
|
||||||
|
import { montgomery } from '../../lib/montgomery.js';
|
||||||
|
|
||||||
const _0n = BigInt(0);
|
const _0n = BigInt(0);
|
||||||
|
|
||||||
@ -32,6 +33,17 @@ function ed448_pow_Pminus3div4(x: bigint): bigint {
|
|||||||
return (pow2(b223, 223n, P) * b222) % P;
|
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 = {
|
const ED448_DEF = {
|
||||||
// Param: a
|
// Param: a
|
||||||
a: BigInt(1),
|
a: BigInt(1),
|
||||||
@ -59,16 +71,7 @@ const ED448_DEF = {
|
|||||||
// SHAKE256(dom4(phflag,context)||x, 114)
|
// SHAKE256(dom4(phflag,context)||x, 114)
|
||||||
hash: shake256_114,
|
hash: shake256_114,
|
||||||
randomBytes,
|
randomBytes,
|
||||||
adjustScalarBytes: (bytes: Uint8Array): Uint8Array => {
|
adjustScalarBytes,
|
||||||
// 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;
|
|
||||||
},
|
|
||||||
// dom4
|
// dom4
|
||||||
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
domain: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => {
|
||||||
if (ctx.length > 255) throw new Error(`Context is too big: ${ctx.length}`);
|
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.
|
// square root exists, and the decoding fails.
|
||||||
return { isValid: mod(x2 * v, P) === u, value: x };
|
return { isValid: mod(x2 * v, P) === u, value: x };
|
||||||
},
|
},
|
||||||
// ECDH
|
} as const;
|
||||||
// basePointU:
|
|
||||||
// '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
export const ed448 = twistedEdwards(ED448_DEF);
|
||||||
// The constant a24 is (156326 - 2) / 4 = 39081 for curve448/X448.
|
// 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'),
|
a24: BigInt('39081'),
|
||||||
montgomeryBits: 448,
|
montgomeryBits: 448,
|
||||||
|
nByteLength: 57,
|
||||||
|
P: ed448P,
|
||||||
|
Gu: '0500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
|
||||||
powPminus2: (x: bigint): bigint => {
|
powPminus2: (x: bigint): bigint => {
|
||||||
const P = ed448P;
|
const P = ed448P;
|
||||||
const Pminus3div4 = ed448_pow_Pminus3div4(x);
|
const Pminus3div4 = ed448_pow_Pminus3div4(x);
|
||||||
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
|
const Pminus3 = pow2(Pminus3div4, BigInt(2), P);
|
||||||
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
|
return mod(Pminus3 * x, P); // Pminus3 * x = Pminus2
|
||||||
},
|
},
|
||||||
|
adjustScalarBytes,
|
||||||
// The 4-isogeny maps between the Montgomery curve and this Edwards
|
// The 4-isogeny maps between the Montgomery curve and this Edwards
|
||||||
// curve are:
|
// curve are:
|
||||||
// (u, v) = (y^2/x^2, (2 - x^2 - y^2)*y/x^3)
|
// (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),
|
// (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^3 - 4*u*v^2 + u)/
|
||||||
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
|
// (u^5 - 2*u^2*v^2 - 2*u^3 - 2*v^2 + u))
|
||||||
UfromPoint: (p: PointType) => {
|
// xyToU: (p: PointType) => {
|
||||||
const P = ed448P;
|
// const P = ed448P;
|
||||||
const { x, y } = p;
|
// const { x, y } = p;
|
||||||
if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
// if (x === _0n) throw new Error(`Point with x=0 doesn't have mapping`);
|
||||||
const invX = invert(x * x, P); // x^2
|
// const invX = invert(x * x, P); // x^2
|
||||||
const u = mod(y * y * invX, P); // (y^2/x^2)
|
// const u = mod(y * y * invX, P); // (y^2/x^2)
|
||||||
return numberToBytesLE(u, 56);
|
// 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 });
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
import { should } from 'micro-should';
|
import { should } from 'micro-should';
|
||||||
import * as fc from 'fast-check';
|
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 { readFileSync } from 'fs';
|
||||||
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
|
||||||
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||||
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
|
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 { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
|
||||||
import { sha512 } from '@noble/hashes/sha512';
|
|
||||||
|
|
||||||
const ed = ed25519;
|
const ed = ed25519;
|
||||||
const hex = bytesToHex;
|
const hex = bytesToHex;
|
||||||
@ -455,7 +454,7 @@ const rfc7748Mul = [
|
|||||||
for (let i = 0; i < rfc7748Mul.length; i++) {
|
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||||
const v = rfc7748Mul[i];
|
const v = rfc7748Mul[i];
|
||||||
should(`RFC7748: scalarMult (${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++) {
|
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||||
const { scalar, iters } = rfc7748Iter[i];
|
const { scalar, iters } = rfc7748Iter[i];
|
||||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
||||||
let k = ed.montgomeryCurve.BASE_POINT_U;
|
let k = x25519.Gu;
|
||||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
|
for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(u, k), k];
|
||||||
deepStrictEqual(hex(k), scalar);
|
deepStrictEqual(hex(k), scalar);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -479,33 +478,33 @@ should('RFC7748 getSharedKey', () => {
|
|||||||
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
|
const bobPrivate = '5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb';
|
||||||
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
|
const bobPublic = 'de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f';
|
||||||
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
|
const shared = '4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742';
|
||||||
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
|
deepStrictEqual(alicePublic, hex(x25519.getPublicKey(alicePrivate)));
|
||||||
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
|
deepStrictEqual(bobPublic, hex(x25519.getPublicKey(bobPrivate)));
|
||||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
|
deepStrictEqual(hex(x25519.scalarMult(bobPublic, alicePrivate)), shared);
|
||||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
|
deepStrictEqual(hex(x25519.scalarMult(alicePublic, bobPrivate)), shared);
|
||||||
});
|
});
|
||||||
|
|
||||||
should('X25519/getSharedSecret() should be commutative', () => {
|
// should('X25519/getSharedSecret() should be commutative', () => {
|
||||||
for (let i = 0; i < 512; i++) {
|
// for (let i = 0; i < 512; i++) {
|
||||||
const asec = ed.utils.randomPrivateKey();
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
const apub = ed.getPublicKey(asec);
|
// const apub = ed.getPublicKey(asec);
|
||||||
const bsec = ed.utils.randomPrivateKey();
|
// const bsec = ed.utils.randomPrivateKey();
|
||||||
const bpub = ed.getPublicKey(bsec);
|
// const bpub = ed.getPublicKey(bsec);
|
||||||
try {
|
// try {
|
||||||
deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('not commutative', { asec, apub, bsec, bpub });
|
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
throw error;
|
// throw error;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
should('X25519: should convert base point to montgomery using fromPoint', () => {
|
// should('X25519: should convert base point to montgomery using fromPoint', () => {
|
||||||
deepStrictEqual(
|
// deepStrictEqual(
|
||||||
hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||||
ed.montgomeryCurve.BASE_POINT_U
|
// ed.montgomeryCurve.BASE_POINT_U
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
|
|
||||||
{
|
{
|
||||||
const group = x25519vectors.testGroups[0];
|
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}`, () => {
|
should(`Wycheproof/X25519(${i}, ${v.result}) ${v.comment}`, () => {
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||||
try {
|
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');
|
deepStrictEqual(shared, v.shared, 'valid');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// We are more strict
|
// We are more strict
|
||||||
@ -525,7 +524,7 @@ should('X25519: should convert base point to montgomery using fromPoint', () =>
|
|||||||
} else if (v.result === 'invalid') {
|
} else if (v.result === 'invalid') {
|
||||||
let failed = false;
|
let failed = false;
|
||||||
try {
|
try {
|
||||||
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
|
x25519.scalarMult(v.public, v.private);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { deepStrictEqual, throws } from 'assert';
|
import { deepStrictEqual, throws } from 'assert';
|
||||||
import { should } from 'micro-should';
|
import { should } from 'micro-should';
|
||||||
import * as fc from 'fast-check';
|
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 { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
|
||||||
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' };
|
||||||
import { default as x448vectors } from './wycheproof/x448_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++) {
|
for (let i = 0; i < rfc7748Mul.length; i++) {
|
||||||
const v = rfc7748Mul[i];
|
const v = rfc7748Mul[i];
|
||||||
should(`RFC7748: scalarMult (${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++) {
|
for (let i = 0; i < rfc7748Iter.length; i++) {
|
||||||
const { scalar, iters } = rfc7748Iter[i];
|
const { scalar, iters } = rfc7748Iter[i];
|
||||||
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
should(`RFC7748: scalarMult iteration (${i})`, () => {
|
||||||
let k = ed.montgomeryCurve.BASE_POINT_U;
|
let k = x448.Gu;
|
||||||
for (let i = 0, u = k; i < iters; i++) [k, u] = [ed.montgomeryCurve.scalarMult(u, k), k];
|
for (let i = 0, u = k; i < iters; i++) [k, u] = [x448.scalarMult(u, k), k];
|
||||||
deepStrictEqual(hex(k), scalar);
|
deepStrictEqual(hex(k), scalar);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -515,10 +515,10 @@ should('RFC7748 getSharedKey', () => {
|
|||||||
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
|
'3eb7a829b0cd20f5bcfc0b599b6feccf6da4627107bdb0d4f345b43027d8b972fc3e34fb4232a13ca706dcb57aec3dae07bdc1c67bf33609';
|
||||||
const shared =
|
const shared =
|
||||||
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
|
'07fff4181ac6cc95ec1c16a94a0f74d12da232ce40a77552281d282bb60c0b56fd2464c335543936521c24403085d59a449a5037514a879d';
|
||||||
deepStrictEqual(alicePublic, hex(ed.montgomeryCurve.getPublicKey(alicePrivate)));
|
deepStrictEqual(alicePublic, hex(x448.getPublicKey(alicePrivate)));
|
||||||
deepStrictEqual(bobPublic, hex(ed.montgomeryCurve.getPublicKey(bobPrivate)));
|
deepStrictEqual(bobPublic, hex(x448.getPublicKey(bobPrivate)));
|
||||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(alicePrivate, bobPublic)), shared);
|
deepStrictEqual(hex(x448.scalarMult(bobPublic, alicePrivate)), shared);
|
||||||
deepStrictEqual(hex(ed.montgomeryCurve.getSharedSecret(bobPrivate, alicePublic)), shared);
|
deepStrictEqual(hex(x448.scalarMult(alicePublic, bobPrivate)), shared);
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -528,7 +528,7 @@ should('RFC7748 getSharedKey', () => {
|
|||||||
should(`Wycheproof/X448(${i}, ${v.result}) ${v.comment}`, () => {
|
should(`Wycheproof/X448(${i}, ${v.result}) ${v.comment}`, () => {
|
||||||
if (v.result === 'valid' || v.result === 'acceptable') {
|
if (v.result === 'valid' || v.result === 'acceptable') {
|
||||||
try {
|
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');
|
deepStrictEqual(shared, v.shared, 'valid');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// We are more strict
|
// We are more strict
|
||||||
@ -540,7 +540,7 @@ should('RFC7748 getSharedKey', () => {
|
|||||||
} else if (v.result === 'invalid') {
|
} else if (v.result === 'invalid') {
|
||||||
let failed = false;
|
let failed = false;
|
||||||
try {
|
try {
|
||||||
ed.montgomeryCurve.getSharedSecret(v.private, v.public);
|
x448.scalarMult(v.public, v.private);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
}
|
||||||
@ -550,27 +550,27 @@ should('RFC7748 getSharedKey', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
should('X448: should convert base point to montgomery using fromPoint', () => {
|
// should('X448: should convert base point to montgomery using fromPoint', () => {
|
||||||
deepStrictEqual(
|
// deepStrictEqual(
|
||||||
hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
// hex(ed.montgomeryCurve.UfromPoint(ed.Point.BASE)),
|
||||||
ed.montgomeryCurve.BASE_POINT_U
|
// ed.montgomeryCurve.BASE_POINT_U
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
|
|
||||||
should('X448/getSharedSecret() should be commutative', async () => {
|
// should('X448/getSharedSecret() should be commutative', async () => {
|
||||||
for (let i = 0; i < 512; i++) {
|
// for (let i = 0; i < 512; i++) {
|
||||||
const asec = ed.utils.randomPrivateKey();
|
// const asec = ed.utils.randomPrivateKey();
|
||||||
const apub = ed.getPublicKey(asec);
|
// const apub = ed.getPublicKey(asec);
|
||||||
const bsec = ed.utils.randomPrivateKey();
|
// const bsec = ed.utils.randomPrivateKey();
|
||||||
const bpub = ed.getPublicKey(bsec);
|
// const bpub = ed.getPublicKey(bsec);
|
||||||
try {
|
// try {
|
||||||
deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
// deepStrictEqual(ed.getSharedSecret(asec, bpub), ed.getSharedSecret(bsec, apub));
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
console.error('not commutative', { asec, apub, bsec, bpub });
|
// console.error('not commutative', { asec, apub, bsec, bpub });
|
||||||
throw error;
|
// throw error;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
const VECTORS_RFC8032_CTX = [
|
const VECTORS_RFC8032_CTX = [
|
||||||
{
|
{
|
||||||
|
15
package.json
15
package.json
@ -32,21 +32,26 @@
|
|||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
"./edwards": {
|
||||||
|
"types": "./lib/edwards.d.ts",
|
||||||
|
"import": "./lib/esm/edwards.js",
|
||||||
|
"default": "./lib/edwards.js"
|
||||||
|
},
|
||||||
"./modular": {
|
"./modular": {
|
||||||
"types": "./lib/modular.d.ts",
|
"types": "./lib/modular.d.ts",
|
||||||
"import": "./lib/esm/modular.js",
|
"import": "./lib/esm/modular.js",
|
||||||
"default": "./lib/modular.js"
|
"default": "./lib/modular.js"
|
||||||
},
|
},
|
||||||
|
"./montgomery": {
|
||||||
|
"types": "./lib/montgomery.d.ts",
|
||||||
|
"import": "./lib/esm/montgomery.js",
|
||||||
|
"default": "./lib/montgomery.js"
|
||||||
|
},
|
||||||
"./weierstrass": {
|
"./weierstrass": {
|
||||||
"types": "./lib/weierstrass.d.ts",
|
"types": "./lib/weierstrass.d.ts",
|
||||||
"import": "./lib/esm/weierstrass.js",
|
"import": "./lib/esm/weierstrass.js",
|
||||||
"default": "./lib/weierstrass.js"
|
"default": "./lib/weierstrass.js"
|
||||||
},
|
},
|
||||||
"./edwards": {
|
|
||||||
"types": "./lib/edwards.d.ts",
|
|
||||||
"import": "./lib/esm/edwards.js",
|
|
||||||
"default": "./lib/edwards.js"
|
|
||||||
},
|
|
||||||
"./utils": {
|
"./utils": {
|
||||||
"types": "./lib/utils.d.ts",
|
"types": "./lib/utils.d.ts",
|
||||||
"import": "./lib/esm/utils.js",
|
"import": "./lib/esm/utils.js",
|
||||||
|
230
src/edwards.ts
230
src/edwards.ts
@ -16,6 +16,7 @@ import {
|
|||||||
concatBytes,
|
concatBytes,
|
||||||
ensureBytes,
|
ensureBytes,
|
||||||
numberToBytesLE,
|
numberToBytesLE,
|
||||||
|
bytesToNumberLE,
|
||||||
nLength,
|
nLength,
|
||||||
hashToPrivateScalar,
|
hashToPrivateScalar,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
@ -56,14 +57,6 @@ export type CurveType = {
|
|||||||
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array;
|
||||||
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
|
uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint };
|
||||||
preHash?: CHash;
|
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
|
// We accept hex strings besides Uint8Array for simplicity
|
||||||
@ -75,11 +68,11 @@ type PrivKey = Hex | bigint | number;
|
|||||||
function validateOpts(curve: CurveType) {
|
function validateOpts(curve: CurveType) {
|
||||||
if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen))
|
if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen))
|
||||||
throw new Error('Invalid hash function');
|
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')
|
if (typeof curve[i] !== 'bigint')
|
||||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`);
|
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 (curve[i] === undefined) continue; // Optional
|
||||||
if (!Number.isSafeInteger(curve[i]))
|
if (!Number.isSafeInteger(curve[i]))
|
||||||
throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof 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) {
|
for (const fn of ['randomBytes'] as const) {
|
||||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
||||||
}
|
}
|
||||||
for (const fn of [
|
for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio'] as const) {
|
||||||
'adjustScalarBytes',
|
|
||||||
'domain',
|
|
||||||
'uvRatio',
|
|
||||||
'powPminus2',
|
|
||||||
'UfromPoint',
|
|
||||||
] as const) {
|
|
||||||
if (curve[fn] === undefined) continue; // Optional
|
if (curve[fn] === undefined) continue; // Optional
|
||||||
if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`);
|
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
|
// Set defaults
|
||||||
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const);
|
||||||
}
|
}
|
||||||
@ -176,20 +158,11 @@ export type SigType = Hex | SignatureType;
|
|||||||
export type CurveFn = {
|
export type CurveFn = {
|
||||||
CURVE: ReturnType<typeof validateOpts>;
|
CURVE: ReturnType<typeof validateOpts>;
|
||||||
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
|
getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array;
|
||||||
getSharedSecret: (privateKey: PrivKey, publicKey: Hex) => Uint8Array;
|
|
||||||
sign: (message: Hex, privateKey: Hex) => Uint8Array;
|
sign: (message: Hex, privateKey: Hex) => Uint8Array;
|
||||||
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean;
|
verify: (sig: SigType, message: Hex, publicKey: PubKey) => boolean;
|
||||||
Point: PointConstructor;
|
Point: PointConstructor;
|
||||||
ExtendedPoint: ExtendedPointConstructor;
|
ExtendedPoint: ExtendedPointConstructor;
|
||||||
Signature: SignatureConstructor;
|
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: {
|
utils: {
|
||||||
mod: (a: bigint, b?: bigint) => bigint;
|
mod: (a: bigint, b?: bigint) => bigint;
|
||||||
invert: (number: bigint, modulo?: bigint) => bigint;
|
invert: (number: bigint, modulo?: bigint) => bigint;
|
||||||
@ -229,8 +202,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
}
|
}
|
||||||
const uvRatio = CURVE.uvRatio || _uvRatio;
|
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 = (bytes: Uint8Array) => bytes; // NOOP
|
||||||
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes;
|
const adjustScalarBytes = CURVE.adjustScalarBytes || _adjustScalarBytes;
|
||||||
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
|
function _domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
|
||||||
@ -553,10 +524,10 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Little Endian
|
// Little Endian
|
||||||
function bytesToNumberLE(uint8a: Uint8Array): bigint {
|
// function bytesToNumberLE(uint8a: Uint8Array): bigint {
|
||||||
if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
// if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||||
return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
|
// return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse()));
|
||||||
}
|
// }
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// Little-endian SHA512 with modulo n
|
// 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.
|
// Enable precomputes. Slows down first publicKey computation by 20ms.
|
||||||
Point.BASE._setWindowSize(8);
|
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 = {
|
const utils = {
|
||||||
getExtendedPublicKey,
|
getExtendedPublicKey,
|
||||||
mod: modP,
|
mod: modP,
|
||||||
invert: (a: bigint, m = CURVE.P) => mod.invert(a, m),
|
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).
|
* 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),
|
hashToPrivateScalar: (hash: Hex): bigint => hashToPrivateScalar(hash, CURVE_ORDER, true),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ed25519 private keys are uniform 32-bit strings. We do not need to check for
|
* 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),
|
randomPrivateKey: (): Uint8Array => randomBytes(fieldLen),
|
||||||
|
|
||||||
@ -891,14 +687,12 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
CURVE,
|
CURVE,
|
||||||
montgomeryCurve,
|
getPublicKey,
|
||||||
getSharedSecret,
|
sign,
|
||||||
|
verify,
|
||||||
ExtendedPoint,
|
ExtendedPoint,
|
||||||
Point,
|
Point,
|
||||||
Signature,
|
Signature,
|
||||||
getPublicKey,
|
|
||||||
utils,
|
utils,
|
||||||
sign,
|
|
||||||
verify,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
201
src/montgomery.ts
Normal file
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,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user