From 88291eba33c70dd67b347ab18b0bd0a74cae3eeb Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Thu, 20 Apr 2023 13:37:21 +0200 Subject: [PATCH] ed25519: fix edwardsToMontgomery formula; implement edwardsToMontgomeryPriv; add tests --- src/ed25519.ts | 39 +++++++++++++++++----- test/ed25519-addons.test.js | 65 +++++++++++++++++++++++++++++++++++-- test/index.test.js | 1 + 3 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/ed25519.ts b/src/ed25519.ts index 2f881fc..a7dce4d 100644 --- a/src/ed25519.ts +++ b/src/ed25519.ts @@ -1,16 +1,16 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ import { sha512 } from '@noble/hashes/sha512'; 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 { mod, pow2, isNegativeLE, Field, FpSqrtEven } from './abstract/modular.js'; +import { Field, FpSqrtEven, isNegativeLE, mod, pow2 } from './abstract/modular.js'; import { - equalBytes, bytesToHex, bytesToNumberLE, - numberToBytesLE, - Hex, ensureBytes, + equalBytes, + Hex, + numberToBytesLE, } from './abstract/utils.js'; import * as htf from './abstract/hash-to-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); // prettier-ignore const _10n = BigInt(10), _20n = BigInt(20), _40n = BigInt(40), _80n = BigInt(80); + function ed25519_pow_2_252_3(x: bigint) { const P = ED25519_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. 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 @@ -61,6 +63,7 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array { bytes[31] |= 64; // 0b0100_0000 return bytes; } + // sqrt(u/v) function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } { const P = ED25519_P; @@ -121,6 +124,7 @@ const ed25519Defaults = { } as const; export const ed25519 = twistedEdwards(ed25519Defaults); + function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { if (ctx.length > 255) throw new Error('Context is too big'); return concatBytes( @@ -130,6 +134,7 @@ function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { data ); } + export const ed25519ctx = twistedEdwards({ ...ed25519Defaults, domain: ed25519_domain }); export const ed25519ph = twistedEdwards({ ...ed25519Defaults, @@ -158,13 +163,26 @@ export const x25519 = montgomery({ * * `(u, v) = ((1+y)/(1-y), sqrt(-486664)*u/x)` * * `(x, y) = (sqrt(-486664)*u/v, (u-1)/(u+1))` * @example - * const aPub = ed25519.getPublicKey(utils.randomPrivateKey()); - * x25519.getSharedSecret(edwardsToMontgomery(aPub), edwardsToMontgomery(someonesPub)) + * const someonesPub = ed25519.getPublicKey(ed25519.utils.randomPrivateKey()); + * const aPriv = x25519.utils.randomPrivateKey(); + * x25519.getSharedSecret(aPriv, edwardsToMontgomery(someonesPub)) */ export function edwardsToMontgomery(edwardsPub: Hex): Uint8Array { const { y } = ed25519.ExtendedPoint.fromHex(edwardsPub); 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) @@ -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 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 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 @@ -239,6 +258,7 @@ function map_to_curve_elligator2_edwards25519(u: bigint) { 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) } + const { hashToCurve, encodeToCurve } = htf.createHasher( ed25519.ExtendedPoint, (scalars: bigint[]) => map_to_curve_elligator2_edwards25519(scalars[0]), @@ -257,6 +277,7 @@ export { hashToCurve, encodeToCurve }; function assertRstPoint(other: unknown) { if (!(other instanceof RistrettoPoint)) throw new Error('RistrettoPoint expected'); } + // √(-1) aka √(a) aka 2^((p-1)/4) const SQRT_M1 = BigInt( '19681161376707505956807079304988542015446066515923890162744021073123829784752' diff --git a/test/ed25519-addons.test.js b/test/ed25519-addons.test.js index 63d7788..3c8e052 100644 --- a/test/ed25519-addons.test.js +++ b/test/ed25519-addons.test.js @@ -1,10 +1,18 @@ 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 { describe, should } from 'micro-should'; import { bytesToNumberLE, numberToBytesLE } from '../esm/abstract/utils.js'; 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 = [ { @@ -141,6 +149,57 @@ describe('RFC7748 X25519 ECDH', () => { 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', () => { const { y } = ed25519ph.ExtendedPoint.BASE; const { Fp } = ed25519ph.CURVE; @@ -298,7 +357,7 @@ describe('ristretto255', () => { // ESM is broken. import url from 'url'; -import { assert } from 'console'; + if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { should.run(); } diff --git a/test/index.test.js b/test/index.test.js index 792c8a2..e94b151 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -5,6 +5,7 @@ import './basic.test.js'; import './nist.test.js'; import './ed448.test.js'; import './ed25519.test.js'; +import './ed25519-addons.test.js'; import './secp256k1.test.js'; import './secp256k1-schnorr.test.js'; import './jubjub.test.js';