/** * 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 as nobleHmac } from '@noble/hashes/hmac.js'; import { ahash } from '@noble/hashes/utils.js'; import { abool, abytes, aInRange, bitLen, bitMask, bytesToHex, bytesToNumberBE, concatBytes, createHmacDrbg, hexToBytes, isBytes, validateObject, randomBytes as wcRandomBytes, type CHash, type HmacFn, type Signer, type TArg, type TRet, } from '../utils.ts'; import { createCurveFields, createKeygen, mulAddUnsafe, normalizeZ, probeRandomBytes, ScalarMultiplier, validatePointCons, type AffinePoint, type CurveLengths, type CurvePoint, type CurvePointCons, } from './curve.ts'; import { DER } from './der.ts'; import { getMinHashLength, invertCt, mapHashToField, type IField } from './modular.ts'; /** Shared affine point shape used by Weierstrass helpers. */ export type { AffinePoint }; // DER codec lives in der.ts; re-exported here because ECDSA signatures are its main consumer. export { DER, DERErr, type IDER } from './der.ts'; type EndoBasis = [[bigint, bigint], [bigint, bigint]]; /** * When Weierstrass curve has `a=0`, it becomes Koblitz curve. * Koblitz curves allow using **efficiently-computable GLV endomorphism ψ**. * Endomorphism speeds up un-precomputed public-scalar multiplication (verification / key * recovery) by splitting a scalar into two half-width halves that share doublings. * * Endomorphism consists of beta, lambda and basises: * * 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 * * `_splitEndoScalar` 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 * {@link https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 | this endomorphism gist}. */ export type EndomorphismOpts = { /** Cube root of unity used by the GLV endomorphism. */ beta: bigint; /** Reduced lattice basis used for scalar splitting. */ basises?: EndoBasis; }; // We construct the basis so `den` is always positive and equals `n`, // but the `num` sign depends on the basis, not on the secret value. // Exact half-way cases round away from zero, which keeps the split symmetric // around the reduced-basis boundaries used by endomorphism decomposition. const divNearest = (num: bigint, den: bigint) => (num + (num >= 0 ? den : -den) / _2n) / den; /** Two half-sized scalar components returned by endomorphism splitting. */ export type ScalarEndoParts = { /** Whether the first split scalar should be negated. */ k1neg: boolean; /** Absolute value of the first split scalar. */ k1: bigint; /** Whether the second split scalar should be negated. */ k2neg: boolean; /** Absolute value of the second split scalar. */ k2: bigint; }; /** Splits scalar for GLV endomorphism. */ export function _splitEndoScalar(k: bigint, basis: EndoBasis, n: bigint): ScalarEndoParts { // Split scalar into two such that part is ~half bits: `abs(part) < sqrt(N)` // Since part can be negative, we need to do this on point. // Callers must provide a reduced GLV basis whose vectors satisfy // `a + b * lambda ≡ 0 (mod n)`; this helper only sees the basis and `n`. // Reject unreduced scalars instead of silently treating them mod n. aInRange('scalar', k, _0n, n); // TODO: verifyScalar function which consumes lambda const [[a1, b1], [a2, b2]] = basis; const c1 = divNearest(b2 * k, n); const c2 = divNearest(-b1 * k, n); // |k1|/|k2| is < sqrt(N), but can be negative. // If we do `k1 mod N`, we'll get big scalar (`> sqrt(N)`): so, we do cheaper negation instead. let k1 = k - c1 * a1 - c2 * a2; let k2 = -c1 * b1 - c2 * b2; const k1neg = k1 < _0n; const k2neg = k2 < _0n; if (k1neg) k1 = -k1; if (k2neg) k2 = -k2; // Double check that resulting scalar is less than half bits of N: the wNAF pair walk // relies on the halves being short. This should only happen on wrong bases. // Also, the math inside is complex enough that this guard is worth keeping. const MAX_NUM = bitMask(Math.ceil(bitLen(n) / 2)) + _1n; // Half bits of N if (k1 < _0n || k1 >= MAX_NUM || k2 < _0n || k2 >= MAX_NUM) { throw new Error('splitScalar (endomorphism): failed for k'); } return { k1neg, k1, k2neg, k2 }; } /** * Option to enable hedged signatures with improved security. * * * Randomly generated k is bad, because broken CSPRNG would leak private keys. * * Deterministic k (RFC6979) is better; but is suspectible to fault attacks. * * We allow using technique described in RFC6979 3.6: additional k', a.k.a. adding randomness * to deterministic sig. If CSPRNG is broken & randomness is weak, it would STILL be as secure * as ordinary sig without ExtraEntropy. * * * `true` means "fetch data, from CSPRNG, incorporate it into k generation" * * `false` means "disable extra entropy, use purely deterministic k" * * `Uint8Array` passed means "incorporate following data into k generation" * * See {@link https://paulmillr.com/posts/deterministic-signatures/ | deterministic signatures}. */ export type ECDSAExtraEntropy = boolean | Uint8Array; /** * - `compact` is the default format * - `recovered` is the same as compact, but with an extra byte indicating recovery byte * - `der` is ASN.1 DER encoding */ export type ECDSASignatureFormat = 'compact' | 'recovered' | 'der'; /** * - `prehash`: (default: true) indicates whether to do sha256(message). * When a custom hash is used, it must be set to `false`. */ export type ECDSARecoverOpts = { /** Whether to hash the message before signature recovery. */ prehash?: boolean; }; /** * - `prehash`: (default: true) indicates whether to do sha256(message). * When a custom hash is used, it must be set to `false`. * - `lowS`: (default: true) prohibits signatures with `sig.s >= CURVE.n/2n`. * Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures, * which is default openssl behavior. * Non-malleable signatures can still be successfully verified in openssl. * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte */ export type ECDSAVerifyOpts = { /** Whether to hash the message before verification. */ prehash?: boolean; /** Whether to reject high-S signatures. */ lowS?: boolean; /** Signature encoding to accept. */ format?: ECDSASignatureFormat; }; /** * - `prehash`: (default: true) indicates whether to do sha256(message). * When a custom hash is used, it must be set to `false`. * - `lowS`: (default: true) prohibits signatures with `sig.s >= CURVE.n/2n`. * Compatible with BTC/ETH. Setting `lowS: false` allows to create malleable signatures, * which is default openssl behavior. * Non-malleable signatures can still be successfully verified in openssl. * - `format`: (default: 'compact') 'compact' or 'recovered' with recovery byte * - `extraEntropy`: (default: false) creates signatures with increased * security, see {@link ECDSAExtraEntropy} */ export type ECDSASignOpts = { /** Whether to hash the message before signing. */ prehash?: boolean; /** Whether to normalize signatures into the low-S half-order. */ lowS?: boolean; /** Signature encoding to produce. */ format?: ECDSASignatureFormat; /** Optional hedging input for deterministic k generation. */ extraEntropy?: ECDSAExtraEntropy; }; function validateSigFormat(format: string): ECDSASignatureFormat { if (!['compact', 'recovered', 'der'].includes(format)) throw new Error('Signature format must be "compact", "recovered", or "der"'); return format as ECDSASignatureFormat; } function validateSigOpts>( opts: T, def: D ): D { validateObject(opts); const optsn = {} as D; // Normalize only the declared option subset from `def`; unknown keys are // intentionally ignored so shared / superset option bags stay valid here too. // `extraEntropy` stays an opaque payload until the signing path consumes it. for (let optName of Object.keys(def) as (keyof D)[]) { // @ts-ignore optsn[optName] = opts[optName] === undefined ? def[optName] : opts[optName]; } abool(optsn.lowS!, 'lowS'); abool(optsn.prehash!, 'prehash'); if (optsn.format !== undefined) validateSigFormat(optsn.format); return optsn; } /** Projective XYZ point used by short Weierstrass curves. */ export interface WeierstrassPoint extends CurvePoint> { /** projective X coordinate. Different from affine x. */ readonly X: T; /** projective Y coordinate. Different from affine y. */ readonly Y: T; /** projective z coordinate */ readonly Z: T; /** affine x coordinate. Different from projective X. */ get x(): T; /** affine y coordinate. Different from projective Y. */ get y(): T; /** * Encode the point into compressed or uncompressed SEC1 bytes. * @param isCompressed - Whether to use the compressed form. * @returns Encoded point bytes. */ toBytes(isCompressed?: boolean): TRet; /** * Encode the point into compressed or uncompressed SEC1 hex. * @param isCompressed - Whether to use the compressed form. * @returns Encoded point hex. */ toHex(isCompressed?: boolean): string; /** * Double-scalar multiplication `a⋅this + b⋅other` via Strauss–Shamir: both scalar walks * share one doubling chain, and GLV endomorphism (when the curve has one) halves the chain * again by splitting each scalar. 1.3-1.7x faster than two `multiplyUnsafe()` calls. * Not constant-time: only for public scalars, e.g. ECDSA verification's `u1⋅G + u2⋅P`. * @param a - Scalar for this point. * @param other - Second point. * @param b - Scalar for the second point. * @returns Combined product point. */ mulAddUnsafe(a: bigint, other: WeierstrassPoint, b: bigint): WeierstrassPoint; } /** Constructor and metadata helpers for Weierstrass points. */ export interface WeierstrassPointCons extends CurvePointCons> { /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ new (X: T, Y: T, Z: T): WeierstrassPoint; /** * Return the curve parameters captured by this point constructor. * @returns Curve parameters. */ CURVE(): WeierstrassOpts; } /** * 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 = Readonly<{ /** Base-field modulus. */ p: bigint; /** Prime subgroup order. */ n: bigint; /** Curve cofactor. */ h: bigint; /** Weierstrass curve parameter `a`. */ a: T; /** Weierstrass curve parameter `b`. */ b: T; /** Generator x coordinate. */ Gx: T; /** Generator y coordinate. */ Gy: T; }>; /** * Optional helpers and overrides for a Weierstrass point constructor. * * When a cofactor != 1, there can be effective methods to: * 1. Determine whether a point is torsion-free * 2. Clear torsion component */ export type WeierstrassExtraOpts = Partial<{ /** Optional base-field override. */ Fp: IField; /** Optional scalar-field override. */ Fn: IField; /** * Whether the point at infinity is a public value of this point type. When unset, infinity * fails `assertValidity()` and has no byte encoding at all: encoding throws and `0x00` does * not decode, which keeps `Point.fromBytes()` usable as a strict key boundary because * infinity cannot be expressed in bytes. When set, infinity validates and uses the SEC 1 * v2.0 §2.3.3 / §2.3.4 single-octet `0x00` form, unless `fromBytes` / `toBytes` override it. */ allowInfinityPoint: boolean; /** Optional GLV endomorphism data. */ endo: EndomorphismOpts; /** RNG override used for scalar blinding. */ randomBytes: (bytesLength?: number) => TRet; /** Optional torsion-check override. */ isTorsionFree: (c: WeierstrassPointCons, point: WeierstrassPoint) => boolean; /** Optional cofactor-clearing override. */ clearCofactor: (c: WeierstrassPointCons, point: WeierstrassPoint) => WeierstrassPoint; /** Optional custom point decoder. */ fromBytes: (bytes: TArg) => AffinePoint; /** Optional custom point encoder. */ toBytes: ( c: WeierstrassPointCons, point: WeierstrassPoint, isCompressed: boolean ) => TRet; }>; /** * Options for ECDSA signatures over a Weierstrass curve. * * * lowS: (default: true) whether produced or verified signatures occupy the * low half of `ecdsaOpts.n`. Prevents malleability. * * hmac: (default: noble-hashes hmac) function, would be used to init hmac-drbg for k generation. * * randomBytes: (default: webcrypto os-level CSPRNG) custom method for fetching secure randomness. * * bits2int, bits2int_modN: used in sigs, sometimes overridden by curves. Custom hooks are * treated as pure functions over validated bytes and MUST NOT mutate caller-owned buffers or * closure-captured option bags. `bits2int_modN` must also return a canonical scalar in * `[0..Point.Fn.ORDER-1]`. */ export type ECDSAOpts = Partial<{ /** Default low-S policy for this ECDSA instance. */ lowS: boolean; /** HMAC implementation used by RFC6979 DRBG. */ hmac: HmacFn; /** RNG override used by helper constructors. */ randomBytes: (bytesLength?: number) => TRet; /** Hash-to-integer conversion override. */ bits2int: (bytes: TArg) => bigint; /** Hash-to-integer-mod-n conversion override. Returns a canonical scalar in `[0..Fn.ORDER-1]`. */ bits2int_modN: (bytes: TArg) => bigint; }>; /** Elliptic Curve Diffie-Hellman helper namespace. */ export interface ECDH { /** * Generate a secret/public key pair. * @param seed - Optional seed material. * @returns Secret/public key pair. */ keygen: (seed?: TArg) => { secretKey: TRet; publicKey: TRet }; /** * Derive the public key from a secret key. * @param secretKey - Secret key bytes. * @param isCompressed - Whether to emit compressed SEC1 bytes. * @returns Encoded public key. */ getPublicKey: (secretKey: TArg, isCompressed?: boolean) => TRet; /** * Compute the shared secret point from a secret key and peer public key. * @param secretKeyA - Local secret key bytes. * @param publicKeyB - Peer public key bytes. * @param isCompressed - Whether to emit compressed SEC1 bytes. * @returns Encoded shared point. */ getSharedSecret: ( secretKeyA: TArg, publicKeyB: TArg, isCompressed?: boolean ) => TRet; /** Point constructor used by this ECDH instance. */ Point: WeierstrassPointCons; /** Validation and random-key helpers. */ utils: { /** Check whether a secret key has the expected encoding. */ isValidSecretKey: (secretKey: TArg) => boolean; /** Check whether a public key decodes to a valid point. */ isValidPublicKey: (publicKey: TArg, isCompressed?: boolean) => boolean; /** Generate a valid random secret key. */ randomSecretKey: (seed?: TArg) => TRet; }; /** Byte lengths for keys and signatures exposed by this curve. */ lengths: CurveLengths; } /** * ECDSA interface. * Only supported for prime fields, not Fp2 (extension fields). */ export interface ECDSA extends ECDH { /** * Sign a message with the given secret key. * @param message - Message bytes. * @param secretKey - Secret key bytes. * @param opts - Optional signing tweaks. See {@link ECDSASignOpts}. * @returns Encoded signature bytes. */ sign: ( message: TArg, secretKey: TArg, opts?: TArg ) => TRet; /** * Verify a signature against a message and public key. * @param signature - Encoded signature bytes. * @param message - Message bytes. * @param publicKey - Encoded public key. * @param opts - Optional verification tweaks. See {@link ECDSAVerifyOpts}. * @returns Whether the signature is valid. */ verify: ( signature: TArg, message: TArg, publicKey: TArg, opts?: TArg ) => boolean; /** * Recover the public key encoded into a recoverable signature. * @param signature - Recoverable signature bytes. * @param message - Message bytes. * @param opts - Optional recovery tweaks. See {@link ECDSARecoverOpts}. * @returns Encoded recovered public key. */ recoverPublicKey( signature: TArg, message: TArg, opts?: TArg ): TRet; /** Signature constructor and parser helpers. */ Signature: ECDSASignatureCons; } // Be friendly to bad ECMAScript parsers by not using bigint literals // prettier-ignore const _0n = /* @__PURE__ */ BigInt(0), _1n = /* @__PURE__ */ BigInt(1), _2n = /* @__PURE__ */ BigInt(2), _3n = /* @__PURE__ */ BigInt(3), _4n = /* @__PURE__ */ BigInt(4); /** * Creates weierstrass Point constructor, based on specified curve options. * * See {@link WeierstrassOpts}. * @param params - Curve parameters. See {@link WeierstrassOpts}. * @param extraOpts - Optional helpers and overrides. See {@link WeierstrassExtraOpts}. * @returns Weierstrass point constructor. * @throws If the curve parameters, overrides, or point codecs are invalid. {@link Error} * * @example * Construct a point type from explicit Weierstrass curve parameters. * * ```js * const opts = { * p: 0xfffffffffffffffffffffffffffffffeffffac73n, * n: 0x100000000000000000001b8fa16dfab9aca16b6b3n, * h: 1n, * a: 0n, * b: 7n, * Gx: 0x3b4c382ce37aa192a4019e763036f4f5dd4d7ebbn, * Gy: 0x938cf935318fdced6bc28286531733c3f03c4feen, * }; * const secp160k1_Point = weierstrass(opts); * ``` */ export function weierstrass( params: WeierstrassOpts, extraOpts: WeierstrassExtraOpts = {} ): WeierstrassPointCons { const validated = createCurveFields('weierstrass', params, extraOpts); const Fp = validated.Fp as IField; const Fn = validated.Fn as IField; let CURVE = validated.CURVE as WeierstrassOpts; const { h: cofactor, n: CURVE_ORDER } = CURVE; validateObject( extraOpts, {}, { allowInfinityPoint: 'boolean', clearCofactor: 'function', isTorsionFree: 'function', fromBytes: 'function', toBytes: 'function', endo: 'object', randomBytes: 'function', } ); // Snapshot every hook and take an owned copy of nested GLV configuration. Later mutations of // the caller's options must not change arithmetic, subgroup, or codec policy. const { endo: endoOpts, allowInfinityPoint, clearCofactor, isTorsionFree, fromBytes, toBytes, } = extraOpts; const randomBytes = extraOpts.randomBytes === undefined ? wcRandomBytes : extraOpts.randomBytes; if (endoOpts) { if (!Fp.is0(CURVE.a) || typeof endoOpts.beta !== 'bigint' || !Array.isArray(endoOpts.basises)) { throw new Error('invalid endo: expected "beta": bigint and "basises": array'); } } const endo = endoOpts ? { beta: endoOpts.beta, basises: endoOpts.basises!.map((basis) => [...basis]) as EndoBasis, } : undefined; const lengths = getWLengths(Fp as TArg>, Fn); 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: WeierstrassPointCons, point: WeierstrassPoint, isCompressed: boolean ): TRet { // Handle infinity before toAffine(), which maps ZERO to (0, 0) silently: without this the // encoder would emit an all-zero byte string that decodes back as a different point. Only // curves that opt into infinity as a public point value get the SEC 1 byte form. if (point.is0()) { if (!allowInfinityPoint) throw new Error('bad point: ZERO'); return Uint8Array.of(0) as TRet; // SEC 1 v2.0 §2.3.3 } 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) as TRet; } else { return concatBytes(Uint8Array.of(0x04), bx, Fp.toBytes(y)) as TRet; } } function pointFromBytes(bytes: TArg) { abytes(bytes, undefined, 'Point'); const { publicKey: comp, publicKeyUncompressed: uncomp } = lengths; // e.g. for 32-byte: 33, 65 const length = bytes.length; const head = bytes[0]; const tail = bytes.subarray(1); // SEC 1 v2.0 §2.3.4 decodes 0x00 as infinity, but §3.2.2 rejects infinity as a public key. // Leaving 0x00 undecodable by default makes that rejection structural rather than a check: // callers reuse this parser as the strict key boundary, and infinity is simply not // expressible. Curves where infinity is a real wire value opt into the codec instead. // secp256k1 crosstests show OpenSSL raw point codecs accept 0x00 too. if (allowInfinityPoint && length === 1 && head === 0x00) return { x: Fp.ZERO, y: Fp.ZERO }; // No actual validation is done here: use .assertValidity() if (length === comp && (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 evenY = Fp.isOdd!(y); const evenH = (head & 1) === 1; // ECDSA-specific if (evenH !== evenY) y = Fp.neg(y); return { x, y }; } else if (length === uncomp && head === 0x04) { // TODO: more checks const L = Fp.BYTES; const x = Fp.fromBytes(tail.subarray(0, L)); const y = Fp.fromBytes(tail.subarray(L, 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=${comp} or uncompressed=${uncomp}` ); } } const encodePoint = toBytes === undefined ? pointToBytes : toBytes; const decodePoint = fromBytes === undefined ? pointFromBytes : fromBytes; // Hoisted from double() / add(): curve params never change after construction. // Koblitz curves (a=0, e.g. secp256k1) skip the three a-multiplications per operation; // the selection depends only on public curve constants. const b3 = Fp.mul(CURVE.b, _3n); const mulA = Fp.is0(CURVE.a) ? (_: T): T => Fp.ZERO : (x: T): T => Fp.mul(CURVE.a, x); function weierstrassEquation(x: T): T { const x2 = Fp.sqr(x); const x3 = Fp.mul(x2, x); return Fp.add(Fp.add(x3, Fp.mul(x, CURVE.a)), CURVE.b); // x³ + a * x + 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); const right = weierstrassEquation(x); // x³ + ax + b return Fp.eql(left, right); } // Keep constructor-time generator validation cheap: callers are responsible for supplying the // correct prime-order base point, while eager subgroup checks here would slow heavy module imports. // 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}`); // Extension-field elements are objects. Keep point coordinates detached from caller-owned // objects so their mutation cannot invalidate cached on-curve / subgroup checks. Primitive // field elements (bigints in shipped prime fields) are already immutable values. return typeof n === 'object' && n !== null ? Fp.create(n) : n; } function aprjpoint(other: unknown): asserts other is Point { if (!(other instanceof Point)) throw new Error('Weierstrass Point expected'); } function splitEndoScalarN(k: bigint) { if (!endo || !endo.basises) throw new Error('no endo'); return _splitEndoScalar(k, endo.basises, Fn.ORDER); } /** * Appends a (point, scalar) pair to the inputs of a vartime wNAF walk * ({@link mulAddUnsafe}). With GLV endomorphism the scalar is split into two half-width * pairs against P and ψ(P) = (β⋅x, y), halving the walk's shared doubling chain; * split signs fold into the points. */ function pushWnafPair(points: Point[], scalars: bigint[], p: Point, k: bigint): void { if (!Fn.isValid(k)) throw new RangeError('invalid scalar: out of range'); // 0 is valid if (endo) { const { k1neg, k1, k2neg, k2 } = splitEndoScalarN(k); const psi = new Point(Fp.mul(p.X, endo.beta), p.Y, p.Z); points.push(k1neg ? p.negate() : p, k2neg ? psi.negate() : psi); scalars.push(k1, k2); } else { points.push(p); scalars.push(k); } } // Successful assertValidity() results are cached: Point instances are frozen at construction, // so on-curve + subgroup facts cannot change afterwards. Only success is cached — invalid // points re-throw on every call. This matters most for pairing curves, where subgroup checks // cost a scalar multiplication and the same instance is re-validated across layers // (signature fromBytes, pairingBatch) or across repeated verifies with a cached public key. const validityCache = new WeakSet(); /** * 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 WeierstrassPoint { static readonly BASE = new Point(CURVE.Gx, CURVE.Gy, Fp.ONE); static readonly ZERO = new Point(Fp.ZERO, Fp.ONE, Fp.ZERO); static readonly Fp = Fp; static readonly Fn = Fn; readonly X: T; readonly Y: T; readonly Z: T; /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ constructor(X: T, Y: T, Z: T) { this.X = acoord('x', X); // This is not just about ZERO / infinity: ambient curves can have real // finite points with y=0. Those points are 2-torsion, so they cannot lie // in the odd prime-order subgroups this point type is meant to represent. this.Y = acoord('y', Y, true); this.Z = acoord('z', Z); Object.freeze(this); } static CURVE(): WeierstrassOpts { return CURVE; } /** Does NOT validate if the point is valid. Use `.assertValidity()`. */ static fromAffine(p: AffinePoint): 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); } static fromBytes(bytes: TArg): Point { const P = Point.fromAffine(decodePoint(abytes(bytes, undefined, 'point'))); P.assertValidity(); return P; } static fromHex(hex: string): Point { return Point.fromBytes(hexToBytes(hex)); } get x(): T { return this.toAffine().x; } get y(): T { return this.toAffine().y; } /** * @param isLazy - true will defer table computation until the first multiplication */ precompute(windowSize: number = 6, isLazy = true): Point { wnaf.setWindowSize(this, windowSize); if (!isLazy) this.multiply(_3n); // random number return this; } // TODO: return `this` /** A point on curve is valid if it conforms to equation. */ assertValidity(): void { const p = this; if (p.is0()) { // (0, 1, 0) aka ZERO is invalid in most contexts. // In BLS, ZERO can be serialized, so we allow it. // Keep the accepted infinity encoding canonical: projective-equivalent (X, Y, 0) points // like (1, 1, 0) compare equal to ZERO, but only (0, 1, 0) should pass this guard. if (allowInfinityPoint && Fp.is0(p.X) && Fp.eql(p.Y, Fp.ONE) && Fp.is0(p.Z)) return; throw new Error('bad point: ZERO'); } if (validityCache.has(p)) return; // 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'); validityCache.add(p); } 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: WeierstrassPoint): boolean { aprjpoint(other); const { X: X1, Y: Y1, Z: Z1 } = this; const { X: X2, Y: Y2, Z: 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.X, Fp.neg(this.Y), this.Z); } // 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 { X: X1, Y: Y1, Z: 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 = mulA(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 = mulA(t2); t3 = Fp.sub(t0, t2); t3 = mulA(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: WeierstrassPoint): Point { aprjpoint(other); const { X: X1, Y: Y1, Z: Z1 } = this; const { X: X2, Y: Y2, Z: Z2 } = other; let X3 = Fp.ZERO, Y3 = Fp.ZERO, Z3 = Fp.ZERO; // prettier-ignore 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 = mulA(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 = mulA(t2); t4 = Fp.mul(b3, t4); t1 = Fp.add(t1, t2); t2 = Fp.sub(t0, t2); // step 30 t2 = mulA(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: WeierstrassPoint) { // Validate before calling `negate()` so wrong inputs fail with the point guard // instead of leaking a foreign `negate()` error. aprjpoint(other); return this.add(other.negate()); } is0(): boolean { return this.equals(Point.ZERO); } /** * Constant time multiplication. * Uses precomputed tables (signed fixed-window wNAF) when available. * Uses scalar blinding and avoids endomorphism splitting in the secret-scalar path. * @param scalar - by which the point would be multiplied * @returns New point */ multiply(scalar: bigint): Point { // Keep the subgroup-scalar contract strict instead of reducing 0 / n to ZERO. // In key/signature-style callers, those values usually mean broken hash/scalar plumbing, // and failing closed is safer than silently producing the identity point. if (!Fn.isValidNot0(scalar)) throw new RangeError('invalid scalar: out of range'); // 0 is invalid const { p, f } = wnaf.mulSecret(this, scalar, cofactor, normalize); return normalize([p, f])[0]; } /** * Non-constant-time multiplication. Uses width-4 wNAF with GLV endomorphism splitting * when available (two half-width scalars sharing one halved doubling chain). * It's faster, but should only be used when you don't care about * an exposed secret key e.g. sig verification, which works over *public* keys. */ multiplyUnsafe(scalar: bigint): Point { const p = this as Point; const sc = scalar; // Public-scalar callers may need 0, but n and larger values stay rejected here too. // Reducing them mod n would turn bad caller input into an accidental identity point. if (!Fn.isValid(sc)) throw new RangeError('invalid scalar: out of range'); // 0 is valid if (sc === _0n || p.is0()) return Point.ZERO; if (sc === _1n) return p; if (wnaf.hasWindowSize(this)) return wnaf.mulUnsafe(p, sc, normalize); // precomputes const points: Point[] = []; const scalars: bigint[] = []; pushWnafPair(points, scalars, p, sc); return mulAddUnsafe(Point, points, scalars); } /** * Non-constant-time double-scalar multiplication `a⋅this + b⋅other` (Strauss–Shamir). * Both walks share one doubling chain via {@link mulAddUnsafe}, and GLV endomorphism * (when available) halves the chain again by splitting each scalar into two half-width * parts. Used by ECDSA verification and public-key recovery for `R = u1⋅G + u2⋅P`. * Only for public scalars. */ mulAddUnsafe(a: bigint, other: Point, b: bigint): Point { aprjpoint(other); const points: Point[] = []; const scalars: bigint[] = []; pushWnafPair(points, scalars, this as Point, a); pushWnafPair(points, scalars, other, b); return mulAddUnsafe(Point, points, scalars); } /** * Converts Projective point to affine (x, y) coordinates. * (X, Y, Z) ∋ (x=X/Z, y=Y/Z). * @param invertedZ - Z^-1 (inverted zero) - optional, precomputation is useful for invertBatch */ toAffine(invertedZ?: T): AffinePoint { const p = this; let iz = invertedZ; if (iz != null && !Fp.isValid(iz)) throw new RangeError('"invertedZ" expected valid field element'); const { X, Y, Z } = p; // Fast-path for normalized points if (Fp.eql(Z, Fp.ONE)) return { x: X, y: 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 x = Fp.mul(X, iz); const y = 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, y }; } /** * Checks whether Point is free of torsion elements (is in prime subgroup). * Always torsion-free for cofactor=1 curves. */ isTorsionFree(): boolean { if (cofactor === _1n) return true; if (isTorsionFree) return isTorsionFree(Point, this); // unsafe() will use the uncached wNAF path internally, since CURVE_ORDER >= Fn.ORDER return wnaf.mulUnsafe(this, CURVE_ORDER).is0(); } clearCofactor(): Point { if (cofactor === _1n) return this; // Fast-path if (clearCofactor) return clearCofactor(Point, this) as Point; // Default fallback assumes the cofactor fits the usual subgroup-scalar // multiplyUnsafe() contract. Curves with larger / structured cofactors // should define a clearCofactor override anyway (e.g. psi/Frobenius maps). return this.multiplyUnsafe(cofactor); } isSmallOrder(): boolean { if (cofactor === _1n) return this.is0(); // Fast-path return this.clearCofactor().is0(); } toBytes(isCompressed = true): TRet { abool(isCompressed, 'isCompressed'); // assertValidity() covers on-curve and subgroup membership. The encoder below repeats the // infinity check rather than relying on this call, so it stays correct for any caller. this.assertValidity(); return encodePoint(Point, this, isCompressed); } toHex(isCompressed = true): string { return bytesToHex(this.toBytes(isCompressed)); } toString() { return ``; } } const normalize = (points: Point[]) => normalizeZ(Point, points); const wnaf = new ScalarMultiplier(Point, randomBytes); // Enable W=6 wNAF precomputes. Slows down first publicKey computation. // Disable for tiny toy curves, with scalar fields < 6 bits. if (wnaf.bits >= 6) Point.BASE.precompute(6); Object.freeze(Point.prototype); Object.freeze(Point); return Point; } /** Parsed ECDSA signature with helpers for recovery and re-encoding. */ export interface ECDSASignature { /** Signature component `r`. */ readonly r: bigint; /** Signature component `s`. */ readonly s: bigint; /** Optional recovery bit for recoverable signatures. */ readonly recovery?: number; /** * Return a copy of the signature with a recovery bit attached. * @param recovery - Recovery bit to attach. * @returns Signature with an attached recovery bit. */ addRecoveryBit(recovery: number): ECDSASignature & { readonly recovery: number }; /** * Check whether the signature uses the high-S half-order. * @returns Whether the signature uses the high-S half-order. */ hasHighS(): boolean; /** * Recover the public key from the hashed message and recovery bit. * @param messageHash - Hashed message bytes. * @returns Recovered public-key point. */ recoverPublicKey(messageHash: TArg): WeierstrassPoint; /** * Encode the signature into bytes. * @param format - Signature encoding to produce. * @returns Encoded signature bytes. */ toBytes(format?: string): TRet; /** * Encode the signature into hex. * @param format - Signature encoding to produce. * @returns Encoded signature hex. */ toHex(format?: string): string; } /** Constructor and decoding helpers for ECDSA signatures. */ export type ECDSASignatureCons = { /** Create a signature from `r`, `s`, and an optional recovery bit. */ new (r: bigint, s: bigint, recovery?: number): ECDSASignature; /** * Decode a signature from bytes. * @param bytes - Encoded signature bytes. * @param format - Signature encoding to parse. * @returns Parsed signature. */ fromBytes(bytes: TArg, format?: ECDSASignatureFormat): ECDSASignature; /** * Decode a signature from hex. * @param hex - Encoded signature hex. * @param format - Signature encoding to parse. * @returns Parsed signature. */ fromHex(hex: string, format?: ECDSASignatureFormat): ECDSASignature; }; // Points start with byte 0x02 when y is even; otherwise 0x03 function pprefix(hasEvenY: boolean): TRet { return Uint8Array.of(hasEvenY ? 0x02 : 0x03) as TRet; } function getWLengths(Fp: TArg>, Fn: TArg>) { return { secretKey: Fn.BYTES, publicKey: 1 + Fp.BYTES, publicKeyUncompressed: 1 + 2 * Fp.BYTES, publicKeyHasPrefix: true, // Raw compact `(r || s)` signature width; DER and recovered signatures use // different lengths outside this helper. signature: 2 * Fn.BYTES, }; } /** * Sometimes users only need getPublicKey, getSharedSecret, and secret key handling. * This helper ensures no signature functionality is present. Less code, smaller bundle size. * @param Point - Weierstrass point constructor. * @param ecdhOpts - Optional randomness helpers: * - `randomBytes` (optional): Optional RNG override. * @returns ECDH helper namespace. * @example * Sometimes users only need getPublicKey, getSharedSecret, and secret key handling. * * ```ts * import { ecdh } from '@noble/curves/abstract/weierstrass.js'; * import { p256 } from '@noble/curves/nist.js'; * const dh = ecdh(p256.Point); * const alice = dh.keygen(); * const shared = dh.getSharedSecret(alice.secretKey, alice.publicKey); * ``` */ export function ecdh( Point: WeierstrassPointCons, ecdhOpts: TArg<{ randomBytes?: (bytesLength?: number) => TRet }> = {} ): ECDH { validatePointCons(Point); const { Fn } = Point; const randomBytes_ = ecdhOpts.randomBytes === undefined ? wcRandomBytes : ecdhOpts.randomBytes; // Keep the advertised seed length aligned with mapHashToField(), which keeps a hard 16-byte // minimum even on toy curves. const lengths = Object.assign(getWLengths(Point.Fp, Fn), { seed: Math.max(getMinHashLength(Fn.ORDER), 16), }); function isValidSecretKey(secretKey: TArg) { try { const num = Fn.fromBytes(secretKey); return Fn.isValidNot0(num); } catch (error) { return false; } } function isValidPublicKey(publicKey: TArg, isCompressed?: boolean): boolean { const { publicKey: comp, publicKeyUncompressed } = lengths; try { const l = publicKey.length; if (isCompressed === true && l !== comp) return false; if (isCompressed === false && l !== publicKeyUncompressed) return false; // SEC 1 §3.2.2: the identity is never a valid public key, even on curves whose codec // can decode it. return !Point.fromBytes(publicKey).is0(); } catch (error) { return false; } } /** * Produces cryptographically secure secret key from random of size * (groupLen + ceil(groupLen / 2)) with modulo bias being negligible. */ function randomSecretKey(seed?: TArg): TRet { seed = seed === undefined ? randomBytes_(lengths.seed) : seed; return mapHashToField(abytes(seed, lengths.seed, 'seed'), Fn.ORDER) as TRet; } /** * Computes public key for a secret key. Checks for validity of the secret key. * @param isCompressed - whether to return compact (default), or full key * @returns Public key, full when isCompressed=false; short when isCompressed=true */ function getPublicKey(secretKey: TArg, isCompressed = true): TRet { return Point.BASE.multiply(Fn.fromBytes(secretKey)).toBytes(isCompressed); } /** * Quick and dirty check for item being public key. Does not validate hex, or being on-curve. */ function isProbPub(item: TArg): boolean | undefined { const { secretKey, publicKey, publicKeyUncompressed } = lengths; const allowedLengths = (Fn as { _lengths?: readonly number[] })._lengths; if (!isBytes(item)) return undefined; const l = abytes(item, undefined, 'key').length; const isPub = l === publicKey || l === publicKeyUncompressed; const isSec = l === secretKey || !!allowedLengths?.includes(l); // P-521 accepts both 65- and 66-byte secret keys, so overlapping lengths stay ambiguous. if (isPub && isSec) return undefined; return isPub; } /** * ECDH (Elliptic Curve Diffie Hellman). * Computes encoded shared point from secret key A and public key B. * Checks: 1) secret key validity 2) shared key is on-curve. * Does NOT hash the result or expose the SEC 1 x-coordinate-only `z`. * Returns the encoded shared point on purpose: callers that need `x_P` * can derive it from the encoded point, but `x_P` alone cannot recover the * point/parity back. * This helper only exposes the fully validated public-key path, not cofactor DH. * @param isCompressed - whether to return compact (default), or full key * @returns shared point encoding */ function getSharedSecret( secretKeyA: TArg, publicKeyB: TArg, isCompressed = true ): TRet { if (isProbPub(secretKeyA) === true) throw new Error('first arg must be private key'); if (isProbPub(publicKeyB) === false) throw new Error('second arg must be public key'); const s = Fn.fromBytes(secretKeyA); const b = Point.fromBytes(publicKeyB); // checks for being on-curve if (b.is0()) throw new Error('invalid public key: point at infinity'); return b.multiply(s).toBytes(isCompressed); } const utils = { isValidSecretKey, isValidPublicKey, randomSecretKey, }; const keygen = createKeygen(randomSecretKey, getPublicKey); Object.freeze(utils); Object.freeze(lengths); return Object.freeze({ getPublicKey, getSharedSecret, keygen, Point, utils, lengths }); } /** * Creates ECDSA signing interface for given elliptic curve `Point` and `hash` function. * * @param Point - created using {@link weierstrass} function * @param hash - used for 1) message prehash-ing 2) k generation in `sign`, using hmac_drbg(hash) * @param ecdsaOpts - rarely needed, see {@link ECDSAOpts}: * - `lowS`: Default low-S policy. * - `hmac`: HMAC implementation used by RFC6979 DRBG. * - `randomBytes`: Optional RNG override. * - `bits2int`: Optional hash-to-int conversion override. * - `bits2int_modN`: Optional hash-to-int-mod-n conversion override. * * @returns ECDSA helper namespace. * @example * Create an ECDSA signer/verifier bundle for one curve implementation. * * ```ts * import { ecdsa } from '@noble/curves/abstract/weierstrass.js'; * import { p256 } from '@noble/curves/nist.js'; * import { sha256 } from '@noble/hashes/sha2.js'; * const p256ecdsa = ecdsa(p256.Point, sha256); * const { secretKey, publicKey } = p256ecdsa.keygen(); * const msg = new TextEncoder().encode('hello noble'); * const sig = p256ecdsa.sign(msg, secretKey); * const isValid = p256ecdsa.verify(sig, msg, publicKey); * ``` */ export function ecdsa( Point: WeierstrassPointCons, hash: TArg, ecdsaOpts: TArg = {} ): ECDSA { validatePointCons(Point); // Custom hash / bits2int hooks are treated as pure functions over validated caller-owned bytes. const hash_ = hash as CHash; ahash(hash_); validateObject( ecdsaOpts, {}, { hmac: 'function', lowS: 'boolean', randomBytes: 'function', bits2int: 'function', bits2int_modN: 'function', } ); const opts = Object.assign({}, ecdsaOpts) as ECDSAOpts; const randomBytes = opts.randomBytes === undefined ? wcRandomBytes : opts.randomBytes; const hmac = opts.hmac === undefined ? (key: TArg, msg: TArg) => nobleHmac(hash_, key, msg) : (opts.hmac as HmacFn); const { Fp, Fn } = Point; const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn; // Nonce-inversion blinding in k2sig draws `getMinHashLength(n)` bytes per sign. Probe the RNG // once (see {@link probeRandomBytes}, shared with ScalarMultiplier): in environments without // working randomness, signing downgrades to Fermat inversion (invertCt) instead of throwing on // every sign(). The shape of returned bytes is still validated (by mapHashToField) on every // blinded call, where breakage fails closed. const blindLength = getMinHashLength(CURVE_ORDER); const csprng = probeRandomBytes(randomBytes, blindLength); const { keygen, getPublicKey, getSharedSecret, utils, lengths } = ecdh(Point, opts); const defaultSigOpts: Required = { prehash: true, lowS: typeof opts.lowS === 'boolean' ? opts.lowS : true, format: 'compact' as ECDSASignatureFormat, extraEntropy: false, }; // SEC 1 4.1.6 public-key recovery tries x = r + jn for j = 0..h. Our recovered-signature // format only stores one overflow bit, so it can only distinguish q.x = r from q.x = r + n. // A third lift would have the form q.x = r + 2n. Since valid ECDSA r is in 1..n-1, the // smallest such lift is 1 + 2n, not 2n. const hasLargeRecoveryLifts = CURVE_ORDER * _2n + _1n < Fp.ORDER; function isBiggerThanHalfOrder(number: bigint) { const HALF = CURVE_ORDER >> _1n; return number > HALF; } function validateRS(title: string, num: bigint): bigint { if (!Fn.isValidNot0(num)) throw new Error(`invalid signature ${title}: out of range 1..Point.Fn.ORDER`); return num; } function assertFieldSignIsSupported(): void { if (!Fp.isOdd) throw new Error("Field doesn't support isOdd"); } // Recovery id of an affine point (x, y) whose x reduces to signature `r` mod n: // bit 0 = y parity, bit 1 = x overflowed the group order (x = r + n). function getRecoveryBit(x: bigint, y: bigint, r: bigint): number { assertFieldSignIsSupported(); return (x === r ? 0 : 2) | Number(Fp.isOdd!(y)); } function assertRecoverableCurve(): void { // ECDSA recovery only supports curves where the current recovery id can distinguish // q.x = r and q.x = r + n; larger lifts may need additional `r + n*i` branches. // SEC 1 4.1.6 recovers candidates via x = r + jn, but this format only encodes j = 0 or 1. // The next possible candidate is q.x = r + 2n, and its smallest valid value is 1 + 2n. // To easily get i, we either need to: // a. increase amount of valid recid values (4, 5...); OR // b. prohibit recovered signatures for those curves. if (hasLargeRecoveryLifts) throw new Error('"recovered" sig type is not supported for cofactor >2 curves'); } function validateSigLength(bytes: TArg, format: ECDSASignatureFormat) { validateSigFormat(format); const size = lengths.signature!; const sizer = format === 'compact' ? size : format === 'recovered' ? size + 1 : undefined; return abytes(bytes, sizer); } /** * ECDSA signature with its (r, s) properties. Supports compact, recovered & DER representations. */ class Signature implements ECDSASignature { readonly r: bigint; readonly s: bigint; readonly recovery?: number; constructor(r: bigint, s: bigint, recovery?: number) { this.r = validateRS('r', r); // r in [1..N-1]; this.s = validateRS('s', s); // s in [1..N-1]; if (recovery != null) { assertRecoverableCurve(); if (![0, 1, 2, 3].includes(recovery)) throw new Error('invalid recovery id'); this.recovery = recovery; } Object.freeze(this); } static fromBytes( bytes: TArg, format: ECDSASignatureFormat = defaultSigOpts.format ): Signature { validateSigLength(bytes, format); let recid: number | undefined; if (format === 'der') { // Valid scalar INTEGERs use at most Fn.BYTES plus one DER sign-padding byte. Keep the // total bound conservative so malformed inputs are rejected before parsing any INTEGER. if (bytes.length > 2 * Fn.BYTES + 16) throw new DER.Err('invalid signature: DER signature too long'); const { r, s } = DER.toSig(abytes(bytes), Fn.BYTES + 1); return new Signature(r, s); } if (format === 'recovered') { recid = bytes[0]; format = 'compact'; bytes = bytes.subarray(1); } const L = lengths.signature! / 2; const r = bytes.subarray(0, L); const s = bytes.subarray(L, L * 2); return new Signature(Fn.fromBytes(r), Fn.fromBytes(s), recid); } static fromHex(hex: string, format?: ECDSASignatureFormat) { return this.fromBytes(hexToBytes(hex), format); } private assertRecovery(): number { const { recovery } = this; if (recovery == null) throw new Error('invalid recovery id: must be present'); return recovery; } addRecoveryBit(recovery: number): RecoveredSignature { return new Signature(this.r, this.s, recovery) as RecoveredSignature; } // Unlike the top-level helper below, this method expects a digest that has // already been hashed to the curve's message representative. recoverPublicKey(messageHash: TArg): WeierstrassPoint { const { r, s } = this; const recovery = this.assertRecovery(); const radj = recovery === 2 || recovery === 3 ? r + CURVE_ORDER : r; if (!Fp.isValid(radj)) throw new Error('invalid recovery id: sig.r+curve.n != R.x'); const x = Fp.toBytes(radj); const R = Point.fromBytes(concatBytes(pprefix((recovery & 1) === 0), x)); const ir = Fn.inv(radj); // r^-1 const h = bits2int_modN(abytes(messageHash, undefined, '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.mulAddUnsafe(u1, R, u2); if (Q.is0()) throw new Error('invalid recovery: point at infinify'); Q.assertValidity(); return Q; } // Signatures should be low-s, to prevent malleability. hasHighS(): boolean { return isBiggerThanHalfOrder(this.s); } toBytes(format: ECDSASignatureFormat = defaultSigOpts.format): TRet { validateSigFormat(format); if (format === 'der') return hexToBytes(DER.hexFromSig(this)) as TRet; const { r, s } = this; const rb = Fn.toBytes(r); const sb = Fn.toBytes(s); if (format === 'recovered') { assertRecoverableCurve(); return concatBytes(Uint8Array.of(this.assertRecovery()), rb, sb) as TRet; } return concatBytes(rb, sb) as TRet; } toHex(format?: ECDSASignatureFormat) { return bytesToHex(this.toBytes(format)); } } type RecoveredSignature = Signature & { recovery: number }; Object.freeze(Signature.prototype); Object.freeze(Signature); // 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: (bytes: TArg) => bigint = opts.bits2int === undefined ? function bits2int_def(bytes: TArg): 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; } : (opts.bits2int as (bytes: TArg) => bigint); const bits2int_modN: (bytes: TArg) => bigint = opts.bits2int_modN === undefined ? function bits2int_modN_def(bytes: TArg): bigint { return Fn.create(bits2int(bytes)); // can't use bytesToNumberBE here } : (opts.bits2int_modN as (bytes: TArg) => bigint); const ORDER_MASK = bitMask(fnBits); // Pads output with zero as per spec. /** Converts to bytes. Checks if num in `[0..ORDER_MASK-1]` e.g.: `[0..2^256-1]`. */ function int2octets(num: bigint): TRet { aInRange('num < 2^' + fnBits, num, _0n, ORDER_MASK); return Fn.toBytes(num) as TRet; } function validateMsgAndHash(message: TArg, prehash: boolean): TRet { abytes(message, undefined, 'message'); return ( prehash ? abytes(hash_(message), undefined, 'prehashed message') : message ) as TRet; } /** * Steps A, D of RFC6979 3.2. * Creates RFC6979 seed; converts msg/privKey to numbers. * Used only in sign, not in verify. * * Warning: we cannot assume here that message 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( message: TArg, secretKey: TArg, opts: TArg ) { const { lowS, prehash, extraEntropy } = validateSigOpts(opts, defaultSigOpts); message = validateMsgAndHash(message, prehash); // RFC6979 3.2 A: h1 = H(m) // 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(message); const d = Fn.fromBytes(secretKey); // validate secret key, convert to bigint if (!Fn.isValidNot0(d)) throw new Error('invalid private key'); const seedArgs: TArg[] = [int2octets(d), int2octets(h1int)]; // extraEntropy. RFC6979 3.6: additional k' (optional). if (extraEntropy != null && extraEntropy !== false) { // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') // gen random bytes OR pass as-is const e = extraEntropy === true ? randomBytes(lengths.secretKey) : extraEntropy; seedArgs.push(abytes(e, undefined, 'extraEntropy')); // check for being bytes } const seed = concatBytes(...seedArgs) as TRet; // Step D of RFC6979 3.2 const m = h1int; // no need to call bits2int second time here, it is inside truncateHash! // Converts signature params into point w r/s, checks result for validity. // To transform k => Signature: // q = k⋅G // r = q.x mod n // s = k^-1(m + rd) mod n // The nonce inversion is blinded: with random b ∈ [1,n−1], s = (bk)^-1(bm + bdr) per // https://tches.iacr.org/index.php/TCHES/article/view/7337/6509. Fn.inv()'s extended-Euclidean // loop count depends on its input (cf. Minerva), but here it only ever sees b·k — uniformly // random, independent of k — so its timing reveals nothing about the nonce; b also masks d in // the products. Without a CSPRNG (probed in ecdsa()) we fall back to Fermat inversion // (invertCt), whose control flow is data-independent, at ~4x the inversion cost. function k2sig(kBytes: TArg): Signature | 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 q = Point.BASE.multiply(k).toAffine(); // q = k⋅G const r = Fn.create(q.x); // r = q.x mod n if (r === _0n) return; let s: bigint; if (csprng !== undefined) { // mapHashToField maps 1.5x-order-length uniform bytes into [1, n-1], negligible bias. const b = bytesToNumberBE(mapHashToField(csprng(blindLength), CURVE_ORDER)); const ibk = Fn.inv(Fn.mul(b, k)); // (bk)^-1: inversion input is decorrelated from k const bm = Fn.mul(b, m); const bd = Fn.mul(b, d); s = Fn.create(ibk * Fn.create(bm + bd * r)); // s = (bk)^-1(bm + bdr) = k^-1(m + rd) mod n } else { const ik = invertCt(k, CURVE_ORDER); // k^-1 mod n with data-independent control flow s = Fn.create(ik * Fn.create(m + r * d)); // s = k^-1(m + rd) mod n } if (s === _0n) return; let recovery = getRecoveryBit(q.x, q.y, r); // recovery bit (2 or 3 when q.x>n) let normS = s; if (lowS && isBiggerThanHalfOrder(s)) { normS = Fn.neg(s); // if lowS was passed, ensure s is always in the bottom half of N recovery ^= 1; } return new Signature(r, normS, hasLargeRecoveryLifts ? undefined : recovery); } return { seed, k2sig }; } /** * Signs a message or message hash with a secret key. * With the default `prehash: true`, raw message bytes are hashed internally; * only `{ prehash: false }` expects a caller-supplied digest. * * ``` * sign(m, d) where * k = rfc6979_hmac_drbg(m, d) * (x, y) = G × k * r = x mod n * s = (m + dr) / k mod n * ``` */ function sign( message: TArg, secretKey: TArg, opts: TArg = {} ): TRet { const { seed, k2sig } = prepSig(message, secretKey, opts); // Steps A, D of RFC6979 3.2. const drbg = createHmacDrbg(hash_.outputLen, Fn.BYTES, hmac); const sig = drbg(seed, k2sig); // Steps B, C, D, E, F, G return sig.toBytes(opts.format); } /** * Verifies a signature against message and public key. * Rejects lowS signatures by default: see {@link ECDSAVerifyOpts}. * Implements section 4.1.4 from https://www.secg.org/sec1-v2.pdf: * * ``` * verify(r, s, h, P) where * u1 = hs^-1 mod n * u2 = rs^-1 mod n * R = u1⋅G + u2⋅P * mod(R.x, n) == r * ``` */ function verify( signature: TArg, message: TArg, publicKey: TArg, opts: TArg = {} ): boolean { const { lowS, prehash, format } = validateSigOpts(opts, defaultSigOpts); publicKey = abytes(publicKey, undefined, 'publicKey'); message = validateMsgAndHash(message, prehash); if (!isBytes(signature as any)) { const end = signature instanceof Signature ? ', use sig.toBytes()' : ''; throw new Error('verify expects Uint8Array signature' + end); } validateSigLength(signature, format); // execute this twice because we want loud error try { const sig = Signature.fromBytes(signature, format); const P = Point.fromBytes(publicKey); // SEC 1 verification keys must not be the identity, even when the generic point decoder // permits infinity for another protocol (for example, a pairing curve). if (P.is0()) return false; if (lowS && sig.hasHighS()) return false; const { r, s } = sig; const h = bits2int_modN(message); // mod n, not mod p const is = Fn.inv(s); // s^-1 mod n const u1 = Fn.create(h * is); // u1 = hs^-1 mod n const u2 = Fn.create(r * is); // u2 = rs^-1 mod n const R = Point.BASE.mulAddUnsafe(u1, P, u2); // u1⋅G + u2⋅P, joint Strauss–Shamir if (R.is0()) return false; const q = R.toAffine(); const v = Fn.create(q.x); // v = R.x mod n if (v !== r) return false; // R is the exact point `recoverPublicKey(r, recid)` reconstructs (sR = hG + rP), // so binding the signature to its recovery id only needs R's parity/overflow bits. if (format === 'recovered' && sig.recovery !== getRecoveryBit(q.x, q.y, r)) return false; return true; } catch (e) { return false; } } function recoverPublicKey( signature: TArg, message: TArg, opts: TArg = {} ): TRet { // Top-level recovery mirrors `sign()` / `verify()`: it hashes raw message // bytes first unless the caller passes `{ prehash: false }`. const { prehash } = validateSigOpts(opts, defaultSigOpts); message = validateMsgAndHash(message, prehash); return Signature.fromBytes(signature, 'recovered').recoverPublicKey(message).toBytes(); } // utils.isValidPublicKey() already rejects the identity, so ECDSA needs no shadow copy. return Object.freeze({ keygen, getPublicKey, getSharedSecret, utils, lengths, Point, sign, verify, recoverPublicKey, Signature, hash: hash_, }) satisfies Signer; }