diff --git a/curve-definitions/benchmark/index.js b/curve-definitions/benchmark/index.js index b35f0fc..905318a 100644 --- a/curve-definitions/benchmark/index.js +++ b/curve-definitions/benchmark/index.js @@ -46,10 +46,15 @@ export const CURVES = { const sig = noble_secp256k1.signSync(msg, priv); return { priv, pub, msg, sig }; }, - getPublicKey: { + getPublicKey1: { samples: 10000, - old: () => noble_secp256k1.getPublicKey(noble_secp256k1.utils.randomPrivateKey()), - noble: () => secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey()), + noble: () => secp256k1.getPublicKey(3n), + old: () => noble_secp256k1.getPublicKey(3n), + }, + getPublicKey255: { + samples: 10000, + noble: () => secp256k1.getPublicKey(2n**255n-1n), + old: () => noble_secp256k1.getPublicKey(2n**255n-1n), }, sign: { samples: 5000, diff --git a/curve-definitions/src/ed25519.ts b/curve-definitions/src/ed25519.ts index 220482e..7e53e03 100644 --- a/curve-definitions/src/ed25519.ts +++ b/curve-definitions/src/ed25519.ts @@ -1,9 +1,17 @@ /*! @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 } from '@noble/curves/edwards'; +import { twistedEdwards, ExtendedPointType } from '@noble/curves/edwards'; import { montgomery } from '@noble/curves/montgomery'; import { mod, pow2, isNegativeLE } from '@noble/curves/modular'; +import { + ensureBytes, + equalBytes, + bytesToHex, + bytesToNumberLE, + numberToBytesLE, + Hex, +} from '@noble/curves/utils'; const ed25519P = BigInt( '57896044618658097711785492504343953926634992332820282019728792003956564819949' @@ -47,6 +55,25 @@ 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 = ed25519P; + const v3 = mod(v * v * v, P); // v³ + const v7 = mod(v3 * v3 * v, P); // v⁷ + // (p+3)/8 and (p-5)/8 + const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8; + let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8 + const vx2 = mod(v * x * x, P); // vx² + const root1 = x; // First root candidate + const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate + const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root + const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4) + const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1) + if (useRoot1) x = root1; + if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time + if (isNegativeLE(x, P)) x = mod(-x, P); + return { isValid: useRoot1 || useRoot2, value: x }; +} // Just in case export const ED25519_TORSION_SUBGROUP = [ @@ -82,24 +109,7 @@ const ED25519_DEF = { // dom2 // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. // Constant-time, u/√v - uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => { - const P = ed25519P; - const v3 = mod(v * v * v, P); // v³ - const v7 = mod(v3 * v3 * v, P); // v⁷ - // (p+3)/8 and (p-5)/8 - const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8; - let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8 - const vx2 = mod(v * x * x, P); // vx² - const root1 = x; // First root candidate - const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate - const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root - const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4) - const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1) - if (useRoot1) x = root1; - if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time - if (isNegativeLE(x, P)) x = mod(-x, P); - return { isValid: useRoot1 || useRoot2, value: x }; - }, + uvRatio, } as const; export const ed25519 = twistedEdwards(ED25519_DEF); @@ -133,3 +143,192 @@ export const x25519 = montgomery({ }, adjustScalarBytes, }); + +/** + * Each ed25519/ExtendedPoint has 8 different equivalent points. This can be + * a source of bugs for protocols like ring signatures. Ristretto was created to solve this. + * Ristretto 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 + */ +function assertRstPoint(other: unknown) { + if (!(other instanceof RistrettoPoint)) throw new TypeError('RistrettoPoint expected'); +} +// √(-1) aka √(a) aka 2^((p-1)/4) +const SQRT_M1 = BigInt( + '19681161376707505956807079304988542015446066515923890162744021073123829784752' +); +// √(ad - 1) +const SQRT_AD_MINUS_ONE = BigInt( + '25063068953384623474111414158702152701244531502492656460079210482610430750235' +); +// 1 / √(a-d) +const INVSQRT_A_MINUS_D = BigInt( + '54469307008909316920995813868745141605393597292927456921205312896311721017578' +); +// 1-d² +const ONE_MINUS_D_SQ = BigInt( + '1159843021668779879193775521855586647937357759715417654439879720876111806838' +); +// (d-1)² +const D_MINUS_ONE_SQ = BigInt( + '40440834346308536858101042469323190826248399146238708352240133220865137265952' +); +// Calculates 1/√(number) +const _0n = BigInt(0); +const _1n = BigInt(1); +const invertSqrt = (number: bigint) => uvRatio(_1n, number); + +const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'); +const bytes255ToNumberLE = (bytes: Uint8Array) => + ed25519.utils.mod(bytesToNumberLE(bytes) & MAX_255B); + +type ExtendedPoint = ExtendedPointType; +export class RistrettoPoint { + static BASE = new RistrettoPoint(ed25519.ExtendedPoint.BASE); + static ZERO = new RistrettoPoint(ed25519.ExtendedPoint.ZERO); + + // Private property to discourage combining ExtendedPoint + RistrettoPoint + // Always use Ristretto encoding/decoding instead. + constructor(private readonly ep: ExtendedPoint) {} + + // Computes Elligator map for Ristretto + // https://ristretto.group/formulas/elligator.html + private static calcElligatorRistrettoMap(r0: bigint): ExtendedPoint { + const { d, P } = ed25519.CURVE; + const { mod } = ed25519.utils; + const r = mod(SQRT_M1 * r0 * r0); // 1 + const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2 + let c = BigInt(-1); // 3 + const D = mod((c - d * r) * mod(r + d)); // 4 + let { isValid: Ns_D_is_sq, value: s } = uvRatio(Ns, D); // 5 + let s_ = mod(s * r0); // 6 + if (!isNegativeLE(s_, P)) s_ = mod(-s_); + if (!Ns_D_is_sq) s = s_; // 7 + if (!Ns_D_is_sq) c = r; // 8 + const Nt = mod(c * (r - _1n) * D_MINUS_ONE_SQ - D); // 9 + const s2 = s * s; + const W0 = mod((s + s) * D); // 10 + const W1 = mod(Nt * SQRT_AD_MINUS_ONE); // 11 + const W2 = mod(_1n - s2); // 12 + const W3 = mod(_1n + s2); // 13 + return new ed25519.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2)); + } + + /** + * Takes uniform output of 64-bit hash function like sha512 and converts it to `RistrettoPoint`. + * 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://ristretto.group/formulas/elligator.html + * @param hex 64-bit output of a hash function + */ + static hashToCurve(hex: Hex): RistrettoPoint { + hex = ensureBytes(hex, 64); + const r1 = bytes255ToNumberLE(hex.slice(0, 32)); + const R1 = this.calcElligatorRistrettoMap(r1); + const r2 = bytes255ToNumberLE(hex.slice(32, 64)); + const R2 = this.calcElligatorRistrettoMap(r2); + return new RistrettoPoint(R1.add(R2)); + } + + /** + * Converts ristretto-encoded string to ristretto point. + * https://ristretto.group/formulas/decoding.html + * @param hex Ristretto-encoded 32 bytes. Not every 32-byte string is valid ristretto encoding + */ + static fromHex(hex: Hex): RistrettoPoint { + hex = ensureBytes(hex, 32); + const { a, d, P } = ed25519.CURVE; + const { mod } = ed25519.utils; + const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint'; + const s = bytes255ToNumberLE(hex); + // 1. Check that s_bytes is the canonical encoding of a field element, or else abort. + // 3. Check that s is non-negative, or else abort + if (!equalBytes(numberToBytesLE(s, 32), hex) || isNegativeLE(s, P)) throw new Error(emsg); + const s2 = mod(s * s); + const u1 = mod(_1n + a * s2); // 4 (a is -1) + const u2 = mod(_1n - a * s2); // 5 + const u1_2 = mod(u1 * u1); + const u2_2 = mod(u2 * u2); + const v = mod(a * d * u1_2 - u2_2); // 6 + const { isValid, value: I } = invertSqrt(mod(v * u2_2)); // 7 + const Dx = mod(I * u2); // 8 + const Dy = mod(I * Dx * v); // 9 + let x = mod((s + s) * Dx); // 10 + if (isNegativeLE(x, P)) x = mod(-x); // 10 + const y = mod(u1 * Dy); // 11 + const t = mod(x * y); // 12 + if (!isValid || isNegativeLE(t, P) || y === _0n) throw new Error(emsg); + return new RistrettoPoint(new ed25519.ExtendedPoint(x, y, _1n, t)); + } + + /** + * Encodes ristretto point to Uint8Array. + * https://ristretto.group/formulas/encoding.html + */ + toRawBytes(): Uint8Array { + let { x, y, z, t } = this.ep; + const { P } = ed25519.CURVE; + const { mod } = ed25519.utils; + const u1 = mod(mod(z + y) * mod(z - y)); // 1 + const u2 = mod(x * y); // 2 + // Square root always exists + const u2sq = mod(u2 * u2); + const { value: invsqrt } = invertSqrt(mod(u1 * u2sq)); // 3 + const D1 = mod(invsqrt * u1); // 4 + const D2 = mod(invsqrt * u2); // 5 + const zInv = mod(D1 * D2 * t); // 6 + let D: bigint; // 7 + if (isNegativeLE(t * zInv, P)) { + let _x = mod(y * SQRT_M1); + let _y = mod(x * SQRT_M1); + x = _x; + y = _y; + D = mod(D1 * INVSQRT_A_MINUS_D); + } else { + D = D2; // 8 + } + if (isNegativeLE(x * zInv, P)) y = mod(-y); // 9 + let s = mod((z - y) * D); // 10 (check footer's note, no sqrt(-a)) + if (isNegativeLE(s, P)) s = mod(-s); + return numberToBytesLE(s, 32); // 11 + } + + toHex(): string { + return bytesToHex(this.toRawBytes()); + } + + toString(): string { + return this.toHex(); + } + + // Compare one point to another. + equals(other: RistrettoPoint): boolean { + assertRstPoint(other); + const a = this.ep; + const b = other.ep; + const { mod } = ed25519.utils; + // (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2) + const one = mod(a.x * b.y) === mod(a.y * b.x); + const two = mod(a.y * b.y) === mod(a.x * b.x); + return one || two; + } + + add(other: RistrettoPoint): RistrettoPoint { + assertRstPoint(other); + return new RistrettoPoint(this.ep.add(other.ep)); + } + + subtract(other: RistrettoPoint): RistrettoPoint { + assertRstPoint(other); + return new RistrettoPoint(this.ep.subtract(other.ep)); + } + + multiply(scalar: number | bigint): RistrettoPoint { + return new RistrettoPoint(this.ep.multiply(scalar)); + } + + multiplyUnsafe(scalar: number | bigint): RistrettoPoint { + return new RistrettoPoint(this.ep.multiplyUnsafe(scalar)); + } +} diff --git a/curve-definitions/src/secp256k1.ts b/curve-definitions/src/secp256k1.ts index 5df8089..2c6b7bb 100644 --- a/curve-definitions/src/secp256k1.ts +++ b/curve-definitions/src/secp256k1.ts @@ -2,6 +2,16 @@ import { sha256 } from '@noble/hashes/sha256'; import { mod, pow2 } from '@noble/curves/modular'; import { createCurve } from './_shortw_utils.js'; +import { PointType } from '@noble/curves/weierstrass'; +import { + ensureBytes, + concatBytes, + Hex, + hexToBytes, + bytesToNumberBE, + PrivKey, +} from '@noble/curves/utils'; +import { randomBytes } from '@noble/hashes/utils'; /** * secp256k1 definition with efficient square root and endomorphism. @@ -17,6 +27,32 @@ const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd const _1n = BigInt(1); const _2n = BigInt(2); const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; + +function sqrtMod(x: bigint): bigint { + const P = secp256k1P; + const _3n = BigInt(3); + const _6n = BigInt(6); + const _11n = BigInt(11); + const _22n = BigInt(22); + const _23n = BigInt(23); + const _44n = BigInt(44); + const _88n = BigInt(88); + const b2 = (x * x * x) % P; // x^3, 11 + const b3 = (b2 * b2 * x) % P; // x^7 + const b6 = (pow2(b3, _3n, P) * b3) % P; + const b9 = (pow2(b6, _3n, P) * b3) % P; + const b11 = (pow2(b9, _2n, P) * b2) % P; + const b22 = (pow2(b11, _11n, P) * b11) % P; + const b44 = (pow2(b22, _22n, P) * b22) % P; + const b88 = (pow2(b44, _44n, P) * b44) % P; + const b176 = (pow2(b88, _88n, P) * b88) % P; + const b220 = (pow2(b176, _44n, P) * b44) % P; + const b223 = (pow2(b220, _3n, P) * b3) % P; + const t1 = (pow2(b223, _23n, P) * b22) % P; + const t2 = (pow2(t1, _6n, P) * b2) % P; + return pow2(t2, _2n, P); +} + export const secp256k1 = createCurve( { a: 0n, @@ -37,30 +73,7 @@ export const secp256k1 = createCurve( // We are unwrapping the loop because it's 2x faster. // (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00] // We are multiplying it bit-by-bit - sqrtMod: (x: bigint): bigint => { - const P = secp256k1P; - const _3n = BigInt(3); - const _6n = BigInt(6); - const _11n = BigInt(11); - const _22n = BigInt(22); - const _23n = BigInt(23); - const _44n = BigInt(44); - const _88n = BigInt(88); - const b2 = (x * x * x) % P; // x^3, 11 - const b3 = (b2 * b2 * x) % P; // x^7 - const b6 = (pow2(b3, _3n, P) * b3) % P; - const b9 = (pow2(b6, _3n, P) * b3) % P; - const b11 = (pow2(b9, _2n, P) * b2) % P; - const b22 = (pow2(b11, _11n, P) * b11) % P; - const b44 = (pow2(b22, _22n, P) * b22) % P; - const b88 = (pow2(b44, _44n, P) * b44) % P; - const b176 = (pow2(b88, _88n, P) * b88) % P; - const b220 = (pow2(b176, _44n, P) * b44) % P; - const b223 = (pow2(b220, _3n, P) * b3) % P; - const t1 = (pow2(b223, _23n, P) * b22) % P; - const t2 = (pow2(t1, _6n, P) * b2) % P; - return pow2(t2, _2n, P); - }, + sqrtMod, endo: { beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), splitScalar: (k: bigint) => { @@ -88,3 +101,160 @@ export const secp256k1 = createCurve( }, sha256 ); + +// Schnorr +const _0n = BigInt(0); +const numTo32b = secp256k1.utils._bigintToBytes; +const numTo32bStr = secp256k1.utils._bigintToString; +const normalizePrivateKey = secp256k1.utils._normalizePrivateKey; + +// TODO: export? +function normalizePublicKey(publicKey: Hex | PointType): PointType { + if (publicKey instanceof secp256k1.Point) { + publicKey.assertValidity(); + return publicKey; + } else { + const bytes = ensureBytes(publicKey); + // Schnorr is 32 bytes + if (bytes.length === 32) { + const x = bytesToNumberBE(bytes); + if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); + const y2 = secp256k1.utils._weierstrassEquation(x); // y² = x³ + ax + b + let y = sqrtMod(y2); // y = y² ^ (p+1)/4 + const isYOdd = (y & _1n) === _1n; + // Schnorr + if (isYOdd) y = mod(-y, secp256k1.CURVE.P); + const point = new secp256k1.Point(x, y); + point.assertValidity(); + return point; + } + // Do we need that in schnorr at all? + return secp256k1.Point.fromHex(publicKey); + } +} + +const isWithinCurveOrder = secp256k1.utils._isWithinCurveOrder; +const isValidFieldElement = secp256k1.utils._isValidFieldElement; + +const TAGS = { + challenge: 'BIP0340/challenge', + aux: 'BIP0340/aux', + nonce: 'BIP0340/nonce', +} as const; + +/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ +const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {}; +export function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === undefined) { + const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return sha256(concatBytes(tagP, ...messages)); +} + +const toRawX = (point: PointType) => point.toRawBytes(true).slice(1); + +// Schnorr signatures are superior to ECDSA from above. +// Below is Schnorr-specific code as per BIP0340. +function schnorrChallengeFinalize(ch: Uint8Array): bigint { + return mod(bytesToNumberBE(ch), secp256k1.CURVE.n); +} +// Do we need this at all for Schnorr? +class SchnorrSignature { + constructor(readonly r: bigint, readonly s: bigint) { + this.assertValidity(); + } + static fromHex(hex: Hex) { + const bytes = ensureBytes(hex); + if (bytes.length !== 64) + throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`); + const r = bytesToNumberBE(bytes.subarray(0, 32)); + const s = bytesToNumberBE(bytes.subarray(32, 64)); + return new SchnorrSignature(r, s); + } + assertValidity() { + const { r, s } = this; + if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) throw new Error('Invalid signature'); + } + toHex(): string { + return numTo32bStr(this.r) + numTo32bStr(this.s); + } + toRawBytes(): Uint8Array { + return hexToBytes(this.toHex()); + } +} + +function schnorrGetScalar(priv: bigint) { + const point = secp256k1.Point.fromPrivateKey(priv); + const scalar = point.hasEvenY() ? priv : secp256k1.CURVE.n - priv; + return { point, scalar, x: toRawX(point) }; +} +/** + * Synchronously creates Schnorr signature. Improved security: verifies itself before + * producing an output. + * @param msg message (not message hash) + * @param privateKey private key + * @param auxRand random bytes that would be added to k. Bad RNG won't break it. + */ +function schnorrSign( + message: Hex, + privateKey: PrivKey, + auxRand: Hex = randomBytes(32) +): Uint8Array { + if (message == null) throw new TypeError(`sign: Expected valid message, not "${message}"`); + const m = ensureBytes(message); + // checks for isWithinCurveOrder + const { x: px, scalar: d } = schnorrGetScalar(normalizePrivateKey(privateKey)); + const rand = ensureBytes(auxRand); + if (rand.length !== 32) throw new TypeError('sign: Expected 32 bytes of aux randomness'); + const tag = taggedHash; + const t0h = tag(TAGS.aux, rand); + const t = numTo32b(d ^ bytesToNumberBE(t0h)); + const k0h = tag(TAGS.nonce, t, px, m); + const k0 = mod(bytesToNumberBE(k0h), secp256k1.CURVE.n); + if (k0 === _0n) throw new Error('sign: Creation of signature failed. k is zero'); + const { point: R, x: rx, scalar: k } = schnorrGetScalar(k0); + const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m)); + const sig = new SchnorrSignature(R.x, mod(k + e * d, secp256k1.CURVE.n)).toRawBytes(); + if (!schnorrVerify(sig, m, px)) throw new Error('sign: Invalid signature produced'); + return sig; +} + +/** + * Verifies Schnorr signature synchronously. + */ +function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean { + try { + const raw = signature instanceof SchnorrSignature; + const sig: SchnorrSignature = raw ? signature : SchnorrSignature.fromHex(signature); + if (raw) sig.assertValidity(); // just in case + + const { r, s } = sig; + const m = ensureBytes(message); + const P = normalizePublicKey(publicKey); + const e = schnorrChallengeFinalize(taggedHash(TAGS.challenge, numTo32b(r), toRawX(P), m)); + // Finalize + // R = s⋅G - e⋅P + // -eP == (n-e)P + const R = secp256k1.Point.BASE.multiplyAndAddUnsafe( + P, + normalizePrivateKey(s), + mod(-e, secp256k1.CURVE.n) + ); + if (!R || !R.hasEvenY() || R.x !== r) return false; + return true; + } catch (error) { + return false; + } +} + +export const schnorr = { + Signature: SchnorrSignature, + // Schnorr's pubkey is just `x` of Point (BIP340) + getPublicKey: (privateKey: PrivKey): Uint8Array => + toRawX(secp256k1.Point.fromPrivateKey(privateKey)), + sign: schnorrSign, + verify: schnorrVerify, +}; diff --git a/curve-definitions/test/basic.test.js b/curve-definitions/test/basic.test.js index 3910225..62441b3 100644 --- a/curve-definitions/test/basic.test.js +++ b/curve-definitions/test/basic.test.js @@ -19,7 +19,8 @@ import { jubjub } from '../lib/jubjub.js'; // prettier-ignore const CURVES = { - secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1, + secp192r1, secp224r1, secp256r1, secp384r1, secp521r1, + secp256k1, ed25519, ed25519ctx, ed25519ph, ed448, ed448ph, starkCurve, @@ -46,14 +47,15 @@ for (const name in CURVES) { const CURVE_ORDER = C.CURVE.n; const FC_BIGINT = fc.bigInt(1n + 1n, CURVE_ORDER - 1n); - const POINTS = { Point: C.Point, JacobianPoint: C.JacobianPoint, ExtendedPoint: C.ExtendedPoint }; // Check that curve doesn't accept points from other curves const O = name === 'secp256k1' ? secp256r1 : secp256k1; - const OTHER_POINTS = { - Point: O.Point, - JacobianPoint: O.JacobianPoint, - ExtendedPoint: O.ExtendedPoint, - }; + const POINTS = {}; + const OTHER_POINTS = {}; + for (const name of ['Point', 'JacobianPoint', 'ExtendedPoint', 'ProjectivePoint']) { + POINTS[name] = C[name]; + OTHER_POINTS[name] = O[name]; + } + for (const pointName in POINTS) { const p = POINTS[pointName]; const o = OTHER_POINTS[pointName]; @@ -120,6 +122,7 @@ for (const name in CURVES) { fc.assert( fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { const c = mod.mod(a + b, CURVE_ORDER); + if (c === CURVE_ORDER || c < 1n) return; const pA = G[1].multiply(a); const pB = G[1].multiply(b); const pC = G[1].multiply(c); @@ -157,7 +160,7 @@ for (const name in CURVES) { throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])'); if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), `Point ${op} ${pointName}`); - throws(() => G[1][op](O.BASE), `${op}/other curve point`); + throws(() => G[1][op](o.BASE), `${op}/other curve point`); }); } @@ -177,7 +180,7 @@ for (const name in CURVES) { throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])'); if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), `Point.equals(${pointName})`); - throws(() => G[1].equals(O.BASE), 'other curve point'); + throws(() => G[1].equals(o.BASE), 'other curve point'); }); for (const op of ['multiply', 'multiplyUnsafe']) { @@ -199,7 +202,7 @@ for (const name in CURVES) { throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])'); throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])'); - throws(() => G[1][op](O.BASE), 'other curve point'); + throws(() => G[1][op](o.BASE), 'other curve point'); }); } // Complex point (Extended/Jacobian/Projective?) diff --git a/curve-definitions/test/ed25519.test.js b/curve-definitions/test/ed25519.test.js index 4dc360a..e0e1da9 100644 --- a/curve-definitions/test/ed25519.test.js +++ b/curve-definitions/test/ed25519.test.js @@ -1,11 +1,12 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; import * as fc from 'fast-check'; -import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/ed25519.js'; +import { ed25519, ed25519ctx, ed25519ph, x25519, RistrettoPoint } from '../lib/ed25519.js'; import { readFileSync } from 'fs'; import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' }; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { numberToBytesLE } from '@noble/curves/utils'; +import { sha512 } from '@noble/hashes/sha512'; import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' }; import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' }; @@ -242,18 +243,17 @@ should('rfc8032 vectors/should create right signature for 0xf5 and long msg', () ); }); -// const { RistrettoPoint } = ed; -// // const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn; -// // const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103])); -// // prettier-ignore -// // const MESSAGE = new Uint8Array([ -// // 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58, -// // ]); -// // const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103])); -// // prettier-ignore -// // const WRONG_MESSAGE = new Uint8Array([ -// // 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28, -// // ]); +// const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn; +// const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103])); +// prettier-ignore +// const MESSAGE = new Uint8Array([ +// 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58, +// ]); +// const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103])); +// prettier-ignore +// const WRONG_MESSAGE = new Uint8Array([ +// 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28, +// ]); // // it("should verify just signed message", async () => { // // await fc.assert(fc.asyncProperty( // // fc.hexa(), @@ -300,104 +300,104 @@ should('rfc8032 vectors/should create right signature for 0xf5 and long msg', () // // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY); // // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false); // // }); -// should('ristretto255/should follow the byte encodings of small multiples', () => { -// const encodingsOfSmallMultiples = [ -// // This is the identity point -// '0000000000000000000000000000000000000000000000000000000000000000', -// // This is the basepoint -// 'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76', -// // These are small multiples of the basepoint -// '6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919', -// '94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259', -// 'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57', -// 'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e', -// 'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403', -// '44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d', -// '903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c', -// '02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031', -// '20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f', -// 'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42', -// 'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460', -// 'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f', -// '46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e', -// 'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e', -// ]; -// let B = RistrettoPoint.BASE; -// let P = RistrettoPoint.ZERO; -// for (const encoded of encodingsOfSmallMultiples) { -// deepStrictEqual(P.toHex(), encoded); -// deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded); -// P = P.add(B); -// } -// }); -// should('ristretto255/should not convert bad bytes encoding', () => { -// const badEncodings = [ -// // These are all bad because they're non-canonical field encodings. -// '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', -// 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', -// 'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', -// 'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', -// // These are all bad because they're negative field elements. -// '0100000000000000000000000000000000000000000000000000000000000000', -// '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', -// 'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20', -// 'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562', -// 'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78', -// '47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24', -// 'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72', -// '87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309', -// // These are all bad because they give a nonsquare x^2. -// '26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371', -// '4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f', -// 'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b', -// 'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042', -// '2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08', -// 'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22', -// '8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731', -// '2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b', -// // These are all bad because they give a negative xy value. -// '3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e', -// 'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220', -// 'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e', -// '8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32', -// '32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b', -// '227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165', -// '5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e', -// '445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b', -// // This is s = -1, which causes y = 0. -// 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', -// ]; -// for (const badBytes of badEncodings) { -// const b = hexToBytes(badBytes); -// throws(() => RistrettoPoint.fromHex(b)); -// } -// }); -// should('ristretto255/should create right points from uniform hash', async () => { -// const labels = [ -// 'Ristretto is traditionally a short shot of espresso coffee', -// 'made with the normal amount of ground coffee but extracted with', -// 'about half the amount of water in the same amount of time', -// 'by using a finer grind.', -// 'This produces a concentrated shot of coffee per volume.', -// 'Just pulling a normal shot short will produce a weaker shot', -// 'and is not a Ristretto as some believe.', -// ]; -// const encodedHashToPoints = [ -// '3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46', -// 'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b', -// '006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826', -// 'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a', -// 'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179', -// 'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628', -// '80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065', -// ]; +should('ristretto255/should follow the byte encodings of small multiples', () => { + const encodingsOfSmallMultiples = [ + // This is the identity point + '0000000000000000000000000000000000000000000000000000000000000000', + // This is the basepoint + 'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76', + // These are small multiples of the basepoint + '6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919', + '94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259', + 'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57', + 'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e', + 'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403', + '44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d', + '903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c', + '02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031', + '20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f', + 'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42', + 'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460', + 'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f', + '46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e', + 'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e', + ]; + let B = RistrettoPoint.BASE; + let P = RistrettoPoint.ZERO; + for (const encoded of encodingsOfSmallMultiples) { + deepStrictEqual(P.toHex(), encoded); + deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded); + P = P.add(B); + } +}); +should('ristretto255/should not convert bad bytes encoding', () => { + const badEncodings = [ + // These are all bad because they're non-canonical field encodings. + '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', + 'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', + 'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', + // These are all bad because they're negative field elements. + '0100000000000000000000000000000000000000000000000000000000000000', + '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', + 'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20', + 'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562', + 'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78', + '47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24', + 'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72', + '87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309', + // These are all bad because they give a nonsquare x^2. + '26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371', + '4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f', + 'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b', + 'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042', + '2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08', + 'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22', + '8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731', + '2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b', + // These are all bad because they give a negative xy value. + '3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e', + 'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220', + 'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e', + '8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32', + '32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b', + '227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165', + '5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e', + '445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b', + // This is s = -1, which causes y = 0. + 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', + ]; + for (const badBytes of badEncodings) { + const b = hexToBytes(badBytes); + throws(() => RistrettoPoint.fromHex(b), badBytes); + } +}); +should('ristretto255/should create right points from uniform hash', async () => { + const labels = [ + 'Ristretto is traditionally a short shot of espresso coffee', + 'made with the normal amount of ground coffee but extracted with', + 'about half the amount of water in the same amount of time', + 'by using a finer grind.', + 'This produces a concentrated shot of coffee per volume.', + 'Just pulling a normal shot short will produce a weaker shot', + 'and is not a Ristretto as some believe.', + ]; + const encodedHashToPoints = [ + '3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46', + 'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b', + '006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826', + 'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a', + 'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179', + 'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628', + '80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065', + ]; -// for (let i = 0; i < labels.length; i++) { -// const hash = sha512(utf8ToBytes(labels[i])); -// const point = RistrettoPoint.hashToCurve(hash); -// deepStrictEqual(point.toHex(), encodedHashToPoints[i]); -// } -// }); + for (let i = 0; i < labels.length; i++) { + const hash = sha512(utf8ToBytes(labels[i])); + const point = RistrettoPoint.hashToCurve(hash); + deepStrictEqual(point.toHex(), encodedHashToPoints[i]); + } +}); should('input immutability: sign/verify are immutable', () => { const privateKey = ed.utils.randomPrivateKey(); diff --git a/curve-definitions/test/secp256k1.test.js b/curve-definitions/test/secp256k1.test.js index a2b6798..699e1be 100644 --- a/curve-definitions/test/secp256k1.test.js +++ b/curve-definitions/test/secp256k1.test.js @@ -1,5 +1,5 @@ import * as fc from 'fast-check'; -import { secp256k1 } from '../lib/secp256k1.js'; +import { secp256k1, schnorr } from '../lib/secp256k1.js'; import { readFileSync } from 'fs'; import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' }; import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' }; @@ -341,37 +341,25 @@ should( } ); -// describe('schnorr', () => { -// // index,secret key,public key,aux_rand,message,signature,verification result,comment -// const vectors = schCsv -// .split('\n') -// .map((line: string) => line.split(',')) -// .slice(1, -1); -// for (let vec of vectors) { -// const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec; -// it(`should sign with Schnorr scheme vector ${index}`, async () => { -// if (sec) { -// expect(hex(secp.schnorr.getPublicKey(sec))).toBe(pub.toLowerCase()); -// const sig = await secp.schnorr.sign(msg, sec, rnd); -// const sigS = secp.schnorr.signSync(msg, sec, rnd); -// expect(hex(sig)).toBe(expSig.toLowerCase()); -// expect(hex(sigS)).toBe(expSig.toLowerCase()); -// expect(await secp.schnorr.verify(sigS, msg, pub)).toBe(true); -// expect(secp.schnorr.verifySync(sig, msg, pub)).toBe(true); -// } else { -// const passed = await secp.schnorr.verify(expSig, msg, pub); -// const passedS = secp.schnorr.verifySync(expSig, msg, pub); -// if (passes === 'TRUE') { -// expect(passed).toBeTruthy(); -// expect(passedS).toBeTruthy(); -// } else { -// expect(passed).toBeFalsy(); -// expect(passedS).toBeFalsy(); -// } -// } -// }); -// } -// }); +// index,secret key,public key,aux_rand,message,signature,verification result,comment +const vectors = schCsv + .split('\n') + .map((line) => line.split(',')) + .slice(1, -1); +for (let vec of vectors) { + const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec; + should(`sign with Schnorr scheme vector ${index}`, () => { + if (sec) { + deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase()); + const sig = schnorr.sign(msg, sec, rnd); + deepStrictEqual(hex(sig), expSig.toLowerCase()); + deepStrictEqual(schnorr.verify(sig, msg, pub), true); + } else { + const passed = schnorr.verify(expSig, msg, pub); + deepStrictEqual(passed, passes === 'TRUE'); + } + }); +} should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', () => { const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; diff --git a/curve-definitions/test/stark/benchmark/index.js b/curve-definitions/test/stark/benchmark/index.js index e6ca058..ba54240 100644 --- a/curve-definitions/test/stark/benchmark/index.js +++ b/curve-definitions/test/stark/benchmark/index.js @@ -1,4 +1,4 @@ -import * as microStark from '../../../lib/starknet.js'; +import * as microStark from '../../../lib/stark.js'; import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils'; import * as bench from 'micro-bmark'; const { run, mark } = bench; // or bench.mark diff --git a/curve-definitions/test/stark/index.test.js b/curve-definitions/test/stark/index.test.js index 4a83616..7dbc77b 100644 --- a/curve-definitions/test/stark/index.test.js +++ b/curve-definitions/test/stark/index.test.js @@ -1,3 +1,5 @@ + import './basic.test.js'; import './stark.test.js'; import './property.test.js'; + diff --git a/src/edwards.ts b/src/edwards.ts index c5b0444..a7646de 100644 --- a/src/edwards.ts +++ b/src/edwards.ts @@ -2,8 +2,8 @@ // Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² // Differences from @noble/ed25519 1.7: -// 1. EDDSA & ECDH have different field element lengths (for ed448/x448 only): -// RFC8032 bitLength is 456 bits (57 bytes), RFC7748 bitLength is 448 (56 bytes) +// 1. Different field element lengths in ed448: +// EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes // 2. Different addition formula (doubling is same) // 3. uvRatio differs between curves (half-expected, not only pow fn changes) // 4. Point decompression code is different too (unexpected), now using generalized formula @@ -19,6 +19,8 @@ import { hashToPrivateScalar, BasicCurve, validateOpts as utilOpts, + Hex, + PrivKey, } from './utils.js'; // TODO: import * as u from './utils.js'? import { Group, GroupConstructor, wNAF } from './group.js'; @@ -48,11 +50,6 @@ export type CurveType = BasicCurve & { preHash?: CHash; }; -// We accept hex strings besides Uint8Array for simplicity -type Hex = Uint8Array | string; -// Very few implementations accept numbers, we do it to ease learning curve -type PrivKey = Hex | bigint | number; - // Should be separate from overrides, since overrides can use information about curve (for example nBits) function validateOpts(curve: CurveType) { const opts = utilOpts(curve); @@ -260,7 +257,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { const { a, d } = CURVE; const { x: X1, y: Y1, z: Z1, t: T1 } = this; const { x: X2, y: Y2, z: Z2, t: T2 } = other; - // Faster algo for adding 2 Extended Points when curve's a=-1. // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-4 // Cost: 8M + 8add + 2*2. @@ -520,7 +516,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } } - // ------------------------- // Little-endian SHA512 with modulo n function modlLE(hash: Uint8Array): bigint { return mod.mod(bytesToNumberLE(hash), CURVE_ORDER); diff --git a/src/utils.ts b/src/utils.ts index 9d97452..03c5e78 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,10 @@ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -export type Hex = string | Uint8Array; + +// We accept hex strings besides Uint8Array for simplicity +export type Hex = Uint8Array | string; +// Very few implementations accept numbers, we do it to ease learning curve +export type PrivKey = Hex | bigint | number; + // NOTE: these are generic, even if curve is on some polynominal field (bls), it will still have P/n/h // But generator can be different (Fp2/Fp6 for bls?) export type BasicCurve = { @@ -124,6 +129,7 @@ export function nLength(n: bigint, nBitLength?: number) { * Can take (n+8) 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. + * https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/ * @param hash hash output from sha512, or a similar function * @returns valid private scalar */ @@ -137,3 +143,10 @@ export function hashToPrivateScalar(hash: Hex, CURVE_ORDER: bigint, isLE = false const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash); return mod.mod(num, CURVE_ORDER - _1n) + _1n; } + +export function equalBytes(b1: Uint8Array, b2: Uint8Array) { + // We don't care about timing attacks here + if (b1.length !== b2.length) return false; + for (let i = 0; i < b1.length; i++) if (b1[i] !== b2[i]) return false; + return true; +} diff --git a/src/weierstrass.ts b/src/weierstrass.ts index 88461bf..a4bea5a 100644 --- a/src/weierstrass.ts +++ b/src/weierstrass.ts @@ -21,6 +21,8 @@ import { hashToPrivateScalar, BasicCurve, validateOpts as utilOpts, + Hex, + PrivKey, } from './utils.js'; import { Group, GroupConstructor, wNAF } from './group.js'; @@ -54,11 +56,6 @@ export type CurveType = BasicCurve & { endo?: EndomorphismOpts; }; -// We accept hex strings besides Uint8Array for simplicity -type Hex = Uint8Array | string; -// Very few implementations accept numbers, we do it to ease learning curve -type PrivKey = Hex | bigint | number; - // Should be separate from overrides, since overrides can use information about curve (for example nBits) function validateOpts(curve: CurveType) { const opts = utilOpts(curve); @@ -244,6 +241,13 @@ export type CurveFn = { utils: { mod: (a: bigint, b?: bigint) => bigint; invert: (number: bigint, modulo?: bigint) => bigint; + _bigintToBytes: (num: bigint) => Uint8Array; + _bigintToString: (num: bigint) => string; + _normalizePrivateKey: (key: PrivKey) => bigint; + _normalizePublicKey: (publicKey: PubKey) => PointType; + _isWithinCurveOrder: (num: bigint) => boolean; + _isValidFieldElement: (num: bigint) => boolean; + _weierstrassEquation: (x: bigint) => bigint; isValidPrivateKey(privateKey: PrivKey): boolean; hashToPrivateKey: (hash: Hex) => Uint8Array; randomPrivateKey: () => Uint8Array; @@ -649,7 +653,6 @@ export function weierstrass(curveDef: CurveType): CurveFn { } } const wnaf = wNAF(JacobianPoint, CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength); - // Stores precomputed values for points. const pointPrecomputes = new WeakMap(); @@ -923,20 +926,22 @@ export function weierstrass(curveDef: CurveType): CurveFn { } }, _bigintToBytes: numToField, + _bigintToString: numToFieldStr, _normalizePrivateKey: normalizePrivateKey, + _normalizePublicKey: normalizePublicKey, + _isWithinCurveOrder: isWithinCurveOrder, + _isValidFieldElement: isValidFieldElement, + _weierstrassEquation: weierstrassEquation, /** - * Can take (keyLength + 8) or more bytes of uniform input e.g. from CSPRNG or KDF - * and convert them into private key, with the modulo bias being neglible. - * As per FIPS 186 B.4.1. - * https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/ - * @param hash hash output from sha512, or a similar function - * @returns valid private key + * Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes. */ hashToPrivateKey: (hash: Hex): Uint8Array => numToField(hashToPrivateScalar(hash, CURVE_ORDER)), - // Takes curve order + 64 bits from CSPRNG so that modulo bias is neglible, - // matches FIPS 186 B.4.1. + /** + * Produces cryptographically secure private key from random of size (nBitLength+64) + * as per FIPS 186 B.4.1 with modulo bias being neglible. + */ randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(CURVE.randomBytes(fieldLen + 8)), /**