ed25519: fix edwardsToMontgomery formula; implement edwardsToMontgomeryPriv; add tests

This commit is contained in:
Mircea Nistor 2023-04-20 13:37:21 +02:00
parent 848a1b0226
commit 88291eba33
No known key found for this signature in database
GPG Key ID: A611674B46EE8605
3 changed files with 93 additions and 12 deletions

@ -1,16 +1,16 @@
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
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, ExtPointType } from './abstract/edwards.js'; import { ExtPointType, twistedEdwards } from './abstract/edwards.js';
import { montgomery } from './abstract/montgomery.js'; import { montgomery } from './abstract/montgomery.js';
import { mod, pow2, isNegativeLE, Field, FpSqrtEven } from './abstract/modular.js'; import { Field, FpSqrtEven, isNegativeLE, mod, pow2 } from './abstract/modular.js';
import { import {
equalBytes,
bytesToHex, bytesToHex,
bytesToNumberLE, bytesToNumberLE,
numberToBytesLE,
Hex,
ensureBytes, ensureBytes,
equalBytes,
Hex,
numberToBytesLE,
} from './abstract/utils.js'; } from './abstract/utils.js';
import * as htf from './abstract/hash-to-curve.js'; import * as htf from './abstract/hash-to-curve.js';
import { AffinePoint } from './abstract/curve.js'; import { AffinePoint } from './abstract/curve.js';
@ -34,6 +34,7 @@ const ED25519_SQRT_M1 = BigInt(
const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _5n = BigInt(5); const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _5n = BigInt(5);
// prettier-ignore // prettier-ignore
const _10n = BigInt(10), _20n = BigInt(20), _40n = BigInt(40), _80n = BigInt(80); const _10n = BigInt(10), _20n = BigInt(20), _40n = BigInt(40), _80n = BigInt(80);
function ed25519_pow_2_252_3(x: bigint) { function ed25519_pow_2_252_3(x: bigint) {
const P = ED25519_P; const P = ED25519_P;
const x2 = (x * x) % P; const x2 = (x * x) % P;
@ -51,6 +52,7 @@ 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 { function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
// Section 5: For X25519, in order to decode 32 random bytes as an integer scalar, // 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 // set the three least significant bits of the first byte
@ -61,6 +63,7 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
bytes[31] |= 64; // 0b0100_0000 bytes[31] |= 64; // 0b0100_0000
return bytes; return bytes;
} }
// sqrt(u/v) // sqrt(u/v)
function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } { function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } {
const P = ED25519_P; const P = ED25519_P;
@ -121,6 +124,7 @@ const ed25519Defaults = {
} as const; } as const;
export const ed25519 = twistedEdwards(ed25519Defaults); export const ed25519 = twistedEdwards(ed25519Defaults);
function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
if (ctx.length > 255) throw new Error('Context is too big'); if (ctx.length > 255) throw new Error('Context is too big');
return concatBytes( return concatBytes(
@ -130,6 +134,7 @@ function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) {
data data
); );
} }
export const ed25519ctx = twistedEdwards({ ...ed25519Defaults, domain: ed25519_domain }); export const ed25519ctx = twistedEdwards({ ...ed25519Defaults, domain: ed25519_domain });
export const ed25519ph = twistedEdwards({ export const ed25519ph = twistedEdwards({
...ed25519Defaults, ...ed25519Defaults,
@ -158,13 +163,26 @@ export const x25519 = montgomery({
* * `(u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)` * * `(u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)`
* * `(x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))` * * `(x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))`
* @example * @example
* const aPub = ed25519.getPublicKey(utils.randomPrivateKey()); * const someonesPub = ed25519.getPublicKey(ed25519.utils.randomPrivateKey());
* x25519.getSharedSecret(edwardsToMontgomery(aPub), edwardsToMontgomery(someonesPub)) * const aPriv = x25519.utils.randomPrivateKey();
* x25519.getSharedSecret(aPriv, edwardsToMontgomery(someonesPub))
*/ */
export function edwardsToMontgomery(edwardsPub: Hex): Uint8Array { export function edwardsToMontgomery(edwardsPub: Hex): Uint8Array {
const { y } = ed25519.ExtendedPoint.fromHex(edwardsPub); const { y } = ed25519.ExtendedPoint.fromHex(edwardsPub);
const _1n = BigInt(1); const _1n = BigInt(1);
return Fp.toBytes(Fp.create((y - _1n) * Fp.inv(y + _1n))); return Fp.toBytes(Fp.create((_1n + y) * Fp.inv(_1n - y)));
}
/**
* Converts ed25519 secret key to x25519 secret key.
* @example
* const someonesPub = x25519.getPublicKey(x25519.utils.randomPrivateKey());
* const aPriv = ed25519.utils.randomPrivateKey();
* x25519.getSharedSecret(edwardsToMontgomeryPriv(aPriv), someonesPub)
*/
export function edwardsToMontgomeryPriv(edwardsPriv: Uint8Array): Uint8Array {
const hashed = ed25519Defaults.hash(edwardsPriv.subarray(0, 32));
return ed25519Defaults.adjustScalarBytes(hashed).subarray(0, 32);
} }
// Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator) // Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator)
@ -223,7 +241,8 @@ function map_to_curve_elligator2_curve25519(u: bigint) {
const ELL2_C1_EDWARDS = FpSqrtEven(Fp, Fp.neg(BigInt(486664))); // sgn0(c1) MUST equal 0 const ELL2_C1_EDWARDS = FpSqrtEven(Fp, Fp.neg(BigInt(486664))); // sgn0(c1) MUST equal 0
function map_to_curve_elligator2_edwards25519(u: bigint) { function map_to_curve_elligator2_edwards25519(u: bigint) {
const { xMn, xMd, yMn, yMd } = map_to_curve_elligator2_curve25519(u); // 1. (xMn, xMd, yMn, yMd) = map_to_curve_elligator2_curve25519(u) const { xMn, xMd, yMn, yMd } = map_to_curve_elligator2_curve25519(u); // 1. (xMn, xMd, yMn, yMd) =
// map_to_curve_elligator2_curve25519(u)
let xn = Fp.mul(xMn, yMd); // 2. xn = xMn * yMd let xn = Fp.mul(xMn, yMd); // 2. xn = xMn * yMd
xn = Fp.mul(xn, ELL2_C1_EDWARDS); // 3. xn = xn * c1 xn = Fp.mul(xn, ELL2_C1_EDWARDS); // 3. xn = xn * c1
let xd = Fp.mul(xMd, yMn); // 4. xd = xMd * yMn # xn / xd = c1 * xM / yM let xd = Fp.mul(xMd, yMn); // 4. xd = xMd * yMn # xn / xd = c1 * xM / yM
@ -239,6 +258,7 @@ function map_to_curve_elligator2_edwards25519(u: bigint) {
const inv = Fp.invertBatch([xd, yd]); // batch division const inv = Fp.invertBatch([xd, yd]); // batch division
return { x: Fp.mul(xn, inv[0]), y: Fp.mul(yn, inv[1]) }; // 13. return (xn, xd, yn, yd) return { x: Fp.mul(xn, inv[0]), y: Fp.mul(yn, inv[1]) }; // 13. return (xn, xd, yn, yd)
} }
const { hashToCurve, encodeToCurve } = htf.createHasher( const { hashToCurve, encodeToCurve } = htf.createHasher(
ed25519.ExtendedPoint, ed25519.ExtendedPoint,
(scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]), (scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]),
@ -257,6 +277,7 @@ export { hashToCurve, encodeToCurve };
function assertRstPoint(other: unknown) { function assertRstPoint(other: unknown) {
if (!(other instanceof RistrettoPoint)) throw new Error('RistrettoPoint expected'); if (!(other instanceof RistrettoPoint)) throw new Error('RistrettoPoint expected');
} }
// √(-1) aka √(a) aka 2^((p-1)/4) // √(-1) aka √(a) aka 2^((p-1)/4)
const SQRT_M1 = BigInt( const SQRT_M1 = BigInt(
'19681161376707505956807079304988542015446066515923890162744021073123829784752' '19681161376707505956807079304988542015446066515923890162744021073123829784752'

@ -1,10 +1,18 @@
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { hexToBytes, bytesToHex as hex } from '@noble/hashes/utils'; import { bytesToHex as hex, hexToBytes } from '@noble/hashes/utils';
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { describe, should } from 'micro-should'; import { describe, should } from 'micro-should';
import { bytesToNumberLE, numberToBytesLE } from '../esm/abstract/utils.js'; import { bytesToNumberLE, numberToBytesLE } from '../esm/abstract/utils.js';
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 { ed25519ctx, ed25519ph, RistrettoPoint, x25519 } from '../esm/ed25519.js'; import {
ed25519,
ed25519ctx,
ed25519ph,
edwardsToMontgomery,
edwardsToMontgomeryPriv,
RistrettoPoint,
x25519,
} from '../esm/ed25519.js';
const VECTORS_RFC8032_CTX = [ const VECTORS_RFC8032_CTX = [
{ {
@ -141,6 +149,57 @@ describe('RFC7748 X25519 ECDH', () => {
deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared); deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared);
}); });
should('X25519/getSharedSecret() should be commutative', () => {
for (let i = 0; i < 512; i++) {
const asec = x25519.utils.randomPrivateKey();
const apub = x25519.getPublicKey(asec);
const bsec = x25519.utils.randomPrivateKey();
const bpub = x25519.getPublicKey(bsec);
try {
deepStrictEqual(x25519.getSharedSecret(asec, bpub), x25519.getSharedSecret(bsec, apub));
} catch (error) {
console.error('not commutative', { asec, apub, bsec, bpub });
throw error;
}
}
});
should('edwardsToMontgomery should produce correct output', () => {
const edSecret = hexToBytes('77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a');
const edPublic = ed25519.getPublicKey(edSecret);
const xPrivate = edwardsToMontgomeryPriv(edSecret);
deepStrictEqual(
hex(xPrivate),
'a8cd44eb8e93319c0570bc11005c0e0189d34ff02f6c17773411ad191293c94f'
);
const xPublic = edwardsToMontgomery(edPublic);
deepStrictEqual(
hex(xPublic),
'ed7749b4d989f6957f3bfde6c56767e988e21c9f8784d91d610011cd553f9b06'
);
});
should('edwardsToMontgomery should produce correct keyPair', () => {
const edSecret = ed25519.utils.randomPrivateKey();
const edPublic = ed25519.getPublicKey(edSecret);
const hashed = ed25519.CURVE.hash(edSecret.subarray(0, 32));
const xSecret = ed25519.CURVE.adjustScalarBytes(hashed.subarray(0, 32));
const expectedXPublic = x25519.getPublicKey(xSecret);
const xPublic = edwardsToMontgomery(edPublic);
deepStrictEqual(xPublic, expectedXPublic);
});
should('ECDH through edwardsToMontgomery should be commutative', () => {
const edSecret1 = ed25519.utils.randomPrivateKey();
const edPublic1 = ed25519.getPublicKey(edSecret1);
const edSecret2 = ed25519.utils.randomPrivateKey();
const edPublic2 = ed25519.getPublicKey(edSecret2);
deepStrictEqual(
x25519.getSharedSecret(edwardsToMontgomeryPriv(edSecret1), edwardsToMontgomery(edPublic2)),
x25519.getSharedSecret(edwardsToMontgomeryPriv(edSecret2), edwardsToMontgomery(edPublic1))
);
});
should('base point', () => { should('base point', () => {
const { y } = ed25519ph.ExtendedPoint.BASE; const { y } = ed25519ph.ExtendedPoint.BASE;
const { Fp } = ed25519ph.CURVE; const { Fp } = ed25519ph.CURVE;
@ -298,7 +357,7 @@ describe('ristretto255', () => {
// ESM is broken. // ESM is broken.
import url from 'url'; import url from 'url';
import { assert } from 'console';
if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { if (import.meta.url === url.pathToFileURL(process.argv[1]).href) {
should.run(); should.run();
} }

@ -5,6 +5,7 @@ import './basic.test.js';
import './nist.test.js'; import './nist.test.js';
import './ed448.test.js'; import './ed448.test.js';
import './ed25519.test.js'; import './ed25519.test.js';
import './ed25519-addons.test.js';
import './secp256k1.test.js'; import './secp256k1.test.js';
import './secp256k1-schnorr.test.js'; import './secp256k1-schnorr.test.js';
import './jubjub.test.js'; import './jubjub.test.js';