From 9a0fb52885995923dac4041083e3ae5da6789ffe Mon Sep 17 00:00:00 2001 From: Jose-Luis Landabaso Date: Fri, 13 Dec 2024 15:22:41 +0100 Subject: [PATCH] Migrate from @noble/secp256k1 to @noble/curves for enhanced security (#10) - Dependency Migration: Replaced `@noble/secp256k1` with `@noble/curves`, enhancing security and maintainability as recommended by @paulmillr. Updated internal implementations to utilize `@noble/curves` APIs while preserving the same external API for users. - Behavior Update: Updated the `signSchnorr` function to remove default zero-filled auxiliary randomness (`e`) initialization. It now defaults to secure random values if not explicitly provided, improving compliance with best practices. - Documentation Update: Revised the README to: - Reflect the migration to noble-curves. - Document the `signSchnorr` behavior change, highlighting the deviation from `bitcoinjs/tiny-secp256k1` for auxiliary randomness and linking relevant discussions for context. Version Bump: Incremented the version to `1.2.0` to indicate the significant internal changes while maintaining external compatibility. Testing: All existing tests have been updated and pass successfully with the new dependency and refactored implementations. --- README.md | 6 +- index.js | 197 +++++++++++++++++++++++----------------------- package-lock.json | 73 +++++++++-------- package.json | 5 +- 4 files changed, 146 insertions(+), 135 deletions(-) diff --git a/README.md b/README.md index a352af1..e6cb723 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Test this: https://github.com/spsina/bip47 # Secp256k1 -@bitcoinerlab/secp256k1 is a Javascript library for performing elliptic curve operations on the secp256k1 curve. It is designed to integrate into the [BitcoinJS](https://github.com/bitcoinjs) and [BitcoinerLAB](https://bitcoinerlab.com) ecosystems and uses the audited [noble-secp256k1 library](https://github.com/paulmillr/noble-secp256k1), created by [Paul Miller](https://paulmillr.com/noble/). +@bitcoinerlab/secp256k1 is a Javascript library for performing elliptic curve operations on the secp256k1 curve. It is designed to integrate into the [BitcoinJS](https://github.com/bitcoinjs) and [BitcoinerLAB](https://bitcoinerlab.com) ecosystems and uses the audited [noble-curves library](https://github.com/paulmillr/noble-curves), created by [Paul Miller](https://paulmillr.com/noble/). This library is compatible with environments that do not support WebAssembly, such as React Native. @@ -34,7 +34,9 @@ npm install @bitcoinerlab/secp256k1 This implementation follows the tiny-secp256k1 API. Please refer to [tiny-secp256k1](https://github.com/bitcoinjs/tiny-secp256k1#documentation) for documentation on the methods. -This method is not yet implemented: `xOnlyPointAddTweakCheck`. It is not used in ecpair or bip32, though. +- **`xOnlyPointAddTweakCheck`**: This method is not yet implemented. It is not used in `ecpair` or `bip32`. + +- **`signSchnorr`**: Starting from version 1.2.0, this function deviates from the exact behavior mapping with [`bitcoinjs/tiny-secp256k1`](https://github.com/bitcoinjs/tiny-secp256k1) and no longer initializes the auxiliary random data parameter (`e`) to a zero-filled array by default. Instead, it requires the caller to explicitly provide randomness if desired. If omitted, the underlying implementation uses cryptographically secure randomness (through `crypto.getRandomValues`). For more details on this change, see the discussion [here](https://github.com/bitcoinerlab/secp256k1/pull/10#discussion_r1876541974) and the conclusions [here](https://github.com/bitcoinerlab/secp256k1/pull/10#issuecomment-2537916286). ### Examples diff --git a/index.js b/index.js index 9acc63d..fcff14c 100644 --- a/index.js +++ b/index.js @@ -14,38 +14,34 @@ * tiny-secp256k1 (https://github.com/bitcoinjs/tiny-secp256k1/tests). */ -import * as necc from '@noble/secp256k1'; -import { hmac } from '@noble/hashes/hmac'; -import { sha256 } from '@noble/hashes/sha256'; +import { secp256k1, schnorr } from "@noble/curves/secp256k1"; +import * as mod from "@noble/curves/abstract/modular"; +import * as utils from "@noble/curves/abstract/utils"; -const THROW_BAD_PRIVATE = 'Expected Private' -const THROW_BAD_POINT = 'Expected Point' -const THROW_BAD_TWEAK = 'Expected Tweak' -const THROW_BAD_HASH = 'Expected Hash' -const THROW_BAD_SIGNATURE = 'Expected Signature' -const THROW_BAD_EXTRA_DATA = 'Expected Extra Data (32 bytes)' -const THROW_BAD_SCALAR = 'Expected Scalar' -const THROW_BAD_RECOVERY_ID = 'Bad Recovery Id' +const Point = secp256k1.ProjectivePoint; -necc.utils.hmacSha256Sync = (key, ...msgs) => - hmac(sha256, key, necc.utils.concatBytes(...msgs)); -necc.utils.sha256Sync = (...msgs) => sha256(necc.utils.concatBytes(...msgs)); - -const normalizePrivateKey = necc.utils._normalizePrivateKey; +const THROW_BAD_PRIVATE = "Expected Private"; +const THROW_BAD_POINT = "Expected Point"; +const THROW_BAD_TWEAK = "Expected Tweak"; +const THROW_BAD_HASH = "Expected Hash"; +const THROW_BAD_SIGNATURE = "Expected Signature"; +const THROW_BAD_EXTRA_DATA = "Expected Extra Data (32 bytes)"; +const THROW_BAD_SCALAR = "Expected Scalar"; +const THROW_BAD_RECOVERY_ID = "Bad Recovery Id"; const HASH_SIZE = 32; const TWEAK_SIZE = 32; const BN32_N = new Uint8Array([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 254, 186, 174, 220, 230, 175, 72, 160, 59, 191, 210, 94, 140, 208, 54, 65, 65 + 254, 186, 174, 220, 230, 175, 72, 160, 59, 191, 210, 94, 140, 208, 54, 65, 65, ]); const EXTRA_DATA_SIZE = 32; - const BN32_ZERO = new Uint8Array(32); const BN32_P_MINUS_N = new Uint8Array([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 69, 81, 35, 25, 80, 183, 95, 196, 64, 45, 161, 114, 47, 201, 186, 238, ]); +const _1n = BigInt(1); function isUint8Array(value) { return value instanceof Uint8Array; @@ -64,9 +60,7 @@ function isZero(x) { return cmpBN32(x, BN32_ZERO) === 0; } - function isTweak(tweak) { - // Check that the tweak is a Uint8Array of the correct length if ( !(tweak instanceof Uint8Array) || tweak.length !== TWEAK_SIZE || @@ -95,7 +89,9 @@ function isSigrLessThanPMinusN(signature) { } function isSignatureNonzeroRS(signature) { - return !(isZero(signature.subarray(0, 32)) || isZero(signature.subarray(32, 64))) + return !( + isZero(signature.subarray(0, 32)) || isZero(signature.subarray(32, 64)) + ); } function isHash(h) { @@ -108,85 +104,76 @@ function isExtraData(e) { ); } -function hexToNumber(hex) { - if (typeof hex !== 'string') { - throw new TypeError('hexToNumber: expected string, got ' + typeof hex); - } - return BigInt(`0x${hex}`); -} - -function bytesToNumber(bytes) { - return hexToNumber(necc.utils.bytesToHex(bytes)); -} - function normalizeScalar(scalar) { let num; - if (typeof scalar === 'bigint') { + if (typeof scalar === "bigint") { num = scalar; } else if ( - typeof scalar === 'number' && + typeof scalar === "number" && Number.isSafeInteger(scalar) && scalar >= 0 ) { num = BigInt(scalar); - } else if (typeof scalar === 'string') { + } else if (typeof scalar === "string") { if (scalar.length !== 64) - throw new Error('Expected 32 bytes of private scalar'); - num = hexToNumber(scalar); + throw new Error("Expected 32 bytes of private scalar"); + num = utils.hexToNumber(scalar); } else if (scalar instanceof Uint8Array) { if (scalar.length !== 32) - throw new Error('Expected 32 bytes of private scalar'); - num = bytesToNumber(scalar); + throw new Error("Expected 32 bytes of private scalar"); + num = utils.bytesToNumberBE(scalar); } else { - throw new TypeError('Expected valid private scalar'); + throw new TypeError("Expected valid private scalar"); } - if (num < 0) throw new Error('Expected private scalar >= 0'); + if (num < 0) throw new Error("Expected private scalar >= 0"); return num; } -const _privateAdd = (privateKey, tweak) => { +function normalizePrivateKey(privateKey) { + return secp256k1.utils.normPrivateKeyToScalar(privateKey); +} + +function _privateAdd(privateKey, tweak) { const p = normalizePrivateKey(privateKey); const t = normalizeScalar(tweak); - const add = necc.utils._bigintTo32Bytes(necc.utils.mod(p + t, necc.CURVE.n)); - if (necc.utils.isValidPrivateKey(add)) return add; - else return null; -}; + const add = utils.numberToBytesBE(mod.mod(p + t, secp256k1.CURVE.n), 32); + return secp256k1.utils.isValidPrivateKey(add) ? add : null; +} -const _privateSub = (privateKey, tweak) => { +function _privateSub(privateKey, tweak) { const p = normalizePrivateKey(privateKey); const t = normalizeScalar(tweak); - const sub = necc.utils._bigintTo32Bytes(necc.utils.mod(p - t, necc.CURVE.n)); - if (necc.utils.isValidPrivateKey(sub)) return sub; - else return null; -}; + const sub = utils.numberToBytesBE(mod.mod(p - t, secp256k1.CURVE.n), 32); + return secp256k1.utils.isValidPrivateKey(sub) ? sub : null; +} -const _privateNegate = privateKey => { +function _privateNegate(privateKey) { const p = normalizePrivateKey(privateKey); - const not = necc.utils._bigintTo32Bytes(necc.CURVE.n - p); - if (necc.utils.isValidPrivateKey(not)) return not; - else return null; -}; + const not = utils.numberToBytesBE(secp256k1.CURVE.n - p, 32); + return secp256k1.utils.isValidPrivateKey(not) ? not : null; +} -const _pointAddScalar = (p, tweak, isCompressed) => { - const P = necc.Point.fromHex(p); +function _pointAddScalar(p, tweak, isCompressed) { + const P = fromHex(p); const t = normalizeScalar(tweak); - const Q = necc.Point.BASE.multiplyAndAddUnsafe(P, t, BigInt(1)); - if (!Q) throw new Error('Tweaked point at infinity'); + // multiplyAndAddUnsafe(P, scalar, 1) = P + scalar*G + const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n); + if (!Q) throw new Error("Tweaked point at infinity"); return Q.toRawBytes(isCompressed); -}; +} -const _pointMultiply = (p, tweak, isCompressed) => { - const P = necc.Point.fromHex(p); - const h = typeof tweak === 'string' ? tweak : necc.utils.bytesToHex(tweak); - const t = BigInt(`0x${h}`); +function _pointMultiply(p, tweak, isCompressed) { + const P = fromHex(p); + const h = typeof tweak === "string" ? tweak : utils.bytesToHex(tweak); + const t = utils.hexToNumber(h); return P.multiply(t).toRawBytes(isCompressed); -}; +} function assumeCompression(compressed, p) { if (compressed === undefined) { return p !== undefined ? isPointCompressed(p) : true; } - return compressed ? true : false; + return !!compressed; } function throwToNull(fn) { @@ -197,10 +184,19 @@ function throwToNull(fn) { } } +function fromXOnly(bytes) { + return schnorr.utils.lift_x(utils.bytesToNumberBE(bytes)); +} + +function fromHex(bytes) { + return bytes.length === 32 ? fromXOnly(bytes) : Point.fromHex(bytes); +} + function _isPoint(p, xOnly) { if ((p.length === 32) !== xOnly) return false; try { - return !!necc.Point.fromHex(p); + if (xOnly) return !!fromXOnly(p); + else return !!Point.fromHex(p); } catch (e) { return false; } @@ -216,7 +212,7 @@ export function isPointCompressed(p) { } export function isPrivate(d) { - return necc.utils.isValidPrivateKey(d); + return secp256k1.utils.isValidPrivateKey(d); } export function isXOnlyPoint(p) { @@ -249,7 +245,7 @@ export function pointFromScalar(sk, compressed) { throw new Error(THROW_BAD_PRIVATE); } return throwToNull(() => - necc.getPublicKey(sk, assumeCompression(compressed)) + secp256k1.getPublicKey(sk, assumeCompression(compressed)), ); } @@ -264,7 +260,7 @@ export function pointCompress(p, compressed) { if (!isPoint(p)) { throw new Error(THROW_BAD_POINT); } - return necc.Point.fromHex(p).toRawBytes(assumeCompression(compressed, p)); + return fromHex(p).toRawBytes(assumeCompression(compressed, p)); } export function pointMultiply(a, tweak, compressed) { @@ -275,7 +271,7 @@ export function pointMultiply(a, tweak, compressed) { throw new Error(THROW_BAD_TWEAK); } return throwToNull(() => - _pointMultiply(a, tweak, assumeCompression(compressed, a)) + _pointMultiply(a, tweak, assumeCompression(compressed, a)), ); } @@ -284,16 +280,16 @@ export function pointAdd(a, b, compressed) { throw new Error(THROW_BAD_POINT); } return throwToNull(() => { - const A = necc.Point.fromHex(a); - const B = necc.Point.fromHex(b); + const A = fromHex(a); + const B = fromHex(b); if (A.equals(B.negate())) { - //https://github.com/paulmillr/noble-secp256k1/issues/91 return null; } else { return A.add(B).toRawBytes(assumeCompression(compressed, a)); } }); } + export function pointAddScalar(p, tweak, compressed) { if (!isPoint(p)) { throw new Error(THROW_BAD_POINT); @@ -302,32 +298,32 @@ export function pointAddScalar(p, tweak, compressed) { throw new Error(THROW_BAD_TWEAK); } return throwToNull(() => - _pointAddScalar(p, tweak, assumeCompression(compressed, p)) + _pointAddScalar(p, tweak, assumeCompression(compressed, p)), ); } export function privateAdd(d, tweak) { - if (isPrivate(d) === false) { + if (!isPrivate(d)) { throw new Error(THROW_BAD_PRIVATE); } - if (isTweak(tweak) === false) { + if (!isTweak(tweak)) { throw new Error(THROW_BAD_TWEAK); } return throwToNull(() => _privateAdd(d, tweak)); } export function privateSub(d, tweak) { - if (isPrivate(d) === false) { + if (!isPrivate(d)) { throw new Error(THROW_BAD_PRIVATE); } - if (isTweak(tweak) === false) { + if (!isTweak(tweak)) { throw new Error(THROW_BAD_TWEAK); } return throwToNull(() => _privateSub(d, tweak)); } export function privateNegate(d) { - if (isPrivate(d) === false) { + if (!isPrivate(d)) { throw new Error(THROW_BAD_PRIVATE); } return _privateNegate(d); @@ -343,7 +339,7 @@ export function sign(h, d, e) { if (!isExtraData(e)) { throw new Error(THROW_BAD_EXTRA_DATA); } - return necc.signSync(h, d, { der: false, extraEntropy: e }); + return secp256k1.sign(h, d, { extraEntropy: e }).toCompactRawBytes(); } export function signRecoverable(h, d, e) { @@ -356,11 +352,14 @@ export function signRecoverable(h, d, e) { if (!isExtraData(e)) { throw new Error(THROW_BAD_EXTRA_DATA); } - const [signature, recoveryId] = necc.signSync(h, d, { der: false, extraEntropy: e, recovered: true }); - return { signature, recoveryId } + const sig = secp256k1.sign(h, d, { extraEntropy: e }); + return { + signature: sig.toCompactRawBytes(), + recoveryId: sig.recovery, + }; } -export function signSchnorr(h, d, e = Buffer.alloc(32, 0x00)) { +export function signSchnorr(h, d, e) { if (!isPrivate(d)) { throw new Error(THROW_BAD_PRIVATE); } @@ -370,27 +369,31 @@ export function signSchnorr(h, d, e = Buffer.alloc(32, 0x00)) { if (!isExtraData(e)) { throw new Error(THROW_BAD_EXTRA_DATA); } - return necc.schnorr.signSync(h, d, e); + return schnorr.sign(h, d, e); } -export function recover(h, signature, recoveryId, compressed){ - if (!isHash(h)){ +export function recover(h, signature, recoveryId, compressed) { + if (!isHash(h)) { throw new Error(THROW_BAD_HASH); } - if(!isSignature(signature) || !isSignatureNonzeroRS(signature)){ - throw new Error(THROW_BAD_SIGNATURE) + if (!isSignature(signature) || !isSignatureNonzeroRS(signature)) { + throw new Error(THROW_BAD_SIGNATURE); } if (recoveryId & 2) { - if (!isSigrLessThanPMinusN(signature)) throw new Error(THROW_BAD_RECOVERY_ID) + if (!isSigrLessThanPMinusN(signature)) + throw new Error(THROW_BAD_RECOVERY_ID); } - - if (!isXOnlyPoint(signature.subarray(0, 32))){ - throw new Error(THROW_BAD_SIGNATURE) + if (!isXOnlyPoint(signature.subarray(0, 32))) { + throw new Error(THROW_BAD_SIGNATURE); } - return necc.recoverPublicKey(h, signature, recoveryId, assumeCompression(compressed)); + const s = + secp256k1.Signature.fromCompact(signature).addRecoveryBit(recoveryId); + const Q = s.recoverPublicKey(h); + if (!Q) throw new Error(THROW_BAD_SIGNATURE); + return Q.toRawBytes(assumeCompression(compressed)); } export function verify(h, Q, signature, strict) { @@ -403,7 +406,7 @@ export function verify(h, Q, signature, strict) { if (!isHash(h)) { throw new Error(THROW_BAD_SCALAR); } - return necc.verify(signature, h, Q, { strict }); + return secp256k1.verify(signature, h, Q, { lowS: strict }); } export function verifySchnorr(h, Q, signature) { @@ -416,5 +419,5 @@ export function verifySchnorr(h, Q, signature) { if (!isHash(h)) { throw new Error(THROW_BAD_SCALAR); } - return necc.schnorr.verifySync(signature, h, Q); + return schnorr.verify(signature, h, Q); } diff --git a/package-lock.json b/package-lock.json index 2cefb86..1f92aca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "@bitcoinerlab/secp256k1", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bitcoinerlab/secp256k1", - "version": "1.1.1", + "version": "1.2.0", "license": "MIT", "dependencies": { - "@noble/hashes": "^1.1.5", - "@noble/secp256k1": "^1.7.1" + "@noble/curves": "^1.7.0" }, "devDependencies": { "@babel/node": "^7.20.7", @@ -470,27 +469,30 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "node_modules/@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "dependencies": { + "@noble/hashes": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@types/estree": { "version": "1.0.0", @@ -3020,15 +3022,20 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "@noble/hashes": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.5.tgz", - "integrity": "sha512-LTMZiiLc+V4v1Yi16TD6aX2gmtKszNye0pQgbaLqkvhIqP7nVsSaJsWloGQjJfJ8offaoP5GtX3yY5swbcJxxQ==" - }, - "@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==" + "@noble/curves": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", + "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", + "requires": { + "@noble/hashes": "1.6.0" + }, + "dependencies": { + "@noble/hashes": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", + "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==" + } + } }, "@types/estree": { "version": "1.0.0", diff --git a/package.json b/package.json index c2accac..8966c26 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@bitcoinerlab/secp256k1", "homepage": "https://bitcoinerlab.com/secp256k1", - "version": "1.1.1", + "version": "1.2.0", "description": "A library for performing elliptic curve operations on the secp256k1 curve. It is designed to integrate into the BitcoinJS & BitcoinerLAB ecosystems and uses the audited noble-secp256k1 library. It is compatible with environments that do not support WASM, such as React Native.", "main": "dist/index.js", "types": "types/index.d.ts", @@ -41,7 +41,6 @@ "tape": "^5.6.1" }, "dependencies": { - "@noble/hashes": "^1.1.5", - "@noble/secp256k1": "^1.7.1" + "@noble/curves": "^1.7.0" } }