Add ristretto, schnorr

This commit is contained in:
Paul Miller 2022-12-14 14:21:07 +00:00
parent 5b305abe85
commit 9e5ad8dc85
No known key found for this signature in database
GPG Key ID: 697079DA6878B89B
11 changed files with 603 additions and 223 deletions

@ -46,10 +46,15 @@ export const CURVES = {
const sig = noble_secp256k1.signSync(msg, priv); const sig = noble_secp256k1.signSync(msg, priv);
return { priv, pub, msg, sig }; return { priv, pub, msg, sig };
}, },
getPublicKey: { getPublicKey1: {
samples: 10000, samples: 10000,
old: () => noble_secp256k1.getPublicKey(noble_secp256k1.utils.randomPrivateKey()), noble: () => secp256k1.getPublicKey(3n),
noble: () => secp256k1.getPublicKey(secp256k1.utils.randomPrivateKey()), old: () => noble_secp256k1.getPublicKey(3n),
},
getPublicKey255: {
samples: 10000,
noble: () => secp256k1.getPublicKey(2n**255n-1n),
old: () => noble_secp256k1.getPublicKey(2n**255n-1n),
}, },
sign: { sign: {
samples: 5000, samples: 5000,

@ -1,9 +1,17 @@
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
import { sha512 } from '@noble/hashes/sha512'; import { sha512 } from '@noble/hashes/sha512';
import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils'; import { concatBytes, randomBytes, utf8ToBytes } from '@noble/hashes/utils';
import { twistedEdwards } from '@noble/curves/edwards'; import { twistedEdwards, ExtendedPointType } from '@noble/curves/edwards';
import { montgomery } from '@noble/curves/montgomery'; import { montgomery } from '@noble/curves/montgomery';
import { mod, pow2, isNegativeLE } from '@noble/curves/modular'; import { mod, pow2, isNegativeLE } from '@noble/curves/modular';
import {
ensureBytes,
equalBytes,
bytesToHex,
bytesToNumberLE,
numberToBytesLE,
Hex,
} from '@noble/curves/utils';
const ed25519P = BigInt( const ed25519P = BigInt(
'57896044618658097711785492504343953926634992332820282019728792003956564819949' '57896044618658097711785492504343953926634992332820282019728792003956564819949'
@ -47,6 +55,25 @@ function adjustScalarBytes(bytes: Uint8Array): Uint8Array {
bytes[31] |= 64; // 0b0100_0000 bytes[31] |= 64; // 0b0100_0000
return bytes; return bytes;
} }
// sqrt(u/v)
function uvRatio(u: bigint, v: bigint): { isValid: boolean; value: bigint } {
const P = ed25519P;
const v3 = mod(v * v * v, P); // v³
const v7 = mod(v3 * v3 * v, P); // v⁷
// (p+3)/8 and (p-5)/8
const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8;
let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8
const vx2 = mod(v * x * x, P); // vx²
const root1 = x; // First root candidate
const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate
const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4)
const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1)
if (useRoot1) x = root1;
if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time
if (isNegativeLE(x, P)) x = mod(-x, P);
return { isValid: useRoot1 || useRoot2, value: x };
}
// Just in case // Just in case
export const ED25519_TORSION_SUBGROUP = [ export const ED25519_TORSION_SUBGROUP = [
@ -82,24 +109,7 @@ const ED25519_DEF = {
// dom2 // dom2
// Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3. // Ratio of u to v. Allows us to combine inversion and square root. Uses algo from RFC8032 5.1.3.
// Constant-time, u/√v // Constant-time, u/√v
uvRatio: (u: bigint, v: bigint): { isValid: boolean; value: bigint } => { uvRatio,
const P = ed25519P;
const v3 = mod(v * v * v, P); // v³
const v7 = mod(v3 * v3 * v, P); // v⁷
// (p+3)/8 and (p-5)/8
const pow = ed25519_pow_2_252_3(u * v7).pow_p_5_8;
let x = mod(u * v3 * pow, P); // (uv³)(uv⁷)^(p-5)/8
const vx2 = mod(v * x * x, P); // vx²
const root1 = x; // First root candidate
const root2 = mod(x * ED25519_SQRT_M1, P); // Second root candidate
const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
const useRoot2 = vx2 === mod(-u, P); // If vx² = -u, set x <-- x * 2^((p-1)/4)
const noRoot = vx2 === mod(-u * ED25519_SQRT_M1, P); // There is no valid root, vx² = -u√(-1)
if (useRoot1) x = root1;
if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time
if (isNegativeLE(x, P)) x = mod(-x, P);
return { isValid: useRoot1 || useRoot2, value: x };
},
} as const; } as const;
export const ed25519 = twistedEdwards(ED25519_DEF); export const ed25519 = twistedEdwards(ED25519_DEF);
@ -133,3 +143,192 @@ export const x25519 = montgomery({
}, },
adjustScalarBytes, adjustScalarBytes,
}); });
/**
* Each ed25519/ExtendedPoint has 8 different equivalent points. This can be
* a source of bugs for protocols like ring signatures. Ristretto was created to solve this.
* Ristretto point operates in X:Y:Z:T extended coordinates like ExtendedPoint,
* but it should work in its own namespace: do not combine those two.
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-ristretto255-decaf448
*/
function assertRstPoint(other: unknown) {
if (!(other instanceof RistrettoPoint)) throw new TypeError('RistrettoPoint expected');
}
// √(-1) aka √(a) aka 2^((p-1)/4)
const SQRT_M1 = BigInt(
'19681161376707505956807079304988542015446066515923890162744021073123829784752'
);
// √(ad - 1)
const SQRT_AD_MINUS_ONE = BigInt(
'25063068953384623474111414158702152701244531502492656460079210482610430750235'
);
// 1 / √(a-d)
const INVSQRT_A_MINUS_D = BigInt(
'54469307008909316920995813868745141605393597292927456921205312896311721017578'
);
// 1-d²
const ONE_MINUS_D_SQ = BigInt(
'1159843021668779879193775521855586647937357759715417654439879720876111806838'
);
// (d-1)²
const D_MINUS_ONE_SQ = BigInt(
'40440834346308536858101042469323190826248399146238708352240133220865137265952'
);
// Calculates 1/√(number)
const _0n = BigInt(0);
const _1n = BigInt(1);
const invertSqrt = (number: bigint) => uvRatio(_1n, number);
const MAX_255B = BigInt('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
const bytes255ToNumberLE = (bytes: Uint8Array) =>
ed25519.utils.mod(bytesToNumberLE(bytes) & MAX_255B);
type ExtendedPoint = ExtendedPointType;
export class RistrettoPoint {
static BASE = new RistrettoPoint(ed25519.ExtendedPoint.BASE);
static ZERO = new RistrettoPoint(ed25519.ExtendedPoint.ZERO);
// Private property to discourage combining ExtendedPoint + RistrettoPoint
// Always use Ristretto encoding/decoding instead.
constructor(private readonly ep: ExtendedPoint) {}
// Computes Elligator map for Ristretto
// https://ristretto.group/formulas/elligator.html
private static calcElligatorRistrettoMap(r0: bigint): ExtendedPoint {
const { d, P } = ed25519.CURVE;
const { mod } = ed25519.utils;
const r = mod(SQRT_M1 * r0 * r0); // 1
const Ns = mod((r + _1n) * ONE_MINUS_D_SQ); // 2
let c = BigInt(-1); // 3
const D = mod((c - d * r) * mod(r + d)); // 4
let { isValid: Ns_D_is_sq, value: s } = uvRatio(Ns, D); // 5
let s_ = mod(s * r0); // 6
if (!isNegativeLE(s_, P)) s_ = mod(-s_);
if (!Ns_D_is_sq) s = s_; // 7
if (!Ns_D_is_sq) c = r; // 8
const Nt = mod(c * (r - _1n) * D_MINUS_ONE_SQ - D); // 9
const s2 = s * s;
const W0 = mod((s + s) * D); // 10
const W1 = mod(Nt * SQRT_AD_MINUS_ONE); // 11
const W2 = mod(_1n - s2); // 12
const W3 = mod(_1n + s2); // 13
return new ed25519.ExtendedPoint(mod(W0 * W3), mod(W2 * W1), mod(W1 * W3), mod(W0 * W2));
}
/**
* Takes uniform output of 64-bit hash function like sha512 and converts it to `RistrettoPoint`.
* The hash-to-group operation applies Elligator twice and adds the results.
* **Note:** this is one-way map, there is no conversion from point to hash.
* https://ristretto.group/formulas/elligator.html
* @param hex 64-bit output of a hash function
*/
static hashToCurve(hex: Hex): RistrettoPoint {
hex = ensureBytes(hex, 64);
const r1 = bytes255ToNumberLE(hex.slice(0, 32));
const R1 = this.calcElligatorRistrettoMap(r1);
const r2 = bytes255ToNumberLE(hex.slice(32, 64));
const R2 = this.calcElligatorRistrettoMap(r2);
return new RistrettoPoint(R1.add(R2));
}
/**
* Converts ristretto-encoded string to ristretto point.
* https://ristretto.group/formulas/decoding.html
* @param hex Ristretto-encoded 32 bytes. Not every 32-byte string is valid ristretto encoding
*/
static fromHex(hex: Hex): RistrettoPoint {
hex = ensureBytes(hex, 32);
const { a, d, P } = ed25519.CURVE;
const { mod } = ed25519.utils;
const emsg = 'RistrettoPoint.fromHex: the hex is not valid encoding of RistrettoPoint';
const s = bytes255ToNumberLE(hex);
// 1. Check that s_bytes is the canonical encoding of a field element, or else abort.
// 3. Check that s is non-negative, or else abort
if (!equalBytes(numberToBytesLE(s, 32), hex) || isNegativeLE(s, P)) throw new Error(emsg);
const s2 = mod(s * s);
const u1 = mod(_1n + a * s2); // 4 (a is -1)
const u2 = mod(_1n - a * s2); // 5
const u1_2 = mod(u1 * u1);
const u2_2 = mod(u2 * u2);
const v = mod(a * d * u1_2 - u2_2); // 6
const { isValid, value: I } = invertSqrt(mod(v * u2_2)); // 7
const Dx = mod(I * u2); // 8
const Dy = mod(I * Dx * v); // 9
let x = mod((s + s) * Dx); // 10
if (isNegativeLE(x, P)) x = mod(-x); // 10
const y = mod(u1 * Dy); // 11
const t = mod(x * y); // 12
if (!isValid || isNegativeLE(t, P) || y === _0n) throw new Error(emsg);
return new RistrettoPoint(new ed25519.ExtendedPoint(x, y, _1n, t));
}
/**
* Encodes ristretto point to Uint8Array.
* https://ristretto.group/formulas/encoding.html
*/
toRawBytes(): Uint8Array {
let { x, y, z, t } = this.ep;
const { P } = ed25519.CURVE;
const { mod } = ed25519.utils;
const u1 = mod(mod(z + y) * mod(z - y)); // 1
const u2 = mod(x * y); // 2
// Square root always exists
const u2sq = mod(u2 * u2);
const { value: invsqrt } = invertSqrt(mod(u1 * u2sq)); // 3
const D1 = mod(invsqrt * u1); // 4
const D2 = mod(invsqrt * u2); // 5
const zInv = mod(D1 * D2 * t); // 6
let D: bigint; // 7
if (isNegativeLE(t * zInv, P)) {
let _x = mod(y * SQRT_M1);
let _y = mod(x * SQRT_M1);
x = _x;
y = _y;
D = mod(D1 * INVSQRT_A_MINUS_D);
} else {
D = D2; // 8
}
if (isNegativeLE(x * zInv, P)) y = mod(-y); // 9
let s = mod((z - y) * D); // 10 (check footer's note, no sqrt(-a))
if (isNegativeLE(s, P)) s = mod(-s);
return numberToBytesLE(s, 32); // 11
}
toHex(): string {
return bytesToHex(this.toRawBytes());
}
toString(): string {
return this.toHex();
}
// Compare one point to another.
equals(other: RistrettoPoint): boolean {
assertRstPoint(other);
const a = this.ep;
const b = other.ep;
const { mod } = ed25519.utils;
// (x1 * y2 == y1 * x2) | (y1 * y2 == x1 * x2)
const one = mod(a.x * b.y) === mod(a.y * b.x);
const two = mod(a.y * b.y) === mod(a.x * b.x);
return one || two;
}
add(other: RistrettoPoint): RistrettoPoint {
assertRstPoint(other);
return new RistrettoPoint(this.ep.add(other.ep));
}
subtract(other: RistrettoPoint): RistrettoPoint {
assertRstPoint(other);
return new RistrettoPoint(this.ep.subtract(other.ep));
}
multiply(scalar: number | bigint): RistrettoPoint {
return new RistrettoPoint(this.ep.multiply(scalar));
}
multiplyUnsafe(scalar: number | bigint): RistrettoPoint {
return new RistrettoPoint(this.ep.multiplyUnsafe(scalar));
}
}

@ -2,6 +2,16 @@
import { sha256 } from '@noble/hashes/sha256'; import { sha256 } from '@noble/hashes/sha256';
import { mod, pow2 } from '@noble/curves/modular'; import { mod, pow2 } from '@noble/curves/modular';
import { createCurve } from './_shortw_utils.js'; import { createCurve } from './_shortw_utils.js';
import { PointType } from '@noble/curves/weierstrass';
import {
ensureBytes,
concatBytes,
Hex,
hexToBytes,
bytesToNumberBE,
PrivKey,
} from '@noble/curves/utils';
import { randomBytes } from '@noble/hashes/utils';
/** /**
* secp256k1 definition with efficient square root and endomorphism. * secp256k1 definition with efficient square root and endomorphism.
@ -17,6 +27,32 @@ const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd
const _1n = BigInt(1); const _1n = BigInt(1);
const _2n = BigInt(2); const _2n = BigInt(2);
const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b;
function sqrtMod(x: bigint): bigint {
const P = secp256k1P;
const _3n = BigInt(3);
const _6n = BigInt(6);
const _11n = BigInt(11);
const _22n = BigInt(22);
const _23n = BigInt(23);
const _44n = BigInt(44);
const _88n = BigInt(88);
const b2 = (x * x * x) % P; // x^3, 11
const b3 = (b2 * b2 * x) % P; // x^7
const b6 = (pow2(b3, _3n, P) * b3) % P;
const b9 = (pow2(b6, _3n, P) * b3) % P;
const b11 = (pow2(b9, _2n, P) * b2) % P;
const b22 = (pow2(b11, _11n, P) * b11) % P;
const b44 = (pow2(b22, _22n, P) * b22) % P;
const b88 = (pow2(b44, _44n, P) * b44) % P;
const b176 = (pow2(b88, _88n, P) * b88) % P;
const b220 = (pow2(b176, _44n, P) * b44) % P;
const b223 = (pow2(b220, _3n, P) * b3) % P;
const t1 = (pow2(b223, _23n, P) * b22) % P;
const t2 = (pow2(t1, _6n, P) * b2) % P;
return pow2(t2, _2n, P);
}
export const secp256k1 = createCurve( export const secp256k1 = createCurve(
{ {
a: 0n, a: 0n,
@ -37,30 +73,7 @@ export const secp256k1 = createCurve(
// We are unwrapping the loop because it's 2x faster. // 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] // (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00]
// We are multiplying it bit-by-bit // We are multiplying it bit-by-bit
sqrtMod: (x: bigint): bigint => { sqrtMod,
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: { endo: {
beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
splitScalar: (k: bigint) => { splitScalar: (k: bigint) => {
@ -88,3 +101,160 @@ export const secp256k1 = createCurve(
}, },
sha256 sha256
); );
// Schnorr
const _0n = BigInt(0);
const numTo32b = secp256k1.utils._bigintToBytes;
const numTo32bStr = secp256k1.utils._bigintToString;
const normalizePrivateKey = secp256k1.utils._normalizePrivateKey;
// TODO: export?
function normalizePublicKey(publicKey: Hex | PointType): PointType {
if (publicKey instanceof secp256k1.Point) {
publicKey.assertValidity();
return publicKey;
} else {
const bytes = ensureBytes(publicKey);
// Schnorr is 32 bytes
if (bytes.length === 32) {
const x = bytesToNumberBE(bytes);
if (!isValidFieldElement(x)) throw new Error('Point is not on curve');
const y2 = secp256k1.utils._weierstrassEquation(x); // y² = x³ + ax + b
let y = sqrtMod(y2); // y = y² ^ (p+1)/4
const isYOdd = (y & _1n) === _1n;
// Schnorr
if (isYOdd) y = mod(-y, secp256k1.CURVE.P);
const point = new secp256k1.Point(x, y);
point.assertValidity();
return point;
}
// Do we need that in schnorr at all?
return secp256k1.Point.fromHex(publicKey);
}
}
const isWithinCurveOrder = secp256k1.utils._isWithinCurveOrder;
const isValidFieldElement = secp256k1.utils._isValidFieldElement;
const TAGS = {
challenge: 'BIP0340/challenge',
aux: 'BIP0340/aux',
nonce: 'BIP0340/nonce',
} as const;
/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */
const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {};
export function taggedHash(tag: string, ...messages: Uint8Array[]): Uint8Array {
let tagP = TAGGED_HASH_PREFIXES[tag];
if (tagP === undefined) {
const tagH = sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0)));
tagP = concatBytes(tagH, tagH);
TAGGED_HASH_PREFIXES[tag] = tagP;
}
return sha256(concatBytes(tagP, ...messages));
}
const toRawX = (point: PointType) => point.toRawBytes(true).slice(1);
// Schnorr signatures are superior to ECDSA from above.
// Below is Schnorr-specific code as per BIP0340.
function schnorrChallengeFinalize(ch: Uint8Array): bigint {
return mod(bytesToNumberBE(ch), secp256k1.CURVE.n);
}
// Do we need this at all for Schnorr?
class SchnorrSignature {
constructor(readonly r: bigint, readonly s: bigint) {
this.assertValidity();
}
static fromHex(hex: Hex) {
const bytes = ensureBytes(hex);
if (bytes.length !== 64)
throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`);
const r = bytesToNumberBE(bytes.subarray(0, 32));
const s = bytesToNumberBE(bytes.subarray(32, 64));
return new SchnorrSignature(r, s);
}
assertValidity() {
const { r, s } = this;
if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) throw new Error('Invalid signature');
}
toHex(): string {
return numTo32bStr(this.r) + numTo32bStr(this.s);
}
toRawBytes(): Uint8Array {
return hexToBytes(this.toHex());
}
}
function schnorrGetScalar(priv: bigint) {
const point = secp256k1.Point.fromPrivateKey(priv);
const scalar = point.hasEvenY() ? priv : secp256k1.CURVE.n - priv;
return { point, scalar, x: toRawX(point) };
}
/**
* Synchronously creates Schnorr signature. Improved security: verifies itself before
* producing an output.
* @param msg message (not message hash)
* @param privateKey private key
* @param auxRand random bytes that would be added to k. Bad RNG won't break it.
*/
function schnorrSign(
message: Hex,
privateKey: PrivKey,
auxRand: Hex = randomBytes(32)
): Uint8Array {
if (message == null) throw new TypeError(`sign: Expected valid message, not "${message}"`);
const m = ensureBytes(message);
// checks for isWithinCurveOrder
const { x: px, scalar: d } = schnorrGetScalar(normalizePrivateKey(privateKey));
const rand = ensureBytes(auxRand);
if (rand.length !== 32) throw new TypeError('sign: Expected 32 bytes of aux randomness');
const tag = taggedHash;
const t0h = tag(TAGS.aux, rand);
const t = numTo32b(d ^ bytesToNumberBE(t0h));
const k0h = tag(TAGS.nonce, t, px, m);
const k0 = mod(bytesToNumberBE(k0h), secp256k1.CURVE.n);
if (k0 === _0n) throw new Error('sign: Creation of signature failed. k is zero');
const { point: R, x: rx, scalar: k } = schnorrGetScalar(k0);
const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m));
const sig = new SchnorrSignature(R.x, mod(k + e * d, secp256k1.CURVE.n)).toRawBytes();
if (!schnorrVerify(sig, m, px)) throw new Error('sign: Invalid signature produced');
return sig;
}
/**
* Verifies Schnorr signature synchronously.
*/
function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): boolean {
try {
const raw = signature instanceof SchnorrSignature;
const sig: SchnorrSignature = raw ? signature : SchnorrSignature.fromHex(signature);
if (raw) sig.assertValidity(); // just in case
const { r, s } = sig;
const m = ensureBytes(message);
const P = normalizePublicKey(publicKey);
const e = schnorrChallengeFinalize(taggedHash(TAGS.challenge, numTo32b(r), toRawX(P), m));
// Finalize
// R = s⋅G - e⋅P
// -eP == (n-e)P
const R = secp256k1.Point.BASE.multiplyAndAddUnsafe(
P,
normalizePrivateKey(s),
mod(-e, secp256k1.CURVE.n)
);
if (!R || !R.hasEvenY() || R.x !== r) return false;
return true;
} catch (error) {
return false;
}
}
export const schnorr = {
Signature: SchnorrSignature,
// Schnorr's pubkey is just `x` of Point (BIP340)
getPublicKey: (privateKey: PrivKey): Uint8Array =>
toRawX(secp256k1.Point.fromPrivateKey(privateKey)),
sign: schnorrSign,
verify: schnorrVerify,
};

@ -19,7 +19,8 @@ import { jubjub } from '../lib/jubjub.js';
// prettier-ignore // prettier-ignore
const CURVES = { const CURVES = {
secp192r1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1, secp192r1, secp224r1, secp256r1, secp384r1, secp521r1,
secp256k1,
ed25519, ed25519ctx, ed25519ph, ed25519, ed25519ctx, ed25519ph,
ed448, ed448ph, ed448, ed448ph,
starkCurve, starkCurve,
@ -46,14 +47,15 @@ for (const name in CURVES) {
const CURVE_ORDER = C.CURVE.n; const CURVE_ORDER = C.CURVE.n;
const FC_BIGINT = fc.bigInt(1n + 1n, CURVE_ORDER - 1n); 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 // Check that curve doesn't accept points from other curves
const O = name === 'secp256k1' ? secp256r1 : secp256k1; const O = name === 'secp256k1' ? secp256r1 : secp256k1;
const OTHER_POINTS = { const POINTS = {};
Point: O.Point, const OTHER_POINTS = {};
JacobianPoint: O.JacobianPoint, for (const name of ['Point', 'JacobianPoint', 'ExtendedPoint', 'ProjectivePoint']) {
ExtendedPoint: O.ExtendedPoint, POINTS[name] = C[name];
}; OTHER_POINTS[name] = O[name];
}
for (const pointName in POINTS) { for (const pointName in POINTS) {
const p = POINTS[pointName]; const p = POINTS[pointName];
const o = OTHER_POINTS[pointName]; const o = OTHER_POINTS[pointName];
@ -120,6 +122,7 @@ for (const name in CURVES) {
fc.assert( fc.assert(
fc.property(FC_BIGINT, FC_BIGINT, (a, b) => { fc.property(FC_BIGINT, FC_BIGINT, (a, b) => {
const c = mod.mod(a + b, CURVE_ORDER); const c = mod.mod(a + b, CURVE_ORDER);
if (c === CURVE_ORDER || c < 1n) return;
const pA = G[1].multiply(a); const pA = G[1].multiply(a);
const pB = G[1].multiply(b); const pB = G[1].multiply(b);
const pC = G[1].multiply(c); const pC = G[1].multiply(c);
@ -157,7 +160,7 @@ for (const name in CURVES) {
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1][op](new Uint8Array(4096).fill(1)), 'ui8a(4096*[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}`); if (G[1].toAffine) throws(() => G[1][op](C.Point.BASE), `Point ${op} ${pointName}`);
throws(() => G[1][op](O.BASE), `${op}/other curve point`); throws(() => G[1][op](o.BASE), `${op}/other curve point`);
}); });
} }
@ -177,7 +180,7 @@ for (const name in CURVES) {
throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])'); throws(() => G[1].equals(new Uint8Array([1])), 'ui8a([1])');
throws(() => G[1].equals(new Uint8Array(4096).fill(1)), 'ui8a(4096*[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})`); if (G[1].toAffine) throws(() => G[1].equals(C.Point.BASE), `Point.equals(${pointName})`);
throws(() => G[1].equals(O.BASE), 'other curve point'); throws(() => G[1].equals(o.BASE), 'other curve point');
}); });
for (const op of ['multiply', 'multiplyUnsafe']) { for (const op of ['multiply', 'multiplyUnsafe']) {
@ -199,7 +202,7 @@ for (const name in CURVES) {
throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])'); throws(() => G[1][op](new Uint8Array([0])), 'ui8a([0])');
throws(() => G[1][op](new Uint8Array([1])), 'ui8a([1])'); 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](new Uint8Array(4096).fill(1)), 'ui8a(4096*[1])');
throws(() => G[1][op](O.BASE), 'other curve point'); throws(() => G[1][op](o.BASE), 'other curve point');
}); });
} }
// Complex point (Extended/Jacobian/Projective?) // Complex point (Extended/Jacobian/Projective?)

