]> git.codecow.com Git - libnemo.git/commitdiff
Remove undesired recovery and signature methods.
authorChris Duncan <chris@zoso.dev>
Thu, 4 Dec 2025 14:29:55 +0000 (06:29 -0800)
committerChris Duncan <chris@zoso.dev>
Thu, 4 Dec 2025 14:29:55 +0000 (06:29 -0800)
src/lib/crypto/secp256k1.ts

index f6ea29d6de52b0f077cddc494c8a2439ae945f7e..dc6e05cc5f8531b6b3fdd956a426274c1e536477 100644 (file)
@@ -27,70 +27,26 @@ type Point = {
        toBytes: (isCompressed?: boolean) => Bytes
        toHex: (isCompressed?: boolean) => string
 }
-type Signature = ReturnType<typeof Secp256k1.Signature>
 
 /** Alias to Uint8Array. */
-export type Bytes = Uint8Array<ArrayBuffer>
-
-/** Signature instance, which allows recovering pubkey from it. */
-
-export type RecoveredSignature = Signature & { recovery: number }
+type Bytes = Uint8Array<ArrayBuffer>
 
 /** 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 extends number ? A : never
 type Length<T extends any[]> = EnsureNumber<T['length']>
 type Tuple<N extends number, T, Accumulator extends T[] = []> = Length<Accumulator> extends N ? Accumulator : Tuple<N, T, [...Accumulator, T]>
 type Add<A extends number, B extends number> = Length<[...Tuple<A, any>, ...Tuple<B, any>]>
-type Subtract<A extends number, B extends number> = Tuple<A, any> extends [...Tuple<B, any>, ...infer Rest,] ? Length<Rest> : never
-type IsLessThanOrEqual<A extends number, B extends number> = Tuple<B, any> extends [...Tuple<A, any>, ...infer _Rest,] ? true : false
-type IsLessThan<A extends number, B extends number> = IsLessThanOrEqual<Add<A, 1>, B>
-type Multiply<A extends number, B extends number> = B extends 0 ? 0 : Add<Multiply<A, Subtract<B, 1>>, A>
-type Quotient<A extends number, B extends number, C extends number = 0> = B extends 0 ? never : B extends 1 ? A : A extends 0 ? 0 : A extends B ? 1 : IsLessThan<A, C> extends true ? never : Multiply<B, C> extends A ? C : Quotient<A, B, Add<C, 1>>
+
+/** Public key length definitions */
 type Secp256k1Lengths = {
        publicKey: Add<typeof Secp256k1.L, 1>
        publicKeyUncompressed: Add<typeof Secp256k1.L2, 1>
-       signature: typeof Secp256k1.L2
-       seed: Add<typeof Secp256k1.L, Quotient<typeof Secp256k1.L, 2>>
 }
 
 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<Bytes> {
-               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<ECDSASignOpts> {
-               const res: ECDSASignOpts = {}
-               Object.keys(this.defaultSignOpts).forEach((k: string) => {
-                       // @ts-ignore
-                       res[k] = opts[k] ?? defaultSignOpts[k]
-               })
-               return res as Required<ECDSASignOpts>
-       }
-
-       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<Bytes> {
-               message = await this.prepMsg(message, this.setDefaults(opts))
-               return this._recover(signature, message)
-       }
-
        // ## Precomputes
        // --------------