From 711401d5468acf14dba099172ec95d2c66329081 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Mon, 10 Apr 2023 19:43:22 +0000 Subject: [PATCH 1/5] Switch from noble-secp256k1 to noble-curves --- README.md | 25 ++++++------- package.json | 18 +++++----- src/aes.ts | 4 ++- src/secp256k1-compat.ts | 66 +++++++++++++++++----------------- src/secp256k1.ts | 25 ++----------- test/test-vectors/hdkey.ts | 2 +- test/test-vectors/secp256k1.ts | 8 ++--- 7 files changed, 66 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index d6c1869..90d42cc 100644 --- a/README.md +++ b/README.md @@ -167,31 +167,29 @@ console.log(getRandomBytesSync(32)); ## secp256k1 curve ```ts -function getPublicKey(privateKey: Uint8Array, isCompressed?: false): Uint8Array; -function getSharedSecret(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array; -function sign(msgHash: Uint8Array, privateKey: Uint8Array, opts?: Options): Promise; -function signSync(msgHash: Uint8Array, privateKey: Uint8Array, opts?: Options): Uint8Array; +function getPublicKey(privateKey: Uint8Array, isCompressed = true): Uint8Array; +function sign(msgHash: Uint8Array, privateKey: Uint8Array): { r: bigint; s: bigint; recovery: number }; function verify(signature: Uint8Array, msgHash: Uint8Array, publicKey: Uint8Array): boolean -function recoverPublicKey(msgHash: Uint8Array, signature: Uint8Array, recovery: number): Uint8Array | undefined; +function getSharedSecret(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array; function utils.randomPrivateKey(): Uint8Array; ``` The `secp256k1` submodule provides a library for elliptic curve operations on -the curve secp256k1. For detailed documentation, follow [README of `noble-secp256k1`](https://github.com/paulmillr/noble-secp256k1), which the module uses as a backend. +the curve secp256k1. For detailed documentation, follow [README of `noble-curves`](https://github.com/paulmillr/noble-curves), which the module uses as a backend. secp256k1 private keys need to be cryptographically secure random numbers with certain caracteristics. If this is not the case, the security of secp256k1 is compromised. We strongly recommend using `utils.randomPrivateKey()` to generate them. ```js -const secp = require("ethereum-cryptography/secp256k1"); +const {secp256k1} = require("ethereum-cryptography/secp256k1"); (async () => { // You pass either a hex string, or Uint8Array const privateKey = "6b911fd37cdf5c81d4c0adb1ab7fa822ed253ab0ad9aa18d77257c88b29b718e"; const messageHash = "a33321f98e4ff1c283c76998f14f57447545d339b3db534c6d886decb4209f28"; - const publicKey = secp.getPublicKey(privateKey); - const signature = await secp.sign(messageHash, privateKey); - const isSigned = secp.verify(signature, messageHash, publicKey); + const publicKey = secp256k1.getPublicKey(privateKey); + const signature = secp256k1.sign(messageHash, privateKey); + const isSigned = secp256k1.verify(signature, messageHash, publicKey); })(); ``` @@ -448,9 +446,12 @@ you found another primitive that is missing. ## Upgrading -Version 1.0 changes from 0.1: +Upgrading from 1.0 to 2.0: `secp256k1` module was changed massively: +before, it was using [noble-secp256k1 1.7](https://github.com/paulmillr/noble-secp256k1); +now it uses safer [noble-curves](https://github.com/paulmillr/noble-curves). Please refer +to [upgrading section from curves README](https://github.com/paulmillr/noble-curves#upgrading). -**Same functionality**, all old APIs remain the same except for the breaking changes: +Upgrading from 0.1 to 1.0: **Same functionality**, all old APIs remain the same except for the breaking changes: 1. We return `Uint8Array` from all methods that worked with `Buffer` before. `Buffer` has never been supported in browsers, while `Uint8Array`s are supported natively in both diff --git a/package.json b/package.json index 3b3b371..4b5aeb5 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "*.d.ts" ], "dependencies": { - "@noble/hashes": "1.2.0", - "@noble/secp256k1": "1.7.1", - "@scure/bip32": "1.1.5", - "@scure/bip39": "1.1.1" + "@noble/curves": "0.9.1", + "@noble/hashes": "1.3.0", + "@scure/bip32": "1.2.0", + "@scure/bip39": "1.2.0" }, "browser": { "crypto": false @@ -53,13 +53,13 @@ "devDependencies": { "@rollup/plugin-commonjs": "22.0.1", "@rollup/plugin-node-resolve": "13.3.0", - "@types/estree": "0.0.47", + "@types/estree": "1.0.0", "@types/mocha": "9.1.1", - "@types/node": "18.0.4", + "@types/node": "18.15.11", "@typescript-eslint/eslint-plugin": "5.30.6", "@typescript-eslint/parser": "5.30.6", "browserify": "17.0.0", - "eslint": "8.19.0", + "eslint": "8.38.0", "eslint-plugin-prettier": "4.2.1", "karma": "6.4.0", "karma-chrome-launcher": "3.1.1", @@ -72,8 +72,8 @@ "rimraf": "~3.0.2", "rollup": "2.76.0", "ts-node": "10.9.1", - "typescript": "4.7.3", - "webpack": "5.73.0", + "typescript": "5.0.2", + "webpack": "5.76.0", "webpack-cli": "4.10" }, "keywords": [ diff --git a/src/aes.ts b/src/aes.ts index 5bfb4ad..fd8c66e 100644 --- a/src/aes.ts +++ b/src/aes.ts @@ -1,6 +1,8 @@ -import { crypto } from "@noble/hashes/crypto"; +import { crypto as cr } from "@noble/hashes/crypto"; import { concatBytes, equalsBytes } from "./utils"; +const crypto: any = { web: cr }; + function validateOpt(key: Uint8Array, iv: Uint8Array, mode: string) { if (!mode.startsWith("aes-")) { throw new Error(`AES submodule doesn't support mode ${mode}`); diff --git a/src/secp256k1-compat.ts b/src/secp256k1-compat.ts index 269fbda..87a5de8 100644 --- a/src/secp256k1-compat.ts +++ b/src/secp256k1-compat.ts @@ -1,10 +1,13 @@ import { sha256 } from "@noble/hashes/sha256"; -import * as secp from "./secp256k1"; +import { mod } from "@noble/curves/abstract/modular"; +import { secp256k1 } from "./secp256k1"; import { assertBool, assertBytes, hexToBytes, toHex } from "./utils"; // Use `secp256k1` module directly. // This is a legacy compatibility layer for the npm package `secp256k1` via noble-secp256k1 +const Point = secp256k1.ProjectivePoint; + function hexToNumber(hex: string): bigint { if (typeof hex !== "string") { throw new TypeError("hexToNumber: expected string, got " + typeof hex); @@ -17,9 +20,8 @@ const bytesToNumber = (bytes: Uint8Array) => hexToNumber(toHex(bytes)); const numberToHex = (num: number | bigint) => num.toString(16).padStart(64, "0"); const numberToBytes = (num: number | bigint) => hexToBytes(numberToHex(num)); -const { mod } = secp.utils; -const ORDER = secp.CURVE.n; +const ORDER = secp256k1.CURVE.n; type Output = Uint8Array | ((len: number) => Uint8Array); interface Signature { @@ -44,11 +46,11 @@ function output( function getSignature(signature: Uint8Array) { assertBytes(signature, 64); - return secp.Signature.fromCompact(signature); + return secp256k1.Signature.fromCompact(signature); } export function createPrivateKeySync(): Uint8Array { - return secp.utils.randomPrivateKey(); + return secp256k1.utils.randomPrivateKey(); } export async function createPrivateKey(): Promise { @@ -57,7 +59,7 @@ export async function createPrivateKey(): Promise { export function privateKeyVerify(privateKey: Uint8Array): boolean { assertBytes(privateKey, 32); - return secp.utils.isValidPrivateKey(privateKey); + return secp256k1.utils.isValidPrivateKey(privateKey); } export function publicKeyCreate( @@ -67,14 +69,14 @@ export function publicKeyCreate( ): Uint8Array { assertBytes(privateKey, 32); assertBool(compressed); - const res = secp.getPublicKey(privateKey, compressed); + const res = secp256k1.getPublicKey(privateKey, compressed); return output(out, compressed ? 33 : 65, res); } export function publicKeyVerify(publicKey: Uint8Array): boolean { assertBytes(publicKey, 33, 65); try { - secp.Point.fromHex(publicKey); + Point.fromHex(publicKey); return true; } catch (e) { return false; @@ -88,7 +90,7 @@ export function publicKeyConvert( ): Uint8Array { assertBytes(publicKey, 33, 65); assertBool(compressed); - const res = secp.Point.fromHex(publicKey).toRawBytes(compressed); + const res = Point.fromHex(publicKey).toRawBytes(compressed); return output(out, compressed ? 33 : 65, res); } @@ -110,11 +112,9 @@ export function ecdsaSign( ) { throw new Error("Secp256k1: noncefn && data is unsupported"); } - const [signature, recid] = secp.signSync(msgHash, privateKey, { - recovered: true, - der: false, - }); - return { signature: output(out, 64, signature), recid }; + const sig = secp256k1.sign(msgHash, privateKey); + const recid = sig.recovery!; + return { signature: output(out, 64, sig.toCompactRawBytes()), recid }; } export function ecdsaRecover( @@ -126,8 +126,8 @@ export function ecdsaRecover( ) { assertBytes(msgHash, 32); assertBool(compressed); - const sign = getSignature(signature).toHex(); - const point = secp.Point.fromSignature(msgHash, sign, recid); + const sign = getSignature(signature); + const point = sign.addRecoveryBit(recid).recoverPublicKey(msgHash); return output(out, compressed ? 33 : 65, point.toRawBytes(compressed)); } @@ -145,14 +145,15 @@ export function ecdsaVerify( if (r >= ORDER || s >= ORDER) { throw new Error("Cannot parse signature"); } - const pub = secp.Point.fromHex(publicKey); // should not throw error + const pub = Point.fromHex(publicKey); // can throw error + pub; // typescript let sig; try { sig = getSignature(signature); } catch (error) { return false; } - return secp.verify(sig, msgHash, pub); + return secp256k1.verify(sig, msgHash, publicKey); } export function privateKeyTweakAdd( @@ -195,7 +196,7 @@ export function publicKeyNegate( ) { assertBytes(publicKey, 33, 65); assertBool(compressed); - const point = secp.Point.fromHex(publicKey).negate(); + const point = Point.fromHex(publicKey).negate(); return output(out, compressed ? 33 : 65, point.toRawBytes(compressed)); } @@ -214,10 +215,10 @@ export function publicKeyCombine( } assertBool(compressed); const combined = publicKeys - .map((pub) => secp.Point.fromHex(pub)) - .reduce((res, curr) => res.add(curr), secp.Point.ZERO); + .map((pub) => Point.fromHex(pub)) + .reduce((res, curr) => res.add(curr), Point.ZERO); // Prohibit returning ZERO point - if (combined.equals(secp.Point.ZERO)) { + if (combined.equals(Point.ZERO)) { throw new Error("Combined result must not be zero"); } return output(out, compressed ? 33 : 65, combined.toRawBytes(compressed)); @@ -232,10 +233,10 @@ export function publicKeyTweakAdd( assertBytes(publicKey, 33, 65); assertBytes(tweak, 32); assertBool(compressed); - const p1 = secp.Point.fromHex(publicKey); - const p2 = secp.Point.fromPrivateKey(tweak); + const p1 = Point.fromHex(publicKey); + const p2 = Point.fromPrivateKey(tweak); const point = p1.add(p2); - if (p2.equals(secp.Point.ZERO) || point.equals(secp.Point.ZERO)) { + if (p2.equals(Point.ZERO) || point.equals(Point.ZERO)) { throw new Error("Tweak must not be zero"); } return output(out, compressed ? 33 : 65, point.toRawBytes(compressed)); @@ -257,7 +258,7 @@ export function publicKeyTweakMul( if (bn <= 1 || bn >= ORDER) { throw new Error("Tweak is zero or bigger than curve order"); } - const point = secp.Point.fromHex(publicKey).multiply(bn); + const point = Point.fromHex(publicKey).multiply(bn); return output(out, compressed ? 33 : 65, point.toRawBytes(compressed)); } @@ -285,8 +286,8 @@ export function signatureExport( signature: Uint8Array, out?: Output ): Uint8Array { - const res = getSignature(signature).toRawBytes(); - return output(out, 72, getSignature(signature).toRawBytes()).slice( + const res = getSignature(signature).toDERRawBytes(); + return output(out, 72, res.slice()).slice( 0, res.length ); @@ -297,7 +298,7 @@ export function signatureImport( out?: Output ): Uint8Array { assertBytes(signature); - const sig = secp.Signature.fromDER(signature); + const sig = secp256k1.Signature.fromDER(signature); return output(out, 64, hexToBytes(sig.toCompactHex())); } @@ -328,7 +329,7 @@ export function ecdh( if (options.data !== undefined) { assertBytes(options.data); } - const point = secp.Point.fromHex(secp.getSharedSecret(privateKey, publicKey)); + const point = Point.fromHex(secp256k1.getSharedSecret(privateKey, publicKey)); if (options.hashfn === undefined) { return output(out, 32, sha256(point.toRawBytes(true))); } @@ -342,10 +343,11 @@ export function ecdh( assertBytes(options.ybuf, 32); } assertBytes(out as Uint8Array, 32); + const { x, y } = point.toAffine(); const xbuf = options.xbuf || new Uint8Array(32); - xbuf.set(numberToBytes(point.x)); + xbuf.set(numberToBytes(x)); const ybuf = options.ybuf || new Uint8Array(32); - ybuf.set(numberToBytes(point.y)); + ybuf.set(numberToBytes(y)); const hash = options.hashfn(xbuf, ybuf, options.data!); if (!(hash instanceof Uint8Array) || hash.length !== 32) { throw new Error("secp256k1.ecdh: invalid options.hashfn output"); diff --git a/src/secp256k1.ts b/src/secp256k1.ts index 2dde19c..2a0e3a2 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -1,23 +1,2 @@ -import { hmac } from "@noble/hashes/hmac"; -import { sha256 } from "@noble/hashes/sha256"; -import { utils as _utils } from "@noble/secp256k1"; -export { - getPublicKey, - sign, - signSync, - verify, - recoverPublicKey, - getSharedSecret, - utils, - CURVE, - Point, - Signature, - schnorr -} from "@noble/secp256k1"; - -// Enable sync API for noble-secp256k1 -_utils.hmacSha256Sync = (key: Uint8Array, ...messages: Uint8Array[]) => { - const h = hmac.create(sha256, key); - messages.forEach(msg => h.update(msg)); - return h.digest(); -}; +// import { secp256k1 } from "@noble/curves/secp256k1"; +export { secp256k1 } from "@noble/curves/secp256k1"; diff --git a/test/test-vectors/hdkey.ts b/test/test-vectors/hdkey.ts index 8c76915..69f742e 100644 --- a/test/test-vectors/hdkey.ts +++ b/test/test-vectors/hdkey.ts @@ -1,4 +1,4 @@ -import * as secp from "@noble/secp256k1"; +import { secp256k1 as secp } from "@noble/curves/secp256k1"; import { HARDENED_OFFSET, HDKey } from "../../src/hdkey"; import { hexToBytes, toHex } from "../../src/utils"; import { deepStrictEqual, throws } from "./assert"; diff --git a/test/test-vectors/secp256k1.ts b/test/test-vectors/secp256k1.ts index 8a84d94..4b93a24 100644 --- a/test/test-vectors/secp256k1.ts +++ b/test/test-vectors/secp256k1.ts @@ -1,4 +1,4 @@ -import * as secp from "../../src/secp256k1"; +import { secp256k1 } from "../../src/secp256k1"; import { deepStrictEqual } from "./assert"; describe("secp256k1", () => { @@ -9,8 +9,8 @@ describe("secp256k1", () => { const y = 17482644437196207387910659778872952193236850502325156318830589868678978890912n; const r = 432420386565659656852420866390673177323n; const s = 115792089237316195423570985008687907852837564279074904382605163141518161494334n; - const pub = new secp.Point(x, y); - const sig = new secp.Signature(r, s); - deepStrictEqual(secp.verify(sig, msg, pub, { strict: false }), true); + const pub = new secp256k1.ProjectivePoint(x, y, 1n); + const sig = new secp256k1.Signature(r, s); + deepStrictEqual(secp256k1.verify(sig, msg, pub.toRawBytes(), { lowS: false }), true); }); }); From 30a5fdadadf07f737f38d29af38d49dc15ceeda9 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Mon, 10 Apr 2023 19:46:19 +0000 Subject: [PATCH 2/5] Nodejs 14 is no longer supported --- .github/workflows/nodejs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index b5d29a6..5c715c5 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node: [14, 16, 18] + node: [16, 18, 19] os: [ubuntu-latest] steps: - uses: actions/checkout@v3 From aeb433ed10466f0dff08bc50cdfb0e83f24ba07f Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 12 Apr 2023 01:53:42 +0000 Subject: [PATCH 3/5] README --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 90d42cc..bdd59a5 100644 --- a/README.md +++ b/README.md @@ -446,10 +446,13 @@ you found another primitive that is missing. ## Upgrading -Upgrading from 1.0 to 2.0: `secp256k1` module was changed massively: -before, it was using [noble-secp256k1 1.7](https://github.com/paulmillr/noble-secp256k1); -now it uses safer [noble-curves](https://github.com/paulmillr/noble-curves). Please refer -to [upgrading section from curves README](https://github.com/paulmillr/noble-curves#upgrading). +Upgrading from 1.0 to 2.0: + +1. `secp256k1` module was changed massively: + before, it was using [noble-secp256k1 1.7](https://github.com/paulmillr/noble-secp256k1); + now it uses safer [noble-curves](https://github.com/paulmillr/noble-curves). Please refer + to [upgrading section from curves README](https://github.com/paulmillr/noble-curves#upgrading). +2. node.js 14 and older support was dropped. Upgrade to node.js 16 or later. Upgrading from 0.1 to 1.0: **Same functionality**, all old APIs remain the same except for the breaking changes: From e1210bce3150248f6e62144dfd2850b301cb61f5 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 12 Apr 2023 02:52:13 +0000 Subject: [PATCH 4/5] Update deps --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4b5aeb5..72bf66f 100644 --- a/package.json +++ b/package.json @@ -24,9 +24,9 @@ "*.d.ts" ], "dependencies": { - "@noble/curves": "0.9.1", + "@noble/curves": "1.0.0", "@noble/hashes": "1.3.0", - "@scure/bip32": "1.2.0", + "@scure/bip32": "1.3.0", "@scure/bip39": "1.2.0" }, "browser": { From f83552f03a00f9195843d693017357112c3f0fc4 Mon Sep 17 00:00:00 2001 From: Patricio Palladino Date: Thu, 13 Apr 2023 17:24:28 -0300 Subject: [PATCH 5/5] Update src/secp256k1.ts --- src/secp256k1.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/secp256k1.ts b/src/secp256k1.ts index 2a0e3a2..28f4d1f 100644 --- a/src/secp256k1.ts +++ b/src/secp256k1.ts @@ -1,2 +1 @@ -// import { secp256k1 } from "@noble/curves/secp256k1"; export { secp256k1 } from "@noble/curves/secp256k1";