Split curves. Improve speed. Better tests

This commit is contained in:
Paul Miller 2022-12-13 22:23:23 +00:00
parent 6b0d9611a5
commit 5b305abe85
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
38 changed files with 1259 additions and 856 deletions

@ -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),
},
},
};

@ -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<Omit<CurveType, 'hash' | 'hmac' | 'randomBytes'>>;
export function createCurve(curveDef: CurveDef, defHash: CHash) {
const create = (hash: CHash) => weierstrass({ ...curveDef, ...getHash(hash) });
return Object.freeze({ ...create(defHash), create });
}

@ -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),
});

@ -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';

@ -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),

@ -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');
}

@ -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<Omit<CurveType, 'hash' | 'hmac' | 'randomBytes'>>;
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
);

@ -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;

@ -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;

@ -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;

@ -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;

@ -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;

@ -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),
});

@ -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
);

@ -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),

@ -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) {

@ -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) {

@ -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) {

@ -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();
}

@ -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();

@ -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();
}

@ -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();
}

@ -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();
}

@ -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);

@ -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', () => {

@ -1,5 +1,3 @@
import './basic.test.js';
import './starknet.test.js';
import './stark.test.js';
import './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);

@ -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' };

@ -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<ExtendedPointType> {
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<ExtendedPointType> {
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<PointType> {
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<PointType> {
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 {

@ -5,8 +5,11 @@ const _1n = BigInt(1);
export interface Group<T extends Group<T>> {
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<T> = {

@ -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);

@ -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<T extends BasicCurve>(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;
}

@ -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<JacobianPointType> {
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<JacobianPointType> {
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<PointType> {
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<PointType> {
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)),
/**