diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 875aeff..2528878 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -3,14 +3,14 @@ name: Node CI on: [push, pull_request] jobs: test: - name: v18 @ ubuntu-latest + name: v19 @ ubuntu-latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node }} uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 19 - run: npm install - run: npm run build --if-present - run: npm test diff --git a/README.md b/README.md index 470b305..1ee6d8e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ # noble-secp256k1 ![Node CI](https://github.com/paulmillr/noble-secp256k1/workflows/Node%20CI/badge.svg) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) -[Fastest](#speed) JS implementation of [secp256k1](https://www.secg.org/sec2-v2.pdf), -an elliptic curve that could be used for ECDSA/Schnorr signature schemes and ECDH key agreement protocol. +[Fastest](#speed) 9KB JS implementation of [secp256k1](https://www.secg.org/sec2-v2.pdf), +an elliptic curve that could be used for asymmetric encryption, +ECDH key agreement protocol and signature schemes. Supports deterministic **ECDSA** from RFC6979. -Conforms to [RFC6979](https://datatracker.ietf.org/doc/rfc6979/) for deterministic ECDSA and to [BIP0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). [**Audited**](#security) by an independent security firm. Check out [the online demo](https://paulmillr.com/ecc) and blog post: [Learning fast elliptic-curve cryptography in JS](https://paulmillr.com/posts/noble-secp256k1-fast-ecc/). +The library does not use dependencies and is as minimal as possible. [noble-curves](https://github.com/paulmillr/noble-curves) is advanced drop-in replacement for noble-secp256k1 with more features such as Schnorr signatures, DER encoding and support for different hash functions. + +Check out: [Upgrading](#upgrading) section for v1 to v2 transition instructions; [the online demo](https://paulmillr.com/ecc) and blog post [Learning fast elliptic-curve cryptography in JS](https://paulmillr.com/posts/noble-secp256k1-fast-ecc/). ### This library belongs to _noble_ crypto @@ -21,353 +24,209 @@ Conforms to [RFC6979](https://datatracker.ietf.org/doc/rfc6979/) for determinist ## Usage -Use NPM in node.js / browser, or include single file from -[GitHub's releases page](https://github.com/paulmillr/noble-secp256k1/releases): +Use NPM in browser and node.js: > npm install @noble/secp256k1 -```js -// Common.js and ECMAScript Modules (ESM) -import * as secp from '@noble/secp256k1'; -// If you're using single file, use global variable instead: `window.nobleSecp256k1` +For [Deno](https://deno.land), the module is available at `x/secp256k1`; or you can use [npm specifier](https://deno.land/manual@v1.28.0/node/npm_specifiers). -// Supports both async and sync methods, see docs +```js +import * as secp from '@noble/secp256k1'; // ESM-only. Use bundler for common.js (async () => { // keys, messages & other inputs can be Uint8Arrays or hex strings // Uint8Array.from([0xde, 0xad, 0xbe, 0xef]) === 'deadbeef' - const privKey = secp.utils.randomPrivateKey(); - const pubKey = secp.getPublicKey(privKey); - const msgHash = await secp.utils.sha256('hello world'); - const signature = await secp.sign(msgHash, privKey); - const isValid = secp.verify(signature, msgHash, pubKey); - - // Schnorr signatures - const rpub = secp.schnorr.getPublicKey(privKey); - const rsignature = await secp.schnorr.sign(message, privKey); - const risValid = await secp.schnorr.verify(rsignature, message, rpub); + const privKey = secp.utils.randomPrivateKey(); // Secure random private key + // sha256 of 'hello world' + const msgHash = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'; + const pubKey = secp.getPublicKey(privKey); // Make pubkey from the private key + const signature = await secp.signAsync(msgHash, privKey); // sign + const isValid = secp.verify(signature, msgHash, pubKey); // verify + + const pubKey2 = getPublicKey(secp.utils.randomPrivateKey()); // Key of user 2 + secp.getSharedSecret(privKey, alicesPubkey); // Elliptic curve diffie-hellman + signature.recoverPublicKey(msgHash); // Public key recovery })(); ``` -To use the module with [Deno](https://deno.land), -you will need [import map](https://deno.land/manual/linking_to_external_code/import_maps): +Advanced examples: -- `deno run --import-map=imports.json app.ts` -- app.ts: `import * as secp from "https://deno.land/x/secp256k1/mod.ts";` -- imports.json: `{"imports": {"crypto": "https://deno.land/std@0.153.0/node/crypto.ts"}}` +```ts +// 1. Use the shim to enable synchronous methods. +// Only async methods are available by default to keep library dependency-free. +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from '@noble/hashes/sha256'; +secp.etc.hmacSha256Sync = (k, ...m) => hmac(sha256, k, secp.etc.concatBytes(...m)) +const signature2 = secp.sign(msgHash, privKey); // Can be used now + +// 2. Use the shim only for node.js <= 18 BEFORE importing noble-secp256k1. +// The library depends on global variable crypto to work. It is available in +// all browsers and many environments, but node.js <= 18 don't have it. +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; + +// Other stuff +// Malleable signatures, incompatible with BTC/ETH, but compatible with openssl +// `lowS: true` prohibits signatures which have (sig.s >= CURVE.n/2n) because of +// malleability +const signatureMalleable = secp.sign(msgHash, privKey, { lowS: false }); + +// Signatures with improved security: adds additional entropy `k` for +// deterministic signature, follows section 3.6 of RFC6979. When `true`, it +// would be filled with 32b from CSPRNG. **Strongly recommended** to pass `true` +// to improve security: +// - No disadvantage: if an entropy generator is broken, sigs would be the same +// as they are without the option +// - It would help a lot in case there is an error somewhere in `k` gen. +// Exposing `k` could leak private keys +// - Sigs with extra entropy would have different `r` / `s`, which means they +// would still be valid, but may break some test vectors if you're +// cross-testing against other libs +const signatureImproved = secp.sign(msgHash, privKey, { extraEntropy: true }); +``` ## API -- [`getPublicKey(privateKey)`](#getpublickeyprivatekey) -- [`sign(msgHash, privateKey)`](#signmsghash-privatekey) -- [`verify(signature, msgHash, publicKey)`](#verifysignature-msghash-publickey) -- [`getSharedSecret(privateKeyA, publicKeyB)`](#getsharedsecretprivatekeya-publickeyb) -- [`recoverPublicKey(hash, signature, recovery)`](#recoverpublickeyhash-signature-recovery) -- [`schnorr.getPublicKey(privateKey)`](#schnorrgetpublickeyprivatekey) -- [`schnorr.sign(message, privateKey)`](#schnorrsignmessage-privatekey) -- [`schnorr.verify(signature, message, publicKey)`](#schnorrverifysignature-message-publickey) -- [Utilities](#utilities) +There are 3 main methods: `getPublicKey(privateKey)`, +`sign(messageHash, privateKey)` and +`verify(signature, messageHash, publicKey)`. -##### `getPublicKey(privateKey)` +Only **async methods are available by default** to keep library dependency-free. +To enable sync methods, see below. ```typescript -function getPublicKey(privateKey: Uint8Array | string | bigint, isCompressed = false): Uint8Array; -``` - -Creates public key for the corresponding private key. The default is full 65-byte key. - -- `isCompressed = false` determines whether to return compact (33-byte), or full (65-byte) key. +type Hex = Uint8Array | string; -Internally, it does `Point.BASE.multiply(privateKey)`. If you need actual `Point` instead of -`Uint8Array`, use `Point.fromPrivateKey(privateKey)`. - -##### `sign(msgHash, privateKey)` - -```typescript -function sign( - msgHash: Uint8Array | string, - privateKey: Uint8Array | string, - opts?: Options +// Generates 33-byte / 65-byte public key from 32-byte private key. +function getPublicKey( + privateKey: Hex, + isCompressed?: boolean // optional arg. (default) true=33b key, false=65b. +): Uint8Array; +function getPublicKeyAsync( + privateKey: Hex, + isCompressed?: boolean ): Promise; -function sign( - msgHash: Uint8Array | string, - privateKey: Uint8Array | string, - opts?: Options -): Promise<[Uint8Array, number]>; -``` - -Generates low-s deterministic ECDSA signature as per RFC6979. - -- `msgHash: Uint8Array | string` - 32-byte message hash which would be signed -- `privateKey: Uint8Array | string | bigint` - private key which will sign the hash -- `options?: Options` - _optional_ object related to signature value and format with following keys: - - `recovered: boolean = false` - whether the recovered bit should be included in the result. In this case, the result would be an array of two items. - - `canonical: boolean = true` - whether a signature `s` should be no more than 1/2 prime order. - `true` (default) makes signatures compatible with libsecp256k1, - `false` makes signatures compatible with openssl - - `der: boolean = true` - whether the returned signature should be in DER format. If `false`, it would be in Compact format (32-byte r + 32-byte s) - - `extraEntropy: Uint8Array | string | true` - additional entropy `k'` for deterministic signature, follows section 3.6 of RFC6979. When `true`, it would automatically be filled with 32 bytes of cryptographically secure entropy. **Strongly recommended** to pass `true` to improve security: - - Schnorr signatures are doing it every time - - It would help a lot in case there is an error somewhere in `k` generation. Exposing `k` could leak private keys - - If the entropy generator is broken, signatures would be the same as they are without the option - - Signatures with extra entropy would have different `r` / `s`, which means they - would still be valid, but may break some test vectors if you're cross-testing against other libs - -The function is asynchronous because we're utilizing built-in HMAC API to not rely on dependencies. - -```ts -(async () => { - // Signatures with improved security - const signatureE = await secp.sign(msgHash, privKey, { extraEntropy: true }); - // Malleable signatures, but compatible with openssl - const signatureM = await secp.sign(msgHash, privKey, { canonical: false }); -})(); -``` - -```typescript -function signSync( - msgHash: Uint8Array | string, - privateKey: Uint8Array | string, - opts?: Options -): Uint8Array | [Uint8Array, number]; -``` - -`signSync` counterpart could also be used, you need to set `utils.hmacSha256Sync` to a function with signature `key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array`. Example with `noble-hashes` package: +// Use: +// - `ProjectivePoint.fromPrivateKey(privateKey)` for Point instance +// - `ProjectivePoint.fromHex(publicKey)` to convert hex / bytes into Point. -```ts -import { hmac } from '@noble/hashes/hmac'; -import { sha256 } from '@noble/hashes/sha256'; -secp256k1.utils.hmacSha256Sync = (key, ...msgs) => hmac(sha256, key, secp256k1.utils.concatBytes(...msgs)) -secp256k1.utils.sha256Sync = (...msgs) => sha256(secp256k1.utils.concatBytes(...msgs)) -// Can be used now -secp256k1.signSync(msgHash, privateKey); -schnorr.signSync(message, privateKey) -``` - -##### `verify(signature, msgHash, publicKey)` - -```typescript +// Generates low-s deterministic-k RFC6979 ECDSA signature. +// Use with `extraEntropy: true` to improve security. +function sign( + messageHash: Hex, // message hash (not message) which would be signed + privateKey: Hex, // private key which will sign the hash + opts = {} // optional params `{ lowS: boolean, extraEntropy: boolean | Hex }` +): Signature; +function signAsync(messageHash: Hex, privateKey: Hex, opts = {}): Promise; + +// Verifies ECDSA signature. +// lowS option Ensures a signature.s is in the lower-half of CURVE.n. +// Used in BTC, ETH. +// `{ lowS: false }` should only be used if you need OpenSSL-compatible signatures function verify( - signature: Uint8Array | string, - msgHash: Uint8Array | string, - publicKey: Uint8Array | string + signature: Hex | Signature, // returned by the `sign` function + messageHash: Hex, // message hash (not message) that must be verified + publicKey: Hex, // public (not private) key + opts?: { lowS: boolean } // optional params; { lowS: true } by default ): boolean; -function verify(signature: Signature, msgHash: Uint8Array | string, publicKey: Point): boolean; -``` - -- `signature: Uint8Array | string | { r: bigint, s: bigint }` - object returned by the `sign` function -- `msgHash: Uint8Array | string` - message hash that needs to be verified -- `publicKey: Uint8Array | string | Point` - e.g. that was generated from `privateKey` by `getPublicKey` -- `options?: Options` - _optional_ object related to signature value and format - - `strict: boolean = true` - whether a signature `s` should be no more than 1/2 prime order. - `true` (default) makes signatures compatible with libsecp256k1, - `false` makes signatures compatible with openssl -- Returns `boolean`: `true` if `signature == hash`; otherwise `false` - -##### `getSharedSecret(privateKeyA, publicKeyB)` -```typescript +// Computes ECDH (Elliptic Curve Diffie-Hellman) shared secret between +// key A and different key B. function getSharedSecret( - privateKeyA: Uint8Array | string | bigint, - publicKeyB: Uint8Array | string | Point, - isCompressed = false + privateKeyA: Uint8Array | string, // Alices's private key + publicKeyB: Uint8Array | string, // Bob's public key + isCompressed = true // optional arg. (default) true=33b key, false=65b. ): Uint8Array; -``` - -Computes ECDH (Elliptic Curve Diffie-Hellman) shared secret between a private key and a different public key. - -- To get Point instance, use `Point.fromHex(publicKeyB).multiply(privateKeyA)` -- `isCompressed = false` determines whether to return compact (33-byte), or full (65-byte) key -- If you have one public key you'll be creating lots of secrets against, - consider massive speed-up by using precomputations: - - ```js - const pub = secp.utils.precompute(8, publicKeyB); - // Use pub everywhere instead of publicKeyB - getSharedSecret(privKey, pub); // Now 12x faster - ``` +// Use `ProjectivePoint.fromHex(publicKeyB).multiply(privateKeyA)` for Point instance -##### `recoverPublicKey(hash, signature, recovery)` - -```typescript -function recoverPublicKey( - msgHash: Uint8Array | string, - signature: Uint8Array | string, - recovery: number, - isCompressed = false +// Recover public key from Signature instance with `recovery` bit set +signature.recoverPublicKey( + msgHash: Uint8Array | string ): Uint8Array | undefined; ``` -Recovers public key from message hash, signature & recovery bit. The default is full 65-byte key. - -- `msgHash: Uint8Array | string` - message hash which would be signed -- `signature: Uint8Array | string | { r: bigint, s: bigint }` - object returned by the `sign` function -- `recovery: number` - recovery bit returned by `sign` with `recovered` option -- `isCompressed = false` determines whether to return compact (33-byte), or full (65-byte) key - -Public key is generated by doing scalar multiplication of a base Point(x, y) by a fixed -integer. The result is another `Point(x, y)` which we will by default encode to hex Uint8Array. -If signature is invalid - function will return `undefined` as result. -To get Point instance, use `Point.fromSignature(hash, signature, recovery)`. - -##### `schnorr.getPublicKey(privateKey)` - -```typescript -function schnorrGetPublicKey(privateKey: Uint8Array | string): Uint8Array; -``` - -Calculates 32-byte public key from a private key. - -_Warning:_ it is incompatible with non-schnorr pubkey. Specifically, its _y_ coordinate may be flipped. See [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) for clarification. - -##### `schnorr.sign(message, privateKey)` - -```typescript -function schnorrSign( - message: Uint8Array | string, - privateKey: Uint8Array | string, - auxilaryRandom?: Uint8Array -): Promise; -``` - -Generates Schnorr signature as per BIP0340. Asynchronous, so use `await`. - -- `message: Uint8Array | string` - message (not hash) which would be signed -- `privateKey: Uint8Array | string | bigint` - private key which will sign the hash -- `auxilaryRandom?: Uint8Array` — optional 32 random bytes. By default, the method gathers cryptogarphically secure entropy -- Returns Schnorr signature in Hex format. - -##### `schnorr.verify(signature, message, publicKey)` +A bunch of useful **utilities** are also exposed: ```typescript -function schnorrVerify( - signature: Uint8Array | string, - message: Uint8Array | string, - publicKey: Uint8Array | string -): boolean; -``` - -- `signature: Uint8Array | string | { r: bigint, s: bigint }` - object returned by the `sign` function -- `message: Uint8Array | string` - message (not hash) that needs to be verified -- `publicKey: Uint8Array | string | Point` - e.g. that was generated from `privateKey` by `getPublicKey` -- Returns `boolean`: `true` if `signature == hash`; otherwise `false` - -#### Utilities - -secp256k1 exposes a few internal utilities for improved developer experience. - -```js -// Default output is Uint8Array. If you need hex string as an output: -console.log(secp.utils.bytesToHex(pubKey)); -``` - -```typescript -const utils: { - // Can take 40 or more bytes of uniform input e.g. from CSPRNG or KDF - // and convert them into private key, with the modulo bias being neglible. - // As per FIPS 186 B.1.1. - hashToPrivateKey: (hash: Hex) => Uint8Array; - // Returns `Uint8Array` of 32 cryptographically secure random bytes that can be used as private key - randomPrivateKey: () => Uint8Array; - // Checks private key for validity - isValidPrivateKey(privateKey: PrivKey): boolean; - - // Returns `Uint8Array` of x cryptographically secure random bytes. - randomBytes: (bytesLength?: number) => Uint8Array; - // Converts Uint8Array to hex string - bytesToHex(uint8a: Uint8Array): string; - hexToBytes(hex: string): Uint8Array; - concatBytes(...arrays: Uint8Array[]): Uint8Array; - // Modular division over curve prime - mod: (number: number | bigint, modulo = CURVE.P): bigint; - // Modular inversion - invert(number: bigint, modulo?: bigint): bigint; - - sha256: (message: Uint8Array) => Promise; - hmacSha256: (key: Uint8Array, ...messages: Uint8Array[]) => Promise; - - // You can set up your synchronous methods for `signSync`/`signSchnorrSync` to work. - // The argument order is identical to async methods from above - sha256Sync: undefined; - hmacSha256Sync: undefined; - - // BIP0340-style tagged hashes - taggedHash: (tag: string, ...messages: Uint8Array[]) => Promise; - taggedHashSync: (tag: string, ...messages: Uint8Array[]) => Uint8Array; - - // 1. Returns cached point which you can use to pass to `getSharedSecret` or to `#multiply` by it. - // 2. Precomputes point multiplication table. Is done by default on first `getPublicKey()` call. - // If you want your first getPublicKey to take 0.16ms instead of 20ms, make sure to call - // utils.precompute() somewhere without arguments first. - precompute(windowSize?: number, point?: Point): Point; +export declare const etc: { + hexToBytes: (hex: string) => Bytes; + bytesToHex: (b: Bytes) => string; + concatBytes: (...arrs: Bytes[]) => Uint8Array; + bytesToNumberBE: (b: Bytes) => bigint; + numberToBytesBE: (num: bigint) => Bytes; + mod: (a: bigint, b?: bigint) => bigint; + invert: (num: bigint, md?: bigint) => bigint; + hmacSha256Async: (key: Bytes, ...msgs: Bytes[]) => Promise; + hmacSha256Sync: HmacFnSync; + hashToPrivateKey: (hash: Hex) => Bytes; + randomBytes: (len: number) => Bytes; }; - -secp256k1.CURVE.P // Field, 2 ** 256 - 2 ** 32 - 977 -secp256k1.CURVE.n // Order, 2 ** 256 - 432420386565659656852420866394968145599 -secp256k1.Point.BASE // new secp256k1.Point(Gx, Gy) where -// Gx = 55066263022277343669578718895168534326250603453777594175500187360389116729240n -// Gy = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; - -// Elliptic curve point in Affine (x, y) coordinates. -secp256k1.Point { - constructor(x: bigint, y: bigint); - // Supports compressed and non-compressed hex - static fromHex(hex: Uint8Array | string); - static fromPrivateKey(privateKey: Uint8Array | string | number | bigint); - static fromSignature( - msgHash: Hex, - signature: Signature, - recovery: number | bigint - ): Point | undefined { - toRawBytes(isCompressed = false): Uint8Array; - toHex(isCompressed = false): string; - equals(other: Point): boolean; - negate(): Point; - add(other: Point): Point; - subtract(other: Point): Point; - // Constant-time scalar multiplication. - multiply(scalar: bigint | Uint8Array): Point; +export declare const utils: { + normPrivateKeyToScalar: (p: PrivKey) => bigint; + randomPrivateKey: () => Bytes; + isValidPrivateKey: (key: Hex) => boolean; + precompute(p: Point, windowSize?: number): Point; +}; +class ProjectivePoint { + readonly px: bigint; + readonly py: bigint; + readonly pz: bigint; + constructor(px: bigint, py: bigint, pz: bigint); + static readonly BASE: Point; + static readonly ZERO: Point; + get x(): bigint; + get y(): bigint; + equals(other: Point): boolean; + add(other: Point): Point; + mul(n: bigint, safe?: boolean): Point; + mulAddQUns(R: Point, u1: bigint, u2: bigint): Point; + multiply(n: bigint): Point; + negate(): Point; + toAffine(): AffinePoint; + assertValidity(): Point; + static fromHex(hex: Hex): Point; + toHex(isCompressed?: boolean): string; + toRawBytes(isCompressed?: boolean): Uint8Array; + static fromPrivateKey(n: PrivKey): Point; } -secp256k1.Signature { - constructor(r: bigint, s: bigint); - // DER encoded ECDSA signature - static fromDER(hex: Uint8Array | string); - // R, S 32-byte each - static fromCompact(hex: Uint8Array | string); - assertValidity(): void; - hasHighS(): boolean; // high-S sigs cannot be produced using { canonical: true } - toDERRawBytes(): Uint8Array; - toDERHex(): string; +class Signature { + readonly r: bigint; + readonly s: bigint; + readonly recovery?: number | undefined; + constructor(r: bigint, s: bigint, recovery?: number | undefined); + ok(): Signature; + static fromCompact(hex: Hex): Signature; + hasHighS(): boolean; + recoverPublicKey(msgh: Hex): Point; toCompactRawBytes(): Uint8Array; toCompactHex(): string; } +CURVE // curve prime; order; equation params, generator coordinates ``` ## Security -Noble is production-ready. +The module is production-ready. Use [noble-curves](https://github.com/paulmillr/noble-curves) if you need advanced security. -1. The library has been audited by an independent security firm cure53: [PDF](https://cure53.de/pentest-report_noble-lib.pdf). See [changes since audit](https://github.com/paulmillr/noble-secp256k1/compare/1.2.0..main). - - The audit has been [crowdfunded](https://gitcoin.co/grants/2451/audit-of-noble-secp256k1-cryptographic-library) by community with help of [Umbra.cash](https://umbra.cash). -2. The library has also been fuzzed by [Guido Vranken's cryptofuzz](https://github.com/guidovranken/cryptofuzz). You can run the fuzzer by yourself to check it. +1. The current version is rewrite of v1, which has been audited by cure53: [PDF](https://cure53.de/pentest-report_noble-lib.pdf) (funded by [Umbra.cash](https://umbra.cash) & community). +2. It's being fuzzed by [Guido Vranken's cryptofuzz](https://github.com/guidovranken/cryptofuzz): run the fuzzer by yourself to check. -We're using built-in JS `BigInt`, which is potentially vulnerable to [timing attacks](https://en.wikipedia.org/wiki/Timing_attack) as [per official spec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#cryptography). But, _JIT-compiler_ and _Garbage Collector_ make "constant time" extremely hard to achieve in a scripting language. Which means _any other JS library doesn't use constant-time bigints_. Including bn.js or anything else. Even statically typed Rust, a language without GC, [makes it harder to achieve constant-time](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security) for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones. Use low-level libraries & languages. Nonetheless we've hardened implementation of ec curve multiplication to be algorithmically constant time. +Our EC multiplication is hardened to be algorithmically constant time. We're using built-in JS `BigInt`, which is potentially vulnerable to [timing attacks](https://en.wikipedia.org/wiki/Timing_attack) as [per official spec](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#cryptography). But, _JIT-compiler_ and _Garbage Collector_ make "constant time" extremely hard to achieve in a scripting language. Which means _any other JS library doesn't use constant-time bigints_. Including bn.js or anything else. Even statically typed Rust, a language without GC, [makes it harder to achieve constant-time](https://www.chosenplaintext.ca/open-source/rust-timing-shield/security) for some cases. If your goal is absolute security, don't use any JS lib — including bindings to native ones. Use low-level libraries & languages. -We however consider infrastructure attacks like rogue NPM modules very important; that's why it's crucial to minimize the amount of 3rd-party dependencies & native bindings. If your app uses 500 dependencies, any dep could get hacked and you'll be downloading malware with every `npm install`. Our goal is to minimize this attack vector. +We consider infrastructure attacks like rogue NPM modules very important; that's why it's crucial to minimize the amount of 3rd-party dependencies & native bindings. If your app uses 500 dependencies, any dep could get hacked and you'll be downloading malware with every `npm install`. Our goal is to minimize this attack vector. ## Speed -Benchmarks measured with Apple M2 on MacOS 12 with node.js 18.8. +Use [noble-curves](https://github.com/paulmillr/noble-curves) if you need even higher performance. + +Benchmarks measured with Apple M2 on MacOS 13 with node.js 19. - getPublicKey(utils.randomPrivateKey()) x 7,093 ops/sec @ 140μs/op - sign x 5,615 ops/sec @ 178μs/op - signSync (@noble/hashes) x 5,209 ops/sec @ 191μs/op - verify x 1,114 ops/sec @ 896μs/op - recoverPublicKey x 1,018 ops/sec @ 982μs/op - getSharedSecret aka ecdh x 665 ops/sec @ 1ms/op - getSharedSecret (precomputed) x 7,426 ops/sec @ 134μs/op - Point.fromHex (decompression) x 14,582 ops/sec @ 68μs/op - schnorr.sign x 805 ops/sec @ 1ms/op - schnorr.verify x 1,129 ops/sec @ 885μs/op + getPublicKey(utils.randomPrivateKey()) x 5,047 ops/sec @ 198μs/op + sign x 2,955 ops/sec @ 338μs/op + verify x 470 ops/sec @ 2ms/op + getSharedSecret x 403 ops/sec @ 2ms/op + recoverPublicKey x 485 ops/sec @ 2ms/op + Point.fromHex (decompression) x 7,546 ops/sec @ 132μs/op Compare to other libraries on M1 (`openssl` uses native bindings, not JS): @@ -390,8 +249,6 @@ Compare to other libraries on M1 (`openssl` uses native bindings, not JS): ## Contributing -Check out a blog post about this library: [Learning fast elliptic-curve cryptography in JS](https://paulmillr.com/posts/noble-secp256k1-fast-ecc/). - 1. Clone the repository. 2. `npm install` to install build dependencies like TypeScript 3. `npm run build` to compile TypeScript code @@ -399,6 +256,26 @@ Check out a blog post about this library: [Learning fast elliptic-curve cryptogr Special thanks to [Roman Koblov](https://github.com/romankoblov), who have helped to improve scalar multiplication speed. +## Upgrading + +noble-secp256k1 v2.0 has been reduced 4x to just over 400 lines. It features improved security and smaller attack surface. + +Some functionality present in v1, such as schnorr and DER, was removed: use [**noble-curves**](https://github.com/paulmillr/noble-curves) if you still need it. + +- `getPublicKey()` and `getSharedSecret()` now produce compressed 33-byte signatures by default. If you + need the old 65-byte behavior, set `isCompressed` option as `false`: `getPublicKey(priv, false)`, `getSharedSecret(a, b, false)` +- `sign()`: now returns `Signature` instance with `{ r, s, recovery }` properties. It could still be passed to `verify` as-is. + - `canonical` has been renamed to `lowS`. The default value is the same as before: `lowS: true` + - `recovered` has been removed. Recovery bit is always returned in the `Signature` instance + - `der` has been removed. DER encoding is no longer supported. Use compact format (32-byte r + 32-byte s), `Signature` instance methods `toCompactRawBytes` / `toCompactHex()`: `(await sign(msgHash, priv)).toCompactRawBytes()`. Use curves if you still need der +- `verify()`: `strict` option has been renamed to `lowS`, default value is still the same +- `recoverPublicKey(msgHash, sig, recovery)` has been changed to `sig.recoverPublicKey(msgHash)` +- `Point` is now `ProjectivePoint`, working in 3d xyz projective coordinates instead of 2d xy affine +- Removed schnorr signatures, asn.1 DER, custom precomputes. Use noble-curves if you need them +- Support for environments that can't parse bigint literals has been removed +- Some utils such as `hmacSha256Sync` have been moved to `etc`: `import { etc } from "@noble/secp256k1"; +- node.js 18 and older are not supported without crypto shim (see [Usage](#usage)) + ## License MIT (c) Paul Miller [(https://paulmillr.com)](https://paulmillr.com), see LICENSE file. diff --git a/build/package-lock.json b/build/package-lock.json new file mode 100644 index 0000000..b3f326c --- /dev/null +++ b/build/package-lock.json @@ -0,0 +1,135 @@ +{ + "name": "build", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "build", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "terser": "5.16.6" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.16.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.6.tgz", + "integrity": "sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/build/package.json b/build/package.json new file mode 100644 index 0000000..bade171 --- /dev/null +++ b/build/package.json @@ -0,0 +1,16 @@ +{ + "name": "build", + "private": true, + "version": "1.0.0", + "description": "Builds minified file", + "main": "noble-secp256k1.js", + "scripts": { + "terser": "terser --ecma 2020 -m -c < ../index.js" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "terser": "5.16.6" + } +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..963ad9c --- /dev/null +++ b/index.d.ts @@ -0,0 +1,92 @@ +export declare const CURVE: { + P: bigint; + n: bigint; + a: bigint; + b: bigint; + Gx: bigint; + Gy: bigint; +}; +declare type Bytes = Uint8Array; +declare type Hex = Bytes | string; +declare type PrivKey = Hex | bigint; +interface AffinePoint { + x: bigint; + y: bigint; +} +declare class Point { + readonly px: bigint; + readonly py: bigint; + readonly pz: bigint; + constructor(px: bigint, py: bigint, pz: bigint); + static readonly BASE: Point; + static readonly ZERO: Point; + get x(): bigint; + get y(): bigint; + equals(other: Point): boolean; + neg(): Point; + dbl(): Point; + add(other: Point): Point; + mul(n: bigint, safe?: boolean): Point; + mulAddQUns(R: Point, u1: bigint, u2: bigint): Point; + aff(): AffinePoint; + ok(): Point; + multiply(n: bigint): Point; + negate(): Point; + toAffine(): AffinePoint; + assertValidity(): Point; + static fromHex(hex: Hex): Point; + toHex(isCompressed?: boolean): string; + toRawBytes(isCompressed?: boolean): Uint8Array; + static fromPrivateKey(n: PrivKey): Point; +} +export declare const getPublicKey: (privKey: PrivKey, isCompressed?: boolean) => Uint8Array; +export declare class Signature { + readonly r: bigint; + readonly s: bigint; + readonly recovery?: number | undefined; + constructor(r: bigint, s: bigint, recovery?: number | undefined); + ok(): Signature; + static fromCompact(hex: Hex): Signature; + hasHighS(): boolean; + recoverPublicKey(msgh: Hex): Point; + toCompactRawBytes(): Uint8Array; + toCompactHex(): string; +} +declare type HmacFnSync = undefined | ((key: Bytes, ...msgs: Bytes[]) => Bytes); +export declare const signAsync: (msgh: Hex, priv: Hex, opts?: { + lowS?: boolean | undefined; + extraEntropy?: boolean | Hex | undefined; +}) => Promise; +export declare const sign: (msgh: Hex, priv: Hex, opts?: { + lowS?: boolean | undefined; + extraEntropy?: boolean | Hex | undefined; +}) => Signature; +declare type SigLike = { + r: bigint; + s: bigint; +}; +export declare const verify: (sig: Hex | SigLike, msgh: Hex, pub: Hex, opts?: { + lowS: boolean; +}) => boolean; +export declare const getSharedSecret: (privA: Hex, pubB: Hex, isCompressed?: boolean) => Uint8Array; +export declare const etc: { + hexToBytes: (hex: string) => Bytes; + bytesToHex: (b: Bytes) => string; + concatBytes: (...arrs: Bytes[]) => Uint8Array; + bytesToNumberBE: (b: Bytes) => bigint; + numberToBytesBE: (num: bigint) => Bytes; + mod: (a: bigint, b?: bigint) => bigint; + invert: (num: bigint, md?: bigint) => bigint; + hmacSha256Async: (key: Bytes, ...msgs: Bytes[]) => Promise; + hmacSha256Sync: HmacFnSync; + hashToPrivateKey: (hash: Hex) => Bytes; + randomBytes: (len: number) => Bytes; +}; +export declare const utils: { + normPrivateKeyToScalar: (p: PrivKey) => bigint; + randomPrivateKey: () => Bytes; + isValidPrivateKey: (key: Hex) => boolean; + precompute(p: Point, windowSize?: number): Point; +}; +export declare const ProjectivePoint: typeof Point; +export {}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..cce3a0b --- /dev/null +++ b/index.js @@ -0,0 +1,526 @@ +/*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ +const B256 = 2n ** 256n; // secp256k1 is short weierstrass curve +const P = B256 - 0x1000003d1n; // curve's field prime +const N = B256 - 0x14551231950b75fc4402da1732fc9bebfn; // curve (group) order +const _a = 0n; // a equation's param +const _b = 7n; // b equation's param +const Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n; // base point x +const Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n; // base point y +export const CURVE = { P, n: N, a: _a, b: _b, Gx, Gy }; // exported variables +const fLen = 32; // field / group byte length +const crv = (x) => mod(mod(x * mod(x * x)) + _a * x + _b); // x³ + ax + b weierstrass formula +const err = (m = '') => { throw new Error(m); }; // error helper, messes-up stack trace +const big = (n) => typeof n === 'bigint'; // is big integer +const str = (s) => typeof s === 'string'; // is string +const fe = (n) => big(n) && 0n < n && n < P; // is field element (invertible) +const ge = (n) => big(n) && 0n < n && n < N; // is group element +const au8 = (a, l) => // is Uint8Array (of specific length) + !(a instanceof Uint8Array) || (typeof l === 'number' && l > 0 && a.length !== l) ? + err('Uint8Array expected') : a; +const u8n = (data) => new Uint8Array(data); // creates Uint8Array +const u8fr = (arr) => Uint8Array.from(arr); // another shortcut +const toU8 = (a, len) => au8(str(a) ? h2b(a) : a, len); // normalize (hex/u8a) to u8a +const toPriv = (p) => { + if (!big(p)) + p = b2n(toU8(p, fLen)); // convert to bigint when bytes + return ge(p) ? p : err('private key out of range'); // check if bigint is in range +}; +let Gpows = undefined; // precomputes for base point G +const isPoint = (p) => (p instanceof Point ? p : err('Point expected')); // is 3d point +class Point { + constructor(px, py, pz) { + this.px = px; + this.py = py; + this.pz = pz; + } // z is optional + get x() { return this.aff().x; } // .x, .y will call expensive toAffine. + get y() { return this.aff().y; } // Should be used with care. + equals(other) { + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = isPoint(other); // isPoint() checks class equality + const X1Z2 = mod(X1 * Z2), X2Z1 = mod(X2 * Z1); + const Y1Z2 = mod(Y1 * Z2), Y2Z1 = mod(Y2 * Z1); + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; + } + neg() { return new Point(this.px, mod(-this.py), this.pz); } // negate, flips point over y coord + dbl() { return this.add(this); } // point doubling + add(other) { + const { px: X1, py: Y1, pz: Z1 } = this; // formula from Renes-Costello-Batina + const { px: X2, py: Y2, pz: Z2 } = isPoint(other); // https://eprint.iacr.org/2015/1060, algo 1 + const { a, b } = CURVE; + let X3 = 0n, Y3 = 0n, Z3 = 0n; // Cost: 12M + 0S + 3*a + 3*b3 + 23add + const b3 = mod(b * 3n); + let t0 = mod(X1 * X2), t1 = mod(Y1 * Y2), t2 = mod(Z1 * Z2), t3 = mod(X1 + Y1); // step 1 + let t4 = mod(X2 + Y2); // step 5 + t3 = mod(t3 * t4); + t4 = mod(t0 + t1); + t3 = mod(t3 - t4); + t4 = mod(X1 + Z1); + let t5 = mod(X2 + Z2); // step 10 + t4 = mod(t4 * t5); + t5 = mod(t0 + t2); + t4 = mod(t4 - t5); + t5 = mod(Y1 + Z1); + X3 = mod(Y2 + Z2); // step 15 + t5 = mod(t5 * X3); + X3 = mod(t1 + t2); + t5 = mod(t5 - X3); + Z3 = mod(a * t4); + X3 = mod(b3 * t2); // step 20 + Z3 = mod(X3 + Z3); + X3 = mod(t1 - Z3); + Z3 = mod(t1 + Z3); + Y3 = mod(X3 * Z3); + t1 = mod(t0 + t0); // step 25 + t1 = mod(t1 + t0); + t2 = mod(a * t2); + t4 = mod(b3 * t4); + t1 = mod(t1 + t2); + t2 = mod(t0 - t2); // step 30 + t2 = mod(a * t2); + t4 = mod(t4 + t2); + t0 = mod(t1 * t4); + Y3 = mod(Y3 + t0); + t0 = mod(t5 * t4); // step 35 + X3 = mod(t3 * X3); + X3 = mod(X3 - t0); + t0 = mod(t3 * t1); + Z3 = mod(t5 * Z3); + Z3 = mod(Z3 + t0); // step 40 + return new Point(X3, Y3, Z3); + } + mul(n, safe = true) { + if (!safe && n === 0n) + return I; // in unsafe mode, allow zero + if (!ge(n)) + err('invalid scalar'); // must be 0 < n < CURVE.n + if (this.equals(G)) + return wNAF(n).p; // Use precomputes for base point + let p = I, f = G; // init result point & fake point + for (let d = this; n > 0n; d = d.dbl(), n >>= 1n) { // double-and-add ladder + if (n & 1n) + p = p.add(d); // if bit is present, add to point + else if (safe) + f = f.add(d); // if not, add to fake for timing safety + } + return p; + } + mulAddQUns(R, u1, u2) { + return this.mul(u1, false).add(R.mul(u2, false)).ok(); // Unsafe: do NOT use for stuff related + } // to private keys. Doesn't use Shamir trick + aff() { + const { px: x, py: y, pz: z } = this; // (x, y, z) ∋ (x=x/z, y=y/z) + if (this.equals(I)) + return { x: 0n, y: 0n }; // fast-path for zero point + if (z === 1n) + return { x, y }; // if z is 1, pass affine coordinates as-is + const iz = inv(z); // z^-1: invert z + if (mod(z * iz) !== 1n) + err('invalid inverse'); // (z * z^-1) must be 1, otherwise bad math + return { x: mod(x * iz), y: mod(y * iz) }; // x = x*z^-1; y = y*z^-1 + } + ok() { + const { x, y } = this.aff(); // convert to 2d xy affine point. + if (!fe(x) || !fe(y)) + err('Point invalid: x or y'); // x and y must be in range 0 < n < P + return mod(y * y) === crv(x) ? // y² = x³ + ax + b, must be equal + this : err('Point invalid: not on curve'); + } + multiply(n) { return this.mul(n); } // Aliases for compatibilty + negate() { return this.neg(); } + toAffine() { return this.aff(); } + assertValidity() { return this.ok(); } + static fromHex(hex) { + hex = toU8(hex); // converts hex string to Uint8Array + let p = undefined; + const head = hex[0], tail = hex.subarray(1); // first byte is prefix, rest is data + const x = slcNum(tail, 0, fLen), len = hex.length; // next 32 bytes are x coordinate + if (len === 33 && [0x02, 0x03].includes(head)) { // compressed points: 33b, start + if (!fe(x)) + err('Point hex invalid: x not FE'); // with byte 0x02 or 0x03. Check if 0 { let r = a % b; return r >= 0n ? r : b + r; }; // mod division +const inv = (num, md = P) => { + if (num === 0n || md <= 0n) + err(`no inverse n=${num} mod=${md}`); // negative exponent not supported + let a = mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n; + while (a !== 0n) { // uses euclidean gcd algorithm + const q = b / a, r = b % a; // not constant-time + const m = x - u * q, n = y - v * q; + b = a, a = r, x = u, y = v, u = m, v = n; + } + return b === 1n ? mod(x, md) : err('no inverse'); // b is gcd at this point +}; +const pow = (num, e, md = P) => { + if (md <= 0n || e < 0n) + err('pow invalid'); // exponentiation by squaring + if (md === 1n) + return 0n; // the ladder can leak exponent bits + let res = 1n; // and is vulnerable to timing attacks + for (; e > 0n; e >>= 1n) { + if (e & 1n) + res = (res * num) % md; + num = (num * num) % md; + } + return res; +}; +const sqrt = (n) => { + const r = pow(n, (P + 1n) / 4n, P); // So, a special, fast case. Paper: "Square + return mod(r * r) === n ? r : err('sqrt invalid'); // Roots from 1;24,51,10 to Dan Shanks" +}; +const padh = (num, pad) => num.toString(16).padStart(pad, '0'); +const b2h = (b) => Array.from(b).map(e => padh(e, 2)).join(''); // bytes to hex +const h2n = (hex) => (str(hex) ? BigInt(`0x${hex || '0'}`) : err()); // hex to number +const h2b = (hex) => { + const l = hex.length; // error if not string, + if (!str(hex) || l % 2) + err('hex invalid'); // or has odd length like 3, 5. + const arr = u8n(l / 2); // create result array + for (let i = 0; i < arr.length; i++) { + const j = i * 2; + const h = hex.slice(j, j + 2); // hexByte. slice is faster than substr + const b = Number.parseInt(h, 16); // byte, created from string part + if (Number.isNaN(b) || b < 0) + err('hex invalid'); // byte must be valid 0 <= byte < 256 + arr[i] = b; + } + return arr; +}; +const b2n = (b) => h2n(b2h(b)); // bytes to number +const slcNum = (b, from, to) => b2n(b.slice(from, to)); // slice bytes num +const n2b = (num) => { + return big(num) && num >= 0n && num < B256 ? h2b(padh(num, 2 * fLen)) : err('bigint expected'); +}; +const n2h = (num) => b2h(n2b(num)); // number to hex +const concatB = (...arrs) => { + const r = u8n(arrs.reduce((sum, a) => sum + a.length, 0)); // create u8a of summed length + let pad = 0; // walk through each array, ensure + arrs.forEach(a => { r.set(au8(a), pad); pad += a.length; }); // they have proper type + return r; +}; +const moreThanHalfN = (n) => n > (N >> 1n); // if a number is bigger than CURVE.n/2 +export const getPublicKey = (privKey, isCompressed = true) => { + return Point.fromPrivateKey(privKey).toRawBytes(isCompressed); // key from private +}; +export class Signature { + constructor(r, s, recovery) { + this.r = r; + this.s = s; + this.recovery = recovery; + this.ok(); + } + ok() { return ge(this.r) && ge(this.s) ? this : err(); } // 0 < r or s < CURVE.n + static fromCompact(hex) { + hex = toU8(hex, 64); // compact repr is (32b r)||(32b s) + return new Signature(slcNum(hex, 0, fLen), slcNum(hex, fLen, 2 * fLen)); + } + hasHighS() { return moreThanHalfN(this.s); } + recoverPublicKey(msgh) { + const { r, s, recovery: rec } = this; // secg.org/sec1-v2.pdf 4.1.6 + if (![0, 1, 2, 3].includes(rec)) + err('recovery id invalid'); // check recovery id + const h = bits2int_modN(toU8(msgh, 32)); // Truncate hash + const radj = rec === 2 || rec === 3 ? r + N : r; // If rec was 2 or 3, q.x is bigger than n + if (radj >= P) + err('q.x invalid'); // ensure q.x is still a field element + const prefix = (rec & 1) === 0 ? '02' : '03'; // prefix is 0x02 or 0x03 + const R = Point.fromHex(`${prefix}${n2h(radj)}`); // concat prefix + hex repr of r + const ir = inv(radj, N); // r^-1 + const u1 = mod(-h * ir, N); // -hr^-1 + const u2 = mod(s * ir, N); // sr^-1 + return G.mulAddQUns(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) + } + toCompactRawBytes() { return h2b(this.toCompactHex()); } // Uint8Array 64b compact repr + toCompactHex() { return n2h(this.r) + n2h(this.s); } // hex 64b compact repr +} +const bits2int = (bytes) => { + const delta = bytes.length * 8 - 256; // RFC suggests optional truncating via bits2octets + const num = b2n(bytes); // FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits, which + return delta > 0 ? num >> BigInt(delta) : num; // matches bits2int. bits2int can produce res>N. +}; +const bits2int_modN = (bytes) => { + return mod(bits2int(bytes), N); // with 0: BAD for trunc as per RFC vectors +}; +const i2o = (num) => n2b(num); // int to octets +const cr = () => // We support: 1) browsers 2) node.js 19+ + typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; +let _hmacSync; // Can be redefined by use in utils; built-ins don't provide it +const stdo = { lowS: true }; // opts for sign() +const vstdo = { lowS: true }; // standard opts for verify() +const prepSig = (msgh, priv, opts = stdo) => { + if (['der', 'recovered', 'canonical'].some(k => k in opts)) // Ban legacy options + err('sign() legacy options not supported'); + let { lowS } = opts; // generates low-s sigs by default + if (lowS == null) + lowS = true; // RFC6979 3.2: we skip step A, because + const h1i = bits2int_modN(toU8(msgh)); + const h1o = i2o(h1i); + const d = toPriv(priv); // validate private key, convert to bigint + const seed = [i2o(d), h1o]; // Step D of RFC6979 3.2 + let ent = opts.extraEntropy; // RFC6979 3.6: additional k' (optional) + if (ent != null) { // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') + if (ent === true) + ent = etc.randomBytes(fLen); // if true, use CSPRNG to generate data + const e = toU8(ent); // convert Hex|Bytes to Bytes + if (e.length !== fLen) + err(); // Expected 32 bytes of extra data + seed.push(e); + } + const m = h1i; // convert msg to bigint + const k2sig = (kBytes) => { + const k = bits2int(kBytes); // Transforms k into Signature + if (!ge(k)) + return; // Check 0 < k < CURVE.n + const ik = inv(k, N); // k^-1 mod n, NOT mod P + const q = G.mul(k).aff(); // q = Gk + const r = mod(q.x, N); // r = q.x mod n + if (r === 0n) + return; // r=0 invalid + const s = mod(ik * mod(m + mod(d * r, N), N), N); // s = k^-1(m + rd) mod n + if (s === 0n) + return; // s=0 invalid + let normS = s; + let rec = (q.x === r ? 0 : 2) | Number(q.y & 1n); // recovery bit + if (lowS && moreThanHalfN(s)) { // if lowS was passed, ensure s is always + normS = mod(-s, N); // in the bottom half of CURVE.n + rec ^= 1; + } + return new Signature(r, normS, rec); // use normS, not s + }; + return { seed: concatB(...seed), k2sig }; +}; +function hmacDrbg(asynchronous) { + let v = u8n(fLen); // Minimal non-full-spec HMAC-DRBG from NIST 800-90 for RFC6979 sigs. + let k = u8n(fLen); // Steps B, C of RFC6979 3.2: set hashLen, in our case always same + let i = 0; // Iterations counter, will throw when over 1000 + const reset = () => { v.fill(1); k.fill(0); i = 0; }; + const _e = 'drbg: tried 1000 values'; + if (asynchronous) { // asynchronous=true + const h = (...b) => etc.hmacSha256Async(k, v, ...b); // hmac(k)(v, ...values) + const reseed = async (seed = u8n()) => { + k = await h(u8fr([0x00]), seed); // k = hmac(K || V || 0x00 || seed) + v = await h(); // v = hmac(K || V) + if (seed.length === 0) + return; + k = await h(u8fr([0x01]), seed); // k = hmac(K || V || 0x01 || seed) + v = await h(); // v = hmac(K || V) + }; + const gen = async () => { + if (i++ >= 1000) + err(_e); + v = await h(); // v = hmac(K || V) + return v; + }; + return async (seed, pred) => { + reset(); // the returned fn, don't, it's: 1. slower (JIT). 2. unsafe (async race conditions) + await reseed(seed); // Steps D-G + let res = undefined; // Step H: grind until k is in [1..n-1] + while (!(res = pred(await gen()))) + await reseed(); + reset(); + return res; + }; + } + else { + const h = (...b) => { + const f = _hmacSync; + if (!f) + err('utils.hmacSha256Sync not set'); + return f(k, v, ...b); // hmac(k)(v, ...values) + }; + const reseed = (seed = u8n()) => { + k = h(u8fr([0x00]), seed); // k = hmac(k || v || 0x00 || seed) + v = h(); // v = hmac(k || v) + if (seed.length === 0) + return; + k = h(u8fr([0x01]), seed); // k = hmac(k || v || 0x01 || seed) + v = h(); // v = hmac(k || v) + }; + const gen = () => { + if (i++ >= 1000) + err(_e); + v = h(); // v = hmac(k || v) + return v; + }; + return (seed, pred) => { + reset(); + reseed(seed); // Steps D-G + let res = undefined; // Step H: grind until k is in [1..n-1] + while (!(res = pred(gen()))) + reseed(); + reset(); + return res; + }; + } +} +; +// ECDSA sig generation via secg.org/sec1-v2.pdf 4.1.2. hmacDrbg() +export const signAsync = async (msgh, priv, opts = stdo) => { + const { seed, k2sig } = prepSig(msgh, priv, opts); + const genUntil = hmacDrbg(true); + return genUntil(seed, k2sig); +}; +export const sign = (msgh, priv, opts = stdo) => { + const { seed, k2sig } = prepSig(msgh, priv, opts); + const genUntil = hmacDrbg(false); + return genUntil(seed, k2sig); +}; +export const verify = (sig, msgh, pub, opts = vstdo) => { + let { lowS } = opts; // ECDSA signature verification + if (lowS == null) + lowS = true; // Default lowS=true + if ('strict' in opts) + err('verify() legacy options not supported'); // legacy param + let sig_, h, P; // secg.org/sec1-v2.pdf 4.1.4 + const rs = sig && typeof sig === 'object' && 'r' in sig; // Previous ver supported DER sigs. We + if (!rs && (toU8(sig).length !== 2 * fLen)) // throw error when DER is suspected now. + err('signature must be 64 bytes'); + try { + sig_ = rs ? new Signature(sig.r, sig.s).ok() : Signature.fromCompact(sig); + h = bits2int_modN(toU8(msgh, fLen)); // Truncate hash + P = pub instanceof Point ? pub.ok() : Point.fromHex(pub); // Validate public key + } + catch (e) { + return false; + } // Check sig for validity in both cases + if (!sig_) + return false; + const { r, s } = sig_; + if (lowS && moreThanHalfN(s)) + return false; // lowS bans sig.s >= CURVE.n/2 + let R; + try { + const is = inv(s, N); // s^-1 + const u1 = mod(h * is, N); // u1 = hs^-1 mod n + const u2 = mod(r * is, N); // u2 = rs^-1 mod n + R = G.mulAddQUns(P, u1, u2).aff(); // R = u1⋅G + u2⋅P + } + catch (error) { + return false; + } + if (!R) + return false; // stop if R is identity / zero point + const v = mod(R.x, N); + return v === r; // mod(R.x, n) == r +}; +export const getSharedSecret = (privA, pubB, isCompressed = true) => { + return Point.fromHex(pubB).mul(toPriv(privA)).toRawBytes(isCompressed); // ECDH +}; +const hashToPrivateKey = (hash) => { + hash = toU8(hash); // produces private keys with modulo bias + const minLen = fLen + 8; // being neglible. + if (hash.length < minLen || hash.length > 1024) + err('expected proper params'); + const num = mod(b2n(hash), N - 1n) + 1n; // takes at least n+8 bytes + return n2b(num); +}; +export const etc = { + hexToBytes: h2b, bytesToHex: b2h, + concatBytes: concatB, bytesToNumberBE: b2n, numberToBytesBE: n2b, + mod, invert: inv, + hmacSha256Async: async (key, ...msgs) => { + const m = concatB(...msgs); // HMAC-SHA256 async. No sync built-in! + const crypto = cr(); + if (!crypto) + return err('utils.hmacSha256Async not set'); + const s = crypto.subtle; + const k = await s.importKey('raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); + return u8n(await s.sign('HMAC', k, m)); + }, + hmacSha256Sync: _hmacSync, + hashToPrivateKey, + randomBytes: (len) => { + const crypto = cr(); // Can be shimmed in node.js <= 18 to prevent error: + // import { webcrypto } from 'node:crypto'; + // if (!globalThis.crypto) globalThis.crypto = webcrypto; + if (!crypto) + err('crypto.getRandomValues must be defined'); + return crypto.getRandomValues(u8n(len)); + }, +}; +export const utils = { + normPrivateKeyToScalar: toPriv, + randomPrivateKey: () => hashToPrivateKey(etc.randomBytes(fLen + 8)), + isValidPrivateKey: (key) => { try { + return !!toPriv(key); + } + catch (e) { + return false; + } }, + precompute(p, windowSize = 8) { return p; } // no-op +}; +Object.defineProperties(etc, { hmacSha256Sync: { + configurable: false, get() { return _hmacSync; }, set(f) { if (!_hmacSync) + _hmacSync = f; }, + } }); +const W = 8; // Precomputes-related code. W = window size +const precompute = () => { + const points = []; // 10x sign(), 2x verify(). To achieve this, + const windows = 256 / W + 1; // app needs to spend 40ms+ to calculate + let p = G, b = p; // a lot of points related to base point G. + for (let w = 0; w < windows; w++) { // Points are stored in array and used + b = p; // any time Gx multiplication is done. + points.push(b); // They consume 16-32 MiB of RAM. + for (let i = 1; i < 2 ** (W - 1); i++) { + b = b.add(p); + points.push(b); + } + p = b.dbl(); // Precomputes don't speed-up getSharedKey, + } // which multiplies user point by scalar, + return points; // when precomputes are using base point +}; +const wNAF = (n) => { + // Compared to other point mult methods, + const comp = Gpows || (Gpows = precompute()); // stores 2x less points using subtraction + const neg = (cnd, p) => { let n = p.neg(); return cnd ? n : p; }; // negate + let p = I, f = G; // f must be G, or could become I in the end + const windows = 1 + 256 / W; // W=8 17 windows + const wsize = 2 ** (W - 1); // W=8 128 window size + const mask = BigInt(2 ** W - 1); // W=8 will create mask 0b11111111 + const maxNum = 2 ** W; // W=8 256 + const shiftBy = BigInt(W); // W=8 8 + for (let w = 0; w < windows; w++) { + const off = w * wsize; + let wbits = Number(n & mask); // extract W bits. + n >>= shiftBy; // shift number by W bits. + if (wbits > wsize) { + wbits -= maxNum; + n += 1n; + } // split if bits > max: +224 => 256-32 + const off1 = off, off2 = off + Math.abs(wbits) - 1; // offsets, evaluate both + const cnd1 = w % 2 !== 0, cnd2 = wbits < 0; // conditions, evaluate both + if (wbits === 0) { + f = f.add(neg(cnd1, comp[off1])); // bits are 0: add garbage to fake point + } + else { // ^ can't add off2, off2 = I + p = p.add(neg(cnd2, comp[off2])); // bits are 1: add to result point + } + } + return { p, f }; // return both real and fake points for JIT +}; // !! you can disable precomputes by commenting-out call of the wNAF() inside Point#mul() +export const ProjectivePoint = Point; diff --git a/index.ts b/index.ts index 0b37042..295c437 100644 --- a/index.ts +++ b/index.ts @@ -1,1697 +1,442 @@ /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ -// https://www.secg.org/sec2-v2.pdf - -// Uses built-in crypto module from node.js to generate randomness / hmac-sha256. -// In browser the line is automatically removed during build time: uses crypto.subtle instead. -import * as nodeCrypto from 'crypto'; - -// Be friendly to bad ECMAScript parsers by not using bigint literals like 123n -const _0n = BigInt(0); -const _1n = BigInt(1); -const _2n = BigInt(2); -const _3n = BigInt(3); -const _8n = BigInt(8); - -// Curve fomula is y² = x³ + ax + b -const CURVE = Object.freeze({ - // Params: a, b - a: _0n, - b: BigInt(7), - // Field over which we'll do calculations. Verify with: - // console.log(CURVE.P === (2n**256n - 2n**32n - 2n**9n - 2n**8n-2n**7n-2n**6n-2n**4n - 1n)) - P: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), - // Curve order, total count of valid points in the field. Verify with: - // console.log(CURVE.n === (2n**256n - 432420386565659656852420866394968145599n)) - n: BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), - // Cofactor. It's 1, so other subgroups don't exist, and default subgroup is prime-order - h: _1n, - // Base point (x, y) aka generator point - Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'), - Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'), - - // Legacy, endo params are defined below - beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), -}); - -const divNearest = (a: bigint, b: bigint) => (a + b / _2n) / b; -// Endomorphism params -const endo = { - beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), - // Split 256-bit K into 2 128-bit (k1, k2) for which k1 + k2 * lambda = K. - // Used for endomorphism https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 - splitScalar(k: bigint) { - const { n } = CURVE; - const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15'); - const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'); - const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'); - const b2 = a1; - const POW_2_128 = BigInt('0x100000000000000000000000000000000'); - - const c1 = divNearest(b2 * k, n); - const c2 = divNearest(-b1 * k, n); - let k1 = mod(k - c1 * a1 - c2 * a2, n); - let k2 = mod(-c1 * b1 - c2 * b2, n); - const k1neg = k1 > POW_2_128; - const k2neg = k2 > POW_2_128; - if (k1neg) k1 = n - k1; - if (k2neg) k2 = n - k2; - if (k1 > POW_2_128 || k2 > POW_2_128) { - throw new Error('splitScalarEndo: Endomorphism failed, k=' + k); - } - return { k1neg, k1, k2neg, k2 }; - }, +const B256 = 2n ** 256n; // secp256k1 is short weierstrass curve +const P = B256 - 0x1000003d1n; // curve's field prime +const N = B256 - 0x14551231950b75fc4402da1732fc9bebfn; // curve (group) order +const _a = 0n; // a equation's param +const _b = 7n; // b equation's param +const Gx = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n; // base point x +const Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n; // base point y +export const CURVE = { P, n: N, a: _a, b: _b, Gx, Gy }; // exported variables +const fLen = 32; // field / group byte length +type Bytes = Uint8Array; type Hex = Bytes | string; type PrivKey = Hex | bigint; +const crv = (x: bigint) => mod(mod(x * mod(x*x)) + _a * x + _b); // x³ + ax + b weierstrass formula +const err = (m = ''): never => { throw new Error(m); }; // error helper, messes-up stack trace +const big = (n: any): n is bigint => typeof n === 'bigint'; // is big integer +const str = (s: any): s is string => typeof s === 'string'; // is string +const fe = (n: bigint) => big(n) && 0n < n && n < P; // is field element (invertible) +const ge = (n: bigint) => big(n) && 0n < n && n < N; // is group element +const au8 = (a: any, l?: number): Bytes => // is Uint8Array (of specific length) + !(a instanceof Uint8Array) || (typeof l === 'number' && l > 0 && a.length !== l) ? + err('Uint8Array expected') : a; +const u8n = (data?: any) => new Uint8Array(data); // creates Uint8Array +const u8fr = (arr: any) => Uint8Array.from(arr); // another shortcut +const toU8 = (a: any, len?: number) => au8(str(a) ? h2b(a) : a, len); // normalize (hex/u8a) to u8a +const toPriv = (p: PrivKey): bigint => { // normalize private key to bigint + if (!big(p)) p = b2n(toU8(p, fLen)); // convert to bigint when bytes + return ge(p) ? p : err('private key out of range'); // check if bigint is in range }; - -// Placeholder for non-sha256 hashes -const fieldLen = 32; // Field element: their range is 0 to CURVE.P -const groupLen = 32; // Group element: their range is 1 to CURVE.n -const hashLen = 32; // Hash used with secp256k1, sha2-256 -const compressedLen = fieldLen + 1; // DER-encoded field element -const uncompressedLen = 2 * fieldLen + 1; // DER-encoded pair of field elements - -// Cleaner js output if that's on a separate line. -export { CURVE }; - -/** - * y² = x³ + ax + b: Short weierstrass curve formula - * @returns y² - */ -function weierstrass(x: bigint): bigint { - const { a, b } = CURVE; - const x2 = mod(x * x); - const x3 = mod(x2 * x); - return mod(x3 + a * x + b); -} - -// We accept hex strings besides Uint8Array for simplicity -type Hex = Uint8Array | string; -// Very few implementations accept numbers, we do it to ease learning curve -type PrivKey = Hex | bigint | number; -// compressed/uncompressed ECDSA key, or Schnorr key - not interchangeable -type PubKey = Hex | Point; -// ECDSA signature -type Sig = Hex | Signature; - -/** - * Always true for secp256k1. - * We're including it here if you'll want to reuse code to support - * different curve (e.g. secp256r1) - just set it to false then. - * Endomorphism only works for Koblitz curves with a == 0. - * It improves efficiency: - * Uses 2x less RAM, speeds up precomputation by 2x and ECDH / sign key recovery by 20%. - * Should always be used for Jacobian's double-and-add multiplication. - * For affines cached multiplication, it trades off 1/2 init time & 1/3 ram for 20% perf hit. - * https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066 - */ -const USE_ENDOMORPHISM = CURVE.a === _0n; - -class ShaError extends Error { - constructor(message: string) { - super(message); - } -} - -function assertJacPoint(other: unknown) { - if (!(other instanceof JacobianPoint)) throw new TypeError('JacobianPoint expected'); -} - -/** - * Jacobian Point works in 3d / jacobi coordinates: (x, y, z) ∋ (x=x/z², y=y/z³) - * Default Point works in 2d / affine coordinates: (x, y) - * We're doing calculations in jacobi, because its operations don't require costly inversion. - */ -class JacobianPoint { - constructor(readonly x: bigint, readonly y: bigint, readonly z: bigint) {} - - static readonly BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n); - static readonly ZERO = new JacobianPoint(_0n, _1n, _0n); - static fromAffine(p: Point): JacobianPoint { - if (!(p instanceof Point)) { - throw new TypeError('JacobianPoint#fromAffine: expected Point'); - } - // fromAffine(x:0, y:0) would produce (x:0, y:0, z:1), but we need (x:0, y:1, z:0) - if (p.equals(Point.ZERO)) return JacobianPoint.ZERO; - return new JacobianPoint(p.x, p.y, _1n); - } - - /** - * Takes a bunch of Jacobian Points but executes only one - * invert on all of them. invert is very slow operation, - * so this improves performance massively. - */ - static toAffineBatch(points: JacobianPoint[]): Point[] { - const toInv = invertBatch(points.map((p) => p.z)); - return points.map((p, i) => p.toAffine(toInv[i])); - } - - static normalizeZ(points: JacobianPoint[]): JacobianPoint[] { - return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine); - } - - /** - * Compare one point to another. - */ - equals(other: JacobianPoint): boolean { - assertJacPoint(other); - const { x: X1, y: Y1, z: Z1 } = this; - const { x: X2, y: Y2, z: Z2 } = other; - const Z1Z1 = mod(Z1 * Z1); - const Z2Z2 = mod(Z2 * Z2); - const U1 = mod(X1 * Z2Z2); - const U2 = mod(X2 * Z1Z1); - const S1 = mod(mod(Y1 * Z2) * Z2Z2); - const S2 = mod(mod(Y2 * Z1) * Z1Z1); - return U1 === U2 && S1 === S2; - } - - /** - * Flips point to one corresponding to (x, -y) in Affine coordinates. - */ - negate(): JacobianPoint { - return new JacobianPoint(this.x, mod(-this.y), this.z); - } - - // Fast algo for doubling 2 Jacobian Points when curve's a=0. - // Note: cannot be reused for other curves when a != 0. - // From: https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l - // Cost: 2M + 5S + 6add + 3*2 + 1*3 + 1*8. - double(): JacobianPoint { - const { x: X1, y: Y1, z: Z1 } = this; - const A = mod(X1 * X1); - const B = mod(Y1 * Y1); - const C = mod(B * B); - const x1b = X1 + B; - const D = mod(_2n * (mod(x1b * x1b) - A - C)); - const E = mod(_3n * A); - const F = mod(E * E); - const X3 = mod(F - _2n * D); - const Y3 = mod(E * (D - X3) - _8n * C); - const Z3 = mod(_2n * Y1 * Z1); - return new JacobianPoint(X3, Y3, Z3); - } - - // Fast algo for adding 2 Jacobian Points. - // https://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 - // Cost: 12M + 4S + 6add + 1*2 - // Note: 2007 Bernstein-Lange (11M + 5S + 9add + 4*2) is actually 10% slower. - add(other: JacobianPoint): JacobianPoint { - assertJacPoint(other); - const { x: X1, y: Y1, z: Z1 } = this; - const { x: X2, y: Y2, z: Z2 } = other; - if (X2 === _0n || Y2 === _0n) return this; - if (X1 === _0n || Y1 === _0n) return other; - // We're using same code in equals() - const Z1Z1 = mod(Z1 * Z1); - const Z2Z2 = mod(Z2 * Z2); - const U1 = mod(X1 * Z2Z2); - const U2 = mod(X2 * Z1Z1); - const S1 = mod(mod(Y1 * Z2) * Z2Z2); - const S2 = mod(mod(Y2 * Z1) * Z1Z1); - const H = mod(U2 - U1); - const r = mod(S2 - S1); - // H = 0 meaning it's the same point. - if (H === _0n) { - if (r === _0n) { - return this.double(); - } else { - return JacobianPoint.ZERO; - } - } - const HH = mod(H * H); - const HHH = mod(H * HH); - const V = mod(U1 * HH); - const X3 = mod(r * r - HHH - _2n * V); - const Y3 = mod(r * (V - X3) - S1 * HHH); - const Z3 = mod(Z1 * Z2 * H); - return new JacobianPoint(X3, Y3, Z3); - } - - subtract(other: JacobianPoint) { - return this.add(other.negate()); - } - - /** - * Non-constant-time multiplication. Uses double-and-add algorithm. - * It's faster, but should only be used when you don't care about - * an exposed private key e.g. sig verification, which works over *public* keys. - */ - multiplyUnsafe(scalar: bigint): JacobianPoint { - const P0 = JacobianPoint.ZERO; - if (typeof scalar === 'bigint' && scalar === _0n) return P0; - // Will throw on 0 - let n = normalizeScalar(scalar); - if (n === _1n) return this; - - // The condition is not executed unless you change global var - if (!USE_ENDOMORPHISM) { - let p = P0; - let d: JacobianPoint = this; - while (n > _0n) { - if (n & _1n) p = p.add(d); - d = d.double(); - n >>= _1n; - } - return p; - } - let { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); - let k1p = P0; - let k2p = P0; - let d: JacobianPoint = this; - while (k1 > _0n || k2 > _0n) { - if (k1 & _1n) k1p = k1p.add(d); - if (k2 & _1n) k2p = k2p.add(d); - d = d.double(); - k1 >>= _1n; - k2 >>= _1n; - } - if (k1neg) k1p = k1p.negate(); - if (k2neg) k2p = k2p.negate(); - k2p = new JacobianPoint(mod(k2p.x * endo.beta), k2p.y, k2p.z); - return k1p.add(k2p); - } - - /** - * Creates a wNAF precomputation window. Used for caching. - * Default window size is set by `utils.precompute()` and is equal to 8. - * Which means we are caching 65536 points: 256 points for every bit from 0 to 256. - * @returns 65K precomputed points, depending on W - */ - private precomputeWindow(W: number): JacobianPoint[] { - // splitScalarEndo could return 129-bit numbers, so we need at least 128 / W + 1 - const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1; - const points: JacobianPoint[] = []; - let p: JacobianPoint = this; - let base = p; - for (let window = 0; window < windows; window++) { - base = p; - points.push(base); - for (let i = 1; i < 2 ** (W - 1); i++) { - base = base.add(p); - points.push(base); - } - p = base.double(); - } - return points; - } - - /** - * Implements w-ary non-adjacent form for calculating ec multiplication. - * @param n - * @param affinePoint optional 2d point to save cached precompute windows on it. - * @returns real and fake (for const-time) points - */ - private wNAF(n: bigint, affinePoint?: Point): { p: JacobianPoint; f: JacobianPoint } { - if (!affinePoint && this.equals(JacobianPoint.BASE)) affinePoint = Point.BASE; - const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; - if (256 % W) { - throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2'); - } - - // Calculate precomputes on a first run, reuse them after - let precomputes = affinePoint && pointPrecomputes.get(affinePoint); - if (!precomputes) { - precomputes = this.precomputeWindow(W); - if (affinePoint && W !== 1) { - precomputes = JacobianPoint.normalizeZ(precomputes); - pointPrecomputes.set(affinePoint, precomputes); - } - } - - // Initialize real and fake points for const-time - let p = JacobianPoint.ZERO; - // Should be G (base) point, since otherwise f can be infinity point in the end - let f = JacobianPoint.BASE; - - const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W); // W=8 17 - const windowSize = 2 ** (W - 1); // W=8 128 - const mask = BigInt(2 ** W - 1); // Create mask with W ones: 0b11111111 for W=8 - const maxNumber = 2 ** W; // W=8 256 - const shiftBy = BigInt(W); // W=8 8 - - for (let window = 0; window < windows; window++) { - const offset = window * windowSize; - // Extract W bits. - let wbits = Number(n & mask); - - // Shift number by W bits. - n >>= shiftBy; - - // If the bits are bigger than max size, we'll split those. - // +224 => 256 - 32 - if (wbits > windowSize) { - wbits -= maxNumber; - n += _1n; - } - - // This code was first written with assumption that 'f' and 'p' will never be infinity point: - // since each addition is multiplied by 2 ** W, it cannot cancel each other. However, - // there is negate now: it is possible that negated element from low value - // would be the same as high element, which will create carry into next window. - // It's not obvious how this can fail, but still worth investigating later. - - // Check if we're onto Zero point. - // Add random point inside current window to f. - const offset1 = offset; - const offset2 = offset + Math.abs(wbits) - 1; - const cond1 = window % 2 !== 0; - const cond2 = wbits < 0; - if (wbits === 0) { - // The most important part for const-time getPublicKey - f = f.add(constTimeNegate(cond1, precomputes[offset1])); - } else { - p = p.add(constTimeNegate(cond2, precomputes[offset2])); - } - } - // JIT-compiler should not eliminate f here, since it will later be used in normalizeZ() - // Even if the variable is still unused, there are some checks which will - // throw an exception, so compiler needs to prove they won't happen, which is hard. - // At this point there is a way to F be infinity-point even if p is not, - // which makes it less const-time: around 1 bigint multiply. - return { p, f }; - } - - /** - * Constant time multiplication. - * Uses wNAF method. Windowed method may be 10% faster, - * but takes 2x longer to generate and consumes 2x memory. - * @param scalar by which the point would be multiplied - * @param affinePoint optional point ot save cached precompute windows on it - * @returns New point - */ - multiply(scalar: number | bigint, affinePoint?: Point): JacobianPoint { - let n = normalizeScalar(scalar); - // Real point. - let point: JacobianPoint; - // Fake point, we use it to achieve constant-time multiplication. - let fake: JacobianPoint; - if (USE_ENDOMORPHISM) { - const { k1neg, k1, k2neg, k2 } = endo.splitScalar(n); - let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint); - let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint); - k1p = constTimeNegate(k1neg, k1p); - k2p = constTimeNegate(k2neg, k2p); - k2p = new JacobianPoint(mod(k2p.x * endo.beta), k2p.y, k2p.z); - point = k1p.add(k2p); - fake = f1p.add(f2p); - } else { - const { p, f } = this.wNAF(n, affinePoint); - point = p; - fake = f; +let Gpows: Point[] | undefined = undefined; // precomputes for base point G +interface AffinePoint { x: bigint, y: bigint } // Point in 2d xy affine coords +const isPoint = (p: any) => (p instanceof Point ? p : err('Point expected')); // is 3d point +class Point { // Point in 3d xyz projective coords + constructor(readonly px: bigint, readonly py: bigint, readonly pz: bigint) {} // z is optional + static readonly BASE = new Point(Gx, Gy, 1n); // generator / base point. + static readonly ZERO = new Point(0n, 1n, 0n); // identity / zero point + get x() { return this.aff().x; } // .x, .y will call expensive toAffine. + get y() { return this.aff().y; } // Should be used with care. + equals(other: Point): boolean { // equality check: compare points + const { px: X1, py: Y1, pz: Z1 } = this; + const { px: X2, py: Y2, pz: Z2 } = isPoint(other); // isPoint() checks class equality + const X1Z2 = mod(X1 * Z2), X2Z1 = mod(X2 * Z1); + const Y1Z2 = mod(Y1 * Z2), Y2Z1 = mod(Y2 * Z1); + return X1Z2 === X2Z1 && Y1Z2 === Y2Z1; + } + neg() { return new Point(this.px, mod(-this.py), this.pz); } // negate, flips point over y coord + dbl() { return this.add(this); } // point doubling + add(other: Point) { // point addition: complete, exception-free + const { px: X1, py: Y1, pz: Z1 } = this; // formula from Renes-Costello-Batina + const { px: X2, py: Y2, pz: Z2 } = isPoint(other); // https://eprint.iacr.org/2015/1060, algo 1 + const { a, b } = CURVE; + let X3 = 0n, Y3 = 0n, Z3 = 0n; // Cost: 12M + 0S + 3*a + 3*b3 + 23add + const b3 = mod(b * 3n); + let t0 = mod(X1 * X2), t1 = mod(Y1 * Y2), t2 = mod(Z1 * Z2), t3 = mod(X1 + Y1); // step 1 + let t4 = mod(X2 + Y2); // step 5 + t3 = mod(t3 * t4); t4 = mod(t0 + t1); t3 = mod(t3 - t4); t4 = mod(X1 + Z1); + let t5 = mod(X2 + Z2); // step 10 + t4 = mod(t4 * t5); t5 = mod(t0 + t2); t4 = mod(t4 - t5); t5 = mod(Y1 + Z1); + X3 = mod(Y2 + Z2); // step 15 + t5 = mod(t5 * X3); X3 = mod(t1 + t2); t5 = mod(t5 - X3); Z3 = mod(a * t4); + X3 = mod(b3 * t2); // step 20 + Z3 = mod(X3 + Z3); X3 = mod(t1 - Z3); Z3 = mod(t1 + Z3); Y3 = mod(X3 * Z3); + t1 = mod(t0 + t0); // step 25 + t1 = mod(t1 + t0); t2 = mod(a * t2); t4 = mod(b3 * t4); t1 = mod(t1 + t2); + t2 = mod(t0 - t2); // step 30 + t2 = mod(a * t2); t4 = mod(t4 + t2); t0 = mod(t1 * t4); Y3 = mod(Y3 + t0); + t0 = mod(t5 * t4); // step 35 + X3 = mod(t3 * X3); X3 = mod(X3 - t0); t0 = mod(t3 * t1); Z3 = mod(t5 * Z3); + Z3 = mod(Z3 + t0); // step 40 + return new Point(X3, Y3, Z3); + } + mul(n: bigint, safe = true) { // Multiply point by scalar n; + if (!safe && n === 0n) return I; // in unsafe mode, allow zero + if (!ge(n)) err('invalid scalar'); // must be 0 < n < CURVE.n + if (this.equals(G)) return wNAF(n).p; // Use precomputes for base point + let p = I, f = G; // init result point & fake point + for (let d: Point = this; n > 0n; d = d.dbl(), n >>= 1n) { // double-and-add ladder + if (n & 1n) p = p.add(d); // if bit is present, add to point + else if (safe) f = f.add(d); // if not, add to fake for timing safety } - // Normalize `z` for both points, but return only real one - return JacobianPoint.normalizeZ([point, fake])[0]; - } - - // Converts Jacobian point to affine (x, y) coordinates. - // Can accept precomputed Z^-1 - for example, from invertBatch. - // (x, y, z) ∋ (x=x/z², y=y/z³) - toAffine(invZ?: bigint): Point { - const { x, y, z } = this; - const is0 = this.equals(JacobianPoint.ZERO); - // If invZ was 0, we return zero point. However we still want to execute - // all operations, so we replace invZ with a random number, 8. - if (invZ == null) invZ = is0 ? _8n : invert(z); - const iz1 = invZ; - const iz2 = mod(iz1 * iz1); - const iz3 = mod(iz2 * iz1); - const ax = mod(x * iz2); - const ay = mod(y * iz3); - const zz = mod(z * iz1); - if (is0) return Point.ZERO; - if (zz !== _1n) throw new Error('invZ was invalid'); - return new Point(ax, ay); - } -} - -// Const-time utility for wNAF -function constTimeNegate(condition: boolean, item: JacobianPoint) { - const neg = item.negate(); - return condition ? neg : item; -} - -// Stores precomputed values for points. -const pointPrecomputes = new WeakMap(); - -/** - * Default Point works in default aka affine coordinates: (x, y) - */ -export class Point { - /** - * Base point aka generator. public_key = Point.BASE * private_key - */ - static BASE: Point = new Point(CURVE.Gx, CURVE.Gy); - /** - * Identity point aka point at infinity. point = point + zero_point - */ - static ZERO: Point = new Point(_0n, _0n); - // We calculate precomputes for elliptic curve point multiplication - // using windowed method. This specifies window size and - // stores precomputed values. Usually only base point would be precomputed. - _WINDOW_SIZE?: number; - - constructor(readonly x: bigint, readonly y: bigint) {} - - // "Private method", don't use it directly - _setWindowSize(windowSize: number) { - this._WINDOW_SIZE = windowSize; - pointPrecomputes.delete(this); - } - - // Checks for y % 2 == 0 - hasEvenY() { - return this.y % _2n === _0n; - } - - /** - * Supports compressed Schnorr and ECDSA points - * @param bytes - * @returns Point instance - */ - private static fromCompressedHex(bytes: Uint8Array) { - const isShort = bytes.length === 32; - const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)); - if (!isValidFieldElement(x)) throw new Error('Point is not on curve'); - const y2 = weierstrass(x); // y² = x³ + ax + b - let y = sqrtMod(y2); // y = y² ^ (p+1)/4 - const isYOdd = (y & _1n) === _1n; - if (isShort) { - // Schnorr - if (isYOdd) y = mod(-y); - } else { - // ECDSA - const isFirstByteOdd = (bytes[0] & 1) === 1; - if (isFirstByteOdd !== isYOdd) y = mod(-y); - } - const point = new Point(x, y); - point.assertValidity(); - return point; - } - - // Schnorr doesn't support uncompressed points, so this is only for ECDSA - private static fromUncompressedHex(bytes: Uint8Array) { - const x = bytesToNumber(bytes.subarray(1, fieldLen + 1)); - const y = bytesToNumber(bytes.subarray(fieldLen + 1, fieldLen * 2 + 1)); - const point = new Point(x, y); - point.assertValidity(); - return point; - } - - /** - * Converts hash string or Uint8Array to Point. - * @param hex schnorr or ECDSA hex - */ - static fromHex(hex: Hex): Point { - const bytes = ensureBytes(hex); - const len = bytes.length; - const header = bytes[0]; - // this.assertValidity() is done inside of those two functions - // Schnorr - if (len === fieldLen) return this.fromCompressedHex(bytes); - // ECDSA - if (len === compressedLen && (header === 0x02 || header === 0x03)) { - return this.fromCompressedHex(bytes); - } - if (len === uncompressedLen && header === 0x04) return this.fromUncompressedHex(bytes); - throw new Error( - `Point.fromHex: received invalid point. Expected 32-${compressedLen} compressed bytes or ${uncompressedLen} uncompressed bytes, not ${len}` - ); - } - - // Multiplies generator point by privateKey. - static fromPrivateKey(privateKey: PrivKey) { - return Point.BASE.multiply(normalizePrivateKey(privateKey)); - } - - /** - * Recovers public key from ECDSA signature. - * https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm#Public_key_recovery - * ``` - * recover(r, s, h) where - * u1 = hs^-1 mod n - * u2 = sr^-1 mod n - * Q = u1⋅G + u2⋅R - * ``` - */ - static fromSignature(msgHash: Hex, signature: Sig, recovery: number): Point { - const { r, s } = normalizeSignature(signature); - if (![0, 1, 2, 3].includes(recovery)) throw new Error('Cannot recover: invalid recovery bit'); - const h = truncateHash(ensureBytes(msgHash)); - const { n } = CURVE; - const radj = recovery === 2 || recovery === 3 ? r + n : r; - const rinv = invert(radj, n); - // Q = u1⋅G + u2⋅R - const u1 = mod(-h * rinv, n); - const u2 = mod(s * rinv, n); - const prefix = recovery & 1 ? '03' : '02'; - const R = Point.fromHex(prefix + numTo32bStr(radj)); - const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); - if (!Q) throw new Error('Cannot recover signature: point at infinify'); - Q.assertValidity(); - return Q; - } - - toRawBytes(isCompressed = false): Uint8Array { - return hexToBytes(this.toHex(isCompressed)); - } - - toHex(isCompressed = false): string { - const x = numTo32bStr(this.x); - if (isCompressed) { - const prefix = this.hasEvenY() ? '02' : '03'; - return `${prefix}${x}`; - } else { - return `04${x}${numTo32bStr(this.y)}`; - } - } - - // Schnorr-related function - toHexX() { - return this.toHex(true).slice(2); - } - - toRawX() { - return this.toRawBytes(true).slice(1); - } - - // A point on curve is valid if it conforms to equation. - assertValidity(): void { - const msg = 'Point is not on elliptic curve'; - const { x, y } = this; - if (!isValidFieldElement(x) || !isValidFieldElement(y)) throw new Error(msg); - const left = mod(y * y); - const right = weierstrass(x); - if (mod(left - right) !== _0n) throw new Error(msg); - } - - equals(other: Point): boolean { - return this.x === other.x && this.y === other.y; - } - - // Returns the same point with inverted `y` - negate() { - return new Point(this.x, mod(-this.y)); - } - - // Adds point to itself - double() { - return JacobianPoint.fromAffine(this).double().toAffine(); - } - - // Adds point to other point - add(other: Point) { - return JacobianPoint.fromAffine(this).add(JacobianPoint.fromAffine(other)).toAffine(); - } - - // Subtracts other point from the point - subtract(other: Point) { - return this.add(other.negate()); - } - - multiply(scalar: number | bigint) { - return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine(); - } - - /** - * Efficiently calculate `aP + bQ`. - * Unsafe, can expose private key, if used incorrectly. - * TODO: Utilize Shamir's trick - * @returns non-zero affine point - */ - multiplyAndAddUnsafe(Q: Point, a: bigint, b: bigint): Point | undefined { - const P = JacobianPoint.fromAffine(this); - const aP = a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a); - const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b); - const sum = aP.add(bQ); - return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine(); - } -} - -function sliceDER(s: string): string { - // Proof: any([(i>=0x80) == (int(hex(i).replace('0x', '').zfill(2)[0], 16)>=8) for i in range(0, 256)]) - // Padding done by numberToHex - return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s; -} - -function parseDERInt(data: Uint8Array) { - if (data.length < 2 || data[0] !== 0x02) { - throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`); - } - const len = data[1]; - const res = data.subarray(2, len + 2); - if (!len || res.length !== len) { - throw new Error(`Invalid signature integer: wrong length`); - } - // Strange condition, its not about length, but about first bytes of number. - if (res[0] === 0x00 && res[1] <= 0x7f) { - throw new Error('Invalid signature integer: trailing length'); - } - return { data: bytesToNumber(res), left: data.subarray(len + 2) }; -} - -function parseDERSignature(data: Uint8Array) { - if (data.length < 2 || data[0] != 0x30) { - throw new Error(`Invalid signature tag: ${bytesToHex(data)}`); - } - if (data[1] !== data.length - 2) { - throw new Error('Invalid signature: incorrect length'); - } - const { data: r, left: sBytes } = parseDERInt(data.subarray(2)); - const { data: s, left: rBytesLeft } = parseDERInt(sBytes); - if (rBytesLeft.length) { - throw new Error(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`); - } - return { r, s }; -} - -// Represents ECDSA signature with its (r, s) properties -export class Signature { - constructor(readonly r: bigint, readonly s: bigint) { - this.assertValidity(); - } - - // pair (32 bytes of r, 32 bytes of s) - static fromCompact(hex: Hex) { - const arr = hex instanceof Uint8Array; - const name = 'Signature.fromCompact'; - if (typeof hex !== 'string' && !arr) - throw new TypeError(`${name}: Expected string or Uint8Array`); - const str = arr ? bytesToHex(hex) : hex; - if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`); - return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128))); - } - - // DER encoded ECDSA signature - // https://bitcoin.stackexchange.com/questions/57644/what-are-the-parts-of-a-bitcoin-transaction-input-script - static fromDER(hex: Hex) { - const arr = hex instanceof Uint8Array; - if (typeof hex !== 'string' && !arr) - throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`); - const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex)); - return new Signature(r, s); - } - - // Don't use this method - static fromHex(hex: Hex) { - return this.fromDER(hex); - } - - assertValidity(): void { - const { r, s } = this; - if (!isWithinCurveOrder(r)) throw new Error('Invalid Signature: r must be 0 < r < n'); - if (!isWithinCurveOrder(s)) throw new Error('Invalid Signature: s must be 0 < s < n'); - } - - // Default signatures are always low-s, to prevent malleability. - // sign(canonical: true) always produces low-s sigs. - // verify(strict: true) always fails for high-s. - // We don't provide `hasHighR` https://github.com/bitcoin/bitcoin/pull/13666 - hasHighS(): boolean { - const HALF = CURVE.n >> _1n; - return this.s > HALF; - } - - normalizeS(): Signature { - return this.hasHighS() ? new Signature(this.r, mod(-this.s, CURVE.n)) : this; - } - - // DER-encoded - toDERRawBytes() { - return hexToBytes(this.toDERHex()); - } - toDERHex() { - const sHex = sliceDER(numberToHexUnpadded(this.s)); - const rHex = sliceDER(numberToHexUnpadded(this.r)); - const sHexL = sHex.length / 2; - const rHexL = rHex.length / 2; - const sLen = numberToHexUnpadded(sHexL); - const rLen = numberToHexUnpadded(rHexL); - const length = numberToHexUnpadded(rHexL + sHexL + 4); - return `30${length}02${rLen}${rHex}02${sLen}${sHex}`; - } - - // Don't use these methods. Use toDER* or toCompact* for explicitness. - toRawBytes() { - return this.toDERRawBytes(); - } - toHex() { - return this.toDERHex(); - } - - // 32 bytes of r, then 32 bytes of s - toCompactRawBytes() { - return hexToBytes(this.toCompactHex()); - } - toCompactHex() { - return numTo32bStr(this.r) + numTo32bStr(this.s); - } -} - -// Copies several Uint8Arrays into one. -function concatBytes(...arrays: Uint8Array[]): Uint8Array { - if (!arrays.every((b) => b instanceof Uint8Array)) throw new Error('Uint8Array list expected'); - if (arrays.length === 1) return arrays[0]; - const length = arrays.reduce((a, arr) => a + arr.length, 0); - const result = new Uint8Array(length); - for (let i = 0, pad = 0; i < arrays.length; i++) { - const arr = arrays[i]; - result.set(arr, pad); - pad += arr.length; - } - return result; -} - -// Convert between types -// --------------------- - -const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')); -function bytesToHex(uint8a: Uint8Array): string { - if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array'); - // pre-caching improves the speed 6x - let hex = ''; - for (let i = 0; i < uint8a.length; i++) { - hex += hexes[uint8a[i]]; - } - return hex; -} - -const POW_2_256 = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000'); -function numTo32bStr(num: bigint): string { - if (typeof num !== 'bigint') throw new Error('Expected bigint'); - if (!(_0n <= num && num < POW_2_256)) throw new Error('Expected number 0 <= n < 2^256'); - return num.toString(16).padStart(64, '0'); -} -function numTo32b(num: bigint): Uint8Array { - const b = hexToBytes(numTo32bStr(num)); - if (b.length !== 32) throw new Error('Error: expected 32 bytes'); - return b; -} - -function numberToHexUnpadded(num: number | bigint): string { - const hex = num.toString(16); - return hex.length & 1 ? `0${hex}` : hex; -} - -function hexToNumber(hex: string): bigint { - if (typeof hex !== 'string') { - throw new TypeError('hexToNumber: expected string, got ' + typeof hex); - } - // Big Endian - return BigInt(`0x${hex}`); -} - -// Caching slows it down 2-3x -function hexToBytes(hex: string): Uint8Array { - if (typeof hex !== 'string') { - throw new TypeError('hexToBytes: expected string, got ' + typeof hex); - } - if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex' + hex.length); - const array = new Uint8Array(hex.length / 2); - for (let i = 0; i < array.length; i++) { - const j = i * 2; - const hexByte = hex.slice(j, j + 2); - const byte = Number.parseInt(hexByte, 16); - if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence'); - array[i] = byte; - } - return array; -} - -// Big Endian -function bytesToNumber(bytes: Uint8Array): bigint { - return hexToNumber(bytesToHex(bytes)); -} - -function ensureBytes(hex: Hex): Uint8Array { - // Uint8Array.from() instead of hash.slice() because node.js Buffer - // is instance of Uint8Array, and its slice() creates **mutable** copy - return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); -} - -function normalizeScalar(num: number | bigint): bigint { - if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) return BigInt(num); - if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num; - throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n'); -} - -// ------------------------- - -// Calculates a modulo b -function mod(a: bigint, b: bigint = CURVE.P): bigint { - const result = a % b; - return result >= _0n ? result : b + result; -} - -// Does x ^ (2 ^ power). E.g. 30 ^ (2 ^ 4) -function pow2(x: bigint, power: bigint): bigint { - const { P } = CURVE; - let res = x; - while (power-- > _0n) { - res *= res; - res %= P; - } - return res; -} - -/** - * Allows to compute square root √y 2x faster. - * To calculate √y, we need to exponentiate it to a very big number: - * `y² = x³ + ax + b; y = y² ^ (p+1)/4` - * We are unwrapping the loop and multiplying it bit-by-bit. - * (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00] - */ -function sqrtMod(x: bigint): bigint { - const { P } = CURVE; - const _6n = BigInt(6); - const _11n = BigInt(11); - const _22n = BigInt(22); - const _23n = BigInt(23); - const _44n = BigInt(44); - const _88n = BigInt(88); - const b2 = (x * x * x) % P; // x^3, 11 - const b3 = (b2 * b2 * x) % P; // x^7 - const b6 = (pow2(b3, _3n) * b3) % P; - const b9 = (pow2(b6, _3n) * b3) % P; - const b11 = (pow2(b9, _2n) * b2) % P; - const b22 = (pow2(b11, _11n) * b11) % P; - const b44 = (pow2(b22, _22n) * b22) % P; - const b88 = (pow2(b44, _44n) * b44) % P; - const b176 = (pow2(b88, _88n) * b88) % P; - const b220 = (pow2(b176, _44n) * b44) % P; - const b223 = (pow2(b220, _3n) * b3) % P; - const t1 = (pow2(b223, _23n) * b22) % P; - const t2 = (pow2(t1, _6n) * b2) % P; - const rt = pow2(t2, _2n); - const xc = (rt * rt) % P; - if (xc !== x) throw new Error('Cannot find square root'); - return rt; -} - -// Inverses number over modulo -function invert(number: bigint, modulo: bigint = CURVE.P): bigint { - if (number === _0n || modulo <= _0n) { - throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); - } - // Eucledian GCD https://brilliant.org/wiki/extended-euclidean-algorithm/ - let a = mod(number, modulo); - let b = modulo; - // prettier-ignore - let x = _0n, y = _1n, u = _1n, v = _0n; - while (a !== _0n) { - const q = b / a; - const r = b % a; - const m = x - u * q; - const n = y - v * q; - // prettier-ignore + return p; + } + mulAddQUns(R: Point, u1: bigint, u2: bigint) { // Q = u1⋅G + u2⋅R: double scalar mult. + return this.mul(u1, false).add(R.mul(u2, false)).ok(); // Unsafe: do NOT use for stuff related + } // to private keys. Doesn't use Shamir trick + aff(): AffinePoint { // Converts point to 2d xy affine point + const { px: x, py: y, pz: z } = this; // (x, y, z) ∋ (x=x/z, y=y/z) + if (this.equals(I)) return { x: 0n, y: 0n }; // fast-path for zero point + if (z === 1n) return { x, y }; // if z is 1, pass affine coordinates as-is + const iz = inv(z); // z^-1: invert z + if (mod(z * iz) !== 1n) err('invalid inverse'); // (z * z^-1) must be 1, otherwise bad math + return { x: mod(x * iz), y: mod(y * iz) }; // x = x*z^-1; y = y*z^-1 + } + ok(): Point { // Checks if the point is valid and on-curve + const { x, y } = this.aff(); // convert to 2d xy affine point. + if (!fe(x) || !fe(y)) err('Point invalid: x or y'); // x and y must be in range 0 < n < P + return mod(y * y) === crv(x) ? // y² = x³ + ax + b, must be equal + this : err('Point invalid: not on curve'); + } + multiply(n: bigint) { return this.mul(n); } // Aliases for compatibilty + negate() { return this.neg(); } + toAffine() { return this.aff(); } + assertValidity() { return this.ok(); } + static fromHex(hex: Hex): Point { // Convert Uint8Array or hex string to Point + hex = toU8(hex); // converts hex string to Uint8Array + let p: Point | undefined = undefined; + const head = hex[0], tail = hex.subarray(1); // first byte is prefix, rest is data + const x = slcNum(tail, 0, fLen), len = hex.length; // next 32 bytes are x coordinate + if (len === 33 && [0x02, 0x03].includes(head)) { // compressed points: 33b, start + if (!fe(x)) err('Point hex invalid: x not FE'); // with byte 0x02 or 0x03. Check if 0 { let r = a % b; return r >= 0n ? r : b + r; }; // mod division +const inv = (num: bigint, md = P): bigint => { // modular inversion + if (num === 0n || md <= 0n) err(`no inverse n=${num} mod=${md}`); // negative exponent not supported + let a = mod(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n; + while (a !== 0n) { // uses euclidean gcd algorithm + const q = b / a, r = b % a; // not constant-time + const m = x - u * q, n = y - v * q; b = a, a = r, x = u, y = v, u = m, v = n; } - const gcd = b; - if (gcd !== _1n) throw new Error('invert: does not exist'); - return mod(x, modulo); -} - -/** - * Takes a list of numbers, efficiently inverts all of them. - * @param nums list of bigints - * @param p modulo - * @returns list of inverted bigints - * @example - * invertBatch([1n, 2n, 4n], 21n); - * // => [1n, 11n, 16n] - */ -function invertBatch(nums: bigint[], p: bigint = CURVE.P): bigint[] { - const scratch = new Array(nums.length); - // Walk from first to last, multiply them by each other MOD p - const lastMultiplied = nums.reduce((acc, num, i) => { - if (num === _0n) return acc; - scratch[i] = acc; - return mod(acc * num, p); - }, _1n); - // Invert last element - const inverted = invert(lastMultiplied, p); - // Walk from last to first, multiply them by inverted each other MOD p - nums.reduceRight((acc, num, i) => { - if (num === _0n) return acc; - scratch[i] = mod(acc * scratch[i], p); - return mod(acc * num, p); - }, inverted); - return scratch; -} - -// Can be replaced by bytesToNumber(). Placeholder for non-sha256 hashes -function bits2int_2(bytes: Uint8Array) { - const delta = bytes.length * 8 - groupLen * 8; // 256-256=0 for sha256/secp256k1 - const num = bytesToNumber(bytes); - return delta > 0 ? num >> BigInt(delta) : num; -} - -// Ensures ECDSA message hashes are 32 bytes and < curve order -function truncateHash(hash: Uint8Array, truncateOnly = false): bigint { - const h = bits2int_2(hash); - if (truncateOnly) return h; - const { n } = CURVE; - return h >= n ? h - n : h; -} - -// RFC6979 related code -type RecoveredSig = { sig: Signature; recovery: number }; -type U8A = Uint8Array; - -type Sha256FnSync = undefined | ((...messages: Uint8Array[]) => Uint8Array); -type HmacFnSync = undefined | ((key: Uint8Array, ...messages: Uint8Array[]) => Uint8Array); -let _sha256Sync: Sha256FnSync; -let _hmacSha256Sync: HmacFnSync; - -// Minimal HMAC-DRBG (NIST 800-90) for signatures -// Used only for RFC6979, does not fully implement DRBG spec. -class HmacDrbg { - k: Uint8Array; - v: Uint8Array; - counter: number; - constructor(public hashLen: number, public qByteLen: number) { - if (typeof hashLen !== 'number' || hashLen < 2) throw new Error('hashLen must be a number'); - if (typeof qByteLen !== 'number' || qByteLen < 2) throw new Error('qByteLen must be a number'); - - // Step B, Step C: set hashLen to 8*ceil(hlen/8) - this.v = new Uint8Array(hashLen).fill(1); - this.k = new Uint8Array(hashLen).fill(0); - this.counter = 0; - } - private hmac(...values: Uint8Array[]) { - return utils.hmacSha256(this.k, ...values); - } - private hmacSync(...values: Uint8Array[]) { - return _hmacSha256Sync!(this.k, ...values); - } - private checkSync() { - if (typeof _hmacSha256Sync !== 'function') throw new ShaError('hmacSha256Sync needs to be set'); - } - incr() { - if (this.counter >= 1000) throw new Error('Tried 1,000 k values for sign(), all were invalid'); - this.counter += 1; - } - - // We concatenate extraData into seed - async reseed(seed = new Uint8Array()) { - this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed); - this.v = await this.hmac(this.v); - if (seed.length === 0) return; - this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed); - this.v = await this.hmac(this.v); - } - reseedSync(seed = new Uint8Array()) { - this.checkSync(); - this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed); - this.v = this.hmacSync(this.v); - if (seed.length === 0) return; - this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed); - this.v = this.hmacSync(this.v); + return b === 1n ? mod(x, md) : err('no inverse'); // b is gcd at this point +}; +const pow = (num: bigint, e: bigint, md = P): bigint => { // modular exponentiation num^e + if (md <= 0n || e < 0n) err('pow invalid'); // exponentiation by squaring + if (md === 1n) return 0n; // the ladder can leak exponent bits + let res = 1n; // and is vulnerable to timing attacks + for (; e > 0n; e >>= 1n) { + if (e & 1n) res = (res * num) % md; + num = (num * num) % md; } - - async generate(): Promise { - this.incr(); - let len = 0; - const out: Uint8Array[] = []; - while (len < this.qByteLen) { - this.v = await this.hmac(this.v); - const sl = this.v.slice(); - out.push(sl); - len += this.v.length; - } - return concatBytes(...out); + return res; +}; +const sqrt = (n: bigint) => { // √n = n^((p+1)/4) for fields p = 3 mod 4 + const r = pow(n, (P + 1n) / 4n, P); // So, a special, fast case. Paper: "Square + return mod(r * r) === n ? r : err('sqrt invalid'); // Roots from 1;24,51,10 to Dan Shanks" +}; +const padh = (num: number | bigint, pad: number) => num.toString(16).padStart(pad, '0'); +const b2h = (b: Bytes): string => Array.from(b).map(e => padh(e, 2)).join(''); // bytes to hex +const h2n = (hex: string): bigint => (str(hex) ? BigInt(`0x${hex||'0'}`) : err()); // hex to number +const h2b = (hex: string): Bytes => { // hex to bytes + const l = hex.length; // error if not string, + if (!str(hex) || l % 2) err('hex invalid'); // or has odd length like 3, 5. + const arr = u8n(l / 2); // create result array + for (let i = 0; i < arr.length; i++) { + const j = i * 2; + const h = hex.slice(j, j + 2); // hexByte. slice is faster than substr + const b = Number.parseInt(h, 16); // byte, created from string part + if (Number.isNaN(b) || b < 0) err('hex invalid'); // byte must be valid 0 <= byte < 256 + arr[i] = b; } - generateSync(): Uint8Array { - this.checkSync(); - this.incr(); - let len = 0; - const out: Uint8Array[] = []; - while (len < this.qByteLen) { - this.v = this.hmacSync(this.v); - const sl = this.v.slice(); - out.push(sl); - len += this.v.length; + return arr; +}; +const b2n = (b: Bytes): bigint => h2n(b2h(b)); // bytes to number +const slcNum = (b: Bytes, from: number, to: number) => b2n(b.slice(from, to)); // slice bytes num +const n2b = (num: bigint): Bytes => { // number to bytes. must be 0 <= num < B256 + return big(num) && num >= 0n && num < B256 ? h2b(padh(num, 2 * fLen)) : err('bigint expected'); +}; +const n2h = (num: bigint): string => b2h(n2b(num)); // number to hex +const concatB = (...arrs: Bytes[]) => { // concatenate Uint8Array-s + const r = u8n(arrs.reduce((sum, a) => sum + a.length, 0)); // create u8a of summed length + let pad = 0; // walk through each array, ensure + arrs.forEach(a => { r.set(au8(a), pad); pad += a.length }); // they have proper type + return r; +}; +const moreThanHalfN = (n: bigint): boolean => n > (N >> 1n) // if a number is bigger than CURVE.n/2 +export const getPublicKey = (privKey: PrivKey, isCompressed = true) => { // calculate public + return Point.fromPrivateKey(privKey).toRawBytes(isCompressed); // key from private +}; +export class Signature { // calculates signature + constructor(readonly r: bigint, readonly s: bigint, readonly recovery?: number) { this.ok(); } + ok(): Signature { return ge(this.r) && ge(this.s) ? this : err(); } // 0 < r or s < CURVE.n + static fromCompact(hex: Hex) { // create signature from 64b compact repr + hex = toU8(hex, 64); // compact repr is (32b r)||(32b s) + return new Signature(slcNum(hex, 0, fLen), slcNum(hex, fLen, 2 * fLen)); + } + hasHighS() { return moreThanHalfN(this.s); } + recoverPublicKey(msgh: Hex): Point { // ECDSA public key recovery + const { r, s, recovery: rec } = this; // secg.org/sec1-v2.pdf 4.1.6 + if (![0, 1, 2, 3].includes(rec!)) err('recovery id invalid'); // check recovery id + const h = bits2int_modN(toU8(msgh, 32)); // Truncate hash + const radj = rec === 2 || rec === 3 ? r + N : r; // If rec was 2 or 3, q.x is bigger than n + if (radj >= P) err('q.x invalid'); // ensure q.x is still a field element + const prefix = (rec! & 1) === 0 ? '02' : '03'; // prefix is 0x02 or 0x03 + const R = Point.fromHex(`${prefix}${n2h(radj)}`); // concat prefix + hex repr of r + const ir = inv(radj, N); // r^-1 + const u1 = mod(-h * ir, N); // -hr^-1 + const u2 = mod(s * ir, N); // sr^-1 + return G.mulAddQUns(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1) + } + toCompactRawBytes() { return h2b(this.toCompactHex()); } // Uint8Array 64b compact repr + toCompactHex() { return n2h(this.r) + n2h(this.s); } // hex 64b compact repr +} +const bits2int = (bytes: Uint8Array): bigint => { // RFC6979: ensure ECDSA msg is X bytes. + const delta = bytes.length * 8 - 256; // RFC suggests optional truncating via bits2octets + const num = b2n(bytes); // FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits, which + return delta > 0 ? num >> BigInt(delta) : num; // matches bits2int. bits2int can produce res>N. +}; +const bits2int_modN = (bytes: Uint8Array): bigint => { // int2octets can't be used; pads small msgs + return mod(bits2int(bytes), N); // with 0: BAD for trunc as per RFC vectors +}; +const i2o = (num: bigint): Bytes => n2b(num); // int to octets +declare const globalThis: Record | undefined; // Typescript symbol present in browsers +const cr = () => // We support: 1) browsers 2) node.js 19+ + typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; +type HmacFnSync = undefined | ((key: Bytes, ...msgs: Bytes[]) => Bytes); +let _hmacSync: HmacFnSync; // Can be redefined by use in utils; built-ins don't provide it +const stdo: { lowS?: boolean; extraEntropy?: boolean | Hex; } = { lowS: true }; // opts for sign() +const vstdo = { lowS: true }; // standard opts for verify() +type BC = { seed: Bytes, k2sig : (kb: Bytes) => Signature | undefined }; +const prepSig = (msgh: Hex, priv: Hex, opts = stdo): BC => { // prepare for RFC6979 sig generation + if (['der', 'recovered', 'canonical'].some(k => k in opts)) // Ban legacy options + err('sign() legacy options not supported'); + let { lowS } = opts; // generates low-s sigs by default + if (lowS == null) lowS = true; // RFC6979 3.2: we skip step A, because + const h1i = bits2int_modN(toU8(msgh)); + const h1o = i2o(h1i); + const d = toPriv(priv); // validate private key, convert to bigint + const seed = [i2o(d), h1o]; // Step D of RFC6979 3.2 + let ent = opts.extraEntropy; // RFC6979 3.6: additional k' (optional) + if (ent != null) { // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') + if (ent === true) ent = etc.randomBytes(fLen); // if true, use CSPRNG to generate data + const e = toU8(ent); // convert Hex|Bytes to Bytes + if (e.length !== fLen) err(); // Expected 32 bytes of extra data + seed.push(e); + } + const m = h1i; // convert msg to bigint + const k2sig = (kBytes: Bytes): Signature | undefined => { + const k = bits2int(kBytes); // Transforms k into Signature + if (!ge(k)) return; // Check 0 < k < CURVE.n + const ik = inv(k, N); // k^-1 mod n, NOT mod P + const q = G.mul(k).aff(); // q = Gk + const r = mod(q.x, N); // r = q.x mod n + if (r === 0n) return; // r=0 invalid + const s = mod(ik * mod(m + mod(d * r, N), N), N); // s = k^-1(m + rd) mod n + if (s === 0n) return; // s=0 invalid + let normS = s; + let rec = (q.x === r ? 0 : 2) | Number(q.y & 1n); // recovery bit + if (lowS && moreThanHalfN(s)) { // if lowS was passed, ensure s is always + normS = mod(-s, N); // in the bottom half of CURVE.n + rec ^= 1; } - return concatBytes(...out); - } - // There is no need in clean() method - // It's useless, there are no guarantees with JS GC - // whether bigints are removed even if you clean Uint8Arrays. -} - -// Valid scalars are [1, n-1] -function isWithinCurveOrder(num: bigint): boolean { - return _0n < num && num < CURVE.n; -} - -// Valid field elements are [1, p-1] -function isValidFieldElement(num: bigint): boolean { - return _0n < num && num < CURVE.P; -} - -/** - * Converts signature params into point & r/s, checks them for validity. - * k must be in range [1, n-1] - * @param k signature's k param: deterministic in our case, random in non-rfc6979 sigs - * @param m message that would be signed - * @param d private key - * @returns Signature with its point on curve Q OR undefined if params were invalid - */ -function kmdToSig(kBytes: Uint8Array, m: bigint, d: bigint, lowS = true): RecoveredSig | undefined { - const { n } = CURVE; - const k = truncateHash(kBytes, true); - if (!isWithinCurveOrder(k)) return; - // Important: all mod() calls in the function must be done over `n` - const kinv = invert(k, n); - const q = Point.BASE.multiply(k); - // r = x mod n - const r = mod(q.x, n); - if (r === _0n) return; - // s = (m + dr)/k mod n where x/k == x*inv(k) - const s = mod(kinv * mod(m + d * r, n), n); - if (s === _0n) return; - - let sig = new Signature(r, s); - // Recovery bit is usually 0 or 1; rarely it's 2 or 3, when q.x > n - let recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n); - if (lowS && sig.hasHighS()) { - sig = sig.normalizeS(); - recovery ^= 1; - } - return { sig, recovery }; -} - -function normalizePrivateKey(key: PrivKey): bigint { - let num: bigint; - if (typeof key === 'bigint') { - num = key; - } else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) { - num = BigInt(key); - } else if (typeof key === 'string') { - if (key.length !== 2 * groupLen) throw new Error('Expected 32 bytes of private key'); - num = hexToNumber(key); - } else if (key instanceof Uint8Array) { - if (key.length !== groupLen) throw new Error('Expected 32 bytes of private key'); - num = bytesToNumber(key); - } else { - throw new TypeError('Expected valid private key'); - } - if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n'); - return num; -} - -/** - * Normalizes hex, bytes, Point to Point. Checks for curve equation. - */ -function normalizePublicKey(publicKey: PubKey): Point { - if (publicKey instanceof Point) { - publicKey.assertValidity(); - return publicKey; + return new Signature(r, normS, rec); // use normS, not s + }; + return { seed: concatB(...seed), k2sig } +}; +type Pred = (v: Uint8Array) => T | undefined; +function hmacDrbg(asynchronous: true): (seed: Bytes, predicate: Pred) => Promise; +function hmacDrbg(asynchronous: false): (seed: Bytes, predicate: Pred) => T; +function hmacDrbg(asynchronous: boolean) { // HMAC-DRBG async + let v = u8n(fLen); // Minimal non-full-spec HMAC-DRBG from NIST 800-90 for RFC6979 sigs. + let k = u8n(fLen); // Steps B, C of RFC6979 3.2: set hashLen, in our case always same + let i = 0; // Iterations counter, will throw when over 1000 + const reset = () => { v.fill(1); k.fill(0); i = 0; }; + const _e = 'drbg: tried 1000 values'; + if (asynchronous) { // asynchronous=true + const h = (...b: Bytes[]) => etc.hmacSha256Async(k, v, ...b); // hmac(k)(v, ...values) + const reseed = async (seed = u8n()) => { // HMAC-DRBG reseed() function. Steps D-G + k = await h(u8fr([0x00]), seed); // k = hmac(K || V || 0x00 || seed) + v = await h(); // v = hmac(K || V) + if (seed.length === 0) return; + k = await h(u8fr([0x01]), seed); // k = hmac(K || V || 0x01 || seed) + v = await h(); // v = hmac(K || V) + }; + const gen = async () => { // HMAC-DRBG generate() function + if (i++ >= 1000) err(_e); + v = await h(); // v = hmac(K || V) + return v; + }; + return async (seed: Bytes, pred: Pred): Promise => { // Even though it feels safe to reuse + reset(); // the returned fn, don't, it's: 1. slower (JIT). 2. unsafe (async race conditions) + await reseed(seed); // Steps D-G + let res: T | undefined = undefined; // Step H: grind until k is in [1..n-1] + while (!(res = pred(await gen()))) await reseed(); + reset(); + return res!; + }; } else { - return Point.fromHex(publicKey); - } -} - -/** - * Signatures can be in 64-byte compact representation, - * or in (variable-length)-byte DER representation. - * Since DER could also be 64 bytes, we check for it first. - */ -function normalizeSignature(signature: Sig): Signature { - if (signature instanceof Signature) { - signature.assertValidity(); - return signature; + const h = (...b: Bytes[]) => { // Same, but synchronous + const f = _hmacSync; + if (!f) err('utils.hmacSha256Sync not set'); + return f!(k, v, ...b); // hmac(k)(v, ...values) + }; + const reseed = (seed = u8n()) => { // HMAC-DRBG reseed() function. Steps D-G + k = h(u8fr([0x00]), seed); // k = hmac(k || v || 0x00 || seed) + v = h(); // v = hmac(k || v) + if (seed.length === 0) return; + k = h(u8fr([0x01]), seed); // k = hmac(k || v || 0x01 || seed) + v = h(); // v = hmac(k || v) + }; + const gen = () => { // HMAC-DRBG generate() function + if (i++ >= 1000) err(_e); + v = h(); // v = hmac(k || v) + return v; + }; + return (seed: Bytes, pred: Pred): T => { + reset(); + reseed(seed); // Steps D-G + let res: T | undefined = undefined; // Step H: grind until k is in [1..n-1] + while (!(res = pred(gen()))) reseed(); + reset(); + return res!; + }; } +}; +// ECDSA sig generation via secg.org/sec1-v2.pdf 4.1.2. hmacDrbg() +export const signAsync = async (msgh: Hex, priv: Hex, opts = stdo): Promise => { + const { seed, k2sig } = prepSig(msgh, priv, opts); + const genUntil = hmacDrbg(true); + return genUntil(seed, k2sig); +}; +export const sign = (msgh: Hex, priv: Hex, opts = stdo): Signature => { + const { seed, k2sig } = prepSig(msgh, priv, opts); + const genUntil = hmacDrbg(false); + return genUntil(seed, k2sig); +}; +type SigLike = { r: bigint, s: bigint }; +export const verify = (sig: Hex | SigLike, msgh: Hex, pub: Hex, opts = vstdo): boolean => { + let { lowS } = opts; // ECDSA signature verification + if (lowS == null) lowS = true; // Default lowS=true + if ('strict' in opts) err('verify() legacy options not supported'); // legacy param + let sig_: Signature, h: bigint, P: Point; // secg.org/sec1-v2.pdf 4.1.4 + const rs = sig && typeof sig === 'object' && 'r' in sig; // Previous ver supported DER sigs. We + if (!rs && (toU8(sig).length !== 2 * fLen)) // throw error when DER is suspected now. + err('signature must be 64 bytes'); try { - return Signature.fromDER(signature); - } catch (error) { - return Signature.fromCompact(signature); - } -} - -/** - * Computes public key for secp256k1 private key. - * @param privateKey 32-byte private key - * @param isCompressed whether to return compact, or full key - * @returns Public key, full by default; short when isCompressed=true - */ -export function getPublicKey(privateKey: PrivKey, isCompressed = false): Uint8Array { - return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed); -} - -/** - * Recovers public key from signature and recovery bit. Throws on invalid sig/hash. - * @param msgHash message hash - * @param signature DER or compact sig - * @param recovery 0 or 1 - * @param isCompressed whether to return compact, or full key - * @returns Public key, full by default; short when isCompressed=true - */ -export function recoverPublicKey( - msgHash: Hex, - signature: Sig, - recovery: number, - isCompressed = false -): Uint8Array { - return Point.fromSignature(msgHash, signature, recovery).toRawBytes(isCompressed); -} - -/** - * Quick and dirty check for item being public key. Does not validate hex, or being on-curve. - */ -function isProbPub(item: PrivKey | PubKey): boolean { - const arr = item instanceof Uint8Array; - const str = typeof item === 'string'; - const len = (arr || str) && (item as Hex).length; - if (arr) return len === compressedLen || len === uncompressedLen; - if (str) return len === compressedLen * 2 || len === uncompressedLen * 2; - if (item instanceof Point) return true; - return false; -} - -/** - * ECDH (Elliptic Curve Diffie Hellman) implementation. - * 1. Checks for validity of private key - * 2. Checks for the public key of being on-curve - * @param privateA private key - * @param publicB different public key - * @param isCompressed whether to return compact, or full key - * @returns shared public key - */ -export function getSharedSecret( - privateA: PrivKey, - publicB: PubKey, - isCompressed = false -): Uint8Array { - if (isProbPub(privateA)) throw new TypeError('getSharedSecret: first arg must be private key'); - if (!isProbPub(publicB)) throw new TypeError('getSharedSecret: second arg must be public key'); - const b = normalizePublicKey(publicB); - b.assertValidity(); - return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed); -} - -type Entropy = Hex | true; -type OptsOther = { canonical?: boolean; der?: boolean; extraEntropy?: Entropy }; -type OptsRecov = { recovered: true } & OptsOther; -type OptsNoRecov = { recovered?: false } & OptsOther; -type Opts = { recovered?: boolean } & OptsOther; -type SignOutput = Uint8Array | [Uint8Array, number]; - -// RFC6979 methods -function bits2int(bytes: Uint8Array) { - const slice = bytes.length > fieldLen ? bytes.slice(0, fieldLen) : bytes; - return bytesToNumber(slice); -} -function bits2octets(bytes: Uint8Array): Uint8Array { - const z1 = bits2int(bytes); - const z2 = mod(z1, CURVE.n); - return int2octets(z2 < _0n ? z1 : z2); -} -function int2octets(num: bigint): Uint8Array { - return numTo32b(num); // prohibits >32 bytes -} - -// Steps A, D of RFC6979 3.2 -// Creates RFC6979 seed; converts msg/privKey to numbers. -function initSigArgs(msgHash: Hex, privateKey: PrivKey, extraEntropy?: Entropy) { - if (msgHash == null) throw new Error(`sign: expected valid message hash, not "${msgHash}"`); - // Step A is ignored, since we already provide hash instead of msg - const h1 = ensureBytes(msgHash); - const d = normalizePrivateKey(privateKey); - // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k') - const seedArgs = [int2octets(d), bits2octets(h1)]; - // RFC6979 3.6: additional k' could be provided - if (extraEntropy != null) { - if (extraEntropy === true) extraEntropy = utils.randomBytes(fieldLen); - const e = ensureBytes(extraEntropy); - if (e.length !== fieldLen) throw new Error(`sign: Expected ${fieldLen} bytes of extra data`); - seedArgs.push(e); - } - // seed is constructed from private key and message - // Step D - // V, 0x00 are done in HmacDRBG constructor. - const seed = concatBytes(...seedArgs); - const m = bits2int(h1); - return { seed, m, d }; -} - -// Takes signature with its recovery bit, normalizes it -// Produces DER/compact signature and proper recovery bit -function finalizeSig(recSig: RecoveredSig, opts: OptsNoRecov | OptsRecov): SignOutput { - const { sig, recovery } = recSig; - const { der, recovered } = Object.assign({ canonical: true, der: true }, opts); - const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes(); - return recovered ? [hashed, recovery] : hashed; -} - -/** - * Signs message hash (not message: you need to hash it by yourself). - * We don't auto-hash because some users would want non-SHA256 hash. - * We are always using deterministic signatures (RFC6979 3.1) instead of - * letting user specify random k. - * HMAC-DRBG generates k, then calculates sig point Q & signature r, s based on it. - * Could receive extra entropy k' as per RFC6979 3.6 Additional data. - * k' is not generated by default, because of backwards-compatibility concerns. - * We strongly recommend to pass {extraEntropy: true}. - * - * low-s signatures are generated by default. If you don't want it, use canonical: false. - * - * ``` - * sign(m, d, k) where - * (x, y) = G × k - * r = x mod n - * s = (m + dr)/k mod n - * ``` - * @param opts `recovered, canonical, der, extraEntropy` - */ -async function sign(msgHash: Hex, privKey: PrivKey, opts: OptsRecov): Promise<[U8A, number]>; -async function sign(msgHash: Hex, privKey: PrivKey, opts?: OptsNoRecov): Promise; -async function sign(msgHash: Hex, privKey: PrivKey, opts: Opts = {}): Promise { - // Steps A, D of RFC6979 3.2. - const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); - // Steps B, C, D, E, F, G - const drbg = new HmacDrbg(hashLen, groupLen); - await drbg.reseed(seed); - // Step H3, repeat until k is in range [1, n-1] - let sig: RecoveredSig | undefined; - while (!(sig = kmdToSig(await drbg.generate(), m, d, opts.canonical))) await drbg.reseed(); - return finalizeSig(sig, opts); -} - -/** - * Signs message hash (not message: you need to hash it by yourself). - * Synchronous version of `sign()`: see its documentation. - * @param opts `recovered, canonical, der, extraEntropy` - */ -function signSync(msgHash: Hex, privKey: PrivKey, opts: OptsRecov): [U8A, number]; -function signSync(msgHash: Hex, privKey: PrivKey, opts?: OptsNoRecov): U8A; -function signSync(msgHash: Hex, privKey: PrivKey, opts: Opts = {}): SignOutput { - // Steps A, D of RFC6979 3.2. - const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); - // Steps B, C, D, E, F, G - const drbg = new HmacDrbg(hashLen, groupLen); - drbg.reseedSync(seed); - // Step H3, repeat until k is in range [1, n-1] - let sig: RecoveredSig | undefined; - while (!(sig = kmdToSig(drbg.generateSync(), m, d, opts.canonical))) drbg.reseedSync(); - return finalizeSig(sig, opts); -} -export { sign, signSync }; - -type VOpts = { strict?: boolean }; -const vopts: VOpts = { strict: true }; - -/** - * Verifies a signature against message hash and public key. - * Rejects non-canonical / high-s signatures by default: to override, - * specify option `{strict: false}`. 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 - * ``` - */ -export function verify(signature: Sig, msgHash: Hex, publicKey: PubKey, opts = vopts): boolean { - let sig; - try { - sig = normalizeSignature(signature); - msgHash = ensureBytes(msgHash); - } catch (error) { - return false; - } - const { r, s } = sig; - if (opts.strict && sig.hasHighS()) return false; - const h = truncateHash(msgHash); - - let P; - try { - P = normalizePublicKey(publicKey); - } catch (error) { - return false; - } - const { n } = CURVE; - const sinv = invert(s, n); // s^-1 - // R = u1⋅G - u2⋅P - const u1 = mod(h * sinv, n); - const u2 = mod(r * sinv, n); - - // Some implementations compare R.x in jacobian, without inversion. - // The speed-up is <5%, so we don't complicate the code. - const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2); - if (!R) return false; - const v = mod(R.x, n); - return v === r; -} - -// Schnorr signatures are superior to ECDSA from above. -// Below is Schnorr-specific code as per BIP0340. -function schnorrChallengeFinalize(ch: Uint8Array): bigint { - return mod(bytesToNumber(ch), CURVE.n); -} - -class SchnorrSignature { - constructor(readonly r: bigint, readonly s: bigint) { - this.assertValidity(); - } - static fromHex(hex: Hex) { - const bytes = ensureBytes(hex); - if (bytes.length !== 64) - throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`); - const r = bytesToNumber(bytes.subarray(0, 32)); - const s = bytesToNumber(bytes.subarray(32, 64)); - return new SchnorrSignature(r, s); - } - assertValidity() { - const { r, s } = this; - if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) throw new Error('Invalid signature'); - } - toHex(): string { - return numTo32bStr(this.r) + numTo32bStr(this.s); - } - toRawBytes(): Uint8Array { - return hexToBytes(this.toHex()); - } -} - -// Schnorr's pubkey is just `x` of Point -// BIP340 -function schnorrGetPublicKey(privateKey: PrivKey): Uint8Array { - return Point.fromPrivateKey(privateKey).toRawX(); -} - -// We are abstracting the signature creation process into the class -// because we need to provide two identical methods: async & sync. Usage: -// new InternalSchnorrSignature(msg, privKey, auxRand).calc() -class InternalSchnorrSignature { - private m: Uint8Array; - private px: Uint8Array; - private d: bigint; - private rand: Uint8Array; - - constructor(message: Hex, privateKey: PrivKey, auxRand: Hex = utils.randomBytes()) { - if (message == null) throw new TypeError(`sign: Expected valid message, not "${message}"`); - this.m = ensureBytes(message); - // checks for isWithinCurveOrder - const { x, scalar } = this.getScalar(normalizePrivateKey(privateKey)); - this.px = x; - this.d = scalar; - this.rand = ensureBytes(auxRand); - if (this.rand.length !== 32) throw new TypeError('sign: Expected 32 bytes of aux randomness'); - } - - private getScalar(priv: bigint) { - const point = Point.fromPrivateKey(priv); - const scalar = point.hasEvenY() ? priv : CURVE.n - priv; - return { point, scalar, x: point.toRawX() }; - } - - private initNonce(d: bigint, t0h: Uint8Array): Uint8Array { - return numTo32b(d ^ bytesToNumber(t0h)); - } - private finalizeNonce(k0h: Uint8Array) { - const k0 = mod(bytesToNumber(k0h), CURVE.n); - if (k0 === _0n) throw new Error('sign: Creation of signature failed. k is zero'); - const { point: R, x: rx, scalar: k } = this.getScalar(k0); - return { R, rx, k }; - } - private finalizeSig(R: Point, k: bigint, e: bigint, d: bigint): Uint8Array { - return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes(); - } - private error() { - throw new Error('sign: Invalid signature produced'); - } - - async calc() { - const { m, d, px, rand } = this; - const tag = utils.taggedHash; - const t = this.initNonce(d, await tag(TAGS.aux, rand)); - const { R, rx, k } = this.finalizeNonce(await tag(TAGS.nonce, t, px, m)); - const e = schnorrChallengeFinalize(await tag(TAGS.challenge, rx, px, m)); - const sig = this.finalizeSig(R, k, e, d); - if (!(await schnorrVerify(sig, m, px))) this.error(); - return sig; - } - calcSync() { - const { m, d, px, rand } = this; - const tag = utils.taggedHashSync; - const t = this.initNonce(d, tag(TAGS.aux, rand)); - const { R, rx, k } = this.finalizeNonce(tag(TAGS.nonce, t, px, m)); - const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m)); - const sig = this.finalizeSig(R, k, e, d); - if (!schnorrVerifySync(sig, m, px)) this.error(); - return sig; - } -} - -/** - * Creates Schnorr signature. Improved security: verifies itself before producing an output. - * @param msg message (not message hash) - * @param privateKey private key - * @param auxRand random bytes that would be added to k. Bad RNG won't break it. - */ -async function schnorrSign(msg: Hex, privKey: PrivKey, auxRand?: Hex): Promise { - return new InternalSchnorrSignature(msg, privKey, auxRand).calc(); -} - -/** - * Synchronously creates Schnorr signature. Improved security: verifies itself before - * producing an output. - * @param msg message (not message hash) - * @param privateKey private key - * @param auxRand random bytes that would be added to k. Bad RNG won't break it. - */ -function schnorrSignSync(msg: Hex, privKey: PrivKey, auxRand?: Hex): Uint8Array { - return new InternalSchnorrSignature(msg, privKey, auxRand).calcSync(); -} - -function initSchnorrVerify(signature: Hex, message: Hex, publicKey: Hex) { - const raw = signature instanceof SchnorrSignature; - const sig: SchnorrSignature = raw ? signature : SchnorrSignature.fromHex(signature); - if (raw) sig.assertValidity(); // just in case - - return { - ...sig, - m: ensureBytes(message), - P: normalizePublicKey(publicKey), - }; -} - -function finalizeSchnorrVerify(r: bigint, P: Point, s: bigint, e: bigint): boolean { - // R = s⋅G - e⋅P - // -eP == (n-e)P - const R = Point.BASE.multiplyAndAddUnsafe(P, normalizePrivateKey(s), mod(-e, CURVE.n)); - if (!R || !R.hasEvenY() || R.x !== r) return false; - return true; -} - -/** - * Verifies Schnorr signature. - */ -async function schnorrVerify(signature: Hex, message: Hex, publicKey: Hex): Promise { - try { - const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); - const e = schnorrChallengeFinalize( - await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m) - ); - return finalizeSchnorrVerify(r, P, s, e); - } catch (error) { - return false; - } -} - -/** - * Verifies Schnorr signature synchronously. - */ -function schnorrVerifySync(signature: Hex, message: Hex, publicKey: Hex): boolean { + sig_ = rs ? new Signature(sig.r, sig.s).ok() : Signature.fromCompact(sig); + h = bits2int_modN(toU8(msgh, fLen)); // Truncate hash + P = pub instanceof Point ? pub.ok() : Point.fromHex(pub); // Validate public key + } catch (e) { return false; } // Check sig for validity in both cases + if (!sig_) return false; + const { r, s } = sig_; + if (lowS && moreThanHalfN(s)) return false; // lowS bans sig.s >= CURVE.n/2 + let R: AffinePoint; try { - const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); - const e = schnorrChallengeFinalize( - utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m) - ); - return finalizeSchnorrVerify(r, P, s, e); - } catch (error) { - if (error instanceof ShaError) throw error; - return false; - } -} - -export const schnorr = { - Signature: SchnorrSignature, - getPublicKey: schnorrGetPublicKey, - sign: schnorrSign, - verify: schnorrVerify, - signSync: schnorrSignSync, - verifySync: schnorrVerifySync, + const is = inv(s, N); // s^-1 + const u1 = mod(h * is, N); // u1 = hs^-1 mod n + const u2 = mod(r * is, N); // u2 = rs^-1 mod n + R = G.mulAddQUns(P, u1, u2).aff(); // R = u1⋅G + u2⋅P + } catch (error) { return false; } + if (!R) return false; // stop if R is identity / zero point + const v = mod(R.x, N); + return v === r; // mod(R.x, n) == r +} +export const getSharedSecret = (privA: Hex, pubB: Hex, isCompressed = true) => { + return Point.fromHex(pubB).mul(toPriv(privA)).toRawBytes(isCompressed); // ECDH }; - -// Enable precomputes. Slows down first publicKey computation by 20ms. -Point.BASE._setWindowSize(8); - -// Global symbol available in browsers only. Ensure we do not depend on @types/dom -declare const self: Record | undefined; -const crypto: { node?: any; web?: any } = { - node: nodeCrypto, - web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, +const hashToPrivateKey = (hash: Hex): Bytes => { // FIPS 186 B.4.1 compliant key generation + hash = toU8(hash); // produces private keys with modulo bias + const minLen = fLen + 8; // being neglible. + if (hash.length < minLen || hash.length > 1024) err('expected proper params'); + const num = mod(b2n(hash), N - 1n) + 1n; // takes at least n+8 bytes + return n2b(num); }; - -const TAGS = { - challenge: 'BIP0340/challenge', - aux: 'BIP0340/aux', - nonce: 'BIP0340/nonce', -} as const; -/** An object mapping tags to their tagged hash prefix of [SHA256(tag) | SHA256(tag)] */ -const TAGGED_HASH_PREFIXES: { [tag: string]: Uint8Array } = {}; - -export const utils = { - bytesToHex, - hexToBytes, - concatBytes, - mod, - invert, - - isValidPrivateKey(privateKey: PrivKey) { - try { - normalizePrivateKey(privateKey); - return true; - } catch (error) { - return false; - } - }, - _bigintTo32Bytes: numTo32b, - _normalizePrivateKey: normalizePrivateKey, - - /** - * Can take (n+8) or more bytes of uniform input e.g. from CSPRNG or KDF - * and convert them into private key, with the modulo bias being neglible. - * As per FIPS 186 B.4.1. - * https://research.kudelskisecurity.com/2020/07/28/the-definitive-guide-to-modulo-bias-and-how-to-avoid-it/ - * @param hash hash output from sha512, or a similar function - * @returns valid private key - */ - hashToPrivateKey: (hash: Hex): Uint8Array => { - hash = ensureBytes(hash); - const minLen = groupLen + 8; - if (hash.length < minLen || hash.length > 1024) { - throw new Error(`Expected valid bytes of private key as per FIPS 186`); - } - const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n; - return numTo32b(num); - }, - - randomBytes: (bytesLength: number = 32): Uint8Array => { - if (crypto.web) { - return crypto.web.getRandomValues(new Uint8Array(bytesLength)); - } else if (crypto.node) { - const { randomBytes } = crypto.node; - return Uint8Array.from(randomBytes(bytesLength)); - } else { - throw new Error("The environment doesn't have randomBytes function"); - } - }, - - // Takes curve order + 64 bits from CSPRNG - // so that modulo bias is neglible, matches FIPS 186 B.4.1. - randomPrivateKey: (): Uint8Array => utils.hashToPrivateKey(utils.randomBytes(groupLen + 8)), - - /** - * 1. Returns cached point which you can use to pass to `getSharedSecret` or `#multiply` by it. - * 2. Precomputes point multiplication table. Is done by default on first `getPublicKey()` call. - * If you want your first getPublicKey to take 0.16ms instead of 20ms, make sure to call - * utils.precompute() somewhere without arguments first. - * @param windowSize 2, 4, 8, 16 - * @returns cached point - */ - precompute(windowSize = 8, point = Point.BASE): Point { - const cached = point === Point.BASE ? point : new Point(point.x, point.y); - cached._setWindowSize(windowSize); - cached.multiply(_3n); - return cached; +export const etc = { // Not placed in `utils` because utils + hexToBytes: h2b, bytesToHex: b2h, // share API with noble-curves. + concatBytes: concatB, bytesToNumberBE: b2n, numberToBytesBE: n2b, + mod, invert: inv, // math utilities + hmacSha256Async: async (key: Bytes, ...msgs: Bytes[]): Promise => { + const m = concatB(...msgs); // HMAC-SHA256 async. No sync built-in! + const crypto = cr(); + if (!crypto) return err('utils.hmacSha256Async not set'); + const s = crypto.subtle; + const k = await s.importKey('raw', key, {name:'HMAC',hash:{name:'SHA-256'}}, false, ['sign']); + return u8n(await s.sign('HMAC', k, m)); }, - - sha256: async (...messages: Uint8Array[]): Promise => { - if (crypto.web) { - const buffer = await crypto.web.subtle.digest('SHA-256', concatBytes(...messages)); - return new Uint8Array(buffer); - } else if (crypto.node) { - const { createHash } = crypto.node; - const hash = createHash('sha256'); - messages.forEach((m) => hash.update(m)); - return Uint8Array.from(hash.digest()); - } else { - throw new Error("The environment doesn't have sha256 function"); - } - }, - - hmacSha256: async (key: Uint8Array, ...messages: Uint8Array[]): Promise => { - if (crypto.web) { - // prettier-ignore - const ckey = await crypto.web.subtle.importKey( - 'raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign'] - ); - const message = concatBytes(...messages); - const buffer = await crypto.web.subtle.sign('HMAC', ckey, message); - return new Uint8Array(buffer); - } else if (crypto.node) { - const { createHmac } = crypto.node; - const hash = createHmac('sha256', key); - messages.forEach((m) => hash.update(m)); - return Uint8Array.from(hash.digest()); - } else { - throw new Error("The environment doesn't have hmac-sha256 function"); - } - }, - - // See Object.defineProp below - sha256Sync: undefined as Sha256FnSync, - hmacSha256Sync: undefined as HmacFnSync, - - taggedHash: async (tag: string, ...messages: Uint8Array[]): Promise => { - let tagP = TAGGED_HASH_PREFIXES[tag]; - if (tagP === undefined) { - const tagH = await utils.sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); - tagP = concatBytes(tagH, tagH); - TAGGED_HASH_PREFIXES[tag] = tagP; - } - - return utils.sha256(tagP, ...messages); - }, - - taggedHashSync: (tag: string, ...messages: Uint8Array[]): Uint8Array => { - if (typeof _sha256Sync !== 'function') - throw new ShaError('sha256Sync is undefined, you need to set it'); - let tagP = TAGGED_HASH_PREFIXES[tag]; - if (tagP === undefined) { - const tagH = _sha256Sync(Uint8Array.from(tag, (c) => c.charCodeAt(0))); - tagP = concatBytes(tagH, tagH); - TAGGED_HASH_PREFIXES[tag] = tagP; - } - - return _sha256Sync(tagP, ...messages); + hmacSha256Sync: _hmacSync, // For TypeScript. Actual logic is below + hashToPrivateKey, + randomBytes: (len: number): Bytes => { // CSPRNG (random number generator) + const crypto = cr(); // Can be shimmed in node.js <= 18 to prevent error: + // import { webcrypto } from 'node:crypto'; + // if (!globalThis.crypto) globalThis.crypto = webcrypto; + if (!crypto) err('crypto.getRandomValues must be defined'); + return crypto.getRandomValues(u8n(len)); }, - - // For tests - _JacobianPoint: JacobianPoint, +} +export const utils = { // utilities + normPrivateKeyToScalar: toPriv, + randomPrivateKey: (): Bytes => hashToPrivateKey(etc.randomBytes(fLen + 8)), // FIPS 186 B.4.1. + isValidPrivateKey: (key: Hex) => { try { return !!toPriv(key); } catch (e) { return false; } }, + precompute(p: Point, windowSize = 8) { return p; } // no-op }; - -// Make sure sync hash could only be set once. -Object.defineProperties(utils, { - sha256Sync: { - configurable: false, - get() { - return _sha256Sync; - }, - set(val) { - if (!_sha256Sync) _sha256Sync = val; - }, - }, - hmacSha256Sync: { - configurable: false, - get() { - return _hmacSha256Sync; - }, - set(val) { - if (!_hmacSha256Sync) _hmacSha256Sync = val; - }, - }, -}); +Object.defineProperties(etc, { hmacSha256Sync: { // Allow setting it once, ignore then + configurable: false, get() { return _hmacSync; }, set(f) { if (!_hmacSync) _hmacSync = f; }, +} }); +const W = 8; // Precomputes-related code. W = window size +const precompute = () => { // They give 12x faster getPublicKey(), + const points: Point[] = []; // 10x sign(), 2x verify(). To achieve this, + const windows = 256 / W + 1; // app needs to spend 40ms+ to calculate + let p = G, b = p; // a lot of points related to base point G. + for (let w = 0; w < windows; w++) { // Points are stored in array and used + b = p; // any time Gx multiplication is done. + points.push(b); // They consume 16-32 MiB of RAM. + for (let i = 1; i < 2 ** (W - 1); i++) { b = b.add(p); points.push(b); } + p = b.dbl(); // Precomputes don't speed-up getSharedKey, + } // which multiplies user point by scalar, + return points; // when precomputes are using base point +} +const wNAF = (n: bigint): { p: Point; f: Point } => { // w-ary non-adjacent form (wNAF) method. + // Compared to other point mult methods, + const comp = Gpows || (Gpows = precompute()); // stores 2x less points using subtraction + const neg = (cnd: boolean, p: Point) => { let n = p.neg(); return cnd ? n : p; } // negate + let p = I, f = G; // f must be G, or could become I in the end + const windows = 1 + 256 / W; // W=8 17 windows + const wsize = 2 ** (W - 1); // W=8 128 window size + const mask = BigInt(2 ** W - 1); // W=8 will create mask 0b11111111 + const maxNum = 2 ** W; // W=8 256 + const shiftBy = BigInt(W); // W=8 8 + for (let w = 0; w < windows; w++) { + const off = w * wsize; + let wbits = Number(n & mask); // extract W bits. + n >>= shiftBy; // shift number by W bits. + if (wbits > wsize) { wbits -= maxNum; n += 1n; } // split if bits > max: +224 => 256-32 + const off1 = off, off2 = off + Math.abs(wbits) - 1; // offsets, evaluate both + const cnd1 = w % 2 !== 0, cnd2 = wbits < 0; // conditions, evaluate both + if (wbits === 0) { + f = f.add(neg(cnd1, comp[off1])); // bits are 0: add garbage to fake point + } else { // ^ can't add off2, off2 = I + p = p.add(neg(cnd2, comp[off2])); // bits are 1: add to result point + } + } + return { p, f } // return both real and fake points for JIT +}; // !! you can disable precomputes by commenting-out call of the wNAF() inside Point#mul() +export const ProjectivePoint = Point; diff --git a/lib/esm/package.json b/lib/esm/package.json deleted file mode 100644 index e8309c4..0000000 --- a/lib/esm/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "module", - "browser": { - "crypto": false - } -} \ No newline at end of file diff --git a/mod.ts b/mod.ts index b21f1a5..900edd0 100644 --- a/mod.ts +++ b/mod.ts @@ -1,11 +1,12 @@ // prettier-ignore import { - CURVE, Point, Signature, - getPublicKey, sign, signSync, verify, recoverPublicKey, getSharedSecret, - schnorr, utils, + CURVE, ProjectivePoint, Signature, + getPublicKey, sign, signAsync, verify, getSharedSecret, + utils, } from './index.ts'; -import { HmacSha256 } from 'https://deno.land/std@0.153.0/hash/sha256.ts'; -import { crypto } from 'https://deno.land/std@0.153.0/crypto/mod.ts'; +import { hmac } from 'npm:@noble/hashes@1.2.0/hmac'; +import { sha256 } from 'npm:@noble/hashes@1.2.0/sha256'; +import { crypto } from 'https://deno.land/std@0.175.0/crypto/mod.ts'; utils.sha256 = async (...msgs: Uint8Array[]): Promise => { return new Uint8Array(await crypto.subtle.digest('SHA-256', utils.concatBytes(...msgs))); @@ -14,19 +15,13 @@ utils.sha256Sync = (...msgs: Uint8Array[]): Uint8Array => { return new Uint8Array(crypto.subtle.digestSync('SHA-256', utils.concatBytes(...msgs))); }; -function hmac(key: Uint8Array, ...messages: Uint8Array[]): Uint8Array { - const sha = new HmacSha256(key); - for (let msg of messages) sha.update(msg); - return new Uint8Array(sha.arrayBuffer()); +utils.hmacSha256Sync = function hmac(key: Uint8Array, ...messages: Uint8Array[]): Uint8Array { + return hmac(sha256, key, ...messages); } -utils.hmacSha256 = async (key: Uint8Array, ...messages: Uint8Array[]) => - Promise.resolve(hmac(key, ...messages)); -utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]) => hmac(key, ...messages); - // prettier-ignore export { - CURVE, Point, Signature, - getPublicKey, sign, signSync, verify, recoverPublicKey, getSharedSecret, - schnorr, utils, + CURVE, ProjectivePoint, Signature, + getPublicKey, sign, signAsync, verify, getSharedSecret, + utils, }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..81199e9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,95 @@ +{ + "name": "@noble/secp256k1", + "version": "2.0.0-pre", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@noble/secp256k1", + "version": "2.0.0-pre", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "devDependencies": { + "@noble/hashes": "1.2.0", + "fast-check": "3.0.0", + "micro-bmark": "0.3.0", + "micro-should": "0.4.0", + "typescript": "4.7.3" + } + }, + "node_modules/@noble/hashes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, + "node_modules/fast-check": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.0.0.tgz", + "integrity": "sha512-uujtrFJEQQqnIMO52ARwzPcuV4omiL1OJBUBLE9WnNFeu0A97sREXDOmCIHY+Z6KLVcemUf09rWr0q0Xy/Y/Ew==", + "dev": true, + "dependencies": { + "pure-rand": "^5.0.1" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + }, + "node_modules/micro-bmark": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/micro-bmark/-/micro-bmark-0.3.0.tgz", + "integrity": "sha512-rYu+AtUq8lC3zPCoxkOOtwhgJoMpCDGe0/BXUCkj6+H9f/U/TunH/n/qkN98yh04dCCtDV8Aj9uYO3+DKxYrcw==", + "dev": true + }, + "node_modules/micro-should": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/micro-should/-/micro-should-0.4.0.tgz", + "integrity": "sha512-Vclj8yrngSYc9Y3dL2C+AdUlTkyx/syWc4R7LYfk4h7+icfF0DoUBGjjUIaEDzZA19RzoI+Hg8rW9IRoNGP0tQ==", + "dev": true + }, + "node_modules/pure-rand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-5.0.5.tgz", + "integrity": "sha512-BwQpbqxSCBJVpamI6ydzcKqyFmnd5msMWUGvzXLm1aXvusbbgkbOto/EUPM00hjveJEaJtdbhUjKSzWRhQVkaw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/typescript": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz", + "integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/package.json b/package.json index 5fb67d9..ba80829 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,24 @@ { "name": "@noble/secp256k1", - "version": "1.7.1", - "description": "Fastest JS implementation of secp256k1. Independently audited, high-security, 0-dependency ECDSA & Schnorr signatures", + "version": "2.0.0-pre", + "description": "Fastest 9KB JS implementation of secp256k1. Auditable, high-security, 0-dependency ECDSA signatures compliant with RFC6979", "files": [ - "lib" + "index.js", + "index.d.ts", + "index.ts" ], - "main": "lib/index.js", - "module": "lib/esm/index.js", - "types": "lib/index.d.ts", + "type": "module", + "main": "index.js", + "module": "index.js", + "types": "index.d.ts", "scripts": { - "build": "tsc && tsc -p tsconfig.esm.json", + "build": "tsc", "build:release": "rollup -c rollup.config.js", - "lint": "prettier --print-width 100 --single-quote --check index.ts", - "format": "prettier --print-width 100 --single-quote --write index.ts", - "test": "jest", + "test": "node test/secp256k1.test.mjs", "coverage": "jest --coverage", - "bench": "node test/benchmark.js" + "bench": "node test/benchmark.js", + "min": "cd build; npm install; npm run terser", + "loc": "echo \"`npm run --silent min | wc -c` symbols `wc -l < index.ts`LOC\"" }, "author": "Paul Miller (https://paulmillr.com)", "homepage": "https://paulmillr.com/noble/", @@ -24,21 +27,11 @@ "url": "https://github.com/paulmillr/noble-secp256k1.git" }, "license": "MIT", - "browser": { - "crypto": false - }, "devDependencies": { - "@noble/hashes": "1.1.2", - "@rollup/plugin-commonjs": "22.0.0", - "@rollup/plugin-node-resolve": "13.3.0", - "@types/jest": "28.1.1", - "@types/node": "17.0.18", + "@noble/hashes": "1.2.0", "fast-check": "3.0.0", - "jest": "28.1.0", - "micro-bmark": "0.2.0", - "prettier": "2.6.2", - "rollup": "2.75.5", - "ts-jest": "28.0.4", + "micro-bmark": "0.3.0", + "micro-should": "0.4.0", "typescript": "4.7.3" }, "keywords": [ @@ -51,12 +44,10 @@ "signature", "ecc", "rfc6979", - "schnorr", "sig", "bip0340", "bip340", "ecdsa", - "endomorphism", "cryptography", "security", "noble" @@ -64,16 +55,9 @@ "exports": { ".": { "types": "./lib/index.d.ts", - "import": "./lib/esm/index.js", "default": "./lib/index.js" } }, - "jest": { - "testRegex": "/test/.*?\\.ts", - "transform": { - "^.+\\.ts$": "ts-jest" - } - }, "funding": [ { "type": "individual", diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index a8119a5..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; - -export default { - input: 'lib/esm/index.js', - output: { - file: 'build/noble-secp256k1.js', - format: 'umd', - name: 'nobleSecp256k1', - exports: 'named', - preferConst: true, - }, - plugins: [resolve({ browser: true })], -}; diff --git a/test/benchmark.js b/test/benchmark.js index 02e8e15..4f3338a 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -1,85 +1,29 @@ -const { run, mark, logMem } = require('micro-bmark'); -const secp = require('..'); -const { join } = require('path'); -const { hmac } = require('@noble/hashes/hmac'); -const { sha256 } = require('@noble/hashes/sha256'); -const points = require('fs') - .readFileSync(join(__dirname, './vectors/points.txt'), 'utf-8') +import { webcrypto } from 'node:crypto'; +if (!globalThis.crypto) globalThis.crypto = webcrypto; +import { run, mark } from 'micro-bmark'; +import * as secp from '../index.js'; +import { join } from 'node:path'; +import { readFileSync } from 'node:fs'; +const points = readFileSync(join('.', 'test/vectors/points.txt'), 'utf-8') .split('\n') .filter((a) => a) .slice(0, 1000); -const { concatBytes } = secp.utils; - -secp.utils.sha256Sync = (...msgs) => - sha256.create().update(concatBytes(...msgs)).digest(), -secp.utils.hmacSha256Sync = (key, ...msgs) => - hmac.create(sha256, key).update(concatBytes(...msgs)).digest(); - -// run([4, 8, 16], async (windowSize) => { -run(async (windowSize) => { - const samples = 1000; - //console.log(`-------\nBenchmarking window=${windowSize} samples=${samples}...`); - await mark(() => { - secp.utils.precompute(windowSize); - }); - - logMem(); - console.log(); - - // await mark('getPublicKey 1 bit', samples * 10, () => { - // secp.getPublicKey('0000000000000000000000000000000000000000000000000000000000000003'); - // }); - - // await mark('getPublicKey 256 bit', samples * 10, () => { - // secp.getPublicKey('7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffcfcb'); - // }); +run(async () => { + secp.getPublicKey(secp.utils.randomPrivateKey(), true); // warmup await mark('getPublicKey(utils.randomPrivateKey())', 5000, () => { secp.getPublicKey(secp.utils.randomPrivateKey(), true); }); - const priv = 'f6fc7fd5acaf8603709160d203253d5cd17daa307483877ad811ec8411df56d2'; const pub = secp.getPublicKey(priv, true); const priv2 = '2e63f49054e1e44ccc2e6ef6ce387936efb16158f89cc302a2426e0b7fd66f66'; const pub2 = secp.getPublicKey(priv2, true); const msg = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; - const signature = await secp.sign(msg, priv); - - await mark('sign', 5000, async () => { - await secp.sign(msg, priv); - }); - - await mark('signSync (@noble/hashes)', 5000, () => secp.signSync(msg, priv)); - - await mark('verify', 1000, () => { - secp.verify(signature, msg, pub); - }); - - const [rsig, reco] = await secp.sign(msg, priv, { canonical: true, recovered: true }); - await mark('recoverPublicKey', 1000, () => { - secp.recoverPublicKey(msg, rsig, reco); - }); - - await mark('getSharedSecret aka ecdh', 600, () => { - secp.getSharedSecret(priv, pub2); - }); - - const pub2Pre = secp.utils.precompute(windowSize, secp.Point.fromHex(pub2)); - await mark('getSharedSecret (precomputed)', 5000, () => { - secp.getSharedSecret(priv, pub2Pre); - }); - + const signature = await secp.signAsync(msg, priv); + await mark('sign', 4000, async () => secp.signAsync(msg, priv)); + await mark('verify', 500, () => secp.verify(signature, msg, pub)); + await mark('getSharedSecret', 500, () => secp.getSharedSecret(priv, pub2)); + await mark('recoverPublicKey', 500, () => signature.recoverPublicKey(msg)); let i = 0; - await mark('Point.fromHex (decompression)', 10000, () => { - const p = points[i++ % points.length]; - secp.Point.fromHex(p); - }); - - const smsg = '0000000000000000000000000000000000000000000000000000000000000000'; - const spri = '0000000000000000000000000000000000000000000000000000000000000003'; - const spub = secp.Point.fromPrivateKey(spri); - const ssig = await secp.schnorr.sign(smsg, spri); - await mark('schnorr.sign', 500, () => secp.schnorr.sign(smsg, spri)); - await mark('schnorr.verify', 1000, () => secp.schnorr.verify(ssig, smsg, spub)); - console.log(); - logMem(); + let len = points.length; + await mark('Point.fromHex (decompression)', 10000, () => secp.ProjectivePoint.fromHex(points[i++ % len])); }); diff --git a/test/index.ts b/test/index.ts deleted file mode 100644 index a118c1f..0000000 --- a/test/index.ts +++ /dev/null @@ -1,569 +0,0 @@ -import * as fc from 'fast-check'; -import * as secp from '..'; -import { readFileSync } from 'fs'; -import { createHash } from 'crypto'; -import * as sysPath from 'path'; -import * as ecdsa from './vectors/ecdsa.json'; -import * as ecdh from './vectors/ecdh.json'; -import * as privates from './vectors/privates.json'; -import * as points from './vectors/points.json'; -import * as wp from './vectors/wychenproof.json'; -const privatesTxt = readFileSync(sysPath.join(__dirname, 'vectors', 'privates-2.txt'), 'utf-8'); -const schCsv = readFileSync(sysPath.join(__dirname, 'vectors', 'schnorr.csv'), 'utf-8'); - -const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n); -// prettier-ignore -const INVALID_ITEMS = ['deadbeef', Math.pow(2, 53), [1], 'xyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxy', secp.CURVE.n + 2n]; - -secp.utils.sha256Sync = (...msgs) => - createHash('sha256') - .update(secp.utils.concatBytes(...msgs)) - .digest(); - -const toBEHex = (n: number | bigint) => n.toString(16).padStart(64, '0'); -const hex = secp.utils.bytesToHex; -const hexToBytes = secp.utils.hexToBytes; - -function hexToNumber(hex: string): bigint { - if (typeof hex !== 'string') { - throw new TypeError('hexToNumber: expected string, got ' + typeof hex); - } - // Big Endian - return BigInt(`0x${hex}`); -} - -describe('secp256k1', () => { - it('.getPublicKey()', () => { - const data = privatesTxt - .split('\n') - .filter((line) => line) - .map((line) => line.split(':')); - for (let [priv, x, y] of data) { - const point = secp.Point.fromPrivateKey(BigInt(priv)); - expect(toBEHex(point.x)).toBe(x); - expect(toBEHex(point.y)).toBe(y); - - const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); - expect(toBEHex(point2.x)).toBe(x); - expect(toBEHex(point2.y)).toBe(y); - - const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); - expect(toBEHex(point3.x)).toBe(x); - expect(toBEHex(point3.y)).toBe(y); - } - }); - it('.getPublicKey() rejects invalid keys', () => { - for (const item of INVALID_ITEMS) { - expect(() => secp.getPublicKey(item as any)).toThrowError(); - } - }); - it('precompute', () => { - secp.utils.precompute(4); - const data = privatesTxt - .split('\n') - .filter((line) => line) - .map((line) => line.split(':')); - for (let [priv, x, y] of data) { - const point = secp.Point.fromPrivateKey(BigInt(priv)); - expect(toBEHex(point.x)).toBe(x); - expect(toBEHex(point.y)).toBe(y); - - const point2 = secp.Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); - expect(toBEHex(point2.x)).toBe(x); - expect(toBEHex(point2.y)).toBe(y); - - const point3 = secp.Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); - expect(toBEHex(point3.x)).toBe(x); - expect(toBEHex(point3.y)).toBe(y); - } - }); - describe('Point', () => { - it('.isValidPoint()', () => { - for (const vector of points.valid.isPoint) { - const { P, expected } = vector; - if (expected) { - secp.Point.fromHex(P); - } else { - expect(() => secp.Point.fromHex(P)).toThrowError(); - } - } - }); - - it('.fromPrivateKey()', () => { - for (const vector of points.valid.pointFromScalar) { - const { d, expected } = vector; - let p = secp.Point.fromPrivateKey(d); - expect(p.toHex(true)).toBe(expected); - } - }); - - it('#toHex(compressed)', () => { - for (const vector of points.valid.pointCompress) { - const { P, compress, expected } = vector; - let p = secp.Point.fromHex(P); - expect(p.toHex(compress)).toBe(expected); - } - }); - - it('#toHex() roundtrip', () => { - fc.assert( - fc.property(FC_BIGINT, (x) => { - const point1 = secp.Point.fromPrivateKey(x); - const hex = point1.toHex(true); - expect(secp.Point.fromHex(hex).toHex(true)).toBe(hex); - }) - ); - }); - - it('#add(other)', () => { - for (const vector of points.valid.pointAdd) { - const { P, Q, expected } = vector; - let p = secp.Point.fromHex(P); - let q = secp.Point.fromHex(Q); - if (expected) { - expect(p.add(q).toHex(true)).toBe(expected); - } else { - if (p.equals(q.negate())) expect(p.add(q).toHex(true)).toBe(secp.Point.ZERO.toHex(true)); - else expect(() => p.add(q).toHex(true)).toThrowError(); - } - } - }); - - it('#multiply(privateKey)', () => { - for (const vector of points.valid.pointMultiply) { - const { P, d, expected } = vector; - const p = secp.Point.fromHex(P); - if (expected) { - expect(p.multiply(hexToNumber(d)).toHex(true)).toBe(expected); - } else { - expect(() => { - p.multiply(hexToNumber(d)).toHex(true); - }).toThrowError(); - } - } - - for (const vector of points.invalid.pointMultiply) { - const { P, d } = vector; - if (hexToNumber(d) < secp.CURVE.n) { - expect(() => { - const p = secp.Point.fromHex(P); - p.multiply(hexToNumber(d)).toHex(true); - }).toThrowError(); - } - } - for (const num of [0n, 0, -1n, -1, 1.1]) { - expect(() => secp.Point.BASE.multiply(num)).toThrowError(); - } - }); - - // multiply() should equal multiplyUnsafe() - // it('JacobianPoint#multiplyUnsafe', () => { - // const p0 = new secp.JacobianPoint( - // 55066263022277343669578718895168534326250603453777594175500187360389116729240n, - // 32670510020758816978083085130507043184471273380659243275938904335757337482424n, - // 1n - // ); - // const z = 106011723082030650010038151861333186846790370053628296836951575624442507889495n; - // console.log(p0.multiply(z)); - // console.log(secp.JacobianPoint.normalizeZ([p0.multiplyUnsafe(z)])[0]) - // }); - }); - - describe('Signature', () => { - it('.fromCompactHex() roundtrip', () => { - fc.assert( - fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { - const sig = new secp.Signature(r, s); - expect(secp.Signature.fromCompact(sig.toCompactHex())).toEqual(sig); - }) - ); - }); - - it('.fromDERHex() roundtrip', () => { - fc.assert( - fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { - const sig = new secp.Signature(r, s); - expect(secp.Signature.fromDER(sig.toDERHex())).toEqual(sig); - }) - ); - }); - }); - - describe('.sign()', () => { - it('should create deterministic signatures with RFC 6979', async () => { - for (const vector of ecdsa.valid) { - let usig = await secp.sign(vector.m, vector.d, { der: false }); - let sig = hex(usig); - const vsig = vector.signature; - expect(sig.slice(0, 64)).toBe(vsig.slice(0, 64)); - expect(sig.slice(64, 128)).toBe(vsig.slice(64, 128)); - } - }); - - it('should not create invalid deterministic signatures with RFC 6979', async () => { - for (const vector of ecdsa.invalid.sign) { - expect(() => { - return secp.sign(vector.m, vector.d, { der: false }); - }).rejects.toThrowError(); - } - }); - - it('edge cases', () => { - // @ts-ignore - expect(async () => await secp.sign()).rejects.toThrowError(); - // @ts-ignore - expect(async () => await secp.sign('')).rejects.toThrowError(); - }); - - it('should create correct DER encoding against libsecp256k1', async () => { - const CASES = [ - [ - 'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b', - '304402203de2559fccb00c148574997f660e4d6f40605acc71267ee38101abf15ff467af02200950abdf40628fd13f547792ba2fc544681a485f2fdafb5c3b909a4df7350e6b', - ], - [ - '5f97983254982546d3976d905c6165033976ee449d300d0e382099fa74deaf82', - '3045022100c046d9ff0bd2845b9aa9dff9f997ecebb31e52349f80fe5a5a869747d31dcb88022011f72be2a6d48fe716b825e4117747b397783df26914a58139c3f4c5cbb0e66c', - ], - [ - '0d7017a96b97cd9be21cf28aada639827b2814a654a478c81945857196187808', - '3045022100d18990bba7832bb283e3ecf8700b67beb39acc73f4200ed1c331247c46edccc602202e5c8bbfe47ae159512c583b30a3fa86575cddc62527a03de7756517ae4c6c73', - ], - ]; - const privKey = hexToBytes( - '0101010101010101010101010101010101010101010101010101010101010101' - ); - for (let [msg, exp] of CASES) { - const res = await secp.sign(msg, privKey, { extraEntropy: undefined }); - expect(hex(res)).toBe(exp); - const rs = secp.Signature.fromDER(res).toCompactHex(); - expect(secp.Signature.fromCompact(rs).toDERHex()).toBe(exp); - } - }); - it('sign ecdsa extraData', async () => { - const ent1 = '0000000000000000000000000000000000000000000000000000000000000000'; - const ent2 = '0000000000000000000000000000000000000000000000000000000000000001'; - const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33'; - const ent4 = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; - const ent5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; - - for (const e of ecdsa.extraEntropy) { - const sign = async (extraEntropy?: string) => { - const s = await secp.sign(e.m, e.d, { der: false, extraEntropy }); - return hex(s); - }; - expect(await sign()).toBe(e.signature); - expect(await sign(ent1)).toBe(e.extraEntropy0); - expect(await sign(ent2)).toBe(e.extraEntropy1); - expect(await sign(ent3)).toBe(e.extraEntropyRand); - expect(await sign(ent4)).toBe(e.extraEntropyN); - expect(await sign(ent5)).toBe(e.extraEntropyMax); - } - }); - }); - - describe('.verify()', () => { - it('should verify signature', async () => { - const MSG = '01'.repeat(32); - const PRIV_KEY = 0x2n; - const signature = await secp.sign(MSG, PRIV_KEY); - const publicKey = secp.getPublicKey(PRIV_KEY); - expect(publicKey.length).toBe(65); - expect(secp.verify(signature, MSG, publicKey)).toBe(true); - }); - it('should not verify signature with wrong public key', async () => { - const MSG = '01'.repeat(32); - const PRIV_KEY = 0x2n; - const WRONG_PRIV_KEY = 0x22n; - const signature = await secp.sign(MSG, PRIV_KEY); - const publicKey = secp.Point.fromPrivateKey(WRONG_PRIV_KEY).toHex(); - expect(publicKey.length).toBe(130); - expect(secp.verify(signature, MSG, publicKey)).toBe(false); - }); - it('should not verify signature with wrong hash', async () => { - const MSG = '01'.repeat(32); - const PRIV_KEY = 0x2n; - const WRONG_MSG = '11'.repeat(32); - const signature = await secp.sign(MSG, PRIV_KEY); - const publicKey = secp.getPublicKey(PRIV_KEY); - expect(publicKey.length).toBe(65); - expect(secp.verify(signature, WRONG_MSG, publicKey)).toBe(false); - }); - it('should verify random signatures', async () => - fc.assert( - fc.asyncProperty( - FC_BIGINT, - fc.hexaString({ minLength: 64, maxLength: 64 }), - async (privKey, msg) => { - const pub = secp.getPublicKey(privKey); - const sig = await secp.sign(msg, privKey); - expect(secp.verify(sig, msg, pub)).toBeTruthy(); - } - ) - )); - it('should not verify signature with invalid r/s', () => { - const msg = new Uint8Array([ - 0xbb, 0x5a, 0x52, 0xf4, 0x2f, 0x9c, 0x92, 0x61, 0xed, 0x43, 0x61, 0xf5, 0x94, 0x22, 0xa1, - 0xe3, 0x00, 0x36, 0xe7, 0xc3, 0x2b, 0x27, 0x0c, 0x88, 0x07, 0xa4, 0x19, 0xfe, 0xca, 0x60, - 0x50, 0x23, - ]); - const x = 100260381870027870612475458630405506840396644859280795015145920502443964769584n; - const y = 41096923727651821103518389640356553930186852801619204169823347832429067794568n; - const r = 1n; - const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n; - - const pub = new secp.Point(x, y); - const signature = new secp.Signature(2n, 2n); - // @ts-ignore - signature.r = r; - // @ts-ignore - signature.s = s; - - const verified = secp.verify(signature, msg, pub); - // Verifies, but it shouldn't, because signature S > curve order - expect(verified).toBeFalsy(); - }); - it('should not verify msg = curve order', async () => { - const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; - const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n; - const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; - const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n; - const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n; - const pub = new secp.Point(x, y); - const sig = new secp.Signature(r, s); - expect(secp.verify(sig, msg, pub)).toBeFalsy(); - }); - it('should verify non-strict msg bb5a...', async () => { - const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023'; - const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n; - const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; - const r = 432420386565659656852420866390673177323n; - const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; - const pub = new secp.Point(x, y); - const sig = new secp.Signature(r, s); - expect(secp.verify(sig, msg, pub, { strict: false })).toBeTruthy(); - }); - it('should not verify invalid deterministic signatures with RFC 6979', () => { - for (const vector of ecdsa.invalid.verify) { - const res = secp.verify(vector.signature, vector.m, vector.Q); - expect(res).toBeFalsy(); - } - }); - }); - - describe('schnorr', () => { - // index,secret key,public key,aux_rand,message,signature,verification result,comment - const vectors = schCsv - .split('\n') - .map((line: string) => line.split(',')) - .slice(1, -1); - for (let vec of vectors) { - const [index, sec, pub, rnd, msg, expSig, passes, comment] = vec; - it(`should sign with Schnorr scheme vector ${index}`, async () => { - if (sec) { - expect(hex(secp.schnorr.getPublicKey(sec))).toBe(pub.toLowerCase()); - const sig = await secp.schnorr.sign(msg, sec, rnd); - const sigS = secp.schnorr.signSync(msg, sec, rnd); - expect(hex(sig)).toBe(expSig.toLowerCase()); - expect(hex(sigS)).toBe(expSig.toLowerCase()); - expect(await secp.schnorr.verify(sigS, msg, pub)).toBe(true); - expect(secp.schnorr.verifySync(sig, msg, pub)).toBe(true); - } else { - const passed = await secp.schnorr.verify(expSig, msg, pub); - const passedS = secp.schnorr.verifySync(expSig, msg, pub); - if (passes === 'TRUE') { - expect(passed).toBeTruthy(); - expect(passedS).toBeTruthy(); - } else { - expect(passed).toBeFalsy(); - expect(passedS).toBeFalsy(); - } - } - }); - } - }); - - describe('.recoverPublicKey()', () => { - it('should recover public key from recovery bit', async () => { - const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; - const privateKey = 123456789n; - const publicKey = secp.Point.fromHex(secp.getPublicKey(privateKey)).toHex(false); - const [signature, recovery] = await secp.sign(message, privateKey, { recovered: true }); - const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery); - expect(recoveredPubkey).not.toBe(null); - expect(hex(recoveredPubkey!)).toBe(publicKey); - expect(secp.verify(signature, message, publicKey)).toBe(true); - }); - it('should not recover zero points', () => { - const msgHash = '6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; - const sig = - '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817986b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; - const recovery = 0; - expect(() => secp.recoverPublicKey(msgHash, sig, recovery)).toThrowError(); - }); - it('should handle all-zeros msghash', async () => { - const privKey = secp.utils.randomPrivateKey(); - const pub = secp.getPublicKey(privKey); - const zeros = '0000000000000000000000000000000000000000000000000000000000000000'; - const [sig, rec] = await secp.sign(zeros, privKey, { recovered: true }); - const recoveredKey = secp.recoverPublicKey(zeros, sig, rec); - expect(recoveredKey).toEqual(pub); - }); - it('should handle RFC 6979 vectors', async () => { - for (const vector of ecdsa.valid) { - if (secp.utils.mod(hexToNumber(vector.m), secp.CURVE.n) === 0n) continue; - let [usig, rec] = await secp.sign(vector.m, vector.d, { der: false, recovered: true }); - let sig = hex(usig); - const vpub = secp.getPublicKey(vector.d); - const recovered = secp.recoverPublicKey(vector.m, sig, rec)!; - expect(hex(recovered)).toBe(hex(vpub)); - } - }); - }); - - describe('.getSharedSecret()', () => { - // TODO: Real implementation. - function derToPub(der: string) { - return der.slice(46); - } - it('should produce correct results', () => { - // TODO: Once der is there, run all tests. - for (const vector of ecdh.testGroups[0].tests.slice(0, 230)) { - if (vector.result === 'invalid' || vector.private.length !== 64) { - expect(() => { - secp.getSharedSecret(vector.private, derToPub(vector.public), true); - }).toThrowError(); - } else if (vector.result === 'valid') { - const res = secp.getSharedSecret(vector.private, derToPub(vector.public), true); - expect(hex(res.slice(1))).toBe(`${vector.shared}`); - } - } - }); - it('priv/pub order matters', () => { - for (const vector of ecdh.testGroups[0].tests.slice(0, 100)) { - if (vector.result === 'valid') { - let priv = vector.private; - priv = priv.length === 66 ? priv.slice(2) : priv; - expect(() => secp.getSharedSecret(derToPub(vector.public), priv, true)).toThrowError(); - } - } - }); - it('rejects invalid keys', () => { - expect(() => secp.getSharedSecret('01', '02')).toThrowError(); - }); - }); - - describe('utils', () => { - it('isValidPrivateKey()', () => { - for (const vector of privates.valid.isPrivate) { - const { d, expected } = vector; - expect(secp.utils.isValidPrivateKey(d)).toBe(expected); - } - }); - const normal = secp.utils._normalizePrivateKey; - type Hex = string | Uint8Array; - type PrivKey = Hex | bigint | number; - const tweakUtils = { - privateAdd: (privateKey: PrivKey, tweak: Hex): Uint8Array => { - const p = normal(privateKey); - const t = normal(tweak); - return secp.utils._bigintTo32Bytes(secp.utils.mod(p + t, secp.CURVE.n)); - }, - - privateNegate: (privateKey: PrivKey): Uint8Array => { - const p = normal(privateKey); - return secp.utils._bigintTo32Bytes(secp.CURVE.n - p); - }, - - pointAddScalar: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => { - const P = secp.Point.fromHex(p); - const t = normal(tweak); - const Q = secp.Point.BASE.multiplyAndAddUnsafe(P, t, 1n); - if (!Q) throw new Error('Tweaked point at infinity'); - return Q.toRawBytes(isCompressed); - }, - - pointMultiply: (p: Hex, tweak: Hex, isCompressed?: boolean): Uint8Array => { - const P = secp.Point.fromHex(p); - const h = typeof tweak === 'string' ? tweak : secp.utils.bytesToHex(tweak); - const t = BigInt(`0x${h}`); - return P.multiply(t).toRawBytes(isCompressed); - }, - }; - - it('privateAdd()', () => { - for (const vector of privates.valid.add) { - const { a, b, expected } = vector; - expect(secp.utils.bytesToHex(tweakUtils.privateAdd(a, b))).toBe(expected); - } - }); - it('privateNegate()', () => { - for (const vector of privates.valid.negate) { - const { a, expected } = vector; - expect(secp.utils.bytesToHex(tweakUtils.privateNegate(a))).toBe(expected); - } - }); - it('pointAddScalar()', () => { - for (const vector of points.valid.pointAddScalar) { - const { description, P, d, expected } = vector; - const compressed = !!expected && expected.length === 66; // compressed === 33 bytes - expect(secp.utils.bytesToHex(tweakUtils.pointAddScalar(P, d, compressed))).toBe(expected); - } - }); - it('pointAddScalar() invalid', () => { - for (const vector of points.invalid.pointAddScalar) { - const { P, d, exception } = vector; - expect(() => tweakUtils.pointAddScalar(P, d)).toThrowError(RegExp(`${exception}`)); - } - }); - it('pointMultiply()', () => { - for (const vector of points.valid.pointMultiply) { - const { P, d, expected } = vector; - expect(secp.utils.bytesToHex(tweakUtils.pointMultiply(P, d, true))).toBe(expected); - } - }); - it('pointMultiply() invalid', () => { - for (const vector of points.invalid.pointMultiply) { - const { P, d, exception } = vector; - expect(() => tweakUtils.pointMultiply(P, d)).toThrowError(RegExp(`${exception}`)); - } - }); - }); - - describe('wychenproof vectors', () => { - it('should pass all tests', async () => { - for (let group of wp.testGroups) { - const pubKey = secp.Point.fromHex(group.key.uncompressed); - for (let test of group.tests) { - const m = await secp.utils.sha256(hexToBytes(test.msg)); - if (test.result === 'valid' || test.result === 'acceptable') { - const verified = secp.verify(test.sig, m, pubKey); - if (secp.Signature.fromDER(test.sig).hasHighS()) { - expect(verified).toBeFalsy(); - } else { - expect(verified).toBeTruthy(); - } - } else if (test.result === 'invalid') { - let failed = false; - try { - const verified = secp.verify(test.sig, m, pubKey); - if (!verified) failed = true; - } catch (error) { - failed = true; - } - expect(failed).toBeTruthy(); - } else { - expect(false).toBeTruthy(); - } - } - } - }); - }); -}); - -describe('JacobianPoint', () => { - const JZERO = secp.utils._JacobianPoint.ZERO; - const AZERO = secp.utils._JacobianPoint.fromAffine(secp.Point.ZERO); - expect(AZERO.equals(JZERO)).toBeTruthy(); - expect(AZERO.toAffine().equals(JZERO.toAffine())).toBeTruthy(); -}); diff --git a/test/secp256k1.helpers.mjs b/test/secp256k1.helpers.mjs new file mode 100644 index 0000000..79b3ee7 --- /dev/null +++ b/test/secp256k1.helpers.mjs @@ -0,0 +1,73 @@ +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; // @ts-ignore +// @ts-ignore +export * as secp from '../index.js'; +// @ts-ignore +import * as secp256k1 from '../index.js'; +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from '@noble/hashes/sha256'; +secp256k1.etc.hmacSha256Sync = (key, ...msgs) => hmac(sha256, key, secp256k1.etc.concatBytes(...msgs)); +const { bytesToNumberBE: b2n, hexToBytes: h2b } = secp256k1.etc; +export const DER = { + // asn.1 DER encoding utils + Err: class DERErr extends Error { + constructor(m = '') { + super(m); + } + }, + _parseInt(data) { + const { Err: E } = DER; + if (data.length < 2 || data[0] !== 0x02) + throw new E('Invalid signature integer tag'); + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) + throw new E('Invalid signature integer: wrong length'); + if (res[0] === 0x00 && res[1] <= 0x7f) + throw new E('Invalid signature integer: trailing length'); + // ^ Weird condition: not about length, but about first bytes of number. + return { d: b2n(res), l: data.subarray(len + 2) }; // d is data, l is left + }, + toSig(hex) { + // parse DER signature + const { Err: E } = DER; + const data = typeof hex === 'string' ? h2b(hex) : hex; + if (!(data instanceof Uint8Array)) + throw new Error('ui8a expected'); + let l = data.length; + if (l < 2 || data[0] != 0x30) + throw new E('Invalid signature tag'); + if (data[1] !== l - 2) + throw new E('Invalid signature: incorrect length'); + const { d: r, l: sBytes } = DER._parseInt(data.subarray(2)); + const { d: s, l: rBytesLeft } = DER._parseInt(sBytes); + if (rBytesLeft.length) + throw new E('Invalid signature: left bytes after parsing'); + return { r, s }; + }, + hexFromSig(sig) { + const slice = (s) => (Number.parseInt(s[0], 16) >= 8 ? '00' + s : s); // slice DER + const h = (num) => { + const hex = num.toString(16); + return hex.length & 1 ? `0${hex}` : hex; + }; + const s = slice(h(sig.s)); + const r = slice(h(sig.r)); + const shl = s.length / 2; + const rhl = r.length / 2; + const sl = h(shl); + const rl = h(rhl); + return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; + }, +}; +export const sigFromDER = (der) => { + const { r, s } = DER.toSig(der); + return new secp256k1.Signature(r, s); +}; +export const sigToDER = (sig) => DER.hexFromSig(sig); +export const selectHash = (secp) => sha256; +export const normVerifySig = (s) => DER.toSig(s); +export const bytesToNumberBE = secp256k1.etc.bytesToNumberBE; +export const numberToBytesBE = secp256k1.etc.numberToBytesBE; +export const mod = secp256k1.etc.mod; diff --git a/test/secp256k1.helpers.mts b/test/secp256k1.helpers.mts new file mode 100644 index 0000000..52f526e --- /dev/null +++ b/test/secp256k1.helpers.mts @@ -0,0 +1,69 @@ +import { webcrypto } from 'node:crypto'; +// @ts-ignore +if (!globalThis.crypto) globalThis.crypto = webcrypto; // @ts-ignore +// @ts-ignore +export * as secp from '../index.js'; +// @ts-ignore +import * as secp256k1 from '../index.js'; +import { hmac } from '@noble/hashes/hmac'; +import { sha256 } from '@noble/hashes/sha256'; +secp256k1.etc.hmacSha256Sync = (key: Uint8Array, ...msgs: Uint8Array[]) => hmac(sha256, key, secp256k1.etc.concatBytes(...msgs)) + +const { bytesToNumberBE: b2n, hexToBytes: h2b } = secp256k1.etc; +export const DER = { + // asn.1 DER encoding utils + Err: class DERErr extends Error { + constructor(m = '') { + super(m); + } + }, + _parseInt(data: Uint8Array): { d: bigint; l: Uint8Array } { + const { Err: E } = DER; + if (data.length < 2 || data[0] !== 0x02) throw new E('Invalid signature integer tag'); + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) throw new E('Invalid signature integer: wrong length'); + if (res[0] === 0x00 && res[1] <= 0x7f) + throw new E('Invalid signature integer: trailing length'); + // ^ Weird condition: not about length, but about first bytes of number. + return { d: b2n(res), l: data.subarray(len + 2) }; // d is data, l is left + }, + toSig(hex: string | Uint8Array): { r: bigint; s: bigint } { + // parse DER signature + const { Err: E } = DER; + const data = typeof hex === 'string' ? h2b(hex) : hex; + if (!(data instanceof Uint8Array)) throw new Error('ui8a expected'); + let l = data.length; + if (l < 2 || data[0] != 0x30) throw new E('Invalid signature tag'); + if (data[1] !== l - 2) throw new E('Invalid signature: incorrect length'); + const { d: r, l: sBytes } = DER._parseInt(data.subarray(2)); + const { d: s, l: rBytesLeft } = DER._parseInt(sBytes); + if (rBytesLeft.length) throw new E('Invalid signature: left bytes after parsing'); + return { r, s }; + }, + hexFromSig(sig: { r: bigint; s: bigint }): string { + const slice = (s: string): string => (Number.parseInt(s[0], 16) >= 8 ? '00' + s : s); // slice DER + const h = (num: number | bigint) => { + const hex = num.toString(16); + return hex.length & 1 ? `0${hex}` : hex; + }; + const s = slice(h(sig.s)); + const r = slice(h(sig.r)); + const shl = s.length / 2; + const rhl = r.length / 2; + const sl = h(shl); + const rl = h(rhl); + return `30${h(rhl + shl + 4)}02${rl}${r}02${sl}${s}`; + }, +}; + +export const sigFromDER = (der: string | Uint8Array) => { + const { r, s } = DER.toSig(der); + return new secp256k1.Signature(r, s); +}; +export const sigToDER = (sig: any) => DER.hexFromSig(sig); +export const selectHash = (secp: any) => sha256; +export const normVerifySig = (s: any) => DER.toSig(s); +export const bytesToNumberBE = secp256k1.etc.bytesToNumberBE; +export const numberToBytesBE = secp256k1.etc.numberToBytesBE; +export const mod = secp256k1.etc.mod; diff --git a/test/secp256k1.test.mjs b/test/secp256k1.test.mjs new file mode 100644 index 0000000..87a8fb3 --- /dev/null +++ b/test/secp256k1.test.mjs @@ -0,0 +1,536 @@ +import { hexToBytes, bytesToHex as hex } from '@noble/hashes/utils'; +import { deepStrictEqual, throws } from 'assert'; +import * as fc from 'fast-check'; +import { readFileSync } from 'fs'; +import { should, describe } from 'micro-should'; +// prettier-ignore +import { + secp, sigFromDER, sigToDER, selectHash, normVerifySig, mod, bytesToNumberBE, numberToBytesBE +} from './secp256k1.helpers.mjs'; + +import { default as ecdsa } from './vectors/ecdsa.json' assert { type: 'json' }; +import { default as ecdh } from './vectors/ecdh.json' assert { type: 'json' }; +import { default as privates } from './vectors/privates.json' assert { type: 'json' }; +import { default as points } from './vectors/points.json' assert { type: 'json' }; +import { default as wp } from './vectors/wychenproof.json' assert { type: 'json' }; + +const Point = secp.ProjectivePoint; +const privatesTxt = readFileSync('./test/vectors/privates-2.txt', 'utf-8'); + +const FC_BIGINT = fc.bigInt(1n + 1n, secp.CURVE.n - 1n); +// prettier-ignore +const INVALID_ITEMS = ['deadbeef', Math.pow(2, 53), [1], 'xyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxyxyzxyzxy', secp.CURVE.n + 2n]; + +const toBEHex = (n) => n.toString(16).padStart(64, '0'); + +function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new Error('hexToNumber: expected string, got ' + typeof hex); + } + // Big Endian + return BigInt(`0x${hex}`); +} + +describe('secp256k1', () => { + should('getPublicKey()', () => { + const data = privatesTxt + .split('\n') + .filter((line) => line) + .map((line) => line.split(':')); + for (let [priv, x, y] of data) { + const point = Point.fromPrivateKey(BigInt(priv)); + deepStrictEqual(toBEHex(point.x), x); + deepStrictEqual(toBEHex(point.y), y); + + const point2 = Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); + deepStrictEqual(toBEHex(point2.x), x); + deepStrictEqual(toBEHex(point2.y), y); + + const point3 = Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); + deepStrictEqual(toBEHex(point3.x), x); + deepStrictEqual(toBEHex(point3.y), y); + } + }); + should('getPublicKey() rejects invalid keys', () => { + for (const item of INVALID_ITEMS) { + throws(() => secp.getPublicKey(item)); + } + }); + should('precompute', () => { + secp.utils.precompute(4); + const data = privatesTxt + .split('\n') + .filter((line) => line) + .map((line) => line.split(':')); + for (let [priv, x, y] of data) { + const point = Point.fromPrivateKey(BigInt(priv)); + deepStrictEqual(toBEHex(point.x), x); + deepStrictEqual(toBEHex(point.y), y); + + const point2 = Point.fromHex(secp.getPublicKey(toBEHex(BigInt(priv)))); + deepStrictEqual(toBEHex(point2.x), x); + deepStrictEqual(toBEHex(point2.y), y); + + const point3 = Point.fromHex(secp.getPublicKey(hexToBytes(toBEHex(BigInt(priv))))); + deepStrictEqual(toBEHex(point3.x), x); + deepStrictEqual(toBEHex(point3.y), y); + } + }); + + describe('Point', () => { + should('fromHex() assertValidity', () => { + for (const vector of points.valid.isPoint) { + const { P, expected } = vector; + if (expected) { + Point.fromHex(P); + } else { + throws(() => Point.fromHex(P)); + } + } + }); + + should('.fromPrivateKey()', () => { + for (const vector of points.valid.pointFromScalar) { + const { d, expected } = vector; + let p = Point.fromPrivateKey(d); + deepStrictEqual(p.toHex(true), expected); + } + }); + + should('#toHex(compressed)', () => { + for (const vector of points.valid.pointCompress) { + const { P, compress, expected } = vector; + let p = Point.fromHex(P); + deepStrictEqual(p.toHex(compress), expected); + } + }); + + should('#toHex() roundtrip (failed case)', () => { + const point1 = + Point.fromPrivateKey( + 88572218780422190464634044548753414301110513745532121983949500266768436236425n + ); + // const hex = point1.toHex(true); + // deepStrictEqual(Point.fromHex(hex).toHex(true), hex); + }); + + should('#toHex() roundtrip', () => { + fc.assert( + fc.property(FC_BIGINT, (x) => { + const point1 = Point.fromPrivateKey(x); + const hex = point1.toHex(true); + deepStrictEqual(Point.fromHex(hex).toHex(true), hex); + }) + ); + }); + + should('#add(other)', () => { + for (const vector of points.valid.pointAdd) { + const { P, Q, expected } = vector; + let p = Point.fromHex(P); + let q = Point.fromHex(Q); + if (expected) { + deepStrictEqual(p.add(q).toHex(true), expected); + } else { + if (!p.equals(q.negate())) { + throws(() => p.add(q).toHex(true)); + } + } + } + }); + + should('#multiply(privateKey)', () => { + for (const vector of points.valid.pointMultiply) { + const { P, d, expected } = vector; + const p = Point.fromHex(P); + if (expected) { + deepStrictEqual(p.multiply(hexToNumber(d)).toHex(true), expected, P); + } else { + throws(() => { + p.multiply(hexToNumber(d)).toHex(true); + }); + } + } + + for (const vector of points.invalid.pointMultiply) { + const { P, d } = vector; + if (hexToNumber(d) < secp.CURVE.n) { + throws(() => { + const p = Point.fromHex(P); + p.multiply(hexToNumber(d)).toHex(true); + }); + } + } + for (const num of [0n, 0, -1n, -1, 1.1]) { + throws(() => Point.BASE.multiply(num)); + } + }); + }); + + // multiply() should equal multiplyUnsafe() + // should('ProjectivePoint#multiplyUnsafe', () => { + // const p0 = new secp.ProjectivePoint( + // 55066263022277343669578718895168534326250603453777594175500187360389116729240n, + // 32670510020758816978083085130507043184471273380659243275938904335757337482424n, + // 1n + // ); + // const z = 106011723082030650010038151861333186846790370053628296836951575624442507889495n; + // console.log(p0.multiply(z)); + // console.log(secp.ProjectivePoint.normalizeZ([p0.multiplyUnsafe(z)])[0]) + // }); + describe('Signature', () => { + should('.fromCompactHex() roundtrip', () => { + fc.assert( + fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { + const sig = new secp.Signature(r, s); + deepStrictEqual(secp.Signature.fromCompact(sig.toCompactHex()), sig); + }) + ); + }); + + should('.fromDERHex() roundtrip', () => { + fc.assert( + fc.property(FC_BIGINT, FC_BIGINT, (r, s) => { + const sig = new secp.Signature(r, s); + deepStrictEqual(sigFromDER(sigToDER(sig)), sig); + }) + ); + }); + }); + + describe('sign()', () => { + should('create deterministic signatures with RFC 6979', () => { + for (const vector of ecdsa.valid) { + let usig = secp.sign(vector.m, vector.d); + let sig = usig.toCompactHex(); + const vsig = vector.signature; + deepStrictEqual(sig.slice(0, 64), vsig.slice(0, 64)); + deepStrictEqual(sig.slice(64, 128), vsig.slice(64, 128)); + } + }); + + should('not create invalid deterministic signatures with RFC 6979', () => { + for (const vector of ecdsa.invalid.sign) { + throws(() => secp.sign(vector.m, vector.d)); + } + }); + + should('edge cases', () => { + throws(() => secp.sign()); + throws(() => secp.sign('')); + }); + + should('create correct DER encoding against libsecp256k1', () => { + const CASES = [ + [ + 'd1a9dc8ed4e46a6a3e5e594615ca351d7d7ef44df1e4c94c1802f3592183794b', + '304402203de2559fccb00c148574997f660e4d6f40605acc71267ee38101abf15ff467af02200950abdf40628fd13f547792ba2fc544681a485f2fdafb5c3b909a4df7350e6b', + ], + [ + '5f97983254982546d3976d905c6165033976ee449d300d0e382099fa74deaf82', + '3045022100c046d9ff0bd2845b9aa9dff9f997ecebb31e52349f80fe5a5a869747d31dcb88022011f72be2a6d48fe716b825e4117747b397783df26914a58139c3f4c5cbb0e66c', + ], + [ + '0d7017a96b97cd9be21cf28aada639827b2814a654a478c81945857196187808', + '3045022100d18990bba7832bb283e3ecf8700b67beb39acc73f4200ed1c331247c46edccc602202e5c8bbfe47ae159512c583b30a3fa86575cddc62527a03de7756517ae4c6c73', + ], + ]; + const privKey = hexToBytes( + '0101010101010101010101010101010101010101010101010101010101010101' + ); + for (const [msg, exp] of CASES) { + const res = secp.sign(msg, privKey, { extraEntropy: undefined }); + deepStrictEqual(sigToDER(res), exp); + const rs = sigFromDER(sigToDER(res)).toCompactHex(); + deepStrictEqual(sigToDER(secp.Signature.fromCompact(rs)), exp); + } + }); + should('handle {extraData} option', () => { + const ent1 = '0000000000000000000000000000000000000000000000000000000000000000'; + const ent2 = '0000000000000000000000000000000000000000000000000000000000000001'; + const ent3 = '6e723d3fd94ed5d2b6bdd4f123364b0f3ca52af829988a63f8afe91d29db1c33'; + const ent4 = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; + const ent5 = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; + + for (const e of ecdsa.extraEntropy) { + const sign = (extraEntropy) => { + const s = secp.sign(e.m, e.d, { extraEntropy }).toCompactHex(); + return s; + }; + deepStrictEqual(sign(), e.signature); + deepStrictEqual(sign(ent1), e.extraEntropy0); + deepStrictEqual(sign(ent2), e.extraEntropy1); + deepStrictEqual(sign(ent3), e.extraEntropyRand); + deepStrictEqual(sign(ent4), e.extraEntropyN); + deepStrictEqual(sign(ent5), e.extraEntropyMax); + } + }); + }); + + describe('verify()', () => { + should('verify signature', () => { + const MSG = '01'.repeat(32); + const PRIV_KEY = 0x2n; + const signature = secp.sign(MSG, PRIV_KEY); + const publicKey = secp.getPublicKey(PRIV_KEY); + deepStrictEqual(publicKey.length, 33); + deepStrictEqual(secp.verify(signature, MSG, publicKey), true); + }); + should(' not verify signature with wrong public key', () => { + const MSG = '01'.repeat(32); + const PRIV_KEY = '01'.repeat(32); + const WRONG_PRIV_KEY = '02'.repeat(32); + const signature = secp.sign(MSG, PRIV_KEY); + const publicKey = Point.fromPrivateKey(WRONG_PRIV_KEY).toHex(); + deepStrictEqual(publicKey.length, 66); + deepStrictEqual(secp.verify(signature, MSG, publicKey), false); + }); + should('not verify signature with wrong hash', () => { + const MSG = '01'.repeat(32); + const PRIV_KEY = 0x2n; + const WRONG_MSG = '11'.repeat(32); + const signature = secp.sign(MSG, PRIV_KEY); + const publicKey = secp.getPublicKey(PRIV_KEY); + deepStrictEqual(publicKey.length, 33); + deepStrictEqual(secp.verify(signature, WRONG_MSG, publicKey), false); + }); + should('verify random signatures', () => + fc.assert( + fc.property(FC_BIGINT, fc.hexaString({ minLength: 64, maxLength: 64 }), (privKey, msg) => { + const pub = secp.getPublicKey(privKey); + const sig = secp.sign(msg, privKey); + deepStrictEqual(secp.verify(sig, msg, pub), true); + }) + ) + ); + should('not verify signature with invalid r/s', () => { + const msg = new Uint8Array([ + 0xbb, 0x5a, 0x52, 0xf4, 0x2f, 0x9c, 0x92, 0x61, 0xed, 0x43, 0x61, 0xf5, 0x94, 0x22, 0xa1, + 0xe3, 0x00, 0x36, 0xe7, 0xc3, 0x2b, 0x27, 0x0c, 0x88, 0x07, 0xa4, 0x19, 0xfe, 0xca, 0x60, + 0x50, 0x23, + ]); + const x = 100260381870027870612475458630405506840396644859280795015145920502443964769584n; + const y = 41096923727651821103518389640356553930186852801619204169823347832429067794568n; + const r = 1n; + const s = 115792089237316195423570985008687907852837564279074904382605163141518162728904n; + + const pub = new Point(x, y, 1n).toRawBytes(); + const signature = new secp.Signature(2n, 2n); + signature.r = r; + signature.s = s; + + const verified = secp.verify(signature, msg, pub); + // Verifies, but it shouldn't, because signature S > curve order + deepStrictEqual(verified, false); + }); + should('not verify msg = curve order', () => { + const msg = 'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'; + const x = 55066263022277343669578718895168534326250603453777594175500187360389116729240n; + const y = 32670510020758816978083085130507043184471273380659243275938904335757337482424n; + const r = 104546003225722045112039007203142344920046999340768276760147352389092131869133n; + const s = 96900796730960181123786672629079577025401317267213807243199432755332205217369n; + const pub = new Point(x, y, 1n).toRawBytes(); + const sig = new secp.Signature(r, s); + deepStrictEqual(secp.verify(sig, msg, pub), false); + }); + should('verify non-strict msg bb5a...', () => { + const msg = 'bb5a52f42f9c9261ed4361f59422a1e30036e7c32b270c8807a419feca605023'; + const x = 3252872872578928810725465493269682203671229454553002637820453004368632726370n; + const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; + const r = 432420386565659656852420866390673177323n; + const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; + const pub = new Point(x, y, 1n).toRawBytes(); + const sig = new secp.Signature(r, s); + deepStrictEqual(secp.verify(sig, msg, pub, { lowS: false }), true); + }); + should('not verify invalid deterministic signatures with RFC 6979', () => { + for (const vector of ecdsa.invalid.verify) { + const res = secp.verify(vector.signature, vector.m, vector.Q); + deepStrictEqual(res, false); + } + }); + }); + describe('recoverPublicKey()', () => { + should('recover public key from recovery bit', () => { + const message = '00000000000000000000000000000000000000000000000000000000deadbeef'; + const privateKey = 123456789n; + const publicKey = Point.fromHex(secp.getPublicKey(privateKey)).toHex(false); + const sig = secp.sign(message, privateKey); + const recoveredPubkey = sig.recoverPublicKey(message); + // const recoveredPubkey = secp.recoverPublicKey(message, signature, recovery); + deepStrictEqual(recoveredPubkey !== null, true); + deepStrictEqual(recoveredPubkey.toHex(false), publicKey); + deepStrictEqual(secp.verify(sig, message, publicKey), true); + }); + should('not recover zero points', () => { + const msgHash = '6b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; + const sig = + '79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817986b8d2c81b11b2d699528dde488dbdf2f94293d0d33c32e347f255fa4a6c1f0a9'; + const recovery = 0; + throws(() => secp.recoverPublicKey(msgHash, sig, recovery)); + }); + should('handle all-zeros msghash', () => { + const privKey = secp.utils.randomPrivateKey(); + const pub = secp.getPublicKey(privKey); + const zeros = '0000000000000000000000000000000000000000000000000000000000000000'; + const sig = secp.sign(zeros, privKey); + const recoveredKey = sig.recoverPublicKey(zeros); + deepStrictEqual(recoveredKey.toRawBytes(), pub); + }); + should('handle RFC 6979 vectors', () => { + for (const vector of ecdsa.valid) { + let usig = secp.sign(vector.m, vector.d); + let sig = sigToDER(usig); + const vpub = secp.getPublicKey(vector.d); + const recovered = usig.recoverPublicKey(vector.m); + deepStrictEqual(recovered.toHex(), hex(vpub)); + } + }); + }); + + describe('getSharedSecret()', () => { + // TODO: Real implementation. + function derToPub(der) { + return der.slice(46); + } + should('produce correct results', () => { + // TODO: Once der is there, run all tests. + for (const vector of ecdh.testGroups[0].tests.slice(0, 230)) { + if (vector.result === 'invalid' || vector.private.length !== 64) { + throws(() => { + secp.getSharedSecret(vector.private, derToPub(vector.public), true); + }); + } else if (vector.result === 'valid') { + const res = secp.getSharedSecret(vector.private, derToPub(vector.public), true); + deepStrictEqual(hex(res.slice(1)), `${vector.shared}`); + } + } + }); + should('priv/pub order matters', () => { + for (const vector of ecdh.testGroups[0].tests.slice(0, 100)) { + if (vector.result === 'valid') { + let priv = vector.private; + priv = priv.length === 66 ? priv.slice(2) : priv; + throws(() => secp.getSharedSecret(derToPub(vector.public), priv, true)); + } + } + }); + should('reject invalid keys', () => { + throws(() => secp.getSharedSecret('01', '02')); + }); + }); + + should('utils.isValidPrivateKey()', () => { + for (const vector of privates.valid.isPrivate) { + const { d, expected } = vector; + deepStrictEqual(secp.utils.isValidPrivateKey(d), expected); + } + }); + should('have proper curve equation in assertValidity()', () => { + throws(() => { + const { Fp } = secp.CURVE; + let point = new Point(Fp.create(-2n), Fp.create(-1n), Fp.create(1n)); + point.assertValidity(); + }); + }); + + describe('tweak utilities (legacy)', () => { + const normal = secp.utils.normPrivateKeyToScalar; + const tweakUtils = { + privateAdd: (privateKey, tweak) => { + return numberToBytesBE(mod(normal(privateKey) + normal(tweak), secp.CURVE.n), 32); + }, + + privateNegate: (privateKey) => { + return numberToBytesBE(mod(-normal(privateKey), secp.CURVE.n), 32); + }, + + pointAddScalar: (p, tweak, isCompressed) => { + const tweaked = Point.fromHex(p).add(Point.fromPrivateKey(tweak)); + if (tweaked.equals(Point.ZERO)) throw new Error('Tweaked point at infinity'); + return tweaked.toRawBytes(isCompressed); + }, + + pointMultiply: (p, tweak, isCompressed) => { + if (typeof tweak === 'string') tweak = hexToBytes(tweak); + const t = bytesToNumberBE(tweak); + return Point.fromHex(p).multiply(t).toRawBytes(isCompressed); + }, + }; + + should('privateAdd()', () => { + for (const vector of privates.valid.add) { + const { a, b, expected } = vector; + deepStrictEqual(hex(tweakUtils.privateAdd(a, b)), expected); + } + }); + should('privateNegate()', () => { + for (const vector of privates.valid.negate) { + const { a, expected } = vector; + deepStrictEqual(hex(tweakUtils.privateNegate(a)), expected); + } + }); + should('pointAddScalar()', () => { + for (const vector of points.valid.pointAddScalar) { + const { description, P, d, expected } = vector; + const compressed = !!expected && expected.length === 66; // compressed === 33 bytes + deepStrictEqual(hex(tweakUtils.pointAddScalar(P, d, compressed)), expected); + } + }); + should('pointAddScalar() invalid', () => { + for (const vector of points.invalid.pointAddScalar) { + const { P, d, exception } = vector; + throws(() => tweakUtils.pointAddScalar(P, d)); + } + }); + should('pointMultiply()', () => { + for (const vector of points.valid.pointMultiply) { + const { P, d, expected } = vector; + deepStrictEqual(hex(tweakUtils.pointMultiply(P, d, true)), expected); + } + }); + should('pointMultiply() invalid', () => { + for (const vector of points.invalid.pointMultiply) { + const { P, d, exception } = vector; + throws(() => tweakUtils.pointMultiply(P, d)); + } + }); + }); + + should('wycheproof vectors', () => { + for (let group of wp.testGroups) { + // const pubKey = Point.fromHex().toRawBytes(); + const pubKey = group.key.uncompressed; + for (let test of group.tests) { + const h = selectHash(secp); + + const m = h(hexToBytes(test.msg)); + if (test.result === 'valid' || test.result === 'acceptable') { + const verified = secp.verify(normVerifySig(test.sig), m, pubKey); + if (sigFromDER(test.sig).hasHighS()) { + deepStrictEqual(verified, false); + } else { + deepStrictEqual(verified, true); + } + } else if (test.result === 'invalid') { + let failed = false; + try { + const verified = secp.verify(test.sig, m, pubKey); + if (!verified) failed = true; + } catch (error) { + failed = true; + } + deepStrictEqual(failed, true); + } else { + deepStrictEqual(false, true); + } + } + } + }); +}); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + should.run(); +} diff --git a/test/tsconfig.json b/test/tsconfig.json index 14e868a..12e568c 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -2,12 +2,12 @@ "compilerOptions": { "strict": true, "target": "es2020", - "module": "commonjs", + "module": "es6", + "moduleResolution": "node16", "noImplicitAny": true, - "removeComments": true, "preserveConstEnums": true, "resolveJsonModule": true }, - "include": ["*.ts"], + "include": ["secp256k1.helpers.mts"], "exclude": ["node_modules"] } diff --git a/tsconfig.esm.json b/tsconfig.esm.json deleted file mode 100644 index 8fc2504..0000000 --- a/tsconfig.esm.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "target": "es2020", - "lib": ["es2020"], - "module": "es6", - "moduleResolution": "node", - "outDir": "lib/esm", - "noImplicitAny": true, - "removeComments": true, - "preserveConstEnums": true, - "baseUrl": ".", - }, - "include": ["index.ts"], - "exclude": ["node_modules", "lib"] -} diff --git a/tsconfig.json b/tsconfig.json index 9c457a7..4edd974 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,13 +5,11 @@ "declarationMap": false, "target": "es2020", "lib": ["es2020"], - "module": "commonjs", + "module": "es6", "moduleResolution": "node", - "outDir": "lib", + "outDir": ".", "noImplicitAny": true, - "removeComments": true, "preserveConstEnums": true, - "resolveJsonModule": true, "baseUrl": ".", }, "include": ["index.ts"],