UNPKG

@noble/curves

Version:

Audited & minimal JS implementation of elliptic curve cryptography

1,334 lines (1,240 loc) 62.9 kB
/** * Short Weierstrass curve methods. The formula is: y² = x³ + ax + b. * * ### Design rationale for types * * * Interaction between classes from different curves should fail: * `k256.Point.BASE.add(p256.Point.BASE)` * * For this purpose we want to use `instanceof` operator, which is fast and works during runtime * * Different calls of `curve()` would return different classes - * `curve(params) !== curve(params)`: if somebody decided to monkey-patch their curve, * it won't affect others * * TypeScript can't infer types for classes created inside a function. Classes is one instance * of nominative types in TypeScript and interfaces only check for shape, so it's hard to create * unique type for every function call. * * We can use generic types via some param, like curve opts, but that would: * 1. Enable interaction between `curve(params)` and `curve(params)` (curves of same params) * which is hard to debug. * 2. Params can be generic and we can't enforce them to be constant value: * if somebody creates curve from non-constant params, * it would be allowed to interact with other curves with non-constant params * * @todo https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#unique-symbol * @module */ /*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */ import { hmac } from '@noble/hashes/hmac.js'; import { _validateObject, abool, abytes, aInRange, bitMask, bytesToHex, bytesToNumberBE, concatBytes, createHmacDrbg, ensureBytes, hexToBytes, inRange, isBytes, memoized, numberToHexUnpadded, randomBytes, type CHash, type Hex, type PrivKey, } from '../utils.ts'; import { _createCurveFields, mulEndoUnsafe, negateCt, normalizeZ, pippenger, wNAF, type AffinePoint, type BasicCurve, type Group, type GroupConstructor, } from './curve.ts'; import { Field, FpInvertBatch, getMinHashLength, mapHashToField, validateField, type IField, type NLength, } from './modular.ts'; export type { AffinePoint }; export type HmacFnSync = (key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array; /** * When Weierstrass curve has `a=0`, it becomes Koblitz curve. * Koblitz curves allow using **efficiently-computable GLV endomorphism ψ**. * Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%. * For precomputed wNAF it trades off 1/2 init time & 1/3 ram for 20% perf hit. * * Endomorphism consists of beta, lambda and splitScalar: * * 1. GLV endomorphism ψ transforms a point: `P = (x, y) ↦ ψ(P) = (β·x mod p, y)` * 2. GLV scalar decomposition transforms a scalar: `k ≡ k₁ + k₂·λ (mod n)` * 3. Then these are combined: `k·P = k₁·P + k₂·ψ(P)` * 4. Two 128-bit point-by-scalar multiplications + one point addition is faster than * one 256-bit multiplication. * * where * * beta: β ∈ Fₚ with β³ = 1, β ≠ 1 * * lambda: λ ∈ Fₙ with λ³ = 1, λ ≠ 1 * * splitScalar decomposes k ↦ k₁, k₂, by using reduced basis vectors. * Gauss lattice reduction calculates them from initial basis vectors `(n, 0), (-λ, 0)` * * Check out `test/misc/endomorphism.js` and * [gist](https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066). */ export type EndomorphismOpts = { beta: bigint; splitScalar: (k: bigint) => { k1neg: boolean; k1: bigint; k2neg: boolean; k2: bigint }; }; export type BasicWCurve<T> = BasicCurve<T> & { // Params: a, b a: T; b: T; // Optional params allowedPrivateKeyLengths?: readonly number[]; // for P521 wrapPrivateKey?: boolean; // bls12-381 requires mod(n) instead of rejecting keys >= n endo?: EndomorphismOpts; // When a cofactor != 1, there can be an effective methods to: // 1. Determine whether a point is torsion-free isTorsionFree?: (c: ProjConstructor<T>, point: ProjPointType<T>) => boolean; // 2. Clear torsion component clearCofactor?: (c: ProjConstructor<T>, point: ProjPointType<T>) => ProjPointType<T>; }; export type Entropy = Hex | boolean; export type SignOpts = { lowS?: boolean; extraEntropy?: Entropy; prehash?: boolean }; export type VerOpts = { lowS?: boolean; prehash?: boolean; format?: 'compact' | 'der' | 'js' | undefined; }; function validateSigVerOpts(opts: SignOpts | VerOpts) { if (opts.lowS !== undefined) abool('lowS', opts.lowS); if (opts.prehash !== undefined) abool('prehash', opts.prehash); } /** Instance methods for 3D XYZ points. */ export interface ProjPointType<T> extends Group<ProjPointType<T>> { /** projective x coordinate. Note: different from .x */ readonly px: T; /** projective y coordinate. Note: different from .y */ readonly py: T; /** projective z coordinate */ readonly pz: T; /** affine x coordinate */ get x(): T; /** affine y coordinate */ get y(): T; assertValidity(): void; clearCofactor(): ProjPointType<T>; is0(): boolean; isTorsionFree(): boolean; multiplyUnsafe(scalar: bigint): ProjPointType<T>; /** * Massively speeds up `p.multiply(n)` by using wnaf precompute tables (caching). * Table generation takes 30MB of ram and 10ms on high-end CPU, but may take * much longer on slow devices. * Actual generation will happen on first call of `.multiply()`. * By default, BASE point is precomputed. * @param windowSize - table window size * @param isLazy - (default true) allows to defer generation */ precompute(windowSize?: number, isLazy?: boolean): ProjPointType<T>; /** Converts 3D XYZ projective point to 2D xy affine coordinates */ toAffine(invertedZ?: T): AffinePoint<T>; /** Encodes point using IEEE P1363 (DER) encoding. First byte is 2/3/4. Default = isCompressed. */ toBytes(isCompressed?: boolean): Uint8Array; toHex(isCompressed?: boolean): string; /** @deprecated use `toBytes` */ toRawBytes(isCompressed?: boolean): Uint8Array; /** @deprecated use `multiplyUnsafe` */ multiplyAndAddUnsafe(Q: ProjPointType<T>, a: bigint, b: bigint): ProjPointType<T> | undefined; /** @deprecated use `p.y % 2n === 0n` */ hasEvenY(): boolean; /** @deprecated use `p.precompute(windowSize)` */ _setWindowSize(windowSize: number): void; } /** Static methods for 3D XYZ points. */ export interface ProjConstructor<T> extends GroupConstructor<ProjPointType<T>> { Fp: IField<T>; Fn: IField<bigint>; /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ new (x: T, y: T, z: T): ProjPointType<T>; /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ fromAffine(p: AffinePoint<T>): ProjPointType<T>; fromBytes(encodedPoint: Uint8Array): ProjPointType<T>; fromHex(hex: Hex): ProjPointType<T>; fromPrivateKey(privateKey: PrivKey): ProjPointType<T>; normalizeZ(points: ProjPointType<T>[]): ProjPointType<T>[]; msm(points: ProjPointType<T>[], scalars: bigint[]): ProjPointType<T>; } export type CurvePointsType<T> = BasicWCurve<T> & { fromBytes?: (bytes: Uint8Array) => AffinePoint<T>; toBytes?: (c: ProjConstructor<T>, point: ProjPointType<T>, isCompressed: boolean) => Uint8Array; }; // LegacyWeierstrassOpts export type CurvePointsTypeWithLength<T> = Readonly<CurvePointsType<T> & Partial<NLength>>; // LegacyWeierstrass export type CurvePointsRes<T> = { /** @deprecated import individual CURVE params */ CURVE: CurvePointsType<T>; Point: ProjConstructor<T>; /** @deprecated use `Point` */ ProjectivePoint: ProjConstructor<T>; /** @deprecated */ normPrivateKeyToScalar: (key: PrivKey) => bigint; /** @deprecated */ weierstrassEquation: (x: T) => T; /** @deprecated use `Point.Fn.isValidNot0(num)` */ isWithinCurveOrder: (num: bigint) => boolean; }; // Aliases to legacy types // export type CurveType = LegacyECDSAOpts; // export type CurveFn = LegacyECDSA; // export type CurvePointsRes<T> = LegacyWeierstrass<T>; // export type CurvePointsType<T> = LegacyWeierstrassOpts<T>; // export type CurvePointsTypeWithLength<T> = LegacyWeierstrassOpts<T>; // export type BasicWCurve<T> = LegacyWeierstrassOpts<T>; /** * Weierstrass curve options. * * * p: prime characteristic (order) of finite field, in which arithmetics is done * * n: order of prime subgroup a.k.a total amount of valid curve points * * h: cofactor, usually 1. h*n is group order; n is subgroup order * * a: formula param, must be in field of p * * b: formula param, must be in field of p * * Gx: x coordinate of generator point a.k.a. base point * * Gy: y coordinate of generator point */ export type WeierstrassOpts<T> = Readonly<{ p: bigint; n: bigint; h: bigint; a: T; b: T; Gx: T; Gy: T; }>; // When a cofactor != 1, there can be an effective methods to: // 1. Determine whether a point is torsion-free // 2. Clear torsion component // wrapPrivateKey: bls12-381 requires mod(n) instead of rejecting keys >= n export type WeierstrassExtraOpts<T> = Partial<{ Fp: IField<T>; Fn: IField<bigint>; // TODO: remove allowedPrivateKeyLengths: readonly number[]; // for P521 allowInfinityPoint: boolean; endo: EndomorphismOpts; wrapPrivateKey: boolean; isTorsionFree: (c: ProjConstructor<T>, point: ProjPointType<T>) => boolean; clearCofactor: (c: ProjConstructor<T>, point: ProjPointType<T>) => ProjPointType<T>; fromBytes: (bytes: Uint8Array) => AffinePoint<T>; toBytes: (c: ProjConstructor<T>, point: ProjPointType<T>, isCompressed: boolean) => Uint8Array; }>; /** * Options for ECDSA signatures over a Weierstrass curve. */ export type ECDSAOpts = { hash: CHash; hmac?: HmacFnSync; randomBytes?: (bytesLength?: number) => Uint8Array; lowS?: boolean; bits2int?: (bytes: Uint8Array) => bigint; bits2int_modN?: (bytes: Uint8Array) => bigint; }; /** ECDSA is only supported for prime fields, not Fp2 (extension fields). */ export interface ECDSA { getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getSharedSecret: (privateA: PrivKey, publicB: Hex, isCompressed?: boolean) => Uint8Array; sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => RecoveredSignatureType; verify: (signature: Hex | SignatureLike, msgHash: Hex, publicKey: Hex, opts?: VerOpts) => boolean; Point: ProjConstructor<bigint>; Signature: SignatureConstructor; utils: { isValidPrivateKey(privateKey: PrivKey): boolean; randomPrivateKey: () => Uint8Array; // TODO: deprecate those two normPrivateKeyToScalar: (key: PrivKey) => bigint; /** @deprecated */ precompute: (windowSize?: number, point?: ProjPointType<bigint>) => ProjPointType<bigint>; }; } export class DERErr extends Error { constructor(m = '') { super(m); } } export type IDER = { // asn.1 DER encoding utils Err: typeof DERErr; // Basic building block is TLV (Tag-Length-Value) _tlv: { encode: (tag: number, data: string) => string; // v - value, l - left bytes (unparsed) decode(tag: number, data: Uint8Array): { v: Uint8Array; l: Uint8Array }; }; // https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag, // since we always use positive integers here. It must always be empty: // - add zero byte if exists // - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding) _int: { encode(num: bigint): string; decode(data: Uint8Array): bigint; }; toSig(hex: string | Uint8Array): { r: bigint; s: bigint }; hexFromSig(sig: { r: bigint; s: bigint }): string; }; /** * ASN.1 DER encoding utilities. ASN is very complex & fragile. Format: * * [0x30 (SEQUENCE), bytelength, 0x02 (INTEGER), intLength, R, 0x02 (INTEGER), intLength, S] * * Docs: https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/, https://luca.ntop.org/Teaching/Appunti/asn1.html */ export const DER: IDER = { // asn.1 DER encoding utils Err: DERErr, // Basic building block is TLV (Tag-Length-Value) _tlv: { encode: (tag: number, data: string): string => { const { Err: E } = DER; if (tag < 0 || tag > 256) throw new E('tlv.encode: wrong tag'); if (data.length & 1) throw new E('tlv.encode: unpadded data'); const dataLen = data.length / 2; const len = numberToHexUnpadded(dataLen); if ((len.length / 2) & 0b1000_0000) throw new E('tlv.encode: long form length too big'); // length of length with long form flag const lenLen = dataLen > 127 ? numberToHexUnpadded((len.length / 2) | 0b1000_0000) : ''; const t = numberToHexUnpadded(tag); return t + lenLen + len + data; }, // v - value, l - left bytes (unparsed) decode(tag: number, data: Uint8Array): { v: Uint8Array; l: Uint8Array } { const { Err: E } = DER; let pos = 0; if (tag < 0 || tag > 256) throw new E('tlv.encode: wrong tag'); if (data.length < 2 || data[pos++] !== tag) throw new E('tlv.decode: wrong tlv'); const first = data[pos++]; const isLong = !!(first & 0b1000_0000); // First bit of first length byte is flag for short/long form let length = 0; if (!isLong) length = first; else { // Long form: [longFlag(1bit), lengthLength(7bit), length (BE)] const lenLen = first & 0b0111_1111; if (!lenLen) throw new E('tlv.decode(long): indefinite length not supported'); if (lenLen > 4) throw new E('tlv.decode(long): byte length is too big'); // this will overflow u32 in js const lengthBytes = data.subarray(pos, pos + lenLen); if (lengthBytes.length !== lenLen) throw new E('tlv.decode: length bytes not complete'); if (lengthBytes[0] === 0) throw new E('tlv.decode(long): zero leftmost byte'); for (const b of lengthBytes) length = (length << 8) | b; pos += lenLen; if (length < 128) throw new E('tlv.decode(long): not minimal encoding'); } const v = data.subarray(pos, pos + length); if (v.length !== length) throw new E('tlv.decode: wrong value length'); return { v, l: data.subarray(pos + length) }; }, }, // https://crypto.stackexchange.com/a/57734 Leftmost bit of first byte is 'negative' flag, // since we always use positive integers here. It must always be empty: // - add zero byte if exists // - if next byte doesn't have a flag, leading zero is not allowed (minimal encoding) _int: { encode(num: bigint): string { const { Err: E } = DER; if (num < _0n) throw new E('integer: negative integers are not allowed'); let hex = numberToHexUnpadded(num); // Pad with zero byte if negative flag is present if (Number.parseInt(hex[0], 16) & 0b1000) hex = '00' + hex; if (hex.length & 1) throw new E('unexpected DER parsing assertion: unpadded hex'); return hex; }, decode(data: Uint8Array): bigint { const { Err: E } = DER; if (data[0] & 0b1000_0000) throw new E('invalid signature integer: negative'); if (data[0] === 0x00 && !(data[1] & 0b1000_0000)) throw new E('invalid signature integer: unnecessary leading zero'); return bytesToNumberBE(data); }, }, toSig(hex: string | Uint8Array): { r: bigint; s: bigint } { // parse DER signature const { Err: E, _int: int, _tlv: tlv } = DER; const data = ensureBytes('signature', hex); const { v: seqBytes, l: seqLeftBytes } = tlv.decode(0x30, data); if (seqLeftBytes.length) throw new E('invalid signature: left bytes after parsing'); const { v: rBytes, l: rLeftBytes } = tlv.decode(0x02, seqBytes); const { v: sBytes, l: sLeftBytes } = tlv.decode(0x02, rLeftBytes); if (sLeftBytes.length) throw new E('invalid signature: left bytes after parsing'); return { r: int.decode(rBytes), s: int.decode(sBytes) }; }, hexFromSig(sig: { r: bigint; s: bigint }): string { const { _tlv: tlv, _int: int } = DER; const rs = tlv.encode(0x02, int.encode(sig.r)); const ss = tlv.encode(0x02, int.encode(sig.s)); const seq = rs + ss; return tlv.encode(0x30, seq); }, }; // Be friendly to bad ECMAScript parsers by not using bigint literals // prettier-ignore const _0n = BigInt(0), _1n = BigInt(1), _2n = BigInt(2), _3n = BigInt(3), _4n = BigInt(4); // TODO: remove export function _legacyHelperEquat<T>(Fp: IField<T>, a: T, b: T): (x: T) => T { /** * y² = x³ + ax + b: Short weierstrass curve formula. Takes x, returns y². * @returns y² */ function weierstrassEquation(x: T): T { const x2 = Fp.sqr(x); // x * x const x3 = Fp.mul(x2, x); // x² * x return Fp.add(Fp.add(x3, Fp.mul(x, a)), b); // x³ + a * x + b } return weierstrassEquation; } export function _legacyHelperNormPriv( Fn: IField<bigint>, allowedPrivateKeyLengths?: readonly number[], wrapPrivateKey?: boolean ): (key: PrivKey) => bigint { const { BYTES: expected } = Fn; // Validates if priv key is valid and converts it to bigint. function normPrivateKeyToScalar(key: PrivKey): bigint { let num: bigint; if (typeof key === 'bigint') { num = key; } else { let bytes = ensureBytes('private key', key); if (allowedPrivateKeyLengths) { if (!allowedPrivateKeyLengths.includes(bytes.length * 2)) throw new Error('invalid private key'); const padded = new Uint8Array(expected); padded.set(bytes, padded.length - bytes.length); bytes = padded; } try { num = Fn.fromBytes(bytes); } catch (error) { throw new Error( `invalid private key: expected ui8a of size ${expected}, got ${typeof key}` ); } } if (wrapPrivateKey) num = Fn.create(num); // disabled by default, enabled for BLS if (!Fn.isValidNot0(num)) throw new Error('invalid private key: out of range [1..N-1]'); return num; } return normPrivateKeyToScalar; } export function weierstrassN<T>( CURVE: WeierstrassOpts<T>, curveOpts: WeierstrassExtraOpts<T> = {} ): ProjConstructor<T> { const { Fp, Fn } = _createCurveFields('weierstrass', CURVE, curveOpts); const { h: cofactor, n: CURVE_ORDER } = CURVE; _validateObject( curveOpts, {}, { allowInfinityPoint: 'boolean', clearCofactor: 'function', isTorsionFree: 'function', fromBytes: 'function', toBytes: 'function', endo: 'object', wrapPrivateKey: 'boolean', } ); const { endo } = curveOpts; if (endo) { // validateObject(endo, { beta: 'bigint', splitScalar: 'function' }); if ( !Fp.is0(CURVE.a) || typeof endo.beta !== 'bigint' || typeof endo.splitScalar !== 'function' ) { throw new Error('invalid endo: expected "beta": bigint and "splitScalar": function'); } } function assertCompressionIsSupported() { if (!Fp.isOdd) throw new Error('compression is not supported: Field does not have .isOdd()'); } // Implements IEEE P1363 point encoding function pointToBytes( _c: ProjConstructor<T>, point: ProjPointType<T>, isCompressed: boolean ): Uint8Array { const { x, y } = point.toAffine(); const bx = Fp.toBytes(x); abool('isCompressed', isCompressed); if (isCompressed) { assertCompressionIsSupported(); const hasEvenY = !Fp.isOdd!(y); return concatBytes(pprefix(hasEvenY), bx); } else { return concatBytes(Uint8Array.of(0x04), bx, Fp.toBytes(y)); } } function pointFromBytes(bytes: Uint8Array) { abytes(bytes); const L = Fp.BYTES; const LC = L + 1; // length compressed, e.g. 33 for 32-byte field const LU = 2 * L + 1; // length uncompressed, e.g. 65 for 32-byte field const length = bytes.length; const head = bytes[0]; const tail = bytes.subarray(1); // No actual validation is done here: use .assertValidity() if (length === LC && (head === 0x02 || head === 0x03)) { const x = Fp.fromBytes(tail); if (!Fp.isValid(x)) throw new Error('bad point: is not on curve, wrong x'); const y2 = weierstrassEquation(x); // y² = x³ + ax + b let y: T; try { y = Fp.sqrt(y2); // y = y² ^ (p+1)/4 } catch (sqrtError) { const err = sqrtError instanceof Error ? ': ' + sqrtError.message : ''; throw new Error('bad point: is not on curve, sqrt error' + err); } assertCompressionIsSupported(); const isYOdd = Fp.isOdd!(y); // (y & _1n) === _1n; const isHeadOdd = (head & 1) === 1; // ECDSA-specific if (isHeadOdd !== isYOdd) y = Fp.neg(y); return { x, y }; } else if (length === LU && head === 0x04) { // TODO: more checks const x = Fp.fromBytes(tail.subarray(L * 0, L * 1)); const y = Fp.fromBytes(tail.subarray(L * 1, L * 2)); if (!isValidXY(x, y)) throw new Error('bad point: is not on curve'); return { x, y }; } else { throw new Error( `bad point: got length ${length}, expected compressed=${LC} or uncompressed=${LU}` ); } } const toBytes = curveOpts.toBytes || pointToBytes; const fromBytes = curveOpts.fromBytes || pointFromBytes; const weierstrassEquation = _legacyHelperEquat(Fp, CURVE.a, CURVE.b); // TODO: move top-level /** Checks whether equation holds for given x, y: y² == x³ + ax + b */ function isValidXY(x: T, y: T): boolean { const left = Fp.sqr(y); // y² const right = weierstrassEquation(x); // x³ + ax + b return Fp.eql(left, right); } // Validate whether the passed curve params are valid. // Test 1: equation y² = x³ + ax + b should work for generator point. if (!isValidXY(CURVE.Gx, CURVE.Gy)) throw new Error('bad curve params: generator point'); // Test 2: discriminant Δ part should be non-zero: 4a³ + 27b² != 0. // Guarantees curve is genus-1, smooth (non-singular). const _4a3 = Fp.mul(Fp.pow(CURVE.a, _3n), _4n); const _27b2 = Fp.mul(Fp.sqr(CURVE.b), BigInt(27)); if (Fp.is0(Fp.add(_4a3, _27b2))) throw new Error('bad curve params: a or b'); /** Asserts coordinate is valid: 0 <= n < Fp.ORDER. */ function acoord(title: string, n: T, banZero = false) { if (!Fp.isValid(n) || (banZero && Fp.is0(n))) throw new Error(`bad point coordinate ${title}`); return n; } function aprjpoint(other: unknown) { if (!(other instanceof Point)) throw new Error('ProjectivePoint expected'); } // Memoized toAffine / validity check. They are heavy. Points are immutable. // Converts Projective point to affine (x, y) coordinates. // Can accept precomputed Z^-1 - for example, from invertBatch. // (X, Y, Z) ∋ (x=X/Z, y=Y/Z) const toAffineMemo = memoized((p: Point, iz?: T): AffinePoint<T> => { const { px: x, py: y, pz: z } = p; // Fast-path for normalized points if (Fp.eql(z, Fp.ONE)) return { x, y }; const is0 = p.is0(); // If invZ was 0, we return zero point. However we still want to execute // all operations, so we replace invZ with a random number, 1. if (iz == null) iz = is0 ? Fp.ONE : Fp.inv(z); const ax = Fp.mul(x, iz); const ay = Fp.mul(y, iz); const zz = Fp.mul(z, iz); if (is0) return { x: Fp.ZERO, y: Fp.ZERO }; if (!Fp.eql(zz, Fp.ONE)) throw new Error('invZ was invalid'); return { x: ax, y: ay }; }); // NOTE: on exception this will crash 'cached' and no value will be set. // Otherwise true will be return const assertValidMemo = memoized((p: Point) => { if (p.is0()) { // (0, 1, 0) aka ZERO is invalid in most contexts. // In BLS, ZERO can be serialized, so we allow it. // (0, 0, 0) is invalid representation of ZERO. if (curveOpts.allowInfinityPoint && !Fp.is0(p.py)) return; throw new Error('bad point: ZERO'); } // Some 3rd-party test vectors require different wording between here & `fromCompressedHex` const { x, y } = p.toAffine(); if (!Fp.isValid(x) || !Fp.isValid(y)) throw new Error('bad point: x or y not field elements'); if (!isValidXY(x, y)) throw new Error('bad point: equation left != right'); if (!p.isTorsionFree()) throw new Error('bad point: not in prime-order subgroup'); return true; }); function finishEndo( endoBeta: EndomorphismOpts['beta'], k1p: Point, k2p: Point, k1neg: boolean, k2neg: boolean ) { k2p = new Point(Fp.mul(k2p.px, endoBeta), k2p.py, k2p.pz); k1p = negateCt(k1neg, k1p); k2p = negateCt(k2neg, k2p); return k1p.add(k2p); } /** * Projective Point works in 3d / projective (homogeneous) coordinates:(X, Y, Z) ∋ (x=X/Z, y=Y/Z). * Default Point works in 2d / affine coordinates: (x, y). * We're doing calculations in projective, because its operations don't require costly inversion. */ class Point implements ProjPointType<T> { // base / generator point static readonly BASE = new Point(CURVE.Gx, CURVE.Gy, Fp.ONE); // zero / infinity / identity point static readonly ZERO = new Point(Fp.ZERO, Fp.ONE, Fp.ZERO); // 0, 1, 0 // fields static readonly Fp = Fp; static readonly Fn = Fn; readonly px: T; readonly py: T; readonly pz: T; /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ constructor(px: T, py: T, pz: T) { this.px = acoord('x', px); this.py = acoord('y', py, true); this.pz = acoord('z', pz); Object.freeze(this); } /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ static fromAffine(p: AffinePoint<T>): Point { const { x, y } = p || {}; if (!p || !Fp.isValid(x) || !Fp.isValid(y)) throw new Error('invalid affine point'); if (p instanceof Point) throw new Error('projective point not allowed'); // (0, 0) would've produced (0, 0, 1) - instead, we need (0, 1, 0) if (Fp.is0(x) && Fp.is0(y)) return Point.ZERO; return new Point(x, y, Fp.ONE); } get x(): T { return this.toAffine().x; } get y(): T { return this.toAffine().y; } static normalizeZ(points: Point[]): Point[] { return normalizeZ(Point, 'pz', points); } static fromBytes(bytes: Uint8Array): Point { abytes(bytes); return Point.fromHex(bytes); } /** Converts hash string or Uint8Array to Point. */ static fromHex(hex: Hex): Point { const P = Point.fromAffine(fromBytes(ensureBytes('pointHex', hex))); P.assertValidity(); return P; } /** Multiplies generator point by privateKey. */ static fromPrivateKey(privateKey: PrivKey) { const normPrivateKeyToScalar = _legacyHelperNormPriv( Fn, curveOpts.allowedPrivateKeyLengths, curveOpts.wrapPrivateKey ); return Point.BASE.multiply(normPrivateKeyToScalar(privateKey)); } /** Multiscalar Multiplication */ static msm(points: Point[], scalars: bigint[]): Point { return pippenger(Point, Fn, points, scalars); } /** * * @param windowSize * @param isLazy true will defer table computation until the first multiplication * @returns */ precompute(windowSize: number = 8, isLazy = true): Point { wnaf.setWindowSize(this, windowSize); if (!isLazy) this.multiply(_3n); // random number return this; } /** "Private method", don't use it directly */ _setWindowSize(windowSize: number) { this.precompute(windowSize); } // TODO: return `this` /** A point on curve is valid if it conforms to equation. */ assertValidity(): void { assertValidMemo(this); } hasEvenY(): boolean { const { y } = this.toAffine(); if (!Fp.isOdd) throw new Error("Field doesn't support isOdd"); return !Fp.isOdd(y); } /** Compare one point to another. */ equals(other: Point): boolean { aprjpoint(other); const { px: X1, py: Y1, pz: Z1 } = this; const { px: X2, py: Y2, pz: Z2 } = other; const U1 = Fp.eql(Fp.mul(X1, Z2), Fp.mul(X2, Z1)); const U2 = Fp.eql(Fp.mul(Y1, Z2), Fp.mul(Y2, Z1)); return U1 && U2; } /** Flips point to one corresponding to (x, -y) in Affine coordinates. */ negate(): Point { return new Point(this.px, Fp.neg(this.py), this.pz); } // Renes-Costello-Batina exception-free doubling formula. // There is 30% faster Jacobian formula, but it is not complete. // https://eprint.iacr.org/2015/1060, algorithm 3 // Cost: 8M + 3S + 3*a + 2*b3 + 15add. double() { const { a, b } = CURVE; const b3 = Fp.mul(b, _3n); const { px: X1, py: Y1, pz: Z1 } = this; let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; // prettier-ignore let t0 = Fp.mul(X1, X1); // step 1 let t1 = Fp.mul(Y1, Y1); let t2 = Fp.mul(Z1, Z1); let t3 = Fp.mul(X1, Y1); t3 = Fp.add(t3, t3); // step 5 Z3 = Fp.mul(X1, Z1); Z3 = Fp.add(Z3, Z3); X3 = Fp.mul(a, Z3); Y3 = Fp.mul(b3, t2); Y3 = Fp.add(X3, Y3); // step 10 X3 = Fp.sub(t1, Y3); Y3 = Fp.add(t1, Y3); Y3 = Fp.mul(X3, Y3); X3 = Fp.mul(t3, X3); Z3 = Fp.mul(b3, Z3); // step 15 t2 = Fp.mul(a, t2); t3 = Fp.sub(t0, t2); t3 = Fp.mul(a, t3); t3 = Fp.add(t3, Z3); Z3 = Fp.add(t0, t0); // step 20 t0 = Fp.add(Z3, t0); t0 = Fp.add(t0, t2); t0 = Fp.mul(t0, t3); Y3 = Fp.add(Y3, t0); t2 = Fp.mul(Y1, Z1); // step 25 t2 = Fp.add(t2, t2); t0 = Fp.mul(t2, t3); X3 = Fp.sub(X3, t0); Z3 = Fp.mul(t2, t1); Z3 = Fp.add(Z3, Z3); // step 30 Z3 = Fp.add(Z3, Z3); return new Point(X3, Y3, Z3); } // Renes-Costello-Batina exception-free addition formula. // There is 30% faster Jacobian formula, but it is not complete. // https://eprint.iacr.org/2015/1060, algorithm 1 // Cost: 12M + 0S + 3*a + 3*b3 + 23add. add(other: Point): Point { aprjpoint(other); const { px: X1, py: Y1, pz: Z1 } = this; const { px: X2, py: Y2, pz: Z2 } = other; let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; // prettier-ignore const a = CURVE.a; const b3 = Fp.mul(CURVE.b, _3n); let t0 = Fp.mul(X1, X2); // step 1 let t1 = Fp.mul(Y1, Y2); let t2 = Fp.mul(Z1, Z2); let t3 = Fp.add(X1, Y1); let t4 = Fp.add(X2, Y2); // step 5 t3 = Fp.mul(t3, t4); t4 = Fp.add(t0, t1); t3 = Fp.sub(t3, t4); t4 = Fp.add(X1, Z1); let t5 = Fp.add(X2, Z2); // step 10 t4 = Fp.mul(t4, t5); t5 = Fp.add(t0, t2); t4 = Fp.sub(t4, t5); t5 = Fp.add(Y1, Z1); X3 = Fp.add(Y2, Z2); // step 15 t5 = Fp.mul(t5, X3); X3 = Fp.add(t1, t2); t5 = Fp.sub(t5, X3); Z3 = Fp.mul(a, t4); X3 = Fp.mul(b3, t2); // step 20 Z3 = Fp.add(X3, Z3); X3 = Fp.sub(t1, Z3); Z3 = Fp.add(t1, Z3); Y3 = Fp.mul(X3, Z3); t1 = Fp.add(t0, t0); // step 25 t1 = Fp.add(t1, t0); t2 = Fp.mul(a, t2); t4 = Fp.mul(b3, t4); t1 = Fp.add(t1, t2); t2 = Fp.sub(t0, t2); // step 30 t2 = Fp.mul(a, t2); t4 = Fp.add(t4, t2); t0 = Fp.mul(t1, t4); Y3 = Fp.add(Y3, t0); t0 = Fp.mul(t5, t4); // step 35 X3 = Fp.mul(t3, X3); X3 = Fp.sub(X3, t0); t0 = Fp.mul(t3, t1); Z3 = Fp.mul(t5, Z3); Z3 = Fp.add(Z3, t0); // step 40 return new Point(X3, Y3, Z3); } subtract(other: Point) { return this.add(other.negate()); } is0(): boolean { return this.equals(Point.ZERO); } /** * Constant time multiplication. * Uses wNAF method. Windowed method may be 10% faster, * but takes 2x longer to generate and consumes 2x memory. * Uses precomputes when available. * Uses endomorphism for Koblitz curves. * @param scalar by which the point would be multiplied * @returns New point */ multiply(scalar: bigint): Point { const { endo } = curveOpts; if (!Fn.isValidNot0(scalar)) throw new Error('invalid scalar: out of range'); // 0 is invalid let point: Point, fake: Point; // Fake point is used to const-time mult const mul = (n: bigint) => wnaf.wNAFCached(this, n, Point.normalizeZ); /** See docs for {@link EndomorphismOpts} */ if (endo) { const { k1neg, k1, k2neg, k2 } = endo.splitScalar(scalar); const { p: k1p, f: k1f } = mul(k1); const { p: k2p, f: k2f } = mul(k2); fake = k1f.add(k2f); point = finishEndo(endo.beta, k1p, k2p, k1neg, k2neg); } else { const { p, f } = mul(scalar); point = p; fake = f; } // Normalize `z` for both points, but return only real one return Point.normalizeZ([point, fake])[0]; } /** * Non-constant-time multiplication. Uses double-and-add algorithm. * It's faster, but should only be used when you don't care about * an exposed private key e.g. sig verification, which works over *public* keys. */ multiplyUnsafe(sc: bigint): Point { const { endo } = curveOpts; const p = this; if (!Fn.isValid(sc)) throw new Error('invalid scalar: out of range'); // 0 is valid if (sc === _0n || p.is0()) return Point.ZERO; if (sc === _1n) return p; // fast-path if (wnaf.hasPrecomputes(this)) return this.multiply(sc); if (endo) { const { k1neg, k1, k2neg, k2 } = endo.splitScalar(sc); // `wNAFCachedUnsafe` is 30% slower const { p1, p2 } = mulEndoUnsafe(Point, p, k1, k2); return finishEndo(endo.beta, p1, p2, k1neg, k2neg); } else { return wnaf.wNAFCachedUnsafe(p, sc); } } multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined { const sum = this.multiplyUnsafe(a).add(Q.multiplyUnsafe(b)); return sum.is0() ? undefined : sum; } /** * Converts Projective point to affine (x, y) coordinates. * @param invertedZ Z^-1 (inverted zero) - optional, precomputation is useful for invertBatch */ toAffine(invertedZ?: T): AffinePoint<T> { return toAffineMemo(this, invertedZ); } /** * Checks whether Point is free of torsion elements (is in prime subgroup). * Always torsion-free for cofactor=1 curves. */ isTorsionFree(): boolean { const { isTorsionFree } = curveOpts; if (cofactor === _1n) return true; if (isTorsionFree) return isTorsionFree(Point, this); return wnaf.wNAFCachedUnsafe(this, CURVE_ORDER).is0(); } clearCofactor(): Point { const { clearCofactor } = curveOpts; if (cofactor === _1n) return this; // Fast-path if (clearCofactor) return clearCofactor(Point, this) as Point; return this.multiplyUnsafe(cofactor); } toBytes(isCompressed = true): Uint8Array { abool('isCompressed', isCompressed); this.assertValidity(); return toBytes(Point, this, isCompressed); } /** @deprecated use `toBytes` */ toRawBytes(isCompressed = true): Uint8Array { return this.toBytes(isCompressed); } toHex(isCompressed = true): string { return bytesToHex(this.toBytes(isCompressed)); } toString() { return `<Point ${this.is0() ? 'ZERO' : this.toHex()}>`; } } const bits = Fn.BITS; const wnaf = wNAF(Point, curveOpts.endo ? Math.ceil(bits / 2) : bits); return Point; } // _legacyWeierstrass /** @deprecated use `weierstrassN` */ export function weierstrassPoints<T>(c: CurvePointsTypeWithLength<T>): CurvePointsRes<T> { const { CURVE, curveOpts } = _weierstrass_legacy_opts_to_new(c); const Point = weierstrassN(CURVE, curveOpts); return _weierstrass_new_output_to_legacy(c, Point); } // Instance export interface SignatureType { readonly r: bigint; readonly s: bigint; readonly recovery?: number; assertValidity(): void; addRecoveryBit(recovery: number): RecoveredSignatureType; hasHighS(): boolean; normalizeS(): SignatureType; recoverPublicKey(msgHash: Hex): ProjPointType<bigint>; toCompactRawBytes(): Uint8Array; toCompactHex(): string; toDERRawBytes(): Uint8Array; toDERHex(): string; // toBytes(format?: string): Uint8Array; } export type RecoveredSignatureType = SignatureType & { readonly recovery: number; }; // Static methods export type SignatureConstructor = { new (r: bigint, s: bigint, recovery?: number): SignatureType; fromCompact(hex: Hex): SignatureType; fromDER(hex: Hex): SignatureType; }; export type SignatureLike = { r: bigint; s: bigint }; export type PubKey = Hex | ProjPointType<bigint>; export type CurveType = BasicWCurve<bigint> & { hash: CHash; // CHash not FHash because we need outputLen for DRBG hmac?: HmacFnSync; randomBytes?: (bytesLength?: number) => Uint8Array; lowS?: boolean; bits2int?: (bytes: Uint8Array) => bigint; bits2int_modN?: (bytes: Uint8Array) => bigint; }; // Points start with byte 0x02 when y is even; otherwise 0x03 function pprefix(hasEvenY: boolean): Uint8Array { return Uint8Array.of(hasEvenY ? 0x02 : 0x03); } export type CurveFn = { CURVE: CurvePointsType<bigint>; getPublicKey: (privateKey: PrivKey, isCompressed?: boolean) => Uint8Array; getSharedSecret: (privateA: PrivKey, publicB: Hex, isCompressed?: boolean) => Uint8Array; sign: (msgHash: Hex, privKey: PrivKey, opts?: SignOpts) => RecoveredSignatureType; verify: (signature: Hex | SignatureLike, msgHash: Hex, publicKey: Hex, opts?: VerOpts) => boolean; Point: ProjConstructor<bigint>; /** @deprecated use `Point` */ ProjectivePoint: ProjConstructor<bigint>; Signature: SignatureConstructor; utils: { normPrivateKeyToScalar: (key: PrivKey) => bigint; isValidPrivateKey(privateKey: PrivKey): boolean; randomPrivateKey: () => Uint8Array; precompute: (windowSize?: number, point?: ProjPointType<bigint>) => ProjPointType<bigint>; }; }; export function ecdsa( Point: ProjConstructor<bigint>, ecdsaOpts: ECDSAOpts, curveOpts: WeierstrassExtraOpts<bigint> = {} ): ECDSA { _validateObject( ecdsaOpts, { hash: 'function' }, { hmac: 'function', lowS: 'boolean', randomBytes: 'function', bits2int: 'function', bits2int_modN: 'function', } ); const randomBytes_ = ecdsaOpts.randomBytes || randomBytes; const hmac_: HmacFnSync = ecdsaOpts.hmac || (((key, ...msgs) => hmac(ecdsaOpts.hash, key, concatBytes(...msgs))) satisfies HmacFnSync); const { Fp, Fn } = Point; const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn; function isBiggerThanHalfOrder(number: bigint) { const HALF = CURVE_ORDER >> _1n; return number > HALF; } function normalizeS(s: bigint) { return isBiggerThanHalfOrder(s) ? Fn.neg(s) : s; } function aValidRS(title: string, num: bigint) { if (!Fn.isValidNot0(num)) throw new Error(`invalid signature ${title}: out of range 1..CURVE.n`); } /** * ECDSA signature with its (r, s) properties. Supports DER & compact representations. */ class Signature implements SignatureType { readonly r: bigint; readonly s: bigint; readonly recovery?: number; constructor(r: bigint, s: bigint, recovery?: number) { aValidRS('r', r); // r in [1..N-1] aValidRS('s', s); // s in [1..N-1] this.r = r; this.s = s; if (recovery != null) this.recovery = recovery; Object.freeze(this); } // pair (bytes of r, bytes of s) static fromCompact(hex: Hex) { const L = Fn.BYTES; const b = ensureBytes('compactSignature', hex, L * 2); return new Signature(Fn.fromBytes(b.subarray(0, L)), Fn.fromBytes(b.subarray(L, L * 2))); } // DER encoded ECDSA signature // https://bitcoin.stackexchange.com/questions/57644/what-are-the-parts-of-a-bitcoin-transaction-input-script static fromDER(hex: Hex) { const { r, s } = DER.toSig(ensureBytes('DER', hex)); return new Signature(r, s); } /** * @todo remove * @deprecated */ assertValidity(): void {} addRecoveryBit(recovery: number): RecoveredSignature { return new Signature(this.r, this.s, recovery) as RecoveredSignature; } // ProjPointType<bigint> recoverPublicKey(msgHash: Hex): typeof Point.BASE { const FIELD_ORDER = Fp.ORDER; const { r, s, recovery: rec } = this; if (rec == null || ![0, 1, 2, 3].includes(rec)) throw new Error('recovery id invalid'); // ECDSA recovery is hard for cofactor > 1 curves. // In sign, `r = q.x mod n`, and here we recover q.x from r. // While recovering q.x >= n, we need to add r+n for cofactor=1 curves. // However, for cofactor>1, r+n may not get q.x: // r+n*i would need to be done instead where i is unknown. // To easily get i, we either need to: // a. increase amount of valid recid values (4, 5...); OR // b. prohibit non-prime-order signatures (recid > 1). const hasCofactor = CURVE_ORDER * _2n < FIELD_ORDER; if (hasCofactor && rec > 1) throw new Error('recovery id is ambiguous for h>1 curve'); const radj = rec === 2 || rec === 3 ? r + CURVE_ORDER : r; if (!Fp.isValid(radj)) throw new Error('recovery id 2 or 3 invalid'); const x = Fp.toBytes(radj); const R = Point.fromHex(concatBytes(pprefix((rec & 1) === 0), x)); const ir = Fn.inv(radj); // r^-1 const h = bits2int_modN(ensureBytes('msgHash', msgHash)); // Truncate hash const u1 = Fn.create(-h * ir); // -hr^-1 const u2 = Fn.create(s * ir); // sr^-1 // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1). unsafe is fine: there is no private data. const Q = Point.BASE.multiplyUnsafe(u1).add(R.multiplyUnsafe(u2)); if (Q.is0()) throw new Error('point at infinify'); Q.assertValidity(); return Q; } // Signatures should be low-s, to prevent malleability. hasHighS(): boolean { return isBiggerThanHalfOrder(this.s); } normalizeS() { return this.hasHighS() ? new Signature(this.r, Fn.neg(this.s), this.recovery) : this; } toBytes(format: 'compact' | 'der') { if (format === 'compact') return concatBytes(Fn.toBytes(this.r), Fn.toBytes(this.s)); if (format === 'der') return hexToBytes(DER.hexFromSig(this)); throw new Error('invalid format'); } // DER-encoded toDERRawBytes() { return this.toBytes('der'); } toDERHex() { return bytesToHex(this.toBytes('der')); } // padded bytes of r, then padded bytes of s toCompactRawBytes() { return this.toBytes('compact'); } toCompactHex() { return bytesToHex(this.toBytes('compact')); } } type RecoveredSignature = Signature & { recovery: number }; const normPrivateKeyToScalar = _legacyHelperNormPriv( Fn, curveOpts.allowedPrivateKeyLengths, curveOpts.wrapPrivateKey ); const utils = { isValidPrivateKey(privateKey: PrivKey) { try { normPrivateKeyToScalar(privateKey); return true; } catch (error) { return false; } }, normPrivateKeyToScalar: normPrivateKeyToScalar, /** * Produces cryptographically secure private key from random of size * (groupLen + ceil(groupLen / 2)) with modulo bias being negligible. */ randomPrivateKey: (): Uint8Array => { const n = CURVE_ORDER; return mapHashToField(randomBytes_(getMinHashLength(n)), n); }, precompute(windowSize = 8, point = Point.BASE): typeof Point.BASE { return point.precompute(windowSize, false); }, }; /** * Computes public key for a private key. Checks for validity of the private key. * @param privateKey private key * @param isCompressed whether to return compact (default), or full key * @returns Public key, full when isCompressed=false; short when isCompressed=true */ function getPublicKey(privateKey: PrivKey, isCompressed = true): Uint8Array { return Point.fromPrivateKey(privateKey).toBytes(isCompressed); } /** * Quick and dirty check for item being public key. Does not validate hex, or being on-curve. */ function isProbPub(item: PrivKey | PubKey): boolean | undefined { if (typeof item === 'bigint') return false; if (item instanceof Point) return true; const arr = ensureBytes('key', item); const length = arr.length; const L = Fp.BYTES; const LC = L + 1; // e.g. 33 for 32 const LU = 2 * L + 1; // e.g. 65 for 32 if (curveOpts.allowedPrivateKeyLengths || Fn.BYTES === LC) { return undefined; } else { return length === LC || length === LU; } } /** * ECDH (Elliptic Curve Diffie Hellman). * Computes shared public key from private key and public key. * Checks: 1) private key validity 2) shared key is on-curve. * Does NOT hash the result. * @param privateA private key * @param publicB different public key * @param isCompressed whether to return compact (default), or full key * @returns shared public key */ function getSharedSecret(privateA: PrivKey, publicB: Hex, isCompressed = true): Uint8Array { if (isProbPub(privateA) === true) throw new Error('first arg must be private key'); if (isProbPub(publicB) === false) throw new Error('second arg must be public key'); const b = Point.fromHex(publicB); // check for being on-curve return b.multiply(normPrivateKeyToScalar(privateA)).toBytes(isCompressed); } // RFC6979: ensure ECDSA msg is X bytes and < N. RFC suggests optional truncating via bits2octets. // FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits, which matches bits2int. // bits2int can produce res>N, we can do mod(res, N) since the bitLen is the same. // int2octets can't be used; pads small msgs with 0: unacceptatble for trunc as per RFC vectors const bits2int = ecdsaOpts.bits2int || function (bytes: Uint8Array): bigint { // Our custom check "just in case", for protection against DoS if (bytes.length > 8192) throw new Error('input is too large'); // For curves with nBitLength % 8 !== 0: bits2octets(bits2octets(m)) !== bits2octets(m) // for some cases, since bytes.length * 8 is not actual bitLength. const num = bytesToNumberBE(bytes); // check for == u8 done here const delta = bytes.length * 8 - fnBits; // truncate to nBitLength leftmost bits return delta > 0 ? num >> BigInt(delta) : num; }; const bits2int_modN = ecdsaOpts.bits2int_modN || function (bytes: Uint8Array): bigint { return Fn.create(bits2int(bytes)); // can't use bytesToNumberBE here }; // NOTE: pads output with zero as per spec const ORDER_MASK = bitMask(fnBits); /** * Converts to bytes. Checks if num in `[0..ORDER_MASK-1]` e.g.: `[0..2^256-1]`. */ function int2octets(num: bigint): Uint8Array { // IMPORTANT: the check ensures working for case `Fn.BYTES != Fn.BITS * 8` aInRange('num < 2^' + fnBits, num, _0n, ORDER_MASK); return Fn.toBytes(num); } // Steps A, D of RFC6979 3.2 // Creates RFC6979 seed; converts msg/privKey to numbers. // Used only in sign, not in verify. // NOTE: we cannot assume here that msgHash has same amount of bytes as curve order, // this will be invalid at least for P521. Also it can be bigger for P224 + SHA256 function prepSig(msgHash: Hex, privateKey: PrivKey, opts = defaultSigOpts) { if (['recovered', 'canonical'].some((k) => k in opts)) throw new Error('sign() legacy options not supported'); const { hash } = ecdsaOpts; let { lowS, prehash, extraEntropy: ent } = opts; // generates low-s sigs by default if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because we already provide hash msgHash = ensureBytes('msgHash', msgHash); validateSigVerOpts(opts); if (prehash) msgHash = ensureBytes('prehashed msgHash', hash(msgHash)); // We can't later call bits2octets, since nested bits2int is broken for curves // with fnBits % 8 !== 0. Because of that, we unwrap it here as int2octets call. // const bits2octets = (bits) => int2octets(bits2int_modN(bits)) const h1int = bits2int_modN(msgHash); const d = normPrivateKeyToScalar(privateKey); // validate private key, convert to bigint const seedArgs = [int2octets(d), int2octets(h1int)]; // extraEntropy. RFC6979 3.6: additional k' (optional). if (ent != null && ent !== false) { // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') const e = ent === true ? randomBytes_(Fp.BYTES) : ent; // generate random bytes OR pass as-is seedArgs.push(ensureBytes('extraEntropy', e)); // check for being bytes } const seed = concatBytes(...seedArgs); // Step D of RFC6979 3.2 const m = h1int; // NOTE: no need to call bits2int second time here, it is inside truncateHash! // Converts signature params into point w r/s, checks result for validity. // Can use scalar blinding b^-1(bm + bdr) where b ∈ [1,q−1] according to // https://tches.iacr.org/index.php/TCHES/article/view/7337/6509. We've decided against it: // a) dependency on CSPRNG b) 15% slowdown c) doesn't really help since bigints are not CT function k2sig(kBytes: Uint8Array): RecoveredSignature | undefined { // RFC 6979 Section 3.2, step 3: k = bits2int(T) // Important: all mod() calls here must be done over N const k = bits2int(kBytes); // Cannot use fields methods, since it is group element if (!Fn.isValidNot0(k)) return; // Valid scalars (including k) must be in 1..N-1 const ik = Fn.inv(k); // k^-1 mod n const q = Point.BASE.multiply(k).toAffine(); // q = Gk const r = Fn.create(q.x); // r = q.x mod n if (r === _0n) return; const s = Fn.create(ik * Fn.create(m + r * d)); // Not using blinding here, see comment above if (s === _0n) return; let recovery = (q.x === r ? 0 : 2) | Number(q.y & _1n); // recovery bit (2 or 3, when q.x > n) let normS = s; if (lowS && isBiggerThanHalfOrder(s)) { normS = normalizeS(s); // if lowS was passed, ensure s is always recovery ^= 1; // // in the bottom half of N } return new Signature(r, normS, recovery) as RecoveredSignature; // use normS, not s } return { seed, k2sig }; } const defaultSigOpts: SignOpts = { lo