From 113d906233cd3a1a4f70ca2c37970c6dd5cacda0 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Sun, 2 Apr 2023 15:35:03 +0000 Subject: [PATCH] edwards: change API. Add options.strict, context. Add edwardsToMontgomery --- src/abstract/edwards.ts | 25 +++++++++----- src/ed25519.ts | 24 ++++++++++--- src/ed448.ts | 30 ++++++++-------- test/ed25519-addons.test.js | 33 ++++++++++-------- test/ed448.test.js | 69 ++++++++++++++++++++----------------- 5 files changed, 108 insertions(+), 73 deletions(-) diff --git a/src/abstract/edwards.ts b/src/abstract/edwards.ts index d030224..4feace6 100644 --- a/src/abstract/edwards.ts +++ b/src/abstract/edwards.ts @@ -18,7 +18,7 @@ export type CurveType = BasicCurve & { adjustScalarBytes?: (bytes: Uint8Array) => Uint8Array; // clears bits to get valid field elemtn domain?: (data: Uint8Array, ctx: Uint8Array, phflag: boolean) => Uint8Array; // Used for hashing uvRatio?: (u: bigint, v: bigint) => { isValid: boolean; value: bigint }; // Ratio √(u/v) - preHash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify() + prehash?: FHash; // RFC 8032 pre-hashing of messages to sign() / verify() mapToCurve?: (scalar: bigint[]) => AffinePoint; // for hash-to-curve standard }; @@ -90,7 +90,15 @@ export type CurveFn = { // It is not generic twisted curve for now, but ed25519/ed448 generic implementation export function twistedEdwards(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef) as ReturnType; - const { Fp, n: CURVE_ORDER, preHash, hash: cHash, randomBytes, nByteLength, h: cofactor } = CURVE; + const { + Fp, + n: CURVE_ORDER, + prehash: preHash, + hash: cHash, + randomBytes, + nByteLength, + h: cofactor, + } = CURVE; const MASK = _2n ** BigInt(nByteLength * 8); const modP = Fp.create; // Function overrides @@ -423,29 +431,30 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } /** Signs message with privateKey. RFC8032 5.1.6 */ - function sign(msg: Hex, privKey: Hex, context?: Hex): Uint8Array { + function sign(msg: Hex, privKey: Hex, options: { context?: Hex } = {}): Uint8Array { msg = ensureBytes('message', msg); if (preHash) msg = preHash(msg); // for ed25519ph etc. const { prefix, scalar, pointBytes } = getExtendedPublicKey(privKey); - const r = hashDomainToScalar(context, prefix, msg); // r = dom2(F, C) || prefix || PH(M) + const r = hashDomainToScalar(options.context, prefix, msg); // r = dom2(F, C) || prefix || PH(M) const R = G.multiply(r).toRawBytes(); // R = rG - const k = hashDomainToScalar(context, R, pointBytes, msg); // R || A || PH(M) + const k = hashDomainToScalar(options.context, R, pointBytes, msg); // R || A || PH(M) const s = modN(r + k * scalar); // S = (r + k * s) mod L assertGE0(s); // 0 <= s < l const res = ut.concatBytes(R, ut.numberToBytesLE(s, Fp.BYTES)); return ensureBytes('result', res, nByteLength * 2); // 64-byte signature } - function verify(sig: Hex, msg: Hex, publicKey: Hex, context?: Hex): boolean { + const verifyOpts: { context?: Hex; strict?: boolean } = { strict: false }; + function verify(sig: Hex, msg: Hex, publicKey: Hex, options = verifyOpts): boolean { const len = Fp.BYTES; // Verifies EdDSA signature against message and public key. RFC8032 5.1.7. sig = ensureBytes('signature', sig, 2 * len); // An extended group equation is checked. msg = ensureBytes('message', msg); // ZIP215 compliant, which means not fully RFC8032 compliant. if (preHash) msg = preHash(msg); // for ed25519ph, etc const A = Point.fromHex(publicKey, false); // Check for s bounds, hex validity - const R = Point.fromHex(sig.slice(0, len), false); // 0 <= R < 2^256: ZIP215 R can be >= P + const R = Point.fromHex(sig.slice(0, len), options.strict); // R

255) throw new Error('Context is too big'); return concatBytes( @@ -130,11 +130,11 @@ function ed25519_domain(data: Uint8Array, ctx: Uint8Array, phflag: boolean) { data ); } -export const ed25519ctx = twistedEdwards({ ...ED25519_DEF, domain: ed25519_domain }); +export const ed25519ctx = twistedEdwards({ ...ed25519Defaults, domain: ed25519_domain }); export const ed25519ph = twistedEdwards({ - ...ED25519_DEF, + ...ed25519Defaults, domain: ed25519_domain, - preHash: sha512, + prehash: sha512, }); export const x25519 = montgomery({ @@ -153,6 +153,20 @@ export const x25519 = montgomery({ randomBytes, }); +/** + * Converts ed25519 public key to x25519 public key. Uses formula: + * * `(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)) + */ +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))); +} + // Hash To Curve Elligator2 Map (NOTE: different from ristretto255 elligator) // NOTE: very important part is usage of FpSqrtEven for ELL2_C1_EDWARDS, since // SageMath returns different root first and everything falls apart diff --git a/src/ed448.ts b/src/ed448.ts index 96aeeaa..e0231d0 100644 --- a/src/ed448.ts +++ b/src/ed448.ts @@ -120,7 +120,7 @@ const ED448_DEF = { 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 ed448ph = twistedEdwards({ ...ED448_DEF, prehash: shake256_64 }); export const x448 = montgomery({ a: BigInt(156326), @@ -136,22 +136,22 @@ export const x448 = montgomery({ }, adjustScalarBytes, randomBytes, - // 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)) - // 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); - // }, }); +/** + * Converts edwards448 public key to x448 public key. Uses formula: + * * `(u, v) = ((y-1)/(y+1), sqrt(156324)*u/x)` + * * `(x, y) = (sqrt(156324)*u/v, (1+u)/(1-u))` + * @example + * const aPub = ed448.getPublicKey(utils.randomPrivateKey()); + * x448.getSharedSecret(edwardsToMontgomery(aPub), edwardsToMontgomery(someonesPub)) + */ +export function edwardsToMontgomery(edwardsPub: string | Uint8Array): Uint8Array { + const { y } = ed448.ExtendedPoint.fromHex(edwardsPub); + const _1n = BigInt(1); + return Fp.toBytes(Fp.create((y - _1n) * Fp.inv(y + _1n))); +} + // Hash To Curve Elligator2 Map const ELL2_C1 = (Fp.ORDER - BigInt(3)) / BigInt(4); // 1. c1 = (q - 3) / 4 # Integer arithmetic const ELL2_J = BigInt(156326); diff --git a/test/ed25519-addons.test.js b/test/ed25519-addons.test.js index 03e010a..dda73d9 100644 --- a/test/ed25519-addons.test.js +++ b/test/ed25519-addons.test.js @@ -62,8 +62,14 @@ describe('RFC8032ctx', () => { const v = VECTORS_RFC8032_CTX[i]; should(`${i}`, () => { deepStrictEqual(hex(ed25519ctx.getPublicKey(v.secretKey)), v.publicKey); - deepStrictEqual(hex(ed25519ctx.sign(v.message, v.secretKey, v.context)), v.signature); - deepStrictEqual(ed25519ctx.verify(v.signature, v.message, v.publicKey, v.context), true); + deepStrictEqual( + hex(ed25519ctx.sign(v.message, v.secretKey, { context: v.context })), + v.signature + ); + deepStrictEqual( + ed25519ctx.verify(v.signature, v.message, v.publicKey, { context: v.context }), + true + ); }); } }); @@ -93,14 +99,7 @@ describe('RFC8032ph', () => { }); // x25519 -should('X25519 base point', () => { - const { y } = ed25519ph.ExtendedPoint.BASE; - const { Fp } = ed25519ph.CURVE; - const u = Fp.create((y + 1n) * Fp.inv(1n - y)); - deepStrictEqual(numberToBytesLE(u, 32), x25519.GuBytes); -}); - -describe('RFC7748', () => { +describe('RFC7748 X25519 ECDH', () => { const rfc7748Mul = [ { scalar: 'a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4', @@ -127,7 +126,7 @@ describe('RFC7748', () => { ]; for (let i = 0; i < rfc7748Iter.length; i++) { const { scalar, iters } = rfc7748Iter[i]; - should(`scalarMult iteration (${i})`, () => { + should(`scalarMult iteration x${iters}`, () => { let k = x25519.GuBytes; for (let i = 0, u = k; i < iters; i++) [k, u] = [x25519.scalarMult(k, u), k]; deepStrictEqual(hex(k), scalar); @@ -145,10 +144,16 @@ describe('RFC7748', () => { deepStrictEqual(hex(x25519.scalarMult(alicePrivate, bobPublic)), shared); deepStrictEqual(hex(x25519.scalarMult(bobPrivate, alicePublic)), shared); }); -}); -describe('Wycheproof', () => { + + should('base point', () => { + const { y } = ed25519ph.ExtendedPoint.BASE; + const { Fp } = ed25519ph.CURVE; + const u = Fp.create((y + 1n) * Fp.inv(1n - y)); + deepStrictEqual(numberToBytesLE(u, 32), x25519.GuBytes); + }); + const group = x25519vectors.testGroups[0]; - should(`X25519`, () => { + should('wycheproof', () => { for (let i = 0; i < group.tests.length; i++) { const v = group.tests[i]; const comment = `(${i}, ${v.result}) ${v.comment}`; diff --git a/test/ed448.test.js b/test/ed448.test.js index 24a0ea5..c105f7d 100644 --- a/test/ed448.test.js +++ b/test/ed448.test.js @@ -513,8 +513,11 @@ describe('ed448', () => { const v = VECTORS_RFC8032_CTX[i]; should(`${i}`, () => { deepStrictEqual(hex(ed.getPublicKey(v.secretKey)), v.publicKey); - deepStrictEqual(hex(ed.sign(v.message, v.secretKey, v.context)), v.signature); - deepStrictEqual(ed.verify(v.signature, v.message, v.publicKey, v.context), true); + deepStrictEqual(hex(ed.sign(v.message, v.secretKey, { context: v.context })), v.signature); + deepStrictEqual( + ed.verify(v.signature, v.message, v.publicKey, { context: v.context }), + true + ); }); } }); @@ -559,8 +562,14 @@ describe('ed448', () => { const v = VECTORS_RFC8032_PH[i]; should(`${i}`, () => { deepStrictEqual(hex(ed448ph.getPublicKey(v.secretKey)), v.publicKey); - deepStrictEqual(hex(ed448ph.sign(v.message, v.secretKey, v.context)), v.signature); - deepStrictEqual(ed448ph.verify(v.signature, v.message, v.publicKey, v.context), true); + deepStrictEqual( + hex(ed448ph.sign(v.message, v.secretKey, { context: v.context })), + v.signature + ); + deepStrictEqual( + ed448ph.verify(v.signature, v.message, v.publicKey, { context: v.context }), + true + ); }); } }); @@ -661,34 +670,32 @@ describe('ed448', () => { deepStrictEqual(hex(x448.scalarMult(bobPrivate, alicePublic)), shared); }); - describe('wycheproof', () => { + should('wycheproof', () => { const group = x448vectors.testGroups[0]; - should(`X448`, () => { - for (let i = 0; i < group.tests.length; i++) { - const v = group.tests[i]; - const index = `(${i}, ${v.result}) ${v.comment}`; - if (v.result === 'valid' || v.result === 'acceptable') { - try { - const shared = hex(x448.scalarMult(v.private, v.public)); - deepStrictEqual(shared, v.shared, index); - } catch (e) { - // We are more strict - if (e.message.includes('Expected valid scalar')) return; - if (e.message.includes('Invalid private or public key received')) return; - if (e.message.includes('Expected 56 bytes')) return; - throw e; - } - } else if (v.result === 'invalid') { - let failed = false; - try { - x448.scalarMult(v.private, v.public); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, index); - } else throw new Error('unknown test result'); - } - }); + for (let i = 0; i < group.tests.length; i++) { + const v = group.tests[i]; + const index = `(${i}, ${v.result}) ${v.comment}`; + if (v.result === 'valid' || v.result === 'acceptable') { + try { + const shared = hex(x448.scalarMult(v.private, v.public)); + deepStrictEqual(shared, v.shared, index); + } catch (e) { + // We are more strict + if (e.message.includes('Expected valid scalar')) return; + if (e.message.includes('Invalid private or public key received')) return; + if (e.message.includes('Expected 56 bytes')) return; + throw e; + } + } else if (v.result === 'invalid') { + let failed = false; + try { + x448.scalarMult(v.private, v.public); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, index); + } else throw new Error('unknown test result'); + } }); should('have proper base point', () => {