diff --git a/README.md b/README.md index a5d14f4..7fc137e 100644 --- a/README.md +++ b/README.md @@ -149,15 +149,25 @@ edwardsToMontgomeryPub(ed25519.getPublicKey(ed25519.utils.randomPrivateKey())); edwardsToMontgomeryPriv(ed25519.utils.randomPrivateKey()); // hash-to-curve, ristretto255 -import { hashToCurve, encodeToCurve, RistrettoPoint } from '@noble/curves/ed25519'; +import { utf8ToBytes } from '@noble/hashes/utils'; +import { sha512 } from '@noble/hashes/sha512'; +import { hashToCurve, encodeToCurve, RistrettoPoint, hash_to_ristretto255 } from '@noble/curves/ed25519'; + +const msg = utf8ToBytes('Ristretto is traditionally a short shot of espresso coffee'); +hashToCurve(msg); + const rp = RistrettoPoint.fromHex( '6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919' ); -RistrettoPoint.hashToCurve('Ristretto is traditionally a short shot of espresso coffee'); -// also has add(), equals(), multiply(), toRawBytes() methods +RistrettoPoint.BASE.multiply(2n).add(rp).subtract(RistrettoPoint.BASE).toRawBytes(); +RistrettoPoint.ZERO.equals(dp) === false; +// pre-hashed hash-to-curve +RistrettoPoint.hashToCurve(sha512(msg)); +// full hash-to-curve including domain separation tag +hash_to_ristretto255(msg, { DST: 'ristretto255_XMD:SHA-512_R255MAP_RO_' }); ``` -#### ed448, X448 +#### ed448, X448, decaf448 ```ts import { ed448 } from '@noble/curves/ed448'; @@ -167,12 +177,38 @@ const msg = new TextEncoder().encode('whatsup'); const sig = ed448.sign(msg, priv); ed448.verify(sig, msg, pub); -import { ed448ph, ed448ctx, x448, hashToCurve, encodeToCurve } from '@noble/curves/ed448'; +// Variants from RFC8032: prehashed +import { ed448ph } from '@noble/curves/ed448'; + +// ECDH using curve448 aka x448 +import { x448 } from '@noble/curves/ed448'; x448.getSharedSecret(priv, pub) === x448.scalarMult(priv, pub); // aliases x448.getPublicKey(priv) === x448.scalarMultBase(priv); + +// ed448 => x448 conversion +import { edwardsToMontgomeryPub } from '@noble/curves/ed448'; +edwardsToMontgomeryPub(ed448.getPublicKey(ed448.utils.randomPrivateKey())); + +// hash-to-curve, decaf448 +import { utf8ToBytes } from '@noble/hashes/utils'; +import { shake256 } from '@noble/hashes/sha3'; +import { hashToCurve, encodeToCurve, DecafPoint, hash_to_decaf448 } from '@noble/curves/ed448'; + +const msg = utf8ToBytes('Ristretto is traditionally a short shot of espresso coffee'); +hashToCurve(msg); + +const dp = DecafPoint.fromHex( + 'c898eb4f87f97c564c6fd61fc7e49689314a1f818ec85eeb3bd5514ac816d38778f69ef347a89fca817e66defdedce178c7cc709b2116e75' +); +DecafPoint.BASE.multiply(2n).add(dp).subtract(DecafPoint.BASE).toRawBytes(); +DecafPoint.ZERO.equals(dp) === false; +// pre-hashed hash-to-curve +DecafPoint.hashToCurve(shake256(msg, { dkLen: 112 })); +// full hash-to-curve including domain separation tag +hash_to_decaf448(msg, { DST: 'decaf448_XOF:SHAKE256_D448MAP_RO_' }); ``` -Same RFC7748 / RFC8032 are followed. +Same RFC7748 / RFC8032 / IRTF draft are followed. #### bls12-381 diff --git a/benchmark/decaf448.js b/benchmark/decaf448.js new file mode 100644 index 0000000..004c62f --- /dev/null +++ b/benchmark/decaf448.js @@ -0,0 +1,18 @@ +import { run, mark, utils } from 'micro-bmark'; +import { shake256 } from '@noble/hashes/sha3'; +import * as mod from '../abstract/modular.js'; +import { ed448, DecafPoint } from '../ed448.js'; + +run(async () => { + const RAM = false; + if (RAM) utils.logMem(); + console.log(`\x1b[36mdecaf448\x1b[0m`); + const priv = mod.hashToPrivateScalar(shake256(ed448.utils.randomPrivateKey(), { dkLen: 112 }), ed448.CURVE.n); + const pub = DecafPoint.BASE.multiply(priv); + const encoded = pub.toRawBytes(); + await mark('add', 1000000, () => pub.add(DecafPoint.BASE)); + await mark('multiply', 1000, () => DecafPoint.BASE.multiply(priv)); + await mark('encode', 10000, () => DecafPoint.BASE.toRawBytes()); + await mark('decode', 10000, () => DecafPoint.fromHex(encoded)); + if (RAM) utils.logMem(); +}); diff --git a/benchmark/hash-to-curve.js b/benchmark/hash-to-curve.js index f622b9a..ad1a407 100644 --- a/benchmark/hash-to-curve.js +++ b/benchmark/hash-to-curve.js @@ -8,8 +8,8 @@ import { hashToCurve as secp256k1 } from '../secp256k1.js'; import { hashToCurve as p256 } from '../p256.js'; import { hashToCurve as p384 } from '../p384.js'; import { hashToCurve as p521 } from '../p521.js'; -import { hashToCurve as ed25519 } from '../ed25519.js'; -import { hashToCurve as ed448 } from '../ed448.js'; +import { hashToCurve as ed25519, hash_to_ristretto255 } from '../ed25519.js'; +import { hashToCurve as ed448, hash_to_decaf448 } from '../ed448.js'; import { utf8ToBytes } from '../abstract/utils.js'; const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; @@ -26,4 +26,7 @@ run(async () => { for (let [title, fn] of Object.entries({ secp256k1, p256, p384, p521, ed25519, ed448 })) { await mark(`hashToCurve ${title}`, 1000, () => fn(msg)); } + + await mark('hash_to_ristretto255', 1000, () => hash_to_ristretto255(msg, { DST: 'ristretto255_XMD:SHA-512_R255MAP_RO_' })); + await mark('hash_to_decaf448', 1000, () => hash_to_decaf448(msg, { DST: 'decaf448_XOF:SHAKE256_D448MAP_RO_' })); }); diff --git a/benchmark/ristretto255.js b/benchmark/ristretto255.js new file mode 100644 index 0000000..5041693 --- /dev/null +++ b/benchmark/ristretto255.js @@ -0,0 +1,18 @@ +import { run, mark, utils } from 'micro-bmark'; +import { sha512 } from '@noble/hashes/sha512'; +import * as mod from '../abstract/modular.js'; +import { ed25519, RistrettoPoint } from '../ed25519.js'; + +run(async () => { + const RAM = false; + if (RAM) utils.logMem(); + console.log(`\x1b[36mristretto255\x1b[0m`); + const priv = mod.hashToPrivateScalar(sha512(ed25519.utils.randomPrivateKey()), ed25519.CURVE.n); + const pub = RistrettoPoint.BASE.multiply(priv); + const encoded = pub.toRawBytes(); + await mark('add', 1000000, () => pub.add(RistrettoPoint.BASE)); + await mark('multiply', 10000, () => RistrettoPoint.BASE.multiply(priv)); + await mark('encode', 10000, () => RistrettoPoint.BASE.toRawBytes()); + await mark('decode', 10000, () => RistrettoPoint.fromHex(encoded)); + if (RAM) utils.logMem(); +}); diff --git a/package.json b/package.json index 8d59a95..294f7e5 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "*.d.ts.map" ], "scripts": { - "bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node hash-to-curve.js; node modular.js; node bls.js", + "bench": "cd benchmark; node secp256k1.js; node curves.js; node ecdh.js; node hash-to-curve.js; node modular.js; node bls.js; node ristretto255.js; node decaf448.js", "build": "tsc && tsc -p tsconfig.esm.json", "build:release": "rollup -c rollup.config.js", "build:clean": "rm *.{js,d.ts,d.ts.map,js.map} esm/*.{js,d.ts,d.ts.map,js.map} 2> /dev/null", diff --git a/src/ed448.ts b/src/ed448.ts index b0cf63e..e5a77a9 100644 --- a/src/ed448.ts +++ b/src/ed448.ts @@ -1,14 +1,25 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ import { shake256 } from '@noble/hashes/sha3'; import { concatBytes, randomBytes, utf8ToBytes, wrapConstructor } from '@noble/hashes/utils'; -import { twistedEdwards } from './abstract/edwards.js'; -import { mod, pow2, Field } from './abstract/modular.js'; +import { ExtPointType, twistedEdwards } from './abstract/edwards.js'; +import { mod, pow2, Field, isNegativeLE } from './abstract/modular.js'; import { montgomery } from './abstract/montgomery.js'; -import { createHasher } from './abstract/hash-to-curve.js'; +import { createHasher, htfBasicOpts, expand_message_xof } from './abstract/hash-to-curve.js'; +import { + bytesToHex, + bytesToNumberLE, + ensureBytes, + equalBytes, + Hex, + numberToBytesLE, +} from './abstract/utils.js'; +import { AffinePoint } from './abstract/curve.js'; /** * Edwards448 (not Ed448-Goldilocks) curve with following addons: - * * X448 ECDH + * - X448 ECDH + * - Decaf cofactor elimination + * - Elligator hash-to-group / point indistinguishability * Conforms to RFC 8032 https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2 */ @@ -18,15 +29,16 @@ const ed448P = BigInt( '726838724295606890549323807888004534353641360687318060281490199180612328166730772686396383698676545930088884461843637361053498018365439' ); +// prettier-ignore +const _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _4n = BigInt(4), _11n = BigInt(11); +// prettier-ignore +const _22n = BigInt(22), _44n = BigInt(44), _88n = BigInt(88), _223n = BigInt(223); + // powPminus3div4 calculates z = x^k mod p, where k = (p-3)/4. // Used for efficient square root calculation. // ((P-3)/4).toString(2) would produce bits [223x 1, 0, 222x 1] function ed448_pow_Pminus3div4(x: bigint): bigint { const P = ed448P; - // prettier-ignore - const _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _11n = BigInt(11); - // prettier-ignore - const _22n = BigInt(22), _44n = BigInt(44), _88n = BigInt(88), _223n = BigInt(223); const b2 = (x * x * x) % P; const b3 = (b2 * b2 * x) % P; const b6 = (pow2(b3, _3n, P) * b3) % P; @@ -53,8 +65,29 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array { return bytes; } +// Constant-time ratio of u to v. Allows to combine inversion and square root u/√v. +// Uses algo from RFC8032 5.1.3. +function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } { + const P = ed448P; + // https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3 + // To compute the square root of (u/v), the first step is to compute the + // candidate root x = (u/v)^((p+1)/4). This can be done using the + // following trick, to use a single modular powering for both the + // inversion of v and the square root: + // x = (u/v)^((p+1)/4) = u³v(u⁵v³)^((p-3)/4) (mod p) + const u2v = mod(u * u * v, P); // u²v + const u3v = mod(u2v * u, P); // u³v + const u5v3 = mod(u3v * u2v * v, P); // u⁵v³ + const root = ed448_pow_Pminus3div4(u5v3); + const x = mod(u3v * root, P); + // Verify that root is exists + const x2 = mod(x * x, P); // x² + // If vx² = u, the recovered x-coordinate is x. Otherwise, no + // square root exists, and the decoding fails. + return { isValid: mod(x2 * v, P) === u, value: x }; +} + const Fp = Field(ed448P, 456, true); -const _4n = BigInt(4); const ED448_DEF = { // Param: a @@ -94,28 +127,7 @@ const ED448_DEF = { data ); }, - - // Constant-time ratio of u to v. Allows to combine inversion and square root u/√v. - // Uses algo from RFC8032 5.1.3. - uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => { - const P = ed448P; - // https://datatracker.ietf.org/doc/html/rfc8032#section-5.2.3 - // To compute the square root of (u/v), the first step is to compute the - // candidate root x = (u/v)^((p+1)/4). This can be done using the - // following trick, to use a single modular powering for both the - // inversion of v and the square root: - // x = (u/v)^((p+1)/4) = u³v(u⁵v³)^((p-3)/4) (mod p) - const u2v = mod(u * u * v, P); // u²v - const u3v = mod(u2v * u, P); // u³v - const u5v3 = mod(u3v * u2v * v, P); // u⁵v³ - const root = ed448_pow_Pminus3div4(u5v3); - const x = mod(u3v * root, P); - // Verify that root is exists - const x2 = mod(x * x, P); // x² - // If vx² = u, the recovered x-coordinate is x. Otherwise, no - // square root exists, and the decoding fails. - return { isValid: mod(x2 * v, P) === u, value: x }; - }, + uvRatio, } as const; export const ed448 = twistedEdwards(ED448_DEF); @@ -245,3 +257,209 @@ const htf = /* @__PURE__ */ (() => ))(); export const hashToCurve = /* @__PURE__ */ (() => htf.hashToCurve)(); export const encodeToCurve = /* @__PURE__ */ (() => htf.encodeToCurve)(); + +function assertDcfPoint(other: unknown) { + if (!(other instanceof DcfPoint)) throw new Error('DecafPoint expected'); +} + +// 1-d +const ONE_MINUS_D = BigInt('39082'); +// 1-2d +const ONE_MINUS_TWO_D = BigInt('78163'); +// √(-d) +const SQRT_MINUS_D = BigInt( + '98944233647732219769177004876929019128417576295529901074099889598043702116001257856802131563896515373927712232092845883226922417596214' +); +// 1 / √(-d) +const INVSQRT_MINUS_D = BigInt( + '315019913931389607337177038330951043522456072897266928557328499619017160722351061360252776265186336876723201881398623946864393857820716' +); +// Calculates 1/√(number) +const invertSqrt = (number: bigint) => uvRatio(_1n, number); + +const MAX_448B = BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' +); +const bytes448ToNumberLE = (bytes: Uint8Array) => + ed448.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_448B); + +type ExtendedPoint = ExtPointType; + +// Computes Elligator map for Decaf +// https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-element-derivation-2 +function calcElligatorDecafMap(r0: bigint): ExtendedPoint { + const { d } = ed448.CURVE; + const P = ed448.CURVE.Fp.ORDER; + const mod = ed448.CURVE.Fp.create; + + const r = mod(-(r0 * r0)); // 1 + const u0 = mod(d * (r - _1n)); // 2 + const u1 = mod((u0 + _1n) * (u0 - r)); // 3 + + const { isValid: was_square, value: v } = uvRatio(ONE_MINUS_TWO_D, mod((r + _1n) * u1)); // 4 + + let v_prime = v; // 5 + if (!was_square) v_prime = mod(r0 * v); + + let sgn = _1n; // 6 + if (!was_square) sgn = mod(-_1n); + + const s = mod(v_prime * (r + _1n)); // 7 + let s_abs = s; + if (isNegativeLE(s, P)) s_abs = mod(-s); + + const s2 = s * s; + const W0 = mod(s_abs * _2n); // 8 + const W1 = mod(s2 + _1n); // 9 + const W2 = mod(s2 - _1n); // 10 + const W3 = mod(v_prime * s * (r - _1n) * ONE_MINUS_TWO_D + sgn); // 11 + return new ed448.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2)); +} + +/** + * Each ed448/ExtendedPoint has 4 different equivalent points. This can be + * a source of bugs for protocols like ring signatures. Decaf was created to solve this. + * Decaf point operates in X:Y:Z:T extended coordinates like ExtendedPoint, + * but it should work in its own namespace: do not combine those two. + * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448 + */ +class DcfPoint { + static BASE: DcfPoint; + static ZERO: DcfPoint; + // Private property to discourage combining ExtendedPoint + DecafPoint + // Always use Decaf encoding/decoding instead. + constructor(private readonly ep: ExtendedPoint) {} + + static fromAffine(ap: AffinePoint) { + return new DcfPoint(ed448.ExtendedPoint.fromAffine(ap)); + } + + /** + * Takes uniform output of 112-byte hash function like shake256 and converts it to `DecafPoint`. + * The hash-to-group operation applies Elligator twice and adds the results. + * **Note:** this is one-way map, there is no conversion from point to hash. + * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-element-derivation-2 + * @param hex 112-byte output of a hash function + */ + static hashToCurve(hex: Hex): DcfPoint { + hex = ensureBytes('decafHash', hex, 112); + const r1 = bytes448ToNumberLE(hex.slice(0, 56)); + const R1 = calcElligatorDecafMap(r1); + const r2 = bytes448ToNumberLE(hex.slice(56, 112)); + const R2 = calcElligatorDecafMap(r2); + return new DcfPoint(R1.add(R2)); + } + + /** + * Converts decaf-encoded string to decaf point. + * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-decode-2 + * @param hex Decaf-encoded 56 bytes. Not every 56-byte string is valid decaf encoding + */ + static fromHex(hex: Hex): DcfPoint { + hex = ensureBytes('decafHex', hex, 56); + const { d } = ed448.CURVE; + const P = ed448.CURVE.Fp.ORDER; + const mod = ed448.CURVE.Fp.create; + const emsg = 'DecafPoint.fromHex: the hex is not valid encoding of DecafPoint'; + const s = bytes448ToNumberLE(hex); + + // 1. Check that s_bytes is the canonical encoding of a field element, or else abort. + // 2. Check that s is non-negative, or else abort + if (!equalBytes(numberToBytesLE(s, 56), hex) || isNegativeLE(s, P)) throw new Error(emsg); + + const s2 = mod(s * s); // 1 + const u1 = mod(_1n + s2); // 2 + const u1sq = mod(u1 * u1); + const u2 = mod(u1sq - _4n * d * s2); // 3 + + const { isValid, value: invsqrt } = invertSqrt(mod(u2 * u1sq)); // 4 + + let u3 = mod((s + s) * invsqrt * u1 * SQRT_MINUS_D); // 5 + if (isNegativeLE(u3, P)) u3 = mod(-u3); + + const x = mod(u3 * invsqrt * u2 * INVSQRT_MINUS_D); // 6 + const y = mod((_1n - s2) * invsqrt * u1); // 7 + const t = mod(x * y); // 8 + + if (!isValid) throw new Error(emsg); + return new DcfPoint(new ed448.ExtendedPoint(x, y, _1n, t)); + } + + /** + * Encodes decaf point to Uint8Array. + * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-encode-2 + */ + toRawBytes(): Uint8Array { + let { ex: x, ey: _y, ez: z, et: t } = this.ep; + const P = ed448.CURVE.Fp.ORDER; + const mod = ed448.CURVE.Fp.create; + + const u1 = mod(mod(x + t) * mod(x - t)); // 1 + const x2 = mod(x * x); + const { value: invsqrt } = invertSqrt(mod(u1 * ONE_MINUS_D * x2)); // 2 + + let ratio = mod(invsqrt * u1 * SQRT_MINUS_D); // 3 + if (isNegativeLE(ratio, P)) ratio = mod(-ratio); + + const u2 = mod(INVSQRT_MINUS_D * ratio * z - t); // 4 + + let s = mod(ONE_MINUS_D * invsqrt * x * u2); // 5 + if (isNegativeLE(s, P)) s = mod(-s); + + return numberToBytesLE(s, 56); + } + + toHex(): string { + return bytesToHex(this.toRawBytes()); + } + + toString(): string { + return this.toHex(); + } + + // Compare one point to another. + // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448-07#name-equals-2 + equals(other: DcfPoint): boolean { + assertDcfPoint(other); + const { ex: X1, ey: Y1 } = this.ep; + const { ex: X2, ey: Y2 } = other.ep; + const mod = ed448.CURVE.Fp.create; + // (x1 * y2 == y1 * x2) + return mod(X1 * Y2) === mod(Y1 * X2); + } + + add(other: DcfPoint): DcfPoint { + assertDcfPoint(other); + return new DcfPoint(this.ep.add(other.ep)); + } + + subtract(other: DcfPoint): DcfPoint { + assertDcfPoint(other); + return new DcfPoint(this.ep.subtract(other.ep)); + } + + multiply(scalar: bigint): DcfPoint { + return new DcfPoint(this.ep.multiply(scalar)); + } + + multiplyUnsafe(scalar: bigint): DcfPoint { + return new DcfPoint(this.ep.multiplyUnsafe(scalar)); + } +} +export const DecafPoint = /* @__PURE__ */ (() => { + // decaf448 base point is ed448 base x 2 + // https://github.com/dalek-cryptography/curve25519-dalek/blob/59837c6ecff02b77b9d5ff84dbc239d0cf33ef90/vendor/ristretto.sage#L699 + if (!DcfPoint.BASE) DcfPoint.BASE = new DcfPoint(ed448.ExtendedPoint.BASE).multiply(_2n); + if (!DcfPoint.ZERO) DcfPoint.ZERO = new DcfPoint(ed448.ExtendedPoint.ZERO); + return DcfPoint; +})(); + +// https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/16/ +// Appendix C. Hashing to decaf448 +export const hash_to_decaf448 = (msg: Uint8Array, options: htfBasicOpts) => { + const d = options.DST; + const DST = typeof d === 'string' ? utf8ToBytes(d) : d; + const uniform_bytes = expand_message_xof(msg, DST, 112, 224, shake256); + const P = DcfPoint.hashToCurve(uniform_bytes); + return P; +}; diff --git a/test/ed448-addons.test.js b/test/ed448-addons.test.js new file mode 100644 index 0000000..02cf82f --- /dev/null +++ b/test/ed448-addons.test.js @@ -0,0 +1,117 @@ +import { bytesToHex as hex, hexToBytes } from '@noble/hashes/utils'; +import { deepStrictEqual, throws } from 'assert'; +import { describe, should } from 'micro-should'; +import { bytesToNumberLE } from '../esm/abstract/utils.js'; +import { ed448, DecafPoint } from '../esm/ed448.js'; + +describe('decaf448', () => { + should('follow the byte encodings of small multiples', () => { + const encodingsOfSmallMultiples = [ + // This is the identity point + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + // This is the basepoint + '6666666666666666666666666666666666666666666666666666666633333333333333333333333333333333333333333333333333333333', + // These are small multiples of the basepoint + 'c898eb4f87f97c564c6fd61fc7e49689314a1f818ec85eeb3bd5514ac816d38778f69ef347a89fca817e66defdedce178c7cc709b2116e75', + 'a0c09bf2ba7208fda0f4bfe3d0f5b29a543012306d43831b5adc6fe7f8596fa308763db15468323b11cf6e4aeb8c18fe44678f44545a69bc', + 'b46f1836aa287c0a5a5653f0ec5ef9e903f436e21c1570c29ad9e5f596da97eeaf17150ae30bcb3174d04bc2d712c8c7789d7cb4fda138f4', + '1c5bbecf4741dfaae79db72dface00eaaac502c2060934b6eaaeca6a20bd3da9e0be8777f7d02033d1b15884232281a41fc7f80eed04af5e', + '86ff0182d40f7f9edb7862515821bd67bfd6165a3c44de95d7df79b8779ccf6460e3c68b70c16aaa280f2d7b3f22d745b97a89906cfc476c', + '502bcb6842eb06f0e49032bae87c554c031d6d4d2d7694efbf9c468d48220c50f8ca28843364d70cee92d6fe246e61448f9db9808b3b2408', + '0c9810f1e2ebd389caa789374d78007974ef4d17227316f40e578b336827da3f6b482a4794eb6a3975b971b5e1388f52e91ea2f1bcb0f912', + '20d41d85a18d5657a29640321563bbd04c2ffbd0a37a7ba43a4f7d263ce26faf4e1f74f9f4b590c69229ae571fe37fa639b5b8eb48bd9a55', + 'e6b4b8f408c7010d0601e7eda0c309a1a42720d6d06b5759fdc4e1efe22d076d6c44d42f508d67be462914d28b8edce32e7094305164af17', + 'be88bbb86c59c13d8e9d09ab98105f69c2d1dd134dbcd3b0863658f53159db64c0e139d180f3c89b8296d0ae324419c06fa87fc7daaf34c1', + 'a456f9369769e8f08902124a0314c7a06537a06e32411f4f93415950a17badfa7442b6217434a3a05ef45be5f10bd7b2ef8ea00c431edec5', + '186e452c4466aa4383b4c00210d52e7922dbf9771e8b47e229a9b7b73c8d10fd7ef0b6e41530f91f24a3ed9ab71fa38b98b2fe4746d51d68', + '4ae7fdcae9453f195a8ead5cbe1a7b9699673b52c40ab27927464887be53237f7f3a21b938d40d0ec9e15b1d5130b13ffed81373a53e2b43', + '841981c3bfeec3f60cfeca75d9d8dc17f46cf0106f2422b59aec580a58f342272e3a5e575a055ddb051390c54c24c6ecb1e0aceb075f6056', + ]; + let B = DecafPoint.BASE; + let P = DecafPoint.ZERO; + for (const encoded of encodingsOfSmallMultiples) { + deepStrictEqual(P.toHex(), encoded); + deepStrictEqual(DecafPoint.fromHex(encoded).toHex(), encoded); + P = P.add(B); + } + }); + should('not convert bad bytes encoding', () => { + const badEncodings = [ + // These are all bad because they're non-canonical field encodings. + '8e24f838059ee9fef1e209126defe53dcd74ef9b6304601c6966099effffffffffffffffffffffffffffffffffffffffffffffffffffffff', + '86fcc7212bd4a0b980928666dc28c444a605ef38e09fb569e28d4443ffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + '866d54bd4c4ff41a55d4eefdbeca73cbd653c7bd3135b383708ec0bdffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + '4a380ccdab9c86364a89e77a464d64f9157538cfdfa686adc0d5ece4ffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + 'f22d9d4c945dd44d11e0b1d3d3d358d959b4844d83b08c44e659d79fffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + '8cdffc681aa99e9c818c8ef4c3808b58e86acdef1ab68c8477af185bffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + '0e1c12ac7b5920effbd044e897c57634e2d05b5c27f8fa3df8a086a1ffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + // These are all bad because they're negative field elements. + '15141bd2121837ef71a0016bd11be757507221c26542244f23806f3fd3496b7d4c36826276f3bf5deea2c60c4fa4cec69946876da497e795', + '455d380238434ab740a56267f4f46b7d2eb2dd8ee905e51d7b0ae8a6cb2bae501e67df34ab21fa45946068c9f233939b1d9521a998b7cb93', + '810b1d8e8bf3a9c023294bbfd3d905a97531709bdc0f42390feedd7010f77e98686d400c9c86ed250ceecd9de0a18888ffecda0f4ea1c60d', + 'd3af9cc41be0e5de83c0c6273bedcb9351970110044a9a41c7b9b2267cdb9d7bf4dc9c2fdb8bed32878184604f1d9944305a8df4274ce301', + '9312bcab09009e4330ff89c4bc1e9e000d863efc3c863d3b6c507a40fd2cdefde1bf0892b4b5ed9780b91ed1398fb4a7344c605aa5efda74', + '53d11bce9e62a29d63ed82ae93761bdd76e38c21e2822d6ebee5eb1c5b8a03eaf9df749e2490eda9d8ac27d1f71150de93668074d18d1c3a', + '697c1aed3cd8858515d4be8ac158b229fe184d79cb2b06e49210a6f3a7cd537bcd9bd390d96c4ab6a4406da5d93640726285370cfa95df80', + // These are all bad because they give a nonsquare x². + '58ad48715c9a102569b68b88362a4b0645781f5a19eb7e59c6a4686fd0f0750ff42e3d7af1ab38c29d69b670f31258919c9fdbf6093d06c0', + '8ca37ee2b15693f06e910cf43c4e32f1d5551dda8b1e48cb6ddd55e440dbc7b296b601919a4e4069f59239ca247ff693f7daa42f086122b1', + '982c0ec7f43d9f97c0a74b36db0abd9ca6bfb98123a90782787242c8a523cdc76df14a910d54471127e7662a1059201f902940cd39d57af5', + 'baa9ab82d07ca282b968a911a6c3728d74bf2fe258901925787f03ee4be7e3cb6684fd1bcfe5071a9a974ad249a4aaa8ca81264216c68574', + '2ed9ffe2ded67a372b181ac524996402c42970629db03f5e8636cbaf6074b523d154a7a8c4472c4c353ab88cd6fec7da7780834cc5bd5242', + 'f063769e4241e76d815800e4933a3a144327a30ec40758ad3723a788388399f7b3f5d45b6351eb8eddefda7d5bff4ee920d338a8b89d8b63', + '5a0104f1f55d152ceb68bc138182499891d90ee8f09b40038ccc1e07cb621fd462f781d045732a4f0bda73f0b2acf94355424ff0388d4b9c', + ]; + for (const badBytes of badEncodings) { + const b = hexToBytes(badBytes); + throws(() => DecafPoint.fromHex(b), badBytes); + } + }); + should('create right points from uniform hash', () => { + const hashes = [ + 'cbb8c991fd2f0b7e1913462d6463e4fd2ce4ccdd28274dc2ca1f4165d5ee6cdccea57be3416e166fd06718a31af45a2f8e987e301be59ae6673e963001dbbda80df47014a21a26d6c7eb4ebe0312aa6fffb8d1b26bc62ca40ed51f8057a635a02c2b8c83f48fa6a2d70f58a1185902c0', + 'b6d8da654b13c3101d6634a231569e6b85961c3f4b460a08ac4a5857069576b64428676584baa45b97701be6d0b0ba18ac28d443403b45699ea0fbd1164f5893d39ad8f29e48e399aec5902508ea95e33bc1e9e4620489d684eb5c26bc1ad1e09aba61fabc2cdfee0b6b6862ffc8e55a', + '36a69976c3e5d74e4904776993cbac27d10f25f5626dd45c51d15dcf7b3e6a5446a6649ec912a56895d6baa9dc395ce9e34b868d9fb2c1fc72eb6495702ea4f446c9b7a188a4e0826b1506b0747a6709f37988ff1aeb5e3788d5076ccbb01a4bc6623c92ff147a1e21b29cc3fdd0e0f4', + 'd5938acbba432ecd5617c555a6a777734494f176259bff9dab844c81aadcf8f7abd1a9001d89c7008c1957272c1786a4293bb0ee7cb37cf3988e2513b14e1b75249a5343643d3c5e5545a0c1a2a4d3c685927c38bc5e5879d68745464e2589e000b31301f1dfb7471a4f1300d6fd0f99', + '4dec58199a35f531a5f0a9f71a53376d7b4bdd6bbd2904234a8ea65bbacbce2a542291378157a8f4be7b6a092672a34d85e473b26ccfbd4cdc6739783dc3f4f6ee3537b7aed81df898c7ea0ae89a15b5559596c2a5eeacf8b2b362f3db2940e3798b63203cae77c4683ebaed71533e51', + 'df2aa1536abb4acab26efa538ce07fd7bca921b13e17bc5ebcba7d1b6b733deda1d04c220f6b5ab35c61b6bcb15808251cab909a01465b8ae3fc770850c66246d5a9eae9e2877e0826e2b8dc1bc08009590bc6778a84e919fbd28e02a0f9c49b48dc689eb5d5d922dc01469968ee81b5', + 'e9fb440282e07145f1f7f5ecf3c273212cd3d26b836b41b02f108431488e5e84bd15f2418b3d92a3380dd66a374645c2a995976a015632d36a6c2189f202fc766e1c82f50ad9189be190a1f0e8f9b9e69c9c18cc98fdd885608f68bf0fdedd7b894081a63f70016a8abf04953affbefa', + ]; + const encodedHashToPoints = [ + '0c709c9607dbb01c94513358745b7c23953d03b33e39c7234e268d1d6e24f34014ccbc2216b965dd231d5327e591dc3c0e8844ccfd568848', + '76ab794e28ff1224c727fa1016bf7f1d329260b7218a39aea2fdb17d8bd9119017b093d641cedf74328c327184dc6f2a64bd90eddccfcdab', + 'c8d7ac384143500e50890a1c25d643343accce584caf2544f9249b2bf4a6921082be0e7f3669bb5ec24535e6c45621e1f6dec676edd8b664', + '62beffc6b8ee11ccd79dbaac8f0252c750eb052b192f41eeecb12f2979713b563caf7d22588eca5e80995241ef963e7ad7cb7962f343a973', + 'f4ccb31d263731ab88bed634304956d2603174c66da38742053fa37dd902346c3862155d68db63be87439e3d68758ad7268e239d39c4fd3b', + '7e79b00e8e0a76a67c0040f62713b8b8c6d6f05e9c6d02592e8a22ea896f5deacc7c7df5ed42beae6fedb9000285b482aa504e279fd49c32', + '20b171cb16be977f15e013b9752cf86c54c631c4fc8cbf7c03c4d3ac9b8e8640e7b0e9300b987fe0ab5044669314f6ed1650ae037db853f1', + ]; + + for (let i = 0; i < hashes.length; i++) { + const hash = hexToBytes(hashes[i]); + const point = DecafPoint.hashToCurve(hash); + deepStrictEqual(point.toHex(), encodedHashToPoints[i]); + } + }); + should('have proper equality testing', () => { + const MAX_448B = BigInt( + '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' + ); + const bytes448ToNumberLE = (bytes) => ed448.CURVE.Fp.create(bytesToNumberLE(bytes) & MAX_448B); + + const priv = new Uint8Array([ + 23, 211, 149, 179, 209, 108, 78, 37, 229, 45, 122, 220, 85, 38, 192, 182, 96, 40, 168, 63, + 175, 194, 73, 202, 14, 175, 78, 15, 117, 175, 40, 32, 218, 221, 151, 58, 158, 91, 250, 141, + 18, 175, 191, 119, 152, 124, 223, 101, 54, 218, 76, 158, 43, 112, 151, 32, + ]); + const pub = DecafPoint.BASE.multiply(bytes448ToNumberLE(priv)); + deepStrictEqual(pub.equals(DecafPoint.ZERO), false); + }); +}); + +// ESM is broken. +import url from 'url'; + +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 e94b151..6c127d9 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -4,6 +4,7 @@ import { should } from 'micro-should'; import './basic.test.js'; import './nist.test.js'; import './ed448.test.js'; +import './ed448-addons.test.js'; import './ed25519.test.js'; import './ed25519-addons.test.js'; import './secp256k1.test.js';