@ -1,11 +1,12 @@
import { deepStrictEqual, throws } from 'assert'; import { deepStrictEqual, throws } from 'assert';
import { should } from 'micro-should'; import { should } from 'micro-should';
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { ed25519, ed25519ctx, ed25519ph, x25519 } from '../lib/ed25519.js'; import { ed25519, ed25519ctx, ed25519ph, x25519, RistrettoPoint } from '../lib/ed25519.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' }; import { default as zip215 } from './ed25519/zip215.json' assert { type: 'json' };
import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils'; import { hexToBytes, bytesToHex, randomBytes } from '@noble/hashes/utils';
import { numberToBytesLE } from '@noble/curves/utils'; import { numberToBytesLE } from '@noble/curves/utils';
import { sha512 } from '@noble/hashes/sha512';
import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' }; import { default as ed25519vectors } from './wycheproof/eddsa_test.json' assert { type: 'json' };
import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' }; import { default as x25519vectors } from './wycheproof/x25519_test.json' assert { type: 'json' };
@ -242,18 +243,17 @@ should('rfc8032 vectors/should create right signature for 0xf5 and long msg', ()
); );
}); });
// const { RistrettoPoint } = ed; // const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn;
// // const PRIVATE_KEY = 0xa665a45920422f9d417e4867efn; // const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103]));
// // const MESSAGE = ripemd160(new Uint8Array([97, 98, 99, 100, 101, 102, 103])); // prettier-ignore
// // prettier-ignore // const MESSAGE = new Uint8Array([
// // const MESSAGE = new Uint8Array([ // 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58,
// // 135, 79, 153, 96, 197, 210, 183, 169, 181, 250, 211, 131, 225, 186, 68, 113, 158, 187, 116, 58, // ]);
// // ]); // const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103]));
// // const WRONG_MESSAGE = ripemd160(new Uint8Array([98, 99, 100, 101, 102, 103])); // prettier-ignore
// // prettier-ignore // const WRONG_MESSAGE = new Uint8Array([
// // const WRONG_MESSAGE = new Uint8Array([ // 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28,
// // 88, 157, 140, 127, 29, 160, 162, 75, 192, 123, 115, 129, 173, 72, 177, 207, 194, 17, 175, 28, // ]);
// // ]);
// // it("should verify just signed message", async () => { // // it("should verify just signed message", async () => {
// // await fc.assert(fc.asyncProperty( // // await fc.assert(fc.asyncProperty(
// // fc.hexa(), // // fc.hexa(),
@ -300,104 +300,104 @@ should('rfc8032 vectors/should create right signature for 0xf5 and long msg', ()
// // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY); // // const signature = await ristretto25519.sign(MESSAGE, PRIVATE_KEY);
// // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false); // // expect(await ristretto25519.verify(signature, WRONG_MESSAGE, publicKey)).toBe(false);
// // }); // // });
// should('ristretto255/should follow the byte encodings of small multiples', () => { should('ristretto255/should follow the byte encodings of small multiples', () => {
// const encodingsOfSmallMultiples = [ const encodingsOfSmallMultiples = [
// // This is the identity point // This is the identity point
// '0000000000000000000000000000000000000000000000000000000000000000', '0000000000000000000000000000000000000000000000000000000000000000',
// // This is the basepoint // This is the basepoint
// 'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76', 'e2f2ae0a6abc4e71a884a961c500515f58e30b6aa582dd8db6a65945e08d2d76',
// // These are small multiples of the basepoint // These are small multiples of the basepoint
// '6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919', '6a493210f7499cd17fecb510ae0cea23a110e8d5b901f8acadd3095c73a3b919',
// '94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259', '94741f5d5d52755ece4f23f044ee27d5d1ea1e2bd196b462166b16152a9d0259',
// 'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57', 'da80862773358b466ffadfe0b3293ab3d9fd53c5ea6c955358f568322daf6a57',
// 'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e', 'e882b131016b52c1d3337080187cf768423efccbb517bb495ab812c4160ff44e',
// 'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403', 'f64746d3c92b13050ed8d80236a7f0007c3b3f962f5ba793d19a601ebb1df403',
// '44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d', '44f53520926ec81fbd5a387845beb7df85a96a24ece18738bdcfa6a7822a176d',
// '903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c', '903293d8f2287ebe10e2374dc1a53e0bc887e592699f02d077d5263cdd55601c',
// '02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031', '02622ace8f7303a31cafc63f8fc48fdc16e1c8c8d234b2f0d6685282a9076031',
// '20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f', '20706fd788b2720a1ed2a5dad4952b01f413bcf0e7564de8cdc816689e2db95f',
// 'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42', 'bce83f8ba5dd2fa572864c24ba1810f9522bc6004afe95877ac73241cafdab42',
// 'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460', 'e4549ee16b9aa03099ca208c67adafcafa4c3f3e4e5303de6026e3ca8ff84460',
// 'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f', 'aa52e000df2e16f55fb1032fc33bc42742dad6bd5a8fc0be0167436c5948501f',
// '46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e', '46376b80f409b29dc2b5f6f0c52591990896e5716f41477cd30085ab7f10301e',
// 'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e', 'e0c418f7c8d9c4cdd7395b93ea124f3ad99021bb681dfc3302a9d99a2e53e64e',
// ]; ];
// let B = RistrettoPoint.BASE; let B = RistrettoPoint.BASE;
// let P = RistrettoPoint.ZERO; let P = RistrettoPoint.ZERO;
// for (const encoded of encodingsOfSmallMultiples) { for (const encoded of encodingsOfSmallMultiples) {
// deepStrictEqual(P.toHex(), encoded); deepStrictEqual(P.toHex(), encoded);
// deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded); deepStrictEqual(RistrettoPoint.fromHex(encoded).toHex(), encoded);
// P = P.add(B); P = P.add(B);
// } }
// }); });
// should('ristretto255/should not convert bad bytes encoding', () => { should('ristretto255/should not convert bad bytes encoding', () => {
// const badEncodings = [ const badEncodings = [
// // These are all bad because they're non-canonical field encodings. // These are all bad because they're non-canonical field encodings.
// '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', '00ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
// 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// 'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', 'f3ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// 'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', 'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// // These are all bad because they're negative field elements. // These are all bad because they're negative field elements.
// '0100000000000000000000000000000000000000000000000000000000000000', '0100000000000000000000000000000000000000000000000000000000000000',
// '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', '01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// 'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20', 'ed57ffd8c914fb201471d1c3d245ce3c746fcbe63a3679d51b6a516ebebe0e20',
// 'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562', 'c34c4e1826e5d403b78e246e88aa051c36ccf0aafebffe137d148a2bf9104562',
// 'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78', 'c940e5a4404157cfb1628b108db051a8d439e1a421394ec4ebccb9ec92a8ac78',
// '47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24', '47cfc5497c53dc8e61c91d17fd626ffb1c49e2bca94eed052281b510b1117a24',
// 'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72', 'f1c6165d33367351b0da8f6e4511010c68174a03b6581212c71c0e1d026c3c72',
// '87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309', '87260f7a2f12495118360f02c26a470f450dadf34a413d21042b43b9d93e1309',
// // These are all bad because they give a nonsquare x^2. // These are all bad because they give a nonsquare x^2.
// '26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371', '26948d35ca62e643e26a83177332e6b6afeb9d08e4268b650f1f5bbd8d81d371',
// '4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f', '4eac077a713c57b4f4397629a4145982c661f48044dd3f96427d40b147d9742f',
// 'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b', 'de6a7b00deadc788eb6b6c8d20c0ae96c2f2019078fa604fee5b87d6e989ad7b',
// 'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042', 'bcab477be20861e01e4a0e295284146a510150d9817763caf1a6f4b422d67042',
// '2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08', '2a292df7e32cababbd9de088d1d1abec9fc0440f637ed2fba145094dc14bea08',
// 'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22', 'f4a9e534fc0d216c44b218fa0c42d99635a0127ee2e53c712f70609649fdff22',
// '8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731', '8268436f8c4126196cf64b3c7ddbda90746a378625f9813dd9b8457077256731',
// '2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b', '2810e5cbc2cc4d4eece54f61c6f69758e289aa7ab440b3cbeaa21995c2f4232b',
// // These are all bad because they give a negative xy value. // These are all bad because they give a negative xy value.
// '3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e', '3eb858e78f5a7254d8c9731174a94f76755fd3941c0ac93735c07ba14579630e',
// 'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220', 'a45fdc55c76448c049a1ab33f17023edfb2be3581e9c7aade8a6125215e04220',
// 'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e', 'd483fe813c6ba647ebbfd3ec41adca1c6130c2beeee9d9bf065c8d151c5f396e',
// '8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32', '8a2e1d30050198c65a54483123960ccc38aef6848e1ec8f5f780e8523769ba32',
// '32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b', '32888462f8b486c68ad7dd9610be5192bbeaf3b443951ac1a8118419d9fa097b',
// '227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165', '227142501b9d4355ccba290404bde41575b037693cef1f438c47f8fbf35d1165',
// '5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e', '5c37cc491da847cfeb9281d407efc41e15144c876e0170b499a96a22ed31e01e',
// '445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b', '445425117cb8c90edcbc7c1cc0e74f747f2c1efa5630a967c64f287792a48a4b',
// // This is s = -1, which causes y = 0. // This is s = -1, which causes y = 0.
// 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f', 'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
// ]; ];
// for (const badBytes of badEncodings) { for (const badBytes of badEncodings) {
// const b = hexToBytes(badBytes); const b = hexToBytes(badBytes);
// throws(() => RistrettoPoint.fromHex(b)); throws(() => RistrettoPoint.fromHex(b), badBytes);
// } }
// }); });
// should('ristretto255/should create right points from uniform hash', async () => { should('ristretto255/should create right points from uniform hash', async () => {
// const labels = [ const labels = [
// 'Ristretto is traditionally a short shot of espresso coffee', 'Ristretto is traditionally a short shot of espresso coffee',
// 'made with the normal amount of ground coffee but extracted with', 'made with the normal amount of ground coffee but extracted with',
// 'about half the amount of water in the same amount of time', 'about half the amount of water in the same amount of time',
// 'by using a finer grind.', 'by using a finer grind.',
// 'This produces a concentrated shot of coffee per volume.', 'This produces a concentrated shot of coffee per volume.',
// 'Just pulling a normal shot short will produce a weaker shot', 'Just pulling a normal shot short will produce a weaker shot',
// 'and is not a Ristretto as some believe.', 'and is not a Ristretto as some believe.',
// ]; ];
// const encodedHashToPoints = [ const encodedHashToPoints = [
// '3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46', '3066f82a1a747d45120d1740f14358531a8f04bbffe6a819f86dfe50f44a0a46',
// 'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b', 'f26e5b6f7d362d2d2a94c5d0e7602cb4773c95a2e5c31a64f133189fa76ed61b',
// '006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826', '006ccd2a9e6867e6a2c5cea83d3302cc9de128dd2a9a57dd8ee7b9d7ffe02826',
// 'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a', 'f8f0c87cf237953c5890aec3998169005dae3eca1fbb04548c635953c817f92a',
// 'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179', 'ae81e7dedf20a497e10c304a765c1767a42d6e06029758d2d7e8ef7cc4c41179',
// 'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628', 'e2705652ff9f5e44d3e841bf1c251cf7dddb77d140870d1ab2ed64f1a9ce8628',
// '80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065', '80bd07262511cdde4863f8a7434cef696750681cb9510eea557088f76d9e5065',
// ]; ];
// for (let i = 0; i < labels.length; i++) { for (let i = 0; i < labels.length; i++) {
// const hash = sha512(utf8ToBytes(labels[i])); const hash = sha512(utf8ToBytes(labels[i]));
// const point = RistrettoPoint.hashToCurve(hash); const point = RistrettoPoint.hashToCurve(hash);
// deepStrictEqual(point.toHex(), encodedHashToPoints[i]); deepStrictEqual(point.toHex(), encodedHashToPoints[i]);
// } }
// }); });
should('input immutability: sign/verify are immutable', () => { should('input immutability: sign/verify are immutable', () => {
const privateKey = ed.utils.randomPrivateKey(); const privateKey = ed.utils.randomPrivateKey();

@ -1,5 +1,5 @@
import * as fc from 'fast-check'; import * as fc from 'fast-check';
import { secp256k1 } from '../lib/secp256k1.js'; import { secp256k1, schnorr } from '../lib/secp256k1.js';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' }; import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' };
import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' }; import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' };
@ -341,37 +341,25 @@ should(
} }
); );
// describe('schnorr', () => { // index,secret key,public key,aux_rand,message,signature,verification result,comment
// // index,secret key,public key,aux_rand,message,signature,verification result,comment const vectors = schCsv
// const vectors = schCsv .split('\n')
// .split('\n') .map((line) => line.split(','))
// .map((line: string) => line.split(',')) .slice(1, -1);
// .slice(1, -1); for (let vec of vectors) {
// for (let vec of vectors) { const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec;
// const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec; should(`sign with Schnorr scheme vector ${index}`, () => {
// it(`should sign with Schnorr scheme vector ${index}`, async () => { if (sec) {
// if (sec) { deepStrictEqual(hex(schnorr.getPublicKey(sec)), pub.toLowerCase());
// expect(hex(secp.schnorr.getPublicKey(sec))).toBe(pub.toLowerCase()); const sig = schnorr.sign(msg, sec, rnd);
// const sig = await secp.schnorr.sign(msg, sec, rnd); deepStrictEqual(hex(sig), expSig.toLowerCase());
// const sigS = secp.schnorr.signSync(msg, sec, rnd); deepStrictEqual(schnorr.verify(sig, msg, pub), true);
// expect(hex(sig)).toBe(expSig.toLowerCase()); } else {
// expect(hex(sigS)).toBe(expSig.toLowerCase()); const passed = schnorr.verify(expSig, msg, pub);
// expect(await secp.schnorr.verify(sigS, msg, pub)).toBe(true); deepStrictEqual(passed, passes === 'TRUE');
// expect(secp.schnorr.verifySync(sig, msg, pub)).toBe(true); }
// } else { });
// const passed = await secp.schnorr.verify(expSig, msg, pub); }
// const passedS = secp.schnorr.verifySync(expSig, msg, pub);
// if (passes === 'TRUE') {
// expect(passed).toBeTruthy();
// expect(passedS).toBeTruthy();
// } else {
// expect(passed).toBeFalsy();
// expect(passedS).toBeFalsy();
// }
// }
// });
// }
// });
should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', () => { should('secp256k1.recoverPublicKey()/should recover public key from recovery bit', () => {
const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; const message = '00000000000000000000000000000000000000000000000000000000deadbeef';

