From 03081fd66941e6d95fbd4214ef98eba792acf63b Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Thu, 4 Dec 2025 06:29:55 -0800 Subject: [PATCH] Remove undesired recovery and signature methods. --- src/lib/crypto/secp256k1.ts | 211 +----------------------------------- 1 file changed, 6 insertions(+), 205 deletions(-) diff --git a/src/lib/crypto/secp256k1.ts b/src/lib/crypto/secp256k1.ts index f6ea29d..dc6e05c 100644 --- a/src/lib/crypto/secp256k1.ts +++ b/src/lib/crypto/secp256k1.ts @@ -27,70 +27,26 @@ type Point = { toBytes: (isCompressed?: boolean) => Bytes toHex: (isCompressed?: boolean) => string } -type Signature = ReturnType /** Alias to Uint8Array. */ -export type Bytes = Uint8Array - -/** Signature instance, which allows recovering pubkey from it. */ - -export type RecoveredSignature = Signature & { recovery: number } +type Bytes = Uint8Array /** Point in 2d xy affine coordinates. */ -export type AffinePoint = { +type AffinePoint = { x: bigint y: bigint } -export type ECDSAExtraEntropy = boolean | Bytes - -/** - * - `compact` is the default format - * - `recovered` is the same as compact, but with an extra byte indicating recovery byte - * - `der` is not supported; and provided for consistency. - * Switch to noble-curves if you need der. - */ -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 = { - 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 which have (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 sigs with increased security, see {@link ECDSAExtraEntropy} - */ -export type ECDSASignOpts = { - prehash?: boolean - lowS?: boolean - format?: ECDSASignatureFormat - extraEntropy?: ECDSAExtraEntropy -} - +/** Type arithmetic to enable validation of literal integer types */ type EnsureNumber = A extends number ? A : never type Length = EnsureNumber type Tuple = Length extends N ? Accumulator : Tuple type Add = Length<[...Tuple, ...Tuple]> -type Subtract = Tuple extends [...Tuple, ...infer Rest,] ? Length : never -type IsLessThanOrEqual = Tuple extends [...Tuple, ...infer _Rest,] ? true : false -type IsLessThan = IsLessThanOrEqual, B> -type Multiply = B extends 0 ? 0 : Add>, A> -type Quotient = B extends 0 ? never : B extends 1 ? A : A extends 0 ? 0 : A extends B ? 1 : IsLessThan extends true ? never : Multiply extends A ? C : Quotient> + +/** Public key length definitions */ type Secp256k1Lengths = { publicKey: Add publicKeyUncompressed: Add - signature: typeof Secp256k1.L2 - seed: Add> } export class Secp256k1 { @@ -115,9 +71,7 @@ export class Secp256k1 { static L2: 64 = 64 static lengths: Secp256k1Lengths = { publicKey: 33, - publicKeyUncompressed: 65, - signature: 64, - seed: 48, + publicKeyUncompressed: 65 } // ## Helpers @@ -144,16 +98,6 @@ export class Secp256k1 { return value } - // ASCII characters - static C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 } as const - - static _char (char: number): number | undefined { - if (char >= this.C._0 && char <= this.C._9) return char - this.C._0 // '2' => 50-48 - if (char >= this.C.A && char <= this.C.F) return char - (this.C.A - 10) // 'B' => 66-(65-10) - if (char >= this.C.a && char <= this.C.f) return char - (this.C.a - 10) // 'b' => 98-(97-10) - return - } - static hexToBytes (hex: string): Bytes { if (!/[0-9A-Fa-f]+/.test(hex ?? '')) return this.err('hex invalid') if (hex.length % 2) hex = `0${hex}` @@ -441,149 +385,6 @@ export class Secp256k1 { } } - static assertRecoveryBit (recovery?: number) { - if (![0, 1, 2, 3].includes(recovery!)) this.err('recovery id must be valid and present') - } - static assertSigFormat (format?: ECDSASignatureFormat) { - if (format != null && !this.ALL_SIG.includes(format)) - this.err(`Signature format must be one of: ${this.ALL_SIG.join(', ')}`) - if (format === this.SIG_DER) this.err('Signature format "der" is not supported: switch to noble-curves') - } - static assertSigLength (sig: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT) { - this.assertSigFormat(format) - const SL = this.lengths.signature - const RL = SL + 1 - let msg = `Signature format "${format}" expects Uint8Array with length ` - if (format === this.SIG_COMPACT && sig.length !== SL) this.err(msg + SL) - if (format === this.SIG_RECOVERED && sig.length !== RL) this.err(msg + RL) - } - - /** - * 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" - * - * https://paulmillr.com/posts/deterministic-signatures/ - */ - // todo: better name - static SIG_COMPACT: ECDSASignatureFormat = 'compact' - static SIG_RECOVERED = 'recovered' - static SIG_DER = 'der' - static ALL_SIG = [this.SIG_COMPACT, this.SIG_RECOVERED, this.SIG_DER] as const - - /** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */ - static Signature (r: bigint, s: bigint, recovery?: number) { - return Object.freeze({ - r: this.FnIsValidNot0(r), // 1 <= r < N - s: this.FnIsValidNot0(s), // 1 <= s < N - recovery: recovery ?? undefined, - addRecoveryBit: (bit: number): RecoveredSignature => { - return this.Signature(r, s, bit) as RecoveredSignature - }, - toBytes: (format: ECDSASignatureFormat = this.SIG_COMPACT): Bytes => { - const res = this.concatBytes(this.bigintTo32Bytes(r), this.bigintTo32Bytes(s)) - if (format === this.SIG_RECOVERED) { - this.assertRecoveryBit(recovery) - return this.concatBytes(Uint8Array.of(recovery!), res) - } - return res - } - }) - } - - static signatureFromBytes (b: Bytes, format: ECDSASignatureFormat = this.SIG_COMPACT): Signature { - this.assertSigLength(b, format) - let recoveryBit: number | undefined - if (format === this.SIG_RECOVERED) { - recoveryBit = b[0] - b = b.subarray(1) - } - const r = this.bytesToBigint(b.subarray(0, this.L)) - const s = this.bytesToBigint(b.subarray(this.L, this.L2)) - return this.Signature(r, s, recoveryBit) - } - - /** - * RFC6979: ensure ECDSA msg is X bytes, convert to BigInt. - * 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. - */ - static bits2int (bytes: Bytes): bigint { - const delta = bytes.length * 8 - 256 - if (delta > 1024) this.err('msg invalid') // our CUSTOM check, "just-in-case": prohibit long inputs - const num = this.bytesToBigint(bytes) - return delta > 0 ? num >> BigInt(delta) : num - } - /** int2octets can't be used; pads small msgs with 0: BAD for truncation as per RFC vectors */ - static bits2int_modN = (bytes: Bytes): bigint => this.modN(this.bits2int(this.abytes(bytes))) - - static defaultSignOpts: ECDSASignOpts = { - lowS: true, - prehash: true, - format: this.SIG_COMPACT, - extraEntropy: false, - } - - static async prepMsg (msg: Bytes, opts: ECDSARecoverOpts): Promise { - this.abytes(msg, undefined, 'message') - if (!opts.prehash) return msg - return new Uint8Array(await crypto.subtle.digest('SHA-256', msg)) - } - - static NULL: Bytes = new Uint8Array(0) - static byte0 = this.u8of(0x00) - static byte1 = this.u8of(0x01) - static _maxDrbgIters = 1000 - static _drbgErr = 'drbg: tried max amount of iterations' - - static setDefaults (opts: ECDSASignOpts): Required { - const res: ECDSASignOpts = {} - Object.keys(this.defaultSignOpts).forEach((k: string) => { - // @ts-ignore - res[k] = opts[k] ?? defaultSignOpts[k] - }) - return res as Required - } - - static _recover (signature: Bytes, messageHash: Bytes): Bytes { - const sig = this.signatureFromBytes(signature, 'recovered') - const { r, s, recovery } = sig - // 0 or 1 recovery id determines sign of "y" coordinate. - // 2 or 3 means q.x was >N. - this.assertRecoveryBit(recovery) - const h = this.bits2int_modN(this.abytes(messageHash, this.L)) // Truncate hash - const radj = recovery === 2 || recovery === 3 ? r + this.N : r - this.FpIsValidNot0(radj) // ensure q.x is still a field element - const head = this.getPrefix(BigInt(recovery!)) // head is 0x02 or 0x03 - const Rb = this.concatBytes(head, this.bigintTo32Bytes(radj)) // concat head + r - const R = this.pointFromBytes(Rb) - const ir = this.invert(radj, this.N) // r^-1 - const u1 = this.modN(-h * ir) // -hr^-1 - const u2 = this.modN(s * ir) // sr^-1 - const point = this.doubleScalarMultiplyUnsafe(R, u1, u2) // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) - return point.toBytes() - } - - /** - * ECDSA public key recovery. Requires msg hash and recovery id. - * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6. - */ - - static async recoverPublicKeyAsync (signature: Bytes, message: Bytes, opts: ECDSARecoverOpts = {}): Promise { - message = await this.prepMsg(message, this.setDefaults(opts)) - return this._recover(signature, message) - } - // ## Precomputes // -------------- -- 2.47.3