diff --git a/curve-definitions/benchmark/index.js b/curve-definitions/benchmark/index.js index cb018ab..b35f0fc 100644 --- a/curve-definitions/benchmark/index.js +++ b/curve-definitions/benchmark/index.js @@ -1,8 +1,9 @@ import * as bench from 'micro-bmark'; const { run, mark } = bench; // or bench.mark // Curves -import * as nist from '../lib/nist.js'; -import * as ed from '../lib/ed.js'; +import { secp256k1 } from '../lib/secp256k1.js'; +import { ed25519 } from '../lib/ed25519.js'; +import { ed448 } from '../lib/ed448.js'; // Others import { hmac } from '@noble/hashes/hmac'; @@ -24,10 +25,9 @@ noble_secp256k1.utils.hmacSha256Sync = (key, ...msgs) => .digest(); import * as noble_ed25519 from '@noble/ed25519'; - -nist.secp256k1.utils.precompute(8); // Not enabled by default? -ed.ed25519.utils.precompute(8); -ed.ed448.utils.precompute(8); +secp256k1.utils.precompute(8); // Not enabled by default? +ed25519.utils.precompute(8); +ed448.utils.precompute(8); noble_ed25519.utils.sha512Sync = (...m) => sha512(concatBytes(...m)); noble_secp256k1.utils.precompute(8); @@ -48,19 +48,18 @@ export const CURVES = { }, getPublicKey: { samples: 10000, - old: () => - noble_secp256k1.getPublicKey(noble_secp256k1.utils.randomPrivateKey()), - noble: () => nist.secp256k1.getPublicKey(nist.secp256k1.utils.randomPrivateKey()), + old: () => noble_secp256k1.getPublicKey(noble_secp256k1.utils.randomPrivateKey()), + noble: () => secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey()), }, sign: { samples: 5000, old: ({ msg, priv }) => noble_secp256k1.signSync(msg, priv), - noble: ({ msg, priv }) => nist.secp256k1.sign(msg, priv), + noble: ({ msg, priv }) => secp256k1.sign(msg, priv), }, getSharedSecret: { samples: 1000, old: ({ pub, priv }) => noble_secp256k1.getSharedSecret(priv, pub), - noble: ({ pub, priv }) => nist.secp256k1.getSharedSecret(priv, pub), + noble: ({ pub, priv }) => secp256k1.getSharedSecret(priv, pub), }, }, ed25519: { @@ -77,21 +76,20 @@ export const CURVES = { }, getPublicKey: { samples: 10000, - old: () => - noble_ed25519.sync.getPublicKey(noble_ed25519.utils.randomPrivateKey()), - noble: () => ed.ed25519.getPublicKey(ed.ed25519.utils.randomPrivateKey()), - 'ed448': () => ed.ed448.getPublicKey(ed.ed448.utils.randomPrivateKey()), + old: () => noble_ed25519.sync.getPublicKey(noble_ed25519.utils.randomPrivateKey()), + noble: () => ed25519.getPublicKey(ed25519.utils.randomPrivateKey()), + ed448: () => ed448.getPublicKey(ed448.utils.randomPrivateKey()), }, sign: { samples: 5000, old: ({ msg, priv }) => noble_ed25519.sync.sign(msg, priv), - noble: ({ msg, priv }) => ed.ed25519.sign(msg, priv), - 'ed448': () => ed.ed448.sign(ed.ed448.utils.randomPrivateKey(), ed.ed448.utils.randomPrivateKey()), + noble: ({ msg, priv }) => ed25519.sign(msg, priv), + ed448: () => ed448.sign(ed448.utils.randomPrivateKey(), ed448.utils.randomPrivateKey()), }, verify: { samples: 1000, old: ({ msg, pub, sig }) => noble_ed25519.sync.verify(sig, msg, pub), - noble: ({ msg, pub, sig }) => ed.ed25519.verify(sig, msg, pub), + noble: ({ msg, pub, sig }) => ed25519.verify(sig, msg, pub), }, }, }; diff --git a/curve-definitions/src/_shortw_utils.ts b/curve-definitions/src/_shortw_utils.ts new file mode 100644 index 0000000..b4968af --- /dev/null +++ b/curve-definitions/src/_shortw_utils.ts @@ -0,0 +1,17 @@ +import { hmac } from '@noble/hashes/hmac'; +import { concatBytes, randomBytes } from '@noble/hashes/utils'; +import { weierstrass, CurveType, CHash } from '@noble/curves/weierstrass'; + +export function getHash(hash: CHash) { + return { + hash, + hmac: (key: Uint8Array, ...msgs: Uint8Array[]) => hmac(hash, key, concatBytes(...msgs)), + randomBytes, + }; +} +// Same API as @noble/hashes, with ability to create curve with custom hash +type CurveDef = Readonly>; +export function createCurve(curveDef: CurveDef, defHash: CHash) { + const create = (hash: CHash) => weierstrass({ ...curveDef, ...getHash(hash) }); + return Object.freeze({ ...create(defHash), create }); +} diff --git a/curve-definitions/src/bn.ts b/curve-definitions/src/bn.ts index 1a51a27..1f36b45 100644 --- a/curve-definitions/src/bn.ts +++ b/curve-definitions/src/bn.ts @@ -1,19 +1,11 @@ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import { weierstrass, CHash } from '@noble/curves/weierstrass'; -import { concatBytes, randomBytes } from '@noble/hashes/utils'; -import { hmac } from '@noble/hashes/hmac'; +import { weierstrass } from '@noble/curves/weierstrass'; import { sha256 } from '@noble/hashes/sha256'; - -function getHash(hash: CHash) { - return { - hash, - hmac: (key: Uint8Array, ...msgs: Uint8Array[]) => hmac(hash, key, concatBytes(...msgs)), - randomBytes, - }; -} +import { getHash } from './_shortw_utils.js'; // Was known as alt_bn128 when it had 128-bit security. Now that it's much lower, the naming // has been changed to its prime bit count. +// https://neuromancer.sk/std/bn/bn254 export const bn254 = weierstrass({ a: 0n, b: 3n, @@ -21,5 +13,6 @@ export const bn254 = weierstrass({ n: 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001n, Gx: 1n, Gy: 2n, + h: BigInt(1), ...getHash(sha256), }); diff --git a/curve-definitions/src/ed25519.ts b/curve-definitions/src/ed25519.ts index ea9af62..220482e 100644 --- a/curve-definitions/src/ed25519.ts +++ b/curve-definitions/src/ed25519.ts @@ -1,3 +1,4 @@ +/*! @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'; diff --git a/curve-definitions/src/ed448.ts b/curve-definitions/src/ed448.ts index 5fda47b..e23200a 100644 --- a/curve-definitions/src/ed448.ts +++ b/curve-definitions/src/ed448.ts @@ -1,3 +1,4 @@ +/*! @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 { PointType, twistedEdwards } from '@noble/curves/edwards'; @@ -43,7 +44,8 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array { bytes[56] = 0; // Byte outside of group (456 buts vs 448 bits) return bytes; } - +// Edwards448 from RFC 8032 (https://www.rfc-editor.org/rfc/rfc8032.html#section-5.2). +// NOTE: Ed448-Goldilocks is different curve const ED448_DEF = { // Param: a a: BigInt(1), diff --git a/curve-definitions/src/jubjub.ts b/curve-definitions/src/jubjub.ts new file mode 100644 index 0000000..d3702ae --- /dev/null +++ b/curve-definitions/src/jubjub.ts @@ -0,0 +1,52 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { sha256 } from '@noble/hashes/sha256'; +import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; +import { twistedEdwards } from '@noble/curves/edwards'; +import { blake2s } from '@noble/hashes/blake2s'; + +// https://neuromancer.sk/std/other/JubJub +export const jubjub = twistedEdwards({ + // Params: a, d + a: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000000'), + d: BigInt('0x2a9318e74bfa2b48f5fd9207e6bd7fd4292d7f6d37579d2601065fd6d6343eb1'), + // Finite field 𝔽p over which we'll do calculations + P: BigInt('0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001'), + // Subgroup order: how many points ed25519 has + // 2n ** 252n + 27742317777372353535851937790883648493n; + n: BigInt('0xe7db4ea6533afa906673b0101343b00a6682093ccc81082d0970e5ed6f72cb7'), + // Cofactor + h: BigInt(8), + // Base point (x, y) aka generator point + Gx: BigInt('0x11dafe5d23e1218086a365b99fbf3d3be72f6afd7d1f72623e6b071492d1122b'), + Gy: BigInt('0x1d523cf1ddab1a1793132e78c866c0c33e26ba5cc220fed7cc3f870e59d292aa'), + hash: sha256, + randomBytes, +} as const); + +const GH_FIRST_BLOCK = utf8ToBytes( + '096b36a5804bfacef1691e173c366a47ff5ba84a44f26ddd7e8d9f79d5b42df0' +); + +// Returns point at JubJub curve which is prime order and not zero +export function groupHash(tag: Uint8Array, personalization: Uint8Array) { + const h = blake2s.create({ personalization, dkLen: 32 }); + h.update(GH_FIRST_BLOCK); + h.update(tag); + // NOTE: returns ExtendedPoint, in case it will be multiplied later + let p = jubjub.ExtendedPoint.fromAffine(jubjub.Point.fromHex(h.digest())); + // NOTE: cannot replace with isSmallOrder, returns Point*8 + p = p.multiply(jubjub.CURVE.h); + if (p.equals(jubjub.ExtendedPoint.ZERO)) throw new Error('Point has small order'); + return p; +} + +export function findGroupHash(m: Uint8Array, personalization: Uint8Array) { + const tag = concatBytes(m, new Uint8Array([0])); + for (let i = 0; i < 256; i++) { + tag[tag.length - 1] = i; + try { + return groupHash(tag, personalization); + } catch (e) {} + } + throw new Error('findGroupHash tag overflow'); +} diff --git a/curve-definitions/src/nist.ts b/curve-definitions/src/nist.ts deleted file mode 100644 index 3ebe312..0000000 --- a/curve-definitions/src/nist.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import { hmac } from '@noble/hashes/hmac'; -import { sha256 } from '@noble/hashes/sha256'; -import { sha384, sha512 } from '@noble/hashes/sha512'; -import { concatBytes, randomBytes } from '@noble/hashes/utils'; -import { weierstrass, CurveType, CHash } from '@noble/curves/weierstrass'; -import { mod, pow2 } from '@noble/curves/modular'; - -function getHash(hash: CHash) { - return { - hash, - hmac: (key: Uint8Array, ...msgs: Uint8Array[]) => hmac(hash, key, concatBytes(...msgs)), - randomBytes, - }; -} -// Same API as @noble/hashes, with ability to create curve with custom hash -type CurveDef = Readonly>; -function createCurve(curveDef: CurveDef, defHash: CHash) { - const create = (hash: CHash) => weierstrass({ ...curveDef, ...getHash(hash) }); - return Object.freeze({ ...create(defHash), create }); -} - -// https://www.secg.org/sec2-v2.pdf -// https://neuromancer.sk/std/secg/secp192r1 -export const P192 = createCurve( - { - // Params: a, b - a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'), - b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'), - // Field over which we'll do calculations. Verify with: 2n ** 192n - 2n ** 64n - 1n - P: BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff'), - // Curve order, total count of valid points in the field. Verify with: - n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'), - // Base point (x, y) aka generator point - Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'), - Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'), - lowS: false, - } as const, - sha256 -); -export const secp192r1 = P192; -// https://neuromancer.sk/std/nist/P-224 -export const P224 = createCurve( - { - // Params: a, b - a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'), - b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'), - // Field over which we'll do calculations. Verify with: - P: 2n ** 224n - 2n ** 96n + 1n, - // Curve order, total count of valid points in the field. Verify with: - n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'), - // Base point (x, y) aka generator point - Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'), - Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'), - lowS: false, - } as const, - sha256 // TODO: replace with sha224 when new @noble/hashes released -); -export const secp224r1 = P224; -// https://neuromancer.sk/std/nist/P-256 -export const P256 = createCurve( - { - // Params: a, b - a: BigInt('0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc'), - b: BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b'), - // Field over which we'll do calculations. Verify with: - // 2n ** 224n * (2n ** 32n - 1n) + 2n ** 192n + 2n ** 96n - 1n, - P: BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff'), - // Curve order, total count of valid points in the field. Verify with: - n: BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'), - // Base point (x, y) aka generator point - Gx: BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296'), - Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'), - lowS: false, - } as const, - sha256 -); -export const secp256r1 = P256; -// https://neuromancer.sk/std/nist/P-384 -// prettier-ignore -export const P384 = createCurve( - { - // Params: a, b - a: BigInt( - '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc' - ), - b: BigInt( - '0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef' - ), - // Field over which we'll do calculations. Verify with: - // 2n ** 384n - 2n ** 128n - 2n ** 96n + 2n ** 32n - 1n - P: BigInt( - '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff' - ), - // Curve order, total count of valid points in the field. Verify with: - n: BigInt( - '0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973' - ), - // Base point (x, y) aka generator point - Gx: BigInt( - '0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7' - ), - Gy: BigInt( - '0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f' - ), - lowS: false, - } as const, - sha384 -); -export const secp384r1 = P384; -// https://neuromancer.sk/std/nist/P-521 -// prettier-ignore -export const P521 = createCurve({ - // Params: a, b - a: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc'), - b: BigInt('0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00'), - // Field over which we'll do calculations. Verify with: - // 2n ** 521n - 1n, - P: BigInt('0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), - // Curve order, total count of valid points in the field. Verify with: - n: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'), - // Base point (x, y) aka generator point - Gx: BigInt('0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66'), - Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'), - lowS: false, -} as const, sha512); -export const secp521r1 = P521; - -/** - * secp256k1 definition with efficient square root and endomorphism. - * Endomorphism works only for Koblitz curves with a == 0. - * It improves efficiency: - * Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%. - * Should always be used for Jacobian's double-and-add multiplication. - * For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit. - * https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 - */ -const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); -const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); -const _1n = BigInt(1); -const _2n = BigInt(2); -const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; -export const secp256k1 = createCurve( - { - a: 0n, - b: 7n, - // Field over which we'll do calculations. Verify with: - // 2n ** 256n - 2n ** 32n - 2n ** 9n - 2n ** 8n - 2n ** 7n - 2n ** 6n - 2n ** 4n - 1n - P: secp256k1P, - // Curve order, total count of valid points in the field. Verify with: - n: secp256k1N, - // Base point (x, y) aka generator point - Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'), - Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'), - // noble-secp256k1 compat - lowS: true, - // Used to calculate y - the square root of y². - // Exponentiates it to very big number (P+1)/4. - // 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); - }, - endo: { - beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), - splitScalar: (k: bigint) => { - const n = secp256k1N; - const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15'); - const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'); - const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'); - const b2 = a1; - const POW_2_128 = BigInt('0x100000000000000000000000000000000'); - - const c1 = divNearest(b2 * k, n); - const c2 = divNearest(-b1 * k, n); - let k1 = mod(k - c1 * a1 - c2 * a2, n); - let k2 = mod(-c1 * b1 - c2 * b2, n); - const k1neg = k1 > POW_2_128; - const k2neg = k2 > POW_2_128; - if (k1neg) k1 = n - k1; - if (k2neg) k2 = n - k2; - if (k1 > POW_2_128 || k2 > POW_2_128) { - throw new Error('splitScalar: Endomorphism failed, k=' + k); - } - return { k1neg, k1, k2neg, k2 }; - }, - }, - }, - sha256 -); diff --git a/curve-definitions/src/p192.ts b/curve-definitions/src/p192.ts new file mode 100644 index 0000000..5ccda46 --- /dev/null +++ b/curve-definitions/src/p192.ts @@ -0,0 +1,24 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { createCurve } from './_shortw_utils.js'; +import { sha256 } from '@noble/hashes/sha256'; + +// https://www.secg.org/sec2-v2.pdf +// https://neuromancer.sk/std/secg/secp192r1 +export const P192 = createCurve( + { + // Params: a, b + a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffc'), + b: BigInt('0x64210519e59c80e70fa7e9ab72243049feb8deecc146b9b1'), + // Field over which we'll do calculations. Verify with: 2n ** 192n - 2n ** 64n - 1n + P: BigInt('0xfffffffffffffffffffffffffffffffeffffffffffffffff'), + // Curve order, total count of valid points in the field. Verify with: + n: BigInt('0xffffffffffffffffffffffff99def836146bc9b1b4d22831'), + // Base point (x, y) aka generator point + Gx: BigInt('0x188da80eb03090f67cbf20eb43a18800f4ff0afd82ff1012'), + Gy: BigInt('0x07192b95ffc8da78631011ed6b24cdd573f977a11e794811'), + h: BigInt(1), + lowS: false, + } as const, + sha256 +); +export const secp192r1 = P192; diff --git a/curve-definitions/src/p224.ts b/curve-definitions/src/p224.ts new file mode 100644 index 0000000..7e76153 --- /dev/null +++ b/curve-definitions/src/p224.ts @@ -0,0 +1,24 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { createCurve } from './_shortw_utils.js'; +import { sha256 } from '@noble/hashes/sha256'; + +// https://www.secg.org/sec2-v2.pdf +// https://neuromancer.sk/std/nist/P-224 +export const P224 = createCurve( + { + // Params: a, b + a: BigInt('0xfffffffffffffffffffffffffffffffefffffffffffffffffffffffe'), + b: BigInt('0xb4050a850c04b3abf54132565044b0b7d7bfd8ba270b39432355ffb4'), + // Field over which we'll do calculations. Verify with: + P: 2n ** 224n - 2n ** 96n + 1n, + // Curve order, total count of valid points in the field. Verify with: + n: BigInt('0xffffffffffffffffffffffffffff16a2e0b8f03e13dd29455c5c2a3d'), + // Base point (x, y) aka generator point + Gx: BigInt('0xb70e0cbd6bb4bf7f321390b94a03c1d356c21122343280d6115c1d21'), + Gy: BigInt('0xbd376388b5f723fb4c22dfe6cd4375a05a07476444d5819985007e34'), + h: BigInt(1), + lowS: false, + } as const, + sha256 // TODO: replace with sha224 when new @noble/hashes released +); +export const secp224r1 = P224; diff --git a/curve-definitions/src/p256.ts b/curve-definitions/src/p256.ts new file mode 100644 index 0000000..6a2898e --- /dev/null +++ b/curve-definitions/src/p256.ts @@ -0,0 +1,25 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { createCurve } from './_shortw_utils.js'; +import { sha256 } from '@noble/hashes/sha256'; + +// https://www.secg.org/sec2-v2.pdf +// https://neuromancer.sk/std/nist/P-256 +export const P256 = createCurve( + { + // Params: a, b + a: BigInt('0xffffffff00000001000000000000000000000000fffffffffffffffffffffffc'), + b: BigInt('0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b'), + // Field over which we'll do calculations. Verify with: + // 2n ** 224n * (2n ** 32n - 1n) + 2n ** 192n + 2n ** 96n - 1n, + P: BigInt('0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff'), + // Curve order, total count of valid points in the field. Verify with: + n: BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'), + // Base point (x, y) aka generator point + Gx: BigInt('0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296'), + Gy: BigInt('0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5'), + h: BigInt(1), + lowS: false, + } as const, + sha256 +); +export const secp256r1 = P256; diff --git a/curve-definitions/src/p384.ts b/curve-definitions/src/p384.ts new file mode 100644 index 0000000..3afef03 --- /dev/null +++ b/curve-definitions/src/p384.ts @@ -0,0 +1,23 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { createCurve } from './_shortw_utils.js'; +import { sha384 } from '@noble/hashes/sha512'; + +// https://www.secg.org/sec2-v2.pdf +// https://neuromancer.sk/std/nist/P-384 +// prettier-ignore +export const P384 = createCurve({ + // Params: a, b + a: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000fffffffc'), + b: BigInt('0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875ac656398d8a2ed19d2a85c8edd3ec2aef'), + // Field over which we'll do calculations. Verify with: + // 2n ** 384n - 2n ** 128n - 2n ** 96n + 2n ** 32n - 1n + P: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffff'), + // Curve order, total count of valid points in the field. Verify with: + n: BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'), + // Base point (x, y) aka generator point + Gx: BigInt('0xaa87ca22be8b05378eb1c71ef320ad746e1d3b628ba79b9859f741e082542a385502f25dbf55296c3a545e3872760ab7'), + Gy: BigInt('0x3617de4a96262c6f5d9e98bf9292dc29f8f41dbd289a147ce9da3113b5f0b8c00a60b1ce1d7e819d7a431d7c90ea0e5f'), + h: BigInt(1), + lowS: false, +} as const, sha384); +export const secp384r1 = P384; diff --git a/curve-definitions/src/p521.ts b/curve-definitions/src/p521.ts new file mode 100644 index 0000000..25847e2 --- /dev/null +++ b/curve-definitions/src/p521.ts @@ -0,0 +1,23 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { createCurve } from './_shortw_utils.js'; +import { sha512 } from '@noble/hashes/sha512'; + +// https://www.secg.org/sec2-v2.pdf +// https://neuromancer.sk/std/nist/P-521 +// prettier-ignore +export const P521 = createCurve({ + // Params: a, b + a: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc'), + b: BigInt('0x0051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00'), + // Field over which we'll do calculations. Verify with: + // 2n ** 521n - 1n, + P: BigInt('0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), + // Curve order, total count of valid points in the field. Verify with: + n: BigInt('0x01fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'), + // Base point (x, y) aka generator point + Gx: BigInt('0x00c6858e06b70404e9cd9e3ecb662395b4429c648139053fb521f828af606b4d3dbaa14b5e77efe75928fe1dc127a2ffa8de3348b3c1856a429bf97e7e31c2e5bd66'), + Gy: BigInt('0x011839296a789a3bc0045c8a5fb42c7d1bd998f54449579b446817afbd17273e662c97ee72995ef42640c550b9013fad0761353c7086a272c24088be94769fd16650'), + h: BigInt(1), + lowS: false, +} as const, sha512); +export const secp521r1 = P521; diff --git a/curve-definitions/src/pasta.ts b/curve-definitions/src/pasta.ts index 8d0d37c..427e297 100644 --- a/curve-definitions/src/pasta.ts +++ b/curve-definitions/src/pasta.ts @@ -1,35 +1,31 @@ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; -import { concatBytes, randomBytes } from '@noble/hashes/utils'; -import { weierstrass, CHash } from '@noble/curves/weierstrass'; +import { weierstrass } from '@noble/curves/weierstrass'; +import { getHash } from './_shortw_utils.js'; +import * as mod from '@noble/curves/modular'; -function getHash(hash: CHash) { - return { - hash, - hmac: (key: Uint8Array, ...msgs: Uint8Array[]) => hmac(hash, key, concatBytes(...msgs)), - randomBytes, - }; -} const p = BigInt('0x40000000000000000000000000000000224698fc094cf91b992d30ed00000001'); const q = BigInt('0x40000000000000000000000000000000224698fc0994a8dd8c46eb2100000001'); +// https://neuromancer.sk/std/other/Pallas export const pallas = weierstrass({ a: BigInt(0), b: BigInt(5), P: p, n: q, - Gx: BigInt(-1), + Gx: mod.mod(BigInt(-1), p), Gy: BigInt(2), + h: BigInt(1), ...getHash(sha256), }); - +// https://neuromancer.sk/std/other/Vesta export const vesta = weierstrass({ a: BigInt(0), b: BigInt(5), P: q, n: p, - Gx: BigInt(-1), + Gx: mod.mod(BigInt(-1), q), Gy: BigInt(2), + h: BigInt(1), ...getHash(sha256), }); diff --git a/curve-definitions/src/secp256k1.ts b/curve-definitions/src/secp256k1.ts new file mode 100644 index 0000000..5df8089 --- /dev/null +++ b/curve-definitions/src/secp256k1.ts @@ -0,0 +1,90 @@ +/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ +import { sha256 } from '@noble/hashes/sha256'; +import { mod, pow2 } from '@noble/curves/modular'; +import { createCurve } from './_shortw_utils.js'; + +/** + * secp256k1 definition with efficient square root and endomorphism. + * Endomorphism works only for Koblitz curves with a == 0. + * It improves efficiency: + * Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%. + * Should always be used for Jacobian's double-and-add multiplication. + * For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit. + * https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 + */ +const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'); +const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'); +const _1n = BigInt(1); +const _2n = BigInt(2); +const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; +export const secp256k1 = createCurve( + { + a: 0n, + b: 7n, + // Field over which we'll do calculations. Verify with: + // 2n ** 256n - 2n ** 32n - 2n ** 9n - 2n ** 8n - 2n ** 7n - 2n ** 6n - 2n ** 4n - 1n + P: secp256k1P, + // Curve order, total count of valid points in the field. Verify with: + n: secp256k1N, + // Base point (x, y) aka generator point + Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'), + Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'), + h: BigInt(1), + // noble-secp256k1 compat + lowS: true, + // Used to calculate y - the square root of y². + // Exponentiates it to very big number (P+1)/4. + // 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); + }, + endo: { + beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), + splitScalar: (k: bigint) => { + const n = secp256k1N; + const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15'); + const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'); + const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'); + const b2 = a1; + const POW_2_128 = BigInt('0x100000000000000000000000000000000'); + + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = mod(k - c1 * a1 - c2 * a2, n); + let k2 = mod(-c1 * b1 - c2 * b2, n); + const k1neg = k1 > POW_2_128; + const k2neg = k2 > POW_2_128; + if (k1neg) k1 = n - k1; + if (k2neg) k2 = n - k2; + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error('splitScalar: Endomorphism failed, k=' + k); + } + return { k1neg, k1, k2neg, k2 }; + }, + }, + }, + sha256 +); diff --git a/curve-definitions/src/starknet.ts b/curve-definitions/src/stark.ts similarity index 99% rename from curve-definitions/src/starknet.ts rename to curve-definitions/src/stark.ts index d554cd5..c33d676 100644 --- a/curve-definitions/src/starknet.ts +++ b/curve-definitions/src/stark.ts @@ -30,6 +30,7 @@ export const starkCurve = weierstrass({ // Base point (x, y) aka generator point Gx: 874739451078007766457464989774322083649278607533249481151382481072868806602n, Gy: 152666792071518830868575557812948353041420400780739481342941381225525861407n, + h: BigInt(1), // Default options lowS: false, ...getHash(sha256), diff --git a/curve-definitions/test/basic.test.js b/curve-definitions/test/basic.test.js index b7f0a81..3910225 100644 --- a/curve-definitions/test/basic.test.js +++ b/curve-definitions/test/basic.test.js @@ -1,341 +1,312 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; -import * as nist from '../lib/nist.js'; -import { hexToBytes, bytesToHex } from '@noble/curves/utils'; -import { default as ecdsa } from './wycheproof/ecdsa_test.json' assert { type: 'json' }; -import { default as ecdh } from './wycheproof/ecdh_test.json' assert { type: 'json' }; +import * as fc from 'fast-check'; +import * as mod from '@noble/curves/modular'; +import { randomBytes } from '@noble/hashes/utils'; +// Generic tests for all curves in package +import { secp192r1 } from '../lib/p192.js'; +import { secp224r1 } from '../lib/p224.js'; +import { secp256r1 } from '../lib/p256.js'; +import { secp384r1 } from '../lib/p384.js'; +import { secp521r1 } from '../lib/p521.js'; +import { secp256k1 } from '../lib/secp256k1.js'; +import { ed25519, ed25519ctx, ed25519ph } from '../lib/ed25519.js'; +import { ed448, ed448ph } from '../lib/ed448.js'; +import { starkCurve } from '../lib/stark.js'; +import { pallas, vesta } from '../lib/pasta.js'; +import { bn254 } from '../lib/bn.js'; +import { jubjub } from '../lib/jubjub.js'; -const hex = bytesToHex; - -should('Curve Fields', () => { - const vectors = { - secp192r1: 0xfffffffffffffffffffffffffffffffeffffffffffffffffn, - secp224r1: 0xffffffffffffffffffffffffffffffff000000000000000000000001n, - secp256r1: 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn, - secp256k1: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn, - secp384r1: - 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffffn, - secp521r1: - 0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, - }; - for (const n in vectors) deepStrictEqual(nist[n].CURVE.P, vectors[n]); -}); - -should('wychenproof ECDSA vectors', () => { - for (const group of ecdsa.testGroups) { - // Tested in secp256k1.test.js - if (group.key.curve === 'secp256k1') continue; - // We don't have SHA-224 - if (group.key.curve === 'secp224r1' && group.sha === 'SHA-224') continue; - const CURVE = nist[group.key.curve]; - if (!CURVE) continue; - const pubKey = CURVE.Point.fromHex(group.key.uncompressed); - deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); - deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); - for (const test of group.tests) { - if (['Hash weaker than DL-group'].includes(test.comment)) { - continue; - } - const m = CURVE.CURVE.hash(hexToBytes(test.msg)); - if (test.result === 'valid' || test.result === 'acceptable') { - try { - CURVE.Signature.fromDER(test.sig); - } catch (e) { - // Some test has invalid signature which we don't accept - if (e.message.includes('Invalid signature: incorrect length')) continue; - throw e; - } - const verified = CURVE.verify(test.sig, m, pubKey); - deepStrictEqual(verified, true, 'valid'); - } else if (test.result === 'invalid') { - let failed = false; - try { - failed = !CURVE.verify(test.sig, m, pubKey); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, 'invalid'); - } else throw new Error('unknown test result'); - } - } -}); - -should('wychenproof ECDH vectors', () => { - for (const group of ecdh.testGroups) { - // // Tested in secp256k1.test.js - // if (group.key.curve === 'secp256k1') continue; - // We don't have SHA-224 - const CURVE = nist[group.curve]; - if (!CURVE) continue; - for (const test of group.tests) { - if (test.result === 'valid' || test.result === 'acceptable') { - try { - const pub = CURVE.Point.fromHex(test.public); - } catch (e) { - if (e.message.includes('Point.fromHex: received invalid point.')) continue; - throw e; - } - const shared = CURVE.getSharedSecret(test.private, test.public); - deepStrictEqual(shared, test.shared, 'valid'); - } else if (test.result === 'invalid') { - let failed = false; - try { - CURVE.getSharedSecret(test.private, test.public); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, 'invalid'); - } else throw new Error('unknown test result'); - } - } -}); - -import { default as ecdh_secp224r1_test } from './wycheproof/ecdh_secp224r1_test.json' assert { type: 'json' }; -import { default as ecdh_secp256r1_test } from './wycheproof/ecdh_secp256r1_test.json' assert { type: 'json' }; -import { default as ecdh_secp256k1_test } from './wycheproof/ecdh_secp256k1_test.json' assert { type: 'json' }; -import { default as ecdh_secp384r1_test } from './wycheproof/ecdh_secp384r1_test.json' assert { type: 'json' }; -import { default as ecdh_secp521r1_test } from './wycheproof/ecdh_secp521r1_test.json' assert { type: 'json' }; - -// More per curve tests -const WYCHEPROOF_ECDH = { - P224: { - curve: nist.P224, - tests: [ecdh_secp224r1_test], - }, - P256: { - curve: nist.P256, - tests: [ecdh_secp256r1_test], - }, - secp256k1: { - curve: nist.secp256k1, - tests: [ecdh_secp256k1_test], - }, - P384: { - curve: nist.P384, - tests: [ecdh_secp384r1_test], - }, - P521: { - curve: nist.P521, - tests: [ecdh_secp521r1_test], - }, +// prettier-ignore +const CURVES = { + secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1, + ed25519, ed25519ctx, ed25519ph, + ed448, ed448ph, + starkCurve, + pallas, vesta, + bn254, + jubjub, }; -for (const name in WYCHEPROOF_ECDH) { - const { curve, tests } = WYCHEPROOF_ECDH[name]; - for (let i = 0; i < tests.length; i++) { - const test = tests[i]; - for (let j = 0; j < test.testGroups.length; j++) { - const group = test.testGroups[j]; - should(`Wycheproof/ECDH ${name} (${i}/${j})`, () => { - for (const test of group.tests) { - if (test.result === 'valid' || test.result === 'acceptable') { - try { - const pub = curve.Point.fromHex(test.public); - } catch (e) { - if (e.message.includes('Point.fromHex: received invalid point.')) continue; - throw e; - } - const shared = curve.getSharedSecret(test.private, test.public); - deepStrictEqual(hex(shared), test.shared, 'valid'); - } else if (test.result === 'invalid') { - let failed = false; - try { - curve.getSharedSecret(test.private, test.public); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, 'invalid'); - } else throw new Error('unknown test result'); +const NUM_RUNS = 5; +const getXY = (p) => ({ x: p.x, y: p.y }); + +function equal(a, b, comment) { + deepStrictEqual(a.equals(b), true, `eq(${comment})`); + if (a.toAffine && b.toAffine) { + deepStrictEqual(getXY(a.toAffine()), getXY(b.toAffine()), `eqToAffine(${comment})`); + } else if (!a.toAffine && !b.toAffine) { + // Already affine + deepStrictEqual(getXY(a), getXY(b), `eqAffine(${comment})`); + } else throw new Error('Different point types'); +} + +for (const name in CURVES) { + const C = CURVES[name]; + 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, + }; + for (const pointName in POINTS) { + const p = POINTS[pointName]; + const o = OTHER_POINTS[pointName]; + if (!p) continue; + + const G = [p.ZERO, p.BASE]; + for (let i = 2; i < 10; i++) G.push(G[1].multiply(i)); + // Here we check basic group laws, to verify that points works as group + should(`${name}/${pointName}/Basic group laws (zero)`, () => { + equal(G[0].double(), G[0], '(0*G).double() = 0'); + equal(G[0].add(G[0]), G[0], '0*G + 0*G = 0'); + equal(G[0].subtract(G[0]), G[0], '0*G - 0*G = 0'); + equal(G[0].negate(), G[0], '-0 = 0'); + for (let i = 0; i < G.length; i++) { + const p = G[i]; + equal(p, p.add(G[0]), `${i}*G + 0 = ${i}*G`); + equal(G[0].multiply(i + 1), G[0], `${i + 1}*0 = 0`); + } + }); + should(`${name}/${pointName}/Basic group laws (one)`, () => { + equal(G[1].double(), G[2], '(1*G).double() = 2*G'); + equal(G[1].subtract(G[1]), G[0], '1*G - 1*G = 0'); + equal(G[1].add(G[1]), G[2], '1*G + 1*G = 2*G'); + }); + should(`${name}/${pointName}/Basic group laws (sanity tests)`, () => { + equal(G[2].double(), G[4], `(2*G).double() = 4*G`); + equal(G[2].add(G[2]), G[4], `2*G + 2*G = 4*G`); + equal(G[7].add(G[3].negate()), G[4], `7*G - 3*G = 4*G`); + }); + should(`${name}/${pointName}/Basic group laws (addition commutativity)`, () => { + equal(G[4].add(G[3]), G[3].add(G[4]), `4*G + 3*G = 3*G + 4*G`); + equal(G[4].add(G[3]), G[3].add(G[2]).add(G[2]), `4*G + 3*G = 3*G + 2*G + 2*G`); + }); + should(`${name}/${pointName}/Basic group laws (double)`, () => { + equal(G[3].double(), G[6], '(3*G).double() = 6*G'); + }); + should(`${name}/${pointName}/Basic group laws (multiply)`, () => { + equal(G[2].multiply(3), G[6], '(2*G).multiply(3) = 6*G'); + }); + should(`${name}/${pointName}/Basic group laws (same point addition)`, () => { + equal(G[3].add(G[3]), G[6], `3*G + 3*G = 6*G`); + }); + should(`${name}/${pointName}/Basic group laws (same point (negative) addition)`, () => { + equal(G[3].add(G[3].negate()), G[0], '3*G + (- 3*G) = 0*G'); + equal(G[3].subtract(G[3]), G[0], '3*G - 3*G = 0*G'); + }); + should(`${name}/${pointName}/Basic group laws (curve order)`, () => { + equal(G[1].multiply(CURVE_ORDER - 1n).add(G[1]), G[0], '(N-1)*G + G = 0'); + equal(G[1].multiply(CURVE_ORDER - 1n).add(G[2]), G[1], '(N-1)*G + 2*G = 1*G'); + equal(G[1].multiply(CURVE_ORDER - 2n).add(G[2]), G[0], '(N-2)*G + 2*G = 0'); + const half = CURVE_ORDER / 2n; + const carry = CURVE_ORDER % 2n === 1n ? G[1] : G[0]; + equal(G[1].multiply(half).double().add(carry), G[0], '((N/2) * G).double() = 0'); + }); + should(`${name}/${pointName}/Basic group laws (inversion)`, () => { + const a = 1234n; + const b = 5678n; + const c = a * b; + equal(G[1].multiply(a).multiply(b), G[1].multiply(c), 'a*b*G = c*G'); + const inv = mod.invert(b, CURVE_ORDER); + equal(G[1].multiply(c).multiply(inv), G[1].multiply(a), 'c*G * (1/b)*G = a*G'); + }); + should(`${name}/${pointName}/Basic group laws (multiply, rand)`, () => + fc.assert( + fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { + const c = mod.mod(a + b, CURVE_ORDER); + const pA = G[1].multiply(a); + const pB = G[1].multiply(b); + const pC = G[1].multiply(c); + equal(pA.add(pB), pB.add(pA), `pA + pB = pB + pA`); + equal(pA.add(pB), pC, `pA + pB = pC`); + }), + { numRuns: NUM_RUNS } + ) + ); + should(`${name}/${pointName}/Basic group laws (multiply2, rand)`, () => + fc.assert( + fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { + const c = mod.mod(a * b, CURVE_ORDER); + const pA = G[1].multiply(a); + const pB = G[1].multiply(b); + equal(pA.multiply(b), pB.multiply(a), `b*pA = a*pB`); + equal(pA.multiply(b), G[1].multiply(c), `b*pA = c*G`); + }), + { numRuns: NUM_RUNS } + ) + ); + + for (const op of ['add', 'subtract']) { + should(`${name}/${pointName}/${op} type check`, () => { + throws(() => G[1][op](0), '0'); + throws(() => G[1][op](0n), '0n'); + G[1][op](G[2]); + throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER'); + throws(() => G[1][op](123.456), '123.456'); + throws(() => G[1][op](true), 'true'); + throws(() => G[1][op]('1'), "'1'"); + throws(() => G[1][op]({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }'); + throws(() => G[1][op](new Uint8Array([])), 'ui8a([])'); + 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])'); + if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), `Point ${op} ${pointName}`); + throws(() => G[1][op](O.BASE), `${op}/other curve point`); + }); + } + + should(`${name}/${pointName}/equals type check`, () => { + throws(() => G[1].equals(0), '0'); + throws(() => G[1].equals(0n), '0n'); + deepStrictEqual(G[1].equals(G[2]), false, '1*G != 2*G'); + deepStrictEqual(G[1].equals(G[1]), true, '1*G == 1*G'); + deepStrictEqual(G[2].equals(G[2]), true, '2*G == 2*G'); + throws(() => G[1].equals(CURVE_ORDER), 'CURVE_ORDER'); + throws(() => G[1].equals(123.456), '123.456'); + throws(() => G[1].equals(true), 'true'); + throws(() => G[1].equals('1'), "'1'"); + throws(() => G[1].equals({ x: 1n, y: 1n, z: 1n, t: 1n }), '{ x: 1n, y: 1n, z: 1n, t: 1n }'); + throws(() => G[1].equals(new Uint8Array([])), 'ui8a([])'); + throws(() => G[1].equals(new Uint8Array([0])), 'ui8a([0])'); + 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'); + }); + + for (const op of ['multiply', 'multiplyUnsafe']) { + if (!p.BASE[op]) continue; + should(`${name}/${pointName}/${op} type check`, () => { + if (op !== 'multiplyUnsafe') { + throws(() => G[1][op](0), '0'); + throws(() => G[1][op](0n), '0n'); } + G[1][op](1n); + G[1][op](CURVE_ORDER - 1n); + throws(() => G[1][op](G[2]), 'G[2]'); + throws(() => G[1][op](CURVE_ORDER), 'CURVE_ORDER'); + throws(() => G[1][op](CURVE_ORDER + 1n), 'CURVE_ORDER+1'); + throws(() => G[1][op](123.456), '123.456'); + throws(() => G[1][op](true), 'true'); + throws(() => G[1][op]('1'), '1'); + throws(() => G[1][op](new Uint8Array([])), 'ui8a([])'); + 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'); + }); + } + // Complex point (Extended/Jacobian/Projective?) + if (p.BASE.toAffine) { + should(`${name}/${pointName}/toAffine()`, () => { + equal(p.ZERO.toAffine(), C.Point.ZERO, `0 = 0`); + equal(p.BASE.toAffine(), C.Point.BASE, `1 = 1`); + }); + } + if (p.fromAffine) { + should(`${name}/${pointName}/fromAffine()`, () => { + equal(p.ZERO, p.fromAffine(C.Point.ZERO), `0 = 0`); + equal(p.BASE, p.fromAffine(C.Point.BASE), `1 = 1`); + }); + } + // toHex/fromHex (if available) + if (p.fromHex && p.BASE.toHex) { + should(`${name}/${pointName}/fromHex(toHex()) roundtrip`, () => { + fc.assert( + fc.property(FC_BIGINT, (x) => { + const hex = p.BASE.multiply(x).toHex(); + deepStrictEqual(p.fromHex(hex).toHex(), hex); + }) + ); }); } } -} + // Generic complex things (getPublicKey/sign/verify/getSharedSecret) + should(`${name}/getPublicKey type check`, () => { + throws(() => C.getPublicKey(0), '0'); + throws(() => C.getPublicKey(0n), '0n'); + throws(() => C.getPublicKey(false), 'false'); + throws(() => C.getPublicKey(123.456), '123.456'); + throws(() => C.getPublicKey(true), 'true'); + throws(() => C.getPublicKey(''), "''"); + // NOTE: passes because of disabled hex padding checks for starknet, maybe enable? + //throws(() => C.getPublicKey('1'), "'1'"); + throws(() => C.getPublicKey('key'), "'key'"); + throws(() => C.getPublicKey(new Uint8Array([]))); + throws(() => C.getPublicKey(new Uint8Array([0]))); + throws(() => C.getPublicKey(new Uint8Array([1]))); + throws(() => C.getPublicKey(new Uint8Array(4096).fill(1))); + }); + should(`${name}.verify()/should verify random signatures`, () => + fc.assert( + fc.property(fc.hexaString({ minLength: 64, maxLength: 64 }), (msg) => { + const priv = C.utils.randomPrivateKey(); + const pub = C.getPublicKey(priv); + const sig = C.sign(msg, priv); + deepStrictEqual(C.verify(sig, msg, pub), true); + }), + { numRuns: NUM_RUNS } + ) + ); + should(`${name}.sign()/edge cases`, () => { + throws(() => C.sign()); + throws(() => C.sign('')); + }); -// Tests with custom hashes -import { default as secp224r1_sha224_test } from './wycheproof/ecdsa_secp224r1_sha224_test.json' assert { type: 'json' }; -import { default as secp224r1_sha256_test } from './wycheproof/ecdsa_secp224r1_sha256_test.json' assert { type: 'json' }; -import { default as secp224r1_sha3_224_test } from './wycheproof/ecdsa_secp224r1_sha3_224_test.json' assert { type: 'json' }; -import { default as secp224r1_sha3_256_test } from './wycheproof/ecdsa_secp224r1_sha3_256_test.json' assert { type: 'json' }; -import { default as secp224r1_sha3_512_test } from './wycheproof/ecdsa_secp224r1_sha3_512_test.json' assert { type: 'json' }; -import { default as secp224r1_sha512_test } from './wycheproof/ecdsa_secp224r1_sha512_test.json' assert { type: 'json' }; + should(`${name}.verify()/should not verify signature with wrong hash`, () => { + const MSG = '01'.repeat(32); + const PRIV_KEY = 0x2n; + const WRONG_MSG = '11'.repeat(32); + const signature = C.sign(MSG, PRIV_KEY); + const publicKey = C.getPublicKey(PRIV_KEY); + deepStrictEqual(C.verify(signature, WRONG_MSG, publicKey), false); + }); + // NOTE: fails for ed, because of empty message. Since we convert it to scalar, + // need to check what other implementations do. Empty message != new Uint8Array([0]), but what scalar should be in that case? + // should(`${name}/should not verify signature with wrong message`, () => { + // fc.assert( + // fc.property( + // fc.array(fc.integer({ min: 0x00, max: 0xff })), + // fc.array(fc.integer({ min: 0x00, max: 0xff })), + // (bytes, wrongBytes) => { + // const privKey = C.utils.randomPrivateKey(); + // const message = new Uint8Array(bytes); + // const wrongMessage = new Uint8Array(wrongBytes); + // const publicKey = C.getPublicKey(privKey); + // const signature = C.sign(message, privKey); + // deepStrictEqual( + // C.verify(signature, wrongMessage, publicKey), + // bytes.toString() === wrongBytes.toString() + // ); + // } + // ), + // { numRuns: NUM_RUNS } + // ); + // }); -import { default as secp256k1_sha256_test } from './wycheproof/ecdsa_secp256k1_sha256_test.json' assert { type: 'json' }; -import { default as secp256k1_sha3_256_test } from './wycheproof/ecdsa_secp256k1_sha3_256_test.json' assert { type: 'json' }; -import { default as secp256k1_sha3_512_test } from './wycheproof/ecdsa_secp256k1_sha3_512_test.json' assert { type: 'json' }; -import { default as secp256k1_sha512_test } from './wycheproof/ecdsa_secp256k1_sha512_test.json' assert { type: 'json' }; - -import { default as secp256r1_sha256_test } from './wycheproof/ecdsa_secp256r1_sha256_test.json' assert { type: 'json' }; -import { default as secp256r1_sha3_256_test } from './wycheproof/ecdsa_secp256r1_sha3_256_test.json' assert { type: 'json' }; -import { default as secp256r1_sha3_512_test } from './wycheproof/ecdsa_secp256r1_sha3_512_test.json' assert { type: 'json' }; -import { default as secp256r1_sha512_test } from './wycheproof/ecdsa_secp256r1_sha512_test.json' assert { type: 'json' }; - -import { default as secp384r1_sha384_test } from './wycheproof/ecdsa_secp384r1_sha384_test.json' assert { type: 'json' }; -import { default as secp384r1_sha3_384_test } from './wycheproof/ecdsa_secp384r1_sha3_384_test.json' assert { type: 'json' }; -import { default as secp384r1_sha3_512_test } from './wycheproof/ecdsa_secp384r1_sha3_512_test.json' assert { type: 'json' }; -import { default as secp384r1_sha512_test } from './wycheproof/ecdsa_secp384r1_sha512_test.json' assert { type: 'json' }; - -import { default as secp521r1_sha3_512_test } from './wycheproof/ecdsa_secp521r1_sha3_512_test.json' assert { type: 'json' }; -import { default as secp521r1_sha512_test } from './wycheproof/ecdsa_secp521r1_sha512_test.json' assert { type: 'json' }; - -import { sha3_224, sha3_256, sha3_384, sha3_512 } from '@noble/hashes/sha3'; -import { sha512, sha384 } from '@noble/hashes/sha512'; -import { sha256 } from '@noble/hashes/sha256'; - -const WYCHEPROOF_ECDSA = { - P224: { - curve: nist.P224, - hashes: { - // sha224 not released yet - // sha224: { - // hash: sha224, - // tests: [secp224r1_sha224_test], - // }, - sha256: { - hash: sha256, - tests: [secp224r1_sha256_test], - }, - sha3_224: { - hash: sha3_224, - tests: [secp224r1_sha3_224_test], - }, - sha3_256: { - hash: sha3_256, - tests: [secp224r1_sha3_256_test], - }, - sha3_512: { - hash: sha3_512, - tests: [secp224r1_sha3_512_test], - }, - sha512: { - hash: sha512, - tests: [secp224r1_sha512_test], - }, - }, - }, - secp256k1: { - curve: nist.secp256k1, - hashes: { - // TODO: debug why fails, can be bug - // sha256: { - // hash: sha256, - // tests: [secp256k1_sha256_test], - // }, - // sha3_256: { - // hash: sha3_256, - // tests: [secp256k1_sha3_256_test], - // }, - // sha3_512: { - // hash: sha3_512, - // tests: [secp256k1_sha3_512_test], - // }, - // sha512: { - // hash: sha512, - // tests: [secp256k1_sha512_test], - // }, - }, - }, - P256: { - curve: nist.P256, - hashes: { - sha256: { - hash: sha256, - tests: [secp256r1_sha256_test], - }, - sha3_256: { - hash: sha3_256, - tests: [secp256r1_sha3_256_test], - }, - sha3_512: { - hash: sha3_512, - tests: [secp256r1_sha3_512_test], - }, - sha512: { - hash: sha512, - tests: [secp256r1_sha512_test], - }, - }, - }, - P384: { - curve: nist.P384, - hashes: { - sha384: { - hash: sha384, - tests: [secp384r1_sha384_test], - }, - sha3_384: { - hash: sha3_384, - tests: [secp384r1_sha3_384_test], - }, - sha3_512: { - hash: sha3_512, - tests: [secp384r1_sha3_512_test], - }, - sha512: { - hash: sha512, - tests: [secp384r1_sha512_test], - }, - }, - }, - P521: { - curve: nist.P521, - hashes: { - sha3_512: { - hash: sha3_512, - tests: [secp521r1_sha3_512_test], - }, - sha512: { - hash: sha512, - tests: [secp521r1_sha512_test], - }, - }, - }, -}; - -for (const name in WYCHEPROOF_ECDSA) { - const { curve, hashes } = WYCHEPROOF_ECDSA[name]; - for (const hName in hashes) { - const { hash, tests } = hashes[hName]; - const CURVE = curve.create(hash); - for (let i = 0; i < tests.length; i++) { - const test = tests[i]; - for (let j = 0; j < test.testGroups.length; j++) { - const group = test.testGroups[j]; - should(`Wycheproof/WYCHEPROOF_ECDSA ${name}/${hName} (${i}/${j})`, () => { - const pubKey = CURVE.Point.fromHex(group.key.uncompressed); - deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); - deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); - for (const test of group.tests) { - // if (['Hash weaker than DL-group'].includes(test.comment)) { - // continue; - // } - const m = CURVE.CURVE.hash(hexToBytes(test.msg)); - if (test.result === 'valid' || test.result === 'acceptable') { - try { - CURVE.Signature.fromDER(test.sig); - } catch (e) { - // Some test has invalid signature which we don't accept - if (e.message.includes('Invalid signature: incorrect length')) continue; - throw e; - } - const verified = CURVE.verify(test.sig, m, pubKey); - deepStrictEqual(verified, true, 'valid'); - } else if (test.result === 'invalid') { - let failed = false; - try { - failed = !CURVE.verify(test.sig, m, pubKey); - } catch (error) { - failed = true; - } - deepStrictEqual(failed, true, 'invalid'); - } else throw new Error('unknown test result'); - } - }); + if (C.getSharedSecret) { + should(`${name}/getSharedSecret() should be commutative`, () => { + for (let i = 0; i < NUM_RUNS; i++) { + const asec = C.utils.randomPrivateKey(); + const apub = C.getPublicKey(asec); + const bsec = C.utils.randomPrivateKey(); + const bpub = C.getPublicKey(bsec); + try { + deepStrictEqual(C.getSharedSecret(asec, bpub), C.getSharedSecret(bsec, apub)); + } catch (error) { + console.error('not commutative', { asec, apub, bsec, bpub }); + throw error; + } } - } + }); } } - // ESM is broken. import url from 'url'; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { diff --git a/curve-definitions/test/ed25519.test.js b/curve-definitions/test/ed25519.test.js index b1b2ed6..4dc360a 100644 --- a/curve-definitions/test/ed25519.test.js +++ b/curve-definitions/test/ed25519.test.js @@ -5,6 +5,7 @@ import { ed25519, ed25519ctx, ed25519ph, x25519 } 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 { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' }; import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' }; @@ -434,7 +435,6 @@ should('ZIP-215 compliance tests/should pass all of them', () => { }); should('ZIP-215 compliance tests/disallows sig.s >= CURVE.n', () => { const sig = new ed.Signature(ed.Point.BASE, 1n); - // @ts-ignore sig.s = ed.CURVE.n + 1n; throws(() => ed.verify(sig, 'deadbeef', ed.Point.BASE)); }); @@ -646,6 +646,12 @@ for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) { }); } +should('X25519 base point', () => { + const { y } = ed25519.Point.BASE; + const u = ed25519.utils.mod((y + 1n) * ed25519.utils.invert(1n - y, ed25519.CURVE.P)); + deepStrictEqual(hex(numberToBytesLE(u, 32)), x25519.Gu); +}); + // ESM is broken. import url from 'url'; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { diff --git a/curve-definitions/test/ed448.test.js b/curve-definitions/test/ed448.test.js index 5dc2942..d0c4fad 100644 --- a/curve-definitions/test/ed448.test.js +++ b/curve-definitions/test/ed448.test.js @@ -3,6 +3,7 @@ import { should } from 'micro-should'; import * as fc from 'fast-check'; import { ed448, ed448ph, x448 } from '../lib/ed448.js'; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; +import { numberToBytesLE } from '@noble/curves/utils'; import { default as ed448vectors } from './wycheproof/ed448_test.json' assert { type: 'json' }; import { default as x448vectors } from './wycheproof/x448_test.json' assert { type: 'json' }; @@ -646,6 +647,14 @@ for (let i = 0; i < VECTORS_RFC8032_PH.length; i++) { }); } +should('X448 base point', () => { + const { x, y } = ed448.Point.BASE; + const { P } = ed448.CURVE; + const invX = ed448.utils.invert(x * x, P); // x^2 + const u = ed448.utils.mod(y * y * invX, P); // (y^2/x^2) + deepStrictEqual(hex(numberToBytesLE(u, 56)), x448.Gu); +}); + // ESM is broken. import url from 'url'; if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { diff --git a/curve-definitions/test/generic.test.js b/curve-definitions/test/generic.test.js deleted file mode 100644 index 0c847b9..0000000 --- a/curve-definitions/test/generic.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { deepStrictEqual, throws } from 'assert'; -import { should } from 'micro-should'; -import * as fc from 'fast-check'; -// Generic tests for all curves in package -import { secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1 } from '../lib/nist.js'; -import { ed25519, ed25519ctx, ed25519ph, ed448, ed448ph } from '../lib/ed.js'; -import { starkCurve } from '../lib/starknet.js'; -import { pallas, vesta } from '../lib/pasta.js'; -import { bn254 } from '../lib/bn.js'; - -// prettier-ignore -const CURVES = { - secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1, - ed25519, ed25519ctx, ed25519ph, ed448, ed448ph, - starkCurve, - pallas, vesta, - bn254, -}; - -for (const name in CURVES) { - const C = CURVES[name]; - // Generic sanity tests: - // - group laws: - // G*(CURVE.n-1) + 1 = Point.ZERO - // G*(CURVE.n-2) + 2 = Point.ZERO - // G*(CURVE.n/2).double() = Point.ZERO or Point.BASE? - // rand*G + rand2*G = G*(rand+rand mod N) - // - double works - // ZERO.double() == zero - // - adding zero point works - // - add(samePoint) works - // - add(-samePoint) works - // - 2+2 = 2.double() = 7-5 (should have different Z coordinates, but it is same point) - // ToAffine: Point.BASE = Extended/Jacobian.toAffine() - // Property tests: - // signatures, getSharedKey/etc - //const FC_BIGINT = fc.bigInt(1n + 1n, C.n - 1n); - - should(`${name}/Basic`, () => {}); - const POINTS = { Point: C.Point, JacobianPoint: C.JacobianPoint, ExtendedPoint: C.ExtendedPoint }; - for (const pointName in POINTS) { - const p = POINTS[pointName]; - if (!p) continue; - - const G = [p.ZERO, p.BASE]; - for (let i = 2; i < 10; i++) G.push(G[1].multiply(i)); - should(`${name}/${pointName}/Basic`, () => { - // ... And we dont have it - //deepStrictEqual(G[2].double().equals(G[4]), true); - //console.log('Z', G); - }); - } -} -// ESM is broken. -import url from 'url'; -if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { - should.run(); -} diff --git a/curve-definitions/test/index.test.js b/curve-definitions/test/index.test.js index 0de02d3..02022a5 100644 --- a/curve-definitions/test/index.test.js +++ b/curve-definitions/test/index.test.js @@ -1,10 +1,12 @@ import { should } from 'micro-should'; +// Should be first to catch obvious things import './basic.test.js'; -import './rfc6979.test.js'; +import './nist.test.js'; import './ed448.test.js'; import './ed25519.test.js'; import './secp256k1.test.js'; -import './starknet/starknet.test.js'; +import './stark/stark.test.js'; +import './jubjub.test.js'; should.run(); diff --git a/curve-definitions/test/jubjub.test.js b/curve-definitions/test/jubjub.test.js new file mode 100644 index 0000000..2b12fcb --- /dev/null +++ b/curve-definitions/test/jubjub.test.js @@ -0,0 +1,74 @@ +import { jubjub, findGroupHash } from '../lib/jubjub.js'; +import { should } from 'micro-should'; +import { deepStrictEqual, throws } from 'assert'; +import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; + +const G_SPEND = new jubjub.ExtendedPoint( + 0x055f1f24f0f0512287e51c3c5a0a6903fc0baf8711de9eafd7c0e66f69d8d2dbn, + 0x566178b2505fdd52132a5007d80a04652842e78ffb376897588f406278214ed7n, + 0x0141fafa1f11088a3b2007c14d652375888f3b37838ba6bdffae096741ceddfen, + 0x12eada93c0b7d595f5f04f5ebfb4b7d033ef2884136475cab5e41ce17db5be9cn +); +const G_PROOF = new jubjub.ExtendedPoint( + 0x0174d54ce9fad258a2f8a86a1deabf15c7a2b51106b0fbcd9d29020f78936f71n, + 0x16871d6d877dcd222e4ec3bccb3f37cb1865a2d37dd3a5dcbc032a69b62b4445n, + 0x57a3cd31e496d82bd4aa78bd5ecd751cfb76d54a5d3f4560866379f9fc11c9b3n, + 0x42cc53f6b519d1f4f52c47ff1256463a616c2c2f49ffe77765481eca04c72081n +); + +const getXY = (p) => ({ x: p.x, y: p.y }); + +should('toHex/fromHex', () => { + // More than field + throws(() => + jubjub.Point.fromHex( + new Uint8Array([ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + ]) + ) + ); + // Multiplicative generator (sqrt == null), not on curve. + throws(() => + jubjub.Point.fromHex( + new Uint8Array([ + 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + ]) + ) + ); + const tmp = jubjub.Point.fromHex( + new Uint8Array([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, + ]) + ); + deepStrictEqual(tmp.x, 0x8d51ccce760304d0ec030002760300000001000000000000n); + deepStrictEqual(tmp.y, 0n); + + const S = G_SPEND.toAffine().toRawBytes(); + const S2 = G_SPEND.double().toAffine().toRawBytes(); + const P = G_PROOF.toAffine().toRawBytes(); + const P2 = G_PROOF.double().toAffine().toRawBytes(); + const S_exp = jubjub.Point.fromHex(S); + const S2_exp = jubjub.Point.fromHex(S2); + const P_exp = jubjub.Point.fromHex(P); + const P2_exp = jubjub.Point.fromHex(P2); + deepStrictEqual(getXY(G_SPEND.toAffine()), getXY(S_exp)); + deepStrictEqual(getXY(G_SPEND.double().toAffine()), getXY(S2_exp)); + deepStrictEqual(getXY(G_PROOF.toAffine()), getXY(P_exp)); + deepStrictEqual(getXY(G_PROOF.double().toAffine()), getXY(P2_exp)); +}); + +should('Find generators', () => { + const spend = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 71, 95])); + const proof = findGroupHash(new Uint8Array(), new Uint8Array([90, 99, 97, 115, 104, 95, 72, 95])); + deepStrictEqual(getXY(spend.toAffine()), getXY(G_SPEND.toAffine())); + deepStrictEqual(getXY(proof.toAffine()), getXY(G_PROOF.toAffine())); +}); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + should.run(); +} diff --git a/curve-definitions/test/nist.test.js b/curve-definitions/test/nist.test.js new file mode 100644 index 0000000..9fd57db --- /dev/null +++ b/curve-definitions/test/nist.test.js @@ -0,0 +1,381 @@ +import { deepStrictEqual, throws } from 'assert'; +import { should } from 'micro-should'; +import { secp192r1, P192 } from '../lib/p192.js'; +import { secp224r1, P224 } from '../lib/p224.js'; +import { secp256r1, P256 } from '../lib/p256.js'; +import { secp384r1, P384 } from '../lib/p384.js'; +import { secp521r1, P521 } from '../lib/p521.js'; +import { secp256k1 } from '../lib/secp256k1.js'; +import { hexToBytes, bytesToHex } from '@noble/curves/utils'; +import { default as ecdsa } from './wycheproof/ecdsa_test.json' assert { type: 'json' }; +import { default as ecdh } from './wycheproof/ecdh_test.json' assert { type: 'json' }; +import { default as rfc6979 } from './fixtures/rfc6979.json' assert { type: 'json' }; + +const hex = bytesToHex; + +// prettier-ignore +const NIST = { + secp192r1, P192, + secp224r1, P224, + secp256r1, P256, + secp384r1, P384, + secp521r1, P521, + secp256k1, +}; + +should('Curve Fields', () => { + const vectors = { + secp192r1: 0xfffffffffffffffffffffffffffffffeffffffffffffffffn, + secp224r1: 0xffffffffffffffffffffffffffffffff000000000000000000000001n, + secp256r1: 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn, + secp256k1: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn, + secp384r1: + 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeffffffff0000000000000000ffffffffn, + secp521r1: + 0x01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn, + }; + for (const n in vectors) deepStrictEqual(NIST[n].CURVE.P, vectors[n]); +}); + +should('wychenproof ECDSA vectors', () => { + for (const group of ecdsa.testGroups) { + // Tested in secp256k1.test.js + if (group.key.curve === 'secp256k1') continue; + // We don't have SHA-224 + if (group.key.curve === 'secp224r1' && group.sha === 'SHA-224') continue; + const CURVE = NIST[group.key.curve]; + if (!CURVE) continue; + const pubKey = CURVE.Point.fromHex(group.key.uncompressed); + deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); + deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); + for (const test of group.tests) { + if (['Hash weaker than DL-group'].includes(test.comment)) { + continue; + } + const m = CURVE.CURVE.hash(hexToBytes(test.msg)); + if (test.result === 'valid' || test.result === 'acceptable') { + try { + CURVE.Signature.fromDER(test.sig); + } catch (e) { + // Some test has invalid signature which we don't accept + if (e.message.includes('Invalid signature: incorrect length')) continue; + throw e; + } + const verified = CURVE.verify(test.sig, m, pubKey); + deepStrictEqual(verified, true, 'valid'); + } else if (test.result === 'invalid') { + let failed = false; + try { + failed = !CURVE.verify(test.sig, m, pubKey); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, 'invalid'); + } else throw new Error('unknown test result'); + } + } +}); + +should('wychenproof ECDH vectors', () => { + for (const group of ecdh.testGroups) { + // // Tested in secp256k1.test.js + // if (group.key.curve === 'secp256k1') continue; + // We don't have SHA-224 + const CURVE = NIST[group.curve]; + if (!CURVE) continue; + for (const test of group.tests) { + if (test.result === 'valid' || test.result === 'acceptable') { + try { + const pub = CURVE.Point.fromHex(test.public); + } catch (e) { + if (e.message.includes('Point.fromHex: received invalid point.')) continue; + throw e; + } + const shared = CURVE.getSharedSecret(test.private, test.public); + deepStrictEqual(shared, test.shared, 'valid'); + } else if (test.result === 'invalid') { + let failed = false; + try { + CURVE.getSharedSecret(test.private, test.public); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, 'invalid'); + } else throw new Error('unknown test result'); + } + } +}); + +import { default as ecdh_secp224r1_test } from './wycheproof/ecdh_secp224r1_test.json' assert { type: 'json' }; +import { default as ecdh_secp256r1_test } from './wycheproof/ecdh_secp256r1_test.json' assert { type: 'json' }; +import { default as ecdh_secp256k1_test } from './wycheproof/ecdh_secp256k1_test.json' assert { type: 'json' }; +import { default as ecdh_secp384r1_test } from './wycheproof/ecdh_secp384r1_test.json' assert { type: 'json' }; +import { default as ecdh_secp521r1_test } from './wycheproof/ecdh_secp521r1_test.json' assert { type: 'json' }; + +// More per curve tests +const WYCHEPROOF_ECDH = { + P224: { + curve: P224, + tests: [ecdh_secp224r1_test], + }, + P256: { + curve: P256, + tests: [ecdh_secp256r1_test], + }, + secp256k1: { + curve: secp256k1, + tests: [ecdh_secp256k1_test], + }, + P384: { + curve: P384, + tests: [ecdh_secp384r1_test], + }, + P521: { + curve: P521, + tests: [ecdh_secp521r1_test], + }, +}; + +for (const name in WYCHEPROOF_ECDH) { + const { curve, tests } = WYCHEPROOF_ECDH[name]; + for (let i = 0; i < tests.length; i++) { + const test = tests[i]; + for (let j = 0; j < test.testGroups.length; j++) { + const group = test.testGroups[j]; + should(`Wycheproof/ECDH ${name} (${i}/${j})`, () => { + for (const test of group.tests) { + if (test.result === 'valid' || test.result === 'acceptable') { + try { + const pub = curve.Point.fromHex(test.public); + } catch (e) { + if (e.message.includes('Point.fromHex: received invalid point.')) continue; + throw e; + } + const shared = curve.getSharedSecret(test.private, test.public); + deepStrictEqual(hex(shared), test.shared, 'valid'); + } else if (test.result === 'invalid') { + let failed = false; + try { + curve.getSharedSecret(test.private, test.public); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, 'invalid'); + } else throw new Error('unknown test result'); + } + }); + } + } +} + +// Tests with custom hashes +import { default as secp224r1_sha224_test } from './wycheproof/ecdsa_secp224r1_sha224_test.json' assert { type: 'json' }; +import { default as secp224r1_sha256_test } from './wycheproof/ecdsa_secp224r1_sha256_test.json' assert { type: 'json' }; +import { default as secp224r1_sha3_224_test } from './wycheproof/ecdsa_secp224r1_sha3_224_test.json' assert { type: 'json' }; +import { default as secp224r1_sha3_256_test } from './wycheproof/ecdsa_secp224r1_sha3_256_test.json' assert { type: 'json' }; +import { default as secp224r1_sha3_512_test } from './wycheproof/ecdsa_secp224r1_sha3_512_test.json' assert { type: 'json' }; +import { default as secp224r1_sha512_test } from './wycheproof/ecdsa_secp224r1_sha512_test.json' assert { type: 'json' }; + +import { default as secp256k1_sha256_test } from './wycheproof/ecdsa_secp256k1_sha256_test.json' assert { type: 'json' }; +import { default as secp256k1_sha3_256_test } from './wycheproof/ecdsa_secp256k1_sha3_256_test.json' assert { type: 'json' }; +import { default as secp256k1_sha3_512_test } from './wycheproof/ecdsa_secp256k1_sha3_512_test.json' assert { type: 'json' }; +import { default as secp256k1_sha512_test } from './wycheproof/ecdsa_secp256k1_sha512_test.json' assert { type: 'json' }; + +import { default as secp256r1_sha256_test } from './wycheproof/ecdsa_secp256r1_sha256_test.json' assert { type: 'json' }; +import { default as secp256r1_sha3_256_test } from './wycheproof/ecdsa_secp256r1_sha3_256_test.json' assert { type: 'json' }; +import { default as secp256r1_sha3_512_test } from './wycheproof/ecdsa_secp256r1_sha3_512_test.json' assert { type: 'json' }; +import { default as secp256r1_sha512_test } from './wycheproof/ecdsa_secp256r1_sha512_test.json' assert { type: 'json' }; + +import { default as secp384r1_sha384_test } from './wycheproof/ecdsa_secp384r1_sha384_test.json' assert { type: 'json' }; +import { default as secp384r1_sha3_384_test } from './wycheproof/ecdsa_secp384r1_sha3_384_test.json' assert { type: 'json' }; +import { default as secp384r1_sha3_512_test } from './wycheproof/ecdsa_secp384r1_sha3_512_test.json' assert { type: 'json' }; +import { default as secp384r1_sha512_test } from './wycheproof/ecdsa_secp384r1_sha512_test.json' assert { type: 'json' }; + +import { default as secp521r1_sha3_512_test } from './wycheproof/ecdsa_secp521r1_sha3_512_test.json' assert { type: 'json' }; +import { default as secp521r1_sha512_test } from './wycheproof/ecdsa_secp521r1_sha512_test.json' assert { type: 'json' }; + +import { sha3_224, sha3_256, sha3_384, sha3_512 } from '@noble/hashes/sha3'; +import { sha512, sha384 } from '@noble/hashes/sha512'; +import { sha256 } from '@noble/hashes/sha256'; + +const WYCHEPROOF_ECDSA = { + P224: { + curve: P224, + hashes: { + // sha224 not released yet + // sha224: { + // hash: sha224, + // tests: [secp224r1_sha224_test], + // }, + sha256: { + hash: sha256, + tests: [secp224r1_sha256_test], + }, + sha3_224: { + hash: sha3_224, + tests: [secp224r1_sha3_224_test], + }, + sha3_256: { + hash: sha3_256, + tests: [secp224r1_sha3_256_test], + }, + sha3_512: { + hash: sha3_512, + tests: [secp224r1_sha3_512_test], + }, + sha512: { + hash: sha512, + tests: [secp224r1_sha512_test], + }, + }, + }, + secp256k1: { + curve: secp256k1, + hashes: { + // TODO: debug why fails, can be bug + sha256: { + hash: sha256, + tests: [secp256k1_sha256_test], + }, + sha3_256: { + hash: sha3_256, + tests: [secp256k1_sha3_256_test], + }, + sha3_512: { + hash: sha3_512, + tests: [secp256k1_sha3_512_test], + }, + sha512: { + hash: sha512, + tests: [secp256k1_sha512_test], + }, + }, + }, + P256: { + curve: P256, + hashes: { + sha256: { + hash: sha256, + tests: [secp256r1_sha256_test], + }, + sha3_256: { + hash: sha3_256, + tests: [secp256r1_sha3_256_test], + }, + sha3_512: { + hash: sha3_512, + tests: [secp256r1_sha3_512_test], + }, + sha512: { + hash: sha512, + tests: [secp256r1_sha512_test], + }, + }, + }, + P384: { + curve: P384, + hashes: { + sha384: { + hash: sha384, + tests: [secp384r1_sha384_test], + }, + sha3_384: { + hash: sha3_384, + tests: [secp384r1_sha3_384_test], + }, + sha3_512: { + hash: sha3_512, + tests: [secp384r1_sha3_512_test], + }, + sha512: { + hash: sha512, + tests: [secp384r1_sha512_test], + }, + }, + }, + P521: { + curve: P521, + hashes: { + sha3_512: { + hash: sha3_512, + tests: [secp521r1_sha3_512_test], + }, + sha512: { + hash: sha512, + tests: [secp521r1_sha512_test], + }, + }, + }, +}; + +for (const name in WYCHEPROOF_ECDSA) { + const { curve, hashes } = WYCHEPROOF_ECDSA[name]; + for (const hName in hashes) { + const { hash, tests } = hashes[hName]; + const CURVE = curve.create(hash); + for (let i = 0; i < tests.length; i++) { + const test = tests[i]; + for (let j = 0; j < test.testGroups.length; j++) { + const group = test.testGroups[j]; + should(`Wycheproof/WYCHEPROOF_ECDSA ${name}/${hName} (${i}/${j})`, () => { + const pubKey = CURVE.Point.fromHex(group.key.uncompressed); + deepStrictEqual(pubKey.x, BigInt(`0x${group.key.wx}`)); + deepStrictEqual(pubKey.y, BigInt(`0x${group.key.wy}`)); + for (const test of group.tests) { + const m = CURVE.CURVE.hash(hexToBytes(test.msg)); + if (test.result === 'valid' || test.result === 'acceptable') { + try { + CURVE.Signature.fromDER(test.sig); + } catch (e) { + // Some tests has invalid signature which we don't accept + if (e.message.includes('Invalid signature: incorrect length')) continue; + throw e; + } + const verified = CURVE.verify(test.sig, m, pubKey); + if (name === 'secp256k1') { + // lowS: true for secp256k1 + deepStrictEqual(verified, !CURVE.Signature.fromDER(test.sig).hasHighS()); + } else { + deepStrictEqual(verified, true, 'valid'); + } + } else if (test.result === 'invalid') { + let failed = false; + try { + failed = !CURVE.verify(test.sig, m, pubKey); + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true, 'invalid'); + } else throw new Error('unknown test result'); + } + }); + } + } + } +} + +const hexToBigint = (hex) => BigInt(`0x${hex}`); +should('RFC6979', () => { + for (const v of rfc6979) { + const curve = NIST[v.curve]; + deepStrictEqual(curve.CURVE.n, hexToBigint(v.q)); + const pubKey = curve.getPublicKey(v.private); + const pubPoint = curve.Point.fromHex(pubKey); + deepStrictEqual(pubPoint.x, hexToBigint(v.Ux)); + deepStrictEqual(pubPoint.y, hexToBigint(v.Uy)); + for (const c of v.cases) { + const h = curve.CURVE.hash(c.message); + const sigObj = curve.sign(h, v.private); + deepStrictEqual(sigObj.r, hexToBigint(c.r), 'R'); + deepStrictEqual(sigObj.s, hexToBigint(c.s), 'S'); + deepStrictEqual(curve.verify(sigObj.toDERRawBytes(), h, pubKey), true, 'verify(1)'); + deepStrictEqual(curve.verify(sigObj, h, pubKey), true, 'verify(2)'); + } + } +}); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + should.run(); +} diff --git a/curve-definitions/test/rfc6979.test.js b/curve-definitions/test/rfc6979.test.js deleted file mode 100644 index ead685e..0000000 --- a/curve-definitions/test/rfc6979.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import { deepStrictEqual } from 'assert'; -import { should } from 'micro-should'; -import * as nist from '../lib/nist.js'; -import { default as rfc6979 } from './fixtures/rfc6979.json' assert { type: 'json' }; -function hexToBigint(hex) { - return BigInt('0x' + hex) -} - -should('RFC6979', () => { - for (const v of rfc6979) { - const curve = nist[v.curve]; - deepStrictEqual(curve.CURVE.n, hexToBigint(v.q)); - const pubKey = curve.getPublicKey(v.private); - const pubPoint = curve.Point.fromHex(pubKey); - deepStrictEqual(pubPoint.x, hexToBigint(v.Ux)); - deepStrictEqual(pubPoint.y, hexToBigint(v.Uy)); - for (const c of v.cases) { - const h = curve.CURVE.hash(c.message); - const sigObj = curve.sign(h, v.private); - deepStrictEqual(sigObj.r, hexToBigint(c.r), 'R'); - deepStrictEqual(sigObj.s, hexToBigint(c.s), 'S'); - deepStrictEqual(curve.verify(sigObj.toDERRawBytes(), h, pubKey), true, 'verify(1)'); - deepStrictEqual(curve.verify(sigObj, h, pubKey), true, 'verify(2)'); - } - } -}); - -// ESM is broken. -import url from 'url'; -if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { - should.run(); -} diff --git a/curve-definitions/test/secp256k1.test.js b/curve-definitions/test/secp256k1.test.js index e4b9e0b..a2b6798 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 * as nist from '../lib/nist.js'; +import { secp256k1 } 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' }; @@ -7,11 +7,11 @@ import { default as privates } from './vectors/privates.json' assert { type: 'js import { default as points } from './vectors/points.json' assert { type: 'json' }; import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' }; import { should } from 'micro-should'; -import { deepStrictEqual, throws, rejects } from 'assert'; +import { deepStrictEqual, throws } from 'assert'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils'; const hex = bytesToHex; -const secp = nist.secp256k1; +const secp = secp256k1; const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8'); const schCsv = readFileSync('./test/vectors/schnorr.csv', 'utf-8'); @@ -192,9 +192,9 @@ should('secp256k1.Signature.fromDERHex() roundtrip', () => { ); }); -should('secp256k1.sign()/should create deterministic signatures with RFC 6979', async () => { +should('secp256k1.sign()/should create deterministic signatures with RFC 6979', () => { for (const vector of ecdsa.valid) { - let usig = await secp.sign(vector.m, vector.d); + let usig = secp.sign(vector.m, vector.d); let sig = usig.toCompactHex(); const vsig = vector.signature; deepStrictEqual(sig.slice(0, 64), vsig.slice(0, 64)); @@ -202,25 +202,18 @@ should('secp256k1.sign()/should create deterministic signatures with RFC 6979', } }); -should( - 'secp256k1.sign()/should not create invalid deterministic signatures with RFC 6979', - async () => { - for (const vector of ecdsa.invalid.sign) { - throws(() => { - return secp.sign(vector.m, vector.d); - }); - } +should('secp256k1.sign()/should not create invalid deterministic signatures with RFC 6979', () => { + for (const vector of ecdsa.invalid.sign) { + throws(() => secp.sign(vector.m, vector.d)); } -); - -should('secp256k1.sign()/edge cases', () => { - // @ts-ignore - rejects(async () => await secp.sign()); - // @ts-ignore - rejects(async () => await secp.sign('')); }); -should('secp256k1.sign()/should create correct DER encoding against libsecp256k1', async () => { +should('secp256k1.sign()/edge cases', () => { + throws(() => secp.sign()); + throws(() => secp.sign('')); +}); + +should('secp256k1.sign()/should create correct DER encoding against libsecp256k1', () => { const CASES = [ [ 'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b', @@ -236,14 +229,14 @@ should('secp256k1.sign()/should create correct DER encoding against libsecp256k1 ], ]; const privKey = hexToBytes('0101010101010101010101010101010101010101010101010101010101010101'); - for (let [msg, exp] of CASES) { - const res = await secp.sign(msg, privKey, { extraEntropy: undefined }); + for (const [msg, exp] of CASES) { + const res = secp.sign(msg, privKey, { extraEntropy: undefined }); deepStrictEqual(res.toDERHex(), exp); const rs = secp.Signature.fromDER(res.toDERHex()).toCompactHex(); deepStrictEqual(secp.Signature.fromCompact(rs).toDERHex(), exp); } }); -should('secp256k1.sign()/sign ecdsa extraData', async () => { +should('secp256k1.sign()/sign ecdsa extraData', () => { const ent1 = '0000000000000000000000000000000000000000000000000000000000000000'; const ent2 = '0000000000000000000000000000000000000000000000000000000000000001'; const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33'; @@ -251,56 +244,52 @@ should('secp256k1.sign()/sign ecdsa extraData', async () => { const ent5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; for (const e of ecdsa.extraEntropy) { - const sign = async (extraEntropy) => { + const sign = (extraEntropy) => { const s = secp.sign(e.m, e.d, { extraEntropy }).toCompactHex(); return s; }; - deepStrictEqual(await sign(), e.signature); - deepStrictEqual(await sign(ent1), e.extraEntropy0); - deepStrictEqual(await sign(ent2), e.extraEntropy1); - deepStrictEqual(await sign(ent3), e.extraEntropyRand); - deepStrictEqual(await sign(ent4), e.extraEntropyN); - deepStrictEqual(await sign(ent5), e.extraEntropyMax); + deepStrictEqual(sign(), e.signature); + deepStrictEqual(sign(ent1), e.extraEntropy0); + deepStrictEqual(sign(ent2), e.extraEntropy1); + deepStrictEqual(sign(ent3), e.extraEntropyRand); + deepStrictEqual(sign(ent4), e.extraEntropyN); + deepStrictEqual(sign(ent5), e.extraEntropyMax); } }); -should('secp256k1.verify()/should verify signature', async () => { +should('secp256k1.verify()/should verify signature', () => { const MSG = '01'.repeat(32); const PRIV_KEY = 0x2n; - const signature = await secp.sign(MSG, PRIV_KEY); + const signature = secp.sign(MSG, PRIV_KEY); const publicKey = secp.getPublicKey(PRIV_KEY); deepStrictEqual(publicKey.length, 65); deepStrictEqual(secp.verify(signature, MSG, publicKey), true); }); -should('secp256k1.verify()/should not verify signature with wrong public key', async () => { +should('secp256k1.verify()/should not verify signature with wrong public key', () => { const MSG = '01'.repeat(32); const PRIV_KEY = 0x2n; const WRONG_PRIV_KEY = 0x22n; - const signature = await secp.sign(MSG, PRIV_KEY); + const signature = secp.sign(MSG, PRIV_KEY); const publicKey = secp.Point.fromPrivateKey(WRONG_PRIV_KEY).toHex(); deepStrictEqual(publicKey.length, 130); deepStrictEqual(secp.verify(signature, MSG, publicKey), false); }); -should('secp256k1.verify()/should not verify signature with wrong hash', async () => { +should('secp256k1.verify()/should not verify signature with wrong hash', () => { const MSG = '01'.repeat(32); const PRIV_KEY = 0x2n; const WRONG_MSG = '11'.repeat(32); - const signature = await secp.sign(MSG, PRIV_KEY); + const signature = secp.sign(MSG, PRIV_KEY); const publicKey = secp.getPublicKey(PRIV_KEY); deepStrictEqual(publicKey.length, 65); deepStrictEqual(secp.verify(signature, WRONG_MSG, publicKey), false); }); -should('secp256k1.verify()/should verify random signatures', async () => +should('secp256k1.verify()/should verify random signatures', () => fc.assert( - fc.asyncProperty( - FC_BIGINT, - fc.hexaString({ minLength: 64, maxLength: 64 }), - async (privKey, msg) => { - const pub = secp.getPublicKey(privKey); - const sig = await secp.sign(msg, privKey); - deepStrictEqual(secp.verify(sig, msg, pub), true); - } - ) + fc.property(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privKey, msg) => { + const pub = secp.getPublicKey(privKey); + const sig = secp.sign(msg, privKey); + deepStrictEqual(secp.verify(sig, msg, pub), true); + }) ) ); should('secp256k1.verify()/should not verify signature with invalid r/s', () => { @@ -315,16 +304,14 @@ should('secp256k1.verify()/should not verify signature with invalid r/s', () => const pub = new secp.Point(x, y); const signature = new secp.Signature(2n, 2n); - // @ts-ignore signature.r = r; - // @ts-ignore signature.s = s; const verified = secp.verify(signature, msg, pub); // Verifies, but it shouldn't, because signature S > curve order deepStrictEqual(verified, false); }); -should('secp256k1.verify()/should not verify msg = curve order', async () => { +should('secp256k1.verify()/should not verify msg = curve order', () => { const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n; const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; @@ -334,7 +321,7 @@ should('secp256k1.verify()/should not verify msg = curve order', async () => { const sig = new secp.Signature(r, s); deepStrictEqual(secp.verify(sig, msg, pub), false); }); -should('secp256k1.verify()/should verify non-strict msg bb5a...', async () => { +should('secp256k1.verify()/should verify non-strict msg bb5a...', () => { const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023'; const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n; const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; @@ -386,11 +373,11 @@ should( // } // }); -should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', async () => { +should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', () => { const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; const privateKey = 123456789n; const publicKey = secp.Point.fromHex(secp.getPublicKey(privateKey)).toHex(false); - const sig = await secp.sign(message, privateKey); + const sig = secp.sign(message, privateKey); const recoveredPubkey = sig.recoverPublicKey(message); // const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery); deepStrictEqual(recoveredPubkey !== null, true); @@ -404,15 +391,15 @@ should('secp256k1.recoverPublicKey()/should not recover zero points', () => { const recovery = 0; throws(() => secp.recoverPublicKey(msgHash, sig, recovery)); }); -should('secp256k1.recoverPublicKey()/should handle all-zeros msghash', async () => { +should('secp256k1.recoverPublicKey()/should handle all-zeros msghash', () => { const privKey = secp.utils.randomPrivateKey(); const pub = secp.getPublicKey(privKey); const zeros = '0000000000000000000000000000000000000000000000000000000000000000'; - const sig = await secp.sign(zeros, privKey, { recovered: true }); + const sig = secp.sign(zeros, privKey, { recovered: true }); const recoveredKey = sig.recoverPublicKey(zeros); deepStrictEqual(recoveredKey.toRawBytes(), pub); }); -should('secp256k1.recoverPublicKey()/should handle RFC 6979 vectors', async () => { +should('secp256k1.recoverPublicKey()/should handle RFC 6979 vectors', () => { for (const vector of ecdsa.valid) { if (secp.utils.mod(hexToNumber(vector.m), secp.CURVE.n) === 0n) continue; let usig = secp.sign(vector.m, vector.d); diff --git a/curve-definitions/test/starknet/basic.test.js b/curve-definitions/test/stark/basic.test.js similarity index 99% rename from curve-definitions/test/starknet/basic.test.js rename to curve-definitions/test/stark/basic.test.js index d2463d0..3b6e43d 100644 --- a/curve-definitions/test/starknet/basic.test.js +++ b/curve-definitions/test/stark/basic.test.js @@ -1,6 +1,6 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; -import * as starknet from '../../lib/starknet.js'; +import * as starknet from '../../lib/stark.js'; import { default as issue2 } from './fixtures/issue2.json' assert { type: 'json' }; should('Basic elliptic sanity check', () => { diff --git a/curve-definitions/test/starknet/benchmark/index.js b/curve-definitions/test/stark/benchmark/index.js similarity index 100% rename from curve-definitions/test/starknet/benchmark/index.js rename to curve-definitions/test/stark/benchmark/index.js diff --git a/curve-definitions/test/starknet/benchmark/package.json b/curve-definitions/test/stark/benchmark/package.json similarity index 100% rename from curve-definitions/test/starknet/benchmark/package.json rename to curve-definitions/test/stark/benchmark/package.json diff --git a/curve-definitions/test/starknet/fixtures/issue2.json b/curve-definitions/test/stark/fixtures/issue2.json similarity index 100% rename from curve-definitions/test/starknet/fixtures/issue2.json rename to curve-definitions/test/stark/fixtures/issue2.json diff --git a/curve-definitions/test/starknet/fixtures/keys_precomputed.json b/curve-definitions/test/stark/fixtures/keys_precomputed.json similarity index 100% rename from curve-definitions/test/starknet/fixtures/keys_precomputed.json rename to curve-definitions/test/stark/fixtures/keys_precomputed.json diff --git a/curve-definitions/test/starknet/fixtures/rfc6979_signature_test_vector.json b/curve-definitions/test/stark/fixtures/rfc6979_signature_test_vector.json similarity index 100% rename from curve-definitions/test/starknet/fixtures/rfc6979_signature_test_vector.json rename to curve-definitions/test/stark/fixtures/rfc6979_signature_test_vector.json diff --git a/curve-definitions/test/starknet/index.test.js b/curve-definitions/test/stark/index.test.js similarity index 63% rename from curve-definitions/test/starknet/index.test.js rename to curve-definitions/test/stark/index.test.js index a8fd25b..4a83616 100644 --- a/curve-definitions/test/starknet/index.test.js +++ b/curve-definitions/test/stark/index.test.js @@ -1,5 +1,3 @@ - import './basic.test.js'; -import './starknet.test.js'; +import './stark.test.js'; import './property.test.js'; - diff --git a/curve-definitions/test/starknet/property.test.js b/curve-definitions/test/stark/property.test.js similarity index 96% rename from curve-definitions/test/starknet/property.test.js rename to curve-definitions/test/stark/property.test.js index 5374cfc..3ee19ca 100644 --- a/curve-definitions/test/starknet/property.test.js +++ b/curve-definitions/test/stark/property.test.js @@ -1,6 +1,6 @@ import { deepStrictEqual, throws } from 'assert'; import { should } from 'micro-should'; -import * as starknet from '../../lib/starknet.js'; +import * as starknet from '../../lib/stark.js'; import * as fc from 'fast-check'; const FC_BIGINT = fc.bigInt(1n + 1n, starknet.CURVE.n - 1n); diff --git a/curve-definitions/test/starknet/starknet.test.js b/curve-definitions/test/stark/stark.test.js similarity index 99% rename from curve-definitions/test/starknet/starknet.test.js rename to curve-definitions/test/stark/stark.test.js index 8c2e9ae..681eba6 100644 --- a/curve-definitions/test/starknet/starknet.test.js +++ b/curve-definitions/test/stark/stark.test.js @@ -3,7 +3,7 @@ import { should } from 'micro-should'; import { hex, utf8 } from '@scure/base'; import * as bip32 from '@scure/bip32'; import * as bip39 from '@scure/bip39'; -import * as starknet from '../../lib/starknet.js'; +import * as starknet from '../../lib/stark.js'; import { default as sigVec } from './fixtures/rfc6979_signature_test_vector.json' assert { type: 'json' }; import { default as precomputedKeys } from './fixtures/keys_precomputed.json' assert { type: 'json' }; diff --git a/src/edwards.ts b/src/edwards.ts index a584741..c5b0444 100644 --- a/src/edwards.ts +++ b/src/edwards.ts @@ -1,10 +1,9 @@ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ // Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² -// Differences with @noble/ed25519: -// 1. EDDSA & ECDH have different field element lengths (for ed448/x448 only) -// https://www.rfc-editor.org/rfc/rfc8032.html says bitLength is 456 (57 bytes) -// https://www.rfc-editor.org/rfc/rfc7748 says bitLength is 448 (56 bytes) +// 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) // 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 @@ -17,10 +16,11 @@ import { ensureBytes, numberToBytesLE, bytesToNumberLE, - nLength, hashToPrivateScalar, -} from './utils.js'; -import { wNAF } from './group.js'; + BasicCurve, + validateOpts as utilOpts, +} from './utils.js'; // TODO: import * as u from './utils.js'? +import { Group, GroupConstructor, wNAF } from './group.js'; // Be friendly to bad ECMAScript parsers by not using bigint literals like 123n const _0n = BigInt(0); @@ -35,21 +35,10 @@ export type CHash = { create(): any; }; -export type CurveType = { +export type CurveType = BasicCurve & { // Params: a, d a: bigint; d: bigint; - // Field over which we'll do calculations. Verify with: - P: bigint; - // Subgroup order: how many points ed25519 has - n: bigint; // in rfc8032 called l - // Cofactor - h: bigint; - nBitLength?: number; - nByteLength?: number; - // Base point (x, y) aka generator point - Gx: bigint; - Gy: bigint; // Hashes hash: CHash; // Because we need outputLen for DRBG randomBytes: (bytesLength?: number) => Uint8Array; @@ -66,26 +55,22 @@ type PrivKey = Hex | bigint | number; // Should be separate from overrides, since overrides can use information about curve (for example nBits) function validateOpts(curve: CurveType) { - if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen)) + const opts = utilOpts(curve); + if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) throw new Error('Invalid hash function'); - for (const i of ['a', 'd', 'P', 'n', 'h', 'Gx', 'Gy'] as const) { - if (typeof curve[i] !== 'bigint') - throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); - } - for (const i of ['nBitLength', 'nByteLength'] as const) { - if (curve[i] === undefined) continue; // Optional - if (!Number.isSafeInteger(curve[i])) - throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + for (const i of ['a', 'd'] as const) { + if (typeof opts[i] !== 'bigint') + throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`); } for (const fn of ['randomBytes'] as const) { - if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); + if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`); } for (const fn of ['adjustScalarBytes', 'domain', 'uvRatio'] as const) { - if (curve[fn] === undefined) continue; // Optional - if (typeof curve[fn] !== 'function') throw new Error(`Invalid ${fn} function`); + if (opts[fn] === undefined) continue; // Optional + if (typeof opts[fn] !== 'function') throw new Error(`Invalid ${fn} function`); } // Set defaults - return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const); + return Object.freeze({ ...opts } as const); } // Instance @@ -103,16 +88,11 @@ export type SignatureConstructor = { }; // Instance -export interface ExtendedPointType { +export interface ExtendedPointType extends Group { readonly x: bigint; readonly y: bigint; readonly z: bigint; readonly t: bigint; - equals(other: ExtendedPointType): boolean; - negate(): ExtendedPointType; - double(): ExtendedPointType; - add(other: ExtendedPointType): ExtendedPointType; - subtract(other: ExtendedPointType): ExtendedPointType; multiply(scalar: number | bigint, affinePoint?: PointType): ExtendedPointType; multiplyUnsafe(scalar: number | bigint): ExtendedPointType; isSmallOrder(): boolean; @@ -120,37 +100,28 @@ export interface ExtendedPointType { toAffine(invZ?: bigint): PointType; } // Static methods -export type ExtendedPointConstructor = { +export interface ExtendedPointConstructor extends GroupConstructor { new (x: bigint, y: bigint, z: bigint, t: bigint): ExtendedPointType; - BASE: ExtendedPointType; - ZERO: ExtendedPointType; fromAffine(p: PointType): ExtendedPointType; toAffineBatch(points: ExtendedPointType[]): PointType[]; normalizeZ(points: ExtendedPointType[]): ExtendedPointType[]; -}; +} // Instance -export interface PointType { +export interface PointType extends Group { readonly x: bigint; readonly y: bigint; _setWindowSize(windowSize: number): void; toRawBytes(isCompressed?: boolean): Uint8Array; toHex(isCompressed?: boolean): string; isTorsionFree(): boolean; - equals(other: PointType): boolean; - negate(): PointType; - add(other: PointType): PointType; - subtract(other: PointType): PointType; - multiply(scalar: number | bigint): PointType; } // Static methods -export type PointConstructor = { - BASE: PointType; - ZERO: PointType; +export interface PointConstructor extends GroupConstructor { new (x: bigint, y: bigint): PointType; fromHex(hex: Hex): PointType; fromPrivateKey(privateKey: PrivKey): PointType; -}; +} export type PubKey = Hex | PointType; export type SigType = Hex | SignatureType; @@ -289,6 +260,27 @@ 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. + // Note: It does not check whether the `other` point is valid. + if (a === BigInt(-1)) { + const A = modP((Y1 - X1) * (Y2 + X2)); + const B = modP((Y1 + X1) * (Y2 - X2)); + const F = modP(B - A); + if (F === _0n) return this.double(); // Same point. + const C = modP(Z1 * _2n * T2); + const D = modP(T1 * _2n * Z2); + const E = D + C; + const G = B + A; + const H = D - C; + const X3 = modP(E * F); + const Y3 = modP(G * H); + const T3 = modP(E * H); + const Z3 = modP(F * G); + return new ExtendedPoint(X3, Y3, Z3, T3); + } const A = modP(X1 * X2); // A = X1*X2 const B = modP(Y1 * Y2); // B = Y1*Y2 const C = modP(T1 * d * T2); // C = T1*d*T2 @@ -466,13 +458,18 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } equals(other: Point): boolean { + if (!(other instanceof Point)) throw new TypeError('Point#equals: expected Point'); return this.x === other.x && this.y === other.y; } - negate() { + negate(): Point { return new Point(modP(-this.x), this.y); } + double(): Point { + return ExtendedPoint.fromAffine(this).double().toAffine(); + } + add(other: Point) { return ExtendedPoint.fromAffine(this).add(ExtendedPoint.fromAffine(other)).toAffine(); } @@ -523,12 +520,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn { } } - // Little Endian - // function bytesToNumberLE(uint8a: Uint8Array): bigint { - // if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); - // return BigInt('0x' + bytesToHex(Uint8Array.from(uint8a).reverse())); - // } - // ------------------------- // Little-endian SHA512 with modulo n function modlLE(hash: Uint8Array): bigint { diff --git a/src/group.ts b/src/group.ts index e4a3cd5..7bb1735 100644 --- a/src/group.ts +++ b/src/group.ts @@ -5,8 +5,11 @@ const _1n = BigInt(1); export interface Group> { double(): T; - add(other: T): T; negate(): T; + add(other: T): T; + subtract(other: T): T; + equals(other: T): boolean; + multiply(scalar: number | bigint): T; } export type GroupConstructor = { diff --git a/src/montgomery.ts b/src/montgomery.ts index 8cdc810..f16ba49 100644 --- a/src/montgomery.ts +++ b/src/montgomery.ts @@ -53,6 +53,8 @@ function validateOpts(curve: CurveType) { return Object.freeze({ ...curve } as const); } +// NOTE: not really montgomery curve, just bunch of very specific methods for X25519/X448 (RFC 7748, https://www.rfc-editor.org/rfc/rfc7748) +// Uses only one coordinate instead of two export function montgomery(curveDef: CurveType): CurveFn { const CURVE = validateOpts(curveDef); const { P } = CURVE; @@ -83,6 +85,16 @@ export function montgomery(curveDef: CurveType): CurveFn { } // cswap from RFC7748 + // NOTE: cswap is not from RFC7748! + /* + cswap(swap, x_2, x_3): + dummy = mask(swap) AND (x_2 XOR x_3) + x_2 = x_2 XOR dummy + x_3 = x_3 XOR dummy + Return (x_2, x_3) + Where mask(swap) is the all-1 or all-0 word of the same length as x_2 + and x_3, computed, e.g., as mask(swap) = 0 - swap. + */ function cswap(swap: bigint, x_2: bigint, x_3: bigint): [bigint, bigint] { const dummy = modP(swap * (x_2 - x_3)); x_2 = modP(x_2 - dummy); diff --git a/src/utils.ts b/src/utils.ts index c9a67b1..9d97452 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,38 @@ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ -// Convert between types -// --------------------- +export type Hex = string | Uint8Array; +// 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 = { + // Field over which we'll do calculations (Fp) + P: bigint; + // Curve order, total count of valid points in the field + n: bigint; + // Bit/byte length of curve order + nBitLength?: number; + nByteLength?: number; + // Cofactor + // NOTE: we can assign default value of 1, but then users will just ignore it, without validating with spec + // Has not use for now, but nice to have in API + h: bigint; + // Base point (x, y) aka generator point + Gx: bigint; + Gy: bigint; +}; + +export function validateOpts(curve: T) { + for (const i of ['P', 'n', 'h', 'Gx', 'Gy'] as const) { + if (typeof curve[i] !== 'bigint') + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + for (const i of ['nBitLength', 'nByteLength'] as const) { + if (curve[i] === undefined) continue; // Optional + if (!Number.isSafeInteger(curve[i])) + throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + } + // Set defaults + return Object.freeze({ ...nLength(curve.n, curve.nBitLength), ...curve } as const); +} -type Hex = string | Uint8Array; import * as mod from './modular.js'; const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')); @@ -100,8 +130,10 @@ export function nLength(n: bigint, nBitLength?: number) { const _1n = BigInt(1); export function hashToPrivateScalar(hash: Hex, CURVE_ORDER: bigint, isLE = false): bigint { hash = ensureBytes(hash); - if (hash.length < 40 || hash.length > 1024) - throw new Error('Expected 40-1024 bytes of private key as per FIPS 186'); + const orderLen = nLength(CURVE_ORDER).nByteLength; + const minLen = orderLen + 8; + if (orderLen < 16 || hash.length < minLen || hash.length > 1024) + throw new Error('Expected valid bytes of private key as per FIPS 186'); const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash); return mod.mod(num, CURVE_ORDER - _1n) + _1n; } diff --git a/src/weierstrass.ts b/src/weierstrass.ts index 2c7f423..88461bf 100644 --- a/src/weierstrass.ts +++ b/src/weierstrass.ts @@ -3,7 +3,7 @@ // TODO: sync vs async naming // TODO: default randomBytes -// Differences from noble/secp256k1: +// Differences from @noble/secp256k1 1.7: // 1. Different double() formula (but same addition) // 2. Different sqrt() function // 3. truncateHash() truncateOnly mode @@ -18,10 +18,11 @@ import { hexToBytes, hexToNumber, numberToHexUnpadded, - nLength, hashToPrivateScalar, + BasicCurve, + validateOpts as utilOpts, } from './utils.js'; -import { wNAF } from './group.js'; +import { Group, GroupConstructor, wNAF } from './group.js'; export type CHash = { (message: Uint8Array | string): Uint8Array; @@ -35,32 +36,19 @@ type EndomorphismOpts = { splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint }; }; -export type CurveType = { +export type CurveType = BasicCurve & { // Params: a, b a: bigint; b: bigint; - // Field over which we'll do calculations. Verify with: - P: bigint; - // Curve order, total count of valid points in the field. Verify with: - n: bigint; - nBitLength?: number; - nByteLength?: number; - // Base point (x, y) aka generator point - Gx: bigint; - Gy: bigint; - // Default options lowS?: boolean; - // Hashes hash: CHash; // Because we need outputLen for DRBG hmac: HmacFnSync; randomBytes: (bytesLength?: number) => Uint8Array; - truncateHash?: (hash: Uint8Array, truncateOnly?: boolean) => bigint; // Some fields can have specialized fast case sqrtMod?: (n: bigint) => bigint; - // TODO: move into options? // Endomorphism options for Koblitz curves endo?: EndomorphismOpts; @@ -73,23 +61,19 @@ type PrivKey = Hex | bigint | number; // Should be separate from overrides, since overrides can use information about curve (for example nBits) function validateOpts(curve: CurveType) { - if (typeof curve.hash !== 'function' || !Number.isSafeInteger(curve.hash.outputLen)) + const opts = utilOpts(curve); + if (typeof opts.hash !== 'function' || !Number.isSafeInteger(opts.hash.outputLen)) throw new Error('Invalid hash function'); - if (typeof curve.hmac !== 'function') throw new Error('Invalid hmac function'); - if (typeof curve.randomBytes !== 'function') throw new Error('Invalid randomBytes function'); + if (typeof opts.hmac !== 'function') throw new Error('Invalid hmac function'); + if (typeof opts.randomBytes !== 'function') throw new Error('Invalid randomBytes function'); - for (const i of ['a', 'b', 'P', 'n', 'Gx', 'Gy'] as const) { - if (typeof curve[i] !== 'bigint') - throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); + for (const i of ['a', 'b'] as const) { + if (typeof opts[i] !== 'bigint') + throw new Error(`Invalid curve param ${i}=${opts[i]} (${typeof opts[i]})`); } - for (const i of ['nBitLength', 'nByteLength'] as const) { - if (curve[i] === undefined) continue; // Optional - if (!Number.isSafeInteger(curve[i])) - throw new Error(`Invalid curve param ${i}=${curve[i]} (${typeof curve[i]})`); - } - const endo = curve.endo; + const endo = opts.endo; if (endo) { - if (curve.a !== _0n) { + if (opts.a !== _0n) { throw new Error('Endomorphism can only be defined for Koblitz curves that have a=0'); } if ( @@ -101,7 +85,7 @@ function validateOpts(curve: CurveType) { } } // Set defaults - return Object.freeze({ lowS: true, ...nLength(curve.n, curve.nBitLength), ...curve } as const); + return Object.freeze({ lowS: true, ...opts } as const); } // TODO: convert bits to bytes aligned to 32 bits? (224 for example) @@ -206,30 +190,23 @@ export type SignatureConstructor = { }; // Instance -export interface JacobianPointType { +export interface JacobianPointType extends Group { readonly x: bigint; readonly y: bigint; readonly z: bigint; - equals(other: JacobianPointType): boolean; - negate(): JacobianPointType; - double(): JacobianPointType; - add(other: JacobianPointType): JacobianPointType; - subtract(other: JacobianPointType): JacobianPointType; multiply(scalar: number | bigint, affinePoint?: PointType): JacobianPointType; multiplyUnsafe(scalar: bigint): JacobianPointType; toAffine(invZ?: bigint): PointType; } // Static methods -export type JacobianPointConstructor = { +export interface JacobianPointConstructor extends GroupConstructor { new (x: bigint, y: bigint, z: bigint): JacobianPointType; - BASE: JacobianPointType; - ZERO: JacobianPointType; fromAffine(p: PointType): JacobianPointType; toAffineBatch(points: JacobianPointType[]): PointType[]; normalizeZ(points: JacobianPointType[]): JacobianPointType[]; -}; +} // Instance -export interface PointType { +export interface PointType extends Group { readonly x: bigint; readonly y: bigint; _setWindowSize(windowSize: number): void; @@ -237,22 +214,14 @@ export interface PointType { toRawBytes(isCompressed?: boolean): Uint8Array; toHex(isCompressed?: boolean): string; assertValidity(): void; - equals(other: PointType): boolean; - negate(): PointType; - double(): PointType; - add(other: PointType): PointType; - subtract(other: PointType): PointType; - multiply(scalar: number | bigint): PointType; multiplyAndAddUnsafe(Q: PointType, a: bigint, b: bigint): PointType | undefined; } // Static methods -export type PointConstructor = { - BASE: PointType; - ZERO: PointType; +export interface PointConstructor extends GroupConstructor { new (x: bigint, y: bigint): PointType; fromHex(hex: Hex): PointType; fromPrivateKey(privateKey: PrivKey): PointType; -}; +} export type PubKey = Hex | PointType; @@ -510,9 +479,9 @@ export function weierstrass(curveDef: CurveType): CurveFn { const { x: X1, y: Y1, z: Z1 } = this; const { a } = CURVE; - // // Faster algorithm: when a=0 - // // From: https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l - // // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8. + // Faster algorithm: when a=0 + // From: https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l + // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8. if (a === _0n) { const A = modP(X1 * X1); const B = modP(Y1 * Y1); @@ -789,6 +758,7 @@ export function weierstrass(curveDef: CurveType): CurveFn { } equals(other: Point): boolean { + if (!(other instanceof Point)) throw new TypeError('Point#equals: expected Point'); return this.x === other.x && this.y === other.y; } @@ -965,8 +935,8 @@ export function weierstrass(curveDef: CurveType): CurveFn { */ 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. + // Takes curve order + 64 bits from CSPRNG so that modulo bias is neglible, + // matches FIPS 186 B.4.1. randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(CURVE.randomBytes(fieldLen + 8)), /**