@ -1,4 +1,4 @@
import * as microStark from '../../../lib/starknet.js'; import * as microStark from '../../../lib/stark.js';
import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils'; import * as starkwareCrypto from '@starkware-industries/starkware-crypto-utils';
import * as bench from 'micro-bmark'; import * as bench from 'micro-bmark';
const { run, mark } = bench; // or bench.mark const { run, mark } = bench; // or bench.mark

@ -1,3 +1,5 @@
import './basic.test.js'; import './basic.test.js';
import './stark.test.js'; import './stark.test.js';
import './property.test.js'; import './property.test.js';

@ -2,8 +2,8 @@
// Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y² // Implementation of Twisted Edwards curve. The formula is: ax² + y² = 1 + dx²y²
// Differences from @noble/ed25519 1.7: // Differences from @noble/ed25519 1.7:
// 1. EDDSA & ECDH have different field element lengths (for ed448/x448 only): // 1. Different field element lengths in ed448:
// RFC8032 bitLength is 456 bits (57 bytes), RFC7748 bitLength is 448 (56 bytes) // EDDSA (RFC8032) is 456 bits / 57 bytes, ECDH (RFC7748) is 448 bits / 56 bytes
// 2. Different addition formula (doubling is same) // 2. Different addition formula (doubling is same)
// 3. uvRatio differs between curves (half-expected, not only pow fn changes) // 3. uvRatio differs between curves (half-expected, not only pow fn changes)
// 4. Point decompression code is different too (unexpected), now using generalized formula // 4. Point decompression code is different too (unexpected), now using generalized formula
@ -19,6 +19,8 @@ import {
hashToPrivateScalar, hashToPrivateScalar,
BasicCurve, BasicCurve,
validateOpts as utilOpts, validateOpts as utilOpts,
Hex,
PrivKey,
} from './utils.js'; // TODO: import * as u from './utils.js'? } from './utils.js'; // TODO: import * as u from './utils.js'?
import { Group, GroupConstructor, wNAF } from './group.js'; import { Group, GroupConstructor, wNAF } from './group.js';
@ -48,11 +50,6 @@ export type CurveType = BasicCurve & {
preHash?: CHash; preHash?: CHash;
}; };
// We accept hex strings besides Uint8Array for simplicity
type Hex = Uint8Array | string;
// Very few implementations accept numbers, we do it to ease learning curve
type PrivKey = Hex | bigint | number;
// Should be separate from overrides, since overrides can use information about curve (for example nBits) // Should be separate from overrides, since overrides can use information about curve (for example nBits)
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = utilOpts(curve); const opts = utilOpts(curve);
@ -260,7 +257,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
const { a, d } = CURVE; const { a, d } = CURVE;
const { x: X1, y: Y1, z: Z1, t: T1 } = this; const { x: X1, y: Y1, z: Z1, t: T1 } = this;
const { x: X2, y: Y2, z: Z2, t: T2 } = other; const { x: X2, y: Y2, z: Z2, t: T2 } = other;
// Faster algo for adding 2 Extended Points when curve's a=-1. // 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 // http://hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html#addition-add-2008-hwcd-4
// Cost: 8M + 8add + 2*2. // Cost: 8M + 8add + 2*2.
@ -520,7 +516,6 @@ export function twistedEdwards(curveDef: CurveType): CurveFn {
} }
} }
// -------------------------
// Little-endian SHA512 with modulo n // Little-endian SHA512 with modulo n
function modlLE(hash: Uint8Array): bigint { function modlLE(hash: Uint8Array): bigint {
return mod.mod(bytesToNumberLE(hash), CURVE_ORDER); return mod.mod(bytesToNumberLE(hash), CURVE_ORDER);

@ -1,5 +1,10 @@
/*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ /*! @noble/curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
export type Hex = string | Uint8Array;
// We accept hex strings besides Uint8Array for simplicity
export type Hex = Uint8Array | string;
// Very few implementations accept numbers, we do it to ease learning curve
export type PrivKey = Hex | bigint | number;
// NOTE: these are generic, even if curve is on some polynominal field (bls), it will still have P/n/h // 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?) // But generator can be different (Fp2/Fp6 for bls?)
export type BasicCurve = { export type BasicCurve = {
@ -124,6 +129,7 @@ export function nLength(n: bigint, nBitLength?: number) {
* Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF * Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF
* and convert them into private scalar, with the modulo bias being neglible. * and convert them into private scalar, with the modulo bias being neglible.
* As per FIPS 186 B.4.1. * As per FIPS 186 B.4.1.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from sha512, or a similar function * @param hash hash output from sha512, or a similar function
* @returns valid private scalar * @returns valid private scalar
*/ */
@ -137,3 +143,10 @@ export function hashToPrivateScalar(hash: Hex, CURVE_ORDER: bigint, isLE = false
const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash); const num = isLE ? bytesToNumberLE(hash) : bytesToNumberBE(hash);
return mod.mod(num, CURVE_ORDER - _1n) + _1n; return mod.mod(num, CURVE_ORDER - _1n) + _1n;
} }
export function equalBytes(b1: Uint8Array, b2: Uint8Array) {
// We don't care about timing attacks here
if (b1.length !== b2.length) return false;
for (let i = 0; i < b1.length; i++) if (b1[i] !== b2[i]) return false;
return true;
}

