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 {
static L2: 64 = 64
static lengths: Secp256k1Lengths = {
publicKey: 33,
- publicKeyUncompressed: 65,
- signature: 64,
- seed: 48,
+ publicKeyUncompressed: 65
}
// ## Helpers
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}`
}
}
- 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
// --------------