@ -21,6 +21,8 @@ import {
hashToPrivateScalar, hashToPrivateScalar,
BasicCurve, BasicCurve,
validateOpts as utilOpts, validateOpts as utilOpts,
Hex,
PrivKey,
} from './utils.js'; } from './utils.js';
import { Group, GroupConstructor, wNAF } from './group.js'; import { Group, GroupConstructor, wNAF } from './group.js';
@ -54,11 +56,6 @@ export type CurveType = BasicCurve & {
endo?: EndomorphismOpts; endo?: EndomorphismOpts;
}; };
// We accept hex strings besides Uint8Array for simplicity
type Hex = Uint8Array | string;
// Very few implementations accept numbers, we do it to ease learning curve
type PrivKey = Hex | bigint | number;
// Should be separate from overrides, since overrides can use information about curve (for example nBits) // Should be separate from overrides, since overrides can use information about curve (for example nBits)
function validateOpts(curve: CurveType) { function validateOpts(curve: CurveType) {
const opts = utilOpts(curve); const opts = utilOpts(curve);
@ -244,6 +241,13 @@ export type CurveFn = {
utils: { utils: {
mod: (a: bigint, b?: bigint) => bigint; mod: (a: bigint, b?: bigint) => bigint;
invert: (number: bigint, modulo?: bigint) => bigint; invert: (number: bigint, modulo?: bigint) => bigint;
_bigintToBytes: (num: bigint) => Uint8Array;
_bigintToString: (num: bigint) => string;
_normalizePrivateKey: (key: PrivKey) => bigint;
_normalizePublicKey: (publicKey: PubKey) => PointType;
_isWithinCurveOrder: (num: bigint) => boolean;
_isValidFieldElement: (num: bigint) => boolean;
_weierstrassEquation: (x: bigint) => bigint;
isValidPrivateKey(privateKey: PrivKey): boolean; isValidPrivateKey(privateKey: PrivKey): boolean;
hashToPrivateKey: (hash: Hex) => Uint8Array; hashToPrivateKey: (hash: Hex) => Uint8Array;
randomPrivateKey: () => Uint8Array; randomPrivateKey: () => Uint8Array;
@ -649,7 +653,6 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
} }
const wnaf = wNAF(JacobianPoint, CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength); const wnaf = wNAF(JacobianPoint, CURVE.endo ? CURVE.nBitLength / 2 : CURVE.nBitLength);
// Stores precomputed values for points. // Stores precomputed values for points.
const pointPrecomputes = new WeakMap<Point, JacobianPoint[]>(); const pointPrecomputes = new WeakMap<Point, JacobianPoint[]>();
@ -923,20 +926,22 @@ export function weierstrass(curveDef: CurveType): CurveFn {
} }
}, },
_bigintToBytes: numToField, _bigintToBytes: numToField,
_bigintToString: numToFieldStr,
_normalizePrivateKey: normalizePrivateKey, _normalizePrivateKey: normalizePrivateKey,
_normalizePublicKey: normalizePublicKey,
_isWithinCurveOrder: isWithinCurveOrder,
_isValidFieldElement: isValidFieldElement,
_weierstrassEquation: weierstrassEquation,
/** /**
* Can take (keyLength + 8) or more bytes of uniform input e.g. from CSPRNG or KDF * Converts some bytes to a valid private key. Needs at least (nBitLength+64) bytes.
* and convert them into private key, with the modulo bias being neglible.
* As per FIPS 186 B.4.1.
* https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/
* @param hash hash output from sha512, or a similar function
* @returns valid private key
*/ */
hashToPrivateKey: (hash: Hex): Uint8Array => numToField(hashToPrivateScalar(hash, CURVE_ORDER)), hashToPrivateKey: (hash: Hex): Uint8Array => numToField(hashToPrivateScalar(hash, CURVE_ORDER)),
// Takes curve order + 64 bits from CSPRNG so that modulo bias is neglible, /**
// matches FIPS 186 B.4.1. * Produces cryptographically secure private key from random of size (nBitLength+64)
* as per FIPS 186 B.4.1 with modulo bias being neglible.
*/
randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(CURVE.randomBytes(fieldLen + 8)), randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(CURVE.randomBytes(fieldLen + 8)),
/